@hanzogui/sheet 7.3.0 → 8.0.0

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 (229) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/controller/index.cjs +2 -0
  3. package/controller/index.d.ts +5 -0
  4. package/controller/index.js +2 -0
  5. package/controller/index.native.cjs +2 -0
  6. package/controller/index.native.js +2 -0
  7. package/dist/cjs/GestureDetectorWrapper.native.js.map +1 -1
  8. package/dist/cjs/SheetImplementationCustom.cjs +137 -74
  9. package/dist/cjs/SheetImplementationCustom.native.js +162 -83
  10. package/dist/cjs/SheetImplementationCustom.native.js.map +1 -1
  11. package/dist/cjs/SheetScrollView.cjs +155 -34
  12. package/dist/cjs/SheetScrollView.native.js +160 -35
  13. package/dist/cjs/SheetScrollView.native.js.map +1 -1
  14. package/dist/cjs/createSheet.cjs +7 -3
  15. package/dist/cjs/createSheet.native.js +7 -3
  16. package/dist/cjs/createSheet.native.js.map +1 -1
  17. package/dist/cjs/gestureState.cjs +13 -2
  18. package/dist/cjs/gestureState.native.js +14 -2
  19. package/dist/cjs/gestureState.native.js.map +1 -1
  20. package/dist/cjs/index.cjs +12 -1
  21. package/dist/cjs/index.native.js +11 -0
  22. package/dist/cjs/index.native.js.map +1 -1
  23. package/dist/cjs/keyboardAvoidance.cjs +93 -0
  24. package/dist/cjs/keyboardAvoidance.native.js +99 -0
  25. package/dist/cjs/keyboardAvoidance.native.js.map +1 -0
  26. package/dist/cjs/nativeSheet.cjs +3 -0
  27. package/dist/cjs/nativeSheet.native.js +3 -0
  28. package/dist/cjs/nativeSheet.native.js.map +1 -1
  29. package/dist/cjs/setupGestureHandler.cjs +9 -6
  30. package/dist/cjs/setupGestureHandler.native.js +11 -6
  31. package/dist/cjs/setupGestureHandler.native.js.map +1 -1
  32. package/dist/cjs/useGestureHandlerPan.cjs +86 -18
  33. package/dist/cjs/useGestureHandlerPan.native.js +91 -18
  34. package/dist/cjs/useGestureHandlerPan.native.js.map +1 -1
  35. package/dist/cjs/useKeyboardControllerSheet.cjs +68 -6
  36. package/dist/cjs/useSafeAreaInsets.cjs +44 -0
  37. package/dist/cjs/useSafeAreaInsets.native.js +48 -0
  38. package/dist/cjs/useSafeAreaInsets.native.js.map +1 -0
  39. package/dist/cjs/useSheetProviderProps.cjs +1 -1
  40. package/dist/cjs/useSheetProviderProps.native.js +1 -1
  41. package/dist/cjs/useSheetProviderProps.native.js.map +1 -1
  42. package/dist/cjs/useSheetScrollViewGestures.cjs +30 -5
  43. package/dist/cjs/useSheetScrollViewGestures.native.js +4 -0
  44. package/dist/cjs/useSheetScrollViewGestures.native.js.map +1 -1
  45. package/dist/cjs/webViewport.cjs +75 -0
  46. package/dist/cjs/webViewport.native.js +81 -0
  47. package/dist/cjs/webViewport.native.js.map +1 -0
  48. package/dist/esm/GestureDetectorWrapper.mjs.map +1 -1
  49. package/dist/esm/GestureDetectorWrapper.native.js.map +1 -1
  50. package/dist/esm/SheetImplementationCustom.mjs +143 -84
  51. package/dist/esm/SheetImplementationCustom.mjs.map +1 -1
  52. package/dist/esm/SheetImplementationCustom.native.js +173 -102
  53. package/dist/esm/SheetImplementationCustom.native.js.map +1 -1
  54. package/dist/esm/SheetScrollView.mjs +157 -36
  55. package/dist/esm/SheetScrollView.mjs.map +1 -1
  56. package/dist/esm/SheetScrollView.native.js +162 -37
  57. package/dist/esm/SheetScrollView.native.js.map +1 -1
  58. package/dist/esm/createSheet.mjs +9 -6
  59. package/dist/esm/createSheet.mjs.map +1 -1
  60. package/dist/esm/createSheet.native.js +19 -15
  61. package/dist/esm/createSheet.native.js.map +1 -1
  62. package/dist/esm/gestureState.mjs +13 -2
  63. package/dist/esm/gestureState.mjs.map +1 -1
  64. package/dist/esm/gestureState.native.js +14 -2
  65. package/dist/esm/gestureState.native.js.map +1 -1
  66. package/dist/esm/index.js +2 -0
  67. package/dist/esm/index.js.map +1 -1
  68. package/dist/esm/index.mjs +2 -0
  69. package/dist/esm/index.mjs.map +1 -1
  70. package/dist/esm/index.native.js +2 -0
  71. package/dist/esm/index.native.js.map +1 -1
  72. package/dist/esm/keyboardAvoidance.mjs +66 -0
  73. package/dist/esm/keyboardAvoidance.mjs.map +1 -0
  74. package/dist/esm/keyboardAvoidance.native.js +69 -0
  75. package/dist/esm/keyboardAvoidance.native.js.map +1 -0
  76. package/dist/esm/nativeSheet.mjs +3 -0
  77. package/dist/esm/nativeSheet.mjs.map +1 -1
  78. package/dist/esm/nativeSheet.native.js +3 -0
  79. package/dist/esm/nativeSheet.native.js.map +1 -1
  80. package/dist/esm/setupGestureHandler.mjs +9 -6
  81. package/dist/esm/setupGestureHandler.mjs.map +1 -1
  82. package/dist/esm/setupGestureHandler.native.js +11 -6
  83. package/dist/esm/setupGestureHandler.native.js.map +1 -1
  84. package/dist/esm/useGestureHandlerPan.mjs +87 -19
  85. package/dist/esm/useGestureHandlerPan.mjs.map +1 -1
  86. package/dist/esm/useGestureHandlerPan.native.js +92 -19
  87. package/dist/esm/useGestureHandlerPan.native.js.map +1 -1
  88. package/dist/esm/useKeyboardControllerSheet.mjs +69 -7
  89. package/dist/esm/useKeyboardControllerSheet.mjs.map +1 -1
  90. package/dist/esm/useSafeAreaInsets.mjs +7 -0
  91. package/dist/esm/useSafeAreaInsets.mjs.map +1 -0
  92. package/dist/esm/useSafeAreaInsets.native.js +8 -0
  93. package/dist/esm/useSafeAreaInsets.native.js.map +1 -0
  94. package/dist/esm/useSheetProviderProps.mjs +1 -1
  95. package/dist/esm/useSheetProviderProps.mjs.map +1 -1
  96. package/dist/esm/useSheetProviderProps.native.js +1 -1
  97. package/dist/esm/useSheetProviderProps.native.js.map +1 -1
  98. package/dist/esm/useSheetScrollViewGestures.mjs +30 -5
  99. package/dist/esm/useSheetScrollViewGestures.mjs.map +1 -1
  100. package/dist/esm/useSheetScrollViewGestures.native.js +4 -0
  101. package/dist/esm/useSheetScrollViewGestures.native.js.map +1 -1
  102. package/dist/esm/webViewport.mjs +43 -0
  103. package/dist/esm/webViewport.mjs.map +1 -0
  104. package/dist/esm/webViewport.native.js +46 -0
  105. package/dist/esm/webViewport.native.js.map +1 -0
  106. package/dist/jsx/GestureDetectorWrapper.mjs.map +1 -1
  107. package/dist/jsx/GestureDetectorWrapper.native.js.map +1 -1
  108. package/dist/jsx/SheetImplementationCustom.mjs +143 -84
  109. package/dist/jsx/SheetImplementationCustom.mjs.map +1 -1
  110. package/dist/jsx/SheetImplementationCustom.native.js +162 -83
  111. package/dist/jsx/SheetImplementationCustom.native.js.map +1 -1
  112. package/dist/jsx/SheetScrollView.mjs +157 -36
  113. package/dist/jsx/SheetScrollView.mjs.map +1 -1
  114. package/dist/jsx/SheetScrollView.native.js +160 -35
  115. package/dist/jsx/SheetScrollView.native.js.map +1 -1
  116. package/dist/jsx/createSheet.mjs +9 -6
  117. package/dist/jsx/createSheet.mjs.map +1 -1
  118. package/dist/jsx/createSheet.native.js +7 -3
  119. package/dist/jsx/createSheet.native.js.map +1 -1
  120. package/dist/jsx/gestureState.mjs +13 -2
  121. package/dist/jsx/gestureState.mjs.map +1 -1
  122. package/dist/jsx/gestureState.native.js +14 -2
  123. package/dist/jsx/gestureState.native.js.map +1 -1
  124. package/dist/jsx/index.js +2 -0
  125. package/dist/jsx/index.js.map +1 -1
  126. package/dist/jsx/index.mjs +2 -0
  127. package/dist/jsx/index.mjs.map +1 -1
  128. package/dist/jsx/index.native.js +11 -0
  129. package/dist/jsx/index.native.js.map +1 -1
  130. package/dist/jsx/keyboardAvoidance.mjs +66 -0
  131. package/dist/jsx/keyboardAvoidance.mjs.map +1 -0
  132. package/dist/jsx/keyboardAvoidance.native.js +99 -0
  133. package/dist/jsx/keyboardAvoidance.native.js.map +1 -0
  134. package/dist/jsx/nativeSheet.mjs +3 -0
  135. package/dist/jsx/nativeSheet.mjs.map +1 -1
  136. package/dist/jsx/nativeSheet.native.js +3 -0
  137. package/dist/jsx/nativeSheet.native.js.map +1 -1
  138. package/dist/jsx/setupGestureHandler.mjs +9 -6
  139. package/dist/jsx/setupGestureHandler.mjs.map +1 -1
  140. package/dist/jsx/setupGestureHandler.native.js +11 -6
  141. package/dist/jsx/setupGestureHandler.native.js.map +1 -1
  142. package/dist/jsx/useGestureHandlerPan.mjs +87 -19
  143. package/dist/jsx/useGestureHandlerPan.mjs.map +1 -1
  144. package/dist/jsx/useGestureHandlerPan.native.js +91 -18
  145. package/dist/jsx/useGestureHandlerPan.native.js.map +1 -1
  146. package/dist/jsx/useKeyboardControllerSheet.mjs +69 -7
  147. package/dist/jsx/useKeyboardControllerSheet.mjs.map +1 -1
  148. package/dist/jsx/useSafeAreaInsets.mjs +7 -0
  149. package/dist/jsx/useSafeAreaInsets.mjs.map +1 -0
  150. package/dist/jsx/useSafeAreaInsets.native.js +48 -0
  151. package/dist/jsx/useSafeAreaInsets.native.js.map +1 -0
  152. package/dist/jsx/useSheetProviderProps.mjs +1 -1
  153. package/dist/jsx/useSheetProviderProps.mjs.map +1 -1
  154. package/dist/jsx/useSheetProviderProps.native.js +1 -1
  155. package/dist/jsx/useSheetProviderProps.native.js.map +1 -1
  156. package/dist/jsx/useSheetScrollViewGestures.mjs +30 -5
  157. package/dist/jsx/useSheetScrollViewGestures.mjs.map +1 -1
  158. package/dist/jsx/useSheetScrollViewGestures.native.js +4 -0
  159. package/dist/jsx/useSheetScrollViewGestures.native.js.map +1 -1
  160. package/dist/jsx/webViewport.mjs +43 -0
  161. package/dist/jsx/webViewport.mjs.map +1 -0
  162. package/dist/jsx/webViewport.native.js +81 -0
  163. package/dist/jsx/webViewport.native.js.map +1 -0
  164. package/next.md +78 -0
  165. package/package.json +30 -48
  166. package/setup-gesture-handler/index.cjs +2 -0
  167. package/setup-gesture-handler/index.js +2 -0
  168. package/setup-gesture-handler/index.native.cjs +2 -0
  169. package/setup-gesture-handler/index.native.js +2 -0
  170. package/src/GestureDetectorWrapper.tsx +0 -3
  171. package/src/SheetImplementationCustom.tsx +387 -127
  172. package/src/SheetScrollView.tsx +195 -22
  173. package/src/createSheet.tsx +20 -7
  174. package/src/gestureState.ts +22 -2
  175. package/src/index.ts +4 -0
  176. package/src/keyboardAvoidance.ts +121 -0
  177. package/src/nativeSheet.tsx +8 -1
  178. package/src/setupGestureHandler.ts +14 -5
  179. package/src/types.tsx +20 -1
  180. package/src/useGestureHandlerPan.tsx +94 -35
  181. package/src/useKeyboardControllerSheet.ts +127 -10
  182. package/src/useSafeAreaInsets.native.ts +29 -0
  183. package/src/useSafeAreaInsets.ts +19 -0
  184. package/src/useSheetProviderProps.tsx +12 -1
  185. package/src/useSheetScrollViewGestures.native.ts +4 -0
  186. package/src/useSheetScrollViewGestures.ts +52 -5
  187. package/src/webViewport.ts +100 -0
  188. package/test/keyboardAvoidance.test.ts +269 -0
  189. package/tsconfig.json +57 -0
  190. package/types/GestureDetectorWrapper.d.ts +1 -1
  191. package/types/GestureDetectorWrapper.d.ts.map +1 -1
  192. package/types/GestureSheetContext.d.ts +1 -1
  193. package/types/GestureSheetContext.d.ts.map +1 -1
  194. package/types/Sheet.d.ts +6 -5
  195. package/types/Sheet.d.ts.map +1 -1
  196. package/types/SheetContext.d.ts +5 -2
  197. package/types/SheetContext.d.ts.map +1 -1
  198. package/types/SheetController.d.ts +1 -2
  199. package/types/SheetController.d.ts.map +1 -1
  200. package/types/SheetImplementationCustom.d.ts +1 -0
  201. package/types/SheetImplementationCustom.d.ts.map +1 -1
  202. package/types/SheetScrollView.d.ts.map +1 -1
  203. package/types/createSheet.d.ts +13 -12
  204. package/types/createSheet.d.ts.map +1 -1
  205. package/types/gestureState.d.ts.map +1 -1
  206. package/types/index.d.ts +1 -0
  207. package/types/index.d.ts.map +1 -1
  208. package/types/keyboardAvoidance.d.ts +26 -0
  209. package/types/keyboardAvoidance.d.ts.map +1 -0
  210. package/types/nativeSheet.d.ts.map +1 -1
  211. package/types/setupGestureHandler.d.ts.map +1 -1
  212. package/types/types.d.ts +10 -1
  213. package/types/types.d.ts.map +1 -1
  214. package/types/useGestureHandlerPan.d.ts +4 -1
  215. package/types/useGestureHandlerPan.d.ts.map +1 -1
  216. package/types/useKeyboardControllerSheet.d.ts +14 -3
  217. package/types/useKeyboardControllerSheet.d.ts.map +1 -1
  218. package/types/useSafeAreaInsets.d.ts +10 -0
  219. package/types/useSafeAreaInsets.d.ts.map +1 -0
  220. package/types/useSafeAreaInsets.native.d.ts +20 -0
  221. package/types/useSafeAreaInsets.native.d.ts.map +1 -0
  222. package/types/useSheetProviderProps.d.ts +3 -0
  223. package/types/useSheetProviderProps.d.ts.map +1 -1
  224. package/types/useSheetScrollViewGestures.d.ts +2 -1
  225. package/types/useSheetScrollViewGestures.d.ts.map +1 -1
  226. package/types/useSheetScrollViewGestures.native.d.ts.map +1 -1
  227. package/types/webViewport.d.ts +36 -0
  228. package/types/webViewport.d.ts.map +1 -0
  229. package/LICENSE +0 -42
