@tamagui/sheet 2.0.0 → 2.1.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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/cjs/SheetImplementationCustom.cjs +73 -24
- package/dist/cjs/SheetImplementationCustom.native.js +83 -28
- package/dist/cjs/SheetImplementationCustom.native.js.map +1 -1
- package/dist/cjs/SheetScrollView.cjs +18 -4
- package/dist/cjs/SheetScrollView.native.js +18 -4
- package/dist/cjs/SheetScrollView.native.js.map +1 -1
- package/dist/cjs/nativeSheet.cjs +3 -0
- package/dist/cjs/nativeSheet.native.js +3 -0
- package/dist/cjs/nativeSheet.native.js.map +1 -1
- package/dist/cjs/useKeyboardControllerSheet.cjs +62 -6
- package/dist/cjs/useSheetProviderProps.native.js.map +1 -1
- package/dist/cjs/useSheetScrollViewGestures.cjs +6 -1
- package/dist/cjs/webViewport.cjs +50 -0
- package/dist/cjs/webViewport.native.js +54 -0
- package/dist/cjs/webViewport.native.js.map +1 -0
- package/dist/esm/SheetImplementationCustom.mjs +73 -24
- package/dist/esm/SheetImplementationCustom.mjs.map +1 -1
- package/dist/esm/SheetImplementationCustom.native.js +83 -28
- package/dist/esm/SheetImplementationCustom.native.js.map +1 -1
- package/dist/esm/SheetScrollView.mjs +19 -5
- package/dist/esm/SheetScrollView.mjs.map +1 -1
- package/dist/esm/SheetScrollView.native.js +19 -5
- package/dist/esm/SheetScrollView.native.js.map +1 -1
- package/dist/esm/nativeSheet.mjs +3 -0
- package/dist/esm/nativeSheet.mjs.map +1 -1
- package/dist/esm/nativeSheet.native.js +3 -0
- package/dist/esm/nativeSheet.native.js.map +1 -1
- package/dist/esm/useKeyboardControllerSheet.mjs +63 -7
- package/dist/esm/useKeyboardControllerSheet.mjs.map +1 -1
- package/dist/esm/useSheetProviderProps.mjs.map +1 -1
- package/dist/esm/useSheetProviderProps.native.js.map +1 -1
- package/dist/esm/useSheetScrollViewGestures.mjs +6 -1
- package/dist/esm/useSheetScrollViewGestures.mjs.map +1 -1
- package/dist/esm/webViewport.mjs +22 -0
- package/dist/esm/webViewport.mjs.map +1 -0
- package/dist/esm/webViewport.native.js +23 -0
- package/dist/esm/webViewport.native.js.map +1 -0
- package/dist/jsx/SheetImplementationCustom.mjs +73 -24
- package/dist/jsx/SheetImplementationCustom.mjs.map +1 -1
- package/dist/jsx/SheetImplementationCustom.native.js +83 -28
- package/dist/jsx/SheetImplementationCustom.native.js.map +1 -1
- package/dist/jsx/SheetScrollView.mjs +19 -5
- package/dist/jsx/SheetScrollView.mjs.map +1 -1
- package/dist/jsx/SheetScrollView.native.js +18 -4
- package/dist/jsx/SheetScrollView.native.js.map +1 -1
- package/dist/jsx/nativeSheet.mjs +3 -0
- package/dist/jsx/nativeSheet.mjs.map +1 -1
- package/dist/jsx/nativeSheet.native.js +3 -0
- package/dist/jsx/nativeSheet.native.js.map +1 -1
- package/dist/jsx/useKeyboardControllerSheet.mjs +63 -7
- package/dist/jsx/useKeyboardControllerSheet.mjs.map +1 -1
- package/dist/jsx/useSheetProviderProps.mjs.map +1 -1
- package/dist/jsx/useSheetProviderProps.native.js.map +1 -1
- package/dist/jsx/useSheetScrollViewGestures.mjs +6 -1
- package/dist/jsx/useSheetScrollViewGestures.mjs.map +1 -1
- package/dist/jsx/webViewport.mjs +22 -0
- package/dist/jsx/webViewport.mjs.map +1 -0
- package/dist/jsx/webViewport.native.js +54 -0
- package/dist/jsx/webViewport.native.js.map +1 -0
- package/package.json +20 -20
- package/src/SheetImplementationCustom.tsx +300 -56
- package/src/SheetScrollView.tsx +48 -9
- package/src/nativeSheet.tsx +3 -0
- package/src/types.tsx +11 -1
- package/src/useKeyboardControllerSheet.ts +106 -10
- package/src/useSheetProviderProps.tsx +16 -0
- package/src/useSheetScrollViewGestures.ts +23 -2
- package/src/webViewport.ts +52 -0
- package/types/SheetContext.d.ts +3 -0
- package/types/SheetContext.d.ts.map +1 -1
- package/types/SheetImplementationCustom.d.ts.map +1 -1
- package/types/SheetScrollView.d.ts.map +1 -1
- package/types/nativeSheet.d.ts.map +1 -1
- package/types/types.d.ts +4 -1
- package/types/types.d.ts.map +1 -1
- package/types/useKeyboardControllerSheet.d.ts +14 -3
- package/types/useKeyboardControllerSheet.d.ts.map +1 -1
- package/types/useSheetProviderProps.d.ts +3 -0
- package/types/useSheetProviderProps.d.ts.map +1 -1
- package/types/useSheetScrollViewGestures.d.ts.map +1 -1
- package/types/webViewport.d.ts +30 -0
- package/types/webViewport.d.ts.map +1 -0
|
@@ -26,6 +26,11 @@ import { getGestureHandlerState } from './gestureState'
|
|
|
26
26
|
import { GestureSheetProvider } from './GestureSheetContext'
|
|
27
27
|
import { resisted } from './helpers'
|
|
28
28
|
import { getKeyboardOccludedHeight } from './keyboardAvoidance'
|
|
29
|
+
import {
|
|
30
|
+
getStableLayoutViewportHeight,
|
|
31
|
+
getWebKeyboardHeight,
|
|
32
|
+
MIN_KEYBOARD_HEIGHT,
|
|
33
|
+
} from './webViewport'
|
|
29
34
|
import { SheetProvider } from './SheetContext'
|
|
30
35
|
import type { SheetProps, SnapPointsMode } from './types'
|
|
31
36
|
import { useGestureHandlerPan } from './useGestureHandlerPan'
|
|
@@ -57,6 +62,19 @@ let sheetHiddenStyleSheet: HTMLStyleElement | null = null
|
|
|
57
62
|
// on web we are always relative to window, on to screen
|
|
58
63
|
const relativeDimensionTo = isWeb ? 'window' : 'screen'
|
|
59
64
|
|
|
65
|
+
// height of the viewport the sheet positions against. on web this MUST be the
|
|
66
|
+
// stable layout viewport and NOT Dimensions.get('window') — react-native-web's
|
|
67
|
+
// Dimensions tracks visualViewport, which shrinks by the soft keyboard. capping
|
|
68
|
+
// frameSize / maxContentSize against that shrinking value corrupts the fit-mode
|
|
69
|
+
// math (translateY = screenSize - frameSize), detaching the sheet's bottom from
|
|
70
|
+
// the screen edge when the keyboard opens. NOTE: window.innerHeight is NOT
|
|
71
|
+
// stable on real iOS Safari (it shrinks with the keyboard too), so we use the
|
|
72
|
+
// self-correcting baseline from webViewport instead. see getStableLayoutViewportHeight.
|
|
73
|
+
function getStableViewportHeight(): number {
|
|
74
|
+
if (isWeb && typeof window !== 'undefined') return getStableLayoutViewportHeight()
|
|
75
|
+
return Dimensions.get(relativeDimensionTo).height
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
61
79
|
function SheetImplementationCustom(props, forwardedRef) {
|
|
62
80
|
const parentSheet = React.useContext(ParentSheetContext)
|
|
@@ -142,6 +160,19 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
142
160
|
setIsShowingInnerSheet(hasChild)
|
|
143
161
|
}, [])
|
|
144
162
|
|
|
163
|
+
// keyboard state tracking — just tracks height/visibility, no position animation.
|
|
164
|
+
// Position animation is handled via keyboard-adjusted positions below,
|
|
165
|
+
// matching the react-native-actions-sheet pattern.
|
|
166
|
+
const {
|
|
167
|
+
keyboardHeight,
|
|
168
|
+
isKeyboardVisible,
|
|
169
|
+
dismissKeyboard,
|
|
170
|
+
pauseKeyboardHandler,
|
|
171
|
+
flushPendingHide,
|
|
172
|
+
} = useKeyboardControllerSheet({
|
|
173
|
+
enabled: Boolean(moveOnKeyboardChange),
|
|
174
|
+
})
|
|
175
|
+
|
|
145
176
|
// FIX: Store stable frameSize to prevent recalculation during exit animation
|
|
146
177
|
const stableFrameSize = React.useRef(frameSize)
|
|
147
178
|
|
|
@@ -152,31 +183,84 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
152
183
|
}
|
|
153
184
|
}, [open, frameSize])
|
|
154
185
|
|
|
186
|
+
// WEB keyboard anchor freeze. on real iOS Safari opening the keyboard shrinks
|
|
187
|
+
// the visual viewport AND innerHeight AND the measured layout, which would
|
|
188
|
+
// re-derive screenSize/frameSize smaller, recompute the fit positions, and
|
|
189
|
+
// fly the frame up then back down ("goes back down after the keyboard opens").
|
|
190
|
+
// so we snapshot the pre-keyboard geometry — captured every render while the
|
|
191
|
+
// 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
|
|
197
|
+
// 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).
|
|
200
|
+
const isWebKbSheet = isWeb && hasFit && moveOnKeyboardChange
|
|
201
|
+
|
|
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
|
+
|
|
155
248
|
// use stableFrameSize when closing to prevent position jumps during exit animation
|
|
156
249
|
// but when opening, always use the current frameSize so positions update correctly
|
|
157
|
-
const effectiveFrameSize =
|
|
250
|
+
const effectiveFrameSize = freezeForKb
|
|
251
|
+
? stableKbGeom.current.frame
|
|
252
|
+
: open
|
|
253
|
+
? frameSize
|
|
254
|
+
: stableFrameSize.current || frameSize
|
|
158
255
|
|
|
159
256
|
const positions = React.useMemo(
|
|
160
257
|
() =>
|
|
161
258
|
snapPoints.map((point) =>
|
|
162
|
-
getYPositions(snapPointsMode, point,
|
|
259
|
+
getYPositions(snapPointsMode, point, effScreenSize, effectiveFrameSize)
|
|
163
260
|
),
|
|
164
|
-
[
|
|
261
|
+
[effScreenSize, effectiveFrameSize, snapPoints, snapPointsMode]
|
|
165
262
|
)
|
|
166
263
|
|
|
167
|
-
// keyboard state tracking — just tracks height/visibility, no position animation.
|
|
168
|
-
// Position animation is handled via keyboard-adjusted positions below,
|
|
169
|
-
// matching the react-native-actions-sheet pattern.
|
|
170
|
-
const {
|
|
171
|
-
keyboardHeight,
|
|
172
|
-
isKeyboardVisible,
|
|
173
|
-
dismissKeyboard,
|
|
174
|
-
pauseKeyboardHandler,
|
|
175
|
-
flushPendingHide,
|
|
176
|
-
} = useKeyboardControllerSheet({
|
|
177
|
-
enabled: !isWeb && Boolean(moveOnKeyboardChange),
|
|
178
|
-
})
|
|
179
|
-
|
|
180
264
|
const [isDragging, setIsDragging_] = React.useState(false)
|
|
181
265
|
|
|
182
266
|
// synchronous dragging ref — set BEFORE async state commits.
|
|
@@ -198,28 +282,32 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
198
282
|
[pauseKeyboardHandler, flushPendingHide]
|
|
199
283
|
)
|
|
200
284
|
|
|
201
|
-
// keyboard-adjusted
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
285
|
+
// keyboard-adjusted snap positions.
|
|
286
|
+
//
|
|
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.
|
|
206
295
|
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
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.
|
|
298
|
+
//
|
|
299
|
+
// IMPORTANT: frozen during drag to prevent gesture handler recreation —
|
|
300
|
+
// when a TextInput blurs mid-drag the keyboard state would otherwise revert
|
|
301
|
+
// and recreate the gesture useMemo, cancelling the active drag.
|
|
211
302
|
const activePositionsRef = React.useRef(positions)
|
|
212
303
|
const activePositions = React.useMemo(() => {
|
|
213
|
-
// during drag, return frozen positions to prevent gesture handler recreation.
|
|
214
|
-
// check both state (for re-render trigger) and ref (for synchronous check
|
|
215
|
-
// when keyboard hide event fires before isDragging state commits)
|
|
216
304
|
if (isDragging || isDraggingRef.current) return activePositionsRef.current
|
|
217
305
|
|
|
218
306
|
let result: number[]
|
|
219
|
-
if (!isKeyboardVisible || keyboardHeight <= 0) {
|
|
307
|
+
if (isWeb || !isKeyboardVisible || keyboardHeight <= 0) {
|
|
220
308
|
result = positions
|
|
221
309
|
} else {
|
|
222
|
-
const safeAreaTop =
|
|
310
|
+
const safeAreaTop = getSafeAreaTopInset()
|
|
223
311
|
result = positions.map((p) => {
|
|
224
312
|
// don't adjust the off-screen/close position (from dismissOnSnapToBottom's 0% snap)
|
|
225
313
|
// — it must stay at screenSize so the user can drag between real snap points
|
|
@@ -232,13 +320,39 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
232
320
|
return result
|
|
233
321
|
}, [positions, isKeyboardVisible, keyboardHeight, screenSize, isDragging])
|
|
234
322
|
|
|
235
|
-
const keyboardOccludedHeight =
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
})
|
|
335
|
+
|
|
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
|
|
242
356
|
|
|
243
357
|
const { useAnimatedNumber, useAnimatedNumberStyle, useAnimatedNumberReaction } =
|
|
244
358
|
animationDriver
|
|
@@ -325,7 +439,11 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
325
439
|
const animateTo = useEvent((position: number, animationOverride?: any) => {
|
|
326
440
|
if (frameSize === 0) return
|
|
327
441
|
|
|
328
|
-
|
|
442
|
+
// use effScreenSize (the frozen anchor space the positions were built in) for
|
|
443
|
+
// the off-screen/close target too, so a close while the keyboard is still up
|
|
444
|
+
// animates fully out instead of to a mismatched live screenSize.
|
|
445
|
+
let toValue =
|
|
446
|
+
isHidden || position === -1 ? effScreenSize : activePositions[position]
|
|
329
447
|
|
|
330
448
|
if (at.current === toValue) return
|
|
331
449
|
|
|
@@ -417,6 +535,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
417
535
|
return
|
|
418
536
|
}
|
|
419
537
|
|
|
538
|
+
// never fight an active drag: the gesture owns the animated position. on
|
|
539
|
+
// web the AnimatedView's onLayout re-fires with sub-pixel jitter as the
|
|
540
|
+
// frame translates (frameSize 499.99996 <-> 500.00003), and since frameSize
|
|
541
|
+
// is a dep of this effect that would re-run it mid-pull and snap the sheet
|
|
542
|
+
// back to its resting snap point. read the live ref so drag-end (which the
|
|
543
|
+
// reconcile-after-drag effect handles) isn't gated by stale deps.
|
|
544
|
+
if (isDraggingRef.current) {
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
|
|
420
548
|
if (!frameSize || !screenSize || isHidden || (hasntMeasured && !open)) {
|
|
421
549
|
return
|
|
422
550
|
}
|
|
@@ -454,7 +582,11 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
454
582
|
if (!frameSize) return
|
|
455
583
|
if (isShowingInnerSheet) return
|
|
456
584
|
|
|
457
|
-
|
|
585
|
+
// use keyboard-adjusted positions (matches the RNGH path): when the
|
|
586
|
+
// keyboard is open the sheet sits at activePositions[0], so clamping drags
|
|
587
|
+
// against the un-adjusted positions[0] would rubber-band the sheet down to
|
|
588
|
+
// near the bottom on any drag.
|
|
589
|
+
const minY = activePositions[0]
|
|
458
590
|
scrollBridge.paneMinY = minY
|
|
459
591
|
let startY = at.current
|
|
460
592
|
|
|
@@ -497,8 +629,8 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
497
629
|
let closestPoint = 0
|
|
498
630
|
let dist = Number.POSITIVE_INFINITY
|
|
499
631
|
|
|
500
|
-
for (let i = 0; i <
|
|
501
|
-
const position =
|
|
632
|
+
for (let i = 0; i < activePositions.length; i++) {
|
|
633
|
+
const position = activePositions[i]
|
|
502
634
|
const curDist = end > position ? end - position : position - end
|
|
503
635
|
if (curDist < dist) {
|
|
504
636
|
dist = curDist
|
|
@@ -530,6 +662,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
530
662
|
return true
|
|
531
663
|
}
|
|
532
664
|
|
|
665
|
+
// touch is on the ScrollView node — the web scroll-view gesture hook
|
|
666
|
+
// owns it and drives drag/release through scrollBridge directly (it
|
|
667
|
+
// re-baselines via scrollBridge.startPanDrag on each pan handoff). if
|
|
668
|
+
// we also granted here, RNW's PanResponder would set the animated
|
|
669
|
+
// position from a second, differently-based offset every move and the
|
|
670
|
+
// sheet would jitter/jump. defer entirely to the hook.
|
|
671
|
+
if (scrollBridge.scrollNodeTouched) {
|
|
672
|
+
return false
|
|
673
|
+
}
|
|
674
|
+
|
|
533
675
|
if (scrollBridge.hasScrollableContent === true) {
|
|
534
676
|
if (scrollBridge.scrollLock) {
|
|
535
677
|
return false
|
|
@@ -575,6 +717,17 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
575
717
|
|
|
576
718
|
let isExternalDrag = false
|
|
577
719
|
|
|
720
|
+
// re-baseline a pan drag to the current animated position. the web
|
|
721
|
+
// scroll-view hook calls this on every transition INTO pan ownership
|
|
722
|
+
// (including handoffs back from scroll), so its panDragOffset — which it
|
|
723
|
+
// resets to 0 at each pan entry — is measured from where the sheet
|
|
724
|
+
// actually is now, not from where the gesture first grabbed it. without
|
|
725
|
+
// this the sheet would jump to a stale origin on a scroll→pan handoff.
|
|
726
|
+
scrollBridge.startPanDrag = () => {
|
|
727
|
+
isExternalDrag = true
|
|
728
|
+
grant()
|
|
729
|
+
}
|
|
730
|
+
|
|
578
731
|
scrollBridge.drag = (dy) => {
|
|
579
732
|
if (!isExternalDrag) {
|
|
580
733
|
isExternalDrag = true
|
|
@@ -616,10 +769,22 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
616
769
|
onPanResponderTerminate: finish,
|
|
617
770
|
onPanResponderRelease: finish,
|
|
618
771
|
})
|
|
619
|
-
}, [
|
|
772
|
+
}, [
|
|
773
|
+
disableDrag,
|
|
774
|
+
isShowingInnerSheet,
|
|
775
|
+
animateTo,
|
|
776
|
+
frameSize,
|
|
777
|
+
activePositions,
|
|
778
|
+
setPosition,
|
|
779
|
+
])
|
|
620
780
|
|
|
621
|
-
// animate to keyboard-adjusted position when keyboard state changes
|
|
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.
|
|
622
786
|
React.useEffect(() => {
|
|
787
|
+
if (isWeb) return
|
|
623
788
|
if (isDragging || isHidden || !open || disableAnimation) return
|
|
624
789
|
if (!frameSize || !screenSize) return
|
|
625
790
|
// use timing animation to match iOS keyboard animation (~250ms)
|
|
@@ -647,6 +812,11 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
647
812
|
React.useEffect(() => {
|
|
648
813
|
if (!open && isKeyboardVisible) {
|
|
649
814
|
dismissKeyboard()
|
|
815
|
+
// if the sheet was closed mid-drag the keyboard-hide handler was paused
|
|
816
|
+
// and a hide could be left pending — clear it so isKeyboardVisible can't
|
|
817
|
+
// stick true after the sheet is gone. (no-op for a normal close.)
|
|
818
|
+
pauseKeyboardHandler.current = false
|
|
819
|
+
flushPendingHide()
|
|
650
820
|
}
|
|
651
821
|
}, [open])
|
|
652
822
|
|
|
@@ -671,31 +841,102 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
671
841
|
pauseKeyboardHandler,
|
|
672
842
|
})
|
|
673
843
|
|
|
844
|
+
// ignore any layout measured while the soft keyboard is up (web +
|
|
845
|
+
// moveOnKeyboardChange): the visual viewport (which RN's web layout follows)
|
|
846
|
+
// shrinks, so the height would be the collapsed sheet — keeping it would
|
|
847
|
+
// recompute the fit anchor and fly the frame. a LIVE DOM check, NOT the
|
|
848
|
+
// isKeyboardVisible React state, is required: the state lags the resize, so
|
|
849
|
+
// 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.
|
|
851
|
+
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
|
+
() =>
|
|
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()
|
|
879
|
+
)
|
|
880
|
+
|
|
674
881
|
const handleAnimationViewLayout = useEvent((e: LayoutChangeEvent) => {
|
|
675
882
|
// don't update frameSize during exit animation to prevent position jumps
|
|
676
883
|
if (!open && stableFrameSize.current !== 0) {
|
|
677
884
|
return
|
|
678
885
|
}
|
|
679
886
|
|
|
887
|
+
const seeding = shouldSeedKbFrame()
|
|
888
|
+
if (!seeding && ignoreLayoutForKeyboard()) return
|
|
889
|
+
|
|
680
890
|
// avoid bugs where it grows forever for whatever reason
|
|
681
891
|
// For inline mode (non-modal), don't cap at window height - use actual layout
|
|
682
892
|
const layoutHeight = e.nativeEvent?.layout.height
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
|
686
901
|
if (!next) return
|
|
687
|
-
|
|
902
|
+
// round: web onLayout reports sub-pixel heights (e.g. 499.99996) that jitter
|
|
903
|
+
// frame to frame as the view transforms; the raw float would re-fire every
|
|
904
|
+
// 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)
|
|
688
919
|
})
|
|
689
920
|
|
|
690
|
-
const handleMaxContentViewLayout = React.useCallback(
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
921
|
+
const handleMaxContentViewLayout = React.useCallback(
|
|
922
|
+
(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
|
|
932
|
+
// avoid bugs where it grows forever for whatever reason
|
|
933
|
+
const next = Math.min(e.nativeEvent?.layout.height, getStableViewportHeight())
|
|
934
|
+
if (!next) return
|
|
935
|
+
// round to avoid sub-pixel churn re-firing size-dependent effects
|
|
936
|
+
setMaxContentSize(Math.round(next))
|
|
937
|
+
},
|
|
938
|
+
[ignoreLayoutForKeyboard, shouldSeedKbScreen]
|
|
939
|
+
)
|
|
699
940
|
|
|
700
941
|
const getAnimatedNumberStyle = React.useCallback(
|
|
701
942
|
(val: number) => {
|
|
@@ -745,6 +986,9 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
745
986
|
<SheetProvider
|
|
746
987
|
{...providerProps}
|
|
747
988
|
keyboardOccludedHeight={keyboardOccludedHeight}
|
|
989
|
+
isKeyboardVisible={isKeyboardVisible}
|
|
990
|
+
keyboardStableFrameHeight={keyboardStableFrameHeight}
|
|
991
|
+
isKeyboardSeeding={seedingKbBaseline}
|
|
748
992
|
setHasScrollView={setHasScrollView}
|
|
749
993
|
>
|
|
750
994
|
<GestureSheetProvider
|
package/src/SheetScrollView.tsx
CHANGED
|
@@ -10,6 +10,7 @@ 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 { getWebKeyboardHeight, MIN_KEYBOARD_HEIGHT } from './webViewport'
|
|
13
14
|
|
|
14
15
|
const SHEET_SCROLL_VIEW_NAME = 'SheetScrollView'
|
|
15
16
|
|
|
@@ -31,6 +32,14 @@ export const SheetScrollView = React.forwardRef<
|
|
|
31
32
|
const gestureContext = useGestureSheetContext()
|
|
32
33
|
const { scrollBridge, setHasScrollView, hasFit, screenSize } = context
|
|
33
34
|
const keyboardOccludedHeight = Math.max(0, context.keyboardOccludedHeight || 0)
|
|
35
|
+
// OR a LIVE DOM check: context.isKeyboardVisible (React state) lags the
|
|
36
|
+
// viewport resize, so on the open-transition render this component can re-run
|
|
37
|
+
// with the shrunk consumer maxHeight BEFORE the context flag flips. reading
|
|
38
|
+
// the keyboard height straight from visualViewport closes that race so the
|
|
39
|
+
// height freeze engages on the same render that would otherwise collapse it.
|
|
40
|
+
const isKeyboardVisible =
|
|
41
|
+
context.isKeyboardVisible === true ||
|
|
42
|
+
(isWeb && getWebKeyboardHeight() >= MIN_KEYBOARD_HEIGHT)
|
|
34
43
|
const [scrollEnabled] = useControllableState({
|
|
35
44
|
prop: scrollEnabledProp,
|
|
36
45
|
defaultProp: true,
|
|
@@ -40,25 +49,47 @@ export const SheetScrollView = React.forwardRef<
|
|
|
40
49
|
const [hasScrollableContent, setHasScrollableContent] = useState(true)
|
|
41
50
|
const parentHeight = useRef(0)
|
|
42
51
|
const contentHeight = useRef(0)
|
|
52
|
+
// the sheet's authoritative pre-keyboard frame height (see SheetImpl). a
|
|
53
|
+
// scroll-view-local high-water mark used to live here, but it was unreliable
|
|
54
|
+
// (the ref could read 0 if the view remounted on focus / never laid out while
|
|
55
|
+
// closed), so the height now comes from the sheet, which doesn't remount.
|
|
56
|
+
const frozenFrameHeight = Math.max(0, context.keyboardStableFrameHeight || 0)
|
|
43
57
|
|
|
44
58
|
// with snapPointsMode="fit", Frame is content-sized (flex: 0, flex-basis: auto, height: undefined).
|
|
45
59
|
// a flex: 1 child can't grow inside a content-sized parent, so the ScrollView (and the Frame
|
|
46
60
|
// around it) collapse to 0 height. instead, let the ScrollView size to its content and cap it
|
|
47
61
|
// at the available viewport (screenSize / maxContentSize) so scrolling kicks in for tall content.
|
|
48
|
-
// when the keyboard forces the sheet against the top safe area, preserve the measured viewport
|
|
49
|
-
// height while adding scrollable tail padding so content can move above the keyboard.
|
|
50
|
-
const keyboardFrozenHeight =
|
|
51
|
-
hasFit && keyboardOccludedHeight > 0 && parentHeight.current
|
|
52
|
-
? parentHeight.current
|
|
53
|
-
: undefined
|
|
54
62
|
const fitSizingStyle = hasFit
|
|
55
63
|
? {
|
|
56
64
|
flex: undefined as undefined,
|
|
57
|
-
height:
|
|
65
|
+
height: undefined as undefined,
|
|
58
66
|
maxHeight: screenSize || undefined,
|
|
59
67
|
}
|
|
60
68
|
: { flex: 1 }
|
|
61
69
|
|
|
70
|
+
// AUTOFOCUS-ON-OPEN seed (web): the sheet is still reconstructing its
|
|
71
|
+
// pre-keyboard frame baseline and needs THIS scroll view to size to its
|
|
72
|
+
// content so it can measure the true content height. so we apply the stable
|
|
73
|
+
// screen only as a maxHeight cap (UNCLIP from the shrunk consumer maxHeight)
|
|
74
|
+
// and leave height undefined so it stays content-sized.
|
|
75
|
+
const isKeyboardSeeding = context.isKeyboardSeeding === true
|
|
76
|
+
|
|
77
|
+
// when the keyboard is open the sheet stays ANCHORED at the bottom and keeps
|
|
78
|
+
// its full pre-keyboard height — the keyboard overlays its lower part and the
|
|
79
|
+
// keyboardOccludedHeight tail padding (added to the scroll content below) +
|
|
80
|
+
// browser scroll-into-view lift the focused input above the keyboard. so we
|
|
81
|
+
// pin the height to the sheet's authoritative frozenFrameHeight, overriding
|
|
82
|
+
// any consumer maxHeight (on web that's often tied to useWindowDimensions,
|
|
83
|
+
// which SHRINKS when the keyboard opens and would otherwise collapse the
|
|
84
|
+
// sheet). holding the height constant means nothing animates on keyboard
|
|
85
|
+
// open/close — no jump/teleport. applied AFTER {...props} so it wins.
|
|
86
|
+
const keyboardFrozenOverride =
|
|
87
|
+
hasFit && isKeyboardVisible && frozenFrameHeight > 0
|
|
88
|
+
? isKeyboardSeeding
|
|
89
|
+
? { maxHeight: frozenFrameHeight }
|
|
90
|
+
: { height: frozenFrameHeight, maxHeight: frozenFrameHeight }
|
|
91
|
+
: null
|
|
92
|
+
|
|
62
93
|
const panGestureRef = gestureContext?.panGestureRef
|
|
63
94
|
const { ScrollView: RNGHScrollView } = getGestureHandlerState()
|
|
64
95
|
const useRNGHScrollView = isGestureHandlerEnabled() && RNGHScrollView && panGestureRef
|
|
@@ -102,6 +133,12 @@ export const SheetScrollView = React.forwardRef<
|
|
|
102
133
|
}
|
|
103
134
|
}
|
|
104
135
|
|
|
136
|
+
// track the fit height for the scrollable-content check. the keyboard-freeze
|
|
137
|
+
// height is supplied by the sheet (frozenFrameHeight), not derived here.
|
|
138
|
+
const recordFitHeight = (height: number) => {
|
|
139
|
+
parentHeight.current = height
|
|
140
|
+
}
|
|
141
|
+
|
|
105
142
|
useEffect(() => {
|
|
106
143
|
scrollBridge.hasScrollableContent = hasScrollableContent
|
|
107
144
|
}, [hasScrollableContent])
|
|
@@ -148,7 +185,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
148
185
|
scrollEnabled={scrollEnabled}
|
|
149
186
|
simultaneousHandlers={[panGestureRef]}
|
|
150
187
|
onLayout={(e: any) => {
|
|
151
|
-
|
|
188
|
+
recordFitHeight(Math.ceil(e.nativeEvent.layout.height))
|
|
152
189
|
updateScrollable()
|
|
153
190
|
}}
|
|
154
191
|
onScroll={(e: any) => {
|
|
@@ -186,6 +223,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
186
223
|
keyboardShouldPersistTaps="always"
|
|
187
224
|
keyboardDismissMode="none"
|
|
188
225
|
{...props}
|
|
226
|
+
{...keyboardFrozenOverride}
|
|
189
227
|
>
|
|
190
228
|
{contentWrapper}
|
|
191
229
|
</RNGHComponent>
|
|
@@ -196,7 +234,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
196
234
|
return (
|
|
197
235
|
<ScrollView
|
|
198
236
|
onLayout={(e) => {
|
|
199
|
-
|
|
237
|
+
recordFitHeight(Math.ceil(e.nativeEvent.layout.height))
|
|
200
238
|
updateScrollable()
|
|
201
239
|
}}
|
|
202
240
|
ref={composeRefs(scrollRef as any, ref)}
|
|
@@ -213,6 +251,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
213
251
|
contentContainerStyle={{ minHeight: '100%' }}
|
|
214
252
|
{...gestureProps}
|
|
215
253
|
{...props}
|
|
254
|
+
{...keyboardFrozenOverride}
|
|
216
255
|
>
|
|
217
256
|
{contentWrapper}
|
|
218
257
|
</ScrollView>
|
package/src/nativeSheet.tsx
CHANGED
package/src/types.tsx
CHANGED
|
@@ -75,7 +75,8 @@ export type SheetProps = ScopedProps<
|
|
|
75
75
|
zIndex?: number
|
|
76
76
|
portalProps?: PortalProps
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
78
|
+
* Makes the sheet move up when the mobile keyboard opens so the focused input remains visible.
|
|
79
|
+
* Works on native (via keyboard events) and on mobile web (via the VisualViewport API).
|
|
79
80
|
*/
|
|
80
81
|
moveOnKeyboardChange?: boolean
|
|
81
82
|
containerComponent?: React.ComponentType<any>
|
|
@@ -131,6 +132,15 @@ export type ScrollBridge = {
|
|
|
131
132
|
isAtTop?: boolean
|
|
132
133
|
// snap sheet to a specific position (for handoff UP)
|
|
133
134
|
snapToPosition?: (positionIndex: number) => void
|
|
135
|
+
// re-baseline the pan drag origin to the current animated position. the web
|
|
136
|
+
// scroll-view hook calls this on each transition into pan ownership so a
|
|
137
|
+
// scroll→pan handoff resumes from where the sheet is, not a stale origin.
|
|
138
|
+
startPanDrag?: () => void
|
|
139
|
+
// web only: true while a touch is active on the ScrollView node. The web
|
|
140
|
+
// scroll-view gesture hook owns drag detection for those touches (it calls
|
|
141
|
+
// drag/release on this bridge), so the PanResponder must NOT also grant for
|
|
142
|
+
// them — otherwise two systems drive the animated position and it jitters.
|
|
143
|
+
scrollNodeTouched?: boolean
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
// keyboard controller sheet types
|