@lodev09/react-native-true-sheet 4.0.0-beta.3 → 4.0.0-beta.30
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 +28 -5
- package/android/build.gradle +23 -1
- package/android/proguard-rules.pro +3 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +303 -184
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +38 -109
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +127 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +29 -3
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +234 -66
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +491 -186
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +64 -10
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -14
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +135 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +23 -23
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +130 -6
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +14 -17
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +148 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +33 -40
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
- package/android/src/main/jni/TrueSheetSpec.h +1 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +37 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +114 -30
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +14 -3
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +5 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +10 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
- package/ios/TrueSheetContainerView.h +23 -7
- package/ios/TrueSheetContainerView.mm +45 -25
- package/ios/TrueSheetContentView.h +44 -15
- package/ios/TrueSheetContentView.mm +221 -190
- package/ios/TrueSheetFooterView.mm +133 -28
- package/ios/TrueSheetModule.h +2 -7
- package/ios/TrueSheetModule.mm +18 -18
- package/ios/TrueSheetOverlayView.h +42 -0
- package/ios/TrueSheetOverlayView.mm +175 -0
- package/ios/TrueSheetView.h +2 -0
- package/ios/TrueSheetView.mm +268 -52
- package/ios/TrueSheetViewController.h +36 -3
- package/ios/TrueSheetViewController.mm +797 -102
- package/ios/core/RNScreensEventObserver.mm +31 -28
- package/ios/core/TrueSheetBlurView.h +0 -2
- package/ios/core/TrueSheetBlurView.mm +3 -36
- package/ios/core/TrueSheetDetentCalculator.h +18 -1
- package/ios/core/TrueSheetDetentCalculator.mm +44 -9
- package/ios/core/TrueSheetDimView.h +27 -0
- package/ios/core/TrueSheetDimView.mm +41 -0
- package/ios/core/TrueSheetGrabberView.mm +13 -3
- package/ios/core/TrueSheetKeyboardObserver.h +7 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +107 -15
- package/ios/core/TrueSheetOverlayContainerView.h +31 -0
- package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
- package/ios/events/TrueSheetLifecycleEvents.h +2 -0
- package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
- package/ios/tvos/TrueSheetStubs.mm +12 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
- package/lib/module/TrueSheet.js +222 -54
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +364 -275
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetBackground.js +15 -0
- package/lib/module/TrueSheetBackground.js.map +1 -0
- package/lib/module/TrueSheetOverlay.js +30 -0
- package/lib/module/TrueSheetOverlay.js.map +1 -0
- package/lib/module/TrueSheetOverlay.web.js +35 -0
- package/lib/module/TrueSheetOverlay.web.js.map +1 -0
- package/lib/module/TrueSheetPeek.web.js +20 -15
- package/lib/module/TrueSheetPeek.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/detentBackgrounds.js +21 -0
- package/lib/module/detentBackgrounds.js.map +1 -0
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
- package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +89 -73
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/mocks/index.js +18 -2
- package/lib/module/mocks/index.js.map +1 -1
- package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
- package/lib/module/navigation/base-types.js +2 -0
- package/lib/module/navigation/base-types.js.map +1 -0
- package/lib/module/navigation/createTrueSheetRouter.js +158 -100
- package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
- package/lib/module/navigation/expo-router/base-types.js +2 -0
- package/lib/module/navigation/expo-router/base-types.js.map +1 -0
- package/lib/module/navigation/expo-router/extendTrueSheetRouter.js +71 -0
- package/lib/module/navigation/expo-router/extendTrueSheetRouter.js.map +1 -0
- package/lib/module/navigation/expo-router/index.js +18 -7
- package/lib/module/navigation/expo-router/index.js.map +1 -1
- package/lib/module/navigation/expo-router/types.js +4 -0
- package/lib/module/navigation/expo-router/types.js.map +1 -0
- package/lib/module/navigation/index.js.map +1 -1
- package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
- package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
- package/lib/module/scrollable.js +28 -0
- package/lib/module/scrollable.js.map +1 -0
- package/lib/module/web/constants.js +1 -1
- package/lib/module/web/constants.js.map +1 -1
- package/lib/module/web/keyboard.js +69 -0
- package/lib/module/web/keyboard.js.map +1 -0
- package/lib/module/web/layout.js +168 -0
- package/lib/module/web/layout.js.map +1 -0
- package/lib/module/web/vaul/constants.js +9 -0
- package/lib/module/web/vaul/constants.js.map +1 -1
- package/lib/module/web/vaul/context.js +4 -0
- package/lib/module/web/vaul/context.js.map +1 -1
- package/lib/module/web/vaul/helpers.js +1 -0
- package/lib/module/web/vaul/helpers.js.map +1 -1
- package/lib/module/web/vaul/index.js +83 -21
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +57 -28
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +18 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +209 -35
- 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/TrueSheetBackground.d.ts +3 -0
- package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
- package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
- package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
- package/lib/typescript/src/TrueSheetPeek.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/detentBackgrounds.d.ts +10 -0
- package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -3
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +76 -65
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +4 -0
- package/lib/typescript/src/mocks/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
- package/lib/typescript/src/navigation/base-types.d.ts +9 -0
- package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +36 -10
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts +33 -0
- package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts +9 -6
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
- package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/index.d.ts +3 -4
- package/lib/typescript/src/navigation/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +43 -6
- 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/constants.d.ts +1 -1
- package/lib/typescript/src/web/constants.d.ts.map +1 -1
- 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 +20 -0
- package/lib/typescript/src/web/layout.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts +3 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/context.d.ts +5 -1
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +20 -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 +6 -4
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +18 -8
- package/react-native.config.js +1 -0
- package/src/TrueSheet.tsx +257 -62
- package/src/TrueSheet.types.ts +225 -35
- package/src/TrueSheet.web.tsx +445 -333
- package/src/TrueSheetBackground.tsx +17 -0
- package/src/TrueSheetOverlay.tsx +24 -0
- package/src/TrueSheetOverlay.web.tsx +33 -0
- package/src/TrueSheetPeek.web.tsx +25 -23
- package/src/TrueSheetProvider.web.tsx +22 -13
- package/src/detentBackgrounds.ts +24 -0
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
- package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +89 -73
- package/src/index.ts +1 -0
- package/src/mocks/index.ts +17 -2
- package/src/mocks/navigationExpoRouter.ts +6 -5
- package/src/navigation/base-types.ts +20 -0
- package/src/navigation/createTrueSheetRouter.ts +220 -144
- package/src/navigation/expo-router/base-types.ts +18 -0
- package/src/navigation/expo-router/extendTrueSheetRouter.ts +99 -0
- package/src/navigation/expo-router/index.ts +41 -30
- package/src/navigation/expo-router/types.ts +116 -0
- package/src/navigation/index.ts +6 -7
- package/src/navigation/screen/useSheetScreenState.ts +2 -0
- package/src/navigation/types.ts +81 -23
- package/src/scrollable.ts +25 -0
- package/src/web/constants.ts +1 -1
- package/src/web/keyboard.ts +79 -0
- package/src/web/layout.ts +196 -0
- package/src/web/vaul/constants.ts +9 -0
- package/src/web/vaul/context.ts +7 -1
- package/src/web/vaul/helpers.ts +5 -0
- package/src/web/vaul/index.tsx +113 -16
- package/src/web/vaul/types.ts +10 -0
- package/src/web/vaul/use-snap-points.ts +86 -33
package/src/TrueSheet.web.tsx
CHANGED
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
useCallback,
|
|
7
7
|
useEffect,
|
|
8
8
|
useImperativeHandle,
|
|
9
|
+
useLayoutEffect,
|
|
9
10
|
useMemo,
|
|
10
11
|
useRef,
|
|
11
12
|
useState,
|
|
13
|
+
type RefObject,
|
|
12
14
|
} from 'react';
|
|
13
15
|
import type { DimensionValue, LayoutChangeEvent } from 'react-native';
|
|
14
16
|
import { StyleSheet, useColorScheme, useWindowDimensions, View } from 'react-native';
|
|
@@ -31,15 +33,25 @@ import type {
|
|
|
31
33
|
TrueSheetStaticMethods,
|
|
32
34
|
WillBlurEvent,
|
|
33
35
|
WillDismissEvent,
|
|
36
|
+
DismissAttemptEvent,
|
|
34
37
|
WillFocusEvent,
|
|
35
38
|
WillPresentEvent,
|
|
36
39
|
} from './TrueSheet.types';
|
|
37
|
-
import {
|
|
38
|
-
|
|
40
|
+
import {
|
|
41
|
+
measurePeekContentHeight,
|
|
42
|
+
TrueSheetPeekContext,
|
|
43
|
+
type TrueSheetPeekContextValue,
|
|
44
|
+
} from './TrueSheetPeek.web';
|
|
45
|
+
import {
|
|
46
|
+
usePortalContainer,
|
|
47
|
+
useRegisterSheet,
|
|
48
|
+
useSheetStack,
|
|
49
|
+
type SheetHandle,
|
|
50
|
+
} from './TrueSheetProvider.web';
|
|
39
51
|
import {
|
|
40
52
|
COLOR_SURFACE_CONTAINER_LOW_DARK,
|
|
41
53
|
COLOR_SURFACE_CONTAINER_LOW_LIGHT,
|
|
42
|
-
|
|
54
|
+
DEFAULT_PLACEMENT_OFFSET,
|
|
43
55
|
DEFAULT_CORNER_RADIUS,
|
|
44
56
|
DEFAULT_DETACHED_OFFSET,
|
|
45
57
|
DEFAULT_FORM_SHEET_HEIGHT_RATIO,
|
|
@@ -52,18 +64,19 @@ import {
|
|
|
52
64
|
DEFAULT_MAX_WIDTH,
|
|
53
65
|
} from './web/constants';
|
|
54
66
|
import { getDOMElement } from './web/dom';
|
|
67
|
+
import { TrueSheetBackground } from './TrueSheetBackground';
|
|
68
|
+
import { getDetentBackgroundIndex, normalizeDetentBackground } from './detentBackgrounds';
|
|
69
|
+
import { revealFocusedElement, useKeyboardViewport } from './web/keyboard';
|
|
70
|
+
import { observeSheetLayout } from './web/layout';
|
|
71
|
+
import { resolveScrollable } from './scrollable';
|
|
55
72
|
import { Drawer } from './web/vaul';
|
|
56
|
-
import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from './web/vaul/constants';
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const { overflowY } = window.getComputedStyle(el);
|
|
64
|
-
if (overflowY === 'auto' || overflowY === 'scroll') return el;
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
73
|
+
import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from './web/vaul/constants';
|
|
74
|
+
|
|
75
|
+
// Resolves the user's `scrollableRef` to its scrollable DOM element. The
|
|
76
|
+
// node's first child is the content container.
|
|
77
|
+
const getScrollableElement = (scrollable: unknown): HTMLElement | null => {
|
|
78
|
+
const node = resolveScrollable(scrollable);
|
|
79
|
+
return node instanceof HTMLElement ? node : null;
|
|
67
80
|
};
|
|
68
81
|
|
|
69
82
|
const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props, ref) => {
|
|
@@ -72,18 +85,26 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
72
85
|
name,
|
|
73
86
|
dismissible = true,
|
|
74
87
|
draggable = true,
|
|
88
|
+
dismissThreshold = 'half',
|
|
75
89
|
cornerRadius,
|
|
76
90
|
style,
|
|
77
91
|
backgroundColor: backgroundColorProp,
|
|
92
|
+
detentBackgrounds,
|
|
93
|
+
background,
|
|
94
|
+
backgroundStyle,
|
|
78
95
|
maxContentHeight,
|
|
79
96
|
maxContentWidth,
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
placement = 'automatic',
|
|
98
|
+
placementOffset = DEFAULT_PLACEMENT_OFFSET,
|
|
99
|
+
scrollableRef,
|
|
100
|
+
scrollableOptions,
|
|
82
101
|
grabber = true,
|
|
83
102
|
grabberOptions,
|
|
84
103
|
accessibilityOptions,
|
|
85
104
|
detents = [0.5, 1],
|
|
86
105
|
dimmed = true,
|
|
106
|
+
dimColor,
|
|
107
|
+
dimOpacity,
|
|
87
108
|
dimmedDetentIndex = 0,
|
|
88
109
|
initialDetentIndex = -1,
|
|
89
110
|
header,
|
|
@@ -103,6 +124,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
103
124
|
onDidPresent,
|
|
104
125
|
onWillDismiss,
|
|
105
126
|
onDidDismiss,
|
|
127
|
+
onDismissAttempt,
|
|
106
128
|
onDetentChange,
|
|
107
129
|
onDragBegin,
|
|
108
130
|
onDragChange,
|
|
@@ -143,12 +165,33 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
143
165
|
const effectiveDetached = isFormSheet || detached;
|
|
144
166
|
|
|
145
167
|
const colorScheme = useColorScheme();
|
|
168
|
+
const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
|
|
169
|
+
const didInitiallyPresentRef = useRef(shouldAutoPresent);
|
|
170
|
+
const [isOpen, setIsOpen] = useState(shouldAutoPresent);
|
|
171
|
+
const isOpenRef = useRef(isOpen);
|
|
172
|
+
isOpenRef.current = isOpen;
|
|
173
|
+
const keyboardViewport = useKeyboardViewport(isOpen);
|
|
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);
|
|
186
|
+
const [displayedBackgroundIndex, setDisplayedBackgroundIndex] = useState(
|
|
187
|
+
shouldAutoPresent ? initialDetentIndex : 0
|
|
188
|
+
);
|
|
146
189
|
const backgroundColor =
|
|
190
|
+
normalizeDetentBackground(
|
|
191
|
+
detentBackgrounds?.[Math.min(displayedBackgroundIndex, validDetents.length - 1)]
|
|
192
|
+
)?.color ??
|
|
147
193
|
backgroundColorProp ??
|
|
148
194
|
(colorScheme === 'dark' ? COLOR_SURFACE_CONTAINER_LOW_DARK : COLOR_SURFACE_CONTAINER_LOW_LIGHT);
|
|
149
|
-
|
|
150
|
-
const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
|
|
151
|
-
const [isOpen, setIsOpen] = useState(shouldAutoPresent);
|
|
152
195
|
const [activeSnapPoint, setActiveSnapPoint] = useState<SheetDetent | null>(
|
|
153
196
|
() => validDetents[shouldAutoPresent ? initialDetentIndex : 0] ?? null
|
|
154
197
|
);
|
|
@@ -164,8 +207,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
164
207
|
const validDetentsRef = useRef(validDetents);
|
|
165
208
|
validDetentsRef.current = validDetents;
|
|
166
209
|
|
|
167
|
-
const hasAutoDetent = validDetents.includes('auto');
|
|
168
|
-
|
|
169
210
|
const handleSetActiveSnapPoint = useCallback((snapPoint: number | string | null) => {
|
|
170
211
|
setActiveSnapPoint(
|
|
171
212
|
snapPoint == null
|
|
@@ -176,13 +217,31 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
176
217
|
);
|
|
177
218
|
}, []);
|
|
178
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
|
+
|
|
179
240
|
const handleOpenChange = useCallback(
|
|
180
241
|
(open: boolean) => {
|
|
181
|
-
if (!open
|
|
182
|
-
setIsOpen(false);
|
|
183
|
-
}
|
|
242
|
+
if (!open) dismissSheet();
|
|
184
243
|
},
|
|
185
|
-
[
|
|
244
|
+
[dismissSheet]
|
|
186
245
|
);
|
|
187
246
|
|
|
188
247
|
const portalContainer = usePortalContainer();
|
|
@@ -196,34 +255,67 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
196
255
|
e.preventDefault();
|
|
197
256
|
return;
|
|
198
257
|
}
|
|
258
|
+
if (!(target instanceof Element)) return;
|
|
259
|
+
const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
|
|
260
|
+
if (!wrapper) return;
|
|
199
261
|
// The footer is rendered via vaul's `detachedSiblings` as a sibling of
|
|
200
262
|
// Drawer.Content inside [data-vaul-detached-wrapper], so Radix treats
|
|
201
263
|
// clicks on it as "outside" the content. Don't dismiss for clicks that
|
|
202
264
|
// landed inside the wrapper.
|
|
203
|
-
if (target
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
265
|
+
if (wrapper.contains(target)) {
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Every sheet runs Radix non-modal and portals its overlay + wrapper as
|
|
270
|
+
// siblings into the shared container, so a press on a sheet stacked above
|
|
271
|
+
// this one (its content or its dim overlay) is also delivered here as
|
|
272
|
+
// "outside". Sheets present in document order, so anything after our
|
|
273
|
+
// wrapper belongs to a descendant. Radix may dispatch on the deferred
|
|
274
|
+
// click (touch), after a dismissing descendant has left the DOM — treat
|
|
275
|
+
// disconnected as above too.
|
|
276
|
+
/* eslint-disable no-bitwise */
|
|
277
|
+
const above = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_DISCONNECTED;
|
|
278
|
+
if (wrapper.compareDocumentPosition(target) & above) {
|
|
279
|
+
e.preventDefault();
|
|
208
280
|
}
|
|
281
|
+
/* eslint-enable no-bitwise */
|
|
209
282
|
};
|
|
210
283
|
|
|
211
284
|
const dismissAboveRef = useRef<(animated?: boolean) => Promise<void>>(async () => {});
|
|
285
|
+
const dismissDescendantsRef = useRef<() => void>(() => {});
|
|
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
|
+
}, []);
|
|
212
308
|
|
|
213
309
|
const methods = useMemo<TrueSheetMethods>(
|
|
214
310
|
() => ({
|
|
215
|
-
present:
|
|
216
|
-
const detent = validDetentsRef.current[index];
|
|
217
|
-
if (detent === undefined) {
|
|
218
|
-
throw new Error(
|
|
219
|
-
`TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
setActiveSnapPoint(detent);
|
|
223
|
-
setIsOpen(true);
|
|
224
|
-
},
|
|
311
|
+
present: (index = 0) => presentSheet(index, false),
|
|
225
312
|
dismiss: async () => {
|
|
226
|
-
|
|
313
|
+
const dismissal = dismissSheet();
|
|
314
|
+
if (!dismissal) {
|
|
315
|
+
console.warn(ALREADY_DISMISSED_WARNING);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
await dismissal;
|
|
227
319
|
},
|
|
228
320
|
resize: async (index) => {
|
|
229
321
|
const detent = validDetentsRef.current[index];
|
|
@@ -235,18 +327,49 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
235
327
|
setActiveSnapPoint(detent);
|
|
236
328
|
},
|
|
237
329
|
dismissStack: async (animated) => {
|
|
330
|
+
if (phaseRef.current === 'dismissed' || phaseRef.current === 'dismissing') {
|
|
331
|
+
console.warn(ALREADY_DISMISSED_WARNING);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
238
334
|
await dismissAboveRef.current(animated);
|
|
239
335
|
},
|
|
240
336
|
}),
|
|
241
|
-
[]
|
|
337
|
+
[presentSheet, dismissSheet]
|
|
242
338
|
);
|
|
243
339
|
|
|
244
340
|
useImperativeHandle(ref, () => methods, [methods]);
|
|
245
341
|
|
|
246
|
-
|
|
247
|
-
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
if (!shouldAutoPresent || didInitiallyPresentRef.current) return;
|
|
344
|
+
didInitiallyPresentRef.current = true;
|
|
345
|
+
if (phaseRef.current === 'dismissed') {
|
|
346
|
+
presentSheet(initialDetentIndex, true);
|
|
347
|
+
}
|
|
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
|
+
);
|
|
358
|
+
|
|
359
|
+
const handleRef = useRef<SheetHandle>({
|
|
360
|
+
methods,
|
|
361
|
+
dismissQuietly: async () => {
|
|
362
|
+
await dismissSheet();
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
useRegisterSheet(name, handleRef);
|
|
248
366
|
|
|
249
367
|
const drawerContentRef = useRef<HTMLDivElement | null>(null);
|
|
368
|
+
const [drawerContent, setDrawerContent] = useState<HTMLDivElement | null>(null);
|
|
369
|
+
const handleDrawerRef = useCallback((node: HTMLDivElement | null) => {
|
|
370
|
+
drawerContentRef.current = node;
|
|
371
|
+
setDrawerContent(node);
|
|
372
|
+
}, []);
|
|
250
373
|
|
|
251
374
|
// Measured header/footer heights drive the 'peek' snap point — mirrors
|
|
252
375
|
// native, where the controller tracks headerHeight/footerHeight.
|
|
@@ -272,15 +395,45 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
272
395
|
// within the content — measured by the peek against `contentRef`.
|
|
273
396
|
const [peekContentHeight, setPeekContentHeight] = useState(0);
|
|
274
397
|
const contentRef = useRef<View>(null);
|
|
275
|
-
const peekElRef = useRef<View>(null);
|
|
276
|
-
|
|
398
|
+
const peekElRef = useRef<View | null>(null);
|
|
399
|
+
|
|
400
|
+
const measurePeek = useCallback(() => {
|
|
401
|
+
const peekEl = getDOMElement(peekElRef.current);
|
|
402
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
403
|
+
if (peekEl && contentEl) {
|
|
404
|
+
setPeekContentHeight(measurePeekContentHeight(peekEl, contentEl));
|
|
405
|
+
}
|
|
406
|
+
}, []);
|
|
407
|
+
|
|
408
|
+
// Last-attached wins — during a screen swap the incoming peek mounts before
|
|
409
|
+
// the outgoing one unmounts, so the outgoing detach must not clear it.
|
|
410
|
+
const peekContext = useMemo<TrueSheetPeekContextValue>(
|
|
411
|
+
() => ({
|
|
412
|
+
contentRef,
|
|
413
|
+
attachPeek: (view) => {
|
|
414
|
+
peekElRef.current = view;
|
|
415
|
+
measurePeek();
|
|
416
|
+
},
|
|
417
|
+
detachPeek: (view) => {
|
|
418
|
+
if (peekElRef.current !== view) return;
|
|
419
|
+
peekElRef.current = null;
|
|
420
|
+
setPeekContentHeight(0);
|
|
421
|
+
},
|
|
422
|
+
measurePeek: (view) => {
|
|
423
|
+
if (peekElRef.current === view) measurePeek();
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
[measurePeek]
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// The peek's own onLayout only fires when *it* resizes. Content added above
|
|
430
|
+
// it moves it without resizing it, so re-measure its offset whenever the
|
|
431
|
+
// content view's layout changes.
|
|
432
|
+
const handleContentLayout = measurePeek;
|
|
277
433
|
|
|
278
434
|
// An absolute (floating) header overlaps the content, so it contributes no
|
|
279
435
|
// height to the 'auto' detent measurement.
|
|
280
436
|
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
281
|
-
const absoluteHeaderRef = useRef(absoluteHeader);
|
|
282
|
-
absoluteHeaderRef.current = absoluteHeader;
|
|
283
|
-
|
|
284
437
|
const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
|
|
285
438
|
|
|
286
439
|
// Same for an absolute (floating) footer — rendered via vaul's
|
|
@@ -316,120 +469,67 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
316
469
|
(footer && absoluteFooter ? footerHeight : 0) +
|
|
317
470
|
peekContentHeight || DEFAULT_PEEK_HEIGHT;
|
|
318
471
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
472
|
+
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
473
|
+
const naturalHeightRef = useRef(0);
|
|
474
|
+
const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
|
|
475
|
+
const footerInsetAdjustment =
|
|
476
|
+
Boolean(footer) &&
|
|
477
|
+
absoluteFooter &&
|
|
478
|
+
(insetBehavior === 'automatic' || insetBehavior === 'footer');
|
|
479
|
+
const measureContent = validDetents.includes('auto') || isFormSheet;
|
|
480
|
+
|
|
481
|
+
useLayoutEffect(() => {
|
|
482
|
+
const layoutEl = sizedLayoutRef.current;
|
|
483
|
+
if (!isOpen || !drawerContent || !layoutEl?.isConnected) return undefined;
|
|
484
|
+
|
|
485
|
+
setHeaderHeight(getDOMElement(headerElRef.current)?.offsetHeight ?? 0);
|
|
486
|
+
setFooterHeight(getDOMElement(footerElRef.current)?.offsetHeight ?? 0);
|
|
487
|
+
measurePeek();
|
|
488
|
+
|
|
489
|
+
const observer = observeSheetLayout({
|
|
490
|
+
drawer: drawerContent,
|
|
491
|
+
layout: layoutEl,
|
|
492
|
+
getContent: () => getDOMElement(contentRef.current),
|
|
493
|
+
getHeader: () => getDOMElement(headerElRef.current),
|
|
494
|
+
getFooter: () => getDOMElement(footerElRef.current),
|
|
495
|
+
getScrollable: () => getScrollableElement(scrollableRef?.current),
|
|
496
|
+
absoluteHeader,
|
|
497
|
+
absoluteFooter,
|
|
498
|
+
footerInsetAdjustment,
|
|
499
|
+
measureContent,
|
|
500
|
+
onHeightChange: (height) => {
|
|
501
|
+
naturalHeightRef.current = height;
|
|
502
|
+
setMeasuredContentHeight(height);
|
|
503
|
+
},
|
|
504
|
+
});
|
|
345
505
|
|
|
346
|
-
|
|
347
|
-
|
|
506
|
+
return () => observer.disconnect();
|
|
507
|
+
}, [
|
|
508
|
+
isOpen,
|
|
509
|
+
drawerContent,
|
|
510
|
+
measurePeek,
|
|
511
|
+
absoluteHeader,
|
|
512
|
+
absoluteFooter,
|
|
513
|
+
footerInsetAdjustment,
|
|
514
|
+
measureContent,
|
|
515
|
+
scrollableRef,
|
|
516
|
+
]);
|
|
348
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;
|
|
349
522
|
let canceled = false;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const drawerEl = drawerContentRef.current;
|
|
357
|
-
if (!drawerEl || !drawerEl.isConnected) {
|
|
358
|
-
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
359
|
-
rafId = window.requestAnimationFrame(attach);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const measure = () => setScrollableAutoHeight(measureNaturalHeight());
|
|
364
|
-
|
|
365
|
-
let observed: {
|
|
366
|
-
contentEl: HTMLElement | null;
|
|
367
|
-
headerEl: HTMLElement | null;
|
|
368
|
-
scroller: HTMLElement;
|
|
369
|
-
scrollContent: Element | null;
|
|
370
|
-
} | null = null;
|
|
371
|
-
|
|
372
|
-
// (Re)pin the first vertical scrollable and observe the nodes whose size
|
|
373
|
-
// feeds the natural height — content growth inside a bounded scroller is
|
|
374
|
-
// invisible to the sheet's own layout (mirrors native's contentSize
|
|
375
|
-
// observation). Resolves the content node live: the layout-branch swap
|
|
376
|
-
// (natural flow ↔ sized) remounts it.
|
|
377
|
-
const observe = () => {
|
|
378
|
-
const contentEl = getDOMElement(contentRef.current);
|
|
379
|
-
const headerEl = getDOMElement(headerElRef.current);
|
|
380
|
-
|
|
381
|
-
// Skip mutations that don't change the observed topology — e.g. a
|
|
382
|
-
// virtualized list mounting rows inside the pinned scroller, which
|
|
383
|
-
// would otherwise trigger a full-subtree rescan (findVerticalScroller
|
|
384
|
-
// resolves computed styles) on every scroll frame. Size changes are
|
|
385
|
-
// already covered by the ResizeObserver.
|
|
386
|
-
if (
|
|
387
|
-
observed &&
|
|
388
|
-
observed.contentEl === contentEl &&
|
|
389
|
-
contentEl?.isConnected &&
|
|
390
|
-
observed.headerEl === headerEl &&
|
|
391
|
-
observed.scroller.isConnected &&
|
|
392
|
-
observed.scrollContent === observed.scroller.firstElementChild
|
|
393
|
-
) {
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const scroller =
|
|
398
|
-
contentEl && contentEl.isConnected ? findVerticalScroller(contentEl) : null;
|
|
399
|
-
pinnedScrollerRef.current = scroller;
|
|
400
|
-
setHasBoundedScrollable(scroller != null);
|
|
401
|
-
observed = scroller
|
|
402
|
-
? { contentEl, headerEl, scroller, scrollContent: scroller.firstElementChild }
|
|
403
|
-
: null;
|
|
404
|
-
|
|
405
|
-
resizeObserver?.disconnect();
|
|
406
|
-
resizeObserver = new ResizeObserver(measure);
|
|
407
|
-
if (contentEl?.isConnected) resizeObserver.observe(contentEl);
|
|
408
|
-
if (headerEl) resizeObserver.observe(headerEl);
|
|
409
|
-
if (scroller) {
|
|
410
|
-
resizeObserver.observe(scroller);
|
|
411
|
-
if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
|
|
412
|
-
}
|
|
413
|
-
measure();
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
observe();
|
|
417
|
-
// Watch the whole drawer subtree — catches the scrollable mounting/
|
|
418
|
-
// unmounting AND the layout-branch swap, which remounts the content node
|
|
419
|
-
// (a content-scoped observer would go stale after the swap).
|
|
420
|
-
mutationObserver = new MutationObserver(observe);
|
|
421
|
-
mutationObserver.observe(drawerEl, { childList: true, subtree: true });
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
rafId = window.requestAnimationFrame(attach);
|
|
425
|
-
|
|
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
|
+
});
|
|
426
529
|
return () => {
|
|
427
530
|
canceled = true;
|
|
428
|
-
window.cancelAnimationFrame(rafId);
|
|
429
|
-
mutationObserver?.disconnect();
|
|
430
|
-
resizeObserver?.disconnect();
|
|
431
531
|
};
|
|
432
|
-
}, [
|
|
532
|
+
}, [keyboardViewport]);
|
|
433
533
|
|
|
434
534
|
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
435
535
|
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
@@ -437,13 +537,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
437
537
|
const isScrollLocked =
|
|
438
538
|
validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
|
|
439
539
|
|
|
440
|
-
// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
|
|
441
|
-
// Track it here so the form sheet can size its card to fit content,
|
|
442
|
-
// clamped between a minimum ratio of the viewport and a maximum derived
|
|
443
|
-
// from `detachedOffset` (the breathing room left at top + bottom of the
|
|
444
|
-
// floating card).
|
|
445
|
-
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
446
|
-
|
|
447
540
|
const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
|
|
448
541
|
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
449
542
|
if (!isFormSheet) return undefined;
|
|
@@ -510,7 +603,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
510
603
|
effectiveMaxContentHeight,
|
|
511
604
|
peekHeight,
|
|
512
605
|
peekContentHeight,
|
|
513
|
-
|
|
606
|
+
keyboardViewport,
|
|
514
607
|
});
|
|
515
608
|
geometryInputsRef.current = {
|
|
516
609
|
effectiveDetached,
|
|
@@ -518,26 +611,26 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
518
611
|
effectiveMaxContentHeight,
|
|
519
612
|
peekHeight,
|
|
520
613
|
peekContentHeight,
|
|
521
|
-
|
|
614
|
+
keyboardViewport,
|
|
522
615
|
};
|
|
523
616
|
|
|
524
617
|
const computeDetentGeometry = useCallback(() => {
|
|
525
618
|
const inputs = geometryInputsRef.current;
|
|
526
|
-
const windowH = window.innerHeight;
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
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;
|
|
530
626
|
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
531
627
|
const ceiling =
|
|
532
628
|
inputs.effectiveMaxContentHeight !== undefined
|
|
533
629
|
? Math.min(effectiveH, inputs.effectiveMaxContentHeight)
|
|
534
630
|
: effectiveH;
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
// so it reads 0 until the ResizeObserver fires). Falls back to the state
|
|
539
|
-
// value, then to vaul's pre-measure fallback (effectiveHeight / 2).
|
|
540
|
-
const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
|
|
631
|
+
// Reuse the intrinsic measurement during position events so animation
|
|
632
|
+
// frames don't clone the content tree again.
|
|
633
|
+
const measuredHeight = naturalHeightRef.current;
|
|
541
634
|
const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
|
|
542
635
|
|
|
543
636
|
// 'peek' is measured live from the DOM (offsetHeight is layout-based, so
|
|
@@ -565,7 +658,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
565
658
|
for (const d of validDetentsRef.current) {
|
|
566
659
|
const h =
|
|
567
660
|
typeof d === 'number'
|
|
568
|
-
? Math.min(d *
|
|
661
|
+
? Math.min(d * fractionH, ceiling)
|
|
569
662
|
: d === 'peek'
|
|
570
663
|
? Math.min(livePeekHeight, ceiling)
|
|
571
664
|
: autoHeight;
|
|
@@ -573,7 +666,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
573
666
|
values.push(effectiveH > 0 ? h / effectiveH : 0);
|
|
574
667
|
}
|
|
575
668
|
return { windowH, effectiveH, ceiling, positions, values };
|
|
576
|
-
}, [
|
|
669
|
+
}, []);
|
|
577
670
|
|
|
578
671
|
// Detent info for lifecycle events. Position/detent come from the active
|
|
579
672
|
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
@@ -652,54 +745,27 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
652
745
|
[computeDetentGeometry]
|
|
653
746
|
);
|
|
654
747
|
|
|
655
|
-
// Mirror native synchronous layout: while dragging (and through the post-
|
|
656
|
-
// release settle) the sized layout tracks the sheet's visible height in
|
|
657
|
-
// realtime, clamped to the smallest detent — dragging below it slides the
|
|
658
|
-
// sheet out without resizing. Once settled, the inline height is cleared so
|
|
659
|
-
// the CSS calc() owns sizing again (adapts to viewport resizes at rest).
|
|
660
748
|
const sizedLayoutRef = useRef<HTMLDivElement>(null);
|
|
661
749
|
const isDraggingRef = useRef(false);
|
|
662
|
-
const isSettlingAfterDragRef = useRef(false);
|
|
663
|
-
const updateSizedLayoutHeight = useCallback(
|
|
664
|
-
(position: number) => {
|
|
665
|
-
const node = sizedLayoutRef.current;
|
|
666
|
-
if (!node) return;
|
|
667
|
-
|
|
668
|
-
const { effectiveH, ceiling, positions } = computeDetentGeometry();
|
|
669
|
-
if (positions.length === 0) return;
|
|
670
|
-
|
|
671
|
-
if (isSettlingAfterDragRef.current) {
|
|
672
|
-
const snap = activeSnapPointRef.current;
|
|
673
|
-
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
674
|
-
const target = index >= 0 ? positions[index]! : null;
|
|
675
|
-
if (target != null && Math.abs(position - target) < 1) {
|
|
676
|
-
isSettlingAfterDragRef.current = false;
|
|
677
|
-
// Restore the calc — height is an inline style, so clearing it would
|
|
678
|
-
// leave the div unsized (React won't re-apply it without a render)
|
|
679
|
-
node.style.height = SIZED_LAYOUT_HEIGHT;
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// positions[0] is the smallest detent's top-Y — its height is the clamp floor.
|
|
685
|
-
const minHeight = effectiveH - positions[0]!;
|
|
686
|
-
const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
|
|
687
|
-
node.style.height = `${height}px`;
|
|
688
|
-
},
|
|
689
|
-
[computeDetentGeometry]
|
|
690
|
-
);
|
|
691
750
|
|
|
692
751
|
const handlePositionChange = useCallback(
|
|
693
752
|
(position: number) => {
|
|
694
|
-
if (isDraggingRef.current || isSettlingAfterDragRef.current) {
|
|
695
|
-
updateSizedLayoutHeight(position);
|
|
696
|
-
}
|
|
697
753
|
const { index, detent } = interpolateFromPosition(position);
|
|
754
|
+
if (isOpenRef.current && !isPresentingRef.current) {
|
|
755
|
+
const { effectiveH, positions } = computeDetentGeometry();
|
|
756
|
+
setDisplayedBackgroundIndex((current) =>
|
|
757
|
+
getDetentBackgroundIndex(
|
|
758
|
+
effectiveH - position,
|
|
759
|
+
positions.map((top) => effectiveH - top),
|
|
760
|
+
current
|
|
761
|
+
)
|
|
762
|
+
);
|
|
763
|
+
}
|
|
698
764
|
onPositionChangeRef.current?.({
|
|
699
765
|
nativeEvent: { index, position, detent, realtime: true },
|
|
700
766
|
} as PositionChangeEvent);
|
|
701
767
|
},
|
|
702
|
-
[interpolateFromPosition,
|
|
768
|
+
[interpolateFromPosition, computeDetentGeometry]
|
|
703
769
|
);
|
|
704
770
|
|
|
705
771
|
// Fire onMount once after first render. React-mount is the earliest point
|
|
@@ -726,6 +792,12 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
726
792
|
|
|
727
793
|
const present = isOpen;
|
|
728
794
|
if (!present) {
|
|
795
|
+
// Mirror native: dismissing a sheet takes every sheet stacked above it
|
|
796
|
+
// (iOS dismisses from the presenter; Android runs dismissStack first).
|
|
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;
|
|
729
801
|
// Pair willBlur with willDismiss on dismiss — mirrors native iOS
|
|
730
802
|
// emitWillDismissEvents (blur fires before dismiss). willPresent is
|
|
731
803
|
// deferred to `start()` below (needs the mounted drawer's geometry).
|
|
@@ -735,11 +807,21 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
735
807
|
|
|
736
808
|
const fireDone = () => {
|
|
737
809
|
if (present) {
|
|
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';
|
|
738
813
|
onDidPresentRef.current?.({ nativeEvent: computeDetentInfo() } as DidPresentEvent);
|
|
739
814
|
onDidFocusRef.current?.({ nativeEvent: null } as DidFocusEvent);
|
|
815
|
+
settle(resolvePresentRef);
|
|
816
|
+
if (shouldDismissOnPresentRef.current) {
|
|
817
|
+
shouldDismissOnPresentRef.current = false;
|
|
818
|
+
setIsOpen(false);
|
|
819
|
+
}
|
|
740
820
|
} else {
|
|
821
|
+
phaseRef.current = 'dismissed';
|
|
741
822
|
onDidBlurRef.current?.({ nativeEvent: null } as DidBlurEvent);
|
|
742
823
|
onDidDismissRef.current?.({ nativeEvent: null } as DidDismissEvent);
|
|
824
|
+
settle(resolveDismissRef);
|
|
743
825
|
}
|
|
744
826
|
};
|
|
745
827
|
|
|
@@ -765,6 +847,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
765
847
|
// willPresent mirrors native iOS where viewWillAppear dispatches
|
|
766
848
|
// both; the descendant-stack focus effect handles subsequent
|
|
767
849
|
// transitions.
|
|
850
|
+
didEmitWillPresentRef.current = true;
|
|
768
851
|
onWillPresentRef.current?.({ nativeEvent: computeDetentInfo() } as WillPresentEvent);
|
|
769
852
|
onWillFocusRef.current?.({ nativeEvent: null } as WillFocusEvent);
|
|
770
853
|
}
|
|
@@ -824,29 +907,31 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
824
907
|
}
|
|
825
908
|
onDragChangeRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragChangeEvent);
|
|
826
909
|
}, [computeDetentInfo]);
|
|
827
|
-
const handleRelease = useCallback(
|
|
828
|
-
(
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
// stays open — a dismissal slides out at the min-clamped height.
|
|
833
|
-
isSettlingAfterDragRef.current = open;
|
|
834
|
-
onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
|
|
835
|
-
},
|
|
836
|
-
[computeDetentInfo]
|
|
837
|
-
);
|
|
910
|
+
const handleRelease = useCallback(() => {
|
|
911
|
+
if (!isDraggingRef.current) return;
|
|
912
|
+
isDraggingRef.current = false;
|
|
913
|
+
onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
|
|
914
|
+
}, [computeDetentInfo]);
|
|
838
915
|
|
|
839
916
|
const { isNested, dismissAbove, descendants } = useSheetStack(
|
|
840
|
-
|
|
917
|
+
handleRef,
|
|
841
918
|
drawerContentRef,
|
|
842
919
|
isOpen,
|
|
843
920
|
isFormSheet
|
|
844
921
|
);
|
|
845
922
|
dismissAboveRef.current = dismissAbove;
|
|
923
|
+
// Captured per render: by the time the dismiss effect runs, this sheet has
|
|
924
|
+
// already been popped from the stack, so `dismissAbove` can't find it.
|
|
925
|
+
dismissDescendantsRef.current = () => {
|
|
926
|
+
for (let i = descendants.length - 1; i >= 0; i--) {
|
|
927
|
+
descendants[i]!.ref.current?.dismissQuietly();
|
|
928
|
+
}
|
|
929
|
+
};
|
|
846
930
|
|
|
847
931
|
// Mirror Android: translate this sheet down to match the deepest descendant's
|
|
848
932
|
// top so the whole stack visually aligns. Cascades because every ancestor
|
|
849
933
|
// re-runs whenever the stack (and thus its descendants) changes.
|
|
934
|
+
const wasCascadedRef = useRef(false);
|
|
850
935
|
useEffect(() => {
|
|
851
936
|
const parent = drawerContentRef.current;
|
|
852
937
|
if (!parent) return;
|
|
@@ -858,7 +943,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
858
943
|
if (!isOpen) return;
|
|
859
944
|
const parentWrapper = parent.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
860
945
|
|
|
861
|
-
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
946
|
+
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
862
947
|
const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
863
948
|
const CLIP_NONE = 'inset(0px round 0px)';
|
|
864
949
|
|
|
@@ -882,23 +967,32 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
882
967
|
};
|
|
883
968
|
|
|
884
969
|
if (descendants.length === 0) {
|
|
970
|
+
if (!wasCascadedRef.current) return;
|
|
971
|
+
wasCascadedRef.current = false;
|
|
885
972
|
parent.style.transition = transition;
|
|
886
973
|
parent.style.transform = '';
|
|
887
974
|
setClip(CLIP_NONE);
|
|
888
975
|
return;
|
|
889
976
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
977
|
+
wasCascadedRef.current = true;
|
|
978
|
+
|
|
979
|
+
// Track the immediate child's *actual* top: its cascade transform when it
|
|
980
|
+
// was itself pushed behind a grandchild, else its own snap point. Mirrors
|
|
981
|
+
// Android's addTranslation propagating extra translation up the stack —
|
|
982
|
+
// a grandchild opening pushes this sheet only as far as our child moved.
|
|
983
|
+
const readTranslateY = (el: HTMLElement) => {
|
|
984
|
+
const match = el.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
|
|
985
|
+
return match ? Number.parseFloat(match[1]!) : null;
|
|
986
|
+
};
|
|
895
987
|
const computeTargetY = () => {
|
|
896
988
|
const parentSnap =
|
|
897
989
|
Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
898
990
|
const node = descendants[0]?.nodeRef.current;
|
|
899
991
|
if (!node) return parentSnap;
|
|
900
|
-
const
|
|
901
|
-
|
|
992
|
+
const childTop =
|
|
993
|
+
readTranslateY(node) ??
|
|
994
|
+
(Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0);
|
|
995
|
+
return Math.max(parentSnap, childTop);
|
|
902
996
|
};
|
|
903
997
|
|
|
904
998
|
// When a form-sheet parent has a form-sheet descendant, clip the parent to
|
|
@@ -934,18 +1028,24 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
934
1028
|
|
|
935
1029
|
const apply = () => {
|
|
936
1030
|
applyFormClip();
|
|
937
|
-
// Mirror
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
|
|
1031
|
+
// Mirror iPad/Android (`isExpanded`): a parent at full height (detent 1)
|
|
1032
|
+
// stays put and the child simply overlays it. Full height is the sheet's
|
|
1033
|
+
// own ceiling — the viewport for a page sheet, the capped content-fit
|
|
1034
|
+
// height for a form sheet — so compare against where detent 1 lands.
|
|
1035
|
+
const parentSnap =
|
|
1036
|
+
Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
1037
|
+
const { effectiveH, ceiling } = computeDetentGeometry();
|
|
1038
|
+
if (parentSnap <= effectiveH - ceiling + 0.5) {
|
|
942
1039
|
parent.style.transition = transition;
|
|
943
1040
|
parent.style.transform = '';
|
|
944
1041
|
return;
|
|
945
1042
|
}
|
|
1043
|
+
// Mirror Android: stay put while the child is being dragged (its live
|
|
1044
|
+
// transform would drag us along during drag-to-dismiss). The release
|
|
1045
|
+
// snap rewrites the child's style and re-applies.
|
|
1046
|
+
if (descendants[0]?.nodeRef.current?.classList.contains(DRAG_CLASS)) return;
|
|
946
1047
|
const targetY = computeTargetY();
|
|
947
|
-
const
|
|
948
|
-
const currentY = match ? Number.parseFloat(match[1]!) : 0;
|
|
1048
|
+
const currentY = readTranslateY(parent) ?? 0;
|
|
949
1049
|
if (Math.abs(currentY - targetY) < 0.5) return;
|
|
950
1050
|
parent.style.transition = transition;
|
|
951
1051
|
parent.style.transform = `translate3d(0, ${targetY}px, 0)`;
|
|
@@ -957,12 +1057,15 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
957
1057
|
// inline style changes.
|
|
958
1058
|
const observer = new MutationObserver(apply);
|
|
959
1059
|
observer.observe(parent, { attributes: true, attributeFilter: ['style'] });
|
|
1060
|
+
// The child's own cascade lands in a later frame — follow it when it moves.
|
|
1061
|
+
const childNode = descendants[0]?.nodeRef.current;
|
|
1062
|
+
if (childNode) observer.observe(childNode, { attributes: true, attributeFilter: ['style'] });
|
|
960
1063
|
|
|
961
1064
|
return () => {
|
|
962
1065
|
cancelAnimationFrame(raf);
|
|
963
1066
|
observer.disconnect();
|
|
964
1067
|
};
|
|
965
|
-
}, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen]);
|
|
1068
|
+
}, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen, computeDetentGeometry]);
|
|
966
1069
|
|
|
967
1070
|
// Focus/blur events fire when a descendant sheet appears on top of this one
|
|
968
1071
|
// (blur) or when all descendants are dismissed (focus). will-events fire
|
|
@@ -973,12 +1076,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
973
1076
|
const onDidBlurRef = useRef(onDidBlur);
|
|
974
1077
|
const onWillFocusRef = useRef(onWillFocus);
|
|
975
1078
|
const onDidFocusRef = useRef(onDidFocus);
|
|
1079
|
+
const onDismissAttemptRef = useRef(onDismissAttempt);
|
|
976
1080
|
useEffect(() => {
|
|
977
1081
|
onWillBlurRef.current = onWillBlur;
|
|
978
1082
|
onDidBlurRef.current = onDidBlur;
|
|
979
1083
|
onWillFocusRef.current = onWillFocus;
|
|
980
1084
|
onDidFocusRef.current = onDidFocus;
|
|
1085
|
+
onDismissAttemptRef.current = onDismissAttempt;
|
|
981
1086
|
});
|
|
1087
|
+
const handleDismissAttempt = useCallback(() => {
|
|
1088
|
+
onDismissAttemptRef.current?.({ nativeEvent: null } as DismissAttemptEvent);
|
|
1089
|
+
}, []);
|
|
982
1090
|
|
|
983
1091
|
const prevDescendantCountRef = useRef(0);
|
|
984
1092
|
useEffect(() => {
|
|
@@ -1026,11 +1134,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1026
1134
|
};
|
|
1027
1135
|
}, [isOpen, descendants.length]);
|
|
1028
1136
|
|
|
1029
|
-
// Definite-height flex layout (per-detent sizing) unless content must be
|
|
1030
|
-
// measured in natural flow: 'auto' detents (without a plugged scrollable —
|
|
1031
|
-
// those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
|
|
1032
|
-
const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
|
|
1033
|
-
|
|
1034
1137
|
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
1035
1138
|
|
|
1036
1139
|
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
@@ -1048,6 +1151,24 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1048
1151
|
? `drop-shadow(0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15))`
|
|
1049
1152
|
: undefined;
|
|
1050
1153
|
|
|
1154
|
+
const backgroundLayerStyle: React.CSSProperties = {
|
|
1155
|
+
position: 'absolute',
|
|
1156
|
+
inset: 0,
|
|
1157
|
+
pointerEvents: 'none',
|
|
1158
|
+
backgroundColor: backgroundColor as string,
|
|
1159
|
+
transition: isPresentingRef.current ? 'none' : 'background-color 200ms',
|
|
1160
|
+
};
|
|
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
|
+
|
|
1051
1172
|
const mergedContentStyle = useMemo<React.CSSProperties>(
|
|
1052
1173
|
() => ({
|
|
1053
1174
|
position: 'fixed',
|
|
@@ -1059,7 +1180,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1059
1180
|
flexDirection: 'column',
|
|
1060
1181
|
borderTopLeftRadius: effectiveCornerRadius,
|
|
1061
1182
|
borderTopRightRadius: effectiveCornerRadius,
|
|
1062
|
-
backgroundColor: backgroundColor as string,
|
|
1063
1183
|
// Clip children to the rounded top so headers/content with their own
|
|
1064
1184
|
// background don't bleed past the corners. `clip` (not `hidden`): a
|
|
1065
1185
|
// hidden box is still a scroll container, so browser focus-reveal (e.g.
|
|
@@ -1069,12 +1189,24 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1069
1189
|
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
1070
1190
|
// A relative footer owns the inset instead (see resolvedFooterStyle); an
|
|
1071
1191
|
// absolute footer floats over the content, so the content keeps its lift.
|
|
1192
|
+
// A plugged scrollable keeps the lift too (contentInsetAdjustment
|
|
1193
|
+
// 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
|
|
1194
|
+
// behind the indicator with user-applied content padding.
|
|
1072
1195
|
paddingBottom:
|
|
1073
|
-
insetAdjustment === 'automatic' &&
|
|
1196
|
+
insetAdjustment === 'automatic' &&
|
|
1197
|
+
!(footerOwnsInset && !absoluteFooter) &&
|
|
1198
|
+
(!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area')
|
|
1074
1199
|
? 'env(safe-area-inset-bottom, 0px)'
|
|
1075
1200
|
: 0,
|
|
1076
1201
|
}),
|
|
1077
|
-
[
|
|
1202
|
+
[
|
|
1203
|
+
effectiveCornerRadius,
|
|
1204
|
+
insetAdjustment,
|
|
1205
|
+
footerOwnsInset,
|
|
1206
|
+
absoluteFooter,
|
|
1207
|
+
scrollableRef,
|
|
1208
|
+
insetBehavior,
|
|
1209
|
+
]
|
|
1078
1210
|
);
|
|
1079
1211
|
|
|
1080
1212
|
const defaultGrabberColor =
|
|
@@ -1112,7 +1244,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1112
1244
|
// `maxContentWidth` is ignored ('form' is absolute).
|
|
1113
1245
|
// - presentation='page' → `maxContentWidth` (any viewport) or
|
|
1114
1246
|
// DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
|
|
1115
|
-
// Detached without a width constraint applies
|
|
1247
|
+
// Detached without a width constraint applies placementOffset on both edges so
|
|
1116
1248
|
// the floating card breathes from the viewport sides.
|
|
1117
1249
|
const wrapperStyle = useMemo<React.CSSProperties | undefined>(() => {
|
|
1118
1250
|
// Mobile portrait ignores width sizing entirely (matches iOS/Android:
|
|
@@ -1131,30 +1263,30 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1131
1263
|
if (dropShadow) next.filter = dropShadow;
|
|
1132
1264
|
if (!needsMargins) return next;
|
|
1133
1265
|
|
|
1134
|
-
let
|
|
1135
|
-
let
|
|
1266
|
+
let marginInlineStart: number | string;
|
|
1267
|
+
let marginInlineEnd: number | string;
|
|
1136
1268
|
if (isFormSheet) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1269
|
+
marginInlineStart = 'auto';
|
|
1270
|
+
marginInlineEnd = 'auto';
|
|
1139
1271
|
} else if (maxWidth == null) {
|
|
1140
|
-
|
|
1141
|
-
|
|
1272
|
+
marginInlineStart = placementOffset;
|
|
1273
|
+
marginInlineEnd = placementOffset;
|
|
1142
1274
|
} else {
|
|
1143
|
-
|
|
1144
|
-
|
|
1275
|
+
marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
|
|
1276
|
+
marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
|
|
1145
1277
|
}
|
|
1146
1278
|
|
|
1147
1279
|
if (maxWidth != null) next.maxWidth = maxWidth;
|
|
1148
|
-
next.
|
|
1149
|
-
next.
|
|
1280
|
+
next.marginInlineStart = marginInlineStart;
|
|
1281
|
+
next.marginInlineEnd = marginInlineEnd;
|
|
1150
1282
|
return next;
|
|
1151
1283
|
}, [
|
|
1152
1284
|
isLandscapeOrTablet,
|
|
1153
1285
|
isFormSheet,
|
|
1154
1286
|
maxContentWidth,
|
|
1155
1287
|
presentation,
|
|
1156
|
-
|
|
1157
|
-
|
|
1288
|
+
placement,
|
|
1289
|
+
placementOffset,
|
|
1158
1290
|
effectiveDetached,
|
|
1159
1291
|
dropShadow,
|
|
1160
1292
|
]);
|
|
@@ -1191,6 +1323,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1191
1323
|
onDrag={handleDrag}
|
|
1192
1324
|
onRelease={handleRelease}
|
|
1193
1325
|
dismissible={dismissible}
|
|
1326
|
+
dismissThreshold={dismissThreshold}
|
|
1327
|
+
onDismissAttempt={handleDismissAttempt}
|
|
1194
1328
|
draggable={draggable}
|
|
1195
1329
|
repositionInputs={false}
|
|
1196
1330
|
modal={dimmed}
|
|
@@ -1200,24 +1334,18 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1200
1334
|
detachedRadius={effectiveCornerRadius}
|
|
1201
1335
|
maxContentHeight={effectiveMaxContentHeight}
|
|
1202
1336
|
peekHeight={peekHeight}
|
|
1203
|
-
initialAnimated={initialDetentAnimated}
|
|
1337
|
+
initialAnimated={shouldAutoPresent ? initialDetentAnimated : true}
|
|
1204
1338
|
detachedWrapperStyle={wrapperStyle}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
// Bounded scrollable → the auto-size wrapper measures 0 (sized
|
|
1208
|
-
// layout is absolute), so feed vaul the natural height instead.
|
|
1209
|
-
hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0
|
|
1210
|
-
? scrollableAutoHeight
|
|
1211
|
-
: undefined
|
|
1212
|
-
}
|
|
1339
|
+
contentHeight={measuredContentHeight}
|
|
1340
|
+
keyboardViewport={keyboardViewport}
|
|
1213
1341
|
activeSnapPoint={activeSnapPoint}
|
|
1214
1342
|
setActiveSnapPoint={handleSetActiveSnapPoint}
|
|
1215
1343
|
{...snapPointsProps}
|
|
1216
1344
|
>
|
|
1217
1345
|
<Drawer.Portal container={portalContainer ?? undefined}>
|
|
1218
|
-
<Drawer.Overlay style={
|
|
1346
|
+
<Drawer.Overlay style={resolvedOverlayStyle} />
|
|
1219
1347
|
<Drawer.Content
|
|
1220
|
-
ref={
|
|
1348
|
+
ref={handleDrawerRef}
|
|
1221
1349
|
style={mergedContentStyle}
|
|
1222
1350
|
onPointerDownOutside={handlePointerDownOutside}
|
|
1223
1351
|
detachedSiblings={
|
|
@@ -1230,66 +1358,35 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1230
1358
|
) : undefined
|
|
1231
1359
|
}
|
|
1232
1360
|
>
|
|
1361
|
+
<div aria-hidden style={backgroundLayerStyle} />
|
|
1233
1362
|
<Drawer.Title style={visuallyHiddenStyle}>
|
|
1234
1363
|
{accessibilityOptions?.paneTitle ?? 'Sheet'}
|
|
1235
1364
|
</Drawer.Title>
|
|
1236
1365
|
{grabber && <Drawer.Handle style={handleStyle} />}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
>
|
|
1249
|
-
{
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
</View>
|
|
1253
|
-
)}
|
|
1254
|
-
{/* Content lays out naturally like native — fill only for auto
|
|
1255
|
-
detents with a plugged scrollable, where natural layout is
|
|
1256
|
-
circular (sheet height derives from the scroll content size).
|
|
1257
|
-
Mirrors the native shadow node's scrollableBounded behavior. */}
|
|
1366
|
+
<div
|
|
1367
|
+
ref={sizedLayoutRef}
|
|
1368
|
+
style={sizedLayoutStyle}
|
|
1369
|
+
data-vaul-scroll-locked={isScrollLocked ? '' : undefined}
|
|
1370
|
+
>
|
|
1371
|
+
<TrueSheetBackground background={background} backgroundStyle={backgroundStyle} />
|
|
1372
|
+
{header && (
|
|
1373
|
+
<View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
|
|
1374
|
+
{isValidElement(header) ? header : createElement(header)}
|
|
1375
|
+
</View>
|
|
1376
|
+
)}
|
|
1377
|
+
<View ref={contentRef} style={[contentStyle, style]} onLayout={handleContentLayout}>
|
|
1378
|
+
{children}
|
|
1379
|
+
</View>
|
|
1380
|
+
{footer && !absoluteFooter && (
|
|
1258
1381
|
<View
|
|
1259
|
-
ref={
|
|
1260
|
-
style={
|
|
1382
|
+
ref={footerElRef}
|
|
1383
|
+
style={[relativeFooterStyle, resolvedFooterStyle]}
|
|
1384
|
+
onLayout={handleFooterLayout}
|
|
1261
1385
|
>
|
|
1262
|
-
{
|
|
1386
|
+
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1263
1387
|
</View>
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
ref={footerElRef}
|
|
1267
|
-
style={[relativeFooterStyle, resolvedFooterStyle]}
|
|
1268
|
-
onLayout={handleFooterLayout}
|
|
1269
|
-
>
|
|
1270
|
-
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1271
|
-
</View>
|
|
1272
|
-
)}
|
|
1273
|
-
</div>
|
|
1274
|
-
) : (
|
|
1275
|
-
// Natural flow so vaul can measure content height — required for
|
|
1276
|
-
// 'auto' detents and form-sheet content-fit sizing.
|
|
1277
|
-
<>
|
|
1278
|
-
{header && (
|
|
1279
|
-
<View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
|
|
1280
|
-
{isValidElement(header) ? header : createElement(header)}
|
|
1281
|
-
</View>
|
|
1282
|
-
)}
|
|
1283
|
-
<View ref={contentRef} style={style}>
|
|
1284
|
-
{children}
|
|
1285
|
-
</View>
|
|
1286
|
-
{footer && !absoluteFooter && (
|
|
1287
|
-
<View ref={footerElRef} style={resolvedFooterStyle} onLayout={handleFooterLayout}>
|
|
1288
|
-
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1289
|
-
</View>
|
|
1290
|
-
)}
|
|
1291
|
-
</>
|
|
1292
|
-
)}
|
|
1388
|
+
)}
|
|
1389
|
+
</div>
|
|
1293
1390
|
</Drawer.Content>
|
|
1294
1391
|
</Drawer.Portal>
|
|
1295
1392
|
</Drawer.Root>
|
|
@@ -1297,6 +1394,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1297
1394
|
);
|
|
1298
1395
|
});
|
|
1299
1396
|
|
|
1397
|
+
const DEFAULT_DIM_OPACITY = 0.5;
|
|
1398
|
+
|
|
1300
1399
|
const overlayStyle: React.CSSProperties = {
|
|
1301
1400
|
position: 'fixed',
|
|
1302
1401
|
inset: 0,
|
|
@@ -1327,12 +1426,11 @@ const sizedLayoutStyle: React.CSSProperties = {
|
|
|
1327
1426
|
height: SIZED_LAYOUT_HEIGHT,
|
|
1328
1427
|
display: 'flex',
|
|
1329
1428
|
flexDirection: 'column',
|
|
1429
|
+
boxSizing: 'border-box',
|
|
1430
|
+
paddingBottom: 'inherit',
|
|
1330
1431
|
};
|
|
1331
1432
|
|
|
1332
|
-
const
|
|
1333
|
-
flex: 1,
|
|
1334
|
-
minHeight: 0,
|
|
1335
|
-
} as const;
|
|
1433
|
+
const contentStyle = { flexShrink: 1, minHeight: 0 } as const;
|
|
1336
1434
|
|
|
1337
1435
|
const visuallyHiddenStyle: React.CSSProperties = {
|
|
1338
1436
|
position: 'absolute',
|
|
@@ -1346,6 +1444,20 @@ const visuallyHiddenStyle: React.CSSProperties = {
|
|
|
1346
1444
|
border: 0,
|
|
1347
1445
|
};
|
|
1348
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
|
+
|
|
1349
1461
|
const STATIC_METHOD_ERROR =
|
|
1350
1462
|
'Static methods are not supported on web. Use the useTrueSheet() hook instead.';
|
|
1351
1463
|
|