@@ -10,7 +10,6 @@ import {
10
10
  useEvent,
11
11
  useThemeName,
12
12
  } from '@hanzogui/core'
13
- import { getSafeArea } from '@hanzogui/native'
14
13
  import { needsPortalRepropagation, Portal } from '@hanzogui/portal'
15
14
  import React, { useState } from 'react'
16
15
  import type {
@@ -22,35 +21,71 @@ import type {
22
21
  import { Dimensions, PanResponder, View } from 'react-native'
23
22
  import { ParentSheetContext, SheetInsideSheetContext } from './contexts'
24
23
  import { GestureDetectorWrapper } from './GestureDetectorWrapper'
24
+ import { getGestureHandlerState } from './gestureState'
25
25
  import { GestureSheetProvider } from './GestureSheetContext'
26
26
  import { resisted } from './helpers'
27
+ import {
28
+ getKeyboardAdjustedSheetY,
29
+ getKeyboardOccludedHeight,
30
+ getSheetReleasePosition,
31
+ } from './keyboardAvoidance'
32
+ import {
33
+ getWebKeyboardResizeHeight,
34
+ getMaxViewportHeight,
35
+ getStableLayoutViewportHeight,
36
+ getWebVisualViewportOffsetTop,
37
+ MIN_KEYBOARD_HEIGHT,
38
+ } from './webViewport'
27
39
  import { SheetProvider } from './SheetContext'
28
40
  import type { SheetProps, SnapPointsMode } from './types'
29
41
  import { useGestureHandlerPan } from './useGestureHandlerPan'
30
42
  import { useKeyboardControllerSheet } from './useKeyboardControllerSheet'
43
+ import { SafeAreaInsetsContext, useSafeAreaInsets } from './useSafeAreaInsets'
31
44
  import { useSheetOpenState } from './useSheetOpenState'
32
45
  import { useSheetProviderProps } from './useSheetProviderProps'
33
46
 
34
47
  const hiddenSize = 10_000.1
35
48
 
36
- // safe area top inset, cached per-session (device-constant value)
37
- let _cachedSafeAreaTop: number | undefined
38
- function getSafeAreaTopInset(): number {
39
- if (_cachedSafeAreaTop !== undefined) return _cachedSafeAreaTop
40
- // use @hanzogui/native abstraction - returns 0 when not enabled
41
- _cachedSafeAreaTop = getSafeArea().getInsets().top
42
- return _cachedSafeAreaTop
43
- }
49
+ // the re-established rngh root for a modal sheet (see modal branch below).
50
+ // GestureHandlerRootView does its own native touch interception and ignores
51
+ // pointerEvents, so it would block the whole app while the sheet sits closed
52
+ // but mounted. instead it stays full-width for correct child layout/measurement
53
+ // and collapses to 0 height when closed so it has no hit area.
54
+ const rnghRootStyleOpen = { width: '100%', height: '100%' } as const
55
+ const rnghRootStyleClosed = { width: '100%', height: 0 } as const
44
56
 
45
57
  let sheetHiddenStyleSheet: HTMLStyleElement | null = null
46
58
 
47
59
  // on web we are always relative to window, on to screen
48
60
  const relativeDimensionTo = isWeb ? 'window' : 'screen'
49
61
 
62
+ // height of the viewport the sheet positions against. on web this MUST be the
63
+ // stable layout viewport and NOT Dimensions.get('window') — react-native-web's
64
+ // Dimensions tracks visualViewport, which shrinks by the soft keyboard. capping
65
+ // frameSize / maxContentSize against that shrinking value corrupts the fit-mode
66
+ // math (translateY = screenSize - frameSize), detaching the sheet's bottom from
67
+ // the screen edge when the keyboard opens. NOTE: window.innerHeight is NOT
68
+ // stable on real iOS Safari (it shrinks with the keyboard too), so we use the
69
+ // self-correcting baseline from webViewport instead. see getStableLayoutViewportHeight.
70
+ function getStableViewportHeight(): number {
71
+ if (isWeb && typeof window !== 'undefined') return getStableLayoutViewportHeight()
72
+ return Dimensions.get(relativeDimensionTo).height
73
+ }
74
+
50
75
  export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
51
76
  function SheetImplementationCustom(props, forwardedRef) {
52
77
  const parentSheet = React.useContext(ParentSheetContext)
53
78
 
79
+ // live safe-area insets (notch / status bar). read here in the component
80
+ // body — which renders INSIDE the app's SafeAreaProvider — so it is correct
81
+ // even though a modal sheet's CONTENT is teleported out through the portal.
82
+ // the keyboard-avoidance clamp below uses the top inset so a keyboard-shifted
83
+ // sheet tops out at the notch instead of sliding under it. web has no native
84
+ // safe-area context (CSS env() handles it) and uses the visual-viewport
85
+ // offset instead.
86
+ const safeAreaInsets = useSafeAreaInsets()
87
+ const safeAreaTopInset = safeAreaInsets?.top ?? 0
88
+
54
89
  const {
55
90
  transition,
56
91
  transitionConfig: transitionConfigProp,
@@ -58,6 +93,7 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
58
93
  zIndex = parentSheet.zIndex + 1,
59
94
  moveOnKeyboardChange = false,
60
95
  unmountChildrenWhenHidden = false,
96
+ disableTransparencyHide = false,
61
97
  portalProps,
62
98
  containerComponent: ContainerComponent = React.Fragment,
63
99
  onAnimationComplete,
@@ -72,6 +108,7 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
72
108
  const {
73
109
  frameSize,
74
110
  setFrameSize,
111
+ dismissOnSnapToBottom,
75
112
  snapPoints,
76
113
  snapPointsMode,
77
114
  hasFit,
@@ -132,6 +169,19 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
132
169
  setIsShowingInnerSheet(hasChild)
133
170
  }, [])
134
171
 
172
+ // keyboard state tracking — just tracks height/visibility, no position animation.
173
+ // Position animation is handled via keyboard-adjusted positions below,
174
+ // matching the react-native-actions-sheet pattern.
175
+ const {
176
+ keyboardHeight,
177
+ isKeyboardVisible,
178
+ dismissKeyboard,
179
+ pauseKeyboardHandler,
180
+ flushPendingHide,
181
+ } = useKeyboardControllerSheet({
182
+ enabled: Boolean(moveOnKeyboardChange),
183
+ })
184
+
135
185
  // FIX: Store stable frameSize to prevent recalculation during exit animation
136
186
  const stableFrameSize = React.useRef(frameSize)
137
187
 
@@ -142,31 +192,63 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
142
192
  }
143
193
  }, [open, frameSize])
