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

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