@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.
Files changed (108) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +8 -2
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +9 -2
  3. package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
  4. package/ios/TrueSheetViewController.mm +1 -4
  5. package/lib/module/TrueSheet.js +12 -6
  6. package/lib/module/TrueSheet.js.map +1 -1
  7. package/lib/module/TrueSheet.web.js +11 -9
  8. package/lib/module/TrueSheet.web.js.map +1 -1
  9. package/lib/module/TrueSheetPeek.web.js +3 -3
  10. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  11. package/lib/module/mocks/navigation.js +31 -21
  12. package/lib/module/mocks/navigation.js.map +1 -1
  13. package/lib/module/mocks/navigationExpoRouter.js +21 -0
  14. package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
  15. package/lib/module/mocks/reanimated.js +1 -1
  16. package/lib/module/mocks/reanimated.js.map +1 -1
  17. package/lib/module/navigation/{TrueSheetView.js → TrueSheetNavigatorContent.js} +14 -12
  18. package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
  19. package/lib/module/navigation/actions.js +50 -0
  20. package/lib/module/navigation/actions.js.map +1 -0
  21. package/lib/module/navigation/{TrueSheetRouter.js → createTrueSheetRouter.js} +20 -24
  22. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
  23. package/lib/module/navigation/expo-router/index.js +48 -0
  24. package/lib/module/navigation/expo-router/index.js.map +1 -0
  25. package/lib/module/navigation/index.js +43 -2
  26. package/lib/module/navigation/index.js.map +1 -1
  27. package/lib/module/navigation/navigator.js +10 -0
  28. package/lib/module/navigation/navigator.js.map +1 -0
  29. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +2 -2
  30. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -1
  31. package/lib/module/navigation/screen/TrueSheetScreen.js +2 -2
  32. package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -1
  33. package/lib/module/navigation/screen/useSheetScreenState.js +4 -4
  34. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  35. package/lib/module/navigation/useTrueSheetNavigation.js +1 -1
  36. package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -1
  37. package/lib/module/web/dom.js +10 -0
  38. package/lib/module/web/dom.js.map +1 -0
  39. package/lib/module/web/vaul/helpers.js +10 -4
  40. package/lib/module/web/vaul/helpers.js.map +1 -1
  41. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  42. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  43. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  44. package/lib/typescript/src/mocks/navigation.d.ts +5 -1
  45. package/lib/typescript/src/mocks/navigation.d.ts.map +1 -1
  46. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
  47. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
  48. package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -1
  49. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
  50. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
  51. package/lib/typescript/src/navigation/actions.d.ts +71 -0
  52. package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
  53. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +14 -0
  54. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
  55. package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
  56. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
  57. package/lib/typescript/src/navigation/index.d.ts +42 -2
  58. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  59. package/lib/typescript/src/navigation/navigator.d.ts +7 -0
  60. package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
  61. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +1 -1
  62. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -1
  63. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +1 -1
  64. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -1
  65. package/lib/typescript/src/navigation/screen/types.d.ts +3 -4
  66. package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -1
  67. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +3 -5
  68. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  69. package/lib/typescript/src/navigation/types.d.ts +24 -0
  70. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  71. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +1 -1
  72. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -1
  73. package/lib/typescript/src/web/dom.d.ts +7 -0
  74. package/lib/typescript/src/web/dom.d.ts.map +1 -0
  75. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  76. package/package.json +23 -8
  77. package/src/TrueSheet.tsx +11 -8
  78. package/src/TrueSheet.web.tsx +14 -18
  79. package/src/TrueSheetPeek.web.tsx +4 -3
  80. package/src/mocks/navigation.ts +32 -25
  81. package/src/mocks/navigationExpoRouter.ts +40 -0
  82. package/src/mocks/reanimated.ts +8 -9
  83. package/src/navigation/{TrueSheetView.tsx → TrueSheetNavigatorContent.tsx} +24 -17
  84. package/src/navigation/actions.ts +78 -0
  85. package/src/navigation/createTrueSheetRouter.ts +222 -0
  86. package/src/navigation/expo-router/index.ts +89 -0
  87. package/src/navigation/index.ts +69 -2
  88. package/src/navigation/navigator.ts +18 -0
  89. package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +2 -2
  90. package/src/navigation/screen/TrueSheetScreen.tsx +2 -2
  91. package/src/navigation/screen/types.ts +4 -6
  92. package/src/navigation/screen/useSheetScreenState.ts +8 -15
  93. package/src/navigation/types.ts +32 -0
  94. package/src/navigation/useTrueSheetNavigation.ts +1 -1
  95. package/src/web/dom.ts +9 -0
  96. package/src/web/vaul/helpers.ts +13 -6
  97. package/lib/module/navigation/TrueSheetRouter.js.map +0 -1
  98. package/lib/module/navigation/TrueSheetView.js.map +0 -1
  99. package/lib/module/navigation/createTrueSheetNavigator.js +0 -63
  100. package/lib/module/navigation/createTrueSheetNavigator.js.map +0 -1
  101. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +0 -57
  102. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +0 -1
  103. package/lib/typescript/src/navigation/TrueSheetView.d.ts +0 -10
  104. package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +0 -1
  105. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +0 -35
  106. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +0 -1
  107. package/src/navigation/TrueSheetRouter.ts +0 -234
  108. 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={[
@@ -10,7 +10,7 @@ import {
10
10
  useRef,
11
11
  useState,
12
12
  } from 'react';
13
- import type { LayoutChangeEvent, ViewStyle } from 'react-native';
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
- paddingBottom: `calc(${baseCss} + env(safe-area-inset-bottom, 0px))`,
306
- } as unknown as ViewStyle,
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 as unknown as HTMLElement | null;
332
+ const contentEl = getDOMElement(contentRef.current);
331
333
  if (!contentEl || !contentEl.isConnected) return 0;
332
- const headerEl = absoluteHeaderRef.current
333
- ? null
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 as unknown as HTMLElement | null;
381
- const headerEl = headerElRef.current as unknown as HTMLElement | null;
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 as unknown as HTMLElement | null;
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
- ? (footerElRef.current as unknown as HTMLElement | null)
556
- : null;
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
- // On web, View refs resolve to the underlying DOM elements.
48
- const peekElement = viewRef.current as unknown as HTMLElement | null;
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
@@ -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
- <T extends ParamListBase = ParamListBase>() =>
40
- ({
41
- navigate: jest.fn(),
42
- goBack: jest.fn(),
43
- reset: jest.fn(),
44
- setParams: jest.fn(),
45
- dispatch: jest.fn(),
46
- isFocused: jest.fn(() => true),
47
- canGoBack: jest.fn(() => true),
48
- getId: jest.fn(),
49
- getParent: jest.fn(),
50
- getState: jest.fn(),
51
- addListener: jest.fn(() => jest.fn()),
52
- removeListener: jest.fn(),
53
- setOptions: jest.fn(),
54
- push: jest.fn(),
55
- pop: jest.fn(),
56
- popTo: jest.fn(),
57
- popToTop: jest.fn(),
58
- replace: jest.fn(),
59
- resize: jest.fn(),
60
- }) as unknown as TrueSheetNavigationProp<T>
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/TrueSheetRouter';
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';
@@ -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
- value: initialValue,
20
- get: () => initialValue,
21
- set: () => {},
22
- addListener: () => () => {},
23
- removeListener: () => {},
24
- modify: () => {},
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 { ParamListBase } from '@react-navigation/core';
1
+ import type { NavigatorArgs } from 'standard-navigation';
2
2
 
3
3
  import type {
4
- TrueSheetDescriptorMap,
5
- TrueSheetNavigationHelpers,
6
- TrueSheetNavigationState,
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
- interface TrueSheetViewProps {
26
- state: TrueSheetNavigationState<ParamListBase>;
27
- navigation: TrueSheetNavigationHelpers;
28
- descriptors: TrueSheetDescriptorMap;
29
- }
25
+ export type TrueSheetNavigatorContentProps = NavigatorArgs<
26
+ TrueSheetNavigationOptions,
27
+ TrueSheetStandardEventMap
28
+ > &
29
+ TrueSheetNavigatorContentExtraProps;
30
30
 
31
- export const TrueSheetView = ({ state, navigation, descriptors }: TrueSheetViewProps) => {
32
- // First route is the base screen, rest are sheets
33
- const [baseRoute, ...sheetRoutes] = state.routes;
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, navigation: screenNavigation, render } = descriptor;
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
- navigation={screenNavigation}
71
- emit={navigation.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
- navigation={screenNavigation}
89
- emit={navigation.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
+ };