@lodev09/react-native-true-sheet 4.0.0-beta.1 → 4.0.0-beta.2
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/TrueSheetFooterView.kt +8 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +9 -2
- package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
- package/ios/TrueSheetViewController.mm +1 -4
- package/lib/module/TrueSheet.js +12 -6
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +11 -9
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetPeek.web.js +3 -3
- package/lib/module/TrueSheetPeek.web.js.map +1 -1
- package/lib/module/mocks/navigation.js +31 -21
- package/lib/module/mocks/navigation.js.map +1 -1
- package/lib/module/mocks/navigationExpoRouter.js +21 -0
- package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
- package/lib/module/mocks/reanimated.js +1 -1
- package/lib/module/mocks/reanimated.js.map +1 -1
- package/lib/module/navigation/{TrueSheetView.js → TrueSheetNavigatorContent.js} +14 -12
- package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
- package/lib/module/navigation/actions.js +50 -0
- package/lib/module/navigation/actions.js.map +1 -0
- package/lib/module/navigation/{TrueSheetRouter.js → createTrueSheetRouter.js} +20 -24
- package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
- package/lib/module/navigation/expo-router/index.js +48 -0
- package/lib/module/navigation/expo-router/index.js.map +1 -0
- package/lib/module/navigation/index.js +43 -2
- package/lib/module/navigation/index.js.map +1 -1
- package/lib/module/navigation/navigator.js +10 -0
- package/lib/module/navigation/navigator.js.map +1 -0
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +2 -2
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -1
- package/lib/module/navigation/screen/TrueSheetScreen.js +2 -2
- package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -1
- package/lib/module/navigation/screen/useSheetScreenState.js +4 -4
- package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
- package/lib/module/navigation/useTrueSheetNavigation.js +1 -1
- package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -1
- package/lib/module/web/dom.js +10 -0
- package/lib/module/web/dom.js.map +1 -0
- package/lib/module/web/vaul/helpers.js +10 -4
- package/lib/module/web/vaul/helpers.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
- package/lib/typescript/src/mocks/navigation.d.ts +5 -1
- package/lib/typescript/src/mocks/navigation.d.ts.map +1 -1
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
- package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -1
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
- package/lib/typescript/src/navigation/actions.d.ts +71 -0
- package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +14 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/index.d.ts +42 -2
- package/lib/typescript/src/navigation/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/navigator.d.ts +7 -0
- package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +1 -1
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +1 -1
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/types.d.ts +3 -4
- package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +3 -5
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +24 -0
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +1 -1
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -1
- package/lib/typescript/src/web/dom.d.ts +7 -0
- package/lib/typescript/src/web/dom.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
- package/package.json +23 -8
- package/src/TrueSheet.tsx +11 -8
- package/src/TrueSheet.web.tsx +14 -18
- package/src/TrueSheetPeek.web.tsx +4 -3
- package/src/mocks/navigation.ts +32 -25
- package/src/mocks/navigationExpoRouter.ts +40 -0
- package/src/mocks/reanimated.ts +8 -9
- package/src/navigation/{TrueSheetView.tsx → TrueSheetNavigatorContent.tsx} +24 -17
- package/src/navigation/actions.ts +78 -0
- package/src/navigation/createTrueSheetRouter.ts +222 -0
- package/src/navigation/expo-router/index.ts +89 -0
- package/src/navigation/index.ts +69 -2
- package/src/navigation/navigator.ts +18 -0
- package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +2 -2
- package/src/navigation/screen/TrueSheetScreen.tsx +2 -2
- package/src/navigation/screen/types.ts +4 -6
- package/src/navigation/screen/useSheetScreenState.ts +8 -15
- package/src/navigation/types.ts +32 -0
- package/src/navigation/useTrueSheetNavigation.ts +1 -1
- package/src/web/dom.ts +9 -0
- package/src/web/vaul/helpers.ts +13 -6
- package/lib/module/navigation/TrueSheetRouter.js.map +0 -1
- package/lib/module/navigation/TrueSheetView.js.map +0 -1
- package/lib/module/navigation/createTrueSheetNavigator.js +0 -63
- package/lib/module/navigation/createTrueSheetNavigator.js.map +0 -1
- package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +0 -57
- package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +0 -1
- package/lib/typescript/src/navigation/TrueSheetView.d.ts +0 -10
- package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +0 -1
- package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +0 -35
- package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +0 -1
- package/src/navigation/TrueSheetRouter.ts +0 -234
- package/src/navigation/createTrueSheetNavigator.tsx +0 -93
package/src/TrueSheet.tsx
CHANGED
|
@@ -553,16 +553,19 @@ export class TrueSheet
|
|
|
553
553
|
onWillBlur={this.onWillBlur}
|
|
554
554
|
onDidBlur={this.onDidBlur}
|
|
555
555
|
onVisibilityChange={this.onVisibilityChange}
|
|
556
|
+
// These must stay on this host view, not on the container. The container is
|
|
557
|
+
// reparented into the native sheet, so this view is never a native ancestor of
|
|
558
|
+
// the sheet content. Claiming the responder here stops the negotiation at the
|
|
559
|
+
// sheet boundary without making an ancestor of the content the JS responder,
|
|
560
|
+
// which would block scrolling inside the sheet.
|
|
561
|
+
onStartShouldSetResponder={absorbUnclaimedTouches}
|
|
562
|
+
onTouchStart={stopTouchPropagation}
|
|
563
|
+
onTouchMove={stopTouchPropagation}
|
|
564
|
+
onTouchEnd={stopTouchPropagation}
|
|
565
|
+
onTouchCancel={stopTouchPropagation}
|
|
556
566
|
>
|
|
557
567
|
{this.state.shouldRenderNativeView && (
|
|
558
|
-
<TrueSheetContainerViewNativeComponent
|
|
559
|
-
style={styles.container}
|
|
560
|
-
onStartShouldSetResponder={absorbUnclaimedTouches}
|
|
561
|
-
onTouchStart={stopTouchPropagation}
|
|
562
|
-
onTouchMove={stopTouchPropagation}
|
|
563
|
-
onTouchEnd={stopTouchPropagation}
|
|
564
|
-
onTouchCancel={stopTouchPropagation}
|
|
565
|
-
>
|
|
568
|
+
<TrueSheetContainerViewNativeComponent style={styles.container}>
|
|
566
569
|
{header && (
|
|
567
570
|
<TrueSheetHeaderViewNativeComponent
|
|
568
571
|
style={[
|
package/src/TrueSheet.web.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
useRef,
|
|
11
11
|
useState,
|
|
12
12
|
} from 'react';
|
|
13
|
-
import type {
|
|
13
|
+
import type { DimensionValue, LayoutChangeEvent } from 'react-native';
|
|
14
14
|
import { StyleSheet, useColorScheme, useWindowDimensions, View } from 'react-native';
|
|
15
15
|
|
|
16
16
|
import type {
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
DEFAULT_GRABBER_WIDTH,
|
|
52
52
|
DEFAULT_MAX_WIDTH,
|
|
53
53
|
} from './web/constants';
|
|
54
|
+
import { getDOMElement } from './web/dom';
|
|
54
55
|
import { Drawer } from './web/vaul';
|
|
55
56
|
import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from './web/vaul/constants';
|
|
56
57
|
|
|
@@ -302,8 +303,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
302
303
|
return [
|
|
303
304
|
footerStyle,
|
|
304
305
|
{
|
|
305
|
-
|
|
306
|
-
|
|
306
|
+
// RN types don't model CSS calc(), but RN-web passes it through
|
|
307
|
+
paddingBottom: `calc(${baseCss} + env(safe-area-inset-bottom, 0px))` as DimensionValue,
|
|
308
|
+
},
|
|
307
309
|
];
|
|
308
310
|
}, [footerOwnsInset, footerStyle]);
|
|
309
311
|
|
|
@@ -327,14 +329,10 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
327
329
|
// scrollable's viewport replaced by its content size) — the height the
|
|
328
330
|
// content wants regardless of the sheet's bounds.
|
|
329
331
|
const measureNaturalHeight = useCallback(() => {
|
|
330
|
-
const contentEl = contentRef.current
|
|
332
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
331
333
|
if (!contentEl || !contentEl.isConnected) return 0;
|
|
332
|
-
const headerEl = absoluteHeaderRef.current
|
|
333
|
-
|
|
334
|
-
: (headerElRef.current as unknown as HTMLElement | null);
|
|
335
|
-
const footerEl = absoluteFooterRef.current
|
|
336
|
-
? null
|
|
337
|
-
: (footerElRef.current as unknown as HTMLElement | null);
|
|
334
|
+
const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
|
|
335
|
+
const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
|
|
338
336
|
let height =
|
|
339
337
|
(headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
|
|
340
338
|
const scroller = pinnedScrollerRef.current;
|
|
@@ -377,8 +375,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
377
375
|
// observation). Resolves the content node live: the layout-branch swap
|
|
378
376
|
// (natural flow ↔ sized) remounts it.
|
|
379
377
|
const observe = () => {
|
|
380
|
-
const contentEl = contentRef.current
|
|
381
|
-
const headerEl = headerElRef.current
|
|
378
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
379
|
+
const headerEl = getDOMElement(headerElRef.current);
|
|
382
380
|
|
|
383
381
|
// Skip mutations that don't change the observed topology — e.g. a
|
|
384
382
|
// virtualized list mounting rows inside the pinned scroller, which
|
|
@@ -549,13 +547,11 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
549
547
|
// measure either: they fire while the portal subtree is still detached.
|
|
550
548
|
// Geometry only runs while the drawer is mounted, so the elements are
|
|
551
549
|
// measurable here; fall back to the state value when they're absent.
|
|
552
|
-
const headerEl = headerElRef.current
|
|
550
|
+
const headerEl = getDOMElement(headerElRef.current);
|
|
553
551
|
// A relative footer is pushed off-screen at the peek detent — excluded
|
|
554
|
-
const footerEl = absoluteFooterRef.current
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const peekEl = peekElRef.current as unknown as HTMLElement | null;
|
|
558
|
-
const contentEl = contentRef.current as unknown as HTMLElement | null;
|
|
552
|
+
const footerEl = absoluteFooterRef.current ? getDOMElement(footerElRef.current) : null;
|
|
553
|
+
const peekEl = getDOMElement(peekElRef.current);
|
|
554
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
559
555
|
const livePeekContentHeight =
|
|
560
556
|
peekEl && contentEl ? measurePeekContentHeight(peekEl, contentEl) : inputs.peekContentHeight;
|
|
561
557
|
const livePeekHeight =
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createContext, type RefObject, useContext, useEffect, useRef } from 'react';
|
|
2
2
|
import { type LayoutChangeEvent, View, type ViewProps } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import { getDOMElement } from './web/dom';
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Reports the measured peek content height to the owning TrueSheet.
|
|
6
8
|
* @internal
|
|
@@ -44,9 +46,8 @@ export const TrueSheetPeek = ({ onLayout, ...rest }: ViewProps) => {
|
|
|
44
46
|
|
|
45
47
|
const handleLayout = (event: LayoutChangeEvent) => {
|
|
46
48
|
if (context) {
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const contentElement = context.contentRef.current as unknown as HTMLElement | null;
|
|
49
|
+
const peekElement = getDOMElement(viewRef.current);
|
|
50
|
+
const contentElement = getDOMElement(context.contentRef.current);
|
|
50
51
|
|
|
51
52
|
const bottom =
|
|
52
53
|
peekElement && contentElement
|
package/src/mocks/navigation.ts
CHANGED
|
@@ -12,6 +12,11 @@ export const createTrueSheetNavigator = jest.fn(() => ({
|
|
|
12
12
|
Group: jest.fn(() => null),
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Mock createTrueSheetScreen for testing the static API.
|
|
17
|
+
*/
|
|
18
|
+
export const createTrueSheetScreen = jest.fn((config: object) => config);
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* Mock TrueSheetActions for testing.
|
|
17
22
|
*/
|
|
@@ -35,32 +40,34 @@ export const TrueSheetActions = {
|
|
|
35
40
|
/**
|
|
36
41
|
* Mock useTrueSheetNavigation hook for testing.
|
|
37
42
|
*/
|
|
38
|
-
export const useTrueSheetNavigation = jest.fn(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
export const useTrueSheetNavigation = jest.fn(<T extends ParamListBase = ParamListBase>() => {
|
|
44
|
+
// Partial drops react-navigation's private-brand member (PrivateValueStore),
|
|
45
|
+
// which no mock can implement — the members themselves stay type-checked.
|
|
46
|
+
const navigation: Partial<TrueSheetNavigationProp<T>> = {
|
|
47
|
+
navigate: jest.fn(),
|
|
48
|
+
goBack: jest.fn(),
|
|
49
|
+
reset: jest.fn(),
|
|
50
|
+
setParams: jest.fn(),
|
|
51
|
+
dispatch: jest.fn(),
|
|
52
|
+
isFocused: jest.fn(() => true),
|
|
53
|
+
canGoBack: jest.fn(() => true),
|
|
54
|
+
getId: jest.fn(),
|
|
55
|
+
getParent: jest.fn(),
|
|
56
|
+
getState: jest.fn(),
|
|
57
|
+
addListener: jest.fn(() => jest.fn()),
|
|
58
|
+
removeListener: jest.fn(),
|
|
59
|
+
setOptions: jest.fn(),
|
|
60
|
+
push: jest.fn(),
|
|
61
|
+
pop: jest.fn(),
|
|
62
|
+
popTo: jest.fn(),
|
|
63
|
+
popToTop: jest.fn(),
|
|
64
|
+
replace: jest.fn(),
|
|
65
|
+
resize: jest.fn(),
|
|
66
|
+
};
|
|
67
|
+
return navigation as TrueSheetNavigationProp<T>;
|
|
68
|
+
});
|
|
62
69
|
|
|
63
|
-
export type { TrueSheetActionType } from '../navigation/
|
|
70
|
+
export type { TrueSheetActionType } from '../navigation/actions';
|
|
64
71
|
export type { DetentInfoEventPayload, PositionChangeEventPayload } from '../TrueSheet.types';
|
|
65
72
|
export type {
|
|
66
73
|
TrueSheetNavigationEventMap,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ParamListBase } from '@react-navigation/native';
|
|
2
|
+
|
|
3
|
+
import type { TrueSheetNavigationProp } from '../navigation/types';
|
|
4
|
+
import {
|
|
5
|
+
TrueSheetActions,
|
|
6
|
+
useTrueSheetNavigation as useTrueSheetNavigationMock,
|
|
7
|
+
} from './navigation';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mock Sheet layout for testing.
|
|
11
|
+
* Import from '@lodev09/react-native-true-sheet/navigation/expo-router/mock' in your test setup.
|
|
12
|
+
*/
|
|
13
|
+
export const Sheet = Object.assign(
|
|
14
|
+
jest.fn(({ children }: { children: React.ReactNode }) => children),
|
|
15
|
+
{
|
|
16
|
+
Screen: jest.fn(() => null),
|
|
17
|
+
Protected: jest.fn(() => null),
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Mock useTrueSheetNavigation hook for testing.
|
|
23
|
+
*/
|
|
24
|
+
export const useTrueSheetNavigation = jest.fn(
|
|
25
|
+
<T extends ParamListBase = ParamListBase>(): TrueSheetNavigationProp<T> =>
|
|
26
|
+
useTrueSheetNavigationMock() as TrueSheetNavigationProp<T>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export { TrueSheetActions };
|
|
30
|
+
|
|
31
|
+
export type { TrueSheetActionType } from '../navigation/actions';
|
|
32
|
+
export type { DetentInfoEventPayload, PositionChangeEventPayload } from '../TrueSheet.types';
|
|
33
|
+
export type {
|
|
34
|
+
TrueSheetNavigationEventMap,
|
|
35
|
+
TrueSheetNavigationHelpers,
|
|
36
|
+
TrueSheetNavigationOptions,
|
|
37
|
+
TrueSheetNavigationProp,
|
|
38
|
+
TrueSheetNavigationState,
|
|
39
|
+
TrueSheetScreenProps,
|
|
40
|
+
} from '../navigation/types';
|
package/src/mocks/reanimated.ts
CHANGED
|
@@ -14,15 +14,14 @@ interface MockReanimatedTrueSheetContextValue {
|
|
|
14
14
|
animatedDetent: SharedValue<number>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const createMockSharedValue = <T>(initialValue: T): SharedValue<T> =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}) as unknown as SharedValue<T>;
|
|
17
|
+
const createMockSharedValue = <T>(initialValue: T): SharedValue<T> => ({
|
|
18
|
+
value: initialValue,
|
|
19
|
+
get: () => initialValue,
|
|
20
|
+
set: () => {},
|
|
21
|
+
addListener: () => {},
|
|
22
|
+
removeListener: () => {},
|
|
23
|
+
modify: () => {},
|
|
24
|
+
});
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* Mock ReanimatedTrueSheet component for testing.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NavigatorArgs } from 'standard-navigation';
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
TrueSheetNavigationOptions,
|
|
5
|
+
TrueSheetNavigatorContentExtraProps,
|
|
6
|
+
TrueSheetStandardEventMap,
|
|
7
7
|
} from './types';
|
|
8
8
|
import { TrueSheetScreen, type TrueSheetScreenProps } from './screen';
|
|
9
9
|
|
|
@@ -22,15 +22,22 @@ const DEFAULT_DETENTS: ('auto' | number)[] = ['auto'];
|
|
|
22
22
|
const clampDetentIndex = (index: number, detentsLength: number): number =>
|
|
23
23
|
Math.min(index, Math.max(detentsLength - 1, 0));
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
export type TrueSheetNavigatorContentProps = NavigatorArgs<
|
|
26
|
+
TrueSheetNavigationOptions,
|
|
27
|
+
TrueSheetStandardEventMap
|
|
28
|
+
> &
|
|
29
|
+
TrueSheetNavigatorContentExtraProps;
|
|
30
30
|
|
|
31
|
-
export const
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
export const TrueSheetNavigatorContent = ({
|
|
32
|
+
routes,
|
|
33
|
+
dispatch,
|
|
34
|
+
descriptors,
|
|
35
|
+
emitter,
|
|
36
|
+
}: TrueSheetNavigatorContentProps) => {
|
|
37
|
+
// First route is the base screen, rest are sheets.
|
|
38
|
+
// Rendered from the raw routes (not the standard state) so custom fields are
|
|
39
|
+
// available and preloaded routes never present as sheets.
|
|
40
|
+
const [baseRoute, ...sheetRoutes] = routes;
|
|
34
41
|
|
|
35
42
|
const baseDescriptor = baseRoute ? descriptors[baseRoute.key] : null;
|
|
36
43
|
|
|
@@ -47,7 +54,7 @@ export const TrueSheetView = ({ state, navigation, descriptors }: TrueSheetViewP
|
|
|
47
54
|
return null;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
const { options,
|
|
57
|
+
const { options, render } = descriptor;
|
|
51
58
|
const {
|
|
52
59
|
detentIndex = 0,
|
|
53
60
|
detents = DEFAULT_DETENTS,
|
|
@@ -67,8 +74,8 @@ export const TrueSheetView = ({ state, navigation, descriptors }: TrueSheetViewP
|
|
|
67
74
|
detentIndex={resolvedIndex}
|
|
68
75
|
resizeKey={route.resizeKey}
|
|
69
76
|
detents={detents}
|
|
70
|
-
|
|
71
|
-
emit={
|
|
77
|
+
dispatch={dispatch}
|
|
78
|
+
emit={emitter.emit}
|
|
72
79
|
positionChangeHandler={positionChangeHandler}
|
|
73
80
|
{...sheetProps}
|
|
74
81
|
>
|
|
@@ -85,8 +92,8 @@ export const TrueSheetView = ({ state, navigation, descriptors }: TrueSheetViewP
|
|
|
85
92
|
detentIndex={resolvedIndex}
|
|
86
93
|
resizeKey={route.resizeKey}
|
|
87
94
|
detents={detents}
|
|
88
|
-
|
|
89
|
-
emit={
|
|
95
|
+
dispatch={dispatch}
|
|
96
|
+
emit={emitter.emit}
|
|
90
97
|
positionChangeHandler={positionChangeHandler}
|
|
91
98
|
{...sheetProps}
|
|
92
99
|
>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type TrueSheetActionType =
|
|
2
|
+
| {
|
|
3
|
+
type: 'PUSH';
|
|
4
|
+
payload: { name: string; params?: object };
|
|
5
|
+
source?: string;
|
|
6
|
+
target?: string;
|
|
7
|
+
}
|
|
8
|
+
| {
|
|
9
|
+
type: 'POP';
|
|
10
|
+
payload: { count: number };
|
|
11
|
+
source?: string;
|
|
12
|
+
target?: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
type: 'POP_TO';
|
|
16
|
+
payload: { name: string; params?: object; merge?: boolean };
|
|
17
|
+
source?: string;
|
|
18
|
+
target?: string;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
type: 'POP_TO_TOP';
|
|
22
|
+
source?: string;
|
|
23
|
+
target?: string;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
type: 'REPLACE';
|
|
27
|
+
payload: { name: string; params?: object };
|
|
28
|
+
source?: string;
|
|
29
|
+
target?: string;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
type: 'GO_BACK';
|
|
33
|
+
source?: string;
|
|
34
|
+
target?: string;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
type: 'RESIZE';
|
|
38
|
+
index: number;
|
|
39
|
+
source?: string;
|
|
40
|
+
target?: string;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
type: 'DISMISS';
|
|
44
|
+
source?: string;
|
|
45
|
+
target?: string;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
type: 'REMOVE';
|
|
49
|
+
source?: string;
|
|
50
|
+
target?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Action creators for the TrueSheet navigator.
|
|
55
|
+
* Plain objects matching `StackActions` shapes, plus sheet-specific actions.
|
|
56
|
+
*/
|
|
57
|
+
export const TrueSheetActions = {
|
|
58
|
+
push: (name: string, params?: object): TrueSheetActionType => ({
|
|
59
|
+
type: 'PUSH',
|
|
60
|
+
payload: { name, params },
|
|
61
|
+
}),
|
|
62
|
+
pop: (count: number = 1): TrueSheetActionType => ({
|
|
63
|
+
type: 'POP',
|
|
64
|
+
payload: { count },
|
|
65
|
+
}),
|
|
66
|
+
popTo: (name: string, params?: object, options?: { merge?: boolean }): TrueSheetActionType => ({
|
|
67
|
+
type: 'POP_TO',
|
|
68
|
+
payload: { name, params, merge: options?.merge },
|
|
69
|
+
}),
|
|
70
|
+
popToTop: (): TrueSheetActionType => ({ type: 'POP_TO_TOP' }),
|
|
71
|
+
replace: (name: string, params?: object): TrueSheetActionType => ({
|
|
72
|
+
type: 'REPLACE',
|
|
73
|
+
payload: { name, params },
|
|
74
|
+
}),
|
|
75
|
+
resize: (index: number): TrueSheetActionType => ({ type: 'RESIZE', index }),
|
|
76
|
+
dismiss: (): TrueSheetActionType => ({ type: 'DISMISS' }),
|
|
77
|
+
remove: (): TrueSheetActionType => ({ type: 'REMOVE' }),
|
|
78
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NavigationAction,
|
|
3
|
+
NavigationState,
|
|
4
|
+
ParamListBase,
|
|
5
|
+
Router,
|
|
6
|
+
StackRouterOptions,
|
|
7
|
+
} from '@react-navigation/core';
|
|
8
|
+
|
|
9
|
+
import { TrueSheetActions, type TrueSheetActionType } from './actions';
|
|
10
|
+
import type { TrueSheetNavigationState } from './types';
|
|
11
|
+
|
|
12
|
+
export type TrueSheetRouterOptions = StackRouterOptions;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The base `StackRouter` factory to wrap, typed generically over the state and
|
|
16
|
+
* action types the wrapper threads through — `StackRouter` is structural and
|
|
17
|
+
* never touches the sheet-specific fields.
|
|
18
|
+
* Injected by each entry point so the shared router never imports
|
|
19
|
+
* `@react-navigation/*` at runtime (Expo Router apps don't install it).
|
|
20
|
+
*/
|
|
21
|
+
export type StackRouterFactory = <
|
|
22
|
+
State extends NavigationState<ParamListBase>,
|
|
23
|
+
Action extends NavigationAction,
|
|
24
|
+
>(
|
|
25
|
+
options: TrueSheetRouterOptions
|
|
26
|
+
) => Router<State, Action>;
|
|
27
|
+
|
|
28
|
+
const uid = () => Math.random().toString(36).slice(2, 10);
|
|
29
|
+
|
|
30
|
+
const ensureBaseRoute = <T extends { routes: { name: string }[] }>(
|
|
31
|
+
state: T,
|
|
32
|
+
baseRouteName: string | undefined,
|
|
33
|
+
routeParamList: Record<string, object | undefined> | undefined
|
|
34
|
+
): T & { index: number; routes: T['routes'] } => {
|
|
35
|
+
if (!baseRouteName) {
|
|
36
|
+
return state as T & { index: number; routes: T['routes'] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const hasBaseRoute = state.routes.some((r) => r.name === baseRouteName);
|
|
40
|
+
|
|
41
|
+
if (!hasBaseRoute) {
|
|
42
|
+
const baseRoute = {
|
|
43
|
+
key: `${baseRouteName}-${uid()}`,
|
|
44
|
+
name: baseRouteName,
|
|
45
|
+
params: routeParamList?.[baseRouteName],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
index: state.routes.length,
|
|
51
|
+
routes: [baseRoute, ...state.routes],
|
|
52
|
+
} as T & { index: number; routes: T['routes'] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return state as T & { index: number; routes: T['routes'] };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const createTrueSheetRouter =
|
|
59
|
+
(stackRouter: StackRouterFactory) =>
|
|
60
|
+
(
|
|
61
|
+
routerOptions: TrueSheetRouterOptions
|
|
62
|
+
): Router<TrueSheetNavigationState<ParamListBase>, TrueSheetActionType> => {
|
|
63
|
+
const baseRouter = stackRouter<TrueSheetNavigationState<ParamListBase>, TrueSheetActionType>(
|
|
64
|
+
routerOptions
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
...baseRouter,
|
|
69
|
+
type: 'true-sheet',
|
|
70
|
+
|
|
71
|
+
getInitialState(options) {
|
|
72
|
+
const state = baseRouter.getInitialState(options);
|
|
73
|
+
const baseRouteName = routerOptions.initialRouteName ?? options.routeNames[0];
|
|
74
|
+
const stateWithBaseRoute = ensureBaseRoute(state, baseRouteName, options.routeParamList);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...stateWithBaseRoute,
|
|
78
|
+
stale: false,
|
|
79
|
+
type: 'true-sheet',
|
|
80
|
+
key: `true-sheet-${uid()}`,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
getStateForAction(state, action, options) {
|
|
85
|
+
switch (action.type) {
|
|
86
|
+
case 'RESIZE': {
|
|
87
|
+
const routeIndex =
|
|
88
|
+
action.target === state.key && action.source
|
|
89
|
+
? state.routes.findIndex((r) => r.key === action.source)
|
|
90
|
+
: state.index;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
...state,
|
|
94
|
+
routes: state.routes.map((route, i) =>
|
|
95
|
+
i === routeIndex
|
|
96
|
+
? {
|
|
97
|
+
...route,
|
|
98
|
+
resizeIndex: action.index,
|
|
99
|
+
resizeKey: (route.resizeKey ?? 0) + 1,
|
|
100
|
+
}
|
|
101
|
+
: route
|
|
102
|
+
),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'GO_BACK':
|
|
107
|
+
case 'DISMISS': {
|
|
108
|
+
return this.getStateForAction(state, TrueSheetActions.pop(1), options);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case 'POP': {
|
|
112
|
+
// Only base screen remains - let parent navigator handle it
|
|
113
|
+
if (state.routes.length <= 1) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const count =
|
|
118
|
+
'payload' in action && typeof action.payload?.count === 'number'
|
|
119
|
+
? action.payload.count
|
|
120
|
+
: 1;
|
|
121
|
+
|
|
122
|
+
// Calculate how many routes we can actually pop (don't pop base screen)
|
|
123
|
+
const maxPopCount = state.routes.length - 1;
|
|
124
|
+
const actualCount = Math.min(count, maxPopCount);
|
|
125
|
+
|
|
126
|
+
// Base screen - let parent navigator handle it
|
|
127
|
+
if (actualCount <= 0) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Target index is the route we want to stay on (land on after pop)
|
|
132
|
+
// closingIndex is the first route to be dismissed (the one after target)
|
|
133
|
+
const targetIndex = state.routes.length - 1 - actualCount;
|
|
134
|
+
const closingIndex = targetIndex + 1;
|
|
135
|
+
|
|
136
|
+
// Mark only the bottom-most route to pop as closing
|
|
137
|
+
// The sheet's dismiss() will handle dismissing sheets above it first
|
|
138
|
+
return {
|
|
139
|
+
...state,
|
|
140
|
+
index: closingIndex,
|
|
141
|
+
routes: state.routes.map((route, i) =>
|
|
142
|
+
i === closingIndex ? { ...route, closing: true } : route
|
|
143
|
+
),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case 'POP_TO_TOP': {
|
|
148
|
+
const popCount = state.routes.length - 1;
|
|
149
|
+
return this.getStateForAction(state, TrueSheetActions.pop(popCount), options);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'POP_TO': {
|
|
153
|
+
const targetName =
|
|
154
|
+
'payload' in action && typeof action.payload?.name === 'string'
|
|
155
|
+
? action.payload.name
|
|
156
|
+
: null;
|
|
157
|
+
|
|
158
|
+
if (!targetName) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const targetIndex = state.routes.findIndex((r) => r.name === targetName);
|
|
163
|
+
|
|
164
|
+
// Target not found or is the current route
|
|
165
|
+
if (targetIndex === -1 || targetIndex >= state.index) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const popCount = state.routes.length - 1 - targetIndex;
|
|
170
|
+
return this.getStateForAction(state, TrueSheetActions.pop(popCount), options);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'REMOVE': {
|
|
174
|
+
// Actually remove the closing route and all routes above it
|
|
175
|
+
const routeKey = action.source;
|
|
176
|
+
const routeIndex = routeKey
|
|
177
|
+
? state.routes.findIndex((r) => r.key === routeKey)
|
|
178
|
+
: state.routes.findIndex((r) => r.closing);
|
|
179
|
+
|
|
180
|
+
if (routeIndex === -1) {
|
|
181
|
+
return state;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Remove the route and all routes above it (they were dismissed together)
|
|
185
|
+
const routes = state.routes.filter((_, i) => i < routeIndex);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
...state,
|
|
189
|
+
index: Math.min(state.index, routes.length - 1),
|
|
190
|
+
routes,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
default:
|
|
195
|
+
return baseRouter.getStateForAction(state, action, options);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
getRehydratedState(partialState, { routeNames, routeParamList, routeGetIdList }) {
|
|
200
|
+
if (partialState.stale === false) {
|
|
201
|
+
return partialState;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const state = baseRouter.getRehydratedState(partialState, {
|
|
205
|
+
routeNames,
|
|
206
|
+
routeParamList,
|
|
207
|
+
routeGetIdList,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const baseRouteName = routerOptions.initialRouteName ?? routeNames[0];
|
|
211
|
+
const stateWithBaseRoute = ensureBaseRoute(state, baseRouteName, routeParamList);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
...stateWithBaseRoute,
|
|
215
|
+
type: 'true-sheet',
|
|
216
|
+
key: `true-sheet-${uid()}`,
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
actionCreators: TrueSheetActions,
|
|
221
|
+
};
|
|
222
|
+
};
|