@tamagui/sheet 2.1.0 → 2.2.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 (165) hide show
  1. package/.turbo/turbo-build.log +3 -2
  2. package/dist/cjs/GestureDetectorWrapper.native.js.map +1 -1
  3. package/dist/cjs/GestureSheetContext.native.js.map +1 -1
  4. package/dist/cjs/Sheet.native.js.map +1 -1
  5. package/dist/cjs/SheetContext.native.js.map +1 -1
  6. package/dist/cjs/SheetController.native.js.map +1 -1
  7. package/dist/cjs/SheetImplementationCustom.cjs +41 -64
  8. package/dist/cjs/SheetImplementationCustom.native.js +56 -69
  9. package/dist/cjs/SheetImplementationCustom.native.js.map +1 -1
  10. package/dist/cjs/SheetScrollView.cjs +57 -5
  11. package/dist/cjs/SheetScrollView.native.js +60 -5
  12. package/dist/cjs/SheetScrollView.native.js.map +1 -1
  13. package/dist/cjs/constants.native.js.map +1 -1
  14. package/dist/cjs/contexts.native.js.map +1 -1
  15. package/dist/cjs/controller.native.js.map +1 -1
  16. package/dist/cjs/createSheet.cjs +5 -2
  17. package/dist/cjs/createSheet.native.js +5 -2
  18. package/dist/cjs/createSheet.native.js.map +1 -1
  19. package/dist/cjs/gestureState.native.js.map +1 -1
  20. package/dist/cjs/helpers.native.js.map +1 -1
  21. package/dist/cjs/index.native.js.map +1 -1
  22. package/dist/cjs/keyboardAvoidance.cjs +52 -1
  23. package/dist/cjs/keyboardAvoidance.native.js +54 -1
  24. package/dist/cjs/keyboardAvoidance.native.js.map +1 -1
  25. package/dist/cjs/nativeSheet.cjs +0 -1
  26. package/dist/cjs/nativeSheet.native.js +0 -1
  27. package/dist/cjs/nativeSheet.native.js.map +1 -1
  28. package/dist/cjs/setupGestureHandler.native.js.map +1 -1
  29. package/dist/cjs/types.native.js.map +1 -1
  30. package/dist/cjs/useGestureHandlerPan.cjs +25 -10
  31. package/dist/cjs/useGestureHandlerPan.native.js +27 -10
  32. package/dist/cjs/useGestureHandlerPan.native.js.map +1 -1
  33. package/dist/cjs/useKeyboardControllerSheet.cjs +10 -4
  34. package/dist/cjs/useSafeAreaInsets.cjs +44 -0
  35. package/dist/cjs/useSafeAreaInsets.native.js +48 -0
  36. package/dist/cjs/useSafeAreaInsets.native.js.map +1 -0
  37. package/dist/cjs/useSheet.native.js.map +1 -1
  38. package/dist/cjs/useSheetController.native.js.map +1 -1
  39. package/dist/cjs/useSheetOffscreenSize.native.js.map +1 -1
  40. package/dist/cjs/useSheetOpenState.native.js.map +1 -1
  41. package/dist/cjs/useSheetProviderProps.native.js.map +1 -1
  42. package/dist/cjs/useSheetScrollViewGestures.cjs +12 -2
  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 +26 -1
  46. package/dist/cjs/webViewport.native.js +28 -1
  47. package/dist/cjs/webViewport.native.js.map +1 -1
  48. package/dist/esm/SheetImplementationCustom.mjs +48 -73
  49. package/dist/esm/SheetImplementationCustom.mjs.map +1 -1
  50. package/dist/esm/SheetImplementationCustom.native.js +70 -89
  51. package/dist/esm/SheetImplementationCustom.native.js.map +1 -1
  52. package/dist/esm/SheetScrollView.mjs +58 -6
  53. package/dist/esm/SheetScrollView.mjs.map +1 -1
  54. package/dist/esm/SheetScrollView.native.js +61 -6
  55. package/dist/esm/SheetScrollView.native.js.map +1 -1
  56. package/dist/esm/createSheet.mjs +7 -5
  57. package/dist/esm/createSheet.mjs.map +1 -1
  58. package/dist/esm/createSheet.native.js +17 -14
  59. package/dist/esm/createSheet.native.js.map +1 -1
  60. package/dist/esm/keyboardAvoidance.mjs +50 -1
  61. package/dist/esm/keyboardAvoidance.mjs.map +1 -1
  62. package/dist/esm/keyboardAvoidance.native.js +52 -1
  63. package/dist/esm/keyboardAvoidance.native.js.map +1 -1
  64. package/dist/esm/nativeSheet.mjs +0 -1
  65. package/dist/esm/nativeSheet.mjs.map +1 -1
  66. package/dist/esm/nativeSheet.native.js +0 -1
  67. package/dist/esm/nativeSheet.native.js.map +1 -1
  68. package/dist/esm/useGestureHandlerPan.mjs +25 -10
  69. package/dist/esm/useGestureHandlerPan.mjs.map +1 -1
  70. package/dist/esm/useGestureHandlerPan.native.js +27 -10
  71. package/dist/esm/useGestureHandlerPan.native.js.map +1 -1
  72. package/dist/esm/useKeyboardControllerSheet.mjs +11 -5
  73. package/dist/esm/useKeyboardControllerSheet.mjs.map +1 -1
  74. package/dist/esm/useSafeAreaInsets.mjs +7 -0
  75. package/dist/esm/useSafeAreaInsets.mjs.map +1 -0
  76. package/dist/esm/useSafeAreaInsets.native.js +8 -0
  77. package/dist/esm/useSafeAreaInsets.native.js.map +1 -0
  78. package/dist/esm/useSheetProviderProps.mjs.map +1 -1
  79. package/dist/esm/useSheetProviderProps.native.js.map +1 -1
  80. package/dist/esm/useSheetScrollViewGestures.mjs +12 -2
  81. package/dist/esm/useSheetScrollViewGestures.mjs.map +1 -1
  82. package/dist/esm/useSheetScrollViewGestures.native.js +4 -0
  83. package/dist/esm/useSheetScrollViewGestures.native.js.map +1 -1
  84. package/dist/esm/webViewport.mjs +23 -2
  85. package/dist/esm/webViewport.mjs.map +1 -1
  86. package/dist/esm/webViewport.native.js +25 -2
  87. package/dist/esm/webViewport.native.js.map +1 -1
  88. package/dist/jsx/SheetImplementationCustom.mjs +48 -73
  89. package/dist/jsx/SheetImplementationCustom.mjs.map +1 -1
  90. package/dist/jsx/SheetImplementationCustom.native.js +56 -69
  91. package/dist/jsx/SheetImplementationCustom.native.js.map +1 -1
  92. package/dist/jsx/SheetScrollView.mjs +58 -6
  93. package/dist/jsx/SheetScrollView.mjs.map +1 -1
  94. package/dist/jsx/SheetScrollView.native.js +60 -5
  95. package/dist/jsx/SheetScrollView.native.js.map +1 -1
  96. package/dist/jsx/createSheet.mjs +7 -5
  97. package/dist/jsx/createSheet.mjs.map +1 -1
  98. package/dist/jsx/createSheet.native.js +5 -2
  99. package/dist/jsx/createSheet.native.js.map +1 -1
  100. package/dist/jsx/keyboardAvoidance.mjs +50 -1
  101. package/dist/jsx/keyboardAvoidance.mjs.map +1 -1
  102. package/dist/jsx/keyboardAvoidance.native.js +54 -1
  103. package/dist/jsx/keyboardAvoidance.native.js.map +1 -1
  104. package/dist/jsx/nativeSheet.mjs +0 -1
  105. package/dist/jsx/nativeSheet.mjs.map +1 -1
  106. package/dist/jsx/nativeSheet.native.js +0 -1
  107. package/dist/jsx/nativeSheet.native.js.map +1 -1
  108. package/dist/jsx/useGestureHandlerPan.mjs +25 -10
  109. package/dist/jsx/useGestureHandlerPan.mjs.map +1 -1
  110. package/dist/jsx/useGestureHandlerPan.native.js +27 -10
  111. package/dist/jsx/useGestureHandlerPan.native.js.map +1 -1
  112. package/dist/jsx/useKeyboardControllerSheet.mjs +11 -5
  113. package/dist/jsx/useKeyboardControllerSheet.mjs.map +1 -1
  114. package/dist/jsx/useSafeAreaInsets.mjs +7 -0
  115. package/dist/jsx/useSafeAreaInsets.mjs.map +1 -0
  116. package/dist/jsx/useSafeAreaInsets.native.js +48 -0
  117. package/dist/jsx/useSafeAreaInsets.native.js.map +1 -0
  118. package/dist/jsx/useSheetProviderProps.mjs.map +1 -1
  119. package/dist/jsx/useSheetProviderProps.native.js.map +1 -1
  120. package/dist/jsx/useSheetScrollViewGestures.mjs +12 -2
  121. package/dist/jsx/useSheetScrollViewGestures.mjs.map +1 -1
  122. package/dist/jsx/useSheetScrollViewGestures.native.js +4 -0
  123. package/dist/jsx/useSheetScrollViewGestures.native.js.map +1 -1
  124. package/dist/jsx/webViewport.mjs +23 -2
  125. package/dist/jsx/webViewport.mjs.map +1 -1
  126. package/dist/jsx/webViewport.native.js +28 -1
  127. package/dist/jsx/webViewport.native.js.map +1 -1
  128. package/package.json +24 -25
  129. package/src/SheetImplementationCustom.tsx +147 -212
  130. package/src/SheetScrollView.tsx +81 -21
  131. package/src/createSheet.tsx +18 -6
  132. package/src/keyboardAvoidance.ts +91 -0
  133. package/src/nativeSheet.tsx +0 -1
  134. package/src/useGestureHandlerPan.tsx +29 -12
  135. package/src/useKeyboardControllerSheet.ts +35 -14
  136. package/src/useSafeAreaInsets.native.ts +29 -0
  137. package/src/useSafeAreaInsets.ts +19 -0
  138. package/src/useSheetProviderProps.tsx +4 -10
  139. package/src/useSheetScrollViewGestures.native.ts +4 -0
  140. package/src/useSheetScrollViewGestures.ts +19 -8
  141. package/src/webViewport.ts +56 -8
  142. package/test/keyboardAvoidance.test.ts +218 -2
  143. package/types/SheetContext.d.ts +0 -1
  144. package/types/SheetContext.d.ts.map +1 -1
  145. package/types/SheetImplementationCustom.d.ts.map +1 -1
  146. package/types/SheetScrollView.d.ts.map +1 -1
  147. package/types/createSheet.d.ts +12 -12
  148. package/types/createSheet.d.ts.map +1 -1
  149. package/types/keyboardAvoidance.d.ts +18 -0
  150. package/types/keyboardAvoidance.d.ts.map +1 -1
  151. package/types/nativeSheet.d.ts.map +1 -1
  152. package/types/useGestureHandlerPan.d.ts +4 -1
  153. package/types/useGestureHandlerPan.d.ts.map +1 -1
  154. package/types/useKeyboardControllerSheet.d.ts +5 -5
  155. package/types/useKeyboardControllerSheet.d.ts.map +1 -1
  156. package/types/useSafeAreaInsets.d.ts +10 -0
  157. package/types/useSafeAreaInsets.d.ts.map +1 -0
  158. package/types/useSafeAreaInsets.native.d.ts +20 -0
  159. package/types/useSafeAreaInsets.native.d.ts.map +1 -0
  160. package/types/useSheetProviderProps.d.ts +0 -1
  161. package/types/useSheetProviderProps.d.ts.map +1 -1
  162. package/types/useSheetScrollViewGestures.d.ts.map +1 -1
  163. package/types/useSheetScrollViewGestures.native.d.ts.map +1 -1
  164. package/types/webViewport.d.ts +13 -7
  165. package/types/webViewport.d.ts.map +1 -1
