react-native-drawer-layout 5.0.0-alpha.2 → 5.0.0-alpha.20

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 (43) hide show
  1. package/lib/module/utils/getDrawerWidth.js +1 -1
  2. package/lib/module/utils/getDrawerWidth.js.map +1 -1
  3. package/lib/module/utils/useDrawerProgress.js +1 -1
  4. package/lib/module/utils/useDrawerProgress.js.map +1 -1
  5. package/lib/module/views/Drawer.js +9 -5
  6. package/lib/module/views/Drawer.js.map +1 -1
  7. package/lib/module/views/Drawer.native.js +162 -57
  8. package/lib/module/views/Drawer.native.js.map +1 -1
  9. package/lib/module/views/GestureHandler.js +4 -14
  10. package/lib/module/views/GestureHandler.js.map +1 -1
  11. package/lib/module/views/GestureHandlerNative.js +1 -1
  12. package/lib/module/views/GestureHandlerNative.js.map +1 -1
  13. package/lib/typescript/src/index.d.ts +3 -3
  14. package/lib/typescript/src/index.d.ts.map +1 -1
  15. package/lib/typescript/src/types.d.ts +4 -4
  16. package/lib/typescript/src/types.d.ts.map +1 -1
  17. package/lib/typescript/src/utils/DrawerGestureContext.d.ts +2 -1
  18. package/lib/typescript/src/utils/DrawerGestureContext.d.ts.map +1 -1
  19. package/lib/typescript/src/utils/getDrawerWidth.d.ts.map +1 -1
  20. package/lib/typescript/src/views/Drawer.d.ts +3 -2
  21. package/lib/typescript/src/views/Drawer.d.ts.map +1 -1
  22. package/lib/typescript/src/views/Drawer.native.d.ts +3 -2
  23. package/lib/typescript/src/views/Drawer.native.d.ts.map +1 -1
  24. package/lib/typescript/src/views/GestureHandler.android.d.ts +1 -1
  25. package/lib/typescript/src/views/GestureHandler.android.d.ts.map +1 -1
  26. package/lib/typescript/src/views/GestureHandler.d.ts +5 -13
  27. package/lib/typescript/src/views/GestureHandler.d.ts.map +1 -1
  28. package/lib/typescript/src/views/GestureHandler.ios.d.ts +1 -1
  29. package/lib/typescript/src/views/GestureHandler.ios.d.ts.map +1 -1
  30. package/lib/typescript/src/views/GestureHandlerNative.d.ts +2 -2
  31. package/lib/typescript/src/views/GestureHandlerNative.d.ts.map +1 -1
  32. package/lib/typescript/src/views/Overlay.d.ts +2 -2
  33. package/lib/typescript/src/views/Overlay.d.ts.map +1 -1
  34. package/lib/typescript/src/views/Overlay.native.d.ts +2 -2
  35. package/lib/typescript/src/views/Overlay.native.d.ts.map +1 -1
  36. package/package.json +17 -15
  37. package/src/types.tsx +6 -4
  38. package/src/utils/getDrawerWidth.tsx +5 -3
  39. package/src/utils/useDrawerProgress.tsx +1 -1
  40. package/src/views/Drawer.native.tsx +208 -59
  41. package/src/views/Drawer.tsx +14 -6
  42. package/src/views/GestureHandler.tsx +13 -15
  43. package/src/views/GestureHandlerNative.tsx +5 -3
@@ -8,16 +8,20 @@ import {
8
8
  StatusBar,
9
9
  StyleSheet,
10
10
  View,
11
+ type ViewProps,
11
12
  } from 'react-native';
12
13
  import Animated, {
14
+ cancelAnimation,
13
15
  interpolate,
14
16
  ReduceMotion,
15
- runOnJS,
17
+ useAnimatedProps,
16
18
  useAnimatedStyle,
17
19
  useDerivedValue,
18
20
  useSharedValue,
21
+ withDelay,
19
22
  withSpring,
20
23
  } from 'react-native-reanimated';
