@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
@@ -1,71 +1,71 @@
1
- import type { ColorValue, ProcessedColorValue, ViewProps } from 'react-native';
2
- import type {
3
- DirectEventHandler,
4
- Double,
5
- Int32,
6
- WithDefault,
7
- } from 'react-native/Libraries/Types/CodegenTypes';
1
+ import type { CodegenTypes, ColorValue, ProcessedColorValue, ViewProps } from 'react-native';
8
2
  import { codegenNativeComponent } from 'react-native';
9
3
 
10
4
  type GrabberOptionsType = Readonly<{
11
- width?: Double;
12
- height?: Double;
13
- topMargin?: Double;
14
- cornerRadius?: WithDefault<Double, -1>;
5
+ width?: CodegenTypes.Double;
6
+ height?: CodegenTypes.Double;
7
+ topMargin?: CodegenTypes.Double;
8
+ cornerRadius?: CodegenTypes.WithDefault<CodegenTypes.Double, -1>;
15
9
  color?: ProcessedColorValue | null;
16
- adaptive?: WithDefault<boolean, true>;
10
+ adaptive?: CodegenTypes.WithDefault<boolean, true>;
17
11
  }>;
18
12
 
19
13
  type AccessibilityOptionsType = Readonly<{
20
- grabberLabel?: WithDefault<string, 'Sheet Grabber'>;
21
- grabberHint?: WithDefault<string, 'Double-tap to expand. Swipe up or down to resize the sheet'>;
22
- expandedValue?: WithDefault<string, 'Expanded'>;
23
- collapsedValue?: WithDefault<string, 'Collapsed'>;
24
- detentValue?: WithDefault<string, 'Detent {index} of {count}'>;
25
- expandActionLabel?: WithDefault<string, 'Expand'>;
26
- collapseActionLabel?: WithDefault<string, 'Collapse'>;
27
- paneTitle?: WithDefault<string, 'Bottom sheet'>;
14
+ grabberLabel?: CodegenTypes.WithDefault<string, 'Sheet Grabber'>;
15
+ grabberHint?: CodegenTypes.WithDefault<
16
+ string,
17
+ 'Double-tap to expand. Swipe up or down to resize the sheet'
18
+ >;
19
+ expandedValue?: CodegenTypes.WithDefault<string, 'Expanded'>;
20
+ collapsedValue?: CodegenTypes.WithDefault<string, 'Collapsed'>;
21
+ detentValue?: CodegenTypes.WithDefault<string, 'Detent {index} of {count}'>;
22
+ expandActionLabel?: CodegenTypes.WithDefault<string, 'Expand'>;
23
+ collapseActionLabel?: CodegenTypes.WithDefault<string, 'Collapse'>;
24
+ paneTitle?: CodegenTypes.WithDefault<string, 'Bottom sheet'>;
28
25
  }>;
29
26
 
30
27
  type ScrollEdgeEffect = 'automatic' | 'hard' | 'soft' | 'hidden';
31
28
 
32
29
  type ScrollableOptionsType = Readonly<{
33
- contentInsetAdjustment?: WithDefault<'automatic' | 'safe-area' | 'footer' | 'never', 'automatic'>;
34
- keyboardScrollOffset?: WithDefault<Double, 0>;
35
- keyboardOffset?: WithDefault<Double, 0>;
36
- scrollingExpandsSheet?: WithDefault<boolean, true>;
37
- topScrollEdgeEffect?: WithDefault<ScrollEdgeEffect, 'hidden'>;
38
- bottomScrollEdgeEffect?: WithDefault<ScrollEdgeEffect, 'hidden'>;
30
+ contentInsetAdjustment?: CodegenTypes.WithDefault<
31
+ 'automatic' | 'safe-area' | 'footer' | 'never',
32
+ 'automatic'
33
+ >;
34
+ keyboardScrollOffset?: CodegenTypes.WithDefault<CodegenTypes.Double, 0>;
35
+ keyboardOffset?: CodegenTypes.WithDefault<CodegenTypes.Double, 0>;
36
+ scrollingExpandsSheet?: CodegenTypes.WithDefault<boolean, true>;
37
+ topScrollEdgeEffect?: CodegenTypes.WithDefault<ScrollEdgeEffect, 'hidden'>;
38
+ bottomScrollEdgeEffect?: CodegenTypes.WithDefault<ScrollEdgeEffect, 'hidden'>;
39
39
  }>;
