@lodev09/react-native-true-sheet 4.0.0-beta.2 → 4.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -4
- package/android/build.gradle +22 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +302 -184
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +21 -110
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +116 -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 +18 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +119 -50
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +360 -163
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +54 -12
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +22 -9
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +84 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -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/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +110 -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 +9 -3
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +3 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +4 -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 +27 -7
- package/ios/TrueSheetContainerView.mm +31 -11
- package/ios/TrueSheetContentView.h +43 -15
- package/ios/TrueSheetContentView.mm +211 -189
- package/ios/TrueSheetFooterView.h +6 -0
- package/ios/TrueSheetFooterView.mm +93 -17
- package/ios/TrueSheetModule.h +2 -7
- package/ios/TrueSheetModule.mm +7 -16
- package/ios/TrueSheetOverlayView.h +32 -0
- package/ios/TrueSheetOverlayView.mm +161 -0
- package/ios/TrueSheetView.mm +212 -55
- package/ios/TrueSheetViewController.h +31 -3
- package/ios/TrueSheetViewController.mm +393 -94
- package/ios/core/TrueSheetBlurView.h +0 -2
- package/ios/core/TrueSheetBlurView.mm +3 -36
- package/ios/core/TrueSheetDetentCalculator.h +7 -1
- package/ios/core/TrueSheetDetentCalculator.mm +27 -4
- package/ios/core/TrueSheetGrabberView.mm +13 -3
- package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
- 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 +173 -39
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +218 -243
- 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/detentBackgrounds.js +21 -0
- package/lib/module/detentBackgrounds.js.map +1 -0
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +14 -10
- 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 +0 -9
- 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/index.js +10 -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/web/constants.js +1 -1
- package/lib/module/web/constants.js.map +1 -1
- package/lib/module/web/layout.js +168 -0
- package/lib/module/web/layout.js.map +1 -0
- package/lib/module/web/vaul/constants.js +4 -0
- package/lib/module/web/vaul/constants.js.map +1 -1
- 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 +44 -6
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +6 -8
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +6 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +135 -34
- 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/detentBackgrounds.d.ts +10 -0
- package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
- 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 +11 -7
- 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 +2 -13
- 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/index.d.ts +8 -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 +41 -5
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- 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/layout.d.ts +19 -0
- package/lib/typescript/src/web/layout.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/context.d.ts +1 -0
- 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 +7 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +17 -8
- package/react-native.config.js +1 -0
- package/src/TrueSheet.tsx +206 -48
- package/src/TrueSheet.types.ts +142 -35
- package/src/TrueSheet.web.tsx +270 -290
- 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/detentBackgrounds.ts +24 -0
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +14 -10
- 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 +5 -33
- package/src/navigation/expo-router/base-types.ts +18 -0
- package/src/navigation/expo-router/index.ts +28 -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 +75 -22
- package/src/web/constants.ts +1 -1
- package/src/web/layout.ts +196 -0
- package/src/web/vaul/constants.ts +4 -0
- package/src/web/vaul/context.ts +1 -0
- package/src/web/vaul/helpers.ts +5 -0
- package/src/web/vaul/index.tsx +67 -5
- package/src/web/vaul/use-snap-points.ts +6 -14
package/src/TrueSheet.web.tsx
CHANGED
|
@@ -31,15 +31,20 @@ import type {
|
|
|
31
31
|
TrueSheetStaticMethods,
|
|
32
32
|
WillBlurEvent,
|
|
33
33
|
WillDismissEvent,
|
|
34
|
+
DismissAttemptEvent,
|
|
34
35
|
WillFocusEvent,
|
|
35
36
|
WillPresentEvent,
|
|
36
37
|
} from './TrueSheet.types';
|
|
37
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
measurePeekContentHeight,
|
|
40
|
+
TrueSheetPeekContext,
|
|
41
|
+
type TrueSheetPeekContextValue,
|
|
42
|
+
} from './TrueSheetPeek.web';
|
|
38
43
|
import { usePortalContainer, useRegisterSheet, useSheetStack } from './TrueSheetProvider.web';
|
|
39
44
|
import {
|
|
40
45
|
COLOR_SURFACE_CONTAINER_LOW_DARK,
|
|
41
46
|
COLOR_SURFACE_CONTAINER_LOW_LIGHT,
|
|
42
|
-
|
|
47
|
+
DEFAULT_PLACEMENT_OFFSET,
|
|
43
48
|
DEFAULT_CORNER_RADIUS,
|
|
44
49
|
DEFAULT_DETACHED_OFFSET,
|
|
45
50
|
DEFAULT_FORM_SHEET_HEIGHT_RATIO,
|
|
@@ -52,18 +57,21 @@ import {
|
|
|
52
57
|
DEFAULT_MAX_WIDTH,
|
|
53
58
|
} from './web/constants';
|
|
54
59
|
import { getDOMElement } from './web/dom';
|
|
60
|
+
import { TrueSheetBackground } from './TrueSheetBackground';
|
|
61
|
+
import { getDetentBackgroundIndex, normalizeDetentBackground } from './detentBackgrounds';
|
|
62
|
+
import { observeSheetLayout } from './web/layout';
|
|
55
63
|
import { Drawer } from './web/vaul';
|
|
56
|
-
import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from './web/vaul/constants';
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
64
|
+
import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from './web/vaul/constants';
|
|
65
|
+
|
|
66
|
+
// Resolves the user's `scrollableRef` to its scrollable DOM element — RN-web
|
|
67
|
+
// ScrollView refs expose the scroll node directly (with helpers attached);
|
|
68
|
+
// list refs (e.g. FlatList) expose it via getScrollableNode(). The node's
|
|
69
|
+
// first child is the content container.
|
|
70
|
+
const getScrollableElement = (scrollable: unknown): HTMLElement | null => {
|
|
71
|
+
if (!scrollable) return null;
|
|
72
|
+
if (scrollable instanceof HTMLElement) return scrollable;
|
|
73
|
+
const node = (scrollable as { getScrollableNode?: () => unknown }).getScrollableNode?.();
|
|
74
|
+
return node instanceof HTMLElement ? node : null;
|
|
67
75
|
};
|
|
68
76
|
|
|
69
77
|
const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props, ref) => {
|
|
@@ -75,10 +83,15 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
75
83
|
cornerRadius,
|
|
76
84
|
style,
|
|
77
85
|
backgroundColor: backgroundColorProp,
|
|
86
|
+
detentBackgrounds,
|
|
87
|
+
background,
|
|
88
|
+
backgroundStyle,
|
|
78
89
|
maxContentHeight,
|
|
79
90
|
maxContentWidth,
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
placement = 'automatic',
|
|
92
|
+
placementOffset = DEFAULT_PLACEMENT_OFFSET,
|
|
93
|
+
scrollableRef,
|
|
94
|
+
scrollableOptions,
|
|
82
95
|
grabber = true,
|
|
83
96
|
grabberOptions,
|
|
84
97
|
accessibilityOptions,
|
|
@@ -103,6 +116,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
103
116
|
onDidPresent,
|
|
104
117
|
onWillDismiss,
|
|
105
118
|
onDidDismiss,
|
|
119
|
+
onDismissAttempt,
|
|
106
120
|
onDetentChange,
|
|
107
121
|
onDragBegin,
|
|
108
122
|
onDragChange,
|
|
@@ -143,12 +157,20 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
143
157
|
const effectiveDetached = isFormSheet || detached;
|
|
144
158
|
|
|
145
159
|
const colorScheme = useColorScheme();
|
|
160
|
+
const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
|
|
161
|
+
const [isOpen, setIsOpen] = useState(shouldAutoPresent);
|
|
162
|
+
const isOpenRef = useRef(isOpen);
|
|
163
|
+
isOpenRef.current = isOpen;
|
|
164
|
+
const isPresentingRef = useRef(shouldAutoPresent);
|
|
165
|
+
const [displayedBackgroundIndex, setDisplayedBackgroundIndex] = useState(
|
|
166
|
+
shouldAutoPresent ? initialDetentIndex : 0
|
|
167
|
+
);
|
|
146
168
|
const backgroundColor =
|
|
169
|
+
normalizeDetentBackground(
|
|
170
|
+
detentBackgrounds?.[Math.min(displayedBackgroundIndex, validDetents.length - 1)]
|
|
171
|
+
)?.color ??
|
|
147
172
|
backgroundColorProp ??
|
|
148
173
|
(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
174
|
const [activeSnapPoint, setActiveSnapPoint] = useState<SheetDetent | null>(
|
|
153
175
|
() => validDetents[shouldAutoPresent ? initialDetentIndex : 0] ?? null
|
|
154
176
|
);
|
|
@@ -164,8 +186,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
164
186
|
const validDetentsRef = useRef(validDetents);
|
|
165
187
|
validDetentsRef.current = validDetents;
|
|
166
188
|
|
|
167
|
-
const hasAutoDetent = validDetents.includes('auto');
|
|
168
|
-
|
|
169
189
|
const handleSetActiveSnapPoint = useCallback((snapPoint: number | string | null) => {
|
|
170
190
|
setActiveSnapPoint(
|
|
171
191
|
snapPoint == null
|
|
@@ -196,19 +216,34 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
196
216
|
e.preventDefault();
|
|
197
217
|
return;
|
|
198
218
|
}
|
|
219
|
+
if (!(target instanceof Element)) return;
|
|
220
|
+
const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
|
|
221
|
+
if (!wrapper) return;
|
|
199
222
|
// The footer is rendered via vaul's `detachedSiblings` as a sibling of
|
|
200
223
|
// Drawer.Content inside [data-vaul-detached-wrapper], so Radix treats
|
|
201
224
|
// clicks on it as "outside" the content. Don't dismiss for clicks that
|
|
202
225
|
// landed inside the wrapper.
|
|
203
|
-
if (target
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
e.preventDefault();
|
|
207
|
-
}
|
|
226
|
+
if (wrapper.contains(target)) {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
return;
|
|
208
229
|
}
|
|
230
|
+
// Every sheet runs Radix non-modal and portals its overlay + wrapper as
|
|
231
|
+
// siblings into the shared container, so a press on a sheet stacked above
|
|
232
|
+
// this one (its content or its dim overlay) is also delivered here as
|
|
233
|
+
// "outside". Sheets present in document order, so anything after our
|
|
234
|
+
// wrapper belongs to a descendant. Radix may dispatch on the deferred
|
|
235
|
+
// click (touch), after a dismissing descendant has left the DOM — treat
|
|
236
|
+
// disconnected as above too.
|
|
237
|
+
/* eslint-disable no-bitwise */
|
|
238
|
+
const above = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_DISCONNECTED;
|
|
239
|
+
if (wrapper.compareDocumentPosition(target) & above) {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
}
|
|
242
|
+
/* eslint-enable no-bitwise */
|
|
209
243
|
};
|
|
210
244
|
|
|
211
245
|
const dismissAboveRef = useRef<(animated?: boolean) => Promise<void>>(async () => {});
|
|
246
|
+
const dismissDescendantsRef = useRef<() => void>(() => {});
|
|
212
247
|
|
|
213
248
|
const methods = useMemo<TrueSheetMethods>(
|
|
214
249
|
() => ({
|
|
@@ -219,6 +254,10 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
219
254
|
`TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`
|
|
220
255
|
);
|
|
221
256
|
}
|
|
257
|
+
if (!isOpenRef.current) {
|
|
258
|
+
isPresentingRef.current = true;
|
|
259
|
+
setDisplayedBackgroundIndex(index);
|
|
260
|
+
}
|
|
222
261
|
setActiveSnapPoint(detent);
|
|
223
262
|
setIsOpen(true);
|
|
224
263
|
},
|
|
@@ -272,15 +311,45 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
272
311
|
// within the content — measured by the peek against `contentRef`.
|
|
273
312
|
const [peekContentHeight, setPeekContentHeight] = useState(0);
|
|
274
313
|
const contentRef = useRef<View>(null);
|
|
275
|
-
const peekElRef = useRef<View>(null);
|
|
276
|
-
|
|
314
|
+
const peekElRef = useRef<View | null>(null);
|
|
315
|
+
|
|
316
|
+
const measurePeek = useCallback(() => {
|
|
317
|
+
const peekEl = getDOMElement(peekElRef.current);
|
|
318
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
319
|
+
if (peekEl && contentEl) {
|
|
320
|
+
setPeekContentHeight(measurePeekContentHeight(peekEl, contentEl));
|
|
321
|
+
}
|
|
322
|
+
}, []);
|
|
323
|
+
|
|
324
|
+
// Last-attached wins — during a screen swap the incoming peek mounts before
|
|
325
|
+
// the outgoing one unmounts, so the outgoing detach must not clear it.
|
|
326
|
+
const peekContext = useMemo<TrueSheetPeekContextValue>(
|
|
327
|
+
() => ({
|
|
328
|
+
contentRef,
|
|
329
|
+
attachPeek: (view) => {
|
|
330
|
+
peekElRef.current = view;
|
|
331
|
+
measurePeek();
|
|
332
|
+
},
|
|
333
|
+
detachPeek: (view) => {
|
|
334
|
+
if (peekElRef.current !== view) return;
|
|
335
|
+
peekElRef.current = null;
|
|
336
|
+
setPeekContentHeight(0);
|
|
337
|
+
},
|
|
338
|
+
measurePeek: (view) => {
|
|
339
|
+
if (peekElRef.current === view) measurePeek();
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
[measurePeek]
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// The peek's own onLayout only fires when *it* resizes. Content added above
|
|
346
|
+
// it moves it without resizing it, so re-measure its offset whenever the
|
|
347
|
+
// content view's layout changes.
|
|
348
|
+
const handleContentLayout = measurePeek;
|
|
277
349
|
|
|
278
350
|
// An absolute (floating) header overlaps the content, so it contributes no
|
|
279
351
|
// height to the 'auto' detent measurement.
|
|
280
352
|
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
281
|
-
const absoluteHeaderRef = useRef(absoluteHeader);
|
|
282
|
-
absoluteHeaderRef.current = absoluteHeader;
|
|
283
|
-
|
|
284
353
|
const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
|
|
285
354
|
|
|
286
355
|
// Same for an absolute (floating) footer — rendered via vaul's
|
|
@@ -316,109 +385,48 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
316
385
|
(footer && absoluteFooter ? footerHeight : 0) +
|
|
317
386
|
peekContentHeight || DEFAULT_PEEK_HEIGHT;
|
|
318
387
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
// Natural content height (header + content + footer, with a pinned
|
|
329
|
-
// scrollable's viewport replaced by its content size) — the height the
|
|
330
|
-
// content wants regardless of the sheet's bounds.
|
|
331
|
-
const measureNaturalHeight = useCallback(() => {
|
|
332
|
-
const contentEl = getDOMElement(contentRef.current);
|
|
333
|
-
if (!contentEl || !contentEl.isConnected) return 0;
|
|
334
|
-
const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
|
|
335
|
-
const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
|
|
336
|
-
let height =
|
|
337
|
-
(headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
|
|
338
|
-
const scroller = pinnedScrollerRef.current;
|
|
339
|
-
const scrollContent = scroller?.firstElementChild;
|
|
340
|
-
if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
|
|
341
|
-
height += scrollContent.offsetHeight - scroller.clientHeight;
|
|
342
|
-
}
|
|
343
|
-
return Math.max(0, height);
|
|
344
|
-
}, []);
|
|
388
|
+
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
389
|
+
const naturalHeightRef = useRef(0);
|
|
390
|
+
const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
|
|
391
|
+
const footerInsetAdjustment =
|
|
392
|
+
Boolean(footer) &&
|
|
393
|
+
absoluteFooter &&
|
|
394
|
+
(insetBehavior === 'automatic' || insetBehavior === 'footer');
|
|
395
|
+
const measureContent = validDetents.includes('auto') || isFormSheet;
|
|
345
396
|
|
|
346
397
|
useEffect(() => {
|
|
347
|
-
if (!isOpen
|
|
398
|
+
if (!isOpen) return undefined;
|
|
348
399
|
|
|
349
400
|
let canceled = false;
|
|
350
401
|
let rafId = 0;
|
|
351
|
-
let
|
|
352
|
-
let resizeObserver: ResizeObserver | null = null;
|
|
402
|
+
let observer: ReturnType<typeof observeSheetLayout> | undefined;
|
|
353
403
|
|
|
354
404
|
const attach = () => {
|
|
355
405
|
if (canceled) return;
|
|
356
|
-
const
|
|
357
|
-
|
|
406
|
+
const layoutEl = sizedLayoutRef.current;
|
|
407
|
+
const drawer = drawerContentRef.current;
|
|
408
|
+
if (!drawer || !layoutEl?.isConnected) {
|
|
358
409
|
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
359
410
|
rafId = window.requestAnimationFrame(attach);
|
|
360
411
|
return;
|
|
361
412
|
}
|
|
362
413
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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 });
|
|
414
|
+
observer = observeSheetLayout({
|
|
415
|
+
drawer,
|
|
416
|
+
layout: layoutEl,
|
|
417
|
+
getContent: () => getDOMElement(contentRef.current),
|
|
418
|
+
getHeader: () => getDOMElement(headerElRef.current),
|
|
419
|
+
getFooter: () => getDOMElement(footerElRef.current),
|
|
420
|
+
getScrollable: () => getScrollableElement(scrollableRef?.current),
|
|
421
|
+
absoluteHeader,
|
|
422
|
+
absoluteFooter,
|
|
423
|
+
footerInsetAdjustment,
|
|
424
|
+
measureContent,
|
|
425
|
+
onHeightChange: (height) => {
|
|
426
|
+
naturalHeightRef.current = height;
|
|
427
|
+
setMeasuredContentHeight(height);
|
|
428
|
+
},
|
|
429
|
+
});
|
|
422
430
|
};
|
|
423
431
|
|
|
424
432
|
rafId = window.requestAnimationFrame(attach);
|
|
@@ -426,10 +434,16 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
426
434
|
return () => {
|
|
427
435
|
canceled = true;
|
|
428
436
|
window.cancelAnimationFrame(rafId);
|
|
429
|
-
|
|
430
|
-
resizeObserver?.disconnect();
|
|
437
|
+
observer?.disconnect();
|
|
431
438
|
};
|
|
432
|
-
}, [
|
|
439
|
+
}, [
|
|
440
|
+
isOpen,
|
|
441
|
+
absoluteHeader,
|
|
442
|
+
absoluteFooter,
|
|
443
|
+
footerInsetAdjustment,
|
|
444
|
+
measureContent,
|
|
445
|
+
scrollableRef,
|
|
446
|
+
]);
|
|
433
447
|
|
|
434
448
|
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
435
449
|
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
@@ -437,13 +451,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
437
451
|
const isScrollLocked =
|
|
438
452
|
validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
|
|
439
453
|
|
|
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
454
|
const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
|
|
448
455
|
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
449
456
|
if (!isFormSheet) return undefined;
|
|
@@ -510,7 +517,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
510
517
|
effectiveMaxContentHeight,
|
|
511
518
|
peekHeight,
|
|
512
519
|
peekContentHeight,
|
|
513
|
-
measuredContentHeight,
|
|
514
520
|
});
|
|
515
521
|
geometryInputsRef.current = {
|
|
516
522
|
effectiveDetached,
|
|
@@ -518,7 +524,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
518
524
|
effectiveMaxContentHeight,
|
|
519
525
|
peekHeight,
|
|
520
526
|
peekContentHeight,
|
|
521
|
-
measuredContentHeight,
|
|
522
527
|
};
|
|
523
528
|
|
|
524
529
|
const computeDetentGeometry = useCallback(() => {
|
|
@@ -532,12 +537,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
532
537
|
inputs.effectiveMaxContentHeight !== undefined
|
|
533
538
|
? Math.min(effectiveH, inputs.effectiveMaxContentHeight)
|
|
534
539
|
: 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;
|
|
540
|
+
// Reuse the intrinsic measurement during position events so animation
|
|
541
|
+
// frames don't clone the content tree again.
|
|
542
|
+
const measuredHeight = naturalHeightRef.current;
|
|
541
543
|
const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
|
|
542
544
|
|
|
543
545
|
// 'peek' is measured live from the DOM (offsetHeight is layout-based, so
|
|
@@ -573,7 +575,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
573
575
|
values.push(effectiveH > 0 ? h / effectiveH : 0);
|
|
574
576
|
}
|
|
575
577
|
return { windowH, effectiveH, ceiling, positions, values };
|
|
576
|
-
}, [
|
|
578
|
+
}, []);
|
|
577
579
|
|
|
578
580
|
// Detent info for lifecycle events. Position/detent come from the active
|
|
579
581
|
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
@@ -652,54 +654,27 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
652
654
|
[computeDetentGeometry]
|
|
653
655
|
);
|
|
654
656
|
|
|
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
657
|
const sizedLayoutRef = useRef<HTMLDivElement>(null);
|
|
661
658
|
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
659
|
|
|
692
660
|
const handlePositionChange = useCallback(
|
|
693
661
|
(position: number) => {
|
|
694
|
-
if (isDraggingRef.current || isSettlingAfterDragRef.current) {
|
|
695
|
-
updateSizedLayoutHeight(position);
|
|
696
|
-
}
|
|
697
662
|
const { index, detent } = interpolateFromPosition(position);
|
|
663
|
+
if (isOpenRef.current && !isPresentingRef.current) {
|
|
664
|
+
const { effectiveH, positions } = computeDetentGeometry();
|
|
665
|
+
setDisplayedBackgroundIndex((current) =>
|
|
666
|
+
getDetentBackgroundIndex(
|
|
667
|
+
effectiveH - position,
|
|
668
|
+
positions.map((top) => effectiveH - top),
|
|
669
|
+
current
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
698
673
|
onPositionChangeRef.current?.({
|
|
699
674
|
nativeEvent: { index, position, detent, realtime: true },
|
|
700
675
|
} as PositionChangeEvent);
|
|
701
676
|
},
|
|
702
|
-
[interpolateFromPosition,
|
|
677
|
+
[interpolateFromPosition, computeDetentGeometry]
|
|
703
678
|
);
|
|
704
679
|
|
|
705
680
|
// Fire onMount once after first render. React-mount is the earliest point
|
|
@@ -726,6 +701,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
726
701
|
|
|
727
702
|
const present = isOpen;
|
|
728
703
|
if (!present) {
|
|
704
|
+
// Mirror native: dismissing a sheet takes every sheet stacked above it
|
|
705
|
+
// (iOS dismisses from the presenter; Android runs dismissStack first).
|
|
706
|
+
dismissDescendantsRef.current();
|
|
729
707
|
// Pair willBlur with willDismiss on dismiss — mirrors native iOS
|
|
730
708
|
// emitWillDismissEvents (blur fires before dismiss). willPresent is
|
|
731
709
|
// deferred to `start()` below (needs the mounted drawer's geometry).
|
|
@@ -735,6 +713,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
735
713
|
|
|
736
714
|
const fireDone = () => {
|
|
737
715
|
if (present) {
|
|
716
|
+
isPresentingRef.current = false;
|
|
738
717
|
onDidPresentRef.current?.({ nativeEvent: computeDetentInfo() } as DidPresentEvent);
|
|
739
718
|
onDidFocusRef.current?.({ nativeEvent: null } as DidFocusEvent);
|
|
740
719
|
} else {
|
|
@@ -824,17 +803,11 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
824
803
|
}
|
|
825
804
|
onDragChangeRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragChangeEvent);
|
|
826
805
|
}, [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
|
-
);
|
|
806
|
+
const handleRelease = useCallback(() => {
|
|
807
|
+
if (!isDraggingRef.current) return;
|
|
808
|
+
isDraggingRef.current = false;
|
|
809
|
+
onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
|
|
810
|
+
}, [computeDetentInfo]);
|
|
838
811
|
|
|
839
812
|
const { isNested, dismissAbove, descendants } = useSheetStack(
|
|
840
813
|
methodsRef,
|
|
@@ -843,10 +816,18 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
843
816
|
isFormSheet
|
|
844
817
|
);
|
|
845
818
|
dismissAboveRef.current = dismissAbove;
|
|
819
|
+
// Captured per render: by the time the dismiss effect runs, this sheet has
|
|
820
|
+
// already been popped from the stack, so `dismissAbove` can't find it.
|
|
821
|
+
dismissDescendantsRef.current = () => {
|
|
822
|
+
for (let i = descendants.length - 1; i >= 0; i--) {
|
|
823
|
+
descendants[i]!.ref.current?.dismiss();
|
|
824
|
+
}
|
|
825
|
+
};
|
|
846
826
|
|
|
847
827
|
// Mirror Android: translate this sheet down to match the deepest descendant's
|
|
848
828
|
// top so the whole stack visually aligns. Cascades because every ancestor
|
|
849
829
|
// re-runs whenever the stack (and thus its descendants) changes.
|
|
830
|
+
const wasCascadedRef = useRef(false);
|
|
850
831
|
useEffect(() => {
|
|
851
832
|
const parent = drawerContentRef.current;
|
|
852
833
|
if (!parent) return;
|
|
@@ -858,7 +839,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
858
839
|
if (!isOpen) return;
|
|
859
840
|
const parentWrapper = parent.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
860
841
|
|
|
861
|
-
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
842
|
+
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
862
843
|
const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
863
844
|
const CLIP_NONE = 'inset(0px round 0px)';
|
|
864
845
|
|
|
@@ -882,23 +863,32 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
882
863
|
};
|
|
883
864
|
|
|
884
865
|
if (descendants.length === 0) {
|
|
866
|
+
if (!wasCascadedRef.current) return;
|
|
867
|
+
wasCascadedRef.current = false;
|
|
885
868
|
parent.style.transition = transition;
|
|
886
869
|
parent.style.transform = '';
|
|
887
870
|
setClip(CLIP_NONE);
|
|
888
871
|
return;
|
|
889
872
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
873
|
+
wasCascadedRef.current = true;
|
|
874
|
+
|
|
875
|
+
// Track the immediate child's *actual* top: its cascade transform when it
|
|
876
|
+
// was itself pushed behind a grandchild, else its own snap point. Mirrors
|
|
877
|
+
// Android's addTranslation propagating extra translation up the stack —
|
|
878
|
+
// a grandchild opening pushes this sheet only as far as our child moved.
|
|
879
|
+
const readTranslateY = (el: HTMLElement) => {
|
|
880
|
+
const match = el.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
|
|
881
|
+
return match ? Number.parseFloat(match[1]!) : null;
|
|
882
|
+
};
|
|
895
883
|
const computeTargetY = () => {
|
|
896
884
|
const parentSnap =
|
|
897
885
|
Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
898
886
|
const node = descendants[0]?.nodeRef.current;
|
|
899
887
|
if (!node) return parentSnap;
|
|
900
|
-
const
|
|
901
|
-
|
|
888
|
+
const childTop =
|
|
889
|
+
readTranslateY(node) ??
|
|
890
|
+
(Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0);
|
|
891
|
+
return Math.max(parentSnap, childTop);
|
|
902
892
|
};
|
|
903
893
|
|
|
904
894
|
// When a form-sheet parent has a form-sheet descendant, clip the parent to
|
|
@@ -934,18 +924,24 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
934
924
|
|
|
935
925
|
const apply = () => {
|
|
936
926
|
applyFormClip();
|
|
937
|
-
// Mirror
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
|
|
927
|
+
// Mirror iPad/Android (`isExpanded`): a parent at full height (detent 1)
|
|
928
|
+
// stays put and the child simply overlays it. Full height is the sheet's
|
|
929
|
+
// own ceiling — the viewport for a page sheet, the capped content-fit
|
|
930
|
+
// height for a form sheet — so compare against where detent 1 lands.
|
|
931
|
+
const parentSnap =
|
|
932
|
+
Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
933
|
+
const { effectiveH, ceiling } = computeDetentGeometry();
|
|
934
|
+
if (parentSnap <= effectiveH - ceiling + 0.5) {
|
|
942
935
|
parent.style.transition = transition;
|
|
943
936
|
parent.style.transform = '';
|
|
944
937
|
return;
|
|
945
938
|
}
|
|
939
|
+
// Mirror Android: stay put while the child is being dragged (its live
|
|
940
|
+
// transform would drag us along during drag-to-dismiss). The release
|
|
941
|
+
// snap rewrites the child's style and re-applies.
|
|
942
|
+
if (descendants[0]?.nodeRef.current?.classList.contains(DRAG_CLASS)) return;
|
|
946
943
|
const targetY = computeTargetY();
|
|
947
|
-
const
|
|
948
|
-
const currentY = match ? Number.parseFloat(match[1]!) : 0;
|
|
944
|
+
const currentY = readTranslateY(parent) ?? 0;
|
|
949
945
|
if (Math.abs(currentY - targetY) < 0.5) return;
|
|
950
946
|
parent.style.transition = transition;
|
|
951
947
|
parent.style.transform = `translate3d(0, ${targetY}px, 0)`;
|
|
@@ -957,12 +953,15 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
957
953
|
// inline style changes.
|
|
958
954
|
const observer = new MutationObserver(apply);
|
|
959
955
|
observer.observe(parent, { attributes: true, attributeFilter: ['style'] });
|
|
956
|
+
// The child's own cascade lands in a later frame — follow it when it moves.
|
|
957
|
+
const childNode = descendants[0]?.nodeRef.current;
|
|
958
|
+
if (childNode) observer.observe(childNode, { attributes: true, attributeFilter: ['style'] });
|
|
960
959
|
|
|
961
960
|
return () => {
|
|
962
961
|
cancelAnimationFrame(raf);
|
|
963
962
|
observer.disconnect();
|
|
964
963
|
};
|
|
965
|
-
}, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen]);
|
|
964
|
+
}, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen, computeDetentGeometry]);
|
|
966
965
|
|
|
967
966
|
// Focus/blur events fire when a descendant sheet appears on top of this one
|
|
968
967
|
// (blur) or when all descendants are dismissed (focus). will-events fire
|
|
@@ -973,12 +972,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
973
972
|
const onDidBlurRef = useRef(onDidBlur);
|
|
974
973
|
const onWillFocusRef = useRef(onWillFocus);
|
|
975
974
|
const onDidFocusRef = useRef(onDidFocus);
|
|
975
|
+
const onDismissAttemptRef = useRef(onDismissAttempt);
|
|
976
976
|
useEffect(() => {
|
|
977
977
|
onWillBlurRef.current = onWillBlur;
|
|
978
978
|
onDidBlurRef.current = onDidBlur;
|
|
979
979
|
onWillFocusRef.current = onWillFocus;
|
|
980
980
|
onDidFocusRef.current = onDidFocus;
|
|
981
|
+
onDismissAttemptRef.current = onDismissAttempt;
|
|
981
982
|
});
|
|
983
|
+
const handleDismissAttempt = useCallback(() => {
|
|
984
|
+
onDismissAttemptRef.current?.({ nativeEvent: null } as DismissAttemptEvent);
|
|
985
|
+
}, []);
|
|
982
986
|
|
|
983
987
|
const prevDescendantCountRef = useRef(0);
|
|
984
988
|
useEffect(() => {
|
|
@@ -1026,11 +1030,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1026
1030
|
};
|
|
1027
1031
|
}, [isOpen, descendants.length]);
|
|
1028
1032
|
|
|
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
1033
|
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
1035
1034
|
|
|
1036
1035
|
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
@@ -1048,6 +1047,14 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1048
1047
|
? `drop-shadow(0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15))`
|
|
1049
1048
|
: undefined;
|
|
1050
1049
|
|
|
1050
|
+
const backgroundLayerStyle: React.CSSProperties = {
|
|
1051
|
+
position: 'absolute',
|
|
1052
|
+
inset: 0,
|
|
1053
|
+
pointerEvents: 'none',
|
|
1054
|
+
backgroundColor: backgroundColor as string,
|
|
1055
|
+
transition: isPresentingRef.current ? 'none' : 'background-color 200ms',
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1051
1058
|
const mergedContentStyle = useMemo<React.CSSProperties>(
|
|
1052
1059
|
() => ({
|
|
1053
1060
|
position: 'fixed',
|
|
@@ -1059,7 +1066,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1059
1066
|
flexDirection: 'column',
|
|
1060
1067
|
borderTopLeftRadius: effectiveCornerRadius,
|
|
1061
1068
|
borderTopRightRadius: effectiveCornerRadius,
|
|
1062
|
-
backgroundColor: backgroundColor as string,
|
|
1063
1069
|
// Clip children to the rounded top so headers/content with their own
|
|
1064
1070
|
// background don't bleed past the corners. `clip` (not `hidden`): a
|
|
1065
1071
|
// hidden box is still a scroll container, so browser focus-reveal (e.g.
|
|
@@ -1069,12 +1075,24 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1069
1075
|
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
1070
1076
|
// A relative footer owns the inset instead (see resolvedFooterStyle); an
|
|
1071
1077
|
// absolute footer floats over the content, so the content keeps its lift.
|
|
1078
|
+
// A plugged scrollable keeps the lift too (contentInsetAdjustment
|
|
1079
|
+
// 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
|
|
1080
|
+
// behind the indicator with user-applied content padding.
|
|
1072
1081
|
paddingBottom:
|
|
1073
|
-
insetAdjustment === 'automatic' &&
|
|
1082
|
+
insetAdjustment === 'automatic' &&
|
|
1083
|
+
!(footerOwnsInset && !absoluteFooter) &&
|
|
1084
|
+
(!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area')
|
|
1074
1085
|
? 'env(safe-area-inset-bottom, 0px)'
|
|
1075
1086
|
: 0,
|
|
1076
1087
|
}),
|
|
1077
|
-
[
|
|
1088
|
+
[
|
|
1089
|
+
effectiveCornerRadius,
|
|
1090
|
+
insetAdjustment,
|
|
1091
|
+
footerOwnsInset,
|
|
1092
|
+
absoluteFooter,
|
|
1093
|
+
scrollableRef,
|
|
1094
|
+
insetBehavior,
|
|
1095
|
+
]
|
|
1078
1096
|
);
|
|
1079
1097
|
|
|
1080
1098
|
const defaultGrabberColor =
|
|
@@ -1112,7 +1130,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1112
1130
|
// `maxContentWidth` is ignored ('form' is absolute).
|
|
1113
1131
|
// - presentation='page' → `maxContentWidth` (any viewport) or
|
|
1114
1132
|
// DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
|
|
1115
|
-
// Detached without a width constraint applies
|
|
1133
|
+
// Detached without a width constraint applies placementOffset on both edges so
|
|
1116
1134
|
// the floating card breathes from the viewport sides.
|
|
1117
1135
|
const wrapperStyle = useMemo<React.CSSProperties | undefined>(() => {
|
|
1118
1136
|
// Mobile portrait ignores width sizing entirely (matches iOS/Android:
|
|
@@ -1131,30 +1149,30 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1131
1149
|
if (dropShadow) next.filter = dropShadow;
|
|
1132
1150
|
if (!needsMargins) return next;
|
|
1133
1151
|
|
|
1134
|
-
let
|
|
1135
|
-
let
|
|
1152
|
+
let marginInlineStart: number | string;
|
|
1153
|
+
let marginInlineEnd: number | string;
|
|
1136
1154
|
if (isFormSheet) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1155
|
+
marginInlineStart = 'auto';
|
|
1156
|
+
marginInlineEnd = 'auto';
|
|
1139
1157
|
} else if (maxWidth == null) {
|
|
1140
|
-
|
|
1141
|
-
|
|
1158
|
+
marginInlineStart = placementOffset;
|
|
1159
|
+
marginInlineEnd = placementOffset;
|
|
1142
1160
|
} else {
|
|
1143
|
-
|
|
1144
|
-
|
|
1161
|
+
marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
|
|
1162
|
+
marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
|
|
1145
1163
|
}
|
|
1146
1164
|
|
|
1147
1165
|
if (maxWidth != null) next.maxWidth = maxWidth;
|
|
1148
|
-
next.
|
|
1149
|
-
next.
|
|
1166
|
+
next.marginInlineStart = marginInlineStart;
|
|
1167
|
+
next.marginInlineEnd = marginInlineEnd;
|
|
1150
1168
|
return next;
|
|
1151
1169
|
}, [
|
|
1152
1170
|
isLandscapeOrTablet,
|
|
1153
1171
|
isFormSheet,
|
|
1154
1172
|
maxContentWidth,
|
|
1155
1173
|
presentation,
|
|
1156
|
-
|
|
1157
|
-
|
|
1174
|
+
placement,
|
|
1175
|
+
placementOffset,
|
|
1158
1176
|
effectiveDetached,
|
|
1159
1177
|
dropShadow,
|
|
1160
1178
|
]);
|
|
@@ -1191,6 +1209,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1191
1209
|
onDrag={handleDrag}
|
|
1192
1210
|
onRelease={handleRelease}
|
|
1193
1211
|
dismissible={dismissible}
|
|
1212
|
+
onDismissAttempt={handleDismissAttempt}
|
|
1194
1213
|
draggable={draggable}
|
|
1195
1214
|
repositionInputs={false}
|
|
1196
1215
|
modal={dimmed}
|
|
@@ -1202,14 +1221,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1202
1221
|
peekHeight={peekHeight}
|
|
1203
1222
|
initialAnimated={initialDetentAnimated}
|
|
1204
1223
|
detachedWrapperStyle={wrapperStyle}
|
|
1205
|
-
|
|
1206
|
-
contentHeight={
|
|
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
|
-
}
|
|
1224
|
+
contentHeight={measuredContentHeight}
|
|
1213
1225
|
activeSnapPoint={activeSnapPoint}
|
|
1214
1226
|
setActiveSnapPoint={handleSetActiveSnapPoint}
|
|
1215
1227
|
{...snapPointsProps}
|
|
@@ -1230,66 +1242,35 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1230
1242
|
) : undefined
|
|
1231
1243
|
}
|
|
1232
1244
|
>
|
|
1245
|
+
<div aria-hidden style={backgroundLayerStyle} />
|
|
1233
1246
|
<Drawer.Title style={visuallyHiddenStyle}>
|
|
1234
1247
|
{accessibilityOptions?.paneTitle ?? 'Sheet'}
|
|
1235
1248
|
</Drawer.Title>
|
|
1236
1249
|
{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. */}
|
|
1250
|
+
<div
|
|
1251
|
+
ref={sizedLayoutRef}
|
|
1252
|
+
style={sizedLayoutStyle}
|
|
1253
|
+
data-vaul-scroll-locked={isScrollLocked ? '' : undefined}
|
|
1254
|
+
>
|
|
1255
|
+
<TrueSheetBackground background={background} backgroundStyle={backgroundStyle} />
|
|
1256
|
+
{header && (
|
|
1257
|
+
<View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
|
|
1258
|
+
{isValidElement(header) ? header : createElement(header)}
|
|
1259
|
+
</View>
|
|
1260
|
+
)}
|
|
1261
|
+
<View ref={contentRef} style={[contentStyle, style]} onLayout={handleContentLayout}>
|
|
1262
|
+
{children}
|
|
1263
|
+
</View>
|
|
1264
|
+
{footer && !absoluteFooter && (
|
|
1258
1265
|
<View
|
|
1259
|
-
ref={
|
|
1260
|
-
style={
|
|
1266
|
+
ref={footerElRef}
|
|
1267
|
+
style={[relativeFooterStyle, resolvedFooterStyle]}
|
|
1268
|
+
onLayout={handleFooterLayout}
|
|
1261
1269
|
>
|
|
1262
|
-
{
|
|
1263
|
-
</View>
|
|
1264
|
-
{footer && !absoluteFooter && (
|
|
1265
|
-
<View
|
|
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}
|
|
1270
|
+
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1285
1271
|
</View>
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1289
|
-
</View>
|
|
1290
|
-
)}
|
|
1291
|
-
</>
|
|
1292
|
-
)}
|
|
1272
|
+
)}
|
|
1273
|
+
</div>
|
|
1293
1274
|
</Drawer.Content>
|
|
1294
1275
|
</Drawer.Portal>
|
|
1295
1276
|
</Drawer.Root>
|
|
@@ -1327,12 +1308,11 @@ const sizedLayoutStyle: React.CSSProperties = {
|
|
|
1327
1308
|
height: SIZED_LAYOUT_HEIGHT,
|
|
1328
1309
|
display: 'flex',
|
|
1329
1310
|
flexDirection: 'column',
|
|
1311
|
+
boxSizing: 'border-box',
|
|
1312
|
+
paddingBottom: 'inherit',
|
|
1330
1313
|
};
|
|
1331
1314
|
|
|
1332
|
-
const
|
|
1333
|
-
flex: 1,
|
|
1334
|
-
minHeight: 0,
|
|
1335
|
-
} as const;
|
|
1315
|
+
const contentStyle = { flexShrink: 1, minHeight: 0 } as const;
|
|
1336
1316
|
|
|
1337
1317
|
const visuallyHiddenStyle: React.CSSProperties = {
|
|
1338
1318
|
position: 'absolute',
|