@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,6 +10,13 @@ import { getGestureHandlerState, isGestureHandlerEnabled } from './gestureState'
10
10
  import { useSheetContext } from './SheetContext'
11
11
  import type { SheetScopedProps } from './types'
12
12
  import { useSheetScrollViewGestures } from './useSheetScrollViewGestures'
13
+ import {
14
+ getStableLayoutViewportHeight,
15
+ getWebKeyboardBottomInset,
16
+ getWebKeyboardResizeHeight,
17
+ isEditableElement,
18
+ MIN_KEYBOARD_HEIGHT,
19
+ } from './webViewport'
13
20
 
14
21
  const SHEET_SCROLL_VIEW_NAME = 'SheetScrollView'
15
22
 
@@ -29,28 +36,83 @@ export const SheetScrollView = React.forwardRef<
29
36
  ) => {
30
37
  const context = useSheetContext(SHEET_SCROLL_VIEW_NAME, __scopeSheet)
31
38
  const gestureContext = useGestureSheetContext()
32
- const { scrollBridge, setHasScrollView } = context
39
+ const { scrollBridge, setHasScrollView, hasFit, screenSize } = context
40
+ const keyboardOccludedHeight = Math.max(0, context.keyboardOccludedHeight || 0)
41
+ // OR a LIVE DOM check: context.isKeyboardVisible (React state) lags the
42
+ // viewport resize, so on the open-transition render this component can re-run
43
+ // with the shrunk consumer maxHeight BEFORE the context flag flips. reading
44
+ // the keyboard height straight from visualViewport closes that race so the
45
+ // height freeze engages on the same render that would otherwise collapse it.
46
+ const isKeyboardVisible =
47
+ context.isKeyboardVisible === true ||
48
+ (isWeb && getWebKeyboardResizeHeight() >= MIN_KEYBOARD_HEIGHT)
33
49
  const [scrollEnabled] = useControllableState({
34
50
  prop: scrollEnabledProp,
35
51
  defaultProp: true,
36
52
  })
37
53
  const scrollRef = React.useRef<RNScrollView | null>(null)
38
54
 
55
+ const [hasScrollableContent, setHasScrollableContent] = useState(true)
56
+ const parentHeight = useRef(0)
57
+ const contentHeight = useRef(0)
58
+ // the sheet's authoritative pre-keyboard frame height (see SheetImpl). a
59
+ // scroll-view-local high-water mark used to live here, but it was unreliable
60
+ // (the ref could read 0 if the view remounted on focus / never laid out while
61
+ // closed), so the height now comes from the sheet, which doesn't remount.
62
+ const frozenFrameHeight = Math.max(0, context.keyboardStableFrameHeight || 0)
63
+
64
+ // with snapPointsMode="fit", Frame is content-sized (flex: 0, flex-basis: auto, height: undefined).
65
+ // a flex: 1 child can't grow inside a content-sized parent, so the ScrollView (and the Frame
66
+ // around it) collapse to 0 height. instead, let the ScrollView size to its content and cap it
67
+ // at the available viewport (screenSize / maxContentSize) so scrolling kicks in for tall content.
68
+ const fitSizingStyle = hasFit
69
+ ? {
70
+ flex: undefined as undefined,
71
+ height: undefined as undefined,
72
+ maxHeight: screenSize || undefined,
73
+ }
74
+ : { flex: 1 }
75
+ const contentContainerStyle = hasFit ? undefined : { minHeight: '100%' as const }
76
+
77
+ // when the keyboard is open, pin the scroll view to the sheet's pre-keyboard
78
+ // frame height (frozenFrameHeight), overriding any consumer maxHeight. on web
79
+ // that maxHeight is often tied to useWindowDimensions, which SHRINKS when the
80
+ // keyboard opens and would otherwise collapse the sheet. holding the height
81
+ // constant means the web frame can translate without resizing. applied
82
+ // AFTER {...props} so it wins.
83
+ const keyboardFrozenOverride =
84
+ hasFit && isKeyboardVisible && frozenFrameHeight > 0
85
+ ? { height: frozenFrameHeight, maxHeight: frozenFrameHeight }
86
+ : null
87
+
39
88
  const panGestureRef = gestureContext?.panGestureRef
40
89
  const { ScrollView: RNGHScrollView } = getGestureHandlerState()
41
90
  const useRNGHScrollView = isGestureHandlerEnabled() && RNGHScrollView && panGestureRef
42
91
 
43
92
  // RNGH scroll locking state
44
93
  const currentScrollOffset = useRef(0)
94
+ const lastEmittedScrollOffset = useRef(0)
45
95
  const lockedScrollY = useRef(0)
96
+ const focusedInputScrollFrame = useRef(0)
46
97
 
47
98
  const setScrollEnabled = (next: boolean, lockTo?: number) => {
48
99
  if (!next) {
49
- const lockY = lockTo ?? currentScrollOffset.current
100
+ const lockY = lockTo ?? scrollBridge.scrollLockY ?? currentScrollOffset.current
101
+ if (scrollBridge.enabled === false && scrollBridge.scrollLockY === lockY) {
102
+ return
103
+ }
104
+ scrollBridge.enabled = false
50
105
  lockedScrollY.current = lockY
51
106
  scrollBridge.scrollLockY = lockY
52
- scrollRef.current?.scrollTo?.({ x: 0, y: lockY, animated: false })
107
+ if (currentScrollOffset.current !== lockY) {
108
+ scrollRef.current?.scrollTo?.({ x: 0, y: lockY, animated: false })
109
+ currentScrollOffset.current = lockY
110
+ }
53
111
  } else {
112
+ if (scrollBridge.enabled === true && scrollBridge.scrollLockY === undefined) {
113
+ return
114
+ }
115
+ scrollBridge.enabled = true
54
116
  lockedScrollY.current = currentScrollOffset.current
55
117
  scrollBridge.scrollLockY = undefined
56
118
  }
@@ -58,8 +120,97 @@ export const SheetScrollView = React.forwardRef<
58
120
 
59
121
  const forceScrollTo = (y: number) => {
60
122
  scrollRef.current?.scrollTo?.({ x: 0, y, animated: false })
123
+ currentScrollOffset.current = y
61
124
  }
62
125
 
126
+ const emitManualScroll = React.useCallback(
127
+ (node: HTMLElement, y: number) => {
128
+ currentScrollOffset.current = y
129
+ scrollBridge.y = y
130
+ if (y > 0) scrollBridge.scrollStartY = -1
131
+ lastEmittedScrollOffset.current = y
132
+ onScroll?.({
133
+ nativeEvent: {
134
+ contentOffset: {
135
+ x: node.scrollLeft,
136
+ y,
137
+ },
138
+ contentSize: {
139
+ height: node.scrollHeight,
140
+ width: node.scrollWidth,
141
+ },
142
+ layoutMeasurement: {
143
+ height: node.offsetHeight,
144
+ width: node.offsetWidth,
145
+ },
146
+ },
147
+ timeStamp: Date.now(),
148
+ } as any)
149
+ },
150
+ [onScroll, scrollBridge]
151
+ )
152
+
153
+ const scrollFocusedInputClearOfKeyboard = React.useCallback(() => {
154
+ if (!isWeb || !hasFit || !isKeyboardVisible || keyboardOccludedHeight <= 0) {
155
+ return
156
+ }
157
+ const node = scrollRef.current?.getScrollableNode() as HTMLElement | undefined
158
+ const active = document.activeElement as HTMLElement | null
159
+ if (!node || !active || !isEditableElement(active) || !node.contains(active)) {
160
+ return
161
+ }
162
+
163
+ const keyboardHeight = Math.max(keyboardOccludedHeight, getWebKeyboardBottomInset())
164
+ if (keyboardHeight <= 0) return
165
+
166
+ const activeRect = active.getBoundingClientRect()
167
+ const nodeRect = node.getBoundingClientRect()
168
+ const margin = 12
169
+ const keyboardTop = getStableLayoutViewportHeight() - keyboardHeight
170
+ const visibleTop = nodeRect.top + margin
171
+ const visibleBottom = Math.min(nodeRect.bottom, keyboardTop) - margin
172
+ let nextScrollTop = node.scrollTop
173
+
174
+ if (activeRect.bottom > visibleBottom) {
175
+ nextScrollTop += Math.ceil(activeRect.bottom - visibleBottom)
176
+ } else if (activeRect.top < visibleTop) {
177
+ nextScrollTop -= Math.ceil(visibleTop - activeRect.top)
178
+ }
179
+
180
+ const maxScrollTop = Math.max(0, node.scrollHeight - node.clientHeight)
181
+ nextScrollTop = Math.max(0, Math.min(maxScrollTop, nextScrollTop))
182
+ if (nextScrollTop === node.scrollTop) return
183
+
184
+ node.scrollTop = nextScrollTop
185
+ currentScrollOffset.current = nextScrollTop
186
+ scrollBridge.y = nextScrollTop
187
+ if (nextScrollTop > 0) scrollBridge.scrollStartY = -1
188
+ }, [hasFit, isKeyboardVisible, keyboardOccludedHeight, scrollBridge])
189
+
190
+ const scheduleFocusedInputScroll = React.useCallback(() => {
191
+ if (!isWeb || !hasFit) return
192
+ cancelAnimationFrame(focusedInputScrollFrame.current)
193
+ focusedInputScrollFrame.current = requestAnimationFrame(() => {
194
+ scrollFocusedInputClearOfKeyboard()
195
+ focusedInputScrollFrame.current = requestAnimationFrame(
196
+ scrollFocusedInputClearOfKeyboard
197
+ )
198
+ })
199
+ }, [hasFit, scrollFocusedInputClearOfKeyboard])
200
+
201
+ useEffect(() => {
202
+ if (!isWeb || !hasFit) return
203
+ scheduleFocusedInputScroll()
204
+ window.addEventListener('focusin', scheduleFocusedInputScroll)
205
+ window.visualViewport?.addEventListener('resize', scheduleFocusedInputScroll)
206
+
207
+ return () => {
208
+ cancelAnimationFrame(focusedInputScrollFrame.current)
209
+ window.removeEventListener('focusin', scheduleFocusedInputScroll)
210
+ window.visualViewport?.removeEventListener('resize', scheduleFocusedInputScroll)
211
+ }
212
+ }, [hasFit, scheduleFocusedInputScroll])
213
+
63
214
  useEffect(() => {
64
215
  setHasScrollView(true)
65
216
  if (isGestureHandlerEnabled()) {
@@ -73,16 +224,18 @@ export const SheetScrollView = React.forwardRef<
73
224
  }
74
225
  }, [])
75
226
 
76
- const [hasScrollableContent, setHasScrollableContent] = useState(true)
77
- const parentHeight = useRef(0)
78
- const contentHeight = useRef(0)
79
-
80
227
  const updateScrollable = () => {
81
228
  if (parentHeight.current && contentHeight.current) {
82
229
  setHasScrollableContent(contentHeight.current > parentHeight.current)
83
230
  }
84
231
  }
85
232
 
233
+ // track the fit height for the scrollable-content check. the keyboard-freeze
234
+ // height is supplied by the sheet (frozenFrameHeight), not derived here.
235
+ const recordFitHeight = (height: number) => {
236
+ parentHeight.current = height
237
+ }
238
+
86
239
  useEffect(() => {
87
240
  scrollBridge.hasScrollableContent = hasScrollableContent
88
241
  }, [hasScrollableContent])
@@ -94,6 +247,7 @@ export const SheetScrollView = React.forwardRef<
94
247
  hasScrollableContent,
95
248
  scrollEnabled,
96
249
  setScrollEnabled,
250
+ onManualScroll: emitManualScroll,
97
251
  })