144
194
 
145
- // use stableFrameSize when closing to prevent position jumps during exit animation
146
- // but when opening, always use the current frameSize so positions update correctly
195
+ // WEB keyboard frame freeze. on real iOS Safari opening the keyboard shrinks
196
+ // the visual viewport AND innerHeight AND the measured layout, which would
197
+ // re-derive screenSize/frameSize smaller, recompute the fit positions, and
198
+ // fly the frame up then back down ("goes back down after the keyboard opens").
199
+ // so we snapshot the pre-keyboard geometry — captured every render while the
200
+ // keyboard is CLOSED, which dodges the open-transition race where a shrunk
201
+ // onLayout lands before isKeyboardVisible flips — and use it for frame-size
202
+ // math while the keyboard is open. the active snap position still shifts up
203
+ // by the keyboard height, capped at the safe-area top; the scroll view gets
204
+ // keyboardOccludedHeight padding for any tail left behind the keyboard.
205
+ // this sheet is the kind the web keyboard frame freeze is designed for — a
206
+ // fit-mode web sheet opted into keyboard handling. percent/constant sheets
207
+ // keep the live geometry (their height isn't pinned, so a frozen frame
208
+ // height would mismatch).
209
+ const isWebKbSheet = isWeb && hasFit && moveOnKeyboardChange
210
+
211
+ // NATIVE keyboard frame freeze. native fit sheets feed keyboardOccludedHeight
212
+ // into the ScrollView as scrollable tail padding so content can clear the
213
+ // keyboard. but that padding grows the fit content, which grows the frame,
214
+ // which grows the occluded height — a feedback loop that balloons the sheet
215
+ // to the full-screen cap. web breaks the loop by freezing geometry against
216
+ // effScreenSize; native has no such freeze, so we pin the ScrollView to the
217
+ // frame height measured while the keyboard was closed (preKeyboardFrameSize)
218
+ // via keyboardStableFrameHeight below. unlike web, the native keyboard is an
219
+ // overlay that never shrinks the measured screen, so screenSize stays live.
220
+ const isNativeKbFitSheet = !isWeb && hasFit && Boolean(moveOnKeyboardChange)
221
+
222
+ // snapshot the fit frame height while the keyboard is closed, so the pin above
223
+ // holds the pre-keyboard viewport rather than a mid-feedback-loop value.
224
+ const preKeyboardFrameSize = React.useRef(0)
225
+ React.useEffect(() => {
226
+ if (isNativeKbFitSheet && open && !isKeyboardVisible && frameSize > 0) {
227
+ preKeyboardFrameSize.current = frameSize
228
+ }
229
+ }, [isNativeKbFitSheet, open, isKeyboardVisible, frameSize])
230
+
231
+ // the space the snap positions are built against. WEB: the stable layout
232
+ // viewport (document.documentElement.clientHeight), which the soft keyboard
233
+ // never shrinks (unlike the measured screenSize / visualViewport). NATIVE:
234
+ // the measured screenSize. activePositions shift up by keyboardHeight below
235
+ // so a small fit sheet keeps its natural height and moves with the keyboard;
236
+ // a tall fit sheet moves until capped at the safe-area top, leaving its tail
237
+ // behind the keyboard for the ScrollView padding to expose.
238
+ const effScreenSize = isWebKbSheet ? getStableViewportHeight() : screenSize
239
+
240
+ // use stableFrameSize when closing to prevent position jumps during the exit
241
+ // animation; while open use the live frameSize.
147
242
  const effectiveFrameSize = open ? frameSize : stableFrameSize.current || frameSize
