@lodev09/react-native-true-sheet 4.0.0-beta.3 → 4.0.0-beta.30

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 (244) hide show
  1. package/README.md +28 -5
  2. package/android/build.gradle +23 -1
  3. package/android/proguard-rules.pro +3 -0
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +303 -184
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +38 -109
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +127 -0
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +29 -3
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +234 -66
  14. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +491 -186
  15. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +64 -10
  16. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -14
  17. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +135 -0
  18. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +23 -23
  19. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +130 -6
  20. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
  21. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +14 -17
  22. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
  23. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +148 -0
  24. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +33 -40
  25. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  26. package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
  27. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
  28. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  29. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  30. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  31. package/android/src/main/jni/TrueSheetStateUpdater.cpp +37 -6
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
  33. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +114 -30
  34. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
  35. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
  36. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
  37. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
  38. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +14 -3
  39. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +5 -6
  40. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
  41. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +10 -6
  42. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
  43. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
  45. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  46. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  47. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
  48. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
  49. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
  50. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
  51. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
  52. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
  53. package/ios/TrueSheetContainerView.h +23 -7
  54. package/ios/TrueSheetContainerView.mm +45 -25
  55. package/ios/TrueSheetContentView.h +44 -15
  56. package/ios/TrueSheetContentView.mm +221 -190
  57. package/ios/TrueSheetFooterView.mm +133 -28
  58. package/ios/TrueSheetModule.h +2 -7
  59. package/ios/TrueSheetModule.mm +18 -18
  60. package/ios/TrueSheetOverlayView.h +42 -0
  61. package/ios/TrueSheetOverlayView.mm +175 -0
  62. package/ios/TrueSheetView.h +2 -0
  63. package/ios/TrueSheetView.mm +268 -52
  64. package/ios/TrueSheetViewController.h +36 -3
  65. package/ios/TrueSheetViewController.mm +797 -102
  66. package/ios/core/RNScreensEventObserver.mm +31 -28
  67. package/ios/core/TrueSheetBlurView.h +0 -2
  68. package/ios/core/TrueSheetBlurView.mm +3 -36
  69. package/ios/core/TrueSheetDetentCalculator.h +18 -1
  70. package/ios/core/TrueSheetDetentCalculator.mm +44 -9
  71. package/ios/core/TrueSheetDimView.h +27 -0
  72. package/ios/core/TrueSheetDimView.mm +41 -0
  73. package/ios/core/TrueSheetGrabberView.mm +13 -3
  74. package/ios/core/TrueSheetKeyboardObserver.h +7 -0
  75. package/ios/core/TrueSheetKeyboardObserver.mm +107 -15
  76. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  77. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  78. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  79. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  80. package/ios/tvos/TrueSheetStubs.mm +12 -0
  81. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  82. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  83. package/lib/module/TrueSheet.js +222 -54
  84. package/lib/module/TrueSheet.js.map +1 -1
  85. package/lib/module/TrueSheet.web.js +364 -275
  86. package/lib/module/TrueSheet.web.js.map +1 -1
  87. package/lib/module/TrueSheetBackground.js +15 -0
  88. package/lib/module/TrueSheetBackground.js.map +1 -0
  89. package/lib/module/TrueSheetOverlay.js +30 -0
  90. package/lib/module/TrueSheetOverlay.js.map +1 -0
  91. package/lib/module/TrueSheetOverlay.web.js +35 -0
  92. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  93. package/lib/module/TrueSheetPeek.web.js +20 -15
  94. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  95. package/lib/module/TrueSheetProvider.web.js +13 -9
  96. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  97. package/lib/module/detentBackgrounds.js +21 -0
  98. package/lib/module/detentBackgrounds.js.map +1 -0
  99. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  100. package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  101. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  102. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +89 -73
  103. package/lib/module/index.js +1 -0
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/mocks/index.js +18 -2
  106. package/lib/module/mocks/index.js.map +1 -1
  107. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  108. package/lib/module/navigation/base-types.js +2 -0
  109. package/lib/module/navigation/base-types.js.map +1 -0
  110. package/lib/module/navigation/createTrueSheetRouter.js +158 -100
  111. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  112. package/lib/module/navigation/expo-router/base-types.js +2 -0
  113. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  114. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js +71 -0
  115. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js.map +1 -0
  116. package/lib/module/navigation/expo-router/index.js +18 -7
  117. package/lib/module/navigation/expo-router/index.js.map +1 -1
  118. package/lib/module/navigation/expo-router/types.js +4 -0
  119. package/lib/module/navigation/expo-router/types.js.map +1 -0
  120. package/lib/module/navigation/index.js.map +1 -1
  121. package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
  122. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  123. package/lib/module/scrollable.js +28 -0
  124. package/lib/module/scrollable.js.map +1 -0
  125. package/lib/module/web/constants.js +1 -1
  126. package/lib/module/web/constants.js.map +1 -1
  127. package/lib/module/web/keyboard.js +69 -0
  128. package/lib/module/web/keyboard.js.map +1 -0
  129. package/lib/module/web/layout.js +168 -0
  130. package/lib/module/web/layout.js.map +1 -0
  131. package/lib/module/web/vaul/constants.js +9 -0
  132. package/lib/module/web/vaul/constants.js.map +1 -1
  133. package/lib/module/web/vaul/context.js +4 -0
  134. package/lib/module/web/vaul/context.js.map +1 -1
  135. package/lib/module/web/vaul/helpers.js +1 -0
  136. package/lib/module/web/vaul/helpers.js.map +1 -1
  137. package/lib/module/web/vaul/index.js +83 -21
  138. package/lib/module/web/vaul/index.js.map +1 -1
  139. package/lib/module/web/vaul/use-snap-points.js +57 -28
  140. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  141. package/lib/typescript/src/TrueSheet.d.ts +18 -1
  142. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  143. package/lib/typescript/src/TrueSheet.types.d.ts +209 -35
  144. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  145. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  146. package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
  147. package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
  148. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  149. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  150. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  151. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  152. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  153. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  154. package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -1
  155. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  156. package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
  157. package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
  158. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -3
  159. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
  160. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  161. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  162. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +76 -65
  163. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  164. package/lib/typescript/src/index.d.ts +1 -0
  165. package/lib/typescript/src/index.d.ts.map +1 -1
  166. package/lib/typescript/src/mocks/index.d.ts +4 -0
  167. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  168. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  169. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  170. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  171. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  172. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +36 -10
  173. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  174. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  175. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  176. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts +33 -0
  177. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts.map +1 -0
  178. package/lib/typescript/src/navigation/expo-router/index.d.ts +9 -6
  179. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  180. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  181. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  182. package/lib/typescript/src/navigation/index.d.ts +3 -4
  183. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  184. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
  185. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  186. package/lib/typescript/src/navigation/types.d.ts +43 -6
  187. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  188. package/lib/typescript/src/scrollable.d.ts +2 -0
  189. package/lib/typescript/src/scrollable.d.ts.map +1 -0
  190. package/lib/typescript/src/web/constants.d.ts +1 -1
  191. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  192. package/lib/typescript/src/web/keyboard.d.ts +15 -0
  193. package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
  194. package/lib/typescript/src/web/layout.d.ts +20 -0
  195. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  196. package/lib/typescript/src/web/vaul/constants.d.ts +3 -0
  197. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  198. package/lib/typescript/src/web/vaul/context.d.ts +5 -1
  199. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  200. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  201. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  202. package/lib/typescript/src/web/vaul/index.d.ts +20 -1
  203. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  204. package/lib/typescript/src/web/vaul/types.d.ts +9 -0
  205. package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
  206. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +6 -4
  207. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  208. package/package.json +18 -8
  209. package/react-native.config.js +1 -0
  210. package/src/TrueSheet.tsx +257 -62
  211. package/src/TrueSheet.types.ts +225 -35
  212. package/src/TrueSheet.web.tsx +445 -333
  213. package/src/TrueSheetBackground.tsx +17 -0
  214. package/src/TrueSheetOverlay.tsx +24 -0
  215. package/src/TrueSheetOverlay.web.tsx +33 -0
  216. package/src/TrueSheetPeek.web.tsx +25 -23
  217. package/src/TrueSheetProvider.web.tsx +22 -13
  218. package/src/detentBackgrounds.ts +24 -0
  219. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  220. package/src/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  221. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  222. package/src/fabric/TrueSheetViewNativeComponent.ts +89 -73
  223. package/src/index.ts +1 -0
  224. package/src/mocks/index.ts +17 -2
  225. package/src/mocks/navigationExpoRouter.ts +6 -5
  226. package/src/navigation/base-types.ts +20 -0
  227. package/src/navigation/createTrueSheetRouter.ts +220 -144
  228. package/src/navigation/expo-router/base-types.ts +18 -0
  229. package/src/navigation/expo-router/extendTrueSheetRouter.ts +99 -0
  230. package/src/navigation/expo-router/index.ts +41 -30
  231. package/src/navigation/expo-router/types.ts +116 -0
  232. package/src/navigation/index.ts +6 -7
  233. package/src/navigation/screen/useSheetScreenState.ts +2 -0
  234. package/src/navigation/types.ts +81 -23
  235. package/src/scrollable.ts +25 -0
  236. package/src/web/constants.ts +1 -1
  237. package/src/web/keyboard.ts +79 -0
  238. package/src/web/layout.ts +196 -0
  239. package/src/web/vaul/constants.ts +9 -0
  240. package/src/web/vaul/context.ts +7 -1
  241. package/src/web/vaul/helpers.ts +5 -0
  242. package/src/web/vaul/index.tsx +113 -16
  243. package/src/web/vaul/types.ts +10 -0
  244. package/src/web/vaul/use-snap-points.ts +86 -33
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { DrawerDirection } from './types';
2
+ import type { DrawerDirection, KeyboardViewport } from './types';
3
3
 