98
252
 
99
253
  // content wrapper for measuring height
@@ -104,10 +258,20 @@ export const SheetScrollView = React.forwardRef<
104
258
  if (height !== contentHeight.current) {
105
259
  contentHeight.current = height
106
260
  updateScrollable()
261
+ if (keyboardOccludedHeight > 0) {
262
+ scheduleFocusedInputScroll()
263
+ }
107
264
  }
108
265
  }}
109
266
  >
110
267
  {children}
268
+ {keyboardOccludedHeight > 0 && (
269
+ <View
270
+ data-sheet-keyboard-scroll-pad
271
+ height={keyboardOccludedHeight}
272
+ width="100%"
273
+ />
274
+ )}
111
275
  </View>
112
276
  )
113
277
 
@@ -117,12 +281,12 @@ export const SheetScrollView = React.forwardRef<
117
281
  return (
118
282
  <RNGHComponent
119
283
  ref={composeRefs(scrollRef as any, ref)}
120
- style={{ flex: 1 }}
284
+ style={fitSizingStyle}
121
285
  scrollEventThrottle={1}
122
286
  scrollEnabled={scrollEnabled}
123
287
  simultaneousHandlers={[panGestureRef]}
124
288
  onLayout={(e: any) => {
125
- parentHeight.current = Math.ceil(e.nativeEvent.layout.height)
289
+ recordFitHeight(Math.ceil(e.nativeEvent.layout.height))
126
290
  updateScrollable()
127
291
  }}
128
292
  onScroll={(e: any) => {
@@ -137,29 +301,35 @@ export const SheetScrollView = React.forwardRef<
137
301
  animated: false,
138
302
  })
139
303
  }
