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