40
40
 
41
41
  type FooterOptionsType = Readonly<{
42
42
  // Named uniquely across option structs — codegen derives the enum name from
43
43
  // the field name, so a second `position` would redefine TrueSheetViewPosition
44
- footerPosition?: WithDefault<'relative' | 'absolute', 'relative'>;
45
- keyboardOffset?: WithDefault<Double, 0>;
46
- avoidKeyboard?: WithDefault<boolean, true>;
44
+ footerPosition?: CodegenTypes.WithDefault<'relative' | 'absolute', 'relative'>;
45
+ keyboardOffset?: CodegenTypes.WithDefault<CodegenTypes.Double, 0>;
46
+ avoidKeyboard?: CodegenTypes.WithDefault<boolean, true>;
47
47
  }>;
48
48
 
49
49
  type HeaderOptionsType = Readonly<{
50
- position?: WithDefault<'relative' | 'absolute', 'relative'>;
50
+ position?: CodegenTypes.WithDefault<'relative' | 'absolute', 'relative'>;
51
51
  }>;
52
52
 
53
53
  export interface DetentInfoEventPayload {
54
- index: Int32;
55
- position: Double;
56
- detent: Double;
54
+ index: CodegenTypes.Int32;
55
+ position: CodegenTypes.Double;
56
+ detent: CodegenTypes.Double;
57
57
  }
58
58
 
59
59
  export interface PositionChangeEventPayload {
60
- index: Double;
61
- position: Double;
62
- detent: Double;
60
+ index: CodegenTypes.Double;
61
+ position: CodegenTypes.Double;
62
+ detent: CodegenTypes.Double;
63
63
  realtime: boolean;
64
64
  }
65
65
 