4
4
  export interface DragEvent {
5
5
  target: EventTarget;
@@ -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>;
@@ -45,12 +46,15 @@ interface DrawerContextValue {
45
46
  container?: HTMLElement | null;
46
47
  autoFocus?: boolean;
47
48
  shouldAnimate?: React.RefObject<boolean>;
49
+ initialAnimated: boolean;
50
+ hasPresented: React.RefObject<boolean>;
48
51
  onPositionChangeRef: React.RefObject<((position: number) => void) | undefined>;
49
52
  setContentHeight: (height: number) => void;
50
53
  detached: boolean;
51
54
  detachedOffset: number;
52
55
  detachedRadius: number;
53
56
  detachedWrapperStyle?: React.CSSProperties;
57
+ keyboardViewport?: KeyboardViewport;
54
58
  }
55
59
 
56
60
  export const DrawerContext = React.createContext<DrawerContextValue>({
@@ -80,6 +84,8 @@ export const DrawerContext = React.createContext<DrawerContextValue>({
80
84
  closeDrawer: () => {},
81
85
  direction: 'bottom',
82
86
  shouldAnimate: { current: true },
87
+ initialAnimated: true,
88
+ hasPresented: { current: false },
83
89
  shouldScaleBackground: false,
84
90
  setBackgroundColorOnScale: true,
85
91
  noBodyStyles: false,
@@ -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,8 +20,15 @@ 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 type { DrawerDirection } from './types';
23
+ import {
24
+ dampenValue,
25
+ getSnapPointTransform,
26
+ getTranslate,
27
+ isVertical,
28
+ reset,
29
+ set,
30
+ } from './helpers';
31
+ import type { DrawerDirection, KeyboardViewport } from './types';
24
32
  import { useComposedRefs } from './use-composed-refs';
25
33
  import { useControllableState } from './use-controllable-state';
26
34
  import { usePositionFixed } from './use-position-fixed';
@@ -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`.
@@ -204,6 +224,12 @@ export type DialogProps = {
204
224
  * from natural flow.
205
225
  */
206
226
  contentHeight?: number;
227
+ /**
228
+ * Lays the drawer out in the visual viewport above a soft keyboard. The
229
+ * wrapper and snap offsets fit the visible area, while fractional snap
230
+ * points keep scaling against the full layout height.
231
+ */
232
+ keyboardViewport?: KeyboardViewport;
207
233
  } & (WithFadeFromProps | WithoutFadeFromProps);
208
234
 
209
235
  export function Root({
@@ -218,6 +244,8 @@ export function Root({
218
244
  closeThreshold = CLOSE_THRESHOLD,
219
245
  scrollLockTimeout = SCROLL_LOCK_TIMEOUT,
220
246
  dismissible = true,
247
+ dismissThreshold = 'half',
248
+ onDismissAttempt,
221
249
  draggable = true,
222
250
  handleOnly = false,
223
251
  fadeFromIndex = snapPoints && snapPoints.length - 1,
@@ -247,6 +275,7 @@ export function Root({
247
275
  initialAnimated = true,
248
276
  onContentHeightChange,
249
277
  contentHeight: contentHeightProp,
278
+ keyboardViewport,
250
279
  }: DialogProps) {
251
280
  const [isOpen = false, setIsOpen] = useControllableState({
252
281
  defaultProp: defaultOpen,
@@ -274,8 +303,10 @@ export function Root({
274
303
  const isAllowedToDrag = React.useRef<boolean>(false);
275
304
  const nestedOpenChangeTimer = React.useRef<NodeJS.Timeout | null>(null);
276
305
  const pointerStart = React.useRef(0);
306
+ const dismissAttempted = React.useRef(false);
277
307
  const keyboardIsOpen = React.useRef(false);
278
308
  const shouldAnimate = React.useRef(!defaultOpen);
309
+ const hasPresented = React.useRef(false);
279
310
  const previousDiffFromInitial = React.useRef(0);
280
311
  const drawerRef = React.useRef<HTMLDivElement>(null);
281
312
  const drawerHeightRef = React.useRef(drawerRef.current?.getBoundingClientRect().height || 0);
@@ -292,10 +323,14 @@ export function Root({
292
323
  onContentHeightChangeRef.current?.(measuredContentHeight);
293
324
  }, [measuredContentHeight]);
294
325
 
295
- const onSnapPointChange = React.useCallback((activeSnapPointIndex: number) => {
326
+ const onSnapPointChange = React.useCallback((activeSnapPointIndex: number, animated: boolean) => {
296
327
  // Change openTime ref when we reach the last snap point to prevent dragging for 500ms incase it's scrollable.
297
328
  if (snapPoints && activeSnapPointIndex === snapPointsOffset.length - 1)
298
329
  openTime.current = new Date();
330
+ // Instant snaps do not emit transition events for position tracking.
331
+ if (!animated && drawerRef.current) {
332
+ onPositionChangeRef.current?.(drawerRef.current.getBoundingClientRect().top);
333
+ }
299
334
  }, []);
300
335
 
301
336
  const {
@@ -324,6 +359,7 @@ export function Root({
324
359
  maxContentHeight,
325
360
  peekHeight,
326
361
  initialAnimated,
362
+ keyboardViewport,
327
363
  });
328
364
 
329
365
  usePreventScroll({
@@ -517,7 +553,17 @@ export function Root({
517
553
  const noCloseSnapPointsPreCondition = snapPoints && !dismissible && !isDraggingInDirection;
518
554
 
519
555
  // 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;
556
+ if (noCloseSnapPointsPreCondition && activeSnapPointIndex === 0) {
557
+ // The drawer stays put. Remember a deliberate pull that would otherwise
558
+ // have dragged it (content at its top) so release() can report it.
559
+ if (
560
+ -draggedDistance > DISMISS_ATTEMPT_THRESHOLD &&
561
+ (isAllowedToDrag.current || shouldDrag(event.target, isDraggingInDirection))
562
+ ) {
563
+ dismissAttempted.current = true;
564
+ }
565
+ return;
566
+ }
521
567
 
522
568
  // We need to capture last time when drag with scroll was triggered and have a timeout between
523
569
  const absDraggedDistance = Math.abs(draggedDistance);
@@ -778,6 +824,21 @@ export function Root({
778
824
  unfreezeScrollables();
779
825
  setIsDragging(false);
780
826
  dragEndTime.current = new Date();
827
+
828
+ if (dismissAttempted.current) {
829
+ dismissAttempted.current = false;
830
+ if (isOpen) onDismissAttempt?.();
831
+ }
832
+
833
+ // The press that started this "drag" may have dismissed the drawer (e.g. a
834
+ // back button acting on pointerdown). Snapping now would overwrite the
835
+ // dismiss transform that closeDrawer just wrote and freeze the sheet in
836
+ // place until it unmounts.
837
+ if (!isOpen) {
838
+ if (pointerEvent) onReleaseProp?.(pointerEvent, false);
839
+ return;
840
+ }
841
+
781
842
  const swipeAmount = getTranslate(drawerRef.current, direction);
782
843
 
783
844
  // Drag overshoot below the lowest snap point translates the detached
@@ -814,6 +875,7 @@ export function Root({
814
875
  closeDrawer,
815
876
  velocity,
816
877
  dismissible,
878
+ dismissThreshold,
817
879
  });
818
880
  if (pointerEvent) onReleaseProp?.(pointerEvent, true);
819
881
  return;
@@ -966,7 +1028,10 @@ export function Root({
966
1028
  onTouchDrag: (event, pointerEvent) => drag(event, pointerEvent),
967
1029
  onTouchRelease: (event, pointerEvent) => release(event, pointerEvent),
968
1030
  dismissible,
1031
+ onDismissAttempt,
969
1032
  shouldAnimate,
1033
+ initialAnimated,
1034
+ hasPresented,
970
1035
  handleOnly,
971
1036
  isOpen,
972
1037
  isDragging,
@@ -992,6 +1057,7 @@ export function Root({
992
1057
  detachedOffset: detached ? detachedOffset : 0,
993
1058
  detachedRadius: detached ? detachedRadius : 0,
994
1059
  detachedWrapperStyle,
1060
+ keyboardViewport,
995
1061
  }}
996
1062
  >
997
1063
  {children}
@@ -1121,9 +1187,12 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1121
1187
  snapPoints,
1122
1188
  setActiveSnapPoint,
1123
1189
  dismissible,
1190
+ onDismissAttempt,
1124
1191
  container,
1125
1192
  handleOnly,
1126
1193
  shouldAnimate,
1194
+ initialAnimated,
1195
+ hasPresented,
1127
1196
  autoFocus,
1128
1197
  onPositionChangeRef,
1129
1198
  setContentHeight,
@@ -1131,6 +1200,7 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1131
1200
  detachedOffset,
1132
1201
  detachedRadius,
1133
1202
  detachedWrapperStyle: detachedWrapperStyleProp,
1203
+ keyboardViewport,
1134
1204
  } = useDrawerContext();
1135
1205
  // Always measure the inner wrapper's natural height. The drawer itself may
1136
1206
  // be styled to a fixed viewport height, so we measure an inner wrapper
@@ -1265,11 +1335,17 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1265
1335
  // same detent). Without this, position goes stale mid-animation because
1266
1336
  // the drawer's `transitionrun` never fires.
1267
1337
  const onTransitionRun = (e: TransitionEvent) => {
1268
- if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
1338
+ if (
1339
+ (e.target === drawer || e.target === wrapper) &&
1340
+ (e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
1341
+ )
1269
1342
  state.start();
1270
1343
  };
1271
1344
  const onTransitionDone = (e: TransitionEvent) => {
1272
- if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
1345
+ if (
1346
+ (e.target === drawer || e.target === wrapper) &&
1347
+ (e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
1348
+ )
1273
1349
  state.stop();
1274
1350
  };
1275
1351
  const onAnimationStart = (e: AnimationEvent) => {
@@ -1327,14 +1403,19 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1327
1403
  // with a bottom gap and rounded bottom corners; otherwise it sits flush.
1328
1404
  // Transform/transition are managed imperatively (via drag overshoot and the
1329
1405
  // dismiss effect) so React doesn't skip DOM writes for values it thinks it
1330
- // already owns.
1406
+ // already owns. Above a soft keyboard the wrapper fits the visual viewport,
1407
+ // which the full-height layout viewport extends behind.
1331
1408
  const wrapperStyle = React.useMemo<React.CSSProperties>(
1332
1409
  () => ({
1333
1410
  position: 'fixed',
1334
- top: 0,
1335
1411
  left: 0,
1336
1412
  right: 0,
1337
- bottom: detached ? detachedOffset : 0,
1413
+ ...(keyboardViewport
1414
+ ? {
1415
+ top: keyboardViewport.top,
1416
+ height: Math.max(0, keyboardViewport.height - (detached ? detachedOffset : 0)),
1417
+ }
1418
+ : { top: 0, bottom: detached ? detachedOffset : 0 }),
1338
1419
  overflow: 'hidden',
1339
1420
  contain: 'paint',
1340
1421
  pointerEvents: 'none',
@@ -1342,7 +1423,7 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1342
1423
  borderBottomRightRadius: detached ? detachedRadius : 0,
1343
1424
  ...detachedWrapperStyleProp,
1344
1425
  }),
1345
- [detached, detachedOffset, detachedRadius, detachedWrapperStyleProp]
1426
+ [detached, detachedOffset, detachedRadius, detachedWrapperStyleProp, keyboardViewport]
1346
1427
  );
1347
1428
 
1348
1429
  // Translate the wrapper off-screen on dismiss so the whole card slides out
@@ -1366,16 +1447,22 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1366
1447
  wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
1367
1448
  } else if (isOpen && !wasOpenRef.current) {
1368
1449
  wrapper.style.transition = 'none';
1369
- wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
1370
- // eslint-disable-next-line no-void
1371
- void wrapper.offsetHeight;
1372
- wrapper.style.transition = transition;
1450
+ if (hasPresented.current || initialAnimated) {
1451
+ wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
1452
+ // eslint-disable-next-line no-void
1453
+ void wrapper.offsetHeight;
1454
+ wrapper.style.transition = transition;
1455
+ }
1373
1456
  wrapper.style.transform = 'translate3d(0, 0, 0)';
1457
+ hasPresented.current = true;
1374
1458
  }
1375
1459
  }
1376
1460
  wasOpenRef.current = isOpen;
1377
- }, [isOpen, detached, drawerRef]);
1461
+ }, [isOpen, detached, drawerRef, initialAnimated, hasPresented]);
1378
1462
 
1463
+ // Subsequent snap writes belong to useSnapPoints. A React style update on
1464
+ // release would overwrite the live drag value before its transition starts.
1465
+ const initialSnapPointOffset = React.useRef(snapPointsOffset?.[activeSnapPointIndex ?? 0] ?? 0);
1379
1466
  const contentNode = (
1380
1467
  <DialogPrimitive.Content
1381
1468
  data-vaul-drawer-direction={direction}
@@ -1390,7 +1477,8 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1390
1477
  style={
1391
1478
  snapPointsOffset && snapPointsOffset.length > 0
1392
1479
  ? ({
1393
- '--snap-point-height': `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
1480
+ '--snap-point-height': `${initialSnapPointOffset.current}px`,
1481
+ 'transform': getSnapPointTransform(direction),
1394
1482
  ...style,
1395
1483
  'pointerEvents': 'auto',
1396
1484
  } as React.CSSProperties)
@@ -1453,6 +1541,8 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1453
1541
  activeSnapPointIndex > 0
1454
1542
  ) {
1455
1543
  setActiveSnapPoint(snapPoints![0]!);
1544
+ } else {
1545
+ onDismissAttempt?.();
1456
1546
  }
1457
1547
  }
1458
1548
  }}