148
243
 
149
244
  const positions = React.useMemo(
150
245
  () =>
151
246
  snapPoints.map((point) =>
152
- getYPositions(snapPointsMode, point, screenSize, effectiveFrameSize)
247
+ getYPositions(snapPointsMode, point, effScreenSize, effectiveFrameSize)
153
248
  ),
154
- [screenSize, effectiveFrameSize, snapPoints, snapPointsMode]
249
+ [effScreenSize, effectiveFrameSize, snapPoints, snapPointsMode]
155
250
  )
156
251
 
157
- // keyboard state tracking — just tracks height/visibility, no position animation.
158
- // Position animation is handled via keyboard-adjusted positions below,
159
- // matching the react-native-actions-sheet pattern.
160
- const {
161
- keyboardHeight,
162
- isKeyboardVisible,
163
- dismissKeyboard,
164
- pauseKeyboardHandler,
165
- flushPendingHide,
166
- } = useKeyboardControllerSheet({
167
- enabled: !isWeb && Boolean(moveOnKeyboardChange),
168
- })
169
-
170
252
  const [isDragging, setIsDragging_] = React.useState(false)
171
253
 
172
254
  // synchronous dragging ref — set BEFORE async state commits.
@@ -188,42 +270,71 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
188
270
  [pauseKeyboardHandler, flushPendingHide]