66
66
  export interface NativeProps extends ViewProps {
67
67
  // Array properties
68
- detents?: ReadonlyArray<Double>;
68
+ detents?: ReadonlyArray<CodegenTypes.Double>;
69
69
  detentBackgrounds?: ReadonlyArray<
70
70
  Readonly<{
71
71
  color?: ProcessedColorValue | null;
@@ -74,18 +74,20 @@ export interface NativeProps extends ViewProps {
74
74
  >;
75
75
 
76
76
  // Number properties - use 0 as default to avoid nil insertion
77
- maxContentHeight?: WithDefault<Double, 0>;
78
- maxContentWidth?: WithDefault<Double, 0>;
79
- cornerRadius?: WithDefault<Double, -1>;
80
- elevation?: WithDefault<Double, -1>;
77
+ maxContentHeight?: CodegenTypes.WithDefault<CodegenTypes.Double, 0>;
78
+ maxContentWidth?: CodegenTypes.WithDefault<CodegenTypes.Double, 0>;
79
+ cornerRadius?: CodegenTypes.WithDefault<CodegenTypes.Double, -1>;
80
+ elevation?: CodegenTypes.WithDefault<CodegenTypes.Double, -1>;
81
81
 
82
82
  // Color properties
83
83
  backgroundColor?: ColorValue;
84
- initialDetentIndex?: WithDefault<Int32, -1>;
85
- dimmedDetentIndex?: WithDefault<Int32, 0>;
84
+ initialDetentIndex?: CodegenTypes.WithDefault<CodegenTypes.Int32, -1>;
85
+ dimColor?: ColorValue;
86
+ dimOpacity?: CodegenTypes.WithDefault<CodegenTypes.Double, -1>;
87
+ dimmedDetentIndex?: CodegenTypes.WithDefault<CodegenTypes.Int32, 0>;
86
88
 
87
89
  // String properties - use empty string as default to avoid nil insertion
88
- backgroundBlur?: WithDefault<
90
+ backgroundBlur?: CodegenTypes.WithDefault<
89
91
  | 'none'
90
92
  | 'light'
91
93
  | 'dark'
@@ -111,43 +113,46 @@ export interface NativeProps extends ViewProps {
111
113
  'none'
112
114
  >;
113
115
 
114
- placement?: WithDefault<'automatic' | 'leading' | 'center' | 'trailing', 'automatic'>;
115
- placementOffset?: WithDefault<Double, 16>;
116
- insetAdjustment?: WithDefault<'automatic' | 'never', 'automatic'>;
117
- dismissThreshold?: WithDefault<'half' | 'short', 'half'>;
116
+ placement?: CodegenTypes.WithDefault<
117
+ 'automatic' | 'leading' | 'center' | 'trailing',
118
+ 'automatic'
119
+ >;
120
+ placementOffset?: CodegenTypes.WithDefault<CodegenTypes.Double, 16>;
121
+ insetAdjustment?: CodegenTypes.WithDefault<'automatic' | 'never', 'automatic'>;
122
+ dismissThreshold?: CodegenTypes.WithDefault<'half' | 'short', 'half'>;
118
123
 
119
124
  // Boolean properties - match defaults from TrueSheet.types.ts
120
- grabber?: WithDefault<boolean, true>;
125
+ grabber?: CodegenTypes.WithDefault<boolean, true>;
121
126
  grabberOptions?: GrabberOptionsType;
122
127
  accessibilityOptions?: AccessibilityOptionsType;
123
- dismissible?: WithDefault<boolean, true>;
124
- draggable?: WithDefault<boolean, true>;
125
- dimmed?: WithDefault<boolean, true>;
126
- initialDetentAnimated?: WithDefault<boolean, true>;
128
+ dismissible?: CodegenTypes.WithDefault<boolean, true>;
129
+ draggable?: CodegenTypes.WithDefault<boolean, true>;
130
+ dimmed?: CodegenTypes.WithDefault<boolean, true>;
131
+ initialDetentAnimated?: CodegenTypes.WithDefault<boolean, true>;
127
132
  // React tag of the scrollable component within the content (see scrollableRef)
128
- scrollableHandle?: WithDefault<Int32, -1>;
133
+ scrollableHandle?: CodegenTypes.WithDefault<CodegenTypes.Int32, -1>;
129
134
  scrollableOptions?: ScrollableOptionsType;
130
135
  headerOptions?: HeaderOptionsType;
131
136
  footerOptions?: FooterOptionsType;
132
- presentation?: WithDefault<'page' | 'form', 'page'>;
137
+ presentation?: CodegenTypes.WithDefault<'page' | 'form', 'page'>;
133
138
 
134
139
  // Event handlers
135
- onMount?: DirectEventHandler<null>;
136
- onWillPresent?: DirectEventHandler<DetentInfoEventPayload>;
137
- onDidPresent?: DirectEventHandler<DetentInfoEventPayload>;
138
- onWillDismiss?: DirectEventHandler<null>;
139
- onDidDismiss?: DirectEventHandler<null>;
140
- onDismissAttempt?: DirectEventHandler<null>;
141
- onDetentChange?: DirectEventHandler<DetentInfoEventPayload>;
142
- onDragBegin?: DirectEventHandler<DetentInfoEventPayload>;
143
- onDragChange?: DirectEventHandler<DetentInfoEventPayload>;
144
- onDragEnd?: DirectEventHandler<DetentInfoEventPayload>;
145
- onPositionChange?: DirectEventHandler<PositionChangeEventPayload>;
146
- onWillFocus?: DirectEventHandler<null>;
147
- onDidFocus?: DirectEventHandler<null>;
148
- onWillBlur?: DirectEventHandler<null>;
149
- onDidBlur?: DirectEventHandler<null>;
150
- onVisibilityChange?: DirectEventHandler<Readonly<{ visible: boolean }>>;
140
+ onMount?: CodegenTypes.DirectEventHandler<null>;
141
+ onWillPresent?: CodegenTypes.DirectEventHandler<DetentInfoEventPayload>;
142
+ onDidPresent?: CodegenTypes.DirectEventHandler<DetentInfoEventPayload>;
143
+ onWillDismiss?: CodegenTypes.DirectEventHandler<null>;
144
+ onDidDismiss?: CodegenTypes.DirectEventHandler<null>;
145
+ onDismissAttempt?: CodegenTypes.DirectEventHandler<null>;
146
+ onDetentChange?: CodegenTypes.DirectEventHandler<DetentInfoEventPayload>;
147
+ onDragBegin?: CodegenTypes.DirectEventHandler<DetentInfoEventPayload>;
148
+ onDragChange?: CodegenTypes.DirectEventHandler<DetentInfoEventPayload>;
149
+ onDragEnd?: CodegenTypes.DirectEventHandler<DetentInfoEventPayload>;
150
+ onPositionChange?: CodegenTypes.DirectEventHandler<PositionChangeEventPayload>;
151
+ onWillFocus?: CodegenTypes.DirectEventHandler<null>;
152
+ onDidFocus?: CodegenTypes.DirectEventHandler<null>;
153
+ onWillBlur?: CodegenTypes.DirectEventHandler<null>;
154
+ onDidBlur?: CodegenTypes.DirectEventHandler<null>;
155
+ onVisibilityChange?: CodegenTypes.DirectEventHandler<Readonly<{ visible: boolean }>>;
151
156
  }
152
157
 
153
158
  export default codegenNativeComponent<NativeProps>('TrueSheetView', {
@@ -1,4 +1,4 @@
1
- import { TrueSheetActions } from './actions';
1
+ import { TrueSheetActions, type TrueSheetActionType } from './actions';
2
2
  import type { TrueSheetRouter, TrueSheetRouterFactory, TrueSheetRouterOptions } from './types';
3
3
 
4
4
  const uid = () => Math.random().toString(36).slice(2, 10);
@@ -31,6 +31,151 @@ const ensureBaseRoute = <T extends { routes: { name: string }[] }>(
31
31
  return state as T & { index: number; routes: T['routes'] };
32
32
  };
33
33
 
34
+ type SheetRoute = {
35
+ key: string;
36
+ name: string;
37
+ closing?: boolean;
38
+ dismissing?: boolean;
39
+ resizeIndex?: number;
40
+ resizeKey?: number;
41
+ };
42
+ type SheetState = { key: string; index: number; routes: SheetRoute[] };
43
+
44
+ /**
45
+ * Index of the route a `RESIZE` targets: the dispatching screen, else the focused route.
46
+ */
47
+ export const getResizeRouteIndex = (
48
+ state: SheetState,
49
+ action: { source?: string; target?: string }
50
+ ): number =>
51
+ action.target === state.key && action.source
52
+ ? state.routes.findIndex((r) => r.key === action.source)
53
+ : state.index;
54
+
55
+ /**
56
+ * Reduces the sheet-specific actions shared by every router flavour.
57
+ * Returns `undefined` for actions the base `StackRouter` should handle.
58
+ */
59
+ export const getTrueSheetStateForAction = <State extends SheetState>(
60
+ state: State,
61
+ action: TrueSheetActionType
62
+ ): State | null | undefined => {
63
+ switch (action.type) {
64
+ case 'RESIZE': {
65
+ const routeIndex = getResizeRouteIndex(state, action);
66
+
67
+ return {
68
+ ...state,
69
+ routes: state.routes.map((route, i) =>
70
+ i === routeIndex
71
+ ? {
72
+ ...route,
73
+ resizeIndex: action.index,
74
+ resizeKey: (route.resizeKey ?? 0) + 1,
75
+ }
76
+ : route
77
+ ),
78
+ };
79
+ }
80
+
81
+ case 'GO_BACK':
82
+ case 'DISMISS': {
83
+ return getTrueSheetStateForAction(state, TrueSheetActions.pop(1));
84
+ }
85
+
86
+ case 'POP': {
87
+ // Only base screen remains - let parent navigator handle it
88
+ if (state.routes.length <= 1) {
89
+ return null;
90
+ }
91
+
92
+ const count =
93
+ 'payload' in action && typeof action.payload?.count === 'number' ? action.payload.count : 1;
94
+
95
+ // Calculate how many routes we can actually pop (don't pop base screen)
96
+ const maxPopCount = state.routes.length - 1;
97
+ const actualCount = Math.min(count, maxPopCount);
98
+
99
+ // Base screen - let parent navigator handle it
100
+ if (actualCount <= 0) {
101
+ return null;
102
+ }
103
+
104
+ // Target index is the route we want to stay on (land on after pop)
105
+ // closingIndex is the first route to be dismissed (the one after target)
106
+ const targetIndex = state.routes.length - 1 - actualCount;
107
+ const closingIndex = targetIndex + 1;
108
+
109
+ // Mark only the bottom-most route to pop as closing
110
+ // The sheet's dismiss() will handle dismissing sheets above it first
111
+ return {
112
+ ...state,
113
+ index: closingIndex,
114
+ routes: state.routes.map((route, i) => {
115
+ if (i === closingIndex) return { ...route, closing: true };
116
+ if (i > closingIndex) return { ...route, dismissing: true };
117
+ return route;
118
+ }),
119
+ };
120
+ }
121
+
122
+ case 'POP_TO_TOP': {
123
+ const popCount = state.routes.length - 1;
124
+ return getTrueSheetStateForAction(state, TrueSheetActions.pop(popCount));
125
+ }
126
+
127
+ case 'POP_TO': {
128
+ const targetName =
129
+ 'payload' in action && typeof action.payload?.name === 'string'
130
+ ? action.payload.name
131
+ : null;
132
+
133
+ if (!targetName) {
134
+ return null;
135
+ }
136
+
137
+ const targetIndex = state.routes.findIndex((r) => r.name === targetName);
138
+
139
+ // Target not found or is the current route
140
+ if (targetIndex === -1 || targetIndex >= state.index) {
141
+ return null;
142
+ }
143
+
144
+ const popCount = state.routes.length - 1 - targetIndex;
145
+ return getTrueSheetStateForAction(state, TrueSheetActions.pop(popCount));
146
+ }
147
+
148
+ case 'REMOVE': {
149
+ // Actually remove the closing route and all routes above it
150
+ const routeKey = action.source;
151
+ const routeIndex = routeKey
152
+ ? state.routes.findIndex((r) => r.key === routeKey)
153
+ : state.routes.findIndex((r) => r.closing);
154
+
155
+ if (routeIndex === -1) {
156
+ return state;
157
+ }
158
+
159
+ // Remove the route and the routes dismissed with it. A popped route keeps the
160
+ // routes pushed after the pop started: they aren't stacked on it natively.
161
+ const removed = state.routes[routeIndex]!;
162
+ const isPopped = removed.closing || removed.dismissing;
163
+ const routes = state.routes.filter(
164
+ (route, i) => i < routeIndex || (i > routeIndex && isPopped && !route.dismissing)
165
+ );
166
+
167
+ return {
168
+ ...state,
169
+ index: Math.min(state.index, routes.length - 1),
170
+ routes,
171
+ };
172
+ }
173
+
174
+ default:
175
+ return undefined;
176
+ }
177
+ };
178
+
34
179
  export const createTrueSheetRouter =
35
180
  (stackRouter: TrueSheetRouterFactory) =>
36
181
  (routerOptions: TrueSheetRouterOptions): TrueSheetRouter => {
@@ -54,118 +199,9 @@ export const createTrueSheetRouter =
54
199
  },
55
200
 
56
201
  getStateForAction(state, action, options) {
57
- switch (action.type) {
58
- case 'RESIZE': {
59
- const routeIndex =
60
- action.target === state.key && action.source
61
- ? state.routes.findIndex((r) => r.key === action.source)
62
- : state.index;
63
-
64
- return {
65
- ...state,
66
- routes: state.routes.map((route, i) =>
67
- i === routeIndex
68
- ? {
69
- ...route,
70
- resizeIndex: action.index,
71
- resizeKey: (route.resizeKey ?? 0) + 1,
72
- }
73
- : route
74
- ),
75
- };
76
- }
77
-
78
- case 'GO_BACK':
79
- case 'DISMISS': {
80
- return this.getStateForAction(state, TrueSheetActions.pop(1), options);
81
- }
82
-
83
- case 'POP': {
84
- // Only base screen remains - let parent navigator handle it
85
- if (state.routes.length <= 1) {
86
- return null;
87
- }
88
-
89
- const count =
90
- 'payload' in action && typeof action.payload?.count === 'number'
91
- ? action.payload.count
92
- : 1;
93
-
94
- // Calculate how many routes we can actually pop (don't pop base screen)
95
- const maxPopCount = state.routes.length - 1;
96
- const actualCount = Math.min(count, maxPopCount);
97
-
98
- // Base screen - let parent navigator handle it
99
- if (actualCount <= 0) {
100
- return null;
101
- }
102
-
103
- // Target index is the route we want to stay on (land on after pop)
104
- // closingIndex is the first route to be dismissed (the one after target)
105
- const targetIndex = state.routes.length - 1 - actualCount;
106
- const closingIndex = targetIndex + 1;
107
-
108
- // Mark only the bottom-most route to pop as closing
109
- // The sheet's dismiss() will handle dismissing sheets above it first
110
- return {
111
- ...state,
112
- index: closingIndex,
113
- routes: state.routes.map((route, i) =>
114
- i === closingIndex ? { ...route, closing: true } : route
115
- ),
116
- };
117
- }
118
-
119
- case 'POP_TO_TOP': {
120
- const popCount = state.routes.length - 1;
121
- return this.getStateForAction(state, TrueSheetActions.pop(popCount), options);
122
- }
123
-
124
- case 'POP_TO': {
125
- const targetName =
126
- 'payload' in action && typeof action.payload?.name === 'string'
127
- ? action.payload.name
128
- : null;
129
-
130
- if (!targetName) {
131
- return null;
132
- }
133
-
134
- const targetIndex = state.routes.findIndex((r) => r.name === targetName);
135
-
136
- // Target not found or is the current route
137
- if (targetIndex === -1 || targetIndex >= state.index) {
138
- return null;
139
- }
140
-
141
- const popCount = state.routes.length - 1 - targetIndex;
142
- return this.getStateForAction(state, TrueSheetActions.pop(popCount), options);
143
- }
144
-
145
- case 'REMOVE': {
146
- // Actually remove the closing route and all routes above it
147
- const routeKey = action.source;
148
- const routeIndex = routeKey
149
- ? state.routes.findIndex((r) => r.key === routeKey)
150
- : state.routes.findIndex((r) => r.closing);
151
-
152
- if (routeIndex === -1) {
153
- return state;
154
- }
155
-
156
- // Remove the route and all routes above it (they were dismissed together)
157
- const routes = state.routes.filter((_, i) => i < routeIndex);
158
-
159
- return {
160
- ...state,
161
- index: Math.min(state.index, routes.length - 1),
162
- routes,
163
- };
164
- }
165
-
166
- default:
167
- return baseRouter.getStateForAction(state, action, options);
168
- }
202
+ const result = getTrueSheetStateForAction(state, action);
203
+
204
+ return result === undefined ? baseRouter.getStateForAction(state, action, options) : result;
169
205
  },
170
206
 
171
207
  getRehydratedState(partialState, { routeNames, routeParamList, routeGetIdList }) {
@@ -0,0 +1,71 @@
1
+ import { TrueSheetActions, type TrueSheetActionType } from '../actions';
2
+ import { getResizeRouteIndex, getTrueSheetStateForAction } from '../createTrueSheetRouter';
3
+ import type { StackRouterFactory, TrueSheetRouterFactory, TrueSheetRouterState } from './types';
4
+
5
+ type ActionResult = { state: TrueSheetRouterState; affectedRouteKey?: string };
6
+
7
+ type RouterExtension = (context: {
8
+ baseRouter: {
9
+ getStateForAction(
10
+ state: TrueSheetRouterState,
11
+ action: TrueSheetActionType,
12
+ options: object
13
+ ): ActionResult | null;
14
+ };
15
+ options: { initialRouteName?: string };
16
+ }) => object;
17
+
18
+ /**
19
+ * `extendRouter` from Expo Router 58+. Declared locally because the
20
+ * Expo Router types this package builds against predate it.
21
+ */
22
+ export type ExtendRouter = (
23
+ base: StackRouterFactory,
24
+ extension: RouterExtension,
25
+ options: { type: string }
26
+ ) => TrueSheetRouterFactory;
27
+
28
+ /**
29
+ * Router for Expo Router 58+, where navigator state is seeded by Expo Router
30
+ * (no `getInitialState`/`getRehydratedState`) and `getStateForAction` returns
31
+ * `{ state, affectedRouteKey }`.
32
+ */
33
+ export const extendTrueSheetRouter = (
34
+ extendRouter: ExtendRouter,
35
+ stackRouter: StackRouterFactory
36
+ ): TrueSheetRouterFactory =>
37
+ extendRouter(
38
+ stackRouter,
39
+ ({ baseRouter, options }) => {
40
+ if (__DEV__ && options.initialRouteName === undefined) {
41
+ console.warn(
42
+ "TrueSheet: this Sheet layout has no `unstable_settings.anchor`. On Expo Router 58+, a sheet route opened directly (deep link or push from another navigator) renders without its base screen. Export `unstable_settings = { anchor: '<base route>' }` from the layout."
43
+ );
44
+ }
45
+
46
+ return {
47
+ getStateForAction(
48
+ state: TrueSheetRouterState,
49
+ action: TrueSheetActionType,
50
+ config: object
51
+ ): ActionResult | null {
52
+ const result = getTrueSheetStateForAction(state, action);
53
+
54
+ if (result === undefined) {
55
+ return baseRouter.getStateForAction(state, action, config);
56
+ }
57
+
58
+ if (result === null) {
59
+ return null;
60
+ }
61
+
62
+ const affectedIndex =
63
+ action.type === 'RESIZE' ? getResizeRouteIndex(state, action) : result.index;
64
+
65
+ return { state: result, affectedRouteKey: result.routes[affectedIndex]?.key };
66
+ },
67
+ actionCreators: TrueSheetActions,
68
+ };
69
+ },
70
+ { type: 'true-sheet' }
71
+ );
@@ -1,8 +1,10 @@
1
+ import * as ExpoRouter from 'expo-router';
1
2
  import { StackRouter, unstable_integrateWithRouter, useNavigation } from 'expo-router';
2
3
 
3
4
  import { createTrueSheetRouter } from '../createTrueSheetRouter';
4
5
  import { trueSheetNavigator } from '../navigator';
5
6
  import type { ParamListBase } from './base-types';
7
+ import { extendTrueSheetRouter, type ExtendRouter } from './extendTrueSheetRouter';
6
8
  import type {
7
9
  StackRouterFactory,
8
10
  TrueSheetNavigationOptions,
@@ -15,12 +17,18 @@ import type {
15
17
  TrueSheetStandardEventMap,
16
18
  } from './types';
17
19
 
18
- // expo-router's vendored StackRouter is structurally identical to @react-navigation's.
19
- // Pinned to this entry's own router types: `createTrueSheetRouter` is shared and typed
20
- // against React Navigation, which resolves to `any` in apps that only install expo-router.
21
- const sheetRouter: TrueSheetRouterFactory = createTrueSheetRouter(
22
- StackRouter as StackRouterFactory
23
- );
20
+ // Expo Router 58+ replaced the React Navigation router contract and ships `extendRouter`
21
+ // for it. Before that, the vendored StackRouter is structurally identical to
22
+ // @react-navigation's. Pinned to this entry's own router types: `createTrueSheetRouter` is
23
+ // shared and typed against React Navigation, which resolves to `any` in apps that only
24
+ // install expo-router.
25
+ const extendRouter = (ExpoRouter as Record<string, unknown>).extendRouter as
26
+ | ExtendRouter
27
+ | undefined;
28
+
29
+ const sheetRouter: TrueSheetRouterFactory = extendRouter
30
+ ? extendTrueSheetRouter(extendRouter, StackRouter as StackRouterFactory)
31
+ : createTrueSheetRouter(StackRouter as StackRouterFactory);
24
32
 
25
33
  // `routes`/`dispatch` are injected by `createProps`, so they are declared as
26
34
  // CreateProps (last generic) and excluded from the public navigator props
@@ -50,7 +50,7 @@ export type TrueSheetRoute = TrueSheetNavigationState<ParamListBase>['routes'][n
50
50
 
51
51
  /**
52
52
  * Props injected into the navigator content by Expo Router's `createProps`.
53
- * Raw routes carry the custom fields (`closing`, `resizeIndex`, `resizeKey`)
53
+ * Raw routes carry the custom fields (`closing`, `dismissing`, `resizeIndex`, `resizeKey`)
54
54
  * that the standard state strips out.
55
55
  */
56
56
  export interface TrueSheetNavigatorContentExtraProps {
@@ -96,6 +96,8 @@ export type TrueSheetStateOf<State extends { routes: readonly unknown[] }> = Omi
96
96
  resizeIndex?: number;
97
97
  resizeKey?: number;
98
98
  closing?: boolean;
99
+ // Stacked above a `closing` route and dismissed natively with it
100
+ dismissing?: boolean;
99
101
  })[];
100
102
  };
101
103
 
@@ -151,7 +153,7 @@ export type TrueSheetEmitFn = NavigatorArgs<
151
153
  /**
152
154
  * Props injected into the navigator content by each adapter
153
155
  * (React Navigation's `mapper` / Expo Router's `createProps`).
154
- * Raw routes carry the custom fields (`closing`, `resizeIndex`, `resizeKey`)
156
+ * Raw routes carry the custom fields (`closing`, `dismissing`, `resizeIndex`, `resizeKey`)
155
157
  * that the standard state strips out.
156
158
  */
157
159
  export interface TrueSheetNavigatorContentExtraProps {
@@ -216,6 +218,8 @@ export type TrueSheetNavigationSheetProps = Pick<
216
218
  | 'grabberOptions'
217
219
  | 'accessibilityOptions'
218
220
  | 'dimmed'
221
+ | 'dimColor'
222
+ | 'dimOpacity'
219
223
  | 'dimmedDetentIndex'
220
224
  | 'backgroundBlur'
221
225
  | 'maxContentHeight'
@@ -0,0 +1,25 @@
1
+ const ACCESSORS = ['getScrollableNode', 'getNativeScrollRef'] as const;
2
+
3
+ // Resolves a `scrollableRef` value to its scroll node: a native tag on native,
4
+ // a DOM node on web. Imperative-handle lists (e.g. LegendList, FlashList) are
5
+ // not host components, so they go through the accessors ScrollView/FlatList
6
+ // also expose. A composite result is resolved again until it yields a tag or
7
+ // host node. `getScrollResponder` is skipped: it returns the ScrollView class
8
+ // instance, which findNodeHandle resolves to its first host child (the
9
+ // SwipeRefreshLayout on Android when a refreshControl is set).
10
+ export function resolveScrollable(scrollable: unknown): unknown {
11
+ if (!scrollable || typeof scrollable !== 'object') return scrollable;
12
+ for (const name of ACCESSORS) {
13
+ const accessor = (scrollable as Record<string, unknown>)[name];
14
+ if (typeof accessor !== 'function') continue;
15
+ let node: unknown;
16
+ try {
17
+ node = accessor.call(scrollable);
18
+ } catch {
19
+ // e.g. LegendList forwarding to a custom scroll component that lacks it
20
+ continue;
21
+ }
22
+ if (node != null && node !== scrollable) return resolveScrollable(node);
23
+ }
24
+ return scrollable;
25
+ }