@lodev09/react-native-true-sheet 4.0.0-beta.14 → 4.0.0-beta.15
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/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/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 +19 -5
- package/ios/TrueSheetView.mm +29 -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 +135 -32
- 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/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/package.json +7 -6
- package/src/TrueSheet.tsx +7 -4
- package/src/TrueSheet.types.ts +67 -25
- package/src/TrueSheet.web.tsx +154 -30
- 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/TrueSheet.web.tsx
CHANGED
|
@@ -35,12 +35,16 @@ import type {
|
|
|
35
35
|
WillFocusEvent,
|
|
36
36
|
WillPresentEvent,
|
|
37
37
|
} from './TrueSheet.types';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
measurePeekContentHeight,
|
|
40
|
+
TrueSheetPeekContext,
|
|
41
|
+
type TrueSheetPeekContextValue,
|
|
42
|
+
} from './TrueSheetPeek.web';
|
|
39
43
|
import { usePortalContainer, useRegisterSheet, useSheetStack } from './TrueSheetProvider.web';
|
|
40
44
|
import {
|
|
41
45
|
COLOR_SURFACE_CONTAINER_LOW_DARK,
|
|
42
46
|
COLOR_SURFACE_CONTAINER_LOW_LIGHT,
|
|
43
|
-
|
|
47
|
+
DEFAULT_PLACEMENT_OFFSET,
|
|
44
48
|
DEFAULT_CORNER_RADIUS,
|
|
45
49
|
DEFAULT_DETACHED_OFFSET,
|
|
46
50
|
DEFAULT_FORM_SHEET_HEIGHT_RATIO,
|
|
@@ -78,8 +82,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
78
82
|
backgroundColor: backgroundColorProp,
|
|
79
83
|
maxContentHeight,
|
|
80
84
|
maxContentWidth,
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
placement = 'automatic',
|
|
86
|
+
placementOffset = DEFAULT_PLACEMENT_OFFSET,
|
|
83
87
|
scrollableRef,
|
|
84
88
|
scrollableOptions,
|
|
85
89
|
grabber = true,
|
|
@@ -291,13 +295,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
291
295
|
// within the content — measured by the peek against `contentRef`.
|
|
292
296
|
const [peekContentHeight, setPeekContentHeight] = useState(0);
|
|
293
297
|
const contentRef = useRef<View>(null);
|
|
294
|
-
const peekElRef = useRef<View>(null);
|
|
295
|
-
const peekContext = useMemo(() => ({ contentRef, peekRef: peekElRef, setPeekContentHeight }), []);
|
|
298
|
+
const peekElRef = useRef<View | null>(null);
|
|
296
299
|
|
|
297
|
-
|
|
298
|
-
// it moves it without resizing it, so re-measure its offset whenever the
|
|
299
|
-
// content view's layout changes.
|
|
300
|
-
const handleContentLayout = useCallback(() => {
|
|
300
|
+
const measurePeek = useCallback(() => {
|
|
301
301
|
const peekEl = getDOMElement(peekElRef.current);
|
|
302
302
|
const contentEl = getDOMElement(contentRef.current);
|
|
303
303
|
if (peekEl && contentEl) {
|
|
@@ -305,6 +305,32 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
305
305
|
}
|
|
306
306
|
}, []);
|
|
307
307
|
|
|
308
|
+
// Last-attached wins — during a screen swap the incoming peek mounts before
|
|
309
|
+
// the outgoing one unmounts, so the outgoing detach must not clear it.
|
|
310
|
+
const peekContext = useMemo<TrueSheetPeekContextValue>(
|
|
311
|
+
() => ({
|
|
312
|
+
contentRef,
|
|
313
|
+
attachPeek: (view) => {
|
|
314
|
+
peekElRef.current = view;
|
|
315
|
+
measurePeek();
|
|
316
|
+
},
|
|
317
|
+
detachPeek: (view) => {
|
|
318
|
+
if (peekElRef.current !== view) return;
|
|
319
|
+
peekElRef.current = null;
|
|
320
|
+
setPeekContentHeight(0);
|
|
321
|
+
},
|
|
322
|
+
measurePeek: (view) => {
|
|
323
|
+
if (peekElRef.current === view) measurePeek();
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
[measurePeek]
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// The peek's own onLayout only fires when *it* resizes. Content added above
|
|
330
|
+
// it moves it without resizing it, so re-measure its offset whenever the
|
|
331
|
+
// content view's layout changes.
|
|
332
|
+
const handleContentLayout = measurePeek;
|
|
333
|
+
|
|
308
334
|
// An absolute (floating) header overlaps the content, so it contributes no
|
|
309
335
|
// height to the 'auto' detent measurement.
|
|
310
336
|
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
@@ -354,10 +380,15 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
354
380
|
const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
|
|
355
381
|
const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
|
|
356
382
|
const detectedScrollerRef = useRef<HTMLElement | null>(null);
|
|
383
|
+
// Bottom padding applied to the plugged scrollable for an absolute footer
|
|
384
|
+
// (see the footer inset effect below)
|
|
385
|
+
const footerInsetRef = useRef(0);
|
|
357
386
|
|
|
358
387
|
// Natural content height (header + content + footer, with a detected
|
|
359
388
|
// scrollable's viewport replaced by its content size) — the height the
|
|
360
|
-
// content wants regardless of the sheet's bounds.
|
|
389
|
+
// content wants regardless of the sheet's bounds. An absolute footer's inset
|
|
390
|
+
// on the scrollable counts like a relative footer's height, so the content
|
|
391
|
+
// ends above the footer (mirrors native's auto detent).
|
|
361
392
|
const measureNaturalHeight = useCallback(() => {
|
|
362
393
|
const contentEl = getDOMElement(contentRef.current);
|
|
363
394
|
if (!contentEl || !contentEl.isConnected) return 0;
|
|
@@ -368,7 +399,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
368
399
|
const scroller = detectedScrollerRef.current;
|
|
369
400
|
const scrollContent = scroller?.firstElementChild;
|
|
370
401
|
if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
|
|
371
|
-
height += scrollContent.offsetHeight - scroller.clientHeight;
|
|
402
|
+
height += scrollContent.offsetHeight - scroller.clientHeight + footerInsetRef.current;
|
|
372
403
|
}
|
|
373
404
|
return Math.max(0, height);
|
|
374
405
|
}, []);
|
|
@@ -731,6 +762,99 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
731
762
|
[interpolateFromPosition, updateSizedLayoutHeight]
|
|
732
763
|
);
|
|
733
764
|
|
|
765
|
+
// Web mirror of native's footer inset (`contentInsetAdjustment`
|
|
766
|
+
// 'automatic' | 'footer'): pad the plugged scrollable by how much the
|
|
767
|
+
// absolute footer covers it. Measured from layout offsets (not client rects)
|
|
768
|
+
// so in-flight transforms don't skew it — the footer's bottom sits at the
|
|
769
|
+
// visible bottom of the layout container, so only a scroller reaching that
|
|
770
|
+
// edge gets an inset, and a safe-area lift (or a short scroller) shrinks it.
|
|
771
|
+
// Padding on the scroll container extends its scrollable overflow, like a
|
|
772
|
+
// native content inset.
|
|
773
|
+
const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
|
|
774
|
+
const footerInsetAdjustment =
|
|
775
|
+
Boolean(footer) &&
|
|
776
|
+
absoluteFooter &&
|
|
777
|
+
(insetBehavior === 'automatic' || insetBehavior === 'footer');
|
|
778
|
+
|
|
779
|
+
useEffect(() => {
|
|
780
|
+
if (!isOpen || !footerInsetAdjustment || !scrollableRef) return undefined;
|
|
781
|
+
|
|
782
|
+
let canceled = false;
|
|
783
|
+
let rafId = 0;
|
|
784
|
+
let mutationObserver: MutationObserver | null = null;
|
|
785
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
786
|
+
let scroller: HTMLElement | null = null;
|
|
787
|
+
|
|
788
|
+
const reset = () => {
|
|
789
|
+
if (scroller) scroller.style.paddingBottom = '';
|
|
790
|
+
footerInsetRef.current = 0;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const apply = () => {
|
|
794
|
+
const footerEl = getDOMElement(footerElRef.current);
|
|
795
|
+
const container = sizedLayoutRef.current ?? drawerContentRef.current;
|
|
796
|
+
if (!scroller?.isConnected || !footerEl || !container) return;
|
|
797
|
+
|
|
798
|
+
// Measure without our own padding so an unbounded scroller (sized by its
|
|
799
|
+
// content) doesn't grow by the inset and feed back into the overlap.
|
|
800
|
+
scroller.style.paddingBottom = '';
|
|
801
|
+
let bottom = scroller.offsetTop + scroller.offsetHeight;
|
|
802
|
+
let el = scroller.offsetParent as HTMLElement | null;
|
|
803
|
+
while (el && el !== container) {
|
|
804
|
+
bottom += el.offsetTop;
|
|
805
|
+
el = el.offsetParent as HTMLElement | null;
|
|
806
|
+
}
|
|
807
|
+
const footerTop = container.offsetHeight - footerEl.offsetHeight;
|
|
808
|
+
const inset = el ? Math.max(0, bottom - footerTop) : 0;
|
|
809
|
+
footerInsetRef.current = inset;
|
|
810
|
+
scroller.style.paddingBottom = inset > 0 ? `${inset}px` : '';
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// (Re)resolve the plugged scrollable — a conditional remount swaps the node
|
|
814
|
+
const observe = () => {
|
|
815
|
+
const resolved = getScrollableElement(scrollableRef.current);
|
|
816
|
+
const next = resolved?.isConnected ? resolved : null;
|
|
817
|
+
if (next === scroller && resizeObserver) return;
|
|
818
|
+
|
|
819
|
+
reset();
|
|
820
|
+
resizeObserver?.disconnect();
|
|
821
|
+
resizeObserver = null;
|
|
822
|
+
scroller = next;
|
|
823
|
+
if (!scroller) return;
|
|
824
|
+
|
|
825
|
+
resizeObserver = new ResizeObserver(apply);
|
|
826
|
+
resizeObserver.observe(scroller);
|
|
827
|
+
const footerEl = getDOMElement(footerElRef.current);
|
|
828
|
+
if (footerEl) resizeObserver.observe(footerEl);
|
|
829
|
+
const container = sizedLayoutRef.current ?? drawerContentRef.current;
|
|
830
|
+
if (container) resizeObserver.observe(container);
|
|
831
|
+
apply();
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
const attach = () => {
|
|
835
|
+
if (canceled) return;
|
|
836
|
+
const drawerEl = drawerContentRef.current;
|
|
837
|
+
if (!drawerEl || !drawerEl.isConnected) {
|
|
838
|
+
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
839
|
+
rafId = window.requestAnimationFrame(attach);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
observe();
|
|
843
|
+
mutationObserver = new MutationObserver(observe);
|
|
844
|
+
mutationObserver.observe(drawerEl, { childList: true, subtree: true });
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
rafId = window.requestAnimationFrame(attach);
|
|
848
|
+
|
|
849
|
+
return () => {
|
|
850
|
+
canceled = true;
|
|
851
|
+
window.cancelAnimationFrame(rafId);
|
|
852
|
+
mutationObserver?.disconnect();
|
|
853
|
+
resizeObserver?.disconnect();
|
|
854
|
+
reset();
|
|
855
|
+
};
|
|
856
|
+
}, [isOpen, footerInsetAdjustment, scrollableRef]);
|
|
857
|
+
|
|
734
858
|
// Fire onMount once after first render. React-mount is the earliest point
|
|
735
859
|
// the component is ready for imperative calls, matching the native
|
|
736
860
|
// "ready for present" contract. Declared before the present/dismiss effect
|
|
@@ -1128,13 +1252,13 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1128
1252
|
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
1129
1253
|
// A relative footer owns the inset instead (see resolvedFooterStyle); an
|
|
1130
1254
|
// absolute footer floats over the content, so the content keeps its lift.
|
|
1131
|
-
// A plugged scrollable keeps the lift too (
|
|
1132
|
-
//
|
|
1133
|
-
// with user-applied content padding.
|
|
1255
|
+
// A plugged scrollable keeps the lift too (contentInsetAdjustment
|
|
1256
|
+
// 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
|
|
1257
|
+
// behind the indicator with user-applied content padding.
|
|
1134
1258
|
paddingBottom:
|
|
1135
1259
|
insetAdjustment === 'automatic' &&
|
|
1136
1260
|
!(footerOwnsInset && !absoluteFooter) &&
|
|
1137
|
-
(!scrollableRef ||
|
|
1261
|
+
(!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area')
|
|
1138
1262
|
? 'env(safe-area-inset-bottom, 0px)'
|
|
1139
1263
|
: 0,
|
|
1140
1264
|
}),
|
|
@@ -1145,7 +1269,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1145
1269
|
footerOwnsInset,
|
|
1146
1270
|
absoluteFooter,
|
|
1147
1271
|
scrollableRef,
|
|
1148
|
-
|
|
1272
|
+
insetBehavior,
|
|
1149
1273
|
]
|
|
1150
1274
|
);
|
|
1151
1275
|
|
|
@@ -1184,7 +1308,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1184
1308
|
// `maxContentWidth` is ignored ('form' is absolute).
|
|
1185
1309
|
// - presentation='page' → `maxContentWidth` (any viewport) or
|
|
1186
1310
|
// DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
|
|
1187
|
-
// Detached without a width constraint applies
|
|
1311
|
+
// Detached without a width constraint applies placementOffset on both edges so
|
|
1188
1312
|
// the floating card breathes from the viewport sides.
|
|
1189
1313
|
const wrapperStyle = useMemo<React.CSSProperties | undefined>(() => {
|
|
1190
1314
|
// Mobile portrait ignores width sizing entirely (matches iOS/Android:
|
|
@@ -1203,30 +1327,30 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1203
1327
|
if (dropShadow) next.filter = dropShadow;
|
|
1204
1328
|
if (!needsMargins) return next;
|
|
1205
1329
|
|
|
1206
|
-
let
|
|
1207
|
-
let
|
|
1330
|
+
let marginInlineStart: number | string;
|
|
1331
|
+
let marginInlineEnd: number | string;
|
|
1208
1332
|
if (isFormSheet) {
|
|
1209
|
-
|
|
1210
|
-
|
|
1333
|
+
marginInlineStart = 'auto';
|
|
1334
|
+
marginInlineEnd = 'auto';
|
|
1211
1335
|
} else if (maxWidth == null) {
|
|
1212
|
-
|
|
1213
|
-
|
|
1336
|
+
marginInlineStart = placementOffset;
|
|
1337
|
+
marginInlineEnd = placementOffset;
|
|
1214
1338
|
} else {
|
|
1215
|
-
|
|
1216
|
-
|
|
1339
|
+
marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
|
|
1340
|
+
marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
|
|
1217
1341
|
}
|
|
1218
1342
|
|
|
1219
1343
|
if (maxWidth != null) next.maxWidth = maxWidth;
|
|
1220
|
-
next.
|
|
1221
|
-
next.
|
|
1344
|
+
next.marginInlineStart = marginInlineStart;
|
|
1345
|
+
next.marginInlineEnd = marginInlineEnd;
|
|
1222
1346
|
return next;
|
|
1223
1347
|
}, [
|
|
1224
1348
|
isLandscapeOrTablet,
|
|
1225
1349
|
isFormSheet,
|
|
1226
1350
|
maxContentWidth,
|
|
1227
1351
|
presentation,
|
|
1228
|
-
|
|
1229
|
-
|
|
1352
|
+
placement,
|
|
1353
|
+
placementOffset,
|
|
1230
1354
|
effectiveDetached,
|
|
1231
1355
|
dropShadow,
|
|
1232
1356
|
]);
|
|
@@ -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)';
|