react-native-drawer-layout 4.0.0-alpha.6 → 4.0.0-alpha.7

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.
Files changed (54) hide show
  1. package/lib/commonjs/utils/getDrawerWidth.js +42 -0
  2. package/lib/commonjs/utils/getDrawerWidth.js.map +1 -0
  3. package/lib/commonjs/utils/useDrawerProgress.js.map +1 -1
  4. package/lib/commonjs/utils/useFakeSharedValue.js +46 -0
  5. package/lib/commonjs/utils/useFakeSharedValue.js.map +1 -0
  6. package/lib/commonjs/views/Drawer.js +55 -279
  7. package/lib/commonjs/views/Drawer.js.map +1 -1
  8. package/lib/commonjs/views/Drawer.native.js +324 -0
  9. package/lib/commonjs/views/Drawer.native.js.map +1 -0
  10. package/lib/commonjs/views/Overlay.js +22 -39
  11. package/lib/commonjs/views/Overlay.js.map +1 -1
  12. package/lib/commonjs/views/Overlay.native.js +58 -0
  13. package/lib/commonjs/views/Overlay.native.js.map +1 -0
  14. package/lib/module/utils/getDrawerWidth.js +36 -0
  15. package/lib/module/utils/getDrawerWidth.js.map +1 -0
  16. package/lib/module/utils/useDrawerProgress.js.map +1 -1
  17. package/lib/module/utils/useFakeSharedValue.js +38 -0
  18. package/lib/module/utils/useFakeSharedValue.js.map +1 -0
  19. package/lib/module/views/Drawer.js +56 -278
  20. package/lib/module/views/Drawer.js.map +1 -1
  21. package/lib/module/views/Drawer.native.js +315 -0
  22. package/lib/module/views/Drawer.native.js.map +1 -0
  23. package/lib/module/views/Overlay.js +22 -39
  24. package/lib/module/views/Overlay.js.map +1 -1
  25. package/lib/module/views/Overlay.native.js +50 -0
  26. package/lib/module/views/Overlay.native.js.map +1 -0
  27. package/lib/typescript/src/types.d.ts +9 -2
  28. package/lib/typescript/src/types.d.ts.map +1 -1
  29. package/lib/typescript/src/utils/DrawerProgressContext.d.ts +2 -2
  30. package/lib/typescript/src/utils/DrawerProgressContext.d.ts.map +1 -1
  31. package/lib/typescript/src/utils/getDrawerWidth.d.ts +9 -0
  32. package/lib/typescript/src/utils/getDrawerWidth.d.ts.map +1 -0
  33. package/lib/typescript/src/utils/useDrawerProgress.d.ts +2 -2
  34. package/lib/typescript/src/utils/useDrawerProgress.d.ts.map +1 -1
  35. package/lib/typescript/src/utils/useFakeSharedValue.d.ts +17 -0
  36. package/lib/typescript/src/utils/useFakeSharedValue.d.ts.map +1 -0
  37. package/lib/typescript/src/views/Drawer.d.ts +1 -6
  38. package/lib/typescript/src/views/Drawer.d.ts.map +1 -1
  39. package/lib/typescript/src/views/Drawer.native.d.ts +4 -0
  40. package/lib/typescript/src/views/Drawer.native.d.ts.map +1 -0
  41. package/lib/typescript/src/views/Overlay.d.ts +2 -204
  42. package/lib/typescript/src/views/Overlay.d.ts.map +1 -1
  43. package/lib/typescript/src/views/Overlay.native.d.ts +4 -0
  44. package/lib/typescript/src/views/Overlay.native.d.ts.map +1 -0
  45. package/package.json +2 -2
  46. package/src/types.tsx +10 -1
  47. package/src/utils/DrawerProgressContext.tsx +2 -2
  48. package/src/utils/getDrawerWidth.tsx +49 -0
  49. package/src/utils/useDrawerProgress.tsx +2 -2
  50. package/src/utils/useFakeSharedValue.tsx +49 -0
  51. package/src/views/Drawer.native.tsx +466 -0
  52. package/src/views/Drawer.tsx +117 -433
  53. package/src/views/Overlay.native.tsx +63 -0
  54. package/src/views/Overlay.tsx +26 -59
