@lodev09/react-native-true-sheet 4.0.0-beta.23 → 4.0.0-beta.25

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 (99) hide show
  1. package/README.md +3 -2
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -0
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +89 -9
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +100 -24
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +13 -10
  7. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -6
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +3 -8
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +14 -1
  10. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +11 -17
  11. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
  12. package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
  13. package/ios/TrueSheetModule.mm +11 -2
  14. package/ios/TrueSheetView.h +2 -0
  15. package/ios/TrueSheetView.mm +62 -7
  16. package/ios/TrueSheetViewController.h +2 -0
  17. package/ios/TrueSheetViewController.mm +158 -4
  18. package/ios/core/RNScreensEventObserver.mm +18 -1
  19. package/ios/core/TrueSheetDimView.h +27 -0
  20. package/ios/core/TrueSheetDimView.mm +41 -0
  21. package/ios/core/TrueSheetKeyboardObserver.mm +11 -8
  22. package/lib/module/TrueSheet.js +88 -17
  23. package/lib/module/TrueSheet.js.map +1 -1
  24. package/lib/module/TrueSheet.web.js +150 -37
  25. package/lib/module/TrueSheet.web.js.map +1 -1
  26. package/lib/module/TrueSheetProvider.web.js +13 -9
  27. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  28. package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  29. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +76 -71
  30. package/lib/module/navigation/createTrueSheetRouter.js +114 -91
  31. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  32. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js +43 -0
  33. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js.map +1 -0
  34. package/lib/module/navigation/expo-router/index.js +9 -4
  35. package/lib/module/navigation/expo-router/index.js.map +1 -1
  36. package/lib/module/scrollable.js +28 -0
  37. package/lib/module/scrollable.js.map +1 -0
  38. package/lib/module/web/keyboard.js +69 -0
  39. package/lib/module/web/keyboard.js.map +1 -0
  40. package/lib/module/web/layout.js +1 -1
  41. package/lib/module/web/layout.js.map +1 -1
  42. package/lib/module/web/vaul/context.js.map +1 -1
  43. package/lib/module/web/vaul/index.js +18 -8
  44. package/lib/module/web/vaul/index.js.map +1 -1
  45. package/lib/module/web/vaul/use-snap-points.js +15 -10
  46. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  47. package/lib/typescript/src/TrueSheet.d.ts +14 -1
  48. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  49. package/lib/typescript/src/TrueSheet.types.d.ts +23 -3
  50. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  51. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  52. package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -1
  53. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  54. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -3
  55. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
  56. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +67 -66
  57. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  58. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +27 -0
  59. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  60. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts +29 -0
  61. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts.map +1 -0
  62. package/lib/typescript/src/navigation/expo-router/index.d.ts +3 -2
  63. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  64. package/lib/typescript/src/navigation/expo-router/types.d.ts +1 -1
  65. package/lib/typescript/src/navigation/types.d.ts +3 -2
  66. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  67. package/lib/typescript/src/scrollable.d.ts +2 -0
  68. package/lib/typescript/src/scrollable.d.ts.map +1 -0
  69. package/lib/typescript/src/web/keyboard.d.ts +15 -0
  70. package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
  71. package/lib/typescript/src/web/layout.d.ts +1 -0
  72. package/lib/typescript/src/web/layout.d.ts.map +1 -1
  73. package/lib/typescript/src/web/vaul/context.d.ts +2 -1
  74. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  75. package/lib/typescript/src/web/vaul/index.d.ts +8 -1
  76. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  77. package/lib/typescript/src/web/vaul/types.d.ts +9 -0
  78. package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
  79. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +3 -2
  80. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  81. package/package.json +5 -4
  82. package/src/TrueSheet.tsx +93 -14
  83. package/src/TrueSheet.types.ts +27 -2
  84. package/src/TrueSheet.web.tsx +170 -41
  85. package/src/TrueSheetProvider.web.tsx +22 -13
  86. package/src/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  87. package/src/fabric/TrueSheetViewNativeComponent.ts +76 -71
  88. package/src/navigation/createTrueSheetRouter.ts +149 -113
  89. package/src/navigation/expo-router/extendTrueSheetRouter.ts +71 -0
  90. package/src/navigation/expo-router/index.ts +14 -6
  91. package/src/navigation/expo-router/types.ts +1 -1
  92. package/src/navigation/types.ts +5 -1
  93. package/src/scrollable.ts +25 -0
  94. package/src/web/keyboard.ts +79 -0
  95. package/src/web/layout.ts +1 -1
  96. package/src/web/vaul/context.ts +2 -1
  97. package/src/web/vaul/index.tsx +20 -5
  98. package/src/web/vaul/types.ts +10 -0
  99. package/src/web/vaul/use-snap-points.ts +19 -9