24
+ import { scheduleOnRN } from 'react-native-worklets';
21
25
  import useLatestCallback from 'use-latest-callback';
22
26
 
23
27
  import type { DrawerProps } from '../types';
@@ -25,10 +29,10 @@ import { DrawerGestureContext } from '../utils/DrawerGestureContext';
25
29
  import { DrawerProgressContext } from '../utils/DrawerProgressContext';
26
30
  import { getDrawerWidthNative } from '../utils/getDrawerWidth';
27
31
  import {
28
- Gesture,
29
32
  GestureDetector,
30
33
  GestureHandlerRootView,
31
- GestureState,
34
+ type PanGestureConfig,
35
+ usePanGesture,
32
36
  } from './GestureHandler';
33
37
  import { Overlay } from './Overlay';
34
38
 
@@ -36,6 +40,18 @@ const SWIPE_EDGE_WIDTH = 32;
36
40
  const SWIPE_MIN_OFFSET = 5;
37
41
  const SWIPE_MIN_DISTANCE = 60;
38
42
  const SWIPE_MIN_VELOCITY = 500;
43
+ const PEEK_DELAY = 160;
44
+ const PEEK_EDGE_WIDTH = 20;
45
+ const PEEK_DISTANCE = 20;
46
+ const PROGRESS_EPSILON = 0.05;
47
+
48
+ const SPRING_CONFIG = {
49
+ stiffness: 500,
50
+ damping: 40,
51
+ mass: 1,
52
+ overshootClamping: true,
53
+ reduceMotion: ReduceMotion.Never,
54
+ };
39
55
 