@@ -0,0 +1,466 @@
1
+ import * as React from 'react';
2
+ import {
3
+ I18nManager,
4
+ InteractionManager,
5
+ Keyboard,
6
+ Platform,
7
+ StatusBar,
8
+ StyleSheet,
9
+ useWindowDimensions,
10
+ View,
11
+ } from 'react-native';
12
+ import Animated, {
13
+ interpolate,
14
+ runOnJS,
15
+ useAnimatedGestureHandler,
16
+ useAnimatedStyle,
17
+ useDerivedValue,
18
+ useSharedValue,
19
+ withSpring,
20
+ } from 'react-native-reanimated';
21
+ import useLatestCallback from 'use-latest-callback';
22
+
23
+ import type { DrawerProps } from '../types';
24
+ import { DrawerProgressContext } from '../utils/DrawerProgressContext';
25
+ import { getDrawerWidth } from '../utils/getDrawerWidth';
26
+ import {
27
+ GestureHandlerRootView,
28
+ GestureState,
29
+ PanGestureHandler,
30
+ type PanGestureHandlerGestureEvent,
31
+ } from './GestureHandler';
32
+ import { Overlay } from './Overlay';
33
+
34
+ const SWIPE_EDGE_WIDTH = 32;
35
+ const SWIPE_MIN_OFFSET = 5;
36
+ const SWIPE_MIN_DISTANCE = 60;
37
+ const SWIPE_MIN_VELOCITY = 500;
38
+
39
+ const DRAWER_BORDER_RADIUS = 16;
40
+
41
+ const minmax = (value: number, start: number, end: number) => {
42
+ 'worklet';
43
+
44
+ return Math.min(Math.max(value, start), end);
45
+ };
46
+
47
+ export function Drawer({
48
+ layout: customLayout,
49
+ drawerPosition = I18nManager.getConstants().isRTL ? 'right' : 'left',
50
+ drawerStyle,
51
+ drawerType = Platform.select({ ios: 'slide', default: 'front' }),
52
+ gestureHandlerProps,
53
+ hideStatusBarOnOpen = false,
54
+ keyboardDismissMode = 'on-drag',
55
+ onClose,
56
+ onOpen,
57
+ onGestureStart,
58
+ onGestureCancel,
59
+ onGestureEnd,
60
+ onTransitionStart,
61
+ onTransitionEnd,
62
+ open,
63
+ overlayStyle,
64
+ overlayAccessibilityLabel,
65
+ statusBarAnimation = 'slide',
66
+ swipeEnabled = Platform.OS !== 'web' &&
67
+ Platform.OS !== 'windows' &&
68
+ Platform.OS !== 'macos',
69
+ swipeEdgeWidth = SWIPE_EDGE_WIDTH,
70
+ swipeMinDistance = SWIPE_MIN_DISTANCE,
71
+ swipeMinVelocity = SWIPE_MIN_VELOCITY,
72
+ renderDrawerContent,
73
+ children,
74
+ style,
75
+ }: DrawerProps) {
76
+ const windowDimensions = useWindowDimensions();
77
+
78
+ const layout = customLayout ?? windowDimensions;
79
+ const drawerWidth = getDrawerWidth({ layout, drawerStyle });
80
+
81
+ const isOpen = drawerType === 'permanent' ? true : open;
82
+ const isRight = drawerPosition === 'right';
83
+
84
+ const getDrawerTranslationX = React.useCallback(
85
+ (open: boolean) => {
86
+ 'worklet';
87
+
88
+ if (drawerPosition === 'left') {
89
+ return open ? 0 : -drawerWidth;
90
+ }
91
+
92
+ return open ? 0 : drawerWidth;
93
+ },
94
+ [drawerPosition, drawerWidth]
95
+ );
96
+
97
+ const hideStatusBar = React.useCallback(
98
+ (hide: boolean) => {
99
+ if (hideStatusBarOnOpen) {
100
+ StatusBar.setHidden(hide, statusBarAnimation);
101
+ }
102
+ },
103
+ [hideStatusBarOnOpen, statusBarAnimation]
104
+ );
105
+
106
+ React.useEffect(() => {
107
+ hideStatusBar(isOpen);
108
+
109
+ return () => hideStatusBar(false);
110
+ }, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
111
+
112
+ const interactionHandleRef = React.useRef<number | null>(null);
113
+
114
+ const startInteraction = () => {
115
+ interactionHandleRef.current = InteractionManager.createInteractionHandle();
116
+ };
117
+
118
+ const endInteraction = () => {
119
+ if (interactionHandleRef.current != null) {
120
+ InteractionManager.clearInteractionHandle(interactionHandleRef.current);
121
+ interactionHandleRef.current = null;
122
+ }
123
+ };
124
+
125
+ const hideKeyboard = () => {
126
+ if (keyboardDismissMode === 'on-drag') {
127
+ Keyboard.dismiss();
128
+ }
129
+ };
130
+
131
+ const onGestureBegin = () => {
132
+ onGestureStart?.();
133
+ startInteraction();
134
+ hideKeyboard();
135
+ hideStatusBar(true);
136
+ };
137
+
138
+ const onGestureFinish = () => {
139
+ onGestureEnd?.();
140
+ endInteraction();
141
+ };
142
+
143
+ const onGestureAbort = () => {
144
+ onGestureCancel?.();
145
+ endInteraction();
146
+ };
147
+
148
+ // FIXME: Currently hitSlop is broken when on Android when drawer is on right
149
+ // https://github.com/software-mansion/react-native-gesture-handler/issues/569
150
+ const hitSlop = isRight
151
+ ? // Extend hitSlop to the side of the screen when drawer is closed
152
+ // This lets the user drag the drawer from the side of the screen
153
+ { right: 0, width: isOpen ? undefined : swipeEdgeWidth }
154
+ : { left: 0, width: isOpen ? undefined : swipeEdgeWidth };
155
+
156
+ const borderRadiiStyle =
157
+ drawerType !== 'permanent'
158
+ ? isRight
159
+ ? {
160
+ borderTopLeftRadius: DRAWER_BORDER_RADIUS,
161
+ borderBottomLeftRadius: DRAWER_BORDER_RADIUS,
162
+ }
163
+ : {
164
+ borderTopRightRadius: DRAWER_BORDER_RADIUS,
165
+ borderBottomRightRadius: DRAWER_BORDER_RADIUS,
166
+ }
167
+ : null;
168
+
169
+ const touchStartX = useSharedValue(0);
170
+ const touchX = useSharedValue(0);
171
+ const translationX = useSharedValue(getDrawerTranslationX(open));
172
+ const gestureState = useSharedValue<GestureState>(GestureState.UNDETERMINED);
173
+
174
+ const handleAnimationStart = useLatestCallback((open: boolean) => {
175
+ onTransitionStart?.(!open);
176
+ });
177
+
178
+ const handleAnimationEnd = useLatestCallback(
179
+ (open: boolean, finished?: boolean) => {
180
+ if (!finished) return;
181
+ onTransitionEnd?.(!open);
182
+ }
183
+ );
184
+
185
+ const toggleDrawer = React.useCallback(
186
+ (open: boolean, velocity?: number) => {
187
+ 'worklet';
188
+
189
+ const translateX = getDrawerTranslationX(open);
190
+
191
+ if (velocity === undefined) {
192
+ runOnJS(handleAnimationStart)(open);
193
+ }
194
+
195
+ touchStartX.value = 0;
196
+ touchX.value = 0;
197
+ translationX.value = withSpring(
198
+ translateX,
199
+ {
200
+ velocity,
201
+ stiffness: 1000,
202
+ damping: 500,
203
+ mass: 3,
204
+ overshootClamping: true,
205
+ restDisplacementThreshold: 0.01,
206
+ restSpeedThreshold: 0.01,
207
+ },
208
+ (finished) => runOnJS(handleAnimationEnd)(open, finished)
209
+ );
210
+
211
+ if (open) {
212
+ runOnJS(onOpen)();
213
+ } else {
214
+ runOnJS(onClose)();
215
+ }
216
+ },
217
+ [
218
+ getDrawerTranslationX,
219
+ handleAnimationEnd,
220
+ handleAnimationStart,
221
+ onClose,
222
+ onOpen,
223
+ touchStartX,
224
+ touchX,
225
+ translationX,
226
+ ]
227
+ );
228
+
229
+ React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
230
+
231
+ const onGestureEvent = useAnimatedGestureHandler<
232
+ PanGestureHandlerGestureEvent,
233
+ { startX: number; hasCalledOnStart: boolean }
234
+ >({
235
+ onStart: (event, ctx) => {
236
+ ctx.hasCalledOnStart = false;
237
+ ctx.startX = translationX.value;
238
+ gestureState.value = event.state;
239
+ touchStartX.value = event.x;
240
+ },
241
+ onCancel: () => {
242
+ runOnJS(onGestureAbort)();
243
+ },
244
+ onActive: (event, ctx) => {
245
+ touchX.value = event.x;
246
+ translationX.value = ctx.startX + event.translationX;
247
+ gestureState.value = event.state;
248
+
249
+ // onStart will _always_ be called, even when the activation
250
+ // criteria isn't met yet. This makes sure onGestureBegin is only
251
+ // called when the criteria is really met.
252
+ if (!ctx.hasCalledOnStart) {
253
+ ctx.hasCalledOnStart = true;
254
+ runOnJS(onGestureBegin)();
255
+ }
256
+ },
257
+ onEnd: (event) => {
258
+ gestureState.value = event.state;
259
+
260
+ const nextOpen =
261
+ (Math.abs(event.translationX) > SWIPE_MIN_OFFSET &&
262
+ Math.abs(event.translationX) > swipeMinVelocity) ||
263
+ Math.abs(event.translationX) > swipeMinDistance
264
+ ? drawerPosition === 'left'
265
+ ? // If swiped to right, open the drawer, otherwise close it
266
+ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0
267
+ : // If swiped to left, open the drawer, otherwise close it
268
+ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0
269
+ : open;
270
+
271
+ toggleDrawer(nextOpen, event.velocityX);
272
+ },
273
+ onFinish: () => {
274
+ runOnJS(onGestureFinish)();
275
+ },
276
+ });
277
+
278
+ const translateX = useDerivedValue(() => {
279
+ // Comment stolen from react-native-gesture-handler/DrawerLayout
280
+ //
281
+ // While closing the drawer when user starts gesture outside of its area (in greyed
282
+ // out part of the window), we want the drawer to follow only once finger reaches the
283
+ // edge of the drawer.
284
+ // E.g. on the diagram below drawer is illustrate by X signs and the greyed out area by
285
+ // dots. The touch gesture starts at '*' and moves left, touch path is indicated by
286
+ // an arrow pointing left
287
+ // 1) +---------------+ 2) +---------------+ 3) +---------------+ 4) +---------------+
288
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
289
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
290
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
291
+ // |XXXXXXXX|......| |XXXXXXXX|.<-*..| |XXXXXXXX|<--*..| |XXXXX|<-----*..|
292
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
293
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
294
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
295
+ // +---------------+ +---------------+ +---------------+ +---------------+
296
+ //
297
+ // For the above to work properly we define animated value that will keep start position
298
+ // of the gesture. Then we use that value to calculate how much we need to subtract from
299
+ // the translationX. If the gesture started on the greyed out area we take the distance from the
300
+ // edge of the drawer to the start position. Otherwise we don't subtract at all and the
301
+ // drawer be pulled back as soon as you start the pan.
302
+ //
303
+ // This is used only when drawerType is "front"
304
+ const touchDistance =
305
+ drawerType === 'front' && gestureState.value === GestureState.ACTIVE
306
+ ? minmax(
307
+ drawerPosition === 'left'
308
+ ? touchStartX.value - drawerWidth
309
+ : layout.width - drawerWidth - touchStartX.value,
310
+ 0,
311
+ layout.width
312
+ )
313
+ : 0;
314
+
315
+ const translateX =
316
+ drawerPosition === 'left'
317
+ ? minmax(translationX.value + touchDistance, -drawerWidth, 0)
318
+ : minmax(translationX.value - touchDistance, 0, drawerWidth);
319
+
320
+ return translateX;
321
+ });
322
+
323
+ const drawerAnimatedStyle = useAnimatedStyle(() => {
324
+ const distanceFromEdge = layout.width - drawerWidth;
325
+
326
+ return {
327
+ transform:
328
+ drawerType === 'permanent'
329
+ ? // Reanimated needs the property to be present, but it results in Browser bug
330
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=20574
331
+ []
332
+ : [
333
+ {
334
+ translateX:
335
+ // The drawer stays in place when `drawerType` is `back`
336
+ (drawerType === 'back' ? 0 : translateX.value) +
337
+ (drawerPosition === 'left' ? 0 : distanceFromEdge),
338
+ },
339
+ ],
340
+ };
341
+ });
342
+
343
+ const contentAnimatedStyle = useAnimatedStyle(() => {
344
+ return {
345
+ transform:
346
+ drawerType === 'permanent'
347
+ ? // Reanimated needs the property to be present, but it results in Browser bug
348
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=20574
349
+ []
350
+ : [
351
+ {
352
+ translateX:
353
+ // The screen content stays in place when `drawerType` is `front`
354
+ drawerType === 'front'
355
+ ? 0
356
+ : translateX.value +
357
+ drawerWidth * (drawerPosition === 'left' ? 1 : -1),
358
+ },
359
+ ],
360
+ };
361
+ });
362
+
363
+ const progress = useDerivedValue(() => {
364
+ return drawerType === 'permanent'
365
+ ? 1
366
+ : interpolate(
367
+ translateX.value,
368
+ [getDrawerTranslationX(false), getDrawerTranslationX(true)],
369
+ [0, 1]
370
+ );
371
+ });
372
+
373
+ return (
374
+ <GestureHandlerRootView style={[styles.container, style]}>
375
+ <DrawerProgressContext.Provider value={progress}>
376
+ <PanGestureHandler
377
+ activeOffsetX={[-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]}
378
+ failOffsetY={[-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]}
379
+ hitSlop={hitSlop}
380
+ enabled={drawerType !== 'permanent' && swipeEnabled}
381
+ onGestureEvent={onGestureEvent}
382
+ {...gestureHandlerProps}
383
+ >
384
+ {/* Immediate child of gesture handler needs to be an Animated.View */}
385
+ <Animated.View
386
+ style={[
387
+ styles.main,
388
+ {
389
+ flexDirection:
390
+ drawerType === 'permanent' && !isRight
391
+ ? 'row-reverse'
392
+ : 'row',
393
+ },
394
+ ]}
395
+ >
396
+ <Animated.View style={[styles.content, contentAnimatedStyle]}>
397
+ <View
398
+ accessibilityElementsHidden={
399
+ isOpen && drawerType !== 'permanent'
400
+ }
401
+ importantForAccessibility={
402
+ isOpen && drawerType !== 'permanent'
403
+ ? 'no-hide-descendants'
404
+ : 'auto'
405
+ }
406
+ style={styles.content}
407
+ >
408
+ {children}
409
+ </View>
410
+ {drawerType !== 'permanent' ? (
411
+ <Overlay
412
+ open={open}
413
+ progress={progress}
414
+ onPress={() => toggleDrawer(false)}
415
+ style={overlayStyle}
416
+ accessibilityLabel={overlayAccessibilityLabel}
417
+ />
418
+ ) : null}
419
+ </Animated.View>
420
+ <Animated.View
421
+ removeClippedSubviews={Platform.OS !== 'ios'}
422
+ style={[
423
+ styles.drawer,
424
+ {
425
+ width: drawerWidth,
426
+ position:
427
+ drawerType === 'permanent' ? 'relative' : 'absolute',
428
+ zIndex: drawerType === 'back' ? -1 : 0,
429
+ },
430
+ borderRadiiStyle,
431
+ drawerAnimatedStyle,
432
+ drawerStyle,
433
+ ]}
434
+ >
435
+ {renderDrawerContent()}
436
+ </Animated.View>
437
+ </Animated.View>
438
+ </PanGestureHandler>
439
+ </DrawerProgressContext.Provider>
440
+ </GestureHandlerRootView>
441
+ );
442
+ }
443
+
444
+ const styles = StyleSheet.create({
445
+ container: {
446
+ flex: 1,
447
+ },
448
+ drawer: {
449
+ top: 0,
450
+ bottom: 0,
451
+ maxWidth: '100%',
452
+ backgroundColor: 'white',
453
+ },
454
+ content: {
455
+ flex: 1,
456
+ },
457
+ main: {
458
+ flex: 1,
459
+ ...Platform.select({
460
+ // FIXME: We need to hide `overflowX` on Web so the translated content doesn't show offscreen.
461
+ // But adding `overflowX: 'hidden'` prevents content from collapsing the URL bar.
462
+ web: null,
463
+ default: { overflow: 'hidden' },
464
+ }),
465
+ },
466
+ });