@@ -10,6 +10,7 @@ import {
10
10
  useMemo,
11
11
  useRef,
12
12
  useState,
13
+ type RefObject,
13
14
  } from 'react';
14
15
  import type { DimensionValue, LayoutChangeEvent } from 'react-native';
15
16
  import { StyleSheet, useColorScheme, useWindowDimensions, View } from 'react-native';
@@ -41,7 +42,12 @@ import {
41
42
  TrueSheetPeekContext,
42
43
  type TrueSheetPeekContextValue,
43
44
  } from './TrueSheetPeek.web';
44
- import { usePortalContainer, useRegisterSheet, useSheetStack } from './TrueSheetProvider.web';
45
+ import {
46
+ usePortalContainer,
47
+ useRegisterSheet,
48
+ useSheetStack,
49
+ type SheetHandle,
50
+ } from './TrueSheetProvider.web';
45
51
  import {
46
52
  COLOR_SURFACE_CONTAINER_LOW_DARK,
47
53
  COLOR_SURFACE_CONTAINER_LOW_LIGHT,
@@ -60,18 +66,16 @@ import {
60
66
  import { getDOMElement } from './web/dom';
61
67
  import { TrueSheetBackground } from './TrueSheetBackground';
62
68
  import { getDetentBackgroundIndex, normalizeDetentBackground } from './detentBackgrounds';
69
+ import { revealFocusedElement, useKeyboardViewport } from './web/keyboard';
63
70
  import { observeSheetLayout } from './web/layout';
71
+ import { resolveScrollable } from './scrollable';
64
72
  import { Drawer } from './web/vaul';
65
73
  import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from './web/vaul/constants';
66
74
 
67
- // Resolves the user's `scrollableRef` to its scrollable DOM element — RN-web
68
- // ScrollView refs expose the scroll node directly (with helpers attached);
69
- // list refs (e.g. FlatList) expose it via getScrollableNode(). The node's
70
- // first child is the content container.
75
+ // Resolves the user's `scrollableRef` to its scrollable DOM element. The
76
+ // node's first child is the content container.
71
77
  const getScrollableElement = (scrollable: unknown): HTMLElement | null => {
72
- if (!scrollable) return null;
73
- if (scrollable instanceof HTMLElement) return scrollable;
74
- const node = (scrollable as { getScrollableNode?: () => unknown }).getScrollableNode?.();
78
+ const node = resolveScrollable(scrollable);
75
79
  return node instanceof HTMLElement ? node : null;
76
80
  };
77
81
 
@@ -99,6 +103,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
99
103
  accessibilityOptions,
100
104
  detents = [0.5, 1],
101
105
  dimmed = true,
106
+ dimColor,
107
+ dimOpacity,
102
108
  dimmedDetentIndex = 0,
103
109
  initialDetentIndex = -1,
104
110
  header,
@@ -164,7 +170,19 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
164
170
  const [isOpen, setIsOpen] = useState(shouldAutoPresent);
165
171
  const isOpenRef = useRef(isOpen);
166
172
  isOpenRef.current = isOpen;
173
+ const keyboardViewport = useKeyboardViewport(isOpen);
167
174
  const isPresentingRef = useRef(shouldAutoPresent);
175
+ // Native's lifecycle state — updated synchronously so calls in the same tick see each other
176
+ const phaseRef = useRef<SheetPhase>(shouldAutoPresent ? 'presenting' : 'dismissed');
177
+ const resolvePresentRef = useRef<(() => void) | null>(null);
178
+ const resolveDismissRef = useRef<(() => void) | null>(null);
179
+ // Whether willPresent fired for the current open cycle — a present undone
180
+ // before it fires has nothing to pair a dismiss with.
181
+ const didEmitWillPresentRef = useRef(false);
182
+ // Whether the current open cycle came from initialDetentIndex. Native mounts those
183
+ // eagerly, so a dismiss before willPresent lands after the present instead of cancelling it.
184
+ const isAutoPresentRef = useRef(shouldAutoPresent);
185
+ const shouldDismissOnPresentRef = useRef(false);
168
186
  const [displayedBackgroundIndex, setDisplayedBackgroundIndex] = useState(
169
187
  shouldAutoPresent ? initialDetentIndex : 0
170
188
  );
@@ -199,13 +217,31 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
199
217
  );
200
218
  }, []);
