@lodev09/react-native-true-sheet 4.0.0-beta.23 → 4.0.0-beta.25

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 (99) hide show
  1. package/README.md +3 -2
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -0
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +89 -9
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +100 -24
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +13 -10
  7. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -6
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +3 -8
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +14 -1
  10. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +11 -17
  11. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
  12. package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
  13. package/ios/TrueSheetModule.mm +11 -2
  14. package/ios/TrueSheetView.h +2 -0
  15. package/ios/TrueSheetView.mm +62 -7
  16. package/ios/TrueSheetViewController.h +2 -0
  17. package/ios/TrueSheetViewController.mm +158 -4
  18. package/ios/core/RNScreensEventObserver.mm +18 -1
  19. package/ios/core/TrueSheetDimView.h +27 -0
  20. package/ios/core/TrueSheetDimView.mm +41 -0
  21. package/ios/core/TrueSheetKeyboardObserver.mm +11 -8
  22. package/lib/module/TrueSheet.js +88 -17
  23. package/lib/module/TrueSheet.js.map +1 -1
  24. package/lib/module/TrueSheet.web.js +150 -37
  25. package/lib/module/TrueSheet.web.js.map +1 -1
  26. package/lib/module/TrueSheetProvider.web.js +13 -9
  27. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  28. package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  29. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +76 -71
  30. package/lib/module/navigation/createTrueSheetRouter.js +114 -91
  31. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  32. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js +43 -0
  33. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js.map +1 -0
  34. package/lib/module/navigation/expo-router/index.js +9 -4
  35. package/lib/module/navigation/expo-router/index.js.map +1 -1
  36. package/lib/module/scrollable.js +28 -0
  37. package/lib/module/scrollable.js.map +1 -0
  38. package/lib/module/web/keyboard.js +69 -0
  39. package/lib/module/web/keyboard.js.map +1 -0
  40. package/lib/module/web/layout.js +1 -1
  41. package/lib/module/web/layout.js.map +1 -1
  42. package/lib/module/web/vaul/context.js.map +1 -1
  43. package/lib/module/web/vaul/index.js +18 -8
  44. package/lib/module/web/vaul/index.js.map +1 -1
  45. package/lib/module/web/vaul/use-snap-points.js +15 -10
  46. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  47. package/lib/typescript/src/TrueSheet.d.ts +14 -1
  48. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  49. package/lib/typescript/src/TrueSheet.types.d.ts +23 -3
  50. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  51. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  52. package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -1
  53. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  54. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -3
  55. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
  56. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +67 -66
  57. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  58. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +27 -0
  59. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  60. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts +29 -0
  61. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts.map +1 -0
  62. package/lib/typescript/src/navigation/expo-router/index.d.ts +3 -2
  63. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  64. package/lib/typescript/src/navigation/expo-router/types.d.ts +1 -1
  65. package/lib/typescript/src/navigation/types.d.ts +3 -2
  66. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  67. package/lib/typescript/src/scrollable.d.ts +2 -0
  68. package/lib/typescript/src/scrollable.d.ts.map +1 -0
  69. package/lib/typescript/src/web/keyboard.d.ts +15 -0
  70. package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
  71. package/lib/typescript/src/web/layout.d.ts +1 -0
  72. package/lib/typescript/src/web/layout.d.ts.map +1 -1
  73. package/lib/typescript/src/web/vaul/context.d.ts +2 -1
  74. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  75. package/lib/typescript/src/web/vaul/index.d.ts +8 -1
  76. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  77. package/lib/typescript/src/web/vaul/types.d.ts +9 -0
  78. package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
  79. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +3 -2
  80. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  81. package/package.json +5 -4
  82. package/src/TrueSheet.tsx +93 -14
  83. package/src/TrueSheet.types.ts +27 -2
  84. package/src/TrueSheet.web.tsx +170 -41
  85. package/src/TrueSheetProvider.web.tsx +22 -13
  86. package/src/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  87. package/src/fabric/TrueSheetViewNativeComponent.ts +76 -71
  88. package/src/navigation/createTrueSheetRouter.ts +149 -113
  89. package/src/navigation/expo-router/extendTrueSheetRouter.ts +71 -0
  90. package/src/navigation/expo-router/index.ts +14 -6
  91. package/src/navigation/expo-router/types.ts +1 -1
  92. package/src/navigation/types.ts +5 -1
  93. package/src/scrollable.ts +25 -0
  94. package/src/web/keyboard.ts +79 -0
  95. package/src/web/layout.ts +1 -1
  96. package/src/web/vaul/context.ts +2 -1
  97. package/src/web/vaul/index.tsx +20 -5
  98. package/src/web/vaul/types.ts +10 -0
  99. package/src/web/vaul/use-snap-points.ts +19 -9