@@ -10,7 +10,6 @@ import {
10
10
  useEvent,
11
11
  useThemeName,
12
12
  } from '@tamagui/core'
13
- import { getSafeArea } from '@tamagui/native'
14
13
  import { needsPortalRepropagation, Portal } from '@tamagui/portal'
15
14
  import React, { useState } from 'react'
16
15
  import type {
@@ -25,16 +24,23 @@ import { GestureDetectorWrapper } from './GestureDetectorWrapper'
25
24
  import { getGestureHandlerState } from './gestureState'
26
25
  import { GestureSheetProvider } from './GestureSheetContext'
27
26
  import { resisted } from './helpers'
28
- import { getKeyboardOccludedHeight } from './keyboardAvoidance'
29
27
  import {
28
+ getKeyboardAdjustedSheetY,
29
+ getKeyboardOccludedHeight,
30
+ getSheetReleasePosition,
31
+ } from './keyboardAvoidance'
32
+ import {
33
+ getWebKeyboardResizeHeight,
34
+ getMaxViewportHeight,
30
35
  getStableLayoutViewportHeight,
31
- getWebKeyboardHeight,
36
+ getWebVisualViewportOffsetTop,
32
37
  MIN_KEYBOARD_HEIGHT,
33
38
  } from './webViewport'
34
39
  import { SheetProvider } from './SheetContext'
35
40
  import type { SheetProps, SnapPointsMode } from './types'
36
41
  import { useGestureHandlerPan } from './useGestureHandlerPan'
37
42
  import { useKeyboardControllerSheet } from './useKeyboardControllerSheet'
43
+ import { SafeAreaInsetsContext, useSafeAreaInsets } from './useSafeAreaInsets'
38
44
  import { useSheetOpenState } from './useSheetOpenState'
39
45
  import { useSheetProviderProps } from './useSheetProviderProps'
40
46
 
@@ -48,15 +54,6 @@ const hiddenSize = 10_000.1
48
54
  const rnghRootStyleOpen = { width: '100%', height: '100%' } as const
49
55
  const rnghRootStyleClosed = { width: '100%', height: 0 } as const
50
56
 
51
- // safe area top inset, cached per-session (device-constant value)
52
- let _cachedSafeAreaTop: number | undefined
53
- function getSafeAreaTopInset(): number {
54
- if (_cachedSafeAreaTop !== undefined) return _cachedSafeAreaTop
55
- // use @tamagui/native abstraction - returns 0 when not enabled
56
- _cachedSafeAreaTop = getSafeArea().getInsets().top
57
- return _cachedSafeAreaTop
58
- }
59
-
60
57
  let sheetHiddenStyleSheet: HTMLStyleElement | null = null
61
58
 
62
59
  // on web we are always relative to window, on to screen
@@ -79,6 +76,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
79
76
  function SheetImplementationCustom(props, forwardedRef) {
80
77
  const parentSheet = React.useContext(ParentSheetContext)
81
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
+
82
89
  const {
83
90
  transition,
84
91
  transitionConfig: transitionConfigProp,
@@ -100,6 +107,7 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
100
107
  const {
101
108
  frameSize,
102
109
  setFrameSize,
110
+ dismissOnSnapToBottom,
103
111
  snapPoints,
104
112
  snapPointsMode,
105
113
  hasFit,
@@ -183,75 +191,34 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
183
191
  }
184
192
  }, [open, frameSize])
185
193
 
186
- // WEB keyboard anchor freeze. on real iOS Safari opening the keyboard shrinks
194
+ // WEB keyboard frame freeze. on real iOS Safari opening the keyboard shrinks
187
195
  // the visual viewport AND innerHeight AND the measured layout, which would
188
196
  // re-derive screenSize/frameSize smaller, recompute the fit positions, and
189
197
  // fly the frame up then back down ("goes back down after the keyboard opens").
190
198
  // so we snapshot the pre-keyboard geometry — captured every render while the
191
199
  // keyboard is CLOSED, which dodges the open-transition race where a shrunk
192
- // onLayout lands before isKeyboardVisible flips — and use it for the anchor
193
- // math while the keyboard is open. the sheet then stays put; only the scroll
194
- // content shifts to clear the keyboard (keyboardOccludedHeight padding below
195
- // + SheetScrollView's frozen height).
196
- // this sheet is the kind the web keyboard anchor freeze is designed for — a
200
+ // onLayout lands before isKeyboardVisible flips — and use it for frame-size
201
+ // math while the keyboard is open. the active snap position still shifts up
202
+ // by the keyboard height, capped at the safe-area top; the scroll view gets
203
+ // keyboardOccludedHeight padding for any tail left behind the keyboard.
204
+ // this sheet is the kind the web keyboard frame freeze is designed for — a
197
205
  // fit-mode web sheet opted into keyboard handling. percent/constant sheets
198
- // keep the live geometry (their height isn't pinned, so a frozen anchor
199
- // would mismatch).
206
+ // keep the live geometry (their height isn't pinned, so a frozen frame
207
+ // height would mismatch).
200
208
  const isWebKbSheet = isWeb && hasFit && moveOnKeyboardChange
201
209
 
202
- // whether we ever captured geometry while the keyboard was CLOSED. that's the
203
- // authoritative baseline; until we have it, the autofocus-on-open seed below
204
- // reconstructs one from keyboard-up measurements.
205
- const hasCleanKbBaseline = React.useRef(false)
206
- // the autofocus-on-open seed has settled on a stable frame height (the
207
- // unclipped content stopped growing). until then we keep the seed phase open.
208
- const seedSettled = React.useRef(false)
209
- const stableKbGeom = React.useRef({ frame: 0, screen: 0 })
210
- if ((!isWeb || !isKeyboardVisible) && frameSize > 0 && screenSize > 0) {
211
- // keyboard-free render the authoritative baseline. mutate in place: the
212
- // ref is only ever read field-by-field, never identity-compared, so this
213
- // avoids a per-render allocation.
214
- stableKbGeom.current.frame = frameSize
215
- stableKbGeom.current.screen = screenSize
216
- hasCleanKbBaseline.current = true
217
- } else if (
218
- isWebKbSheet &&
219
- isKeyboardVisible &&
220
- !hasCleanKbBaseline.current &&
221
- screenSize > 0
222
- ) {
223
- // AUTOFOCUS-ON-OPEN seed. when the input autofocuses as the sheet animates
224
- // in, the keyboard rises BEFORE any keyboard-free layout lands, so the
225
- // branch above never runs (frameSize/screenSize were 0 the whole time the
226
- // keyboard was up) and freezeForKb stays false, collapsing the sheet to the
227
- // shrunk consumer maxHeight. instead, reconstruct the baseline from the
228
- // keyboard-up render: screen = the stable layout viewport
229
- // (keyboard-independent). the frame is grown by the seeding layout path
230
- // below: while seeding, the tail padding is suppressed and the scroll view
231
- // is unclipped (via keyboardStableFrameHeight = stable screen), so the frame
232
- // converges on its pure pre-keyboard content height across a couple layout
233
- // passes. a real keyboard-free render later replaces it with the exact value
234
- // (branch above).
235
- stableKbGeom.current.screen = Math.max(stableKbGeom.current.screen, screenSize)
236
- }
237
- // are we still reconstructing the baseline from a keyboard-up render? true
238
- // until either a clean baseline lands or the seeded frame settles.
239
- const seedingKbBaseline =
240
- isWebKbSheet &&
241
- isKeyboardVisible &&
242
- !hasCleanKbBaseline.current &&
243
- !seedSettled.current
244
- const freezeForKb =
245
- isWebKbSheet && isKeyboardVisible && stableKbGeom.current.frame > 0
246
- const effScreenSize = freezeForKb ? stableKbGeom.current.screen : screenSize
247
-
248
- // use stableFrameSize when closing to prevent position jumps during exit animation
249
- // but when opening, always use the current frameSize so positions update correctly
250
- const effectiveFrameSize = freezeForKb
251
- ? stableKbGeom.current.frame
252
- : open
253
- ? frameSize
254
- : stableFrameSize.current || frameSize
210
+ // the space the snap positions are built against. WEB: the stable layout
211
+ // viewport (document.documentElement.clientHeight), which the soft keyboard
212
+ // never shrinks (unlike the measured screenSize / visualViewport). NATIVE:
213
+ // the measured screenSize. activePositions shift up by keyboardHeight below
214
+ // so a small fit sheet keeps its natural height and moves with the keyboard;
215
+ // a tall fit sheet moves until capped at the safe-area top, leaving its tail
216
+ // behind the keyboard for the ScrollView padding to expose.
217
+ const effScreenSize = isWebKbSheet ? getStableViewportHeight() : screenSize
218
+
219
+ // use stableFrameSize when closing to prevent position jumps during the exit
220
+ // animation; while open use the live frameSize.
221
+ const effectiveFrameSize = open ? frameSize : stableFrameSize.current || frameSize
255
222
 
256
223
  const positions = React.useMemo(
257
224
  () =>
@@ -284,17 +251,9 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
284
251
 
285
252
  // keyboard-adjusted snap positions.
286
253
  //
287
- // WEB: the sheet stays ANCHORED at the bottom and keeps its full pre-keyboard
288
- // height it does NOT shift up or resize. Shifting/resizing with the
289
- // translateY model detaches the bottom from the screen edge or teleports it.
290
- // Instead the frame's anchor geometry is frozen (effScreenSize/effectiveFrameSize
291
- // above) and its height is pinned (keyboardStableFrameHeight -> SheetScrollView);
292
- // the scroll content is padded by keyboardOccludedHeight and the browser
293
- // scroll-into-view lifts the focused input above the keyboard. So
294
- // activePositions === positions on web.
295
- //
296
- // NATIVE: shift snap points up by keyboard height (the native keyboard is
297
- // opaque and pushes content), capped at the safe-area top inset.
254
+ // WEB + NATIVE: shift snap points up by keyboard height, capped at the
255
+ // safe-area top inset. web fit sheets keep their frozen pre-keyboard height
256
+ // and add bottom spacer padding when the safe-area cap leaves a hidden tail.
298
257
  //
299
258
  // IMPORTANT: frozen during drag to prevent gesture handler recreation —
300
259
  // when a TextInput blurs mid-drag the keyboard state would otherwise revert
@@ -304,55 +263,51 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
304
263
  if (isDragging || isDraggingRef.current) return activePositionsRef.current
305
264
 
306
265
  let result: number[]
307
- if (isWeb || !isKeyboardVisible || keyboardHeight <= 0) {
266
+
267
+ if (!isKeyboardVisible || keyboardHeight <= 0) {
308
268
  result = positions
309
269
  } else {
310
- const safeAreaTop = getSafeAreaTopInset()
311
- result = positions.map((p) => {
312
- // don't adjust the off-screen/close position (from dismissOnSnapToBottom's 0% snap)
313
- // — it must stay at screenSize so the user can drag between real snap points
314
- // without accidentally closing the sheet
315
- if (screenSize && p >= screenSize) return p
316
- return Math.max(safeAreaTop, p - keyboardHeight)
317
- })
270
+ result = positions.map((p) =>
271
+ getKeyboardAdjustedSheetY({
272
+ sheetY: p,
273
+ screenSize: effScreenSize,
274
+ isKeyboardVisible,
275
+ keyboardHeight,
276
+ shouldTranslate: true,
277
+ safeAreaTop: isWeb ? getWebVisualViewportOffsetTop() : safeAreaTopInset,
278
+ })
279
+ )
318
280
  }
319
281
  activePositionsRef.current = result
320
282
  return result
321
- }, [positions, isKeyboardVisible, keyboardHeight, screenSize, isDragging])
322
-
323
- const keyboardOccludedHeight = seedingKbBaseline
324
- ? // while seeding, suppress the keyboard tail padding so the frame measures
325
- // its PURE pre-keyboard content height (the padding would otherwise inflate
326
- // it toward the full screen). re-enabled once the baseline is captured.
327
- 0
328
- : getKeyboardOccludedHeight({
329
- frameSize: effectiveFrameSize,
330
- isKeyboardVisible,
331
- keyboardHeight,
332
- screenSize: effScreenSize,
333
- sheetY: position >= 0 ? activePositions[position] : undefined,
334
- })
283
+ }, [
284
+ positions,
285
+ isKeyboardVisible,
286
+ keyboardHeight,
287
+ effScreenSize,
288
+ isDragging,
289
+ safeAreaTopInset,
290
+ ])
335
291
 
