@lodev09/react-native-true-sheet 4.0.0-beta.2 → 4.0.0-beta.20

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 (203) hide show
  1. package/README.md +25 -4
  2. package/android/build.gradle +22 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +302 -184
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +21 -110
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +116 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +18 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +119 -50
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +360 -163
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +54 -12
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +22 -9
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +84 -5
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
  17. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
  18. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  19. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  20. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
  21. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  22. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  23. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  24. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +110 -30
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +9 -3
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +3 -6
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
  33. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +4 -6
  34. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
  35. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
  36. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
  37. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  38. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  39. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
  40. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
  41. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
  42. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
  43. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
  45. package/ios/TrueSheetContainerView.h +27 -7
  46. package/ios/TrueSheetContainerView.mm +31 -11
  47. package/ios/TrueSheetContentView.h +43 -15
  48. package/ios/TrueSheetContentView.mm +211 -189
  49. package/ios/TrueSheetFooterView.h +6 -0
  50. package/ios/TrueSheetFooterView.mm +93 -17
  51. package/ios/TrueSheetModule.h +2 -7
  52. package/ios/TrueSheetModule.mm +7 -16
  53. package/ios/TrueSheetOverlayView.h +32 -0
  54. package/ios/TrueSheetOverlayView.mm +161 -0
  55. package/ios/TrueSheetView.mm +212 -55
  56. package/ios/TrueSheetViewController.h +31 -3
  57. package/ios/TrueSheetViewController.mm +393 -94
  58. package/ios/core/TrueSheetBlurView.h +0 -2
  59. package/ios/core/TrueSheetBlurView.mm +3 -36
  60. package/ios/core/TrueSheetDetentCalculator.h +7 -1
  61. package/ios/core/TrueSheetDetentCalculator.mm +27 -4
  62. package/ios/core/TrueSheetGrabberView.mm +13 -3
  63. package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
  64. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  65. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  66. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  67. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  68. package/ios/tvos/TrueSheetStubs.mm +12 -0
  69. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  70. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  71. package/lib/module/TrueSheet.js +173 -39
  72. package/lib/module/TrueSheet.js.map +1 -1
  73. package/lib/module/TrueSheet.web.js +218 -243
  74. package/lib/module/TrueSheet.web.js.map +1 -1
  75. package/lib/module/TrueSheetBackground.js +15 -0
  76. package/lib/module/TrueSheetBackground.js.map +1 -0
  77. package/lib/module/TrueSheetOverlay.js +30 -0
  78. package/lib/module/TrueSheetOverlay.js.map +1 -0
  79. package/lib/module/TrueSheetOverlay.web.js +35 -0
  80. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  81. package/lib/module/TrueSheetPeek.web.js +20 -15
  82. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  83. package/lib/module/detentBackgrounds.js +21 -0
  84. package/lib/module/detentBackgrounds.js.map +1 -0
  85. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  86. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  87. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +14 -10
  88. package/lib/module/index.js +1 -0
  89. package/lib/module/index.js.map +1 -1
  90. package/lib/module/mocks/index.js +18 -2
  91. package/lib/module/mocks/index.js.map +1 -1
  92. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  93. package/lib/module/navigation/base-types.js +2 -0
  94. package/lib/module/navigation/base-types.js.map +1 -0
  95. package/lib/module/navigation/createTrueSheetRouter.js +0 -9
  96. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  97. package/lib/module/navigation/expo-router/base-types.js +2 -0
  98. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  99. package/lib/module/navigation/expo-router/index.js +10 -7
  100. package/lib/module/navigation/expo-router/index.js.map +1 -1
  101. package/lib/module/navigation/expo-router/types.js +4 -0
  102. package/lib/module/navigation/expo-router/types.js.map +1 -0
  103. package/lib/module/navigation/index.js.map +1 -1
  104. package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
  105. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  106. package/lib/module/web/constants.js +1 -1
  107. package/lib/module/web/constants.js.map +1 -1
  108. package/lib/module/web/layout.js +168 -0
  109. package/lib/module/web/layout.js.map +1 -0
  110. package/lib/module/web/vaul/constants.js +4 -0
  111. package/lib/module/web/vaul/constants.js.map +1 -1
  112. package/lib/module/web/vaul/context.js.map +1 -1
  113. package/lib/module/web/vaul/helpers.js +1 -0
  114. package/lib/module/web/vaul/helpers.js.map +1 -1
  115. package/lib/module/web/vaul/index.js +44 -6
  116. package/lib/module/web/vaul/index.js.map +1 -1
  117. package/lib/module/web/vaul/use-snap-points.js +6 -8
  118. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  119. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  120. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  121. package/lib/typescript/src/TrueSheet.types.d.ts +135 -34
  122. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  123. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  124. package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
  125. package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
  126. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  127. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  128. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  129. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  130. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  131. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  132. package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
  133. package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
  134. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  135. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  136. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +11 -7
  137. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  138. package/lib/typescript/src/index.d.ts +1 -0
  139. package/lib/typescript/src/index.d.ts.map +1 -1
  140. package/lib/typescript/src/mocks/index.d.ts +4 -0
  141. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  142. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  143. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  144. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  145. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  146. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +2 -13
  147. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  148. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  149. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  150. package/lib/typescript/src/navigation/expo-router/index.d.ts +8 -6
  151. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  152. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  153. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  154. package/lib/typescript/src/navigation/index.d.ts +3 -4
  155. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  156. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
  157. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  158. package/lib/typescript/src/navigation/types.d.ts +41 -5
  159. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  160. package/lib/typescript/src/web/constants.d.ts +1 -1
  161. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  162. package/lib/typescript/src/web/layout.d.ts +19 -0
  163. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  164. package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
  165. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  166. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  167. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  168. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  169. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  170. package/lib/typescript/src/web/vaul/index.d.ts +7 -1
  171. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  172. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  173. package/package.json +17 -8
  174. package/react-native.config.js +1 -0
  175. package/src/TrueSheet.tsx +206 -48
  176. package/src/TrueSheet.types.ts +142 -35
  177. package/src/TrueSheet.web.tsx +270 -290
  178. package/src/TrueSheetBackground.tsx +17 -0
  179. package/src/TrueSheetOverlay.tsx +24 -0
  180. package/src/TrueSheetOverlay.web.tsx +33 -0
  181. package/src/TrueSheetPeek.web.tsx +25 -23
  182. package/src/detentBackgrounds.ts +24 -0
  183. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  184. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  185. package/src/fabric/TrueSheetViewNativeComponent.ts +14 -10
  186. package/src/index.ts +1 -0
  187. package/src/mocks/index.ts +17 -2
  188. package/src/mocks/navigationExpoRouter.ts +6 -5
  189. package/src/navigation/base-types.ts +20 -0
  190. package/src/navigation/createTrueSheetRouter.ts +5 -33
  191. package/src/navigation/expo-router/base-types.ts +18 -0
  192. package/src/navigation/expo-router/index.ts +28 -30
  193. package/src/navigation/expo-router/types.ts +116 -0
  194. package/src/navigation/index.ts +6 -7
  195. package/src/navigation/screen/useSheetScreenState.ts +2 -0
  196. package/src/navigation/types.ts +75 -22
  197. package/src/web/constants.ts +1 -1
  198. package/src/web/layout.ts +196 -0
  199. package/src/web/vaul/constants.ts +4 -0
  200. package/src/web/vaul/context.ts +1 -0
  201. package/src/web/vaul/helpers.ts +5 -0
  202. package/src/web/vaul/index.tsx +67 -5
  203. package/src/web/vaul/use-snap-points.ts +6 -14