201
219
 
220
+ // Returns null when there's nothing to dismiss. Resolves on didDismiss.
221
+ const dismissSheet = useCallback((): Promise<void> | null => {
222
+ if (phaseRef.current === 'dismissed' || phaseRef.current === 'dismissing') return null;
223
+ settle(resolvePresentRef);
224
+ if (didEmitWillPresentRef.current) {
225
+ setIsOpen(false);
226
+ } else if (isAutoPresentRef.current) {
227
+ shouldDismissOnPresentRef.current = true;
228
+ } else {
229
+ // Mirror native's lazy-mount cancel: nothing shown yet, nothing to animate out
230
+ setIsOpen(false);
231
+ phaseRef.current = 'dismissed';
232
+ return Promise.resolve();
233
+ }
234
+ phaseRef.current = 'dismissing';
235
+ return new Promise((resolve) => {
236
+ resolveDismissRef.current = resolve;
237
+ });
238
+ }, []);
239
+
202
240
  const handleOpenChange = useCallback(
203
241
  (open: boolean) => {
204
- if (!open && isOpen) {
205
- setIsOpen(false);
206
- }
242
+ if (!open) dismissSheet();
207
243
  },
208
- [isOpen]
244
+ [dismissSheet]
209
245
  );
210
246
 
211
247
  const portalContainer = usePortalContainer();
@@ -248,24 +284,38 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
248
284
  const dismissAboveRef = useRef<(animated?: boolean) => Promise<void>>(async () => {});
249
285
  const dismissDescendantsRef = useRef<() => void>(() => {});
250
286
 
287
+ const presentSheet = useCallback(async (index: number, auto: boolean) => {
288
+ const detent = validDetentsRef.current[index];
289
+ if (detent === undefined) {
290
+ throw new Error(
291
+ `TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`
292
+ );
293
+ }
294
+ if (phaseRef.current !== 'dismissed') {
295
+ console.warn(ALREADY_PRESENTED_WARNING);
296
+ return;
297
+ }
298
+ phaseRef.current = 'presenting';
299
+ isAutoPresentRef.current = auto;
300
+ isPresentingRef.current = true;
301
+ setDisplayedBackgroundIndex(index);
302
+ setActiveSnapPoint(detent);
303
+ setIsOpen(true);
304
+ await new Promise<void>((resolve) => {
305
+ resolvePresentRef.current = resolve;
306
+ });
307
+ }, []);
308
+
251
309
  const methods = useMemo<TrueSheetMethods>(
252
310
  () => ({
253
- present: async (index = 0) => {
254
- const detent = validDetentsRef.current[index];
255
- if (detent === undefined) {
256
- throw new Error(
257
- `TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`
258
- );
259
- }
260
- if (!isOpenRef.current) {
261
- isPresentingRef.current = true;
262
- setDisplayedBackgroundIndex(index);
263
- }
264
- setActiveSnapPoint(detent);
265
- setIsOpen(true);
266
- },
311
+ present: (index = 0) => presentSheet(index, false),
267
312
  dismiss: async () => {
268
- setIsOpen(false);
313
+ const dismissal = dismissSheet();
314
+ if (!dismissal) {
315
+ console.warn(ALREADY_DISMISSED_WARNING);
316
+ return;
317
+ }
318
+ await dismissal;
269
319
  },
270
320
  resize: async (index) => {
271
321
  const detent = validDetentsRef.current[index];
@@ -277,10 +327,14 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
277
327
  setActiveSnapPoint(detent);
278
328
  },
279
329
  dismissStack: async (animated) => {
330
+ if (phaseRef.current === 'dismissed' || phaseRef.current === 'dismissing') {
331
+ console.warn(ALREADY_DISMISSED_WARNING);
332
+ return;
333
+ }
280
334
  await dismissAboveRef.current(animated);
281
335
  },
282
336
  }),
283
- []
337
+ [presentSheet, dismissSheet]
284
338
  );