336
- // the authoritative pre-keyboard frame height to pin the scroll view to while
337
- // the keyboard is open (web). stableKbGeom.frame is captured every render the
338
- // keyboard is closed, so it survives the open transition; SheetScrollView
339
- // gates application on its own live keyboard check.
340
- //
341
- // AUTOFOCUS-ON-OPEN seed: while still reconstructing the baseline (seeding,
342
- // not yet settled) we use the STABLE SCREEN SIZE — a safe upper bound (a fit
343
- // frame never exceeds the screen) that is > 0, so SheetScrollView's height
344
- // override engages and UNCLIPS the scroll view from the shrunk consumer
345
- // maxHeight. with the tail padding suppressed (above) the content then lays
346
- // out to its pure intrinsic height, which the seeding layout grows the frame
347
- // baseline toward across a couple passes. once settled (or once a real
348
- // keyboard-free baseline lands) we pin that exact frame height.
349
- const keyboardStableFrameHeight = !isWebKbSheet
350
- ? 0
351
- : seedingKbBaseline
352
- ? stableKbGeom.current.screen || screenSize
353
- : stableKbGeom.current.frame > 0
354
- ? stableKbGeom.current.frame
355
- : 0
292
+ // bottom spacer for the part of the sheet hidden by the keyboard after the
293
+ // keyboard translation and safe-area clamping. if a small sheet fits above
294
+ // the keyboard this is 0; if a tall sheet is capped at the safe-area top, the
295
+ // spacer makes the remaining hidden tail scrollable.
296
+ const keyboardOccludedHeight = getKeyboardOccludedHeight({
297
+ frameSize: effectiveFrameSize,
298
+ isKeyboardVisible,
299
+ keyboardHeight,
300
+ screenSize: effScreenSize,
301
+ sheetY: position >= 0 ? activePositions[position] : undefined,
302
+ })
303
+
304
+ // pin the scroll view to the held (pre-keyboard) frame height while the
305
+ // keyboard is up on web. on older iOS the consumer's window-derived maxHeight
306
+ // shrinks with the keyboard, which would clip the scroll view (and the frame)
307
+ // smaller; this override keeps it at the full height so the frame translates
308
+ // with the keyboard but never resizes. 0 = no override (use the consumer maxHeight).
309
+ const keyboardStableFrameHeight =
310
+ isWebKbSheet && isKeyboardVisible && frameSize > 0 ? frameSize : 0
356
311
 
