@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
|
@@ -5,23 +5,24 @@ import { createElement, forwardRef, isValidElement, useCallback, useEffect, useI
|
|
|
5
5
|
import { StyleSheet, useColorScheme, useWindowDimensions, View } from 'react-native';
|
|
6
6
|
import { measurePeekContentHeight, TrueSheetPeekContext } from "./TrueSheetPeek.web.js";
|
|
7
7
|
import { usePortalContainer, useRegisterSheet, useSheetStack } from "./TrueSheetProvider.web.js";
|
|
8
|
-
import { COLOR_SURFACE_CONTAINER_LOW_DARK, COLOR_SURFACE_CONTAINER_LOW_LIGHT,
|
|
8
|
+
import { COLOR_SURFACE_CONTAINER_LOW_DARK, COLOR_SURFACE_CONTAINER_LOW_LIGHT, DEFAULT_PLACEMENT_OFFSET, DEFAULT_CORNER_RADIUS, DEFAULT_DETACHED_OFFSET, DEFAULT_FORM_SHEET_HEIGHT_RATIO, DEFAULT_FORM_SHEET_WIDTH, DEFAULT_GRABBER_COLOR_DARK, DEFAULT_GRABBER_COLOR_LIGHT, DEFAULT_GRABBER_HEIGHT, DEFAULT_GRABBER_TOP_MARGIN, DEFAULT_GRABBER_WIDTH, DEFAULT_MAX_WIDTH } from "./web/constants.js";
|
|
9
9
|
import { getDOMElement } from "./web/dom.js";
|
|
10
|
+
import { TrueSheetBackground } from "./TrueSheetBackground.js";
|
|
11
|
+
import { getDetentBackgroundIndex, normalizeDetentBackground } from "./detentBackgrounds.js";
|
|
12
|
+
import { observeSheetLayout } from "./web/layout.js";
|
|
10
13
|
import { Drawer } from "./web/vaul/index.js";
|
|
11
|
-
import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from "./web/vaul/constants.js";
|
|
14
|
+
import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from "./web/vaul/constants.js";
|
|
12
15
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
return null;
|
|
16
|
+
// Resolves the user's `scrollableRef` to its scrollable DOM element — RN-web
|
|
17
|
+
// ScrollView refs expose the scroll node directly (with helpers attached);
|
|
18
|
+
// list refs (e.g. FlatList) expose it via getScrollableNode(). The node's
|
|
19
|
+
// first child is the content container.
|
|
20
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
21
|
+
const getScrollableElement = scrollable => {
|
|
22
|
+
if (!scrollable) return null;
|
|
23
|
+
if (scrollable instanceof HTMLElement) return scrollable;
|
|
24
|
+
const node = scrollable.getScrollableNode?.();
|
|
25
|
+
return node instanceof HTMLElement ? node : null;
|
|
25
26
|
};
|
|
26
27
|
const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
27
28
|
const {
|
|
@@ -32,10 +33,15 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
32
33
|
cornerRadius,
|
|
33
34
|
style,
|
|
34
35
|
backgroundColor: backgroundColorProp,
|
|
36
|
+
detentBackgrounds,
|
|
37
|
+
background,
|
|
38
|
+
backgroundStyle,
|
|
35
39
|
maxContentHeight,
|
|
36
40
|
maxContentWidth,
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
placement = 'automatic',
|
|
42
|
+
placementOffset = DEFAULT_PLACEMENT_OFFSET,
|
|
43
|
+
scrollableRef,
|
|
44
|
+
scrollableOptions,
|
|
39
45
|
grabber = true,
|
|
40
46
|
grabberOptions,
|
|
41
47
|
accessibilityOptions,
|
|
@@ -60,6 +66,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
60
66
|
onDidPresent,
|
|
61
67
|
onWillDismiss,
|
|
62
68
|
onDidDismiss,
|
|
69
|
+
onDismissAttempt,
|
|
63
70
|
onDetentChange,
|
|
64
71
|
onDragBegin,
|
|
65
72
|
onDragChange,
|
|
@@ -91,9 +98,13 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
91
98
|
// is explicitly set.
|
|
92
99
|
const effectiveDetached = isFormSheet || detached;
|
|
93
100
|
const colorScheme = useColorScheme();
|
|
94
|
-
const backgroundColor = backgroundColorProp ?? (colorScheme === 'dark' ? COLOR_SURFACE_CONTAINER_LOW_DARK : COLOR_SURFACE_CONTAINER_LOW_LIGHT);
|
|
95
101
|
const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
|
|
96
102
|
const [isOpen, setIsOpen] = useState(shouldAutoPresent);
|
|
103
|
+
const isOpenRef = useRef(isOpen);
|
|
104
|
+
isOpenRef.current = isOpen;
|
|
105
|
+
const isPresentingRef = useRef(shouldAutoPresent);
|
|
106
|
+
const [displayedBackgroundIndex, setDisplayedBackgroundIndex] = useState(shouldAutoPresent ? initialDetentIndex : 0);
|
|
107
|
+
const backgroundColor = normalizeDetentBackground(detentBackgrounds?.[Math.min(displayedBackgroundIndex, validDetents.length - 1)])?.color ?? backgroundColorProp ?? (colorScheme === 'dark' ? COLOR_SURFACE_CONTAINER_LOW_DARK : COLOR_SURFACE_CONTAINER_LOW_LIGHT);
|
|
97
108
|
const [activeSnapPoint, setActiveSnapPoint] = useState(() => validDetents[shouldAutoPresent ? initialDetentIndex : 0] ?? null);
|
|
98
109
|
|
|
99
110
|
// Keep activeSnapPoint valid if detents change (e.g., prop updates).
|
|
@@ -103,7 +114,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
103
114
|
}, [validDetents]);
|
|
104
115
|
const validDetentsRef = useRef(validDetents);
|
|
105
116
|
validDetentsRef.current = validDetents;
|
|
106
|
-
const hasAutoDetent = validDetents.includes('auto');
|
|
107
117
|
const handleSetActiveSnapPoint = useCallback(snapPoint => {
|
|
108
118
|
setActiveSnapPoint(snapPoint == null ? null : typeof snapPoint === 'number' || snapPoint === 'auto' || snapPoint === 'peek' ? snapPoint : null);
|
|
109
119
|
}, []);
|
|
@@ -122,24 +132,43 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
122
132
|
e.preventDefault();
|
|
123
133
|
return;
|
|
124
134
|
}
|
|
135
|
+
if (!(target instanceof Element)) return;
|
|
136
|
+
const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
|
|
137
|
+
if (!wrapper) return;
|
|
125
138
|
// The footer is rendered via vaul's `detachedSiblings` as a sibling of
|
|
126
139
|
// Drawer.Content inside [data-vaul-detached-wrapper], so Radix treats
|
|
127
140
|
// clicks on it as "outside" the content. Don't dismiss for clicks that
|
|
128
141
|
// landed inside the wrapper.
|
|
129
|
-
if (target
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
e.preventDefault();
|
|
133
|
-
}
|
|
142
|
+
if (wrapper.contains(target)) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
return;
|
|
134
145
|
}
|
|
146
|
+
// Every sheet runs Radix non-modal and portals its overlay + wrapper as
|
|
147
|
+
// siblings into the shared container, so a press on a sheet stacked above
|
|
148
|
+
// this one (its content or its dim overlay) is also delivered here as
|
|
149
|
+
// "outside". Sheets present in document order, so anything after our
|
|
150
|
+
// wrapper belongs to a descendant. Radix may dispatch on the deferred
|
|
151
|
+
// click (touch), after a dismissing descendant has left the DOM — treat
|
|
152
|
+
// disconnected as above too.
|
|
153
|
+
/* eslint-disable no-bitwise */
|
|
154
|
+
const above = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_DISCONNECTED;
|
|
155
|
+
if (wrapper.compareDocumentPosition(target) & above) {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
}
|
|
158
|
+
/* eslint-enable no-bitwise */
|
|
135
159
|
};
|
|
136
160
|
const dismissAboveRef = useRef(async () => {});
|
|
161
|
+
const dismissDescendantsRef = useRef(() => {});
|
|
137
162
|
const methods = useMemo(() => ({
|
|
138
163
|
present: async (index = 0) => {
|
|
139
164
|
const detent = validDetentsRef.current[index];
|
|
140
165
|
if (detent === undefined) {
|
|
141
166
|
throw new Error(`TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`);
|
|
142
167
|
}
|
|
168
|
+
if (!isOpenRef.current) {
|
|
169
|
+
isPresentingRef.current = true;
|
|
170
|
+
setDisplayedBackgroundIndex(index);
|
|
171
|
+
}
|
|
143
172
|
setActiveSnapPoint(detent);
|
|
144
173
|
setIsOpen(true);
|
|
145
174
|
},
|
|
@@ -185,17 +214,40 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
185
214
|
const [peekContentHeight, setPeekContentHeight] = useState(0);
|
|
186
215
|
const contentRef = useRef(null);
|
|
187
216
|
const peekElRef = useRef(null);
|
|
217
|
+
const measurePeek = useCallback(() => {
|
|
218
|
+
const peekEl = getDOMElement(peekElRef.current);
|
|
219
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
220
|
+
if (peekEl && contentEl) {
|
|
221
|
+
setPeekContentHeight(measurePeekContentHeight(peekEl, contentEl));
|
|
222
|
+
}
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
// Last-attached wins — during a screen swap the incoming peek mounts before
|
|
226
|
+
// the outgoing one unmounts, so the outgoing detach must not clear it.
|
|
188
227
|
const peekContext = useMemo(() => ({
|
|
189
228
|
contentRef,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
229
|
+
attachPeek: view => {
|
|
230
|
+
peekElRef.current = view;
|
|
231
|
+
measurePeek();
|
|
232
|
+
},
|
|
233
|
+
detachPeek: view => {
|
|
234
|
+
if (peekElRef.current !== view) return;
|
|
235
|
+
peekElRef.current = null;
|
|
236
|
+
setPeekContentHeight(0);
|
|
237
|
+
},
|
|
238
|
+
measurePeek: view => {
|
|
239
|
+
if (peekElRef.current === view) measurePeek();
|
|
240
|
+
}
|
|
241
|
+
}), [measurePeek]);
|
|
242
|
+
|
|
243
|
+
// The peek's own onLayout only fires when *it* resizes. Content added above
|
|
244
|
+
// it moves it without resizing it, so re-measure its offset whenever the
|
|
245
|
+
// content view's layout changes.
|
|
246
|
+
const handleContentLayout = measurePeek;
|
|
193
247
|
|
|
194
248
|
// An absolute (floating) header overlaps the content, so it contributes no
|
|
195
249
|
// height to the 'auto' detent measurement.
|
|
196
250
|
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
197
|
-
const absoluteHeaderRef = useRef(absoluteHeader);
|
|
198
|
-
absoluteHeaderRef.current = absoluteHeader;
|
|
199
251
|
const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
|
|
200
252
|
|
|
201
253
|
// Same for an absolute (floating) footer — rendered via vaul's
|
|
@@ -222,115 +274,54 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
222
274
|
// A relative footer is laid out below the content, so it's pushed off-screen
|
|
223
275
|
// at the peek detent and contributes no height.
|
|
224
276
|
const peekHeight = (header ? headerHeight : 0) + (footer && absoluteFooter ? footerHeight : 0) + peekContentHeight || DEFAULT_PEEK_HEIGHT;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// `naturalHeight`.
|
|
231
|
-
const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
|
|
232
|
-
const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
|
|
233
|
-
const pinnedScrollerRef = useRef(null);
|
|
234
|
-
|
|
235
|
-
// Natural content height (header + content + footer, with a pinned
|
|
236
|
-
// scrollable's viewport replaced by its content size) — the height the
|
|
237
|
-
// content wants regardless of the sheet's bounds.
|
|
238
|
-
const measureNaturalHeight = useCallback(() => {
|
|
239
|
-
const contentEl = getDOMElement(contentRef.current);
|
|
240
|
-
if (!contentEl || !contentEl.isConnected) return 0;
|
|
241
|
-
const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
|
|
242
|
-
const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
|
|
243
|
-
let height = (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
|
|
244
|
-
const scroller = pinnedScrollerRef.current;
|
|
245
|
-
const scrollContent = scroller?.firstElementChild;
|
|
246
|
-
if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
|
|
247
|
-
height += scrollContent.offsetHeight - scroller.clientHeight;
|
|
248
|
-
}
|
|
249
|
-
return Math.max(0, height);
|
|
250
|
-
}, []);
|
|
277
|
+
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
278
|
+
const naturalHeightRef = useRef(0);
|
|
279
|
+
const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
|
|
280
|
+
const footerInsetAdjustment = Boolean(footer) && absoluteFooter && (insetBehavior === 'automatic' || insetBehavior === 'footer');
|
|
281
|
+
const measureContent = validDetents.includes('auto') || isFormSheet;
|
|
251
282
|
useEffect(() => {
|
|
252
|
-
if (!isOpen
|
|
283
|
+
if (!isOpen) return undefined;
|
|
253
284
|
let canceled = false;
|
|
254
285
|
let rafId = 0;
|
|
255
|
-
let
|
|
256
|
-
let resizeObserver = null;
|
|
286
|
+
let observer;
|
|
257
287
|
const attach = () => {
|
|
258
288
|
if (canceled) return;
|
|
259
|
-
const
|
|
260
|
-
|
|
289
|
+
const layoutEl = sizedLayoutRef.current;
|
|
290
|
+
const drawer = drawerContentRef.current;
|
|
291
|
+
if (!drawer || !layoutEl?.isConnected) {
|
|
261
292
|
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
262
293
|
rafId = window.requestAnimationFrame(attach);
|
|
263
294
|
return;
|
|
264
295
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// would otherwise trigger a full-subtree rescan (findVerticalScroller
|
|
280
|
-
// resolves computed styles) on every scroll frame. Size changes are
|
|
281
|
-
// already covered by the ResizeObserver.
|
|
282
|
-
if (observed && observed.contentEl === contentEl && contentEl?.isConnected && observed.headerEl === headerEl && observed.scroller.isConnected && observed.scrollContent === observed.scroller.firstElementChild) {
|
|
283
|
-
return;
|
|
296
|
+
observer = observeSheetLayout({
|
|
297
|
+
drawer,
|
|
298
|
+
layout: layoutEl,
|
|
299
|
+
getContent: () => getDOMElement(contentRef.current),
|
|
300
|
+
getHeader: () => getDOMElement(headerElRef.current),
|
|
301
|
+
getFooter: () => getDOMElement(footerElRef.current),
|
|
302
|
+
getScrollable: () => getScrollableElement(scrollableRef?.current),
|
|
303
|
+
absoluteHeader,
|
|
304
|
+
absoluteFooter,
|
|
305
|
+
footerInsetAdjustment,
|
|
306
|
+
measureContent,
|
|
307
|
+
onHeightChange: height => {
|
|
308
|
+
naturalHeightRef.current = height;
|
|
309
|
+
setMeasuredContentHeight(height);
|
|
284
310
|
}
|
|
285
|
-
const scroller = contentEl && contentEl.isConnected ? findVerticalScroller(contentEl) : null;
|
|
286
|
-
pinnedScrollerRef.current = scroller;
|
|
287
|
-
setHasBoundedScrollable(scroller != null);
|
|
288
|
-
observed = scroller ? {
|
|
289
|
-
contentEl,
|
|
290
|
-
headerEl,
|
|
291
|
-
scroller,
|
|
292
|
-
scrollContent: scroller.firstElementChild
|
|
293
|
-
} : null;
|
|
294
|
-
resizeObserver?.disconnect();
|
|
295
|
-
resizeObserver = new ResizeObserver(measure);
|
|
296
|
-
if (contentEl?.isConnected) resizeObserver.observe(contentEl);
|
|
297
|
-
if (headerEl) resizeObserver.observe(headerEl);
|
|
298
|
-
if (scroller) {
|
|
299
|
-
resizeObserver.observe(scroller);
|
|
300
|
-
if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
|
|
301
|
-
}
|
|
302
|
-
measure();
|
|
303
|
-
};
|
|
304
|
-
observe();
|
|
305
|
-
// Watch the whole drawer subtree — catches the scrollable mounting/
|
|
306
|
-
// unmounting AND the layout-branch swap, which remounts the content node
|
|
307
|
-
// (a content-scoped observer would go stale after the swap).
|
|
308
|
-
mutationObserver = new MutationObserver(observe);
|
|
309
|
-
mutationObserver.observe(drawerEl, {
|
|
310
|
-
childList: true,
|
|
311
|
-
subtree: true
|
|
312
311
|
});
|
|
313
312
|
};
|
|
314
313
|
rafId = window.requestAnimationFrame(attach);
|
|
315
314
|
return () => {
|
|
316
315
|
canceled = true;
|
|
317
316
|
window.cancelAnimationFrame(rafId);
|
|
318
|
-
|
|
319
|
-
resizeObserver?.disconnect();
|
|
317
|
+
observer?.disconnect();
|
|
320
318
|
};
|
|
321
|
-
}, [isOpen,
|
|
319
|
+
}, [isOpen, absoluteHeader, absoluteFooter, footerInsetAdjustment, measureContent, scrollableRef]);
|
|
322
320
|
|
|
323
321
|
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
324
322
|
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
325
323
|
// the scroll container and everything inside it (see vaul/style.css).
|
|
326
324
|
const isScrollLocked = validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
|
|
327
|
-
|
|
328
|
-
// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
|
|
329
|
-
// Track it here so the form sheet can size its card to fit content,
|
|
330
|
-
// clamped between a minimum ratio of the viewport and a maximum derived
|
|
331
|
-
// from `detachedOffset` (the breathing room left at top + bottom of the
|
|
332
|
-
// floating card).
|
|
333
|
-
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
334
325
|
const effectiveMaxContentHeight = useMemo(() => {
|
|
335
326
|
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
336
327
|
if (!isFormSheet) return undefined;
|
|
@@ -393,16 +384,14 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
393
384
|
effectiveDetachedOffset,
|
|
394
385
|
effectiveMaxContentHeight,
|
|
395
386
|
peekHeight,
|
|
396
|
-
peekContentHeight
|
|
397
|
-
measuredContentHeight
|
|
387
|
+
peekContentHeight
|
|
398
388
|
});
|
|
399
389
|
geometryInputsRef.current = {
|
|
400
390
|
effectiveDetached,
|
|
401
391
|
effectiveDetachedOffset,
|
|
402
392
|
effectiveMaxContentHeight,
|
|
403
393
|
peekHeight,
|
|
404
|
-
peekContentHeight
|
|
405
|
-
measuredContentHeight
|
|
394
|
+
peekContentHeight
|
|
406
395
|
};
|
|
407
396
|
const computeDetentGeometry = useCallback(() => {
|
|
408
397
|
const inputs = geometryInputsRef.current;
|
|
@@ -410,12 +399,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
410
399
|
const effectiveH = inputs.effectiveDetached ? windowH - inputs.effectiveDetachedOffset : windowH;
|
|
411
400
|
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
412
401
|
const ceiling = inputs.effectiveMaxContentHeight !== undefined ? Math.min(effectiveH, inputs.effectiveMaxContentHeight) : effectiveH;
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
// so it reads 0 until the ResizeObserver fires). Falls back to the state
|
|
417
|
-
// value, then to vaul's pre-measure fallback (effectiveHeight / 2).
|
|
418
|
-
const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
|
|
402
|
+
// Reuse the intrinsic measurement during position events so animation
|
|
403
|
+
// frames don't clone the content tree again.
|
|
404
|
+
const measuredHeight = naturalHeightRef.current;
|
|
419
405
|
const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
|
|
420
406
|
|
|
421
407
|
// 'peek' is measured live from the DOM (offsetHeight is layout-based, so
|
|
@@ -446,7 +432,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
446
432
|
positions,
|
|
447
433
|
values
|
|
448
434
|
};
|
|
449
|
-
}, [
|
|
435
|
+
}, []);
|
|
450
436
|
|
|
451
437
|
// Detent info for lifecycle events. Position/detent come from the active
|
|
452
438
|
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
@@ -533,50 +519,20 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
533
519
|
detent: values[count - 1]
|
|
534
520
|
};
|
|
535
521
|
}, [computeDetentGeometry]);
|
|
536
|
-
|
|
537
|
-
// Mirror native synchronous layout: while dragging (and through the post-
|
|
538
|
-
// release settle) the sized layout tracks the sheet's visible height in
|
|
539
|
-
// realtime, clamped to the smallest detent — dragging below it slides the
|
|
540
|
-
// sheet out without resizing. Once settled, the inline height is cleared so
|
|
541
|
-
// the CSS calc() owns sizing again (adapts to viewport resizes at rest).
|
|
542
522
|
const sizedLayoutRef = useRef(null);
|
|
543
523
|
const isDraggingRef = useRef(false);
|
|
544
|
-
const isSettlingAfterDragRef = useRef(false);
|
|
545
|
-
const updateSizedLayoutHeight = useCallback(position => {
|
|
546
|
-
const node = sizedLayoutRef.current;
|
|
547
|
-
if (!node) return;
|
|
548
|
-
const {
|
|
549
|
-
effectiveH,
|
|
550
|
-
ceiling,
|
|
551
|
-
positions
|
|
552
|
-
} = computeDetentGeometry();
|
|
553
|
-
if (positions.length === 0) return;
|
|
554
|
-
if (isSettlingAfterDragRef.current) {
|
|
555
|
-
const snap = activeSnapPointRef.current;
|
|
556
|
-
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
557
|
-
const target = index >= 0 ? positions[index] : null;
|
|
558
|
-
if (target != null && Math.abs(position - target) < 1) {
|
|
559
|
-
isSettlingAfterDragRef.current = false;
|
|
560
|
-
// Restore the calc — height is an inline style, so clearing it would
|
|
561
|
-
// leave the div unsized (React won't re-apply it without a render)
|
|
562
|
-
node.style.height = SIZED_LAYOUT_HEIGHT;
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// positions[0] is the smallest detent's top-Y — its height is the clamp floor.
|
|
568
|
-
const minHeight = effectiveH - positions[0];
|
|
569
|
-
const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
|
|
570
|
-
node.style.height = `${height}px`;
|
|
571
|
-
}, [computeDetentGeometry]);
|
|
572
524
|
const handlePositionChange = useCallback(position => {
|
|
573
|
-
if (isDraggingRef.current || isSettlingAfterDragRef.current) {
|
|
574
|
-
updateSizedLayoutHeight(position);
|
|
575
|
-
}
|
|
576
525
|
const {
|
|
577
526
|
index,
|
|
578
527
|
detent
|
|
579
528
|
} = interpolateFromPosition(position);
|
|
529
|
+
if (isOpenRef.current && !isPresentingRef.current) {
|
|
530
|
+
const {
|
|
531
|
+
effectiveH,
|
|
532
|
+
positions
|
|
533
|
+
} = computeDetentGeometry();
|
|
534
|
+
setDisplayedBackgroundIndex(current => getDetentBackgroundIndex(effectiveH - position, positions.map(top => effectiveH - top), current));
|
|
535
|
+
}
|
|
580
536
|
onPositionChangeRef.current?.({
|
|
581
537
|
nativeEvent: {
|
|
582
538
|
index,
|
|
@@ -585,7 +541,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
585
541
|
realtime: true
|
|
586
542
|
}
|
|
587
543
|
});
|
|
588
|
-
}, [interpolateFromPosition,
|
|
544
|
+
}, [interpolateFromPosition, computeDetentGeometry]);
|
|
589
545
|
|
|
590
546
|
// Fire onMount once after first render. React-mount is the earliest point
|
|
591
547
|
// the component is ready for imperative calls, matching the native
|
|
@@ -611,6 +567,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
611
567
|
if (isOpen === wasOpen) return undefined;
|
|
612
568
|
const present = isOpen;
|
|
613
569
|
if (!present) {
|
|
570
|
+
// Mirror native: dismissing a sheet takes every sheet stacked above it
|
|
571
|
+
// (iOS dismisses from the presenter; Android runs dismissStack first).
|
|
572
|
+
dismissDescendantsRef.current();
|
|
614
573
|
// Pair willBlur with willDismiss on dismiss — mirrors native iOS
|
|
615
574
|
// emitWillDismissEvents (blur fires before dismiss). willPresent is
|
|
616
575
|
// deferred to `start()` below (needs the mounted drawer's geometry).
|
|
@@ -623,6 +582,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
623
582
|
}
|
|
624
583
|
const fireDone = () => {
|
|
625
584
|
if (present) {
|
|
585
|
+
isPresentingRef.current = false;
|
|
626
586
|
onDidPresentRef.current?.({
|
|
627
587
|
nativeEvent: computeDetentInfo()
|
|
628
588
|
});
|
|
@@ -728,12 +688,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
728
688
|
nativeEvent: computeDetentInfo(true)
|
|
729
689
|
});
|
|
730
690
|
}, [computeDetentInfo]);
|
|
731
|
-
const handleRelease = useCallback((
|
|
691
|
+
const handleRelease = useCallback(() => {
|
|
732
692
|
if (!isDraggingRef.current) return;
|
|
733
693
|
isDraggingRef.current = false;
|
|
734
|
-
// Track the sized layout through the settle animation only when the sheet
|
|
735
|
-
// stays open — a dismissal slides out at the min-clamped height.
|
|
736
|
-
isSettlingAfterDragRef.current = open;
|
|
737
694
|
onDragEndRef.current?.({
|
|
738
695
|
nativeEvent: computeDetentInfo(true)
|
|
739
696
|
});
|
|
@@ -744,10 +701,18 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
744
701
|
descendants
|
|
745
702
|
} = useSheetStack(methodsRef, drawerContentRef, isOpen, isFormSheet);
|
|
746
703
|
dismissAboveRef.current = dismissAbove;
|
|
704
|
+
// Captured per render: by the time the dismiss effect runs, this sheet has
|
|
705
|
+
// already been popped from the stack, so `dismissAbove` can't find it.
|
|
706
|
+
dismissDescendantsRef.current = () => {
|
|
707
|
+
for (let i = descendants.length - 1; i >= 0; i--) {
|
|
708
|
+
descendants[i].ref.current?.dismiss();
|
|
709
|
+
}
|
|
710
|
+
};
|
|
747
711
|
|
|
748
712
|
// Mirror Android: translate this sheet down to match the deepest descendant's
|
|
749
713
|
// top so the whole stack visually aligns. Cascades because every ancestor
|
|
750
714
|
// re-runs whenever the stack (and thus its descendants) changes.
|
|
715
|
+
const wasCascadedRef = useRef(false);
|
|
751
716
|
useEffect(() => {
|
|
752
717
|
const parent = drawerContentRef.current;
|
|
753
718
|
if (!parent) return;
|
|
@@ -758,7 +723,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
758
723
|
// out — nothing to align with anymore.
|
|
759
724
|
if (!isOpen) return;
|
|
760
725
|
const parentWrapper = parent.closest('[data-vaul-detached-wrapper]');
|
|
761
|
-
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
726
|
+
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
762
727
|
const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
763
728
|
const CLIP_NONE = 'inset(0px round 0px)';
|
|
764
729
|
|
|
@@ -781,22 +746,29 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
781
746
|
parentWrapper.style.clipPath = next;
|
|
782
747
|
};
|
|
783
748
|
if (descendants.length === 0) {
|
|
749
|
+
if (!wasCascadedRef.current) return;
|
|
750
|
+
wasCascadedRef.current = false;
|
|
784
751
|
parent.style.transition = transition;
|
|
785
752
|
parent.style.transform = '';
|
|
786
753
|
setClip(CLIP_NONE);
|
|
787
754
|
return;
|
|
788
755
|
}
|
|
756
|
+
wasCascadedRef.current = true;
|
|
789
757
|
|
|
790
|
-
// Track
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
// grandchild
|
|
758
|
+
// Track the immediate child's *actual* top: its cascade transform when it
|
|
759
|
+
// was itself pushed behind a grandchild, else its own snap point. Mirrors
|
|
760
|
+
// Android's addTranslation propagating extra translation up the stack —
|
|
761
|
+
// a grandchild opening pushes this sheet only as far as our child moved.
|
|
762
|
+
const readTranslateY = el => {
|
|
763
|
+
const match = el.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
|
|
764
|
+
return match ? Number.parseFloat(match[1]) : null;
|
|
765
|
+
};
|
|
794
766
|
const computeTargetY = () => {
|
|
795
767
|
const parentSnap = Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
796
768
|
const node = descendants[0]?.nodeRef.current;
|
|
797
769
|
if (!node) return parentSnap;
|
|
798
|
-
const
|
|
799
|
-
return Math.max(parentSnap,
|
|
770
|
+
const childTop = readTranslateY(node) ?? (Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0);
|
|
771
|
+
return Math.max(parentSnap, childTop);
|
|
800
772
|
};
|
|
801
773
|
|
|
802
774
|
// When a form-sheet parent has a form-sheet descendant, clip the parent to
|
|
@@ -830,18 +802,26 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
830
802
|
};
|
|
831
803
|
const apply = () => {
|
|
832
804
|
applyFormClip();
|
|
833
|
-
// Mirror
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
805
|
+
// Mirror iPad/Android (`isExpanded`): a parent at full height (detent 1)
|
|
806
|
+
// stays put and the child simply overlays it. Full height is the sheet's
|
|
807
|
+
// own ceiling — the viewport for a page sheet, the capped content-fit
|
|
808
|
+
// height for a form sheet — so compare against where detent 1 lands.
|
|
809
|
+
const parentSnap = Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
810
|
+
const {
|
|
811
|
+
effectiveH,
|
|
812
|
+
ceiling
|
|
813
|
+
} = computeDetentGeometry();
|
|
814
|
+
if (parentSnap <= effectiveH - ceiling + 0.5) {
|
|
838
815
|
parent.style.transition = transition;
|
|
839
816
|
parent.style.transform = '';
|
|
840
817
|
return;
|
|
841
818
|
}
|
|
819
|
+
// Mirror Android: stay put while the child is being dragged (its live
|
|
820
|
+
// transform would drag us along during drag-to-dismiss). The release
|
|
821
|
+
// snap rewrites the child's style and re-applies.
|
|
822
|
+
if (descendants[0]?.nodeRef.current?.classList.contains(DRAG_CLASS)) return;
|
|
842
823
|
const targetY = computeTargetY();
|
|
843
|
-
const
|
|
844
|
-
const currentY = match ? Number.parseFloat(match[1]) : 0;
|
|
824
|
+
const currentY = readTranslateY(parent) ?? 0;
|
|
845
825
|
if (Math.abs(currentY - targetY) < 0.5) return;
|
|
846
826
|
parent.style.transition = transition;
|
|
847
827
|
parent.style.transform = `translate3d(0, ${targetY}px, 0)`;
|
|
@@ -855,11 +835,17 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
855
835
|
attributes: true,
|
|
856
836
|
attributeFilter: ['style']
|
|
857
837
|
});
|
|
838
|
+
// The child's own cascade lands in a later frame — follow it when it moves.
|
|
839
|
+
const childNode = descendants[0]?.nodeRef.current;
|
|
840
|
+
if (childNode) observer.observe(childNode, {
|
|
841
|
+
attributes: true,
|
|
842
|
+
attributeFilter: ['style']
|
|
843
|
+
});
|
|
858
844
|
return () => {
|
|
859
845
|
cancelAnimationFrame(raf);
|
|
860
846
|
observer.disconnect();
|
|
861
847
|
};
|
|
862
|
-
}, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen]);
|
|
848
|
+
}, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen, computeDetentGeometry]);
|
|
863
849
|
|
|
864
850
|
// Focus/blur events fire when a descendant sheet appears on top of this one
|
|
865
851
|
// (blur) or when all descendants are dismissed (focus). will-events fire
|
|
@@ -870,12 +856,19 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
870
856
|
const onDidBlurRef = useRef(onDidBlur);
|
|
871
857
|
const onWillFocusRef = useRef(onWillFocus);
|
|
872
858
|
const onDidFocusRef = useRef(onDidFocus);
|
|
859
|
+
const onDismissAttemptRef = useRef(onDismissAttempt);
|
|
873
860
|
useEffect(() => {
|
|
874
861
|
onWillBlurRef.current = onWillBlur;
|
|
875
862
|
onDidBlurRef.current = onDidBlur;
|
|
876
863
|
onWillFocusRef.current = onWillFocus;
|
|
877
864
|
onDidFocusRef.current = onDidFocus;
|
|
865
|
+
onDismissAttemptRef.current = onDismissAttempt;
|
|
878
866
|
});
|
|
867
|
+
const handleDismissAttempt = useCallback(() => {
|
|
868
|
+
onDismissAttemptRef.current?.({
|
|
869
|
+
nativeEvent: null
|
|
870
|
+
});
|
|
871
|
+
}, []);
|
|
879
872
|
const prevDescendantCountRef = useRef(0);
|
|
880
873
|
useEffect(() => {
|
|
881
874
|
const prevCount = prevDescendantCountRef.current;
|
|
@@ -924,11 +917,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
924
917
|
window.cancelAnimationFrame(rafId);
|
|
925
918
|
};
|
|
926
919
|
}, [isOpen, descendants.length]);
|
|
927
|
-
|
|
928
|
-
// Definite-height flex layout (per-detent sizing) unless content must be
|
|
929
|
-
// measured in natural flow: 'auto' detents (without a plugged scrollable —
|
|
930
|
-
// those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
|
|
931
|
-
const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
|
|
932
920
|
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
933
921
|
|
|
934
922
|
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
@@ -942,6 +930,13 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
942
930
|
// wrapper edges). drop-shadow on the wrapper follows the post-clip silhouette
|
|
943
931
|
// and isn't clipped by the wrapper itself.
|
|
944
932
|
const dropShadow = elevation > 0 ? `drop-shadow(0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15))` : undefined;
|
|
933
|
+
const backgroundLayerStyle = {
|
|
934
|
+
position: 'absolute',
|
|
935
|
+
inset: 0,
|
|
936
|
+
pointerEvents: 'none',
|
|
937
|
+
backgroundColor: backgroundColor,
|
|
938
|
+
transition: isPresentingRef.current ? 'none' : 'background-color 200ms'
|
|
939
|
+
};
|
|
945
940
|
const mergedContentStyle = useMemo(() => ({
|
|
946
941
|
position: 'fixed',
|
|
947
942
|
top: 0,
|
|
@@ -952,7 +947,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
952
947
|
flexDirection: 'column',
|
|
953
948
|
borderTopLeftRadius: effectiveCornerRadius,
|
|
954
949
|
borderTopRightRadius: effectiveCornerRadius,
|
|
955
|
-
backgroundColor: backgroundColor,
|
|
956
950
|
// Clip children to the rounded top so headers/content with their own
|
|
957
951
|
// background don't bleed past the corners. `clip` (not `hidden`): a
|
|
958
952
|
// hidden box is still a scroll container, so browser focus-reveal (e.g.
|
|
@@ -962,8 +956,11 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
962
956
|
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
963
957
|
// A relative footer owns the inset instead (see resolvedFooterStyle); an
|
|
964
958
|
// absolute footer floats over the content, so the content keeps its lift.
|
|
965
|
-
|
|
966
|
-
|
|
959
|
+
// A plugged scrollable keeps the lift too (contentInsetAdjustment
|
|
960
|
+
// 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
|
|
961
|
+
// behind the indicator with user-applied content padding.
|
|
962
|
+
paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) && (!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area') ? 'env(safe-area-inset-bottom, 0px)' : 0
|
|
963
|
+
}), [effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter, scrollableRef, insetBehavior]);
|
|
967
964
|
const defaultGrabberColor = colorScheme === 'dark' ? DEFAULT_GRABBER_COLOR_DARK : DEFAULT_GRABBER_COLOR_LIGHT;
|
|
968
965
|
const grabberHeight = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
|
|
969
966
|
|
|
@@ -994,7 +991,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
994
991
|
// `maxContentWidth` is ignored ('form' is absolute).
|
|
995
992
|
// - presentation='page' → `maxContentWidth` (any viewport) or
|
|
996
993
|
// DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
|
|
997
|
-
// Detached without a width constraint applies
|
|
994
|
+
// Detached without a width constraint applies placementOffset on both edges so
|
|
998
995
|
// the floating card breathes from the viewport sides.
|
|
999
996
|
const wrapperStyle = useMemo(() => {
|
|
1000
997
|
// Mobile portrait ignores width sizing entirely (matches iOS/Android:
|
|
@@ -1006,23 +1003,23 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1006
1003
|
const next = {};
|
|
1007
1004
|
if (dropShadow) next.filter = dropShadow;
|
|
1008
1005
|
if (!needsMargins) return next;
|
|
1009
|
-
let
|
|
1010
|
-
let
|
|
1006
|
+
let marginInlineStart;
|
|
1007
|
+
let marginInlineEnd;
|
|
1011
1008
|
if (isFormSheet) {
|
|
1012
|
-
|
|
1013
|
-
|
|
1009
|
+
marginInlineStart = 'auto';
|
|
1010
|
+
marginInlineEnd = 'auto';
|
|
1014
1011
|
} else if (maxWidth == null) {
|
|
1015
|
-
|
|
1016
|
-
|
|
1012
|
+
marginInlineStart = placementOffset;
|
|
1013
|
+
marginInlineEnd = placementOffset;
|
|
1017
1014
|
} else {
|
|
1018
|
-
|
|
1019
|
-
|
|
1015
|
+
marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
|
|
1016
|
+
marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
|
|
1020
1017
|
}
|
|
1021
1018
|
if (maxWidth != null) next.maxWidth = maxWidth;
|
|
1022
|
-
next.
|
|
1023
|
-
next.
|
|
1019
|
+
next.marginInlineStart = marginInlineStart;
|
|
1020
|
+
next.marginInlineEnd = marginInlineEnd;
|
|
1024
1021
|
return next;
|
|
1025
|
-
}, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation,
|
|
1022
|
+
}, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation, placement, placementOffset, effectiveDetached, dropShadow]);
|
|
1026
1023
|
|
|
1027
1024
|
// Absolute-position the grabber so it overlays the content top-edge
|
|
1028
1025
|
// instead of consuming flow height — mirrors native iOS/Android, where
|
|
@@ -1052,6 +1049,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1052
1049
|
onDrag: handleDrag,
|
|
1053
1050
|
onRelease: handleRelease,
|
|
1054
1051
|
dismissible: dismissible,
|
|
1052
|
+
onDismissAttempt: handleDismissAttempt,
|
|
1055
1053
|
draggable: draggable,
|
|
1056
1054
|
repositionInputs: false,
|
|
1057
1055
|
modal: dimmed,
|
|
@@ -1063,11 +1061,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1063
1061
|
peekHeight: peekHeight,
|
|
1064
1062
|
initialAnimated: initialDetentAnimated,
|
|
1065
1063
|
detachedWrapperStyle: wrapperStyle,
|
|
1066
|
-
|
|
1067
|
-
contentHeight:
|
|
1068
|
-
// Bounded scrollable → the auto-size wrapper measures 0 (sized
|
|
1069
|
-
// layout is absolute), so feed vaul the natural height instead.
|
|
1070
|
-
hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0 ? scrollableAutoHeight : undefined,
|
|
1064
|
+
contentHeight: measuredContentHeight,
|
|
1071
1065
|
activeSnapPoint: activeSnapPoint,
|
|
1072
1066
|
setActiveSnapPoint: handleSetActiveSnapPoint,
|
|
1073
1067
|
...snapPointsProps,
|
|
@@ -1088,31 +1082,30 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1088
1082
|
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
1089
1083
|
})
|
|
1090
1084
|
}) : undefined,
|
|
1091
|
-
children: [/*#__PURE__*/_jsx(
|
|
1085
|
+
children: [/*#__PURE__*/_jsx("div", {
|
|
1086
|
+
"aria-hidden": true,
|
|
1087
|
+
style: backgroundLayerStyle
|
|
1088
|
+
}), /*#__PURE__*/_jsx(Drawer.Title, {
|
|
1092
1089
|
style: visuallyHiddenStyle,
|
|
1093
1090
|
children: accessibilityOptions?.paneTitle ?? 'Sheet'
|
|
1094
1091
|
}), grabber && /*#__PURE__*/_jsx(Drawer.Handle, {
|
|
1095
1092
|
style: handleStyle
|
|
1096
|
-
}),
|
|
1097
|
-
/*#__PURE__*/
|
|
1098
|
-
// vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
|
|
1099
|
-
// flow-root) which doesn't honor descendant flex layout. Use an
|
|
1100
|
-
// absolute fill sized to the visible portion (via vaul's
|
|
1101
|
-
// `--snap-point-height` var) so the inner flex column has a
|
|
1102
|
-
// definite height for flex layouts to fill — mirrors native, where
|
|
1103
|
-
// the container is sized to the sheet's visible height per detent.
|
|
1104
|
-
_jsxs("div", {
|
|
1093
|
+
}), /*#__PURE__*/_jsxs("div", {
|
|
1105
1094
|
ref: sizedLayoutRef,
|
|
1106
1095
|
style: sizedLayoutStyle,
|
|
1107
1096
|
"data-vaul-scroll-locked": isScrollLocked ? '' : undefined,
|
|
1108
|
-
children: [
|
|
1097
|
+
children: [/*#__PURE__*/_jsx(TrueSheetBackground, {
|
|
1098
|
+
background: background,
|
|
1099
|
+
backgroundStyle: backgroundStyle
|
|
1100
|
+
}), header && /*#__PURE__*/_jsx(View, {
|
|
1109
1101
|
ref: headerElRef,
|
|
1110
1102
|
style: resolvedHeaderStyle,
|
|
1111
1103
|
onLayout: handleHeaderLayout,
|
|
1112
1104
|
children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
|
|
1113
1105
|
}), /*#__PURE__*/_jsx(View, {
|
|
1114
1106
|
ref: contentRef,
|
|
1115
|
-
style:
|
|
1107
|
+
style: [contentStyle, style],
|
|
1108
|
+
onLayout: handleContentLayout,
|
|
1116
1109
|
children: children
|
|
1117
1110
|
}), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
|
|
1118
1111
|
ref: footerElRef,
|
|
@@ -1120,26 +1113,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1120
1113
|
onLayout: handleFooterLayout,
|
|
1121
1114
|
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
1122
1115
|
})]
|
|
1123
|
-
}) :
|
|
1124
|
-
/*#__PURE__*/
|
|
1125
|
-
// Natural flow so vaul can measure content height — required for
|
|
1126
|
-
// 'auto' detents and form-sheet content-fit sizing.
|
|
1127
|
-
_jsxs(_Fragment, {
|
|
1128
|
-
children: [header && /*#__PURE__*/_jsx(View, {
|
|
1129
|
-
ref: headerElRef,
|
|
1130
|
-
style: resolvedHeaderStyle,
|
|
1131
|
-
onLayout: handleHeaderLayout,
|
|
1132
|
-
children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
|
|
1133
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
1134
|
-
ref: contentRef,
|
|
1135
|
-
style: style,
|
|
1136
|
-
children: children
|
|
1137
|
-
}), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
|
|
1138
|
-
ref: footerElRef,
|
|
1139
|
-
style: resolvedFooterStyle,
|
|
1140
|
-
onLayout: handleFooterLayout,
|
|
1141
|
-
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
1142
|
-
})]
|
|
1143
1116
|
})]
|
|
1144
1117
|
})]
|
|
1145
1118
|
})
|
|
@@ -1173,10 +1146,12 @@ const sizedLayoutStyle = {
|
|
|
1173
1146
|
right: 0,
|
|
1174
1147
|
height: SIZED_LAYOUT_HEIGHT,
|
|
1175
1148
|
display: 'flex',
|
|
1176
|
-
flexDirection: 'column'
|
|
1149
|
+
flexDirection: 'column',
|
|
1150
|
+
boxSizing: 'border-box',
|
|
1151
|
+
paddingBottom: 'inherit'
|
|
1177
1152
|
};
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1153
|
+
const contentStyle = {
|
|
1154
|
+
flexShrink: 1,
|
|
1180
1155
|
minHeight: 0
|
|
1181
1156
|
};
|
|
1182
1157
|
const visuallyHiddenStyle = {
|