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