357
312
  const { useAnimatedNumber, useAnimatedNumberStyle, useAnimatedNumberReaction } =
358
313
  animationDriver
@@ -442,8 +397,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
442
397
  // use effScreenSize (the frozen anchor space the positions were built in) for
443
398
  // the off-screen/close target too, so a close while the keyboard is still up
444
399
  // animates fully out instead of to a mismatched live screenSize.
445
- let toValue =
446
- isHidden || position === -1 ? effScreenSize : activePositions[position]
400
+ //
401
+ // web: clear the maximum the viewport can ever reveal, not just the current
402
+ // layout viewport. iOS Safari retracts its chrome on scroll and exposes area
403
+ // below the current viewport, so a sheet parked at effScreenSize would peek
404
+ // back in as the page scrolls. getMaxViewportHeight floors the target past
405
+ // anything Safari can expose.
406
+ const closeTarget = isWeb
407
+ ? Math.max(effScreenSize, getMaxViewportHeight())
408
+ : effScreenSize
409
+ let toValue = isHidden || position === -1 ? closeTarget : activePositions[position]
447
410
 
448
411
  if (at.current === toValue) return
449
412
 
@@ -571,6 +534,10 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
571
534
  scrollBridge.setScrollEnabled?.(false)
572
535
  }
573
536
  }