189
271
  )
190
272
 
191
- // keyboard-adjusted positions: shift snap points up by keyboard height
192
- // when keyboard is visible. This drives both gesture snap calculation
193
- // and animation targets — keyboard never dismissed during drag.
194
- // Capped at safe area top inset so the sheet never goes above the notch/status bar
195
- // (matching the react-native-actions-sheet pattern).
273
+ // keyboard-adjusted snap positions.
196
274
  //
197
- // IMPORTANT: frozen during drag to prevent gesture handler recreation.
198
- // When user drags, TextInput may blur keyboard dismisses positions would revert,
199
- // causing the gesture useMemo to recreate and cancel the active drag.
200
- // The post-drag reconciliation effect handles animating to correct position after drag ends.
275
+ // WEB + NATIVE: shift snap points up by keyboard height, capped at the
276
+ // safe-area top inset. web fit sheets keep their frozen pre-keyboard height
277
+ // and add bottom spacer padding when the safe-area cap leaves a hidden tail.
278
+ //
279
+ // IMPORTANT: frozen during drag to prevent gesture handler recreation —
280
+ // when a TextInput blurs mid-drag the keyboard state would otherwise revert
281
+ // and recreate the gesture useMemo, cancelling the active drag.
201
282
  const activePositionsRef = React.useRef(positions)
202
283
  const activePositions = React.useMemo(() => {
203
- // during drag, return frozen positions to prevent gesture handler recreation.
204
- // check both state (for re-render trigger) and ref (for synchronous check
205
- // when keyboard hide event fires before isDragging state commits)
206
284
  if (isDragging || isDraggingRef.current) return activePositionsRef.current
207
285
 
208
286
  let result: number[]
287
+
209
288
  if (!isKeyboardVisible || keyboardHeight <= 0) {
210
289
  result = positions
211
290
  } else {
212
- const safeAreaTop = isWeb ? 0 : getSafeAreaTopInset()
213
- result = positions.map((p) => {
214
- // don't adjust the off-screen/close position (from dismissOnSnapToBottom's 0% snap)
215
- // — it must stay at screenSize so the user can drag between real snap points
216
- // without accidentally closing the sheet
217
- if (screenSize && p >= screenSize) return p
218
- return Math.max(safeAreaTop, p - keyboardHeight)
219
- })
291
+ result = positions.map((p) =>
292
+ getKeyboardAdjustedSheetY({
293
+ sheetY: p,
294
+ screenSize: effScreenSize,
295
+ isKeyboardVisible,
296
+ keyboardHeight,
297
+ shouldTranslate: true,
298
+ safeAreaTop: isWeb ? getWebVisualViewportOffsetTop() : safeAreaTopInset,
299
+ })
300
+ )
220
301
  }
221
302
  activePositionsRef.current = result
222
303
  return result
223
- }, [positions, isKeyboardVisible, keyboardHeight, screenSize, isDragging])
304
+ }, [
305
+ positions,
306
+ isKeyboardVisible,
307
+ keyboardHeight,
308
+ effScreenSize,
309
+ isDragging,
310
+ safeAreaTopInset,
311
+ ])
312
+
313
+ // bottom spacer for the part of the sheet hidden by the keyboard after the
314
+ // keyboard translation and safe-area clamping. if a small sheet fits above
315
+ // the keyboard this is 0; if a tall sheet is capped at the safe-area top, the
316
+ // spacer makes the remaining hidden tail scrollable.
317
+ const keyboardOccludedHeight = getKeyboardOccludedHeight({
318
+ frameSize: effectiveFrameSize,
319
+ isKeyboardVisible,
320
+ keyboardHeight,
321
+ screenSize: effScreenSize,
322
+ sheetY: position >= 0 ? activePositions[position] : undefined,
323
+ })
224
324
 
225
- const { useAnimatedNumber, useAnimatedNumberStyle, useAnimatedNumberReaction } =
226
- animationDriver
325
+ // pin the scroll view to the held (pre-keyboard) frame height while the
326
+ // keyboard is up on web. on older iOS the consumer's window-derived maxHeight
327
+ // shrinks with the keyboard, which would clip the scroll view (and the frame)
328
+ // smaller; this override keeps it at the full height so the frame translates
329
+ // with the keyboard but never resizes. 0 = no override (use the consumer maxHeight).
330
+ const keyboardStableFrameHeight =
331
+ isWebKbSheet && isKeyboardVisible && frameSize > 0
332
+ ? frameSize
333
+ : isNativeKbFitSheet && isKeyboardVisible && preKeyboardFrameSize.current > 0
334
+ ? preKeyboardFrameSize.current
335
+ : 0
336
+
337
+ const { useAnimatedNumber, useAnimatedNumberStyle } = animationDriver
227
338
  const AnimatedView = (animationDriver.View ?? GuiView) as typeof Animated.View
228
339
 