304
+ currentScrollOffset.current = scrollBridge.scrollLockY
140
305
  scrollBridge.y = scrollBridge.scrollLockY
141
- onScroll?.({
142
- ...e,
143
- nativeEvent: {
144
- ...e.nativeEvent,
145
- contentOffset: {
146
- ...e.nativeEvent.contentOffset,
147
- y: scrollBridge.scrollLockY,
306
+ if (lastEmittedScrollOffset.current !== scrollBridge.scrollLockY) {
307
+ lastEmittedScrollOffset.current = scrollBridge.scrollLockY
308
+ onScroll?.({
309
+ ...e,
310
+ nativeEvent: {
311
+ ...e.nativeEvent,
312
+ contentOffset: {
313
+ ...e.nativeEvent.contentOffset,
314
+ y: scrollBridge.scrollLockY,
315
+ },
148
316
  },
149
- },
150
- })
317
+ })
318
+ }
151
319
  return
152
320
  }
153
321
 
154
322
  scrollBridge.y = y
155
323
  if (y > 0) scrollBridge.scrollStartY = -1
324
+ lastEmittedScrollOffset.current = y
156
325
  onScroll?.(e)
157
326
  }}
158
- contentContainerStyle={{ minHeight: '100%' }}
327
+ contentContainerStyle={contentContainerStyle}
159
328
  bounces={false}