@@ -4,16 +4,17 @@ import {
4
4
  type StandardNavigationTypeBagBase,
5
5
  } from '@react-navigation/native';
6
6
 
7
- import { createTrueSheetRouter, type StackRouterFactory } from './createTrueSheetRouter';
7
+ import { createTrueSheetRouter } from './createTrueSheetRouter';
8
8
  import { trueSheetNavigator } from './navigator';
9
9
  import type {
10
+ StackRouterFactory,
10
11
  TrueSheetActionHelpers,
11
12
  TrueSheetNavigationOptions,
12
13
  TrueSheetNavigationEventMap,
13
14
  TrueSheetNavigationState,
14
15
  TrueSheetNavigatorContentExtraProps,
16
+ TrueSheetRouterOptions,
15
17
  } from './types';
16
- import type { TrueSheetRouterOptions } from './createTrueSheetRouter';
17
18
 
18
19
  export interface TrueSheetNavigationTypeBag extends StandardNavigationTypeBagBase {
19
20
  State: TrueSheetNavigationState<this['ParamList']>;
@@ -62,20 +63,18 @@ export const createTrueSheetNavigator = createNavigator;
62
63
  export const createTrueSheetScreen = createScreen;
63
64
 
64
65
  export { TrueSheetActions, type TrueSheetActionType } from './actions';
65
- export {
66
- createTrueSheetRouter,
67
- type StackRouterFactory,
68
- type TrueSheetRouterOptions,
69
- } from './createTrueSheetRouter';
66
+ export { createTrueSheetRouter } from './createTrueSheetRouter';
70
67
  export { useTrueSheetNavigation } from './useTrueSheetNavigation';
71
68
 
72
69
  export type { DetentInfoEventPayload, PositionChangeEventPayload } from '../TrueSheet.types';
73
70
 
74
71
  export type {
72
+ StackRouterFactory,
75
73
  TrueSheetNavigationEventMap,
76
74
  TrueSheetNavigationHelpers,
77
75
  TrueSheetNavigationOptions,
78
76
  TrueSheetNavigationProp,
79
77
  TrueSheetNavigationState,
78
+ TrueSheetRouterOptions,
80
79
  TrueSheetScreenProps,
81
80
  } from './types';
@@ -14,6 +14,7 @@ import type {
14
14
  PositionChangeEventPayload,
15
15
  WillBlurEvent,
16
16
  WillDismissEvent,
17
+ DismissAttemptEvent,
17
18
  WillFocusEvent,
18
19
  WillPresentEvent,
19
20
  } from '../../TrueSheet.types';
@@ -78,6 +79,7 @@ export const useSheetScreenState = (props: UseSheetScreenStateProps) => {
78
79
  onDidPresent: (e: DidPresentEvent) => emitEvent('sheetDidPresent', e.nativeEvent),
79
80
  onWillDismiss: (_e: WillDismissEvent) => emitEvent('sheetWillDismiss', undefined),
80
81
  onDidDismiss,
82
+ onDismissAttempt: (_e: DismissAttemptEvent) => emitEvent('sheetDismissAttempt', undefined),
81
83
  onDetentChange: (e: DetentChangeEvent) => emitEvent('sheetDetentChange', e.nativeEvent),
82
84
  onDragBegin: (e: DragBeginEvent) => emitEvent('sheetDragBegin', e.nativeEvent),
83
85
  onDragChange: (e: DragChangeEvent) => emitEvent('sheetDragChange', e.nativeEvent),
@@ -1,21 +1,24 @@
1
+ import type { NavigatorArgs } from 'standard-navigation';
2
+
3
+ import type {
4
+ DetentInfoEventPayload,
5
+ PositionChangeEventPayload,
6
+ TrueSheetProps,
7
+ } from '../TrueSheet.types';
8
+ import type { TrueSheetActionType } from './actions';
1
9
  import type {
2
10
  DefaultNavigatorOptions,
3
11
  Descriptor,
12
+ NavigationAction,
4
13
  NavigationHelpers,
5
14
  NavigationProp,
6
15
  NavigationState,
7
16
  ParamListBase,
8
17
  RouteProp,
18
+ Router,
9
19
  StackActionHelpers,
10
- } from '@react-navigation/core';
11
- import type { NavigatorArgs } from 'standard-navigation';
12
-
13
- import type {
14
- DetentInfoEventPayload,
15
- PositionChangeEventPayload,
16
- TrueSheetProps,
17
- } from '../TrueSheet.types';
18
- import type { TrueSheetActionType } from './actions';
20
+ StackRouterOptions,
21
+ } from './base-types';
19
22
 
20
23
  export type PositionChangeHandler = (payload: PositionChangeEventPayload) => void;
21
24
 
@@ -36,6 +39,10 @@ export type TrueSheetNavigationEventMap = {
36
39
  * Event fired when the sheet has been dismissed.
37
40
  */
38
41
  sheetDidDismiss: { data: undefined };
42
+ /**
43
+ * Event fired when the user tries to dismiss a non-dismissible sheet.
44
+ */
45
+ sheetDismissAttempt: { data: undefined };
39
46
  /**
40
47
  * Event fired when the sheet's detent changes.
41
48
  */
@@ -74,18 +81,54 @@ export type TrueSheetNavigationEventMap = {
74
81
  sheetDidBlur: { data: undefined };
75
82
  };
76
83
 
77
- export type TrueSheetNavigationState<ParamList extends ParamListBase> = Omit<
78
- NavigationState<ParamList>,
84
+ /**
85
+ * A navigation state tagged as `true-sheet`, with the sheet fields the router
86
+ * adds to each route.
87
+ * Generic over the base state so each entry point can bind its own
88
+ * `NavigationState` — see `./base-types`.
89
+ */
90
+ export type TrueSheetStateOf<State extends { routes: readonly unknown[] }> = Omit<
91
+ State,
79
92
  'routes'
80
93
  > & {
81
94
  type: 'true-sheet';
82
- routes: (NavigationState<ParamList>['routes'][number] & {
95
+ routes: (State['routes'][number] & {
83
96
  resizeIndex?: number;
84
97
  resizeKey?: number;
85
98
  closing?: boolean;
86
99
  })[];
87
100
  };
88
101
 
102
+ export type TrueSheetNavigationState<ParamList extends ParamListBase> = TrueSheetStateOf<
103
+ NavigationState<ParamList>
104
+ >;
105
+
106
+ export type TrueSheetRouterState = TrueSheetNavigationState<ParamListBase>;
107
+
108
+ export type TrueSheetRouterOptions = StackRouterOptions;
109
+
110
+ export type TrueSheetRouter = Router<TrueSheetRouterState, TrueSheetActionType>;
111
+
112
+ /**
113
+ * The wrapped router factory — also the shape `createTrueSheetRouter` accepts,
114
+ * pre-instantiated so either entry point's `StackRouterFactory` fits it.
115
+ */
116
+ export type TrueSheetRouterFactory = (options: TrueSheetRouterOptions) => TrueSheetRouter;
117
+
118
+ /**
119
+ * The base `StackRouter` factory to wrap, typed generically over the state and
120
+ * action types the wrapper threads through — `StackRouter` is structural and
121
+ * never touches the sheet-specific fields.
122
+ * Injected by each entry point so the shared router never imports
123
+ * `@react-navigation/*` at runtime (Expo Router apps don't install it).
124
+ */
125
+ export type StackRouterFactory = <
126
+ State extends NavigationState<ParamListBase>,
127
+ Action extends NavigationAction,
128
+ >(
129
+ options: TrueSheetRouterOptions
130
+ ) => Router<State, Action>;
131
+
89
132
  /**
90
133
  * `TrueSheetNavigationEventMap` in the `standard-navigation` event-map shape.
91
134
  */
@@ -116,13 +159,20 @@ export interface TrueSheetNavigatorContentExtraProps {
116
159
  dispatch: TrueSheetDispatch;
117
160
  }
118
161
 
119
- export type TrueSheetActionHelpers<ParamList extends ParamListBase> =
120
- StackActionHelpers<ParamList> & {
121
- /**
122
- * Resize the sheet to a specific detent index.
123
- */
124
- resize(index?: number): void;
125
- };
162
+ /**
163
+ * Generic over the base helpers so each entry point can bind its own
164
+ * `StackActionHelpers` — see `./base-types`.
165
+ */
166
+ export type TrueSheetActionHelpersOf<Helpers> = Helpers & {
167
+ /**
168
+ * Resize the sheet to a specific detent index.
169
+ */
170
+ resize(index?: number): void;
171
+ };
172
+
173
+ export type TrueSheetActionHelpers<ParamList extends ParamListBase> = TrueSheetActionHelpersOf<
174
+ StackActionHelpers<ParamList>
175
+ >;
126
176
 
127
177
  export type TrueSheetNavigationProp<
128
178
  ParamList extends ParamListBase,
@@ -156,6 +206,9 @@ export type TrueSheetNavigationSheetProps = Pick<
156
206
  TrueSheetProps,
157
207
  | 'detents'
158
208
  | 'backgroundColor'
209
+ | 'detentBackgrounds'
210
+ | 'background'
211
+ | 'backgroundStyle'
159
212
  | 'cornerRadius'
160
213
  | 'dismissible'
161
214
  | 'draggable'
@@ -165,7 +218,6 @@ export type TrueSheetNavigationSheetProps = Pick<
165
218
  | 'dimmed'
166
219
  | 'dimmedDetentIndex'
167
220
  | 'backgroundBlur'
168
- | 'blurOptions'
169
221
  | 'maxContentHeight'
170
222
  | 'maxContentWidth'
171
223
  | 'presentation'
@@ -176,10 +228,11 @@ export type TrueSheetNavigationSheetProps = Pick<
176
228
  | 'footer'
177
229
  | 'footerStyle'
178
230
  | 'footerOptions'
231
+ | 'scrollableRef'
179
232
  | 'scrollableOptions'
180
233
  | 'insetAdjustment'
181
- | 'anchor'
182
- | 'anchorOffset'
234
+ | 'placement'
235
+ | 'placementOffset'
183
236
  | 'elevation'
184
237
  | 'detached'
185
238
  | 'detachedOffset'
@@ -4,7 +4,7 @@ export const DEFAULT_FORM_SHEET_WIDTH = 580;
4
4
  export const DEFAULT_FORM_SHEET_HEIGHT_RATIO = 0.5;
5
5
  export const COLOR_SURFACE_CONTAINER_LOW_LIGHT = '#F7F2FA';
6
6
  export const COLOR_SURFACE_CONTAINER_LOW_DARK = '#1D1B20';
7
- export const DEFAULT_ANCHOR_OFFSET = 16;
7
+ export const DEFAULT_PLACEMENT_OFFSET = 16;
8
8
  export const DEFAULT_DETACHED_OFFSET = 16;
9
9
  export const DEFAULT_GRABBER_COLOR_LIGHT = 'rgba(0, 0, 0, 0.3)';
10
10
  export const DEFAULT_GRABBER_COLOR_DARK = 'rgba(255, 255, 255, 0.3)';
@@ -0,0 +1,196 @@
1
+ /// <reference lib="dom" />
2
+
3
+ const FOOTER_INSET = '--truesheet-footer-inset';
4
+
5
+ interface SheetLayoutOptions {
6
+ drawer: HTMLElement;
7
+ layout: HTMLElement;
8
+ getContent: () => HTMLElement | null;
9
+ getHeader: () => HTMLElement | null;
10
+ getFooter: () => HTMLElement | null;
11
+ getScrollable: () => HTMLElement | null;
12
+ absoluteHeader: boolean;
13
+ absoluteFooter: boolean;
14
+ footerInsetAdjustment: boolean;
15
+ measureContent: boolean;
16
+ onHeightChange: (height: number) => void;
17
+ }
18
+
19
+ export function observeSheetLayout({
20
+ drawer,
21
+ layout,
22
+ getContent,
23
+ getHeader,
24
+ getFooter,
25
+ getScrollable,
26
+ absoluteHeader,
27
+ absoluteFooter,
28
+ footerInsetAdjustment,
29
+ measureContent,
30
+ onHeightChange,
31
+ }: SheetLayoutOptions) {
32
+ let frame = 0;
33
+ let disposed = false;
34
+ let width = -1;
35
+ let height = -1;
36
+ let scroller: HTMLElement | null = null;
37
+ let originalPadding = '';
38
+ let insetPadding = '';
39
+ let appliedInset = 0;
40
+ const observed = new Set<Element>();
41
+
42
+ // This tree has no viewport constraint or injected footer inset. Its resize
43
+ // observer catches intrinsic changes without observing the live flex layout.
44
+ const measurement = measureContent ? document.createElement('div') : null;
45
+ if (measurement) {
46
+ Object.assign(measurement.style, {
47
+ position: 'absolute',
48
+ top: '0',
49
+ left: '0',
50
+ display: 'flex',
51
+ flexDirection: 'column',
52
+ visibility: 'hidden',
53
+ pointerEvents: 'none',
54
+ });
55
+ measurement.style.setProperty(FOOTER_INSET, '0px');
56
+ measurement.inert = true;
57
+ measurement.setAttribute('aria-hidden', 'true');
58
+ measurement.setAttribute('data-truesheet-measure', '');
59
+ drawer.appendChild(measurement);
60
+ }
61
+
62
+ const measure = () => {
63
+ if (!measurement?.isConnected) return;
64
+ const headerHeight = absoluteHeader ? 0 : (getHeader()?.offsetHeight ?? 0);
65
+ const footerHeight =
66
+ !absoluteFooter || (footerInsetAdjustment && scroller) ? (getFooter()?.offsetHeight ?? 0) : 0;
67
+ const next = measurement.offsetHeight + headerHeight + footerHeight;
68
+ if (height === next) return;
69
+ height = next;
70
+ onHeightChange(next);
71
+ };
72
+
73
+ const updateInset = () => {
74
+ let inset = 0;
75
+ const footer = getFooter();
76
+ if (footerInsetAdjustment && scroller && footer) {
77
+ let bottom = scroller.offsetTop + scroller.offsetHeight;
78
+ let parent = scroller.offsetParent;
79
+ while (parent instanceof HTMLElement && parent !== layout) {
80
+ bottom += parent.offsetTop;
81
+ parent = parent.offsetParent;
82
+ }
83
+ if (parent === layout) {
84
+ inset = Math.min(
85
+ footer.offsetHeight,
86
+ Math.max(0, bottom - (layout.clientHeight - footer.offsetHeight))
87
+ );
88
+ }
89
+ }
90
+ if (inset === appliedInset) return;
91
+ appliedInset = inset;
92
+ layout.style.setProperty(FOOTER_INSET, `${inset}px`);
93
+ };
94
+
95
+ const resizeObserver = new ResizeObserver(() => {
96
+ if (measurement && width !== layout.clientWidth) {
97
+ width = layout.clientWidth;
98
+ measurement.style.width = `${width}px`;
99
+ }
100
+ updateInset();
101
+ measure();
102
+ });
103
+ const measurementObserver = new ResizeObserver(measure);
104
+ if (measurement) measurementObserver.observe(measurement);
105
+
106
+ const restorePadding = () => {
107
+ if (scroller && scroller.style.paddingBottom === insetPadding) {
108
+ scroller.style.paddingBottom = originalPadding;
109
+ }
110
+ };
111
+
112
+ const sync = () => {
113
+ if (disposed) return;
114
+ const content = getContent();
115
+ const nextScroller = getScrollable();
116
+ if (nextScroller !== scroller) {
117
+ restorePadding();
118
+ scroller = nextScroller;
119
+ insetPadding = '';
120
+ }
121
+ if (
122
+ footerInsetAdjustment &&
123
+ scroller &&
124
+ (!insetPadding || scroller.style.paddingBottom !== insetPadding)
125
+ ) {
126
+ originalPadding = scroller.style.paddingBottom;
127
+ const base = getComputedStyle(scroller).paddingBottom;
128
+ insetPadding = `calc(${base} + var(${FOOTER_INSET}, 0px))`;
129
+ scroller.style.paddingBottom = insetPadding;
130
+ insetPadding = scroller.style.paddingBottom;
131
+ }
132
+
133
+ const nodes = new Set<Element>([layout]);
134
+ for (const node of [getHeader(), getFooter(), scroller]) {
135
+ if (node) nodes.add(node);
136
+ }
137
+ for (const node of observed) {
138
+ if (!nodes.has(node)) {
139
+ resizeObserver.unobserve(node);
140
+ observed.delete(node);
141
+ }
142
+ }
143
+ for (const node of nodes) {
144
+ if (!observed.has(node)) {
145
+ resizeObserver.observe(node);
146
+ observed.add(node);
147
+ }
148
+ }
149
+
150
+ if (measurement && content) {
151
+ width = layout.clientWidth;
152
+ measurement.style.width = `${width}px`;
153
+ measurement.replaceChildren(content.cloneNode(true));
154
+ }
155
+ updateInset();
156
+ measure();
157
+ };
158
+
159
+ const mutationObserver = new MutationObserver((mutations) => {
160
+ const changed = mutations.some(
161
+ (mutation) => mutation.target !== layout || mutation.type === 'childList'
162
+ );
163
+ if (!changed || frame) return;
164
+ frame = requestAnimationFrame(() => {
165
+ frame = 0;
166
+ sync();
167
+ });
168
+ });
169
+
170
+ sync();
171
+ mutationObserver.observe(layout, {
172
+ childList: true,
173
+ characterData: true,
174
+ attributes: true,
175
+ attributeFilter: ['style', 'class'],
176
+ subtree: true,
177
+ });
178
+ const footer = getFooter();
179
+ if (absoluteFooter && footer) {
180
+ mutationObserver.observe(footer, { childList: true, characterData: true, subtree: true });
181
+ }
182
+
183
+ return {
184
+ measure,
185
+ disconnect: () => {
186
+ disposed = true;
187
+ cancelAnimationFrame(frame);
188
+ mutationObserver.disconnect();
189
+ resizeObserver.disconnect();
190
+ measurementObserver.disconnect();
191
+ restorePadding();
192
+ layout.style.removeProperty(FOOTER_INSET);
193
+ measurement?.remove();
194
+ },
195
+ };
196
+ }
@@ -9,6 +9,10 @@ export const DEFAULT_PEEK_HEIGHT = 150;
9
9
 
10
10
  export const CLOSE_THRESHOLD = 0.25;
11
11
 
12
+ // Minimum downward pull (px) on a non-dismissible drawer at its first snap
13
+ // point before the release counts as a dismiss attempt.
14
+ export const DISMISS_ATTEMPT_THRESHOLD = 24;
15
+
12
16
  export const SCROLL_LOCK_TIMEOUT = 100;
13
17
 
14
18
  export const BORDER_RADIUS = 8;
@@ -23,6 +23,7 @@ interface DrawerContextValue {
23
23
  onNestedOpenChange: (o: boolean) => void;
24
24
  onNestedRelease: (event: React.PointerEvent<HTMLDivElement>, open: boolean) => void;
25
25
  dismissible: boolean;
26
+ onDismissAttempt?: () => void;
26
27
  isOpen: boolean;
27
28
  isDragging: boolean;
28
29
  keyboardIsOpen: React.MutableRefObject<boolean>;
@@ -80,6 +80,11 @@ export const isVertical = (direction: DrawerDirection) => {
80
80
  }
81
81
  };
82
82
 
83
+ export const getSnapPointTransform = (direction: DrawerDirection) =>
84
+ isVertical(direction)
85
+ ? 'translate3d(0, var(--snap-point-height), 0)'
86
+ : 'translate3d(var(--snap-point-height), 0, 0)';
87
+
83
88
  export function getTranslate(element: HTMLElement, direction: DrawerDirection): number | null {
84
89
  if (!element) {
85
90
  return null;
@@ -12,6 +12,7 @@ import { isIOS, isMobileFirefox } from './browser';
12
12
  import {
13
13
  BORDER_RADIUS,
14
14
  CLOSE_THRESHOLD,
15
+ DISMISS_ATTEMPT_THRESHOLD,
15
16
  DRAG_CLASS,
16
17
  NESTED_DISPLACEMENT,
17
18
  SCROLL_LOCK_TIMEOUT,
@@ -19,7 +20,14 @@ import {
19
20
  VELOCITY_THRESHOLD,
20
21
  WINDOW_TOP_OFFSET,
21
22
  } from './constants';
22
- import { dampenValue, getTranslate, isVertical, reset, set } from './helpers';
23
+ import {
24
+ dampenValue,
25
+ getSnapPointTransform,
26
+ getTranslate,
27
+ isVertical,
28
+ reset,
29
+ set,
30
+ } from './helpers';
23
31
  import type { DrawerDirection } from './types';
24
32
  import { useComposedRefs } from './use-composed-refs';
25
33
  import { useControllableState } from './use-controllable-state';
@@ -93,6 +101,12 @@ export type DialogProps = {
93
101
  * @default true
94
102
  */
95
103
  dismissible?: boolean;
104
+ /**
105
+ * Called when the user tries to close a non-dismissible drawer — dragging down
106
+ * at the first snap point, pressing escape, or tapping the handle of a
107
+ * single-snap-point drawer. Overlay clicks don't count, mirroring iOS.
108
+ */
109
+ onDismissAttempt?: () => void;
96
110
  /**
97
111
  * When `false` the drawer can't be dragged by the user. Programmatic snap-point
98
112
  * changes still animate. Defaults to `true`.
@@ -218,6 +232,7 @@ export function Root({
218
232
  closeThreshold = CLOSE_THRESHOLD,
219
233
  scrollLockTimeout = SCROLL_LOCK_TIMEOUT,
220
234
  dismissible = true,
235
+ onDismissAttempt,
221
236
  draggable = true,
222
237
  handleOnly = false,
223
238
  fadeFromIndex = snapPoints && snapPoints.length - 1,
@@ -274,6 +289,7 @@ export function Root({
274
289
  const isAllowedToDrag = React.useRef<boolean>(false);
275
290
  const nestedOpenChangeTimer = React.useRef<NodeJS.Timeout | null>(null);
276
291
  const pointerStart = React.useRef(0);
292
+ const dismissAttempted = React.useRef(false);
277
293
  const keyboardIsOpen = React.useRef(false);
278
294
  const shouldAnimate = React.useRef(!defaultOpen);
279
295
  const previousDiffFromInitial = React.useRef(0);
@@ -517,7 +533,17 @@ export function Root({
517
533
  const noCloseSnapPointsPreCondition = snapPoints && !dismissible && !isDraggingInDirection;
518
534
 
519
535
  // Disallow dragging down to close when first snap point is the active one and dismissible prop is set to false.
520
- if (noCloseSnapPointsPreCondition && activeSnapPointIndex === 0) return;
536
+ if (noCloseSnapPointsPreCondition && activeSnapPointIndex === 0) {
537
+ // The drawer stays put. Remember a deliberate pull that would otherwise
538
+ // have dragged it (content at its top) so release() can report it.
539
+ if (
540
+ -draggedDistance > DISMISS_ATTEMPT_THRESHOLD &&
541
+ (isAllowedToDrag.current || shouldDrag(event.target, isDraggingInDirection))
542
+ ) {
543
+ dismissAttempted.current = true;
544
+ }
545
+ return;
546
+ }
521
547
 
522
548
  // We need to capture last time when drag with scroll was triggered and have a timeout between
523
549
  const absDraggedDistance = Math.abs(draggedDistance);
@@ -778,6 +804,21 @@ export function Root({
778
804
  unfreezeScrollables();
779
805
  setIsDragging(false);
780
806
  dragEndTime.current = new Date();
807
+
808
+ if (dismissAttempted.current) {
809
+ dismissAttempted.current = false;
810
+ if (isOpen) onDismissAttempt?.();
811
+ }
812
+
813
+ // The press that started this "drag" may have dismissed the drawer (e.g. a
814
+ // back button acting on pointerdown). Snapping now would overwrite the
815
+ // dismiss transform that closeDrawer just wrote and freeze the sheet in
816
+ // place until it unmounts.
817
+ if (!isOpen) {
818
+ if (pointerEvent) onReleaseProp?.(pointerEvent, false);
819
+ return;
820
+ }
821
+
781
822
  const swipeAmount = getTranslate(drawerRef.current, direction);
782
823
 
783
824
  // Drag overshoot below the lowest snap point translates the detached
@@ -966,6 +1007,7 @@ export function Root({
966
1007
  onTouchDrag: (event, pointerEvent) => drag(event, pointerEvent),
967
1008
  onTouchRelease: (event, pointerEvent) => release(event, pointerEvent),
968
1009
  dismissible,
1010
+ onDismissAttempt,
969
1011
  shouldAnimate,
970
1012
  handleOnly,
971
1013
  isOpen,
@@ -1121,6 +1163,7 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1121
1163
  snapPoints,
1122
1164
  setActiveSnapPoint,
1123
1165
  dismissible,
1166
+ onDismissAttempt,
1124
1167
  container,
1125
1168
  handleOnly,
1126
1169
  shouldAnimate,
@@ -1265,11 +1308,17 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1265
1308
  // same detent). Without this, position goes stale mid-animation because
1266
1309
  // the drawer's `transitionrun` never fires.
1267
1310
  const onTransitionRun = (e: TransitionEvent) => {
1268
- if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
1311
+ if (
1312
+ (e.target === drawer || e.target === wrapper) &&
1313
+ (e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
1314
+ )
1269
1315
  state.start();
1270
1316
  };
1271
1317
  const onTransitionDone = (e: TransitionEvent) => {
1272
- if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
1318
+ if (
1319
+ (e.target === drawer || e.target === wrapper) &&
1320
+ (e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
1321
+ )
1273
1322
  state.stop();
1274
1323
  };
1275
1324
  const onAnimationStart = (e: AnimationEvent) => {
@@ -1376,6 +1425,9 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1376
1425
  wasOpenRef.current = isOpen;
1377
1426
  }, [isOpen, detached, drawerRef]);
1378
1427
 
1428
+ // Subsequent snap writes belong to useSnapPoints. A React style update on
1429
+ // release would overwrite the live drag value before its transition starts.
1430
+ const initialSnapPointOffset = React.useRef(snapPointsOffset?.[activeSnapPointIndex ?? 0] ?? 0);
1379
1431
  const contentNode = (
1380
1432
  <DialogPrimitive.Content
1381
1433
  data-vaul-drawer-direction={direction}
@@ -1390,7 +1442,8 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1390
1442
  style={
1391
1443
  snapPointsOffset && snapPointsOffset.length > 0
1392
1444
  ? ({
1393
- '--snap-point-height': `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
1445
+ '--snap-point-height': `${initialSnapPointOffset.current}px`,
1446
+ 'transform': getSnapPointTransform(direction),
1394
1447
  ...style,
1395
1448
  'pointerEvents': 'auto',
1396
1449
  } as React.CSSProperties)
@@ -1453,6 +1506,8 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1453
1506
  activeSnapPointIndex > 0
1454
1507
  ) {
1455
1508
  setActiveSnapPoint(snapPoints![0]!);
1509
+ } else {
1510
+ onDismissAttempt?.();
1456
1511
  }
1457
1512
  }
1458
1513
  }}
@@ -1594,6 +1649,7 @@ export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(
1594
1649
  activeSnapPoint,
1595
1650
  setActiveSnapPoint,
1596
1651
  dismissible,
1652
+ onDismissAttempt,
1597
1653
  handleOnly,
1598
1654
  isOpen,
1599
1655
  onPress,
@@ -1637,6 +1693,12 @@ export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(
1637
1693
  return;
1638
1694
  }
1639
1695
 
1696
+ // Mirrors native: a single-detent sheet's handle tap is a dismiss.
1697
+ if (isLastSnapPoint && snapPoints.length === 1) {
1698
+ onDismissAttempt?.();
1699
+ return;
1700
+ }
1701
+
1640
1702
  const currentSnapIndex = snapPoints.findIndex((point) => point === activeSnapPoint);
1641
1703
  if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
1642
1704
  const nextSnapPoint = snapPoints[currentSnapIndex + 1];
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck — vendored upstream, incompatible with noUncheckedIndexedAccess
2
2
  import React from 'react';
3
- import { set, isVertical } from './helpers';
3
+ import { set, isVertical, getSnapPointTransform } from './helpers';
4
4
  import { DEFAULT_PEEK_HEIGHT, TRANSITIONS, VELOCITY_THRESHOLD } from './constants';
5
5
  import { useControllableState } from './use-controllable-state';
6
6
  import type { DrawerDirection } from './types';
@@ -198,16 +198,12 @@ export function useSnapPoints({
198
198
 
199
199
  const animateThisSnap = hasSnappedRef.current || initialAnimated;
200
200
  hasSnappedRef.current = true;
201
- // `--snap-point-height` transitions alongside `transform` (registered via
202
- // @property) so layouts derived from it (e.g. the scrollable fill) resize
203
- // in sync with the drawer's slide instead of jumping to the target.
201
+ // One animated value drives both the translation and the visible height.
204
202
  set(drawerRef.current, {
205
203
  'transition': animateThisSnap
206
- ? `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
204
+ ? `--snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
207
205
  : 'none',
208
- 'transform': isVertical(direction)
209
- ? `translate3d(0, ${dimension}px, 0)`
210
- : `translate3d(${dimension}px, 0, 0)`,
206
+ 'transform': getSnapPointTransform(direction),
211
207
  '--snap-point-height': `${dimension}px`,
212
208
  });
213
209
 
@@ -364,9 +360,7 @@ export function useSnapPoints({
364
360
  if ((direction === 'bottom' || direction === 'right') && newValue > snapPointsOffset[0]) {
365
361
  const excess = newValue - snapPointsOffset[0];
366
362
  set(drawerRef.current, {
367
- 'transform': isVertical(direction)
368
- ? `translate3d(0, ${snapPointsOffset[0]}px, 0)`
369
- : `translate3d(${snapPointsOffset[0]}px, 0, 0)`,
363
+ 'transform': getSnapPointTransform(direction),
370
364
  '--snap-point-height': `${snapPointsOffset[0]}px`,
371
365
  });
372
366
  setDetachedWrapperTransform(excess, false);
@@ -378,9 +372,7 @@ export function useSnapPoints({
378
372
  // derived from it (e.g. the scrollable fill) resize with the drawer
379
373
  // instead of staying cut off at the last detent's visible height.
380
374
  set(drawerRef.current, {
381
- 'transform': isVertical(direction)
382
- ? `translate3d(0, ${newValue}px, 0)`
383
- : `translate3d(${newValue}px, 0, 0)`,
375
+ 'transform': getSnapPointTransform(direction),
384
376
  '--snap-point-height': `${newValue}px`,
385
377
  });
386
378
  }