229
340
  useIsomorphicLayoutEffect(() => {
@@ -259,42 +370,52 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
259
370
  // safety fallback timer for sheet close opacity
260
371
  const opacityFallbackTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
261
372
 
262
- useAnimatedNumberReaction(
263
- {
264
- value: animatedNumber,
265
- hostRef: sheetRef,
266
- },
267
- React.useCallback(
268
- (value) => {
269
- at.current = value
270
- scrollBridge.paneY = value
271
- // update isAtTop for scroll enable/disable
272
- // activePositions[0] is the top snap point (keyboard-adjusted minY)
273
- const minY = activePositions[0]
274
- const wasAtTop = scrollBridge.isAtTop
275
- const nowAtTop = value <= minY + 5
276
- if (wasAtTop !== nowAtTop) {
277
- scrollBridge.isAtTop = nowAtTop
278
- // when reaching top, enable scroll; when leaving top, disable scroll
279
- // this preemptively sets scroll state before any gestures start
280
- if (nowAtTop) {
281
- // if scroll drifted during drag (e.g. fast swipe from position 1),
282
- // reset it to 0 before enabling free scroll
283
- if (scrollBridge.y > 0) {
284
- scrollBridge.forceScrollTo?.(0)
285
- scrollBridge.y = 0
286
- }
287
- scrollBridge.scrollLockY = undefined
288
- scrollBridge.setScrollEnabled?.(true)
289
- } else {
290
- scrollBridge.scrollLockY = 0
291
- scrollBridge.setScrollEnabled?.(false)
292
- }
373
+ const syncAnimatedPosition = useEvent((value: number) => {
374
+ at.current = value
375
+ scrollBridge.paneY = value
376
+
377
+ // update isAtTop for scroll enable/disable. activePositions[0] is the
378
+ // top snap point (keyboard-adjusted minY).
379
+ const minY = activePositions[0]
380
+ const wasAtTop = scrollBridge.isAtTop
381
+ const nowAtTop = value <= minY + 5
382
+ if (wasAtTop === nowAtTop) return
383
+
384
+ scrollBridge.isAtTop = nowAtTop
385
+ if (nowAtTop) {
386
+ if (scrollBridge.lockScrollAtTop) {
387
+ if (scrollBridge.y > 0) {
388
+ scrollBridge.forceScrollTo?.(0)
389
+ scrollBridge.y = 0
293
390
  }
294
- },
295
- [animationDriver, activePositions]
296
- )
297
- )
391
+ scrollBridge.scrollLockY = 0
392
+ scrollBridge.setScrollEnabled?.(false, 0)
393
+ return
394
+ }
395
+ // if scroll drifted during drag (e.g. fast swipe from position 1),
396
+ // reset it to 0 before enabling free scroll.
397
+ if (scrollBridge.y > 0) {
398
+ scrollBridge.forceScrollTo?.(0)
399
+ scrollBridge.y = 0
400
+ }
401
+ scrollBridge.scrollLockY = undefined
402
+ scrollBridge.setScrollEnabled?.(true)
403
+ } else {
404
+ scrollBridge.scrollLockY = 0
405
+ scrollBridge.setScrollEnabled?.(false)
406
+ }
407
+ })
408
+
409
+ const setAnimatedPositionDirect = useEvent((value: number) => {
410
+ syncAnimatedPosition(value)
411
+ animatedNumber.setValue(value, { type: 'direct' })
412
+ })
413
+
414
+ const getAnimatedPosition = useEvent(() => {
415
+ const value = animatedNumber.getValue()
416
+ syncAnimatedPosition(value)
417
+ return value
418
+ })
298
419
 
299
420
  function stopSpring() {
300
421
  animatedNumber.stop()
@@ -307,7 +428,19 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
307
428
  const animateTo = useEvent((position: number, animationOverride?: any) => {
308
429
  if (frameSize === 0) return
309
430
 
310
- let toValue = isHidden || position === -1 ? screenSize : activePositions[position]
431
+ // use effScreenSize (the frozen anchor space the positions were built in) for
432
+ // the off-screen/close target too, so a close while the keyboard is still up
433
+ // animates fully out instead of to a mismatched live screenSize.
434
+ //
435
+ // web: clear the maximum the viewport can ever reveal, not just the current
436
+ // layout viewport. iOS Safari retracts its chrome on scroll and exposes area
437
+ // below the current viewport, so a sheet parked at effScreenSize would peek
438
+ // back in as the page scrolls. getMaxViewportHeight floors the target past
439
+ // anything Safari can expose.
440
+ const closeTarget = isWeb
441
+ ? Math.max(effScreenSize, getMaxViewportHeight())
442
+ : effScreenSize
443
+ let toValue = isHidden || position === -1 ? closeTarget : activePositions[position]
311
444
 
312
445
  if (at.current === toValue) return
313
446
 
@@ -323,6 +456,7 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
323
456
  }
324
457
 
325
458
  const animationCompleteCallback = () => {
459
+ syncAnimatedPosition(toValue)
326
460
  if (opacityFallbackTimer.current) {
327
461
  clearTimeout(opacityFallbackTimer.current)
328
462
  opacityFallbackTimer.current = null
@@ -386,6 +520,7 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
386
520
  duration: 0,
387
521
  },
388
522
  () => {
523
+ syncAnimatedPosition(screenSize)
389
524
  // imperfect but struggling to render properly here
390
525
  setTimeout(() => {
391
526
  setDisableAnimation(false)
@@ -399,6 +534,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
399
534
  return
400
535
  }
401
536
 
537
+ // never fight an active drag: the gesture owns the animated position. on
538
+ // web the AnimatedView's onLayout re-fires with sub-pixel jitter as the
539
+ // frame translates (frameSize 499.99996 <-> 500.00003), and since frameSize
540
+ // is a dep of this effect that would re-run it mid-pull and snap the sheet
541
+ // back to its resting snap point. read the live ref so drag-end (which the
542
+ // reconcile-after-drag effect handles) isn't gated by stale deps.
543
+ if (isDraggingRef.current) {
544
+ return
545
+ }
546
+
402
547
  if (!frameSize || !screenSize || isHidden || (hasntMeasured && !open)) {
403
548
  return
404
549
  }
@@ -425,6 +570,10 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
425
570
  scrollBridge.setScrollEnabled?.(false)
426
571
  }
427
572
  }
573
+ // NOTE: effScreenSize/effectiveFrameSize are intentionally NOT deps. With the
574
+ // spacer approach the frame's position target is frozen across keyboard
575
+ // open/close (same stable baseline), so it must NOT re-animate — keyboard
576
+ // avoidance is the bottom spacer + scroll, not a frame move.
428
577
  }, [hasntMeasured, disableAnimation, isHidden, frameSize, screenSize, open, position])
429
578
 
430
579
  const disableDrag = props.disableDrag ?? controller?.disableDrag
@@ -436,7 +585,11 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
436
585
  if (!frameSize) return
437
586
  if (isShowingInnerSheet) return
438
587
 
439
- const minY = positions[0]
588
+ // use keyboard-adjusted positions (matches the RNGH path): when the
589
+ // keyboard is open the sheet sits at activePositions[0], so clamping drags
590
+ // against the un-adjusted positions[0] would rubber-band the sheet down to
591
+ // near the bottom on any drag.
592
+ const minY = activePositions[0]
440
593
  scrollBridge.paneMinY = minY
441
594
  let startY = at.current
442
595
 
@@ -476,17 +629,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
476
629
  // vy goes up to about 4 at most (+ is down, - is up)
477
630
  const end = currentPos + frameSize * vy * 0.2
478
631
 
479
- let closestPoint = 0
480
- let dist = Number.POSITIVE_INFINITY
481
-
482
- for (let i = 0; i < positions.length; i++) {
483
- const position = positions[i]
484
- const curDist = end > position ? end - position : position - end
485
- if (curDist < dist) {
486
- dist = curDist
487
- closestPoint = i
488
- }
489
- }
632
+ const closestPoint = getSheetReleasePosition({
633
+ positions: activePositions,
634
+ projectedEnd: end,
635
+ currentPosition: currentPos,
636
+ frameSize,
637
+ dismissOnSnapToBottom,
638
+ snapPointsMode,
639
+ isKeyboardVisible,
640
+ isWeb,
641
+ })
490
642
 
491
643
  // have to call both because state may not change but need to snap back
492
644
  setPosition(closestPoint)
@@ -512,6 +664,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
512
664
  return true
513
665
  }
514
666
 
667
+ // touch is on the ScrollView node — the web scroll-view gesture hook
668
+ // owns it and drives drag/release through scrollBridge directly (it
669
+ // re-baselines via scrollBridge.startPanDrag on each pan handoff). if
670
+ // we also granted here, RNW's PanResponder would set the animated
671
+ // position from a second, differently-based offset every move and the
672
+ // sheet would jitter/jump. defer entirely to the hook.
673
+ if (scrollBridge.scrollNodeTouched) {
674
+ return false
675
+ }
676
+
515
677
  if (scrollBridge.hasScrollableContent === true) {
516
678
  if (scrollBridge.scrollLock) {
517
679
  return false
@@ -552,18 +714,29 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
552
714
  const grant = () => {
553
715
  setPanning(true)
554
716
  stopSpring()
555
- startY = at.current
717
+ startY = getAnimatedPosition()
556
718
  }
557
719
 
558
720
  let isExternalDrag = false
559
721
 
722
+ // re-baseline a pan drag to the current animated position. the web
723
+ // scroll-view hook calls this on every transition INTO pan ownership
724
+ // (including handoffs back from scroll), so its panDragOffset — which it
725
+ // resets to 0 at each pan entry — is measured from where the sheet
726
+ // actually is now, not from where the gesture first grabbed it. without
727
+ // this the sheet would jump to a stale origin on a scroll→pan handoff.
728
+ scrollBridge.startPanDrag = () => {
729
+ isExternalDrag = true
730
+ grant()
731
+ }
732
+
560
733
  scrollBridge.drag = (dy) => {
561
734
  if (!isExternalDrag) {
562
735
  isExternalDrag = true
563
736
  grant()
564
737
  }
565
738
  const to = dy + startY
566
- animatedNumber.setValue(resisted(to, minY), { type: 'direct' })
739
+ setAnimatedPositionDirect(resisted(to, minY))
567
740
  }
568
741
 
569
742
  scrollBridge.release = release
@@ -579,6 +752,10 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
579
752
 
580
753
  return PanResponder.create({
581
754
  onMoveShouldSetPanResponder: onMoveShouldSet,
755
+ // once we own the drag, don't yield it to another responder
756
+ // (re-renders during the drag were cooperatively terminating it under
757
+ // load, killing the gesture mid-drag)
758
+ onPanResponderTerminationRequest: () => false,
582
759
  onPanResponderGrant: grant,
583
760
  onPanResponderMove: (_e, { dy }) => {
584
761
  const toFull = dy + startY
@@ -592,19 +769,31 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
592
769
  scrollBridge.setParentDragging(true)
593
770
  }
594
771
 
595
- animatedNumber.setValue(to, { type: 'direct' })
772
+ setAnimatedPositionDirect(to)
596
773
  },
597
774
  onPanResponderEnd: finish,
598
775
  onPanResponderTerminate: finish,
599
776
  onPanResponderRelease: finish,
600
777
  })
601
- }, [disableDrag, isShowingInnerSheet, animateTo, frameSize, positions, setPosition])
778
+ }, [
779
+ disableDrag,
780
+ isShowingInnerSheet,
781
+ animateTo,
782
+ frameSize,
783
+ activePositions,
784
+ setPosition,
785
+ dismissOnSnapToBottom,
786
+ snapPointsMode,
787
+ ])
602
788
 