160
329
  keyboardShouldPersistTaps="always"
161
330
  keyboardDismissMode="none"
162
331
  {...props}
332
+ {...keyboardFrozenOverride}
163
333
  >
164
334
  {contentWrapper}
165
335
  </RNGHComponent>
@@ -170,23 +340,26 @@ export const SheetScrollView = React.forwardRef<
170
340
  return (
171
341
  <ScrollView
172
342
  onLayout={(e) => {
173
- parentHeight.current = Math.ceil(e.nativeEvent.layout.height)
343
+ recordFitHeight(Math.ceil(e.nativeEvent.layout.height))
174
344
  updateScrollable()
175
345
  }}
176
346
  ref={composeRefs(scrollRef as any, ref)}
177
- flex={1}
347
+ {...fitSizingStyle}
178
348
  scrollEventThrottle={1}
179
349
  className="_ovs-contain"
180
350
  scrollEnabled={scrollEnabled}
181
351
  onScroll={(e) => {
182
352
  const { y } = e.nativeEvent.contentOffset
353
+ currentScrollOffset.current = y
183
354
  scrollBridge.y = y
184
355
  if (y > 0) scrollBridge.scrollStartY = -1
356
+ lastEmittedScrollOffset.current = y
185
357
  onScroll?.(e)
186
358
  }}
187
- contentContainerStyle={{ minHeight: '100%' }}
359
+ contentContainerStyle={contentContainerStyle}
188
360
  {...gestureProps}
189
361
  {...props}
362
+ {...keyboardFrozenOverride}
190
363
  >
191
364
  {contentWrapper}
192
365
  </ScrollView>
@@ -1,5 +1,5 @@
1
1
  import { useComposedRefs } from '@hanzogui/compose-refs'
2
- import { useIsomorphicLayoutEffect } from '@hanzogui/constants'
2
+ import { isWeb, useIsomorphicLayoutEffect } from '@hanzogui/constants'
3
3
  import type {
4
4
  GetProps,
5
5
  ViewProps,
@@ -25,6 +25,7 @@ import { SheetScrollView } from './SheetScrollView'
25
25
  import type { SheetProps, SheetScopedProps } from './types'
26
26
  import { useSheetController } from './useSheetController'
27
27
  import { useSheetOffscreenSize } from './useSheetOffscreenSize'
28
+ import { getMaxViewportHeight } from './webViewport'
28
29
 
29
30
  type SharedSheetProps = {
30
31
  open?: boolean
@@ -107,6 +108,7 @@ export function createSheet<
107
108
  // @ts-ignore
108
109
  <Overlay
109
110
  {...props}
111
+ ref={ref}
110
112
  onPress={composeEventHandlers(
111
113
  props.onPress,
112
114
  context.dismissOnOverlayPress
@@ -117,7 +119,7 @@ export function createSheet<
117
119
  )}
118
120
  />
119
121
  )
120
- }, [props.onPress, props.opacity, context.dismissOnOverlayPress])
122
+ }, [props, ref, context.dismissOnOverlayPress, context.setOpen])
121
123
 