285
339
 
286
340
  useImperativeHandle(ref, () => methods, [methods]);
@@ -288,13 +342,27 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
288
342
  useEffect(() => {
289
343
  if (!shouldAutoPresent || didInitiallyPresentRef.current) return;
290
344
  didInitiallyPresentRef.current = true;
291
- if (!isOpenRef.current) {
292
- methods.present(initialDetentIndex);
345
+ if (phaseRef.current === 'dismissed') {
346
+ presentSheet(initialDetentIndex, true);
293
347
  }
294
- }, [initialDetentIndex, shouldAutoPresent, methods]);
348
+ }, [initialDetentIndex, shouldAutoPresent, presentSheet]);
349
+
350
+ // Unmounted mid-transition: don't leave callers awaiting forever
351
+ useEffect(
352
+ () => () => {
353
+ settle(resolvePresentRef);
354
+ settle(resolveDismissRef);
355
+ },
356
+ []
357
+ );
295
358
 
296
- const methodsRef = useRef<TrueSheetMethods | null>(methods);
297
- useRegisterSheet(name, methodsRef);
359
+ const handleRef = useRef<SheetHandle>({
360
+ methods,
361
+ dismissQuietly: async () => {
362
+ await dismissSheet();
363
+ },
364
+ });
365
+ useRegisterSheet(name, handleRef);
298
366
 
299
367
  const drawerContentRef = useRef<HTMLDivElement | null>(null);
300
368
  const [drawerContent, setDrawerContent] = useState<HTMLDivElement | null>(null);
@@ -447,6 +515,22 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
447
515
  scrollableRef,
448
516
  ]);
449
517
 
518
+ // Re-reveal the focused field once the sheet settles above the keyboard
519
+ useEffect(() => {
520
+ const drawer = drawerContentRef.current;
521
+ if (!keyboardViewport || !drawer) return undefined;
522
+ let canceled = false;
523
+ // Flush styles so this commit's snap transition shows up in getAnimations()
524
+ // eslint-disable-next-line no-void
525
+ void drawer.offsetHeight;
526
+ Promise.allSettled(drawer.getAnimations().map((a) => a.finished)).then(() => {
527
+ if (!canceled) revealFocusedElement(drawer);
528
+ });
529
+ return () => {
530
+ canceled = true;
531
+ };
532
+ }, [keyboardViewport]);
533
+
450
534
  // Below the last detent a vertical touch pan moves the sheet, not the
451
535
  // content — `[data-vaul-scroll-locked]` disables vertical touch panning on
452
536
  // the scroll container and everything inside it (see vaul/style.css).
@@ -519,6 +603,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
519
603
  effectiveMaxContentHeight,
520
604
  peekHeight,
521
605
  peekContentHeight,
606
+ keyboardViewport,
522
607
  });
523
608
  geometryInputsRef.current = {
524
609
  effectiveDetached,
@@ -526,14 +611,18 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
526
611
  effectiveMaxContentHeight,
527
612
  peekHeight,
528
613
  peekContentHeight,
614
+ keyboardViewport,
529
615
  };
530
616
 
