@lodev09/react-native-true-sheet 4.0.0-beta.15 → 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/TrueSheetOverlayView.kt +11 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
- package/ios/TrueSheetFooterView.mm +27 -11
- package/ios/TrueSheetView.mm +1 -0
- package/lib/module/TrueSheet.web.js +49 -277
- package/lib/module/TrueSheet.web.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.web.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 +1 -1
- package/src/TrueSheet.web.tsx +78 -333
- 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
|
@@ -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
|
}
|