537
+ // NOTE: effScreenSize/effectiveFrameSize are intentionally NOT deps. With the
538
+ // spacer approach the frame's position target is frozen across keyboard
539
+ // open/close (same stable baseline), so it must NOT re-animate — keyboard
540
+ // avoidance is the bottom spacer + scroll, not a frame move.
574
541
  }, [hasntMeasured, disableAnimation, isHidden, frameSize, screenSize, open, position])
575
542
 
576
543
  const disableDrag = props.disableDrag ?? controller?.disableDrag
@@ -626,17 +593,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
626
593
  // vy goes up to about 4 at most (+ is down, - is up)
627
594
  const end = currentPos + frameSize * vy * 0.2
628
595
 
629
- let closestPoint = 0
630
- let dist = Number.POSITIVE_INFINITY
631
-
632
- for (let i = 0; i < activePositions.length; i++) {
633
- const position = activePositions[i]
634
- const curDist = end > position ? end - position : position - end
635
- if (curDist < dist) {
636
- dist = curDist
637
- closestPoint = i
638
- }
639
- }
596
+ const closestPoint = getSheetReleasePosition({
597
+ positions: activePositions,
598
+ projectedEnd: end,
599
+ currentPosition: currentPos,
600
+ frameSize,
601
+ dismissOnSnapToBottom,
602
+ snapPointsMode,
603
+ isKeyboardVisible,
604
+ isWeb,
605
+ })
640
606
 
