@lodev09/react-native-true-sheet 4.0.0-beta.24 → 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.
- package/README.md +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +89 -9
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +100 -24
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +13 -10
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -6
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +3 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +14 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +11 -17
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
- package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
- package/ios/TrueSheetModule.mm +11 -2
- package/ios/TrueSheetView.h +2 -0
- package/ios/TrueSheetView.mm +53 -7
- package/ios/TrueSheetViewController.h +2 -0
- package/ios/TrueSheetViewController.mm +158 -4
- package/ios/core/RNScreensEventObserver.mm +18 -1
- package/ios/core/TrueSheetDimView.h +27 -0
- package/ios/core/TrueSheetDimView.mm +41 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +11 -8
- package/lib/module/TrueSheet.js +88 -17
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +150 -37
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +13 -9
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +2 -0
- package/lib/module/scrollable.js +28 -0
- package/lib/module/scrollable.js.map +1 -0
- package/lib/module/web/keyboard.js +69 -0
- package/lib/module/web/keyboard.js.map +1 -0
- package/lib/module/web/layout.js +1 -1
- package/lib/module/web/layout.js.map +1 -1
- package/lib/module/web/vaul/context.js.map +1 -1
- package/lib/module/web/vaul/index.js +18 -8
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +15 -10
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +14 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +22 -2
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/scrollable.d.ts +2 -0
- package/lib/typescript/src/scrollable.d.ts.map +1 -0
- package/lib/typescript/src/web/keyboard.d.ts +15 -0
- package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
- package/lib/typescript/src/web/layout.d.ts +1 -0
- package/lib/typescript/src/web/layout.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/context.d.ts +2 -1
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +8 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/types.d.ts +9 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +3 -2
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +91 -14
- package/src/TrueSheet.types.ts +26 -2
- package/src/TrueSheet.web.tsx +170 -41
- package/src/TrueSheetProvider.web.tsx +22 -13
- package/src/fabric/TrueSheetViewNativeComponent.ts +2 -0
- package/src/navigation/types.ts +2 -0
- package/src/scrollable.ts +25 -0
- package/src/web/keyboard.ts +79 -0
- package/src/web/layout.ts +1 -1
- package/src/web/vaul/context.ts +2 -1
- package/src/web/vaul/index.tsx +20 -5
- package/src/web/vaul/types.ts +10 -0
- package/src/web/vaul/use-snap-points.ts +19 -9
package/src/TrueSheet.tsx
CHANGED
|
@@ -39,6 +39,7 @@ import TrueSheetFooterViewNativeComponent from './fabric/TrueSheetFooterViewNati
|
|
|
39
39
|
import TrueSheetModule from './specs/NativeTrueSheetModule';
|
|
40
40
|
import { TrueSheetBackground } from './TrueSheetBackground';
|
|
41
41
|
import { normalizeDetentBackground } from './detentBackgrounds';
|
|
42
|
+
import { resolveScrollable } from './scrollable';
|
|
42
43
|
|
|
43
44
|
import {
|
|
44
45
|
Platform,
|
|
@@ -139,9 +140,19 @@ export class TrueSheet
|
|
|
139
140
|
private static readonly instances: { [name: string]: TrueSheet } = {};
|
|
140
141
|
|
|
141
142
|
/**
|
|
142
|
-
*
|
|
143
|
+
* All mounted sheets, named or not.
|
|
143
144
|
*/
|
|
144
|
-
private
|
|
145
|
+
private static readonly mountedInstances = new Set<TrueSheet>();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolver to be called when mount event is received, or with `false` when the present is cancelled
|
|
149
|
+
*/
|
|
150
|
+
private presentationResolver: ((mounted: boolean) => void) | null = null;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Settles once a scheduled lazy unmount has committed
|
|
154
|
+
*/
|
|
155
|
+
private nativeViewUnmount: Promise<void> | null = null;
|
|
145
156
|
|
|
146
157
|
/**
|
|
147
158
|
* Tracks if a present operation is in progress
|
|
@@ -198,13 +209,17 @@ export class TrueSheet
|
|
|
198
209
|
if (scrollableRef?.current) {
|
|
199
210
|
try {
|
|
200
211
|
scrollableHandle = findNodeHandle(
|
|
201
|
-
scrollableRef.current as Parameters<typeof findNodeHandle>[0]
|
|
212
|
+
resolveScrollable(scrollableRef.current) as Parameters<typeof findNodeHandle>[0]
|
|
202
213
|
);
|
|
203
214
|
} catch (error) {
|
|
204
|
-
//
|
|
205
|
-
//
|
|
215
|
+
// findNodeHandle throws on a stale ref (e.g. a wrapper that never clears
|
|
216
|
+
// it on unmount) or a handle with no usable scroll accessor. Treat it as
|
|
217
|
+
// no scrollable instead of crashing.
|
|
206
218
|
if (__DEV__) {
|
|
207
|
-
console.warn(
|
|
219
|
+
console.warn(
|
|
220
|
+
'TrueSheet: scrollableRef could not be resolved to a native view. It may be stale or expose no scroll node.',
|
|
221
|
+
error
|
|
222
|
+
);
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
225
|
}
|
|
@@ -343,6 +358,7 @@ export class TrueSheet
|
|
|
343
358
|
* @returns Promise that resolves when all sheets are dismissed
|
|
344
359
|
*/
|
|
345
360
|
public static async dismissAll(animated: boolean = true): Promise<void> {
|
|
361
|
+
TrueSheet.mountedInstances.forEach((sheet) => sheet.cancelPendingPresent());
|
|
346
362
|
return TrueSheetModule?.dismissAll(animated);
|
|
347
363
|
}
|
|
348
364
|
|
|
@@ -396,16 +412,17 @@ export class TrueSheet
|
|
|
396
412
|
|
|
397
413
|
// Non-lazy content stays mounted; otherwise clean it up unless another presentation is active.
|
|
398
414
|
if (!this.isPresenting && this.props.lazy !== false) {
|
|
399
|
-
this.
|
|
415
|
+
this.unmountNativeView();
|
|
400
416
|
}
|
|
401
417
|
|
|
402
418
|
this.props.onDidDismiss?.(event);
|
|
403
419
|
}
|
|
404
420
|
|
|
405
421
|
private onMount(event: MountEvent): void {
|
|
406
|
-
// Resolve the mount promise if waiting
|
|
407
|
-
|
|
408
|
-
|
|
422
|
+
// Resolve the mount promise if waiting. While an unmount is pending, the event is from
|
|
423
|
+
// the outgoing view — present() waits for the remount instead.
|
|
424
|
+
if (this.presentationResolver && !this.nativeViewUnmount) {
|
|
425
|
+
this.presentationResolver(true);
|
|
409
426
|
this.presentationResolver = null;
|
|
410
427
|
}
|
|
411
428
|
|
|
@@ -465,7 +482,9 @@ export class TrueSheet
|
|
|
465
482
|
return onBackPress?.() ?? onDismissAttempt != null;
|
|
466
483
|
}
|
|
467
484
|
|
|
468
|
-
|
|
485
|
+
// Rejects when the native view is gone but JS hasn't unmounted yet (e.g. the surface
|
|
486
|
+
// stopped while this back press was queued). Nothing is left to dismiss.
|
|
487
|
+
TrueSheetModule?.handleBackPress(nodeHandle).catch(() => {});
|
|
469
488
|
return this.props.onBackPress?.() ?? true;
|
|
470
489
|
}
|
|
471
490
|
|
|
@@ -482,20 +501,67 @@ export class TrueSheet
|
|
|
482
501
|
);
|
|
483
502
|
}
|
|
484
503
|
|
|
504
|
+
// Still waiting on the lazy mount — like native, a second present is a no-op
|
|
505
|
+
if (this.presentationResolver) {
|
|
506
|
+
console.warn('TrueSheet: sheet is already presented. Use resize() to change detent.');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
485
510
|
this.isPresenting = true;
|
|
486
511
|
|
|
487
512
|
// Lazy load: render native view if not already rendered
|
|
488
|
-
|
|
489
|
-
|
|
513
|
+
const unmount = this.nativeViewUnmount;
|
|
514
|
+
if (unmount || !this.state.shouldRenderNativeView) {
|
|
515
|
+
const mounted = await new Promise<boolean>((resolve) => {
|
|
490
516
|
this.presentationResolver = resolve;
|
|
491
|
-
|
|
517
|
+
|
|
518
|
+
const mount = () => {
|
|
519
|
+
if (this.presentationResolver === resolve) {
|
|
520
|
+
this.setState({ shouldRenderNativeView: true });
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// An unmount from the last dismiss may not have committed yet — state still reads
|
|
525
|
+
// mounted, and presenting now would lose the sheet when it lands. Remount after it.
|
|
526
|
+
if (unmount) {
|
|
527
|
+
unmount.then(mount);
|
|
528
|
+
} else {
|
|
529
|
+
mount();
|
|
530
|
+
}
|
|
492
531
|
});
|
|
532
|
+
|
|
533
|
+
// Dismissed before the native view mounted
|
|
534
|
+
if (!mounted) {
|
|
535
|
+
this.isPresenting = false;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
493
538
|
}
|
|
494
539
|
|
|
495
540
|
await TrueSheetModule?.presentByRef(this.handle, index, animated);
|
|
496
541
|
this.isPresenting = false;
|
|
497
542
|
}
|
|
498
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Cancels a present still waiting on the lazy mount. Returns whether one was pending.
|
|
546
|
+
*/
|
|
547
|
+
private cancelPendingPresent(): boolean {
|
|
548
|
+
if (!this.presentationResolver) return false;
|
|
549
|
+
|
|
550
|
+
this.presentationResolver(false);
|
|
551
|
+
this.presentationResolver = null;
|
|
552
|
+
this.unmountNativeView();
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private unmountNativeView(): void {
|
|
557
|
+
this.nativeViewUnmount = new Promise((resolve) => {
|
|
558
|
+
this.setState({ shouldRenderNativeView: false }, () => {
|
|
559
|
+
this.nativeViewUnmount = null;
|
|
560
|
+
resolve();
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
499
565
|
/**
|
|
500
566
|
* Resize the sheet to a given detent index.
|
|
501
567
|
* @param index - The detent index to resize to
|
|
@@ -509,6 +575,9 @@ export class TrueSheet
|
|
|
509
575
|
* @param animated - Whether to animate the dismissal (default: true)
|
|
510
576
|
*/
|
|
511
577
|
public async dismiss(animated: boolean = true): Promise<void> {
|
|
578
|
+
// Dismissing before the lazy mount finishes cancels the present — nothing was presented
|
|
579
|
+
if (this.cancelPendingPresent()) return;
|
|
580
|
+
|
|
512
581
|
return TrueSheetModule?.dismissByRef(this.handle, animated);
|
|
513
582
|
}
|
|
514
583
|
|
|
@@ -522,6 +591,7 @@ export class TrueSheet
|
|
|
522
591
|
}
|
|
523
592
|
|
|
524
593
|
componentDidMount(): void {
|
|
594
|
+
TrueSheet.mountedInstances.add(this);
|
|
525
595
|
this.registerInstance();
|
|
526
596
|
this.updateScrollableHandle();
|
|
527
597
|
}
|
|
@@ -558,9 +628,12 @@ export class TrueSheet
|
|
|
558
628
|
}
|
|
559
629
|
|
|
560
630
|
componentWillUnmount(): void {
|
|
631
|
+
TrueSheet.mountedInstances.delete(this);
|
|
561
632
|
this.unregisterInstance();
|
|
562
633
|
this.backHandlerSubscription?.remove();
|
|
563
634
|
this.backHandlerSubscription = null;
|
|
635
|
+
// Don't leave a present waiting on the lazy mount hanging
|
|
636
|
+
this.presentationResolver?.(false);
|
|
564
637
|
this.presentationResolver = null;
|
|
565
638
|
}
|
|
566
639
|
|
|
@@ -578,6 +651,8 @@ export class TrueSheet
|
|
|
578
651
|
grabberOptions,
|
|
579
652
|
accessibilityOptions,
|
|
580
653
|
dimmed = true,
|
|
654
|
+
dimColor,
|
|
655
|
+
dimOpacity,
|
|
581
656
|
initialDetentIndex = -1,
|
|
582
657
|
initialDetentAnimated = true,
|
|
583
658
|
dimmedDetentIndex,
|
|
@@ -686,6 +761,8 @@ export class TrueSheet
|
|
|
686
761
|
grabberOptions={this.resolvedGrabberOptions}
|
|
687
762
|
accessibilityOptions={accessibilityOptions}
|
|
688
763
|
dimmed={dimmed}
|
|
764
|
+
dimColor={dimColor}
|
|
765
|
+
dimOpacity={dimOpacity}
|
|
689
766
|
dimmedDetentIndex={dimmedDetentIndex}
|
|
690
767
|
initialDetentIndex={gatedInitialDetentIndex}
|
|
691
768
|
initialDetentAnimated={initialDetentAnimated}
|
package/src/TrueSheet.types.ts
CHANGED
|
@@ -439,6 +439,22 @@ export interface TrueSheetProps extends ViewProps {
|
|
|
439
439
|
*/
|
|
440
440
|
dimmed?: boolean;
|
|
441
441
|
|
|
442
|
+
/**
|
|
443
|
+
* The color of the dim overlay behind the sheet.
|
|
444
|
+
* This is ignored if `dimmed` is set to `false`.
|
|
445
|
+
*
|
|
446
|
+
* @default '#000000'
|
|
447
|
+
*/
|
|
448
|
+
dimColor?: ColorValue;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The maximum opacity of the dim overlay behind the sheet (0 to 1).
|
|
452
|
+
* This is ignored if `dimmed` is set to `false`.
|
|
453
|
+
*
|
|
454
|
+
* @default 0.5
|
|
455
|
+
*/
|
|
456
|
+
dimOpacity?: number;
|
|
457
|
+
|
|
442
458
|
/**
|
|
443
459
|
* Initially present the sheet, after mounting, at a given detent index.
|
|
444
460
|
*
|
|
@@ -677,7 +693,9 @@ export interface TrueSheetProps extends ViewProps {
|
|
|
677
693
|
|
|
678
694
|
/**
|
|
679
695
|
* A ref to the scrollable component (e.g. `ScrollView`, `FlatList`)
|
|
680
|
-
* rendered within the sheet content.
|
|
696
|
+
* rendered within the sheet content. Lists that expose an imperative handle
|
|
697
|
+
* (e.g. `LegendList`, `FlashList`) work too, through their
|
|
698
|
+
* `getScrollableNode()` or `getNativeScrollRef()`.
|
|
681
699
|
* Required for scrollable handling — nested scrolling, keyboard insets,
|
|
682
700
|
* and `auto` detent sizing are wired to this scrollable.
|
|
683
701
|
*
|
|
@@ -690,7 +708,13 @@ export interface TrueSheetProps extends ViewProps {
|
|
|
690
708
|
* </TrueSheet>
|
|
691
709
|
* ```
|
|
692
710
|
*/
|
|
693
|
-
scrollableRef?: RefObject<
|
|
711
|
+
scrollableRef?: RefObject<
|
|
712
|
+
| Component<unknown>
|
|
713
|
+
| HostInstance
|
|
714
|
+
| { getScrollableNode(): unknown }
|
|
715
|
+
| { getNativeScrollRef(): unknown }
|
|
716
|
+
| null
|
|
717
|
+
>;
|
|
694
718
|
|
|
695
719
|
/**
|
|
696
720
|
* Options for scrollable behavior.
|
package/src/TrueSheet.web.tsx
CHANGED
|
@@ -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 {
|
|
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
|
|
68
|
-
//
|
|
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
|
-
|
|
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
|
|
205
|
-
setIsOpen(false);
|
|
206
|
-
}
|
|
242
|
+
if (!open) dismissSheet();
|
|
207
243
|
},
|
|
208
|
-
[
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
292
|
-
|
|
345
|
+
if (phaseRef.current === 'dismissed') {
|
|
346
|
+
presentSheet(initialDetentIndex, true);
|
|
293
347
|
}
|
|
294
|
-
}, [initialDetentIndex, shouldAutoPresent,
|
|
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
|
|
297
|
-
|
|
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
|
|
535
|
-
|
|
536
|
-
|
|
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 *
|
|
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
|
-
|
|
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?.
|
|
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={
|
|
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
|
|