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

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 (205) 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 +303 -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 +124 -50
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +369 -163
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +59 -12
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +126 -0
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +22 -9
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +84 -5
  17. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
  18. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
  19. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  20. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  21. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
  22. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  23. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  24. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +110 -30
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +9 -3
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +3 -6
  33. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
  34. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +4 -6
  35. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
  36. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
  37. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
  38. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  39. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  40. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
  41. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
  42. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
  43. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
  45. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
  46. package/ios/TrueSheetContainerView.h +27 -7
  47. package/ios/TrueSheetContainerView.mm +31 -11
  48. package/ios/TrueSheetContentView.h +43 -15
  49. package/ios/TrueSheetContentView.mm +211 -189
  50. package/ios/TrueSheetFooterView.h +6 -0
  51. package/ios/TrueSheetFooterView.mm +93 -17
  52. package/ios/TrueSheetModule.h +2 -7
  53. package/ios/TrueSheetModule.mm +7 -16
  54. package/ios/TrueSheetOverlayView.h +32 -0
  55. package/ios/TrueSheetOverlayView.mm +161 -0
  56. package/ios/TrueSheetView.mm +275 -56
  57. package/ios/TrueSheetViewController.h +31 -3
  58. package/ios/TrueSheetViewController.mm +393 -94
  59. package/ios/core/TrueSheetBlurView.h +0 -2
  60. package/ios/core/TrueSheetBlurView.mm +3 -36
  61. package/ios/core/TrueSheetDetentCalculator.h +7 -1
  62. package/ios/core/TrueSheetDetentCalculator.mm +27 -4
  63. package/ios/core/TrueSheetGrabberView.mm +13 -3
  64. package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
  65. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  66. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  67. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  68. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  69. package/ios/tvos/TrueSheetStubs.mm +12 -0
  70. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  71. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  72. package/lib/module/TrueSheet.js +175 -39
  73. package/lib/module/TrueSheet.js.map +1 -1
  74. package/lib/module/TrueSheet.web.js +220 -243
  75. package/lib/module/TrueSheet.web.js.map +1 -1
  76. package/lib/module/TrueSheetBackground.js +15 -0
  77. package/lib/module/TrueSheetBackground.js.map +1 -0
  78. package/lib/module/TrueSheetOverlay.js +30 -0
  79. package/lib/module/TrueSheetOverlay.js.map +1 -0
  80. package/lib/module/TrueSheetOverlay.web.js +35 -0
  81. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  82. package/lib/module/TrueSheetPeek.web.js +20 -15
  83. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  84. package/lib/module/detentBackgrounds.js +21 -0
  85. package/lib/module/detentBackgrounds.js.map +1 -0
  86. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  87. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  88. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +15 -10
  89. package/lib/module/index.js +1 -0
  90. package/lib/module/index.js.map +1 -1
  91. package/lib/module/mocks/index.js +18 -2
  92. package/lib/module/mocks/index.js.map +1 -1
  93. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  94. package/lib/module/navigation/base-types.js +2 -0
  95. package/lib/module/navigation/base-types.js.map +1 -0
  96. package/lib/module/navigation/createTrueSheetRouter.js +0 -9
  97. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  98. package/lib/module/navigation/expo-router/base-types.js +2 -0
  99. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  100. package/lib/module/navigation/expo-router/index.js +10 -7
  101. package/lib/module/navigation/expo-router/index.js.map +1 -1
  102. package/lib/module/navigation/expo-router/types.js +4 -0
  103. package/lib/module/navigation/expo-router/types.js.map +1 -0
  104. package/lib/module/navigation/index.js.map +1 -1
  105. package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
  106. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  107. package/lib/module/web/constants.js +1 -1
  108. package/lib/module/web/constants.js.map +1 -1
  109. package/lib/module/web/layout.js +168 -0
  110. package/lib/module/web/layout.js.map +1 -0
  111. package/lib/module/web/vaul/constants.js +9 -0
  112. package/lib/module/web/vaul/constants.js.map +1 -1
  113. package/lib/module/web/vaul/context.js.map +1 -1
  114. package/lib/module/web/vaul/helpers.js +1 -0
  115. package/lib/module/web/vaul/helpers.js.map +1 -1
  116. package/lib/module/web/vaul/index.js +47 -7
  117. package/lib/module/web/vaul/index.js.map +1 -1
  118. package/lib/module/web/vaul/use-snap-points.js +21 -10
  119. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  120. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  121. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  122. package/lib/typescript/src/TrueSheet.types.d.ts +157 -34
  123. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  124. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  125. package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
  126. package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
  127. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  128. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  129. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  130. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  131. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  132. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  133. package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
  134. package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
  135. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  136. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  137. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +12 -7
  138. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  139. package/lib/typescript/src/index.d.ts +1 -0
  140. package/lib/typescript/src/index.d.ts.map +1 -1
  141. package/lib/typescript/src/mocks/index.d.ts +4 -0
  142. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  143. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  144. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  145. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  146. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  147. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +2 -13
  148. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  149. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  150. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  151. package/lib/typescript/src/navigation/expo-router/index.d.ts +8 -6
  152. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  153. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  154. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  155. package/lib/typescript/src/navigation/index.d.ts +3 -4
  156. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  157. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
  158. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  159. package/lib/typescript/src/navigation/types.d.ts +41 -5
  160. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  161. package/lib/typescript/src/web/constants.d.ts +1 -1
  162. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  163. package/lib/typescript/src/web/layout.d.ts +19 -0
  164. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  165. package/lib/typescript/src/web/vaul/constants.d.ts +3 -0
  166. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  167. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  168. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  169. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  170. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  171. package/lib/typescript/src/web/vaul/index.d.ts +13 -1
  172. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  173. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +2 -1
  174. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  175. package/package.json +17 -8
  176. package/react-native.config.js +1 -0
  177. package/src/TrueSheet.tsx +208 -48
  178. package/src/TrueSheet.types.ts +166 -35
  179. package/src/TrueSheet.web.tsx +272 -290
  180. package/src/TrueSheetBackground.tsx +17 -0
  181. package/src/TrueSheetOverlay.tsx +24 -0
  182. package/src/TrueSheetOverlay.web.tsx +33 -0
  183. package/src/TrueSheetPeek.web.tsx +25 -23
  184. package/src/detentBackgrounds.ts +24 -0
  185. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  186. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  187. package/src/fabric/TrueSheetViewNativeComponent.ts +15 -10
  188. package/src/index.ts +1 -0
  189. package/src/mocks/index.ts +17 -2
  190. package/src/mocks/navigationExpoRouter.ts +6 -5
  191. package/src/navigation/base-types.ts +20 -0
  192. package/src/navigation/createTrueSheetRouter.ts +5 -33
  193. package/src/navigation/expo-router/base-types.ts +18 -0
  194. package/src/navigation/expo-router/index.ts +28 -30
  195. package/src/navigation/expo-router/types.ts +116 -0
  196. package/src/navigation/index.ts +6 -7
  197. package/src/navigation/screen/useSheetScreenState.ts +2 -0
  198. package/src/navigation/types.ts +75 -22
  199. package/src/web/constants.ts +1 -1
  200. package/src/web/layout.ts +196 -0
  201. package/src/web/vaul/constants.ts +9 -0
  202. package/src/web/vaul/context.ts +1 -0
  203. package/src/web/vaul/helpers.ts +5 -0
  204. package/src/web/vaul/index.tsx +75 -5
  205. package/src/web/vaul/use-snap-points.ts +32 -15
