@react-native-ama/bottom-sheet 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,482 @@
1
+
2
+ import { act, render } from '@testing-library/react-native';
3
+ import * as React from 'react';
4
+ import { KeyboardAvoidingView, Text } from 'react-native';
5
+ import * as UseBottomSheetGestureHandler from '../hooks/useBottomSheetGestureHandler';
6
+ import { BottomSheet } from './BottomSheet';
7
+
8
+ const mockOnTimeout = jest.fn();
9
+
10
+ jest.mock('@react-native-ama/core', () => ({
11
+ __esModule: true,
12
+ useTimedAction: jest.fn(() => ({ onTimeout: mockOnTimeout })),
13
+ }));
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+
18
+ jest
19
+ .spyOn(UseBottomSheetGestureHandler, 'useBottomSheetGestureHandler')
20
+ .mockReturnValue({ gestureHandler: jest.fn() as any });
21
+ });
22
+
23
+ jest.useFakeTimers();
24
+
25
+ describe('BottomSheet', () => {
26
+
27
+ it('does not render the content when not visible', () => {
28
+ const { toJSON } = render(
29
+ <BottomSheet
30
+ topInset={0}
31
+ visible={false}
32
+ onClose={() => { }}
33
+ closeActionAccessibilityLabel={'close me'}
34
+ headerComponent={<Text>Header</Text>}
35
+ testID="bottom-sheet"
36
+ />,
37
+ );
38
+
39
+ expect(toJSON()).toMatchInlineSnapshot(`
40
+ <Modal
41
+ animationType="none"
42
+ onRequestClose={[Function]}
43
+ ref={null}
44
+ statusBarTranslucent={true}
45
+ testID="bottom-sheet"
46
+ transparent={true}
47
+ visible={false}
48
+ />
49
+ `);
50
+ });
51
+
52
+ describe('Styling...', () => {
53
+ it('allows customising the overlay style', () => {
54
+ const { getByTestId } = render(
55
+ <BottomSheet
56
+ topInset={0}
57
+ visible={true}
58
+ onClose={() => { }}
59
+ closeActionAccessibilityLabel={'close me'}
60
+ headerComponent={<Text>Header</Text>}
61
+ overlayStyle={{ backgroundColor: 'yellow', width: 42 }}
62
+ testID="bottom-sheet"
63
+ />,
64
+ );
65
+
66
+ const overlayStyle = getByTestId('bottom-sheet-overlay-wrapper').props.style;
67
+ const flatStyle = Array.isArray(overlayStyle)
68
+ ? Object.assign({}, ...overlayStyle.map((s: any) => s || {}))
69
+ : overlayStyle;
70
+
71
+ expect(flatStyle).toMatchObject({
72
+ backgroundColor: 'yellow',
73
+ flex: 1,
74
+ width: 42,
75
+ });
76
+ });
77
+
78
+ it('allows customising the bottom sheet panel', () => {
79
+ const { getByTestId } = render(
80
+ <BottomSheet
81
+ topInset={0}
82
+ visible={true}
83
+ onClose={() => { }}
84
+ closeActionAccessibilityLabel={'close me'}
85
+ headerComponent={<Text>Header</Text>}
86
+ bottomSheetStyle={{ borderRadius: 42 }}
87
+ testID="bottom-sheet"
88
+ />,
89
+ );
90
+
91
+ const wrapperStyle = getByTestId('bottom-sheet-wrapper').props.style;
92
+ const flatStyle = Array.isArray(wrapperStyle)
93
+ ? Object.assign({}, ...wrapperStyle.map((s: any) => s || {}))
94
+ : wrapperStyle;
95
+
96
+ expect(flatStyle).toMatchObject({
97
+ backgroundColor: '#fff',
98
+ borderRadius: 42,
99
+ flex: 1,
100
+ width: '100%',
101
+ });
102
+ });
103
+
104
+ it('allows customising the line style', () => {
105
+ const { getByTestId } = render(
106
+ <BottomSheet
107
+ topInset={0}
108
+ visible={true}
109
+ onClose={() => { }}
110
+ closeActionAccessibilityLabel={'close me'}
111
+ headerComponent={<Text>Header</Text>}
112
+ handleStyle={{ width: '100%' }}
113
+ testID="bottom-sheet"
114
+ />,
115
+ );
116
+
117
+ expect(getByTestId('bottom-sheet-line').props.style).toEqual([
118
+ {
119
+ alignSelf: 'center',
120
+ backgroundColor: 'grey',
121
+ borderRadius: 2,
122
+ height: 4,
123
+ marginBottom: 24,
124
+ marginTop: 12,
125
+ width: 48,
126
+ },
127
+ { width: '100%' },
128
+ ]);
129
+ });
130
+
131
+ it('allows not rendering the line', () => {
132
+ const { getByTestId } = render(
133
+ <BottomSheet
134
+ topInset={0}
135
+ visible={true}
136
+ onClose={() => { }}
137
+ closeActionAccessibilityLabel={'close me'}
138
+ headerComponent={<Text>Header</Text>}
139
+ handleComponent="none"
140
+ testID="bottom-sheet"
141
+ />,
142
+ );
143
+
144
+ expect(() => getByTestId('bottom-sheet-line')).toThrow();
145
+ });
146
+
147
+ it('allows customising the ScrollView style', () => {
148
+ const { getByTestId } = render(
149
+ <BottomSheet
150
+ visible={true}
151
+ onClose={() => { }}
152
+ closeActionAccessibilityLabel={'close me'}
153
+ headerComponent={<Text>Header</Text>}
154
+ scrollViewProps={{
155
+ style: {
156
+ backgroundColor: 'fucsia',
157
+ },
158
+ }}
159
+ topInset={3}
160
+ testID="bottom-sheet"
161
+ />,
162
+ );
163
+
164
+ const scrollStyle = getByTestId('bottom-sheet-scrollview').props.style;
165
+ expect(Array.isArray(scrollStyle)).toBe(true);
166
+ expect(scrollStyle[1]).toEqual({ backgroundColor: 'fucsia' });
167
+ });
168
+
169
+ it('allows rendering a custom header', () => {
170
+ const { getByTestId } = render(
171
+ <BottomSheet
172
+ topInset={0}
173
+ visible={true}
174
+ onClose={() => { }}
175
+ closeActionAccessibilityLabel={'close me'}
176
+ headerComponent={<Text testID="Header">Header</Text>}
177
+ />,
178
+ );
179
+
180
+ expect(getByTestId('Header')).toBeDefined();
181
+ });
182
+ });
183
+
184
+ it('on close it waits for the animation to be completed before hiding the modal', () => {
185
+ const { rerender, getByTestId } = render(
186
+ <BottomSheet
187
+ topInset={0}
188
+ visible={true}
189
+ onClose={() => { }}
190
+ closeActionAccessibilityLabel={'close me'}
191
+ animationDuration={100}
192
+ headerComponent={<Text testID="Header">Header</Text>}
193
+ />,
194
+ );
195
+
196
+ act(() =>
197
+ rerender(
198
+ <BottomSheet
199
+ topInset={0}
200
+ visible={false}
201
+ onClose={() => { }}
202
+ closeActionAccessibilityLabel={'close me'}
203
+ headerComponent={<Text testID="Header">Header</Text>}
204
+ animationDuration={100}
205
+ testID="bottom-sheet"
206
+ />,
207
+ ),
208
+ );
209
+
210
+ expect(getByTestId('bottom-sheet').props.visible).toBe(true);
211
+
212
+ jest.advanceTimersByTime(99);
213
+
214
+ expect(getByTestId('bottom-sheet').props.visible).toBe(true);
215
+
216
+ act(() => {
217
+ jest.advanceTimersByTime(2);
218
+ });
219
+
220
+ expect(getByTestId('bottom-sheet').props.visible).toBe(false);
221
+ });
222
+
223
+ it('uses the animatedStyle for the bottom sheet wrapper', () => {
224
+ const { getByTestId } = render(
225
+ <BottomSheet
226
+ topInset={0}
227
+ visible={true}
228
+ onClose={() => { }}
229
+ closeActionAccessibilityLabel={'close me'}
230
+ animationDuration={100}
231
+ headerComponent={<Text testID="Header">Header</Text>}
232
+ testID="bottom-sheet"
233
+ />,
234
+ );
235
+
236
+ const wrapperStyle = getByTestId('bottom-sheet-wrapper').props.style;
237
+ const flatStyle = Array.isArray(wrapperStyle)
238
+ ? Object.assign({}, ...wrapperStyle.map((s: any) => s || {}))
239
+ : wrapperStyle;
240
+
241
+ expect(flatStyle).toMatchObject({
242
+ alignSelf: 'flex-end',
243
+ bottom: 0,
244
+ flex: 1,
245
+ flexDirection: 'column',
246
+ position: 'absolute',
247
+ width: '100%',
248
+ });
249
+ });
250
+
251
+ // TODO: Fix
252
+ it.skip('passes the layout height into to the useBottomSheetGestureHandler hook', () => {
253
+ const useBottomSheetGestureHandlerSpy = jest
254
+ .spyOn(UseBottomSheetGestureHandler, 'useBottomSheetGestureHandler')
255
+ .mockReturnValue({ gestureHandler: jest.fn() as any });
256
+ const onClose = jest.fn();
257
+
258
+ const { getByTestId } = render(
259
+ <BottomSheet
260
+ topInset={0}
261
+ visible={true}
262
+ closeDistance={0.1}
263
+ onClose={onClose}
264
+ closeActionAccessibilityLabel={'close me'}
265
+ animationDuration={100}
266
+ headerComponent={<Text testID="Header">Header</Text>}
267
+ testID="bottom-sheet"
268
+ />,
269
+ );
270
+
271
+ act(() => {
272
+ getByTestId('bottom-sheet-panel').props.onLayout({
273
+ nativeEvent: { layout: { height: 42 } },
274
+ });
275
+ });
276
+
277
+ expect(useBottomSheetGestureHandlerSpy).toHaveBeenCalledWith(
278
+ expect.objectContaining({
279
+ closeDistance: 0.1,
280
+ contentHeight: { value: 42 },
281
+ onClose: onClose,
282
+ translateY: { value: 0 },
283
+ dragOpacity: { value: 0 },
284
+ minVelocityToClose: 1000,
285
+ overlayOpacity: 1,
286
+ }),
287
+ );
288
+ });
289
+
290
+ it('uses the onTimeout function from the useTimedAction hook to auto-close the bottom sheet', () => {
291
+ render(
292
+ <BottomSheet
293
+ topInset={0}
294
+ visible={true}
295
+ autoCloseDelay={100}
296
+ closeDistance={0.1}
297
+ onClose={() => { }}
298
+ closeActionAccessibilityLabel={'close me'}
299
+ animationDuration={100}
300
+ headerComponent={<Text testID="Header">Header</Text>}
301
+ testID="bottom-sheet"
302
+ />,
303
+ );
304
+
305
+ expect(mockOnTimeout).toHaveBeenCalledWith(expect.any(Function), 100);
306
+ });
307
+
308
+ it('wraps the Modal content inside KeyboardAvoidingView when the avoidKeyboard prop is true and shouldHandleKeyboardEvents = false', () => {
309
+ const { UNSAFE_getByType } = render(
310
+ <BottomSheet
311
+ topInset={0}
312
+ visible={true}
313
+ autoCloseDelay={100}
314
+ closeDistance={0.1}
315
+ onClose={() => { }}
316
+ closeActionAccessibilityLabel={'close me'}
317
+ animationDuration={100}
318
+ headerComponent={<Text testID="Header">Header</Text>}
319
+ testID="bottom-sheet"
320
+ avoidKeyboard={true}
321
+ shouldHandleKeyboardEvents={false}
322
+ />,
323
+ );
324
+
325
+ expect(UNSAFE_getByType(KeyboardAvoidingView)).toBeDefined();
326
+ });
327
+
328
+ it('does not wrap the Modal content inside KeyboardAvoidingView when the avoidKeyboard prop is true and shouldHandleKeyboardEvents = true', () => {
329
+ const { UNSAFE_getByType } = render(
330
+ <BottomSheet
331
+ topInset={0}
332
+ visible={true}
333
+ autoCloseDelay={100}
334
+ closeDistance={0.1}
335
+ onClose={() => { }}
336
+ closeActionAccessibilityLabel={'close me'}
337
+ animationDuration={100}
338
+ headerComponent={<Text testID="Header">Header</Text>}
339
+ testID="bottom-sheet"
340
+ avoidKeyboard={true}
341
+ />,
342
+ );
343
+
344
+ expect(() => UNSAFE_getByType(KeyboardAvoidingView)).toThrow();
345
+ });
346
+
347
+ it.each([undefined, false])(
348
+ 'does not wraps the Modal content inside KeyboardAvoidingView when avoidKeyboard prop is %s',
349
+ avoidKeyboard => {
350
+ const { UNSAFE_getByType } = render(
351
+ <BottomSheet
352
+ topInset={0}
353
+ visible={true}
354
+ autoCloseDelay={100}
355
+ closeDistance={0.1}
356
+ onClose={() => { }}
357
+ closeActionAccessibilityLabel={'close me'}
358
+ animationDuration={100}
359
+ headerComponent={<Text testID="Header">Header</Text>}
360
+ testID="bottom-sheet"
361
+ avoidKeyboard={avoidKeyboard}
362
+ />,
363
+ );
364
+
365
+ expect(() => UNSAFE_getByType(KeyboardAvoidingView)).toThrow();
366
+ },
367
+ );
368
+
369
+ it('calls onClose correctly', () => {
370
+ const onClose = jest.fn();
371
+
372
+ const { getByTestId } = render(
373
+ <BottomSheet
374
+ topInset={0}
375
+ visible={true}
376
+ closeDistance={0.1}
377
+ onClose={onClose}
378
+ closeActionAccessibilityLabel={'close me'}
379
+ animationDuration={100}
380
+ headerComponent={<Text testID="Header">Header</Text>}
381
+ testID="bottom-sheet"
382
+ />,
383
+ );
384
+
385
+ act(() => {
386
+ const event = { some: 'props' };
387
+ getByTestId('bottom-sheet').props.onRequestClose(event);
388
+ });
389
+
390
+ expect(onClose).toHaveBeenCalled();
391
+ });
392
+
393
+ it.todo('calculate correct max height');
394
+ it.todo('calculate correct scrollview height');
395
+ it.todo('handles keyboard correctly');
396
+
397
+ it('renders content without ScrollView when hasScrollableContent is false', () => {
398
+ const { getByTestId, queryByTestId } = render(
399
+ <BottomSheet
400
+ topInset={0}
401
+ visible={true}
402
+ onClose={() => {}}
403
+ closeActionAccessibilityLabel="close me"
404
+ hasScrollableContent={false}
405
+ testID="bottom-sheet"
406
+ >
407
+ <Text testID="child-content">content</Text>
408
+ </BottomSheet>,
409
+ );
410
+
411
+ expect(getByTestId('child-content')).toBeDefined();
412
+ expect(queryByTestId('bottom-sheet-scrollview')).toBeNull();
413
+ });
414
+
415
+ it('exposes isVisible() via ref returning true when visible', () => {
416
+ const ref = React.createRef<any>();
417
+ render(
418
+ <BottomSheet
419
+ ref={ref}
420
+ topInset={0}
421
+ visible={true}
422
+ onClose={() => {}}
423
+ closeActionAccessibilityLabel="close me"
424
+ testID="bottom-sheet"
425
+ />,
426
+ );
427
+
428
+ expect(ref.current?.isVisible()).toBe(true);
429
+ });
430
+
431
+ it('exposes isVisible() via ref returning false when not visible', () => {
432
+ const ref = React.createRef<any>();
433
+ render(
434
+ <BottomSheet
435
+ ref={ref}
436
+ topInset={0}
437
+ visible={false}
438
+ onClose={() => {}}
439
+ closeActionAccessibilityLabel="close me"
440
+ testID="bottom-sheet"
441
+ />,
442
+ );
443
+
444
+ expect(ref.current?.isVisible()).toBe(false);
445
+ });
446
+
447
+ it('calling close() via ref resolves immediately when not visible', async () => {
448
+ const ref = React.createRef<any>();
449
+ render(
450
+ <BottomSheet
451
+ ref={ref}
452
+ topInset={0}
453
+ visible={false}
454
+ onClose={() => {}}
455
+ closeActionAccessibilityLabel="close me"
456
+ testID="bottom-sheet"
457
+ />,
458
+ );
459
+
460
+ await expect(ref.current?.close()).resolves.toBeUndefined();
461
+ });
462
+
463
+ it('triggers onLayout and updates content height', () => {
464
+ const { getByTestId } = render(
465
+ <BottomSheet
466
+ topInset={0}
467
+ visible={true}
468
+ onClose={() => {}}
469
+ closeActionAccessibilityLabel="close me"
470
+ testID="bottom-sheet"
471
+ />,
472
+ );
473
+
474
+ act(() => {
475
+ getByTestId('bottom-sheet-wrapper').props.onLayout({
476
+ nativeEvent: { layout: { height: 500 } },
477
+ });
478
+ });
479
+ });
480
+ });
481
+
482
+ jest.mock('../hooks/useBottomSheetGestureHandler');