122
124
  useIsomorphicLayoutEffect(() => {
123
125
  context.onOverlayComponent?.(element)
@@ -136,9 +138,9 @@ export function createSheet<
136
138
 
137
139
  type ExtraFrameProps = {
138
140
  /**
139
- * By default the sheet adds a view below its bottom that extends down another 50%,
140
- * this is useful if your Sheet has a spring animation that bounces "past" the top when
141
- * opening, preventing it from showing the content underneath.
141
+ * by default the sheet adds a view below its bottom that extends past the
142
+ * largest visible viewport height. this covers spring overshoot when opening
143
+ * so page content never shows through below the sheet.
142
144
  */
143
145
  disableHideBottomOverflow?: boolean
144
146
 
@@ -225,14 +227,25 @@ export function createSheet<
225
227
  <Frame
226
228
  {...props}
227
229
  componentName="SheetCover"
230
+ data-sheet-cover=""
228
231
  children={null}
229
232
  // Don't inherit testID - this is a visual helper element
230
233
  testID={undefined}
231
234
  id={undefined}
232
235
  position="absolute"
233
- bottom="-100%"
236
+ // anchor the cover's top at the sheet's bottom edge (top: 100% of the
237
+ // container), then extend it downward. on web extend it past the
238
+ // largest viewport safari can reveal as its chrome retracts, so the
239
+ // sheet background covers the bottom instead of revealing the page.
240
+ // native keeps frameSize.
241
+ top="100%"
234
242
  zIndex={-1}
235
- height={context.frameSize}
243
+ height={
244
+ isWeb
245
+ ? Math.max(context.frameSize, getMaxViewportHeight())
246
+ : context.frameSize
247
+ }
248
+ maxHeight={isWeb ? 'none' : undefined}
236
249
  left={0}
237
250
  right={0}
238
251
  borderWidth={0}
@@ -7,16 +7,36 @@ import { getGestureHandler, type GestureState } from '@hanzogui/native'
7
7
 
8
8
  export type { GestureState as GestureHandlerState } from '@hanzogui/native'
9
9
 
10
+ const SHEET_GESTURE_STATE_KEY = '__hanzogui_sheet_gesture_state__'
11
+
12
+ function getSheetGestureHandlerState(): GestureState {
13
+ const g = globalThis as typeof globalThis & {
14
+ [SHEET_GESTURE_STATE_KEY]?: GestureState
15
+ }
16
+
17
+ return g[SHEET_GESTURE_STATE_KEY] ?? getGestureHandler().state
18
+ }
19
+
10
20
  // backward compat helpers
11
21
  export function isGestureHandlerEnabled(): boolean {
12
- return getGestureHandler().isEnabled
22
+ return getSheetGestureHandlerState().enabled
13
23
  }
14
24
 
15
25
  export function getGestureHandlerState(): GestureState {
16
- return getGestureHandler().state
26
+ return getSheetGestureHandlerState()
17
27
  }
18
28
 
19
29
  export function setGestureHandlerState(updates: Partial<GestureState>): void {
30
+ const g = globalThis as typeof globalThis & {
31
+ [SHEET_GESTURE_STATE_KEY]?: GestureState
32
+ }
33
+
34
+ const sheetState = g[SHEET_GESTURE_STATE_KEY]
35
+ if (sheetState) {
36
+ Object.assign(sheetState, updates)
37
+ return
38
+ }
39
+
20
40
  getGestureHandler().set(updates)
21
41
  }
22
42
 
package/src/index.ts CHANGED
@@ -9,3 +9,7 @@ export * from './SheetScrollView'
9
9
  export * from './nativeSheet'
10
10
  export * from './types'
11
11
  export * from './contexts'
12
+ export {
13
+ getGestureHandlerState as unstable_getSheetGestureHandlerState,
14
+ isGestureHandlerEnabled as unstable_isSheetGestureHandlerEnabled,
15
+ } from './gestureState'
@@ -0,0 +1,121 @@
1
+ export function getKeyboardAdjustedSheetY({
2
+ sheetY,
3
+ screenSize,
4
+ isKeyboardVisible,
5
+ keyboardHeight,
6
+ shouldTranslate,
7
+ safeAreaTop,
8
+ }: {
9
+ sheetY: number
10
+ screenSize: number
11
+ isKeyboardVisible: boolean
12
+ keyboardHeight: number
13
+ shouldTranslate: boolean
14
+ safeAreaTop: number
15
+ }) {
16
+ if (
17
+ !shouldTranslate ||
18
+ !isKeyboardVisible ||
19
+ keyboardHeight <= 0 ||
20
+ screenSize <= 0 ||
21
+ sheetY >= screenSize
22
+ ) {
23
+ return sheetY
24
+ }
25
+
26
+ return Math.max(safeAreaTop, sheetY - keyboardHeight)
27
+ }
28
+
29
+ export function getSheetReleasePosition({
30
+ positions,
31
+ projectedEnd,
32
+ currentPosition,
33
+ frameSize,
34
+ dismissOnSnapToBottom,
35
+ snapPointsMode,
36
+ isKeyboardVisible,
37
+ isWeb,
38
+ }: {
39
+ positions: number[]
40
+ projectedEnd: number
41
+ currentPosition: number
42
+ frameSize: number
43
+ dismissOnSnapToBottom: boolean
44
+ snapPointsMode: 'percent' | 'constant' | 'fit' | 'mixed'
45
+ isKeyboardVisible: boolean
46
+ isWeb: boolean
47
+ }) {
48
+ let closestPoint = 0
49
+ let dist = Number.POSITIVE_INFINITY
50
+
51
+ for (let i = 0; i < positions.length; i++) {
52
+ const position = positions[i]
53
+ const curDist = Math.abs(projectedEnd - position)
54
+ if (curDist < dist) {
55
+ dist = curDist
56
+ closestPoint = i
57
+ }
58
+ }
59
+
60
+ // the keyboard-open dismiss threshold is a WEB keyboard-handoff fix
61
+ // (75de9c9694). on native the keyboard is opaque and the sheet snaps purely by
62
+ // projected position (the pre-rework behavior), so gate it to web only —
63
+ // otherwise a native fit+dismiss sheet snaps to the wrong point with the
64
+ // keyboard up.
65
+ const dismissPoint = positions.length - 1
66
+ const isKeyboardFitDismiss =
67
+ isWeb &&
68
+ dismissOnSnapToBottom &&
69
+ isKeyboardVisible &&
70
+ snapPointsMode === 'fit' &&
71
+ positions.length === 2 &&
72
+ closestPoint === dismissPoint
73
+
74
+ if (!isKeyboardFitDismiss) {
75
+ return closestPoint
76
+ }
77
+
78
+ const openPosition = positions[0]
79
+ const dismissPosition = positions[dismissPoint]
80
+ const dismissDistance = dismissPosition - openPosition
81
+ if (dismissDistance <= 0 || frameSize <= 0) {
82
+ return closestPoint
83
+ }
84
+
85
+ const draggedDistance = Math.max(0, currentPosition - openPosition)
86
+ const thresholdBase = Math.min(frameSize, dismissDistance)
87
+ const dismissThreshold = Math.max(120, thresholdBase * 0.35)
88
+
89
+ return draggedDistance >= dismissThreshold ? closestPoint : 0
90
+ }
91
+
92
+ export function getKeyboardOccludedHeight({
93
+ frameSize,
94
+ isKeyboardVisible,
95
+ keyboardHeight,
96
+ screenSize,
97
+ sheetY,
98
+ }: {
99
+ frameSize: number
100
+ isKeyboardVisible: boolean
101
+ keyboardHeight: number
102
+ screenSize: number
103
+ sheetY: number | undefined
104
+ }) {
105
+ if (
106
+ !isKeyboardVisible ||
107
+ keyboardHeight <= 0 ||
108
+ screenSize <= 0 ||
109
+ frameSize <= 0 ||
110
+ sheetY === undefined ||
111
+ sheetY >= screenSize
112
+ ) {
113
+ return 0
114
+ }
115
+
116
+ const keyboardTop = screenSize - keyboardHeight
117
+ const sheetBottom = sheetY + frameSize
118
+ const occludedHeight = Math.ceil(sheetBottom - keyboardTop)
119
+
120
+ return Math.min(frameSize, Math.max(0, occludedHeight))
121
+ }
@@ -59,7 +59,14 @@ export function setupNativeSheet(
59
59
 
60
60
  return (
61
61
  <>
62
- <SheetProvider setHasScrollView={emptyFn} {...providerProps} onlyShowFrame>
62
+ <SheetProvider
63
+ setHasScrollView={emptyFn}
64
+ keyboardOccludedHeight={0}
65
+ isKeyboardVisible={false}
66
+ keyboardStableFrameHeight={0}
67
+ {...providerProps}
68
+ onlyShowFrame
69
+ >
63
70
  <ModalSheetView ref={ref} onModalDidDismiss={() => setOpenInternal(false)}>
64
71
  <ModalSheetViewMainContent>
65
72
  <View style={{ flex: 1 }}>{props.children}</View>
@@ -4,8 +4,16 @@
4
4
 
5
5
  import { getGestureHandler } from '@hanzogui/native'
6
6
 
7
+ import type { GestureState } from '@hanzogui/native'
8
+
9
+ const SHEET_GESTURE_STATE_KEY = '__hanzogui_sheet_gesture_state__'
10
+
7
11
  export function isGestureHandlerEnabled(): boolean {
8
- return getGestureHandler().isEnabled
12
+ const g = globalThis as typeof globalThis & {
13
+ [SHEET_GESTURE_STATE_KEY]?: GestureState
14
+ }
15
+
16
+ return g[SHEET_GESTURE_STATE_KEY]?.enabled ?? getGestureHandler().isEnabled
9
17
  }
10
18
 
11
19
  export interface SetupGestureHandlerConfig {
@@ -16,19 +24,20 @@ export interface SetupGestureHandlerConfig {
16
24
 
17
25
  export function setupGestureHandler(config: SetupGestureHandlerConfig): void {
18
26
  const g = globalThis as any
19
- if (g.__gui_sheet_gesture_handler_setup) {
27
+ if (g.__hanzogui_sheet_gesture_handler_setup) {
20
28
  return
21
29
  }
22
- g.__gui_sheet_gesture_handler_setup = true
30
+ g.__hanzogui_sheet_gesture_handler_setup = true
23
31
 
24
32
  const { Gesture, GestureDetector, ScrollView } = config
25
33
 
26
34
  if (Gesture && GestureDetector) {
27
- getGestureHandler().set({
35
+ g[SHEET_GESTURE_STATE_KEY] = {
28
36
  enabled: true,
29
37
  Gesture,
30
38
  GestureDetector,
31
39
  ScrollView: ScrollView || null,
32
- })
40
+ RootView: null,
41
+ } satisfies GestureState
33
42
  }
34
43
  }
package/src/types.tsx CHANGED
@@ -60,6 +60,12 @@ export type SheetProps = ScopedProps<
60
60
  */
61
61
  unmountChildrenWhenHidden?: boolean
62
62
 
63
+ /**
64
+ * Keep the sheet content wrapper opaque while the sheet is hidden.
65
+ * Useful for native visual effects that cannot initialize below a transparent ancestor.
66
+ */
67
+ disableTransparencyHide?: boolean
68
+
63
69
  /**
64
70
  * Adapts the sheet to use native sheet on the given platform (if available)
65
71
  */
@@ -75,7 +81,8 @@ export type SheetProps = ScopedProps<
75
81
  zIndex?: number
76
82
  portalProps?: PortalProps
77
83
  /**
78
- * Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible
84
+ * Makes the sheet move up when the mobile keyboard opens so the focused input remains visible.
85
+ * Works on native (via keyboard events) and on mobile web (via the VisualViewport API).
79
86
  */
80
87
  moveOnKeyboardChange?: boolean
81
88
  containerComponent?: React.ComponentType<any>
@@ -125,12 +132,24 @@ export type ScrollBridge = {
125
132
  _lastTouchY?: number
126
133
  // scroll lock position for forcing scroll back when pan handles
127
134
  scrollLockY?: number
135
+ // keep scroll locked while a drag that started below the top snap is still
136
+ // deciding whether to hand off into content scrolling.
137
+ lockScrollAtTop?: boolean
128
138
  // force scroll to position (compensates for async setNativeProps)
129
139
  forceScrollTo?: (y: number) => void
130
140
  // whether sheet is at top position (for scroll enable/disable)
131
141
  isAtTop?: boolean
132
142
  // snap sheet to a specific position (for handoff UP)
133
143
  snapToPosition?: (positionIndex: number) => void
144
+ // re-baseline the pan drag origin to the current animated position. the web
145
+ // scroll-view hook calls this on each transition into pan ownership so a
146
+ // scroll→pan handoff resumes from where the sheet is, not a stale origin.
147
+ startPanDrag?: () => void
148
+ // web only: true while a touch is active on the ScrollView node. The web
149
+ // scroll-view gesture hook owns drag detection for those touches (it calls
150
+ // drag/release on this bridge), so the PanResponder must NOT also grant for
151
+ // them — otherwise two systems drive the animated position and it jitters.
152
+ scrollNodeTouched?: boolean
134
153
  }
135
154
 
136
155
  // keyboard controller sheet types