603
- // animate to keyboard-adjusted position when keyboard state changes
789
+ // animate to the current keyboard-adjusted position when the keyboard state
790
+ // changes. activePositions shift the frame up by keyboardHeight, capped at
791
+ // the safe-area top; tall web fit sheets keep a frozen height and gain scroll
792
+ // padding for whatever tail remains behind the keyboard.
604
793
  React.useEffect(() => {
605
794
  if (isDragging || isHidden || !open || disableAnimation) return
606
795
  if (!frameSize || !screenSize) return
607
- // use timing animation to match iOS keyboard animation (~250ms)
796
+ // timing animation matches the iOS keyboard animation (~250ms)
608
797
  animateTo(position, { type: 'timing', duration: 250 })
609
798
  }, [isKeyboardVisible, keyboardHeight])
610
799
 
@@ -629,6 +818,11 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
629
818
  React.useEffect(() => {
630
819
  if (!open && isKeyboardVisible) {
631
820
  dismissKeyboard()
821
+ // if the sheet was closed mid-drag the keyboard-hide handler was paused
822
+ // and a hide could be left pending — clear it so isKeyboardVisible can't
823
+ // stick true after the sheet is gone. (no-op for a normal close.)
824
+ pauseKeyboardHandler.current = false
825
+ flushPendingHide()
632
826
  }
633
827
  }, [open])
634
828
 
@@ -641,43 +835,72 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
641
835
  stopSpring,
642
836
  scrollBridge,
643
837
  setIsDragging,
644
- getCurrentPosition: () => at.current,
838
+ getCurrentPosition: getAnimatedPosition,
645
839
  resisted,
646
840
  disableDrag,
647
841
  isShowingInnerSheet,
648
- setAnimatedPosition: (val: number) => {
649
- // directly set the animated value for smooth dragging
650
- at.current = val
651
- animatedNumber.setValue(val, { type: 'direct' })
652
- },
842
+ // directly set the animated value for smooth dragging
843
+ setAnimatedPosition: setAnimatedPositionDirect,
844
+ dismissOnSnapToBottom,
845
+ snapPointsMode,
846
+ isKeyboardVisible,
653
847
  pauseKeyboardHandler,
654
848
  })
655
849
 
850
+ // ignore any layout measured while the soft keyboard is up (web +
851
+ // moveOnKeyboardChange): the visual viewport (which RN's web layout follows)
852
+ // shrinks, so the height would be the collapsed sheet — keeping it would
853
+ // recompute the fit anchor and fly the frame. a LIVE DOM check, NOT the
854
+ // isKeyboardVisible React state, is required: the state lags the resize, so
855
+ // the first shrunk onLayout lands before the flag flips. holding the measured
856
+ // size keeps the fit geometry stable while the frame translates with the kb.
857
+ const ignoreLayoutForKeyboard = useEvent(
858
+ () =>
859
+ isWeb &&
860
+ moveOnKeyboardChange &&
861
+ getWebKeyboardResizeHeight() >= MIN_KEYBOARD_HEIGHT
862
+ )
863
+
656
864
  const handleAnimationViewLayout = useEvent((e: LayoutChangeEvent) => {
657
865
  // don't update frameSize during exit animation to prevent position jumps
658
866
  if (!open && stableFrameSize.current !== 0) {
659
867
  return
660
868
  }
661
869
 
870
+ const layoutHeight = e.nativeEvent?.layout.height
871
+ // drop a layout measured while the keyboard is up: on older iOS the web
872
+ // viewport shrinks and the frame would resize. keep the pre-keyboard frame
873
+ // height so the web frame translates without resizing.
874
+ // exception: if we have no frame height yet (sheet opened with the keyboard
875
+ // already up), accept it so the sheet can appear at all.
876
+ if (ignoreLayoutForKeyboard() && frameSize > 0) return
877
+
662
878
  // avoid bugs where it grows forever for whatever reason
663
879
  // For inline mode (non-modal), don't cap at window height - use actual layout
664
- const layoutHeight = e.nativeEvent?.layout.height
665
880
  const next = modal
666
- ? Math.min(layoutHeight, Dimensions.get(relativeDimensionTo).height)
881
+ ? Math.min(layoutHeight, getStableViewportHeight())
667
882
  : layoutHeight
668
883
  if (!next) return
669
- setFrameSize(next)
884
+ // round: web onLayout reports sub-pixel heights (e.g. 499.99996) that jitter
885
+ // frame to frame as the view transforms; the raw float would re-fire every
886
+ // effect that depends on frameSize on each drag move.
887
+ setFrameSize(Math.round(next))
670
888
  })