531
617
  const computeDetentGeometry = useCallback(() => {
532
618
  const inputs = geometryInputsRef.current;
533
- const windowH = window.innerHeight;
534
- const effectiveH = inputs.effectiveDetached
535
- ? windowH - inputs.effectiveDetachedOffset
536
- : windowH;
619
+ const windowH = inputs.keyboardViewport?.height ?? window.innerHeight;
620
+ const bottomGap = inputs.effectiveDetached ? inputs.effectiveDetachedOffset : 0;
621
+ const effectiveH = windowH - bottomGap;
622
+ // Above a keyboard, fractions keep their full-height size (see vaul).
623
+ const fractionH = inputs.keyboardViewport
624
+ ? inputs.keyboardViewport.layoutHeight - bottomGap
625
+ : effectiveH;
537
626
  // Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
538
627
  const ceiling =
539
628
  inputs.effectiveMaxContentHeight !== undefined
@@ -569,7 +658,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
569
658
  for (const d of validDetentsRef.current) {
570
659
  const h =
571
660
  typeof d === 'number'
572
- ? Math.min(d * effectiveH, ceiling)
661
+ ? Math.min(d * fractionH, ceiling)
573
662
  : d === 'peek'
574
663
  ? Math.min(livePeekHeight, ceiling)
575
664
  : autoHeight;
@@ -706,6 +795,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
706
795
  // Mirror native: dismissing a sheet takes every sheet stacked above it
707
796
  // (iOS dismisses from the presenter; Android runs dismissStack first).
708
797
  dismissDescendantsRef.current();
798
+ // Mirror native's lazy-mount cancel: dismissed before willPresent, no events
799
+ if (!didEmitWillPresentRef.current) return undefined;
800
+ didEmitWillPresentRef.current = false;
709
801
  // Pair willBlur with willDismiss on dismiss — mirrors native iOS
710
802
  // emitWillDismissEvents (blur fires before dismiss). willPresent is
711
803
  // deferred to `start()` below (needs the mounted drawer's geometry).
@@ -716,11 +808,20 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
716
808
  const fireDone = () => {
717
809
  if (present) {
718
810
  isPresentingRef.current = false;
811
+ // A dismiss may already be underway (it lands on the next commit)
812
+ if (phaseRef.current === 'presenting') phaseRef.current = 'presented';
719
813
  onDidPresentRef.current?.({ nativeEvent: computeDetentInfo() } as DidPresentEvent);
720
814
  onDidFocusRef.current?.({ nativeEvent: null } as DidFocusEvent);
815
+ settle(resolvePresentRef);
816
+ if (shouldDismissOnPresentRef.current) {
817
+ shouldDismissOnPresentRef.current = false;
818
+ setIsOpen(false);
819
+ }
721
820
  } else {
821
+ phaseRef.current = 'dismissed';
722
822
  onDidBlurRef.current?.({ nativeEvent: null } as DidBlurEvent);
723
823
  onDidDismissRef.current?.({ nativeEvent: null } as DidDismissEvent);
824
+ settle(resolveDismissRef);
724
825
  }
725
826
  };
726
827
 
@@ -746,6 +847,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
746
847
  // willPresent mirrors native iOS where viewWillAppear dispatches
747
848
  // both; the descendant-stack focus effect handles subsequent
748
849
  // transitions.
850
+ didEmitWillPresentRef.current = true;
749
851
  onWillPresentRef.current?.({ nativeEvent: computeDetentInfo() } as WillPresentEvent);
750
852
  onWillFocusRef.current?.({ nativeEvent: null } as WillFocusEvent);
751
853
  }
@@ -812,7 +914,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
812
914
  }, [computeDetentInfo]);
813
915
 
814
916
  const { isNested, dismissAbove, descendants } = useSheetStack(
815
- methodsRef,
917
+ handleRef,
816
918
  drawerContentRef,
817
919
  isOpen,
818
920
  isFormSheet
@@ -822,7 +924,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
822
924
  // already been popped from the stack, so `dismissAbove` can't find it.
823
925
  dismissDescendantsRef.current = () => {
824
926
  for (let i = descendants.length - 1; i >= 0; i--) {
825
- descendants[i]!.ref.current?.dismiss();
927
+ descendants[i]!.ref.current?.dismissQuietly();
826
928
  }
827
929
  };
828
930
 
@@ -1057,6 +1159,16 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1057
1159
  transition: isPresentingRef.current ? 'none' : 'background-color 200ms',
1058
1160
  };
1059
1161
 