@@ -0,0 +1,79 @@
1
+ /// <reference lib="dom" />
2
+ import { useEffect, useState } from 'react';
3
+
4
+ import { FOOTER_INSET } from './layout';
5
+ import { getScrollParent } from './vaul/use-prevent-scroll';
6
+ import type { KeyboardViewport } from './vaul/types';
7
+
8
+ // Subpixel tolerance between the visual and layout viewport heights
9
+ const VIEWPORT_EPSILON = 1;
10
+
11
+ const readKeyboardViewport = (): KeyboardViewport | undefined => {
12
+ const vv = window.visualViewport;
13
+ if (!vv) return undefined;
14
+ const layoutHeight = document.documentElement.clientHeight;
15
+ // Pinch-zoom shrinks the visual viewport too — scaling it back out leaves
16
+ // only what the keyboard covers.
17
+ if (vv.height * vv.scale >= layoutHeight - VIEWPORT_EPSILON) return undefined;
18
+ return { top: vv.offsetTop, height: vv.height, layoutHeight };
19
+ };
20
+
21
+ const isSameViewport = (a: KeyboardViewport | undefined, b: KeyboardViewport | undefined) =>
22
+ a === b ||
23
+ (a !== undefined &&
24
+ b !== undefined &&
25
+ a.top === b.top &&
26
+ a.height === b.height &&
27
+ a.layoutHeight === b.layoutHeight);
28
+
29
+ /**
30
+ * Tracks the visual viewport while a soft keyboard shrinks it. iOS Safari and
31
+ * Chrome (default `interactive-widget`) keep the layout viewport — and every
32
+ * fixed element — full height under the keyboard, so the sheet has to be laid
33
+ * out against the visual viewport instead.
34
+ */
35
+ export function useKeyboardViewport(enabled: boolean): KeyboardViewport | undefined {
36
+ const [viewport, setViewport] = useState<KeyboardViewport | undefined>();
37
+
38
+ useEffect(() => {
39
+ const vv = window.visualViewport;
40
+ if (!enabled || !vv) {
41
+ setViewport(undefined);
42
+ return undefined;
43
+ }
44
+
45
+ const update = () => {
46
+ const next = readKeyboardViewport();
47
+ setViewport((current) => (isSameViewport(current, next) ? current : next));
48
+ };
49
+
50
+ update();
51
+ vv.addEventListener('resize', update);
52
+ vv.addEventListener('scroll', update);
53
+ window.addEventListener('resize', update);
54
+ return () => {
55
+ vv.removeEventListener('resize', update);
56
+ vv.removeEventListener('scroll', update);
57
+ window.removeEventListener('resize', update);
58
+ };
59
+ }, [enabled]);
60
+
61
+ return viewport;
62
+ }
63
+
64
+ /**
65
+ * Scrolls the focused field in `container` back above its scroll parent's
66
+ * bottom edge, clearing a floating footer. The browser only reveals it on
67
+ * focus — before the sheet fits above the keyboard.
68
+ */
69
+ export function revealFocusedElement(container: HTMLElement) {
70
+ const focused = document.activeElement;
71
+ if (!(focused instanceof HTMLElement) || !container.contains(focused)) return;
72
+ const scroller = getScrollParent(focused);
73
+ if (!container.contains(scroller)) return;
74
+ const footerInset = parseFloat(getComputedStyle(scroller).getPropertyValue(FOOTER_INSET)) || 0;
75
+ const overflow =
76
+ focused.getBoundingClientRect().bottom -
77
+ (scroller.getBoundingClientRect().bottom - footerInset);
78
+ if (overflow > 0) scroller.scrollTop += overflow;
79
+ }
package/src/web/layout.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="dom" />
2
2
 