671
889
 
672
- const handleMaxContentViewLayout = React.useCallback((e: LayoutChangeEvent) => {
673
- // avoid bugs where it grows forever for whatever reason
674
- const next = Math.min(
675
- e.nativeEvent?.layout.height,
676
- Dimensions.get(relativeDimensionTo).height
677
- )
678
- if (!next) return
679
- setMaxContentSize(next)
680
- }, [])
890
+ const handleMaxContentViewLayout = React.useCallback(
891
+ (e: LayoutChangeEvent) => {
892
+ // keep maxContentSize at the full pre-keyboard viewport: drop layouts
893
+ // measured while the keyboard is up (the shrunk viewport), unless we have
894
+ // none yet (keyboard-already-up open).
895
+ if (ignoreLayoutForKeyboard() && screenSize > 0) return
896
+ // avoid bugs where it grows forever for whatever reason
897
+ const next = Math.min(e.nativeEvent?.layout.height, getStableViewportHeight())
898
+ if (!next) return
899
+ // round to avoid sub-pixel churn re-firing size-dependent effects
900
+ setMaxContentSize(Math.round(next))
901
+ },
902
+ [ignoreLayoutForKeyboard, screenSize]
903
+ )
681
904
 
682
905
  const getAnimatedNumberStyle = React.useCallback(
683
906
  (val: number) => {
@@ -724,7 +947,13 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
724
947
  let contents = (
725
948
  <LayoutMeasurementController disable={!open}>
726
949
  <ParentSheetContext.Provider value={nextParentContext}>
727
- <SheetProvider {...providerProps} setHasScrollView={setHasScrollView}>
950
+ <SheetProvider
951
+ {...providerProps}
952
+ keyboardOccludedHeight={keyboardOccludedHeight}
953
+ isKeyboardVisible={isKeyboardVisible}
954
+ keyboardStableFrameHeight={keyboardStableFrameHeight}
955
+ setHasScrollView={setHasScrollView}
956
+ >
728
957
  <GestureSheetProvider
729
958
  isDragging={isDragging}
730
959
  blockPan={blockPan}
@@ -766,7 +995,11 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
766
995
  width: '100%',
767
996
  height: forcedContentHeight,
768
997
  minHeight: forcedContentHeight,
769
- opacity: !shouldHideParentSheet ? opacity : 0,
998
+ opacity: shouldHideParentSheet
999
+ ? 0
1000
+ : disableTransparencyHide
1001
+ ? 1
1002
+ : opacity,
770
1003
  ...((shouldHideParentSheet || !open) && {
771
1004
  pointerEvents: 'none',
772
1005
  }),
@@ -774,7 +1007,7 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
774
1007
  animatedStyle,
775
1008
  ]}
776
1009
  >
777
- {/* wrap children with plain RN View for panResponder - gui views no longer handle responder events on web */}
1010
+ {/* wrap children with plain RN View for panResponder - hanzogui views no longer handle responder events on web */}
778
1011
  {gestureHandlerEnabled && panGesture ? (
779
1012
  <GestureDetectorWrapper gesture={panGesture} style={{ flex: 1 }}>
780
1013
  {props.children}
@@ -799,8 +1032,13 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
799
1032
  const adaptContext = useAdaptContext()
800
1033
  contents = (
801
1034
  <ProvideAdaptContext {...adaptContext}>
802
- {/* @ts-ignore */}
803
- {contents}
1035
+ {/* re-propagate safe-area insets across the teleport: the sheet content
1036
+ renders at the portal host, OUTSIDE the app's SafeAreaProvider, so
1037
+ without this useSafeAreaInsets() inside the sheet reads 0. */}
1038
+ <SafeAreaInsetsContext.Provider value={safeAreaInsets}>
1039
+ {/* @ts-ignore */}
1040
+ {contents}
1041
+ </SafeAreaInsetsContext.Provider>
804
1042
  </ProvideAdaptContext>
805
1043
  )
806
1044
  }
@@ -809,9 +1047,31 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
809
1047
  const shouldMountChildren = unmountChildrenWhenHidden ? !!opacity : true
810
1048
 
811
1049
  if (modal) {
1050
+ // a modal sheet is teleported through <Portal> to the root portal host.
1051
+ // that host is mounted by GuiProvider, which may sit ABOVE the app's
1052
+ // GestureHandlerRootView - so the teleported content lands outside any
1053
+ // rngh root and every gesture inside the sheet (the drag pan, pressables
1054
+ // on the rngh press path) silently goes dead. re-establish an rngh root
1055
+ // around the teleported content so it works regardless of where the app
1056
+ // mounts GestureHandlerRootView.
1057
+ //
1058
+ // the root stays mounted and full-width whenever the sheet content is
1059
+ // (so child layout/measurement/close-animation are unaffected) and only
1060
+ // collapses to 0 height while closed so it occupies no hit area - see
1061
+ // rnghRootStyleOpen/Closed above for why pointerEvents can't be used.
1062
+ const RNGHRoot = getGestureHandlerState().RootView
1063
+ const mountedContents = shouldMountChildren ? (
1064
+ <ContainerComponent>{contents}</ContainerComponent>
1065
+ ) : null
812
1066
  const modalContents = (
813
1067
  <Portal stackZIndex={zIndex} {...portalProps}>
814
- {shouldMountChildren && <ContainerComponent>{contents}</ContainerComponent>}
1068
+ {mountedContents && RNGHRoot ? (
1069
+ <RNGHRoot style={open ? rnghRootStyleOpen : rnghRootStyleClosed}>
1070
+ {mountedContents}
1071
+ </RNGHRoot>
1072
+ ) : (
1073
+ mountedContents
1074
+ )}
815
1075
  </Portal>
816
1076
  )
817
1077