@@ -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,15 @@ export const DEFAULT_PEEK_HEIGHT = 150;
9
9
 
10
10
  export const CLOSE_THRESHOLD = 0.25;
11
11
 
12
+ // Compose Material3 SheetState's dismiss thresholds, used by the 'short'
13
+ // dismiss threshold: velocity in px/ms, distance in px.
14
+ export const SHORT_DISMISS_VELOCITY = 0.125;
15
+ export const SHORT_DISMISS_DISTANCE = 56;
16
+
17
+ // Minimum downward pull (px) on a non-dismissible drawer at its first snap
18
+ // point before the release counts as a dismiss attempt.
19
+ export const DISMISS_ATTEMPT_THRESHOLD = 24;
20
+
12
21
  export const SCROLL_LOCK_TIMEOUT = 100;
13
22
 
14
23
  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,18 @@ export type DialogProps = {
93
101
  * @default true
94
102
  */
95
103
  dismissible?: boolean;
104
+ /**
105
+ * How far the drawer must be dragged past its first snap point to close.
106
+ * `'half'` closes past half of it, `'short'` on a short drag or any fling.
107
+ * @default 'half'
108
+ */
109
+ dismissThreshold?: 'half' | 'short';
110
+ /**
111
+ * Called when the user tries to close a non-dismissible drawer — dragging down
112
+ * at the first snap point, pressing escape, or tapping the handle of a
113
+ * single-snap-point drawer. Overlay clicks don't count, mirroring iOS.
114
+ */
115
+ onDismissAttempt?: () => void;
96
116
  /**
97
117
  * When `false` the drawer can't be dragged by the user. Programmatic snap-point
98
118
  * changes still animate. Defaults to `true`.
@@ -218,6 +238,8 @@ export function Root({
218
238
  closeThreshold = CLOSE_THRESHOLD,
219
239
  scrollLockTimeout = SCROLL_LOCK_TIMEOUT,
220
240
  dismissible = true,
241
+ dismissThreshold = 'half',
242
+ onDismissAttempt,
221
243
  draggable = true,
222
244
  handleOnly = false,
223
245
  fadeFromIndex = snapPoints && snapPoints.length - 1,
@@ -274,6 +296,7 @@ export function Root({
274
296
  const isAllowedToDrag = React.useRef<boolean>(false);
275
297
  const nestedOpenChangeTimer = React.useRef<NodeJS.Timeout | null>(null);
276
298
  const pointerStart = React.useRef(0);
299
+ const dismissAttempted = React.useRef(false);
277
300
  const keyboardIsOpen = React.useRef(false);
278
301
  const shouldAnimate = React.useRef(!defaultOpen);
279
302
  const previousDiffFromInitial = React.useRef(0);
@@ -517,7 +540,17 @@ export function Root({
517
540
  const noCloseSnapPointsPreCondition = snapPoints && !dismissible && !isDraggingInDirection;
518
541
 
519
542
  // 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;
543
+ if (noCloseSnapPointsPreCondition && activeSnapPointIndex === 0) {
544
+ // The drawer stays put. Remember a deliberate pull that would otherwise
545
+ // have dragged it (content at its top) so release() can report it.
546
+ if (
547
+ -draggedDistance > DISMISS_ATTEMPT_THRESHOLD &&
548
+ (isAllowedToDrag.current || shouldDrag(event.target, isDraggingInDirection))
549
+ ) {
550
+ dismissAttempted.current = true;
551
+ }
552
+ return;
553
+ }
521
554
 
522
555
  // We need to capture last time when drag with scroll was triggered and have a timeout between
523
556
  const absDraggedDistance = Math.abs(draggedDistance);
@@ -778,6 +811,21 @@ export function Root({
778
811
  unfreezeScrollables();
779
812
  setIsDragging(false);
780
813
  dragEndTime.current = new Date();
814
+
815
+ if (dismissAttempted.current) {
816
+ dismissAttempted.current = false;
817
+ if (isOpen) onDismissAttempt?.();
818
+ }
819
+
820
+ // The press that started this "drag" may have dismissed the drawer (e.g. a
821
+ // back button acting on pointerdown). Snapping now would overwrite the
822
+ // dismiss transform that closeDrawer just wrote and freeze the sheet in
823
+ // place until it unmounts.
824
+ if (!isOpen) {
825
+ if (pointerEvent) onReleaseProp?.(pointerEvent, false);
826
+ return;
827
+ }
828
+
781
829
  const swipeAmount = getTranslate(drawerRef.current, direction);
782
830
 
783
831
  // Drag overshoot below the lowest snap point translates the detached
@@ -814,6 +862,7 @@ export function Root({
814
862
  closeDrawer,
815
863
  velocity,
816
864
  dismissible,
865
+ dismissThreshold,
817
866
  });
818
867
  if (pointerEvent) onReleaseProp?.(pointerEvent, true);
819
868
  return;
@@ -966,6 +1015,7 @@ export function Root({
966
1015
  onTouchDrag: (event, pointerEvent) => drag(event, pointerEvent),
967
1016
  onTouchRelease: (event, pointerEvent) => release(event, pointerEvent),
968
1017
  dismissible,
1018
+ onDismissAttempt,
969
1019
  shouldAnimate,
970
1020
  handleOnly,
971
1021
  isOpen,
@@ -1121,6 +1171,7 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1121
1171
  snapPoints,
1122
1172
  setActiveSnapPoint,
1123
1173
  dismissible,
1174
+ onDismissAttempt,
1124
1175
  container,
1125
1176
  handleOnly,
1126
1177
  shouldAnimate,
@@ -1265,11 +1316,17 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1265
1316
  // same detent). Without this, position goes stale mid-animation because
1266
1317
  // the drawer's `transitionrun` never fires.
1267
1318
  const onTransitionRun = (e: TransitionEvent) => {
1268
- if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
1319
+ if (
1320
+ (e.target === drawer || e.target === wrapper) &&
1321
+ (e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
1322
+ )
1269
1323
  state.start();
1270
1324
  };
1271
1325
  const onTransitionDone = (e: TransitionEvent) => {
1272
- if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
1326
+ if (
1327
+ (e.target === drawer || e.target === wrapper) &&
1328
+ (e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
1329
+ )
1273
1330
  state.stop();
1274
1331
  };
1275
1332
  const onAnimationStart = (e: AnimationEvent) => {
@@ -1376,6 +1433,9 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1376
1433
  wasOpenRef.current = isOpen;
1377
1434
  }, [isOpen, detached, drawerRef]);
1378
1435
 
1436
+ // Subsequent snap writes belong to useSnapPoints. A React style update on
1437
+ // release would overwrite the live drag value before its transition starts.
1438
+ const initialSnapPointOffset = React.useRef(snapPointsOffset?.[activeSnapPointIndex ?? 0] ?? 0);
1379
1439
  const contentNode = (
1380
1440
  <DialogPrimitive.Content
1381
1441
  data-vaul-drawer-direction={direction}
@@ -1390,7 +1450,8 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1390
1450
  style={
1391
1451
  snapPointsOffset && snapPointsOffset.length > 0
1392
1452
  ? ({
1393
- '--snap-point-height': `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
1453
+ '--snap-point-height': `${initialSnapPointOffset.current}px`,
1454
+ 'transform': getSnapPointTransform(direction),
1394
1455
  ...style,
1395
1456
  'pointerEvents': 'auto',
1396
1457
  } as React.CSSProperties)
@@ -1453,6 +1514,8 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1453
1514
  activeSnapPointIndex > 0
1454
1515
  ) {
1455
1516
  setActiveSnapPoint(snapPoints![0]!);
1517
+ } else {
1518
+ onDismissAttempt?.();
1456
1519
  }
1457
1520
  }
1458
1521
  }}
@@ -1594,6 +1657,7 @@ export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(
1594
1657
  activeSnapPoint,
1595
1658
  setActiveSnapPoint,
1596
1659
  dismissible,
1660
+ onDismissAttempt,
1597
1661
  handleOnly,
1598
1662
  isOpen,
1599
1663
  onPress,
@@ -1637,6 +1701,12 @@ export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(
1637
1701
  return;
1638
1702
  }
1639
1703
 
1704
+ // Mirrors native: a single-detent sheet's handle tap is a dismiss.
1705
+ if (isLastSnapPoint && snapPoints.length === 1) {
1706
+ onDismissAttempt?.();
1707
+ return;
1708
+ }
1709
+
1640
1710
  const currentSnapIndex = snapPoints.findIndex((point) => point === activeSnapPoint);
1641
1711
  if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
1642
1712
  const nextSnapPoint = snapPoints[currentSnapIndex + 1];