40
56
  const minmax = (value: number, start: number, end: number) => {
41
57
  'worklet';
@@ -104,7 +120,7 @@ export function Drawer({
104
120
  [hideStatusBarOnOpen, statusBarAnimation]
105
121
  );
106
122
 
107
- React.useEffect(() => {
123
+ React.useLayoutEffect(() => {
108
124
  hideStatusBar(isOpen);
109
125
 
110
126
  return () => hideStatusBar(false);
@@ -147,13 +163,17 @@ export function Drawer({
147
163
  getDrawerTranslationX(open, initialWidth)
148
164
  );
149
165
 
150
- const contentRef = React.useRef<View>(null);
166
+ const contentRef = React.useRef<React.ComponentRef<typeof View>>(null);
167
+
168
+ const updateLayout = useLatestCallback((width: number) => {
169
+ layoutWidth.set(width);
170
+ translationX.set(getDrawerTranslationX(open, width));
171
+ });
151
172
 
152
173
  React.useLayoutEffect(() => {
153
174
  const measureLayout = () => {
154
175
  contentRef.current?.measure((_x, _y, width) => {
155
- layoutWidth.set(width);
156
- translationX.set(getDrawerTranslationX(open, width));
176
+ updateLayout(width);
157
177
  });
158
178
  };
159
179
 
@@ -163,6 +183,7 @@ export function Drawer({
163
183
  // Measure the layout again on screen rotation
164
184
  let handle: number | undefined;
165
185
 
186
+ // eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener
166
187
  const subscription = Dimensions.addEventListener('change', () => {
167
188
  // The measurement is not accurate without the delay
168
189
  handle = requestAnimationFrame(() => {
@@ -183,15 +204,26 @@ export function Drawer({
183
204
  // eslint-disable-next-line react-hooks/exhaustive-deps
184
205
  }, []);
185
206
 
186
- const onLayout = (event: LayoutChangeEvent) => {
187
- layoutWidth.set(event.nativeEvent.layout.width);
188
- };
207
+ const onLayout = React.useCallback(
208
+ (event: LayoutChangeEvent) => {
209
+ updateLayout(event.nativeEvent.layout.width);
210
+ },
211
+ [updateLayout]
212
+ );
189
213
 
190
214
  const touchStartX = useSharedValue(0);
191
215
  const touchX = useSharedValue(0);
192
- const gestureState = useSharedValue<GestureState>(GestureState.UNDETERMINED);
216
+ const isGestureActive = useSharedValue(false);
217
+ const isPeeking = useSharedValue(false);
218
+
219
+ const onOpenLatest = useLatestCallback(onOpen);
220
+ const onCloseLatest = useLatestCallback(onClose);
193
221
 
194
222
  const onAnimationStart = useLatestCallback((open: boolean) => {
223
+ if (drawerType !== 'permanent') {
224
+ hideKeyboard();
225
+ }
226
+
195
227
  onTransitionStart?.(!open);
196
228
  });
197
229
 
@@ -213,6 +245,7 @@ export function Drawer({
213
245
 
214
246
  touchStartX.set(0);
215
247
  touchX.set(0);
248
+ isPeeking.set(false);
216
249
 
217
250
  const containerWidth = layoutWidth.get();
218
251
  const toValue = getDrawerTranslationX(open, containerWidth);
@@ -226,87 +259,159 @@ export function Drawer({
226
259
  }
227
260
 
228
261
  if (velocity === undefined) {
229
- runOnJS(onAnimationStart)(open);
262
+ scheduleOnRN(onAnimationStart, open);
230
263
  }
231
264
 
265
+ // Ignore velocity that opposes the spring direction.
266
+ // Otherwise overshootClamping makes the spring snap instantly.
267
+ const isSameDirection =
268
+ velocity !== undefined &&
269
+ ((toValue > translationX.get() && velocity > 0) ||
270
+ (toValue < translationX.get() && velocity < 0));
271
+
272
+ const effectiveVelocity = isSameDirection ? velocity : 0;
273
+
232
274
  animatingTo.set(open ? 'open' : 'close');
233
275
  translationX.set(
234
276
  withSpring(
235
277
  toValue,
236
278
  {
237
- velocity,
238
- stiffness: 1000,
239
- damping: 500,
240
- mass: 3,
279
+ velocity: effectiveVelocity,
280
+ stiffness: 500,
281
+ damping: 40,
282
+ mass: 1,
241
283
  overshootClamping: true,
242
284
  reduceMotion: ReduceMotion.Never,
243
285
  },
244
286
  (finished) => {
245
287
  animatingTo.set(null);
246
- runOnJS(onAnimationEnd)(open, finished);
288
+ scheduleOnRN(onAnimationEnd, open, finished);
247
289
  }
248
290
  )
249
291
  );
250
292
 
251
293
  if (open) {
252
- runOnJS(onOpen)();
294
+ scheduleOnRN(onOpenLatest);
253
295
  } else {
254
- runOnJS(onClose)();
296
+ scheduleOnRN(onCloseLatest);
255
297
  }
256
298
  },
257
299
  [
258
300
  touchStartX,
259
301
  touchX,
302
+ isPeeking,
260
303
  layoutWidth,
261
304
  getDrawerTranslationX,
262
305
  translationX,
263
306
  animatingTo,
264
307
  onAnimationStart,
265
308
  onAnimationEnd,
266
- onOpen,
267
- onClose,
309
+ onOpenLatest,
310
+ onCloseLatest,
268
311
  ]
269
312
  );
270
313
 
271
- React.useEffect(() => {
314
+ React.useLayoutEffect(() => {
272
315
  toggleDrawer(open);
273
316
  }, [animatingTo, open, toggleDrawer]);
274
317
 
275
- const startX = useSharedValue(0);
276
-
277
- const pan = React.useMemo(() => {
278
- let panGesture = Gesture?.Pan()
279
- .onBegin((event) => {
318
+ const panGestureConfig = React.useMemo(() => {
319
+ const config: PanGestureConfig = {
320
+ onBegin: (event) => {
280
321
  'worklet';
281
322
 
282
- startX.set(translationX.get());
283
- gestureState.set(event.state);
284
323
  touchStartX.set(event.x);
285
- })
286
- .onStart(() => {
324
+
325
+ if (open) {
326
+ return;
327
+ }
328
+
329
+ const distanceFromEdge =
330
+ drawerPosition === 'left' ? event.x : layoutWidth.get() - event.x;
331
+
332
+ if (distanceFromEdge > PEEK_EDGE_WIDTH) {
333
+ return;
334
+ }
335
+
336
+ const closedTranslation = getDrawerTranslationX(
337
+ false,
338
+ layoutWidth.get()
339
+ );
340
+
341
+ // Don't start the peek if the drawer isn't fully closed
342
+ if (translationX.get() !== closedTranslation) {
343
+ return;
344
+ }
345
+
346
+ const peekDistance = Math.min(
347
+ PEEK_DISTANCE,
348
+ Math.abs(closedTranslation)
349
+ );
350
+
351
+ if (peekDistance <= 0) {
352
+ return;
353
+ }
354
+
355
+ const peekTranslation =
356
+ closedTranslation +
357
+ peekDistance * (drawerPosition === 'left' ? 1 : -1);
358
+
359
+ isPeeking.set(true);
360
+ translationX.set(
361
+ withDelay(PEEK_DELAY, withSpring(peekTranslation, SPRING_CONFIG))
362
+ );
363
+ },
364
+ onActivate: (event) => {
287
365
  'worklet';
288
366
 
289
- runOnJS(onGestureBegin)();
290
- })
291
- .onChange((event) => {
367
+ const wasPeeking = isPeeking.get();
368
+
369
+ if (wasPeeking) {
370
+ // If a peek is pending or the drawer is peeking,
371
+ // cancel the peek animation
372
+ cancelAnimation(translationX);
373
+ }
374
+
375
+ const currentTranslation = translationX.get();
376
+ const closedTranslation = getDrawerTranslationX(
377
+ false,
378
+ layoutWidth.get()
379
+ );
380
+
381
+ // Start the gesture from the pan's initial translation
382
+ // We avoid this if the drawer has already moved for the peek,
383
+ // otherwise there can be a jump to the pan's initial translation
384
+ if (!wasPeeking || currentTranslation === closedTranslation) {
385
+ translationX.set(currentTranslation + event.translationX);
386
+ }
387
+
388
+ isPeeking.set(false);
389
+ isGestureActive.set(true);
390
+
391
+ scheduleOnRN(onGestureBegin);
392
+ },
393
+ onUpdate: (event) => {
292
394
  'worklet';
293
395
 
396
+ isGestureActive.set(true);
294
397
  touchX.set(event.x);
295
- translationX.set(startX.get() + event.translationX);
296
- gestureState.set(event.state);
297
- })
298
- .onEnd((event, success) => {
398
+ translationX.set(translationX.get() + event.changeX);
399
+ },
400
+ onDeactivate: (event) => {
299
401
  'worklet';
300
402
 
301
- gestureState.set(event.state);
403
+ isGestureActive.set(false);
302
404
 
303
- if (!success) {
304
- runOnJS(onGestureAbort)();
405
+ if (event.canceled) {
406
+ toggleDrawer(open, event.velocityX);
407
+ scheduleOnRN(onGestureAbort);
408
+
409
+ return;
305
410
  }
306
411
 
307
412
  const nextOpen =
308
413
  (Math.abs(event.translationX) > SWIPE_MIN_OFFSET &&
309
- Math.abs(event.translationX) > swipeMinVelocity) ||
414
+ Math.abs(event.velocityX) > swipeMinVelocity) ||
310
415
  Math.abs(event.translationX) > swipeMinDistance
311
416
  ? drawerPosition === 'left'
312
417
  ? // If swiped to right, open the drawer, otherwise close it
@@ -318,29 +423,42 @@ export function Drawer({
318
423
  : open;
319
424
 
320
425
  toggleDrawer(nextOpen, event.velocityX);
321
- runOnJS(onGestureFinish)();
322
- })
323
- .activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
324
- .failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
325
- .hitSlop(hitSlop)
326
- .enabled(drawerType !== 'permanent' && swipeEnabled);
327
-
328
- if (panGesture && configureGestureHandler) {
329
- panGesture = configureGestureHandler(panGesture);
330
- }
426
+ scheduleOnRN(onGestureFinish);
427
+ },
428
+ onFinalize: () => {
429
+ 'worklet';
430
+
431
+ if (isPeeking.get()) {
432
+ isPeeking.set(false);
433
+
434
+ translationX.set(
435
+ withSpring(
436
+ getDrawerTranslationX(false, layoutWidth.get()),
437
+ SPRING_CONFIG
438
+ )
439
+ );
440
+ }
441
+ },
442
+ activeOffsetX: [-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET],
443
+ failOffsetY: [-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET],
444
+ hitSlop,
445
+ enabled: drawerType !== 'permanent' && swipeEnabled,
446
+ };
331
447
 
332
- return panGesture;
448
+ return configureGestureHandler?.(config) ?? config;
333
449
  }, [
334
450
  configureGestureHandler,
335
451
  drawerPosition,
336
452
  drawerType,
337
- gestureState,
453
+ getDrawerTranslationX,
338
454
  hitSlop,
455
+ isGestureActive,
456
+ isPeeking,
457
+ layoutWidth,
339
458
  onGestureBegin,
340
459
  onGestureAbort,
341
460
  onGestureFinish,
342
461
  open,
343
- startX,
344
462
  swipeEnabled,
345
463
  swipeMinDistance,
346
464
  swipeMinVelocity,
@@ -350,6 +468,8 @@ export function Drawer({
350
468
  translationX,
351
469
  ]);
352
470
 
471
+ const pan = usePanGesture(panGestureConfig);
472
+
353
473
  const translateX = useDerivedValue(() => {
354
474
  const drawerWidth = getDrawerWidthNative({
355
475
  containerWidth: layoutWidth.get(),
@@ -382,7 +502,7 @@ export function Drawer({
382
502
  //
383
503
  // This is used only when drawerType is "front"
384
504
  const touchDistance =
385
- drawerType === 'front' && gestureState.get() === GestureState.ACTIVE
505
+ drawerType === 'front' && isGestureActive.get()
386
506
  ? minmax(
387
507
  drawerPosition === 'left'
388
508
  ? touchStartX.get() - drawerWidth
@@ -494,6 +614,34 @@ export function Drawer({
494
614
  );
495
615
  });
496
616
 
617
+ const contentAnimatedProps = useAnimatedProps<ViewProps>(() => {
618
+ const hidden =
619
+ drawerType !== 'permanent' && progress.value >= 1 - PROGRESS_EPSILON;
620
+
621
+ if (Platform.OS === 'android') {
622
+ const importantForAccessibility: ViewProps['importantForAccessibility'] =
623
+ hidden ? 'no-hide-descendants' : 'auto';
624
+
625
+ return { importantForAccessibility };
626
+ }
627
+
628
+ return { accessibilityElementsHidden: hidden };
629
+ }, [drawerType, progress]);
630
+
631
+ const drawerContentAnimatedProps = useAnimatedProps<ViewProps>(() => {
632
+ const hidden =
633
+ drawerType !== 'permanent' && progress.value < 1 - PROGRESS_EPSILON;
634
+
635
+ if (Platform.OS === 'android') {
636
+ const importantForAccessibility: ViewProps['importantForAccessibility'] =
637
+ hidden ? 'no-hide-descendants' : 'auto';
638
+
639
+ return { importantForAccessibility };
640
+ }
641
+
642
+ return { accessibilityElementsHidden: hidden };
643
+ }, [drawerType, progress]);
644
+
497
645
  return (
498
646
  <GestureHandlerRootView style={[styles.container, style]}>
499
647
  <DrawerProgressContext.Provider value={progress}>
@@ -519,12 +667,12 @@ export function Drawer({
519
667
  onLayout={onLayout}
520
668
  style={[styles.content, contentAnimatedStyle]}
521
669
  >
522
- <View
523
- aria-hidden={isOpen && drawerType !== 'permanent'}
670
+ <Animated.View
671
+ animatedProps={contentAnimatedProps}
524
672
  style={styles.content}
525
673
  >
526
674
  {children}
527
- </View>
675
+ </Animated.View>
528
676
  {drawerType !== 'permanent' ? (
529
677
  <Overlay
530
678
  open={open}
@@ -536,6 +684,7 @@ export function Drawer({
536
684
  ) : null}
537
685
  </Animated.View>
538
686
  <Animated.View
687
+ animatedProps={drawerContentAnimatedProps}
539
688
  removeClippedSubviews={Platform.OS !== 'ios'}
540
689
  style={[
541
690
  styles.drawer,
@@ -34,18 +34,26 @@ export function Drawer({
34
34
  progress.set(open ? 1 : 0);
35
35
  }, [open, progress]);
36
36
 
37
- const drawerRef = React.useRef<View>(null);
37
+ const drawerRef = React.useRef<React.ComponentRef<typeof View>>(null);
38
38
 
39
- const onTransitionStartLatest = useLatestCallback(() => {
40
- onTransitionStart?.(open === false);
39
+ const onTransitionStartLatest = useLatestCallback((e: TransitionEvent) => {
40
+ if (e.target === e.currentTarget && e.propertyName === 'transform') {
41
+ onTransitionStart?.(open === false);
42
+ }
41
43
  });
42
44
 
43
- const onTransitionEndLatest = useLatestCallback(() => {
44
- onTransitionEnd?.(open === false);
45
+ const onTransitionEndLatest = useLatestCallback((e: TransitionEvent) => {
46
+ if (e.target === e.currentTarget && e.propertyName === 'transform') {
47
+ onTransitionEnd?.(open === false);
48
+ }
45
49
  });
46
50
 
47
51
  React.useEffect(() => {
48
- const element = drawerRef.current as HTMLDivElement | null;
52
+ const element =
53
+ typeof HTMLElement !== 'undefined' &&
54
+ drawerRef.current instanceof HTMLElement
55
+ ? drawerRef.current
56
+ : null;
49
57
 
50
58
  element?.addEventListener('transitionstart', onTransitionStartLatest);
51
59
  element?.addEventListener('transitionend', onTransitionEndLatest);
@@ -1,12 +1,12 @@
1
1
  import * as React from 'react';
2
2
  import { View } from 'react-native';
3
- import type { GestureType } from 'react-native-gesture-handler';
3
+ import type {
4
+ PanGesture,
5
+ PanGestureConfig,
6
+ } from 'react-native-gesture-handler';
4
7
 
5
- // FIXME: Inline this type instead of getting it from react-native-gesture-handler
6
- // Otherwise, we get a type error:
7
- // Exported variable 'GestureDetector' has or is using name 'GestureDetectorProps' from external module ".." but cannot be named.
8
8
  type GestureDetectorProps = {
9
- gesture: GestureType | undefined;
9
+ gesture: PanGesture | undefined;
10
10
  userSelect?: 'none' | 'auto' | 'text';
11
11
  children: React.ReactNode;
12
12
  };
@@ -21,15 +21,13 @@ export const GestureDetector = ({
21
21
 
22
22
  export const GestureHandlerRootView = View;
23
23
 
24
- export const Gesture:
25
- | typeof import('react-native-gesture-handler').Gesture
26
- | undefined = undefined;
24
+ export function usePanGesture(config: PanGestureConfig) {
25
+ React.useDebugValue(config);
27
26
 
28
- export const enum GestureState {
29
- UNDETERMINED = 0,
30
- FAILED = 1,
31
- BEGAN = 2,
32
- CANCELLED = 3,
33
- ACTIVE = 4,
34
- END = 5,
27
+ return undefined;
35
28
  }
29
+
30
+ export type {
31
+ PanGesture,
32
+ PanGestureConfig,
33
+ } from 'react-native-gesture-handler';
@@ -1,7 +1,9 @@
1
- export type { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
1
+ export type {
2
+ PanGesture,
3
+ PanGestureConfig,
4
+ } from 'react-native-gesture-handler';
2
5
  export {
3
- Gesture,
4
6
  GestureDetector,
5
7
  GestureHandlerRootView,
6
- State as GestureState,
8
+ usePanGesture,
7
9
  } from 'react-native-gesture-handler';