641
607
  // have to call both because state may not change but need to snap back
642
608
  setPosition(closestPoint)
@@ -750,6 +716,10 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
750
716
 
751
717
  return PanResponder.create({
752
718
  onMoveShouldSetPanResponder: onMoveShouldSet,
719
+ // once we own the drag, don't yield it to another responder
720
+ // (re-renders during the drag were cooperatively terminating it under
721
+ // load, killing the gesture mid-drag)
722
+ onPanResponderTerminationRequest: () => false,
753
723
  onPanResponderGrant: grant,
754
724
  onPanResponderMove: (_e, { dy }) => {
755
725
  const toFull = dy + startY
@@ -776,18 +746,18 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
776
746
  frameSize,
777
747
  activePositions,
778
748
  setPosition,
749
+ dismissOnSnapToBottom,
750
+ snapPointsMode,
779
751
  ])
780
752
 
781
- // animate to keyboard-adjusted position when keyboard state changes.
782
- // WEB skips this: the sheet stays anchored at its frozen position/height
783
- // (activePositions don't change with the keyboard, and the anchor geometry is
784
- // frozen), so there's nothing to re-animate. Running a timing animation here
785
- // would only introduce movement where the sheet should hold perfectly still.
753
+ // animate to the current keyboard-adjusted position when the keyboard state
754
+ // changes. activePositions shift the frame up by keyboardHeight, capped at
755
+ // the safe-area top; tall web fit sheets keep a frozen height and gain scroll
756
+ // padding for whatever tail remains behind the keyboard.
786
757
  React.useEffect(() => {
787
- if (isWeb) return
788
758
  if (isDragging || isHidden || !open || disableAnimation) return
789
759
  if (!frameSize || !screenSize) return
790
- // use timing animation to match iOS keyboard animation (~250ms)
760
+ // timing animation matches the iOS keyboard animation (~250ms)
791
761
  animateTo(position, { type: 'timing', duration: 250 })
792
762
  }, [isKeyboardVisible, keyboardHeight])
793
763
 
@@ -838,6 +808,9 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
838
808
  at.current = val
839
809
  animatedNumber.setValue(val, { type: 'direct' })
840
810
  },