@@ -1594,6 +1684,7 @@ export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(
1594
1684
  activeSnapPoint,
1595
1685
  setActiveSnapPoint,
1596
1686
  dismissible,
1687
+ onDismissAttempt,
1597
1688
  handleOnly,
1598
1689
  isOpen,
1599
1690
  onPress,
@@ -1637,6 +1728,12 @@ export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(
1637
1728
  return;
1638
1729
  }
1639
1730
 
1731
+ // Mirrors native: a single-detent sheet's handle tap is a dismiss.
1732
+ if (isLastSnapPoint && snapPoints.length === 1) {
1733
+ onDismissAttempt?.();
1734
+ return;
1735
+ }
1736
+
1640
1737
  const currentSnapIndex = snapPoints.findIndex((point) => point === activeSnapPoint);
1641
1738
  if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
1642
1739
  const nextSnapPoint = snapPoints[currentSnapIndex + 1];
@@ -5,3 +5,13 @@ export interface SnapPoint {
5
5
  }
6
6
 
7
7
  export type AnyFunction = (...args: any) => any;
8
+
9
+ /**
10
+ * The visual viewport while a soft keyboard shrinks it, in layout-viewport px.
11
+ * `layoutHeight` is the full (keyboard-free) height.
12
+ */
13
+ export interface KeyboardViewport {
14
+ top: number;
15
+ height: number;
16
+ layoutHeight: number;
17
+ }
@@ -1,9 +1,15 @@
1
1
  // @ts-nocheck — vendored upstream, incompatible with noUncheckedIndexedAccess
2
2
  import React from 'react';
3
- import { set, isVertical } from './helpers';
4
- import { DEFAULT_PEEK_HEIGHT, TRANSITIONS, VELOCITY_THRESHOLD } from './constants';
3
+ import { set, isVertical, getSnapPointTransform } from './helpers';
4
+ import {
5
+ DEFAULT_PEEK_HEIGHT,
6
+ SHORT_DISMISS_DISTANCE,
7
+ SHORT_DISMISS_VELOCITY,
8
+ TRANSITIONS,
9
+ VELOCITY_THRESHOLD,
10
+ } from './constants';
5
11
  import { useControllableState } from './use-controllable-state';
6
- import type { DrawerDirection } from './types';
12
+ import type { DrawerDirection, KeyboardViewport } from './types';
7
13
 
8
14
  export function useSnapPoints({
9
15
  activeSnapPointProp,
@@ -22,6 +28,7 @@ export function useSnapPoints({
22
28
  maxContentHeight,
23
29
  peekHeight,
24
30
  initialAnimated = true,
31
+ keyboardViewport,
25
32
  }: {
26
33
  activeSnapPointProp?: number | string | null;
27
34
  setActiveSnapPointProp?(snapPoint: number | null | string): void;
@@ -29,7 +36,7 @@ export function useSnapPoints({
29
36
  fadeFromIndex?: number;
30
37
  drawerRef: React.RefObject<HTMLDivElement | null>;
31
38
  overlayRef: React.RefObject<HTMLDivElement | null>;
32
- onSnapPointChange(activeSnapPointIndex: number): void;
39
+ onSnapPointChange(activeSnapPointIndex: number, animated: boolean): void;
33
40
  direction?: DrawerDirection;
34
41
  container?: HTMLElement | null | undefined;
35
42
  snapToSequentialPoint?: boolean;
@@ -39,6 +46,7 @@ export function useSnapPoints({
39
46
  maxContentHeight?: number;
40
47
  peekHeight?: number;
41
48
  initialAnimated?: boolean;
49
+ keyboardViewport?: KeyboardViewport;
42
50
  }) {
43
51
  const [activeSnapPoint, setActiveSnapPoint] = useControllableState<string | number | null>({
44
52
  prop: activeSnapPointProp,
@@ -92,13 +100,18 @@ export function useSnapPoints({
92
100
  height: container.getBoundingClientRect().height,
93
101
  }
94
102
  : typeof window !== 'undefined'
95
- ? { width: window.innerWidth, height: window.innerHeight }
103
+ ? { width: window.innerWidth, height: keyboardViewport?.height ?? window.innerHeight }
96
104
  : { width: 0, height: 0 };
97
105
 
98
106
  // Shrink the effective vertical area by detachedOffset so snap math
99
107
  // reserves a fixed gap at the bottom — the floating card stays offset
100
108
  // from the viewport edge through drag, snap and resize.
101
109
  const effectiveHeight = Math.max(0, containerSize.height - (detachedOffset || 0));
110
+ // Above a keyboard, fractional snap points keep their full-height size and
111
+ // only cap at the visible area — mirrors native, where the sheet rises.
112
+ const fractionHeight = keyboardViewport
113
+ ? Math.max(0, keyboardViewport.layoutHeight - (detachedOffset || 0))
114
+ : effectiveHeight;
102
115
 
103
116
  return (
104
117
  snapPoints?.map((snapPoint) => {
@@ -123,11 +136,11 @@ export function useSnapPoints({
123
136
  const rawHeight = isPx
124
137
  ? snapPointAsNumber
125
138
  : windowDimensions
126
- ? resolved * effectiveHeight
139
+ ? resolved * fractionHeight
127
140
  : 0;
128
- // Cap px/'auto' snap points so a content taller than the viewport
129
- // doesn't push the drawer past the top edge (offset would go
130
- // negative). Fractional detents are already ≤ effectiveHeight.
141
+ // Cap snap points so content taller than the viewport (or a
142
+ // fraction above a keyboard) doesn't push the drawer past the top
143
+ // edge (offset would go negative).
131
144
  // `maxContentHeight`, when set, caps the visible sheet further so
132
145
  // full/'auto' detents respect the user's ceiling.
133
146
  const ceiling =
@@ -164,8 +177,11 @@ export function useSnapPoints({
164
177
  detachedOffset,
165
178
  maxContentHeight,
166
179
  peekHeight,
180
+ keyboardViewport,
167
181
  ]);
168
182
 
183
+ const getViewportHeight = () => keyboardViewport?.height ?? window.innerHeight;
184
+
169
185
  const activeSnapPointOffset = React.useMemo(
170
186
  () => (activeSnapPointIndex !== null ? snapPointsOffset?.[activeSnapPointIndex] : null),
171
187
  [snapPointsOffset, activeSnapPointIndex]
@@ -187,33 +203,46 @@ export function useSnapPoints({
187
203
  });
188
204
  };
189
205
 
190
- // Skip the transition on the very first snap when the consumer opts out of
191
- // the initial presentation animation. All subsequent snaps still animate.
206
+ // Initial measurements can take several commits before the first paint.
207
+ // Keep all of them unanimated when initial presentation animation is disabled.
192
208
  const hasSnappedRef = React.useRef(false);
209
+ const initialSnapFrameRef = React.useRef<number | null>(null);
210
+ React.useEffect(
211
+ () => () => {
212
+ if (initialSnapFrameRef.current !== null) {
213
+ window.cancelAnimationFrame(initialSnapFrameRef.current);
214
+ initialSnapFrameRef.current = null;
215
+ }
216
+ },
217
+ []
218
+ );
193
219
  const snapToPoint = React.useCallback(
194
220
  (dimension: number) => {
221
+ if (!drawerRef.current) return;
195
222
  const newSnapPointIndex =
196
223
  snapPointsOffset?.findIndex((snapPointDim) => snapPointDim === dimension) ?? null;
197
- onSnapPointChange(newSnapPointIndex);
198
224
 
199
225
  const animateThisSnap = hasSnappedRef.current || initialAnimated;
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.
226
+ if (initialSnapFrameRef.current === null) {
227
+ initialSnapFrameRef.current = window.requestAnimationFrame(() => {
228
+ hasSnappedRef.current = true;
229
+ });
230
+ }
231
+ // One animated value drives both the translation and the visible height.
204
232
  set(drawerRef.current, {
205
233
  'transition': animateThisSnap
206
- ? `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
234
+ ? `--snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
207
235
  : 'none',
208
- 'transform': isVertical(direction)
209
- ? `translate3d(0, ${dimension}px, 0)`
210
- : `translate3d(${dimension}px, 0, 0)`,
236
+ 'transform': getSnapPointTransform(direction),
211
237
  '--snap-point-height': `${dimension}px`,
212
238
  });
213
239
 
214
240
  // Snapping implies drag overshoot (if any) should be undone.
215
241
  setDetachedWrapperTransform(0, animateThisSnap);
216
242
 
243
+ const overlayTransition = animateThisSnap
244
+ ? `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
245
+ : 'none';
217
246
  if (
218
247
  snapPointsOffset &&
219
248
  newSnapPointIndex !== snapPointsOffset.length - 1 &&
@@ -222,24 +251,33 @@ export function useSnapPoints({
222
251
  newSnapPointIndex < fadeFromIndex
223
252
  ) {
224
253
  set(overlayRef.current, {
225
- transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
254
+ transition: overlayTransition,
226
255
  opacity: '0',
227
256
  pointerEvents: 'none',
228
257
  });
229
258
  } else {
230
259
  set(overlayRef.current, {
231
- transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
260
+ transition: overlayTransition,
232
261
  opacity: '1',
233
262
  pointerEvents: 'auto',
234
263
  });
235
264
  }
236
265
 
266
+ onSnapPointChange(newSnapPointIndex, animateThisSnap);
237
267
  setActiveSnapPoint(snapPoints?.[Math.max(newSnapPointIndex, 0)]);
238
268
  },
239
- [drawerRef.current, snapPoints, snapPointsOffset, fadeFromIndex, overlayRef, setActiveSnapPoint]
269
+ [
270
+ drawerRef.current,
271
+ snapPoints,
272
+ snapPointsOffset,
273
+ fadeFromIndex,
274
+ overlayRef,
275
+ setActiveSnapPoint,
276
+ initialAnimated,
277
+ ]
240
278
  );
241
279
 
242
- React.useEffect(() => {
280
+ React.useLayoutEffect(() => {
243
281
  if (!isOpen) return;
244
282
  if (activeSnapPoint || activeSnapPointProp) {
245
283
  const newIndex =
@@ -257,11 +295,13 @@ export function useSnapPoints({
257
295
  closeDrawer,
258
296
  velocity,
259
297
  dismissible,
298
+ dismissThreshold,
260
299
  }: {
261
300
  draggedDistance: number;
262
301
  closeDrawer: () => void;
263
302
  velocity: number;
264
303
  dismissible: boolean;
304
+ dismissThreshold: 'half' | 'short';
265
305
  }) {
266
306
  if (fadeFromIndex === undefined) return;
267
307
 
@@ -279,6 +319,23 @@ export function useSnapPoints({
279
319
  });
280
320
  }
281
321
 
322
+ // Short threshold: below the lowest detent, close on a short drag or any
323
+ // downward fling, like Compose Material3's sheet.
324
+ if (
325
+ dismissThreshold === 'short' &&
326
+ dismissible &&
327
+ (direction === 'bottom' || direction === 'right') &&
328
+ currentPosition > snapPointsOffset[0]
329
+ ) {
330
+ const overshoot = currentPosition - snapPointsOffset[0];
331
+ if (velocity > SHORT_DISMISS_VELOCITY || overshoot > SHORT_DISMISS_DISTANCE) {
332
+ closeDrawer();
333
+ } else {
334
+ snapToPoint(snapPointsOffset[0]);
335
+ }
336
+ return;
337
+ }
338
+
282
339
  // Distance-based dismiss: when overshooting past the lowest detent by more
283
340
  // than half of its visible height, commit to dismiss regardless of
284
341
  // velocity — lets a slow drag-down still close the sheet.
@@ -287,7 +344,7 @@ export function useSnapPoints({
287
344
  (direction === 'bottom' || direction === 'right') &&
288
345
  currentPosition > snapPointsOffset[0]
289
346
  ) {
290
- const viewportDim = isVertical(direction) ? window.innerHeight : window.innerWidth;
347
+ const viewportDim = isVertical(direction) ? getViewportHeight() : window.innerWidth;
291
348
  const effectiveDim = Math.max(0, viewportDim - (detachedOffset || 0));
292
349
  const visibleAtLowest = Math.max(0, effectiveDim - snapPointsOffset[0]);
293
350
  const overshoot = currentPosition - snapPointsOffset[0];
@@ -315,7 +372,7 @@ export function useSnapPoints({
315
372
  return Math.abs(curr - currentPosition) < Math.abs(prev - currentPosition) ? curr : prev;
316
373
  });
317
374
 
318
- const dim = isVertical(direction) ? window.innerHeight : window.innerWidth;
375
+ const dim = isVertical(direction) ? getViewportHeight() : window.innerWidth;
319
376
  if (velocity > VELOCITY_THRESHOLD && Math.abs(draggedDistance) < dim * 0.4) {
320
377
  const dragDirection = hasDraggedUp ? 1 : -1; // 1 = up, -1 = down
321
378
 
@@ -364,9 +421,7 @@ export function useSnapPoints({
364
421
  if ((direction === 'bottom' || direction === 'right') && newValue > snapPointsOffset[0]) {
365
422
  const excess = newValue - snapPointsOffset[0];
366
423
  set(drawerRef.current, {
367
- 'transform': isVertical(direction)
368
- ? `translate3d(0, ${snapPointsOffset[0]}px, 0)`
369
- : `translate3d(${snapPointsOffset[0]}px, 0, 0)`,
424
+ 'transform': getSnapPointTransform(direction),
370
425
  '--snap-point-height': `${snapPointsOffset[0]}px`,
371
426
  });
372
427
  setDetachedWrapperTransform(excess, false);
@@ -378,9 +433,7 @@ export function useSnapPoints({
378
433
  // derived from it (e.g. the scrollable fill) resize with the drawer
379
434
  // instead of staying cut off at the last detent's visible height.
380
435
  set(drawerRef.current, {
381
- 'transform': isVertical(direction)
382
- ? `translate3d(0, ${newValue}px, 0)`
383
- : `translate3d(${newValue}px, 0, 0)`,
436
+ 'transform': getSnapPointTransform(direction),
384
437
  '--snap-point-height': `${newValue}px`,
385
438
  });
386
439
  }
@@ -401,7 +454,7 @@ export function useSnapPoints({
401
454
  if (activeSnapPointIndex === 0 && !isDraggingDown) {
402
455
  const activeOffset = snapPointsOffset[activeSnapPointIndex];
403
456
  if (activeOffset === undefined) return 0;
404
- const windowDim = isVertical(direction) ? window.innerHeight : window.innerWidth;
457
+ const windowDim = isVertical(direction) ? getViewportHeight() : window.innerWidth;
405
458
  const dismissOffset =
406
459
  direction === 'bottom' || direction === 'right' ? windowDim : -windowDim;
407
460
  const dismissRange = Math.abs(dismissOffset - activeOffset);