@lodev09/react-native-true-sheet 4.0.0-beta.15 → 4.0.0-beta.17

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.
Files changed (32) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +0 -5
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +5 -95
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -1
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +18 -46
  5. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +1 -1
  6. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
  7. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  8. package/ios/TrueSheetFooterView.mm +27 -11
  9. package/ios/TrueSheetView.mm +1 -0
  10. package/lib/module/TrueSheet.web.js +49 -277
  11. package/lib/module/TrueSheet.web.js.map +1 -1
  12. package/lib/module/web/layout.js +168 -0
  13. package/lib/module/web/layout.js.map +1 -0
  14. package/lib/module/web/vaul/helpers.js +1 -0
  15. package/lib/module/web/vaul/helpers.js.map +1 -1
  16. package/lib/module/web/vaul/index.js +9 -4
  17. package/lib/module/web/vaul/index.js.map +1 -1
  18. package/lib/module/web/vaul/use-snap-points.js +6 -8
  19. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  20. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  21. package/lib/typescript/src/web/layout.d.ts +19 -0
  22. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  23. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  24. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  25. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  26. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/TrueSheet.web.tsx +78 -333
  29. package/src/web/layout.ts +196 -0
  30. package/src/web/vaul/helpers.ts +5 -0
  31. package/src/web/vaul/index.tsx +21 -4
  32. 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
+ }
@@ -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;
@@ -20,7 +20,14 @@ import {
20
20
  VELOCITY_THRESHOLD,
21
21
  WINDOW_TOP_OFFSET,
22
22
  } from './constants';
23
- import { dampenValue, getTranslate, isVertical, reset, set } from './helpers';
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 ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
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 ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
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': `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
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
- // `--snap-point-height` transitions alongside `transform` (registered via
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
- ? `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
204
+ ? `--snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
207
205
  : 'none',
208
- 'transform': isVertical(direction)
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': isVertical(direction)
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': isVertical(direction)
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
  }