811
+ dismissOnSnapToBottom,
812
+ snapPointsMode,
813
+ isKeyboardVisible,
841
814
  pauseKeyboardHandler,
842
815
  })
843
816
 
@@ -847,35 +820,12 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
847
820
  // recompute the fit anchor and fly the frame. a LIVE DOM check, NOT the
848
821
  // isKeyboardVisible React state, is required: the state lags the resize, so
849
822
  // the first shrunk onLayout lands before the flag flips. holding the measured
850
- // sizes keeps the sheet anchored; the scroll content shifts to clear the kb.
823
+ // size keeps the fit geometry stable while the frame translates with the kb.
851
824
  const ignoreLayoutForKeyboard = useEvent(
852
- () => isWeb && moveOnKeyboardChange && getWebKeyboardHeight() >= MIN_KEYBOARD_HEIGHT
853
- )
854
-
855
- // AUTOFOCUS-ON-OPEN seed gates. normally a layout measured while the keyboard
856
- // is up is dropped (it's the shrunk/collapsed sheet). but if the keyboard rose
857
- // before ANY keyboard-free baseline was captured (the autofocus race),
858
- // dropping every measurement leaves frameSize/screenSize stuck at 0 forever
859
- // and the freeze never engages.
860
- //
861
- // frame: while seeding (not settled) the scroll view is unclipped and the
862
- // tail padding suppressed, so the frame measures toward its pure pre-keyboard
863
- // content height. the first keyboard-up pass is still clipped at the consumer
864
- // maxHeight; the override only unclips it the following pass, so the frame
865
- // grows. we take the MAX and mark the seed SETTLED once it stops growing
866
- // (handled in handleAnimationViewLayout) — then later keyboard-up layouts are
867
- // dropped again so the re-enabled tail padding can't inflate the frame.
868
- const shouldSeedKbFrame = useEvent(
869
825
  () =>
870
- isWebKbSheet &&
871
- !hasCleanKbBaseline.current &&
872
- !seedSettled.current &&
873
- ignoreLayoutForKeyboard()
874
- )
875
- // screen (= maxContentSize): keep reconstructing from the stable viewport for
876
- // the whole keyboard-up-without-clean-baseline window.
877
- const shouldSeedKbScreen = useEvent(
878
- () => isWebKbSheet && !hasCleanKbBaseline.current && ignoreLayoutForKeyboard()
826
+ isWeb &&
827
+ moveOnKeyboardChange &&
828
+ getWebKeyboardResizeHeight() >= MIN_KEYBOARD_HEIGHT
879
829
  )
880
830
 