1162
+ // Vaul fades the overlay through its opacity, so the dim opacity goes into the color
1163
+ const resolvedOverlayStyle = useMemo<React.CSSProperties>(() => {
1164
+ if (dimColor == null && dimOpacity == null) return overlayStyle;
1165
+ const percentage = Math.min(1, Math.max(0, dimOpacity ?? DEFAULT_DIM_OPACITY)) * 100;
1166
+ return {
1167
+ ...overlayStyle,
1168
+ backgroundColor: `color-mix(in srgb, ${String(dimColor ?? 'black')} ${percentage}%, transparent)`,
1169
+ };
1170
+ }, [dimColor, dimOpacity]);
1171
+
1060
1172
  const mergedContentStyle = useMemo<React.CSSProperties>(
1061
1173
  () => ({
1062
1174
  position: 'fixed',
@@ -1225,12 +1337,13 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1225
1337
  initialAnimated={shouldAutoPresent ? initialDetentAnimated : true}
1226
1338
  detachedWrapperStyle={wrapperStyle}
1227
1339
  contentHeight={measuredContentHeight}
1340
+ keyboardViewport={keyboardViewport}
1228
1341
  activeSnapPoint={activeSnapPoint}
1229
1342
  setActiveSnapPoint={handleSetActiveSnapPoint}
1230
1343
  {...snapPointsProps}
1231
1344
  >
1232
1345
  <Drawer.Portal container={portalContainer ?? undefined}>
1233
- <Drawer.Overlay style={overlayStyle} />
1346
+ <Drawer.Overlay style={resolvedOverlayStyle} />
1234
1347
  <Drawer.Content
1235
1348
  ref={handleDrawerRef}
1236
1349
  style={mergedContentStyle}
@@ -1281,6 +1394,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1281
1394
  );
1282
1395
  });
1283
1396
 
