@lodev09/react-native-true-sheet 4.0.0-beta.14 → 4.0.0-beta.16
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/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +28 -6
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +99 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +12 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +18 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +40 -39
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +18 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +14 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +8 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +32 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
- package/ios/TrueSheetContainerView.h +17 -4
- package/ios/TrueSheetContainerView.mm +23 -6
- package/ios/TrueSheetContentView.h +13 -1
- package/ios/TrueSheetContentView.mm +93 -35
- package/ios/TrueSheetFooterView.mm +46 -16
- package/ios/TrueSheetView.mm +30 -28
- package/ios/TrueSheetViewController.h +21 -1
- package/ios/TrueSheetViewController.mm +54 -16
- package/ios/core/TrueSheetDetentCalculator.h +6 -1
- package/ios/core/TrueSheetDetentCalculator.mm +9 -4
- package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
- package/lib/module/TrueSheet.js +8 -5
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +93 -218
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetPeek.web.js +20 -15
- package/lib/module/TrueSheetPeek.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -3
- package/lib/module/navigation/expo-router/index.js +5 -4
- package/lib/module/navigation/expo-router/index.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/helpers.js +1 -0
- package/lib/module/web/vaul/helpers.js.map +1 -1
- package/lib/module/web/vaul/index.js +9 -4
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +6 -8
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +64 -25
- 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/TrueSheetPeek.web.d.ts +8 -4
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -3
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/index.d.ts +5 -3
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- 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/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.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/TrueSheet.tsx +7 -4
- package/src/TrueSheet.types.ts +67 -25
- package/src/TrueSheet.web.tsx +132 -263
- package/src/TrueSheetPeek.web.tsx +25 -23
- package/src/fabric/TrueSheetViewNativeComponent.ts +5 -3
- package/src/navigation/expo-router/index.ts +8 -11
- package/src/navigation/types.ts +3 -2
- package/src/web/constants.ts +1 -1
- package/src/web/layout.ts +196 -0
- package/src/web/vaul/helpers.ts +5 -0
- package/src/web/vaul/index.tsx +21 -4
- package/src/web/vaul/use-snap-points.ts +6 -14
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { createContext, type RefObject, useContext,
|
|
1
|
+
import { createContext, type RefObject, useContext, useLayoutEffect, useRef } from 'react';
|
|
2
2
|
import { type LayoutChangeEvent, View, type ViewProps } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import { getDOMElement } from './web/dom';
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
|
-
*
|
|
5
|
+
* Registers a peek with the owning TrueSheet so it can measure the peek live.
|
|
8
6
|
* @internal
|
|
9
7
|
*/
|
|
10
8
|
export interface TrueSheetPeekContextValue {
|
|
11
9
|
contentRef: RefObject<View | null>;
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
attachPeek: (view: View) => void;
|
|
11
|
+
detachPeek: (view: View) => void;
|
|
12
|
+
measurePeek: (view: View) => void;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export const TrueSheetPeekContext = createContext<TrueSheetPeekContextValue | null>(null);
|
|
@@ -19,14 +18,19 @@ export const TrueSheetPeekContext = createContext<TrueSheetPeekContextValue | nu
|
|
|
19
18
|
* Distance from the top of the content view to the bottom of the peek view,
|
|
20
19
|
* so the peek view's offset within the content (padding, views above it)
|
|
21
20
|
* counts toward the peek detent. Both rects live in the drawer's transformed
|
|
22
|
-
* subtree, so an in-flight translate cancels out of the delta.
|
|
21
|
+
* subtree, so an in-flight translate cancels out of the delta. Rounded so the
|
|
22
|
+
* sub-pixel noise of a mid-transition measurement doesn't register as a
|
|
23
|
+
* change — the content re-measures every frame while the snap animates, and
|
|
24
|
+
* a new value would restart the snap.
|
|
23
25
|
* @internal
|
|
24
26
|
*/
|
|
25
27
|
export const measurePeekContentHeight = (
|
|
26
28
|
peekElement: HTMLElement,
|
|
27
29
|
contentElement: HTMLElement
|
|
28
30
|
): number =>
|
|
29
|
-
|
|
31
|
+
Math.round(
|
|
32
|
+
peekElement.getBoundingClientRect().bottom - contentElement.getBoundingClientRect().top
|
|
33
|
+
);
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Wrapper component that marks its children as the sheet's peek content.
|
|
@@ -36,25 +40,23 @@ export const measurePeekContentHeight = (
|
|
|
36
40
|
*/
|
|
37
41
|
export const TrueSheetPeek = ({ onLayout, ...rest }: ViewProps) => {
|
|
38
42
|
const context = useContext(TrueSheetPeekContext);
|
|
39
|
-
const
|
|
43
|
+
const viewRef = useRef<View>(null);
|
|
40
44
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
45
|
+
// Attach by instance rather than sharing a ref — during a screen swap the
|
|
46
|
+
// incoming peek mounts before the outgoing one unmounts, and a shared ref
|
|
47
|
+
// would be nulled by the outgoing unmount.
|
|
48
|
+
useLayoutEffect(() => {
|
|
49
|
+
const view = viewRef.current;
|
|
50
|
+
if (!context || !view) return;
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
context.attachPeek(view);
|
|
53
|
+
return () => context.detachPeek(view);
|
|
54
|
+
}, [context]);
|
|
46
55
|
|
|
47
56
|
const handleLayout = (event: LayoutChangeEvent) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const bottom =
|
|
53
|
-
peekElement && contentElement
|
|
54
|
-
? measurePeekContentHeight(peekElement, contentElement)
|
|
55
|
-
: event.nativeEvent.layout.height;
|
|
56
|
-
|
|
57
|
-
context.setPeekContentHeight(bottom);
|
|
57
|
+
const view = viewRef.current;
|
|
58
|
+
if (context && view) {
|
|
59
|
+
context.measurePeek(view);
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
onLayout?.(event);
|
|
@@ -35,7 +35,7 @@ type BlurOptionsType = Readonly<{
|
|
|
35
35
|
type ScrollEdgeEffect = 'automatic' | 'hard' | 'soft' | 'hidden';
|
|
36
36
|
|
|
37
37
|
type ScrollableOptionsType = Readonly<{
|
|
38
|
-
|
|
38
|
+
contentInsetAdjustment?: WithDefault<'automatic' | 'safe-area' | 'footer' | 'never', 'automatic'>;
|
|
39
39
|
keyboardScrollOffset?: WithDefault<Double, 0>;
|
|
40
40
|
keyboardOffset?: WithDefault<Double, 0>;
|
|
41
41
|
scrollingExpandsSheet?: WithDefault<boolean, true>;
|
|
@@ -48,6 +48,7 @@ type FooterOptionsType = Readonly<{
|
|
|
48
48
|
// the field name, so a second `position` would redefine TrueSheetViewPosition
|
|
49
49
|
footerPosition?: WithDefault<'relative' | 'absolute', 'relative'>;
|
|
50
50
|
keyboardOffset?: WithDefault<Double, 0>;
|
|
51
|
+
avoidKeyboard?: WithDefault<boolean, true>;
|
|
51
52
|
}>;
|
|
52
53
|
|
|
53
54
|
type HeaderOptionsType = Readonly<{
|
|
@@ -109,14 +110,15 @@ export interface NativeProps extends ViewProps {
|
|
|
109
110
|
'none'
|
|
110
111
|
>;
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
placement?: WithDefault<'automatic' | 'leading' | 'center' | 'trailing', 'automatic'>;
|
|
114
|
+
placementOffset?: WithDefault<Double, 16>;
|
|
114
115
|
insetAdjustment?: WithDefault<'automatic' | 'never', 'automatic'>;
|
|
115
116
|
|
|
116
117
|
// Blur options
|
|
117
118
|
blurOptions?: BlurOptionsType;
|
|
118
119
|
|
|
119
120
|
// Boolean properties - match defaults from TrueSheet.types.ts
|
|
121
|
+
glass?: WithDefault<boolean, true>;
|
|
120
122
|
grabber?: WithDefault<boolean, true>;
|
|
121
123
|
grabberOptions?: GrabberOptionsType;
|
|
122
124
|
accessibilityOptions?: AccessibilityOptionsType;
|
|
@@ -22,12 +22,15 @@ const sheetRouter: TrueSheetRouterFactory = createTrueSheetRouter(
|
|
|
22
22
|
StackRouter as StackRouterFactory
|
|
23
23
|
);
|
|
24
24
|
|
|
25
|
+
// `routes`/`dispatch` are injected by `createProps`, so they are declared as
|
|
26
|
+
// CreateProps (last generic) and excluded from the public navigator props
|
|
25
27
|
const SheetNavigator = unstable_integrateWithRouter<
|
|
26
28
|
TrueSheetNavigationOptions,
|
|
27
29
|
TrueSheetNavigationState<ParamListBase>,
|
|
28
30
|
TrueSheetStandardEventMap,
|
|
29
|
-
|
|
30
|
-
TrueSheetRouterOptions
|
|
31
|
+
object,
|
|
32
|
+
TrueSheetRouterOptions,
|
|
33
|
+
TrueSheetNavigatorContentExtraProps
|
|
31
34
|
>(trueSheetNavigator, sheetRouter, {
|
|
32
35
|
createProps: ({ state, dispatch }) => ({
|
|
33
36
|
routes: state.routes,
|
|
@@ -35,15 +38,9 @@ const SheetNavigator = unstable_integrateWithRouter<
|
|
|
35
38
|
}),
|
|
36
39
|
});
|
|
37
40
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
// callbacks - replace it with the exact map
|
|
42
|
-
type SheetProps = Omit<
|
|
43
|
-
React.ComponentProps<typeof SheetNavigator>,
|
|
44
|
-
'screenListeners' | 'routes' | 'dispatch'
|
|
45
|
-
> &
|
|
46
|
-
Partial<Pick<React.ComponentProps<typeof SheetNavigator>, 'routes' | 'dispatch'>> &
|
|
41
|
+
// expo-router intersects the event map with an index signature which breaks
|
|
42
|
+
// precisely typed `screenListeners` callbacks - replace it with the exact map
|
|
43
|
+
type SheetProps = Omit<React.ComponentProps<typeof SheetNavigator>, 'screenListeners'> &
|
|
47
44
|
Pick<TrueSheetNavigatorProps, 'screenListeners'>;
|
|
48
45
|
|
|
49
46
|
/**
|
package/src/navigation/types.ts
CHANGED
|
@@ -216,6 +216,7 @@ export type TrueSheetNavigationSheetProps = Pick<
|
|
|
216
216
|
| 'dimmedDetentIndex'
|
|
217
217
|
| 'backgroundBlur'
|
|
218
218
|
| 'blurOptions'
|
|
219
|
+
| 'glass'
|
|
219
220
|
| 'maxContentHeight'
|
|
220
221
|
| 'maxContentWidth'
|
|
221
222
|
| 'presentation'
|
|
@@ -229,8 +230,8 @@ export type TrueSheetNavigationSheetProps = Pick<
|
|
|
229
230
|
| 'scrollableRef'
|
|
230
231
|
| 'scrollableOptions'
|
|
231
232
|
| 'insetAdjustment'
|
|
232
|
-
| '
|
|
233
|
-
| '
|
|
233
|
+
| 'placement'
|
|
234
|
+
| 'placementOffset'
|
|
234
235
|
| 'elevation'
|
|
235
236
|
| 'detached'
|
|
236
237
|
| 'detachedOffset'
|
package/src/web/constants.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const DEFAULT_FORM_SHEET_WIDTH = 580;
|
|
|
4
4
|
export const DEFAULT_FORM_SHEET_HEIGHT_RATIO = 0.5;
|
|
5
5
|
export const COLOR_SURFACE_CONTAINER_LOW_LIGHT = '#F7F2FA';
|
|
6
6
|
export const COLOR_SURFACE_CONTAINER_LOW_DARK = '#1D1B20';
|
|
7
|
-
export const
|
|
7
|
+
export const DEFAULT_PLACEMENT_OFFSET = 16;
|
|
8
8
|
export const DEFAULT_DETACHED_OFFSET = 16;
|
|
9
9
|
export const DEFAULT_GRABBER_COLOR_LIGHT = 'rgba(0, 0, 0, 0.3)';
|
|
10
10
|
export const DEFAULT_GRABBER_COLOR_DARK = 'rgba(255, 255, 255, 0.3)';
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
const FOOTER_INSET = '--truesheet-footer-inset';
|
|
4
|
+
|
|
5
|
+
interface SheetLayoutOptions {
|
|
6
|
+
drawer: HTMLElement;
|
|
7
|
+
layout: HTMLElement;
|
|
8
|
+
getContent: () => HTMLElement | null;
|
|
9
|
+
getHeader: () => HTMLElement | null;
|
|
10
|
+
getFooter: () => HTMLElement | null;
|
|
11
|
+
getScrollable: () => HTMLElement | null;
|
|
12
|
+
absoluteHeader: boolean;
|
|
13
|
+
absoluteFooter: boolean;
|
|
14
|
+
footerInsetAdjustment: boolean;
|
|
15
|
+
measureContent: boolean;
|
|
16
|
+
onHeightChange: (height: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function observeSheetLayout({
|
|
20
|
+
drawer,
|
|
21
|
+
layout,
|
|
22
|
+
getContent,
|
|
23
|
+
getHeader,
|
|
24
|
+
getFooter,
|
|
25
|
+
getScrollable,
|
|
26
|
+
absoluteHeader,
|
|
27
|
+
absoluteFooter,
|
|
28
|
+
footerInsetAdjustment,
|
|
29
|
+
measureContent,
|
|
30
|
+
onHeightChange,
|
|
31
|
+
}: SheetLayoutOptions) {
|
|
32
|
+
let frame = 0;
|
|
33
|
+
let disposed = false;
|
|
34
|
+
let width = -1;
|
|
35
|
+
let height = -1;
|
|
36
|
+
let scroller: HTMLElement | null = null;
|
|
37
|
+
let originalPadding = '';
|
|
38
|
+
let insetPadding = '';
|
|
39
|
+
let appliedInset = 0;
|
|
40
|
+
const observed = new Set<Element>();
|
|
41
|
+
|
|
42
|
+
// This tree has no viewport constraint or injected footer inset. Its resize
|
|
43
|
+
// observer catches intrinsic changes without observing the live flex layout.
|
|
44
|
+
const measurement = measureContent ? document.createElement('div') : null;
|
|
45
|
+
if (measurement) {
|
|
46
|
+
Object.assign(measurement.style, {
|
|
47
|
+
position: 'absolute',
|
|
48
|
+
top: '0',
|
|
49
|
+
left: '0',
|
|
50
|
+
display: 'flex',
|
|
51
|
+
flexDirection: 'column',
|
|
52
|
+
visibility: 'hidden',
|
|
53
|
+
pointerEvents: 'none',
|
|
54
|
+
});
|
|
55
|
+
measurement.style.setProperty(FOOTER_INSET, '0px');
|
|
56
|
+
measurement.inert = true;
|
|
57
|
+
measurement.setAttribute('aria-hidden', 'true');
|
|
58
|
+
measurement.setAttribute('data-truesheet-measure', '');
|
|
59
|
+
drawer.appendChild(measurement);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const measure = () => {
|
|
63
|
+
if (!measurement?.isConnected) return;
|
|
64
|
+
const headerHeight = absoluteHeader ? 0 : (getHeader()?.offsetHeight ?? 0);
|
|
65
|
+
const footerHeight =
|
|
66
|
+
!absoluteFooter || (footerInsetAdjustment && scroller) ? (getFooter()?.offsetHeight ?? 0) : 0;
|
|
67
|
+
const next = measurement.offsetHeight + headerHeight + footerHeight;
|
|
68
|
+
if (height === next) return;
|
|
69
|
+
height = next;
|
|
70
|
+
onHeightChange(next);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const updateInset = () => {
|
|
74
|
+
let inset = 0;
|
|
75
|
+
const footer = getFooter();
|
|
76
|
+
if (footerInsetAdjustment && scroller && footer) {
|
|
77
|
+
let bottom = scroller.offsetTop + scroller.offsetHeight;
|
|
78
|
+
let parent = scroller.offsetParent;
|
|
79
|
+
while (parent instanceof HTMLElement && parent !== layout) {
|
|
80
|
+
bottom += parent.offsetTop;
|
|
81
|
+
parent = parent.offsetParent;
|
|
82
|
+
}
|
|
83
|
+
if (parent === layout) {
|
|
84
|
+
inset = Math.min(
|
|
85
|
+
footer.offsetHeight,
|
|
86
|
+
Math.max(0, bottom - (layout.clientHeight - footer.offsetHeight))
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (inset === appliedInset) return;
|
|
91
|
+
appliedInset = inset;
|
|
92
|
+
layout.style.setProperty(FOOTER_INSET, `${inset}px`);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
96
|
+
if (measurement && width !== layout.clientWidth) {
|
|
97
|
+
width = layout.clientWidth;
|
|
98
|
+
measurement.style.width = `${width}px`;
|
|
99
|
+
}
|
|
100
|
+
updateInset();
|
|
101
|
+
measure();
|
|
102
|
+
});
|
|
103
|
+
const measurementObserver = new ResizeObserver(measure);
|
|
104
|
+
if (measurement) measurementObserver.observe(measurement);
|
|
105
|
+
|
|
106
|
+
const restorePadding = () => {
|
|
107
|
+
if (scroller && scroller.style.paddingBottom === insetPadding) {
|
|
108
|
+
scroller.style.paddingBottom = originalPadding;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const sync = () => {
|
|
113
|
+
if (disposed) return;
|
|
114
|
+
const content = getContent();
|
|
115
|
+
const nextScroller = getScrollable();
|
|
116
|
+
if (nextScroller !== scroller) {
|
|
117
|
+
restorePadding();
|
|
118
|
+
scroller = nextScroller;
|
|
119
|
+
insetPadding = '';
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
footerInsetAdjustment &&
|
|
123
|
+
scroller &&
|
|
124
|
+
(!insetPadding || scroller.style.paddingBottom !== insetPadding)
|
|
125
|
+
) {
|
|
126
|
+
originalPadding = scroller.style.paddingBottom;
|
|
127
|
+
const base = getComputedStyle(scroller).paddingBottom;
|
|
128
|
+
insetPadding = `calc(${base} + var(${FOOTER_INSET}, 0px))`;
|
|
129
|
+
scroller.style.paddingBottom = insetPadding;
|
|
130
|
+
insetPadding = scroller.style.paddingBottom;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const nodes = new Set<Element>([layout]);
|
|
134
|
+
for (const node of [getHeader(), getFooter(), scroller]) {
|
|
135
|
+
if (node) nodes.add(node);
|
|
136
|
+
}
|
|
137
|
+
for (const node of observed) {
|
|
138
|
+
if (!nodes.has(node)) {
|
|
139
|
+
resizeObserver.unobserve(node);
|
|
140
|
+
observed.delete(node);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const node of nodes) {
|
|
144
|
+
if (!observed.has(node)) {
|
|
145
|
+
resizeObserver.observe(node);
|
|
146
|
+
observed.add(node);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (measurement && content) {
|
|
151
|
+
width = layout.clientWidth;
|
|
152
|
+
measurement.style.width = `${width}px`;
|
|
153
|
+
measurement.replaceChildren(content.cloneNode(true));
|
|
154
|
+
}
|
|
155
|
+
updateInset();
|
|
156
|
+
measure();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const mutationObserver = new MutationObserver((mutations) => {
|
|
160
|
+
const changed = mutations.some(
|
|
161
|
+
(mutation) => mutation.target !== layout || mutation.type === 'childList'
|
|
162
|
+
);
|
|
163
|
+
if (!changed || frame) return;
|
|
164
|
+
frame = requestAnimationFrame(() => {
|
|
165
|
+
frame = 0;
|
|
166
|
+
sync();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
sync();
|
|
171
|
+
mutationObserver.observe(layout, {
|
|
172
|
+
childList: true,
|
|
173
|
+
characterData: true,
|
|
174
|
+
attributes: true,
|
|
175
|
+
attributeFilter: ['style', 'class'],
|
|
176
|
+
subtree: true,
|
|
177
|
+
});
|
|
178
|
+
const footer = getFooter();
|
|
179
|
+
if (absoluteFooter && footer) {
|
|
180
|
+
mutationObserver.observe(footer, { childList: true, characterData: true, subtree: true });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
measure,
|
|
185
|
+
disconnect: () => {
|
|
186
|
+
disposed = true;
|
|
187
|
+
cancelAnimationFrame(frame);
|
|
188
|
+
mutationObserver.disconnect();
|
|
189
|
+
resizeObserver.disconnect();
|
|
190
|
+
measurementObserver.disconnect();
|
|
191
|
+
restorePadding();
|
|
192
|
+
layout.style.removeProperty(FOOTER_INSET);
|
|
193
|
+
measurement?.remove();
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
package/src/web/vaul/helpers.ts
CHANGED
|
@@ -80,6 +80,11 @@ export const isVertical = (direction: DrawerDirection) => {
|
|
|
80
80
|
}
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
export const getSnapPointTransform = (direction: DrawerDirection) =>
|
|
84
|
+
isVertical(direction)
|
|
85
|
+
? 'translate3d(0, var(--snap-point-height), 0)'
|
|
86
|
+
: 'translate3d(var(--snap-point-height), 0, 0)';
|
|
87
|
+
|
|
83
88
|
export function getTranslate(element: HTMLElement, direction: DrawerDirection): number | null {
|
|
84
89
|
if (!element) {
|
|
85
90
|
return null;
|
package/src/web/vaul/index.tsx
CHANGED
|
@@ -20,7 +20,14 @@ import {
|
|
|
20
20
|
VELOCITY_THRESHOLD,
|
|
21
21
|
WINDOW_TOP_OFFSET,
|
|
22
22
|
} from './constants';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
dampenValue,
|
|
25
|
+
getSnapPointTransform,
|
|
26
|
+
getTranslate,
|
|
27
|
+
isVertical,
|
|
28
|
+
reset,
|
|
29
|
+
set,
|
|
30
|
+
} from './helpers';
|
|
24
31
|
import type { DrawerDirection } from './types';
|
|
25
32
|
import { useComposedRefs } from './use-composed-refs';
|
|
26
33
|
import { useControllableState } from './use-controllable-state';
|
|
@@ -1301,11 +1308,17 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
|
|
|
1301
1308
|
// same detent). Without this, position goes stale mid-animation because
|
|
1302
1309
|
// the drawer's `transitionrun` never fires.
|
|
1303
1310
|
const onTransitionRun = (e: TransitionEvent) => {
|
|
1304
|
-
if (
|
|
1311
|
+
if (
|
|
1312
|
+
(e.target === drawer || e.target === wrapper) &&
|
|
1313
|
+
(e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
|
|
1314
|
+
)
|
|
1305
1315
|
state.start();
|
|
1306
1316
|
};
|
|
1307
1317
|
const onTransitionDone = (e: TransitionEvent) => {
|
|
1308
|
-
if (
|
|
1318
|
+
if (
|
|
1319
|
+
(e.target === drawer || e.target === wrapper) &&
|
|
1320
|
+
(e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
|
|
1321
|
+
)
|
|
1309
1322
|
state.stop();
|
|
1310
1323
|
};
|
|
1311
1324
|
const onAnimationStart = (e: AnimationEvent) => {
|
|
@@ -1412,6 +1425,9 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
|
|
|
1412
1425
|
wasOpenRef.current = isOpen;
|
|
1413
1426
|
}, [isOpen, detached, drawerRef]);
|
|
1414
1427
|
|
|
1428
|
+
// Subsequent snap writes belong to useSnapPoints. A React style update on
|
|
1429
|
+
// release would overwrite the live drag value before its transition starts.
|
|
1430
|
+
const initialSnapPointOffset = React.useRef(snapPointsOffset?.[activeSnapPointIndex ?? 0] ?? 0);
|
|
1415
1431
|
const contentNode = (
|
|
1416
1432
|
<DialogPrimitive.Content
|
|
1417
1433
|
data-vaul-drawer-direction={direction}
|
|
@@ -1426,7 +1442,8 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
|
|
|
1426
1442
|
style={
|
|
1427
1443
|
snapPointsOffset && snapPointsOffset.length > 0
|
|
1428
1444
|
? ({
|
|
1429
|
-
'--snap-point-height': `${
|
|
1445
|
+
'--snap-point-height': `${initialSnapPointOffset.current}px`,
|
|
1446
|
+
'transform': getSnapPointTransform(direction),
|
|
1430
1447
|
...style,
|
|
1431
1448
|
'pointerEvents': 'auto',
|
|
1432
1449
|
} as React.CSSProperties)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck — vendored upstream, incompatible with noUncheckedIndexedAccess
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { set, isVertical } from './helpers';
|
|
3
|
+
import { set, isVertical, getSnapPointTransform } from './helpers';
|
|
4
4
|
import { DEFAULT_PEEK_HEIGHT, TRANSITIONS, VELOCITY_THRESHOLD } from './constants';
|
|
5
5
|
import { useControllableState } from './use-controllable-state';
|
|
6
6
|
import type { DrawerDirection } from './types';
|
|
@@ -198,16 +198,12 @@ export function useSnapPoints({
|
|
|
198
198
|
|
|
199
199
|
const animateThisSnap = hasSnappedRef.current || initialAnimated;
|
|
200
200
|
hasSnappedRef.current = true;
|
|
201
|
-
//
|
|
202
|
-
// @property) so layouts derived from it (e.g. the scrollable fill) resize
|
|
203
|
-
// in sync with the drawer's slide instead of jumping to the target.
|
|
201
|
+
// One animated value drives both the translation and the visible height.
|
|
204
202
|
set(drawerRef.current, {
|
|
205
203
|
'transition': animateThisSnap
|
|
206
|
-
?
|
|
204
|
+
? `--snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
|
|
207
205
|
: 'none',
|
|
208
|
-
'transform':
|
|
209
|
-
? `translate3d(0, ${dimension}px, 0)`
|
|
210
|
-
: `translate3d(${dimension}px, 0, 0)`,
|
|
206
|
+
'transform': getSnapPointTransform(direction),
|
|
211
207
|
'--snap-point-height': `${dimension}px`,
|
|
212
208
|
});
|
|
213
209
|
|
|
@@ -364,9 +360,7 @@ export function useSnapPoints({
|
|
|
364
360
|
if ((direction === 'bottom' || direction === 'right') && newValue > snapPointsOffset[0]) {
|
|
365
361
|
const excess = newValue - snapPointsOffset[0];
|
|
366
362
|
set(drawerRef.current, {
|
|
367
|
-
'transform':
|
|
368
|
-
? `translate3d(0, ${snapPointsOffset[0]}px, 0)`
|
|
369
|
-
: `translate3d(${snapPointsOffset[0]}px, 0, 0)`,
|
|
363
|
+
'transform': getSnapPointTransform(direction),
|
|
370
364
|
'--snap-point-height': `${snapPointsOffset[0]}px`,
|
|
371
365
|
});
|
|
372
366
|
setDetachedWrapperTransform(excess, false);
|
|
@@ -378,9 +372,7 @@ export function useSnapPoints({
|
|
|
378
372
|
// derived from it (e.g. the scrollable fill) resize with the drawer
|
|
379
373
|
// instead of staying cut off at the last detent's visible height.
|
|
380
374
|
set(drawerRef.current, {
|
|
381
|
-
'transform':
|
|
382
|
-
? `translate3d(0, ${newValue}px, 0)`
|
|
383
|
-
: `translate3d(${newValue}px, 0, 0)`,
|
|
375
|
+
'transform': getSnapPointTransform(direction),
|
|
384
376
|
'--snap-point-height': `${newValue}px`,
|
|
385
377
|
});
|
|
386
378
|
}
|