881
831
  const handleAnimationViewLayout = useEvent((e: LayoutChangeEvent) => {
@@ -884,58 +834,39 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
884
834
  return
885
835
  }
886
836
 
887
- const seeding = shouldSeedKbFrame()
888
- if (!seeding && ignoreLayoutForKeyboard()) return
837
+ const layoutHeight = e.nativeEvent?.layout.height
838
+ // drop a layout measured while the keyboard is up: on older iOS the web
839
+ // viewport shrinks and the frame would resize. keep the pre-keyboard frame
840
+ // height so the web frame translates without resizing.
841
+ // exception: if we have no frame height yet (sheet opened with the keyboard
842
+ // already up), accept it so the sheet can appear at all.
843
+ if (ignoreLayoutForKeyboard() && frameSize > 0) return
889
844
 
890
845
  // avoid bugs where it grows forever for whatever reason
891
846
  // For inline mode (non-modal), don't cap at window height - use actual layout
892
- const layoutHeight = e.nativeEvent?.layout.height
893
- // while seeding, the keyboardStableFrameHeight fallback has unclipped the
894
- // scroll view (capped at the stable screen) and the tail padding is
895
- // suppressed, so this measures the pure pre-keyboard content height — cap it
896
- // at the stable viewport like modal does.
897
- const next =
898
- modal || seeding
899
- ? Math.min(layoutHeight, getStableViewportHeight())
900
- : layoutHeight
847
+ const next = modal
848
+ ? Math.min(layoutHeight, getStableViewportHeight())
849
+ : layoutHeight
901
850
  if (!next) return
902
851
  // round: web onLayout reports sub-pixel heights (e.g. 499.99996) that jitter
903
852
  // frame to frame as the view transforms; the raw float would re-fire every
904
853
  // effect that depends on frameSize on each drag move.
905
- const rounded = Math.round(next)
906
- // seeding the frame: grow the baseline toward the unclipped content height.
907
- // the first keyboard-up pass is clipped; the scroll-view override unclips it
908
- // the next pass so it grows. once a pass doesn't exceed the running max the
909
- // content has settled — mark the seed done so the next render exits the seed
910
- // phase (freezeForKb pins this height, tail padding re-enables for scroll).
911
- if (seeding) {
912
- if (rounded > stableKbGeom.current.frame) {
913
- stableKbGeom.current.frame = rounded
914
- } else if (stableKbGeom.current.frame > 0) {
915
- seedSettled.current = true
916
- }
917
- }
918
- setFrameSize(rounded)
854
+ setFrameSize(Math.round(next))
919
855
  })
920
856
 
921
857
  const handleMaxContentViewLayout = React.useCallback(
922
858
  (e: LayoutChangeEvent) => {
923
- // same keyboard guard so screenSize (= maxContentSize) stays the full
924
- // pre-keyboard viewport except while seeding the baseline (see above),
925
- // where we use the stable layout viewport directly so screenSize lands at
926
- // the full pre-keyboard height instead of the shrunk visual viewport.
927
- if (shouldSeedKbScreen()) {
928
- setMaxContentSize(Math.round(getStableViewportHeight()))
929
- return
930
- }
931
- if (ignoreLayoutForKeyboard()) return
859
+ // keep maxContentSize at the full pre-keyboard viewport: drop layouts
860
+ // measured while the keyboard is up (the shrunk viewport), unless we have
861
+ // none yet (keyboard-already-up open).
862
+ if (ignoreLayoutForKeyboard() && screenSize > 0) return
932
863
  // avoid bugs where it grows forever for whatever reason
933
864
  const next = Math.min(e.nativeEvent?.layout.height, getStableViewportHeight())
934
865
  if (!next) return
935
866
  // round to avoid sub-pixel churn re-firing size-dependent effects
936
867
  setMaxContentSize(Math.round(next))
937
868
  },
938
- [ignoreLayoutForKeyboard, shouldSeedKbScreen]
869
+ [ignoreLayoutForKeyboard, screenSize]
939
870
  )
940
871
 
941
872
  const getAnimatedNumberStyle = React.useCallback(
@@ -988,7 +919,6 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
988
919
  keyboardOccludedHeight={keyboardOccludedHeight}
989
920
  isKeyboardVisible={isKeyboardVisible}
990
921
  keyboardStableFrameHeight={keyboardStableFrameHeight}
991
- isKeyboardSeeding={seedingKbBaseline}
992
922
  setHasScrollView={setHasScrollView}
993
923
  >
994
924
  <GestureSheetProvider
@@ -1065,8 +995,13 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
1065
995
  const adaptContext = useAdaptContext()
1066
996
  contents = (
1067
997
  <ProvideAdaptContext {...adaptContext}>
1068
- {/* @ts-ignore */}
1069
- {contents}
998
+ {/* re-propagate safe-area insets across the teleport: the sheet content
999
+ renders at the portal host, OUTSIDE the app's SafeAreaProvider, so
1000
+ without this useSafeAreaInsets() inside the sheet reads 0. */}
1001
+ <SafeAreaInsetsContext.Provider value={safeAreaInsets}>
1002
+ {/* @ts-ignore */}
1003
+ {contents}
1004
+ </SafeAreaInsetsContext.Provider>
1070
1005
  </ProvideAdaptContext>
1071
1006
  )
1072
1007
  }