1397
+ const DEFAULT_DIM_OPACITY = 0.5;
1398
+
1284
1399
  const overlayStyle: React.CSSProperties = {
1285
1400
  position: 'fixed',
1286
1401
  inset: 0,
@@ -1329,6 +1444,20 @@ const visuallyHiddenStyle: React.CSSProperties = {
1329
1444
  border: 0,
1330
1445
  };
1331
1446
 
1447
+ type SheetPhase = 'dismissed' | 'presenting' | 'presented' | 'dismissing';
1448
+
1449
+ const settle = (resolveRef: RefObject<(() => void) | null>) => {
1450
+ const resolve = resolveRef.current;
1451
+ resolveRef.current = null;
1452
+ resolve?.();
1453
+ };
1454
+
1455
+ // Same as native
1456
+ const ALREADY_PRESENTED_WARNING =
1457
+ 'TrueSheet: sheet is already presented. Use resize() to change detent.';
1458
+ const ALREADY_DISMISSED_WARNING =
1459
+ 'TrueSheet: sheet is already dismissed. No need to dismiss it again.';
1460
+
1332
1461
  const STATIC_METHOD_ERROR =
1333
1462
  'Static methods are not supported on web. Use the useTrueSheet() hook instead.';
1334
1463
 
@@ -13,7 +13,13 @@ import {
13
13
 
14
14
  import type { TrueSheetMethods, TrueSheetStaticMethods } from './TrueSheet.types';
15
15
 
16
- type SheetRef = RefObject<TrueSheetMethods | null>;
16
+ export interface SheetHandle {
17
+ methods: TrueSheetMethods;
18
+ // dismiss() without the already-dismissed warning, for stack-wide dismissals
19
+ dismissQuietly: (animated?: boolean) => Promise<void>;
20
+ }
21
+
22
+ type SheetRef = RefObject<SheetHandle | null>;
17
23
  type NodeRef = RefObject<HTMLDivElement | null>;
18
24
 
19
25
  interface StackEntry {
@@ -23,7 +29,8 @@ interface StackEntry {
23
29
  }
24
30
 
25
31
  interface SheetContextValue {
26
- registerByName: (name: string, ref: SheetRef) => () => void;
32
+ registerSheet: (ref: SheetRef, name?: string) => () => void;
33
+ getSheets: () => ReadonlySet<SheetRef>;
27
34
  resolveByName: (name: string) => TrueSheetMethods;
28
35
  pushOpen: (entry: StackEntry) => void;
29
36
  popOpen: (entry: StackEntry) => void;
@@ -44,6 +51,7 @@ export interface TrueSheetProviderProps {
44
51
  }
45
52
 
46
53
  export function TrueSheetProvider({ children }: TrueSheetProviderProps) {
54
+ const sheetsRef = useRef<Set<SheetRef>>(new Set());
47
55
  const namedSheetsRef = useRef<Map<string, SheetRef>>(new Map());
48
56
  const stackRef = useRef<readonly StackEntry[]>(EMPTY_STACK);
49
57
  const listenersRef = useRef<Set<() => void>>(new Set());
@@ -72,17 +80,20 @@ export function TrueSheetProvider({ children }: TrueSheetProviderProps) {
72
80
  const notify = () => listenersRef.current.forEach((listener) => listener());
73
81
 
74
82
  return {
75
- registerByName: (name, ref) => {
76
- namedSheetsRef.current.set(name, ref);
83
+ registerSheet: (ref, name) => {
84
+ sheetsRef.current.add(ref);
85
+ if (name) namedSheetsRef.current.set(name, ref);
77
86
  return () => {
78
- if (namedSheetsRef.current.get(name) === ref) {
87
+ sheetsRef.current.delete(ref);
88
+ if (name && namedSheetsRef.current.get(name) === ref) {
79
89
  namedSheetsRef.current.delete(name);
80
90
  }
81
91
  };
82
92
  },
93
+ getSheets: () => sheetsRef.current,
83
94
  resolveByName: (name) => {
84
95
  const ref = namedSheetsRef.current.get(name);
85
- const methods = ref?.current;
96
+ const methods = ref?.current?.methods;
86
97
  if (!methods) {
87
98
  throw new Error(`TrueSheet: no sheet registered with name "${name}"`);
88
99
  }
@@ -145,11 +156,9 @@ export function useTrueSheet(): TrueSheetStaticMethods {
145
156
  resize: (name, index) => ctx.resolveByName(name).resize(index),
146
157
  dismiss: (name, animated) => ctx.resolveByName(name).dismiss(animated),
147
158
  dismissStack: (name, animated) => ctx.resolveByName(name).dismissStack(animated),
159
+ // Every sheet, not just the open stack — it misses a present() that hasn't committed yet
148
160
  dismissAll: async (animated) => {
149
- const stack = ctx.getStackSnapshot();
150
- await Promise.all(
151
- [...stack].reverse().map((entry) => entry.ref.current?.dismiss(animated))
152
- );
161
+ await Promise.all([...ctx.getSheets()].map((ref) => ref.current?.dismissQuietly(animated)));
153
162
  },
154
163
  };
155
164
  }, [ctx]);
@@ -158,8 +167,8 @@ export function useTrueSheet(): TrueSheetStaticMethods {
158
167
  export function useRegisterSheet(name: string | undefined, ref: SheetRef): void {
159
168
  const ctx = useContext(SheetContext);
160
169
  useEffect(() => {
161
- if (!ctx || !name) return;
162
- return ctx.registerByName(name, ref);
170
+ if (!ctx) return;
171
+ return ctx.registerSheet(ref, name);
163
172
  }, [ctx, name, ref]);
164
173
  }
165
174
 
@@ -211,7 +220,7 @@ export function useSheetStack(
211
220
  const idx = snapshot.findIndex((e) => e.ref === ref);
212
221
  if (idx < 0) return;
213
222
  const above = snapshot.slice(idx + 1);
214
- await Promise.all([...above].reverse().map((e) => e.ref.current?.dismiss(animated)));
223
+ await Promise.all([...above].reverse().map((e) => e.ref.current?.dismissQuietly(animated)));
215
224
  },
216
225
  [ctx, ref]
217
226
  );
@@ -1,12 +1,11 @@
1
- import type { ViewProps } from 'react-native';
2
- import type { WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
1
+ import type { CodegenTypes, ViewProps } from 'react-native';
3
2
  import { codegenNativeComponent } from 'react-native';
4
3
 
5
4
  export interface NativeProps extends ViewProps {
6
5
  // Seeds the footer's first layout with the sheet's bottom safe-area inset
7
6
  // (insetAdjustment: 'automatic') so a late-set footer doesn't need a second
8
7
  // layout pass — and a visible sheet resize — once native pushes the inset
9
- autoBottomInset?: WithDefault<boolean, false>;
8
+ autoBottomInset?: CodegenTypes.WithDefault<boolean, false>;
10
9
  }
11
10
 
12
11
  // interfaceOnly: shadow node/state/descriptor are custom (common/cpp) so the