3
- const FOOTER_INSET = '--truesheet-footer-inset';
3
+ export const FOOTER_INSET = '--truesheet-footer-inset';
4
4
 
5
5
  interface SheetLayoutOptions {
6
6
  drawer: HTMLElement;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { DrawerDirection } from './types';
2
+ import type { DrawerDirection, KeyboardViewport } from './types';
3
3
 
4
4
  export interface DragEvent {
5
5
  target: EventTarget;
@@ -54,6 +54,7 @@ interface DrawerContextValue {
54
54
  detachedOffset: number;
55
55
  detachedRadius: number;
56
56
  detachedWrapperStyle?: React.CSSProperties;
57
+ keyboardViewport?: KeyboardViewport;
57
58
  }
58
59
 
59
60
  export const DrawerContext = React.createContext<DrawerContextValue>({
@@ -28,7 +28,7 @@ import {
28
28
  reset,
29
29
  set,
30
30
  } from './helpers';
31
- import type { DrawerDirection } from './types';
31
+ import type { DrawerDirection, KeyboardViewport } from './types';
32
32
  import { useComposedRefs } from './use-composed-refs';
33
33
  import { useControllableState } from './use-controllable-state';
34
34
  import { usePositionFixed } from './use-position-fixed';
@@ -224,6 +224,12 @@ export type DialogProps = {
224
224
  * from natural flow.
225
225
  */
226
226
  contentHeight?: number;
227
+ /**
228
+ * Lays the drawer out in the visual viewport above a soft keyboard. The
229
+ * wrapper and snap offsets fit the visible area, while fractional snap
230
+ * points keep scaling against the full layout height.
231
+ */
232
+ keyboardViewport?: KeyboardViewport;
227
233
  } & (WithFadeFromProps | WithoutFadeFromProps);
228
234
 
229
235
  export function Root({
@@ -269,6 +275,7 @@ export function Root({
269
275
  initialAnimated = true,
270
276
  onContentHeightChange,
271
277
  contentHeight: contentHeightProp,
278
+ keyboardViewport,
272
279
  }: DialogProps) {
273
280
  const [isOpen = false, setIsOpen] = useControllableState({
274
281
  defaultProp: defaultOpen,
@@ -352,6 +359,7 @@ export function Root({
352
359
  maxContentHeight,
353
360
  peekHeight,
354
361
  initialAnimated,
362
+ keyboardViewport,
355
363
  });
356
364
 
357
365
  usePreventScroll({
@@ -1049,6 +1057,7 @@ export function Root({
1049
1057
  detachedOffset: detached ? detachedOffset : 0,
1050
1058
  detachedRadius: detached ? detachedRadius : 0,
1051
1059
  detachedWrapperStyle,
1060
+ keyboardViewport,
1052
1061
  }}
1053
1062
  >
1054
1063
  {children}
@@ -1191,6 +1200,7 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1191
1200
  detachedOffset,
1192
1201
  detachedRadius,
1193
1202
  detachedWrapperStyle: detachedWrapperStyleProp,
1203
+ keyboardViewport,
1194
1204
  } = useDrawerContext();
1195
1205
  // Always measure the inner wrapper's natural height. The drawer itself may
1196
1206
  // be styled to a fixed viewport height, so we measure an inner wrapper
@@ -1393,14 +1403,19 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1393
1403
  // with a bottom gap and rounded bottom corners; otherwise it sits flush.
1394
1404
  // Transform/transition are managed imperatively (via drag overshoot and the
1395
1405
  // dismiss effect) so React doesn't skip DOM writes for values it thinks it
1396
- // already owns.
1406
+ // already owns. Above a soft keyboard the wrapper fits the visual viewport,
1407
+ // which the full-height layout viewport extends behind.
1397
1408
  const wrapperStyle = React.useMemo<React.CSSProperties>(
1398
1409
  () => ({
1399
1410
  position: 'fixed',
1400
- top: 0,
1401
1411
  left: 0,
1402
1412
  right: 0,
1403
- bottom: detached ? detachedOffset : 0,
1413
+ ...(keyboardViewport
1414
+ ? {
1415
+ top: keyboardViewport.top,
1416
+ height: Math.max(0, keyboardViewport.height - (detached ? detachedOffset : 0)),
1417
+ }
1418
+ : { top: 0, bottom: detached ? detachedOffset : 0 }),
1404
1419
  overflow: 'hidden',
1405
1420
  contain: 'paint',
1406
1421
  pointerEvents: 'none',
@@ -1408,7 +1423,7 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1408
1423
  borderBottomRightRadius: detached ? detachedRadius : 0,
1409
1424
  ...detachedWrapperStyleProp,
1410
1425
  }),
1411
- [detached, detachedOffset, detachedRadius, detachedWrapperStyleProp]
1426
+ [detached, detachedOffset, detachedRadius, detachedWrapperStyleProp, keyboardViewport]
1412
1427
  );
1413
1428
 
1414
1429
  // Translate the wrapper off-screen on dismiss so the whole card slides out
@@ -5,3 +5,13 @@ export interface SnapPoint {
5
5
  }
6
6
 
7
7
  export type AnyFunction = (...args: any) => any;
8
+
9
+ /**
10
+ * The visual viewport while a soft keyboard shrinks it, in layout-viewport px.
11
+ * `layoutHeight` is the full (keyboard-free) height.
12
+ */
13
+ export interface KeyboardViewport {
14
+ top: number;
15
+ height: number;
16
+ layoutHeight: number;
17
+ }
@@ -9,7 +9,7 @@ import {
9
9
  VELOCITY_THRESHOLD,
10
10
  } from './constants';
11
11
  import { useControllableState } from './use-controllable-state';
12
- import type { DrawerDirection } from './types';
12
+ import type { DrawerDirection, KeyboardViewport } from './types';
13
13
 
14
14
  export function useSnapPoints({
15
15
  activeSnapPointProp,
@@ -28,6 +28,7 @@ export function useSnapPoints({
28
28
  maxContentHeight,
29
29
  peekHeight,
30
30
  initialAnimated = true,
31
+ keyboardViewport,
31
32
  }: {
32
33
  activeSnapPointProp?: number | string | null;
33
34
  setActiveSnapPointProp?(snapPoint: number | null | string): void;
@@ -45,6 +46,7 @@ export function useSnapPoints({
45
46
  maxContentHeight?: number;
46
47
  peekHeight?: number;
47
48
  initialAnimated?: boolean;
49
+ keyboardViewport?: KeyboardViewport;
48
50
  }) {
49
51
  const [activeSnapPoint, setActiveSnapPoint] = useControllableState<string | number | null>({
50
52
  prop: activeSnapPointProp,
@@ -98,13 +100,18 @@ export function useSnapPoints({
98
100
  height: container.getBoundingClientRect().height,
99
101
  }
100
102
  : typeof window !== 'undefined'
101
- ? { width: window.innerWidth, height: window.innerHeight }
103
+ ? { width: window.innerWidth, height: keyboardViewport?.height ?? window.innerHeight }
102
104
  : { width: 0, height: 0 };
103
105
 
104
106
  // Shrink the effective vertical area by detachedOffset so snap math
105
107
  // reserves a fixed gap at the bottom — the floating card stays offset
106
108
  // from the viewport edge through drag, snap and resize.
107
109
  const effectiveHeight = Math.max(0, containerSize.height - (detachedOffset || 0));
110
+ // Above a keyboard, fractional snap points keep their full-height size and
111
+ // only cap at the visible area — mirrors native, where the sheet rises.
112
+ const fractionHeight = keyboardViewport
113
+ ? Math.max(0, keyboardViewport.layoutHeight - (detachedOffset || 0))
114
+ : effectiveHeight;
108
115
 
109
116
  return (
110
117
  snapPoints?.map((snapPoint) => {
@@ -129,11 +136,11 @@ export function useSnapPoints({
129
136
  const rawHeight = isPx
130
137
  ? snapPointAsNumber
131
138
  : windowDimensions
132
- ? resolved * effectiveHeight
139
+ ? resolved * fractionHeight
133
140
  : 0;
134
- // Cap px/'auto' snap points so a content taller than the viewport
135
- // doesn't push the drawer past the top edge (offset would go
136
- // negative). Fractional detents are already ≤ effectiveHeight.
141
+ // Cap snap points so content taller than the viewport (or a
142
+ // fraction above a keyboard) doesn't push the drawer past the top
143
+ // edge (offset would go negative).
137
144
  // `maxContentHeight`, when set, caps the visible sheet further so
138
145
  // full/'auto' detents respect the user's ceiling.
139
146
  const ceiling =
@@ -170,8 +177,11 @@ export function useSnapPoints({
170
177
  detachedOffset,
171
178
  maxContentHeight,
172
179
  peekHeight,
180
+ keyboardViewport,
173
181
  ]);
174
182
 
183
+ const getViewportHeight = () => keyboardViewport?.height ?? window.innerHeight;
184
+
175
185
  const activeSnapPointOffset = React.useMemo(
176
186
  () => (activeSnapPointIndex !== null ? snapPointsOffset?.[activeSnapPointIndex] : null),
177
187
  [snapPointsOffset, activeSnapPointIndex]
@@ -334,7 +344,7 @@ export function useSnapPoints({
334
344
  (direction === 'bottom' || direction === 'right') &&
335
345
  currentPosition > snapPointsOffset[0]
336
346
  ) {
337
- const viewportDim = isVertical(direction) ? window.innerHeight : window.innerWidth;
347
+ const viewportDim = isVertical(direction) ? getViewportHeight() : window.innerWidth;
338
348
  const effectiveDim = Math.max(0, viewportDim - (detachedOffset || 0));
339
349
  const visibleAtLowest = Math.max(0, effectiveDim - snapPointsOffset[0]);
340
350
  const overshoot = currentPosition - snapPointsOffset[0];
@@ -362,7 +372,7 @@ export function useSnapPoints({
362
372
  return Math.abs(curr - currentPosition) < Math.abs(prev - currentPosition) ? curr : prev;
363
373
  });
364
374
 
365
- const dim = isVertical(direction) ? window.innerHeight : window.innerWidth;
375
+ const dim = isVertical(direction) ? getViewportHeight() : window.innerWidth;
366
376
  if (velocity > VELOCITY_THRESHOLD && Math.abs(draggedDistance) < dim * 0.4) {
367
377
  const dragDirection = hasDraggedUp ? 1 : -1; // 1 = up, -1 = down
368
378
 
@@ -444,7 +454,7 @@ export function useSnapPoints({
444
454
  if (activeSnapPointIndex === 0 && !isDraggingDown) {
445
455
  const activeOffset = snapPointsOffset[activeSnapPointIndex];
446
456
  if (activeOffset === undefined) return 0;
447
- const windowDim = isVertical(direction) ? window.innerHeight : window.innerWidth;
457
+ const windowDim = isVertical(direction) ? getViewportHeight() : window.innerWidth;
448
458
  const dismissOffset =
449
459
  direction === 'bottom' || direction === 'right' ? windowDim : -windowDim;
450
460
  const dismissRange = Math.abs(dismissOffset - activeOffset);