@tamagui/sheet 2.0.0 → 2.1.0-1780182832733
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 +46 -22
- package/dist/cjs/SheetImplementationCustom.native.js +58 -27
- package/dist/cjs/SheetImplementationCustom.native.js.map +1 -1
- package/dist/cjs/SheetScrollView.cjs +15 -4
- package/dist/cjs/SheetScrollView.native.js +15 -4
- package/dist/cjs/SheetScrollView.native.js.map +1 -1
- package/dist/cjs/createSheet.cjs +5 -2
- package/dist/cjs/createSheet.native.js +5 -2
- package/dist/cjs/createSheet.native.js.map +1 -1
- package/dist/cjs/nativeSheet.cjs +2 -0
- package/dist/cjs/nativeSheet.native.js +2 -0
- package/dist/cjs/nativeSheet.native.js.map +1 -1
- package/dist/cjs/useKeyboardControllerSheet.cjs +65 -6
- package/dist/cjs/useSheetProviderProps.native.js.map +1 -1
- package/dist/cjs/useSheetScrollViewGestures.cjs +6 -1
- package/dist/cjs/webViewport.cjs +58 -0
- package/dist/cjs/webViewport.native.js +63 -0
- package/dist/cjs/webViewport.native.js.map +1 -0
- package/dist/esm/SheetImplementationCustom.mjs +46 -22
- package/dist/esm/SheetImplementationCustom.mjs.map +1 -1
- package/dist/esm/SheetImplementationCustom.native.js +58 -27
- package/dist/esm/SheetImplementationCustom.native.js.map +1 -1
- package/dist/esm/SheetScrollView.mjs +16 -5
- package/dist/esm/SheetScrollView.mjs.map +1 -1
- package/dist/esm/SheetScrollView.native.js +16 -5
- package/dist/esm/SheetScrollView.native.js.map +1 -1
- package/dist/esm/createSheet.mjs +6 -3
- package/dist/esm/createSheet.mjs.map +1 -1
- package/dist/esm/createSheet.native.js +6 -3
- package/dist/esm/createSheet.native.js.map +1 -1
- package/dist/esm/nativeSheet.mjs +2 -0
- package/dist/esm/nativeSheet.mjs.map +1 -1
- package/dist/esm/nativeSheet.native.js +2 -0
- package/dist/esm/nativeSheet.native.js.map +1 -1
- package/dist/esm/useKeyboardControllerSheet.mjs +66 -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 +29 -0
- package/dist/esm/webViewport.mjs.map +1 -0
- package/dist/esm/webViewport.native.js +31 -0
- package/dist/esm/webViewport.native.js.map +1 -0
- package/dist/jsx/SheetImplementationCustom.mjs +46 -22
- package/dist/jsx/SheetImplementationCustom.mjs.map +1 -1
- package/dist/jsx/SheetImplementationCustom.native.js +58 -27
- package/dist/jsx/SheetImplementationCustom.native.js.map +1 -1
- package/dist/jsx/SheetScrollView.mjs +16 -5
- package/dist/jsx/SheetScrollView.mjs.map +1 -1
- package/dist/jsx/SheetScrollView.native.js +15 -4
- package/dist/jsx/SheetScrollView.native.js.map +1 -1
- package/dist/jsx/createSheet.mjs +6 -3
- package/dist/jsx/createSheet.mjs.map +1 -1
- package/dist/jsx/createSheet.native.js +5 -2
- package/dist/jsx/createSheet.native.js.map +1 -1
- package/dist/jsx/nativeSheet.mjs +2 -0
- package/dist/jsx/nativeSheet.mjs.map +1 -1
- package/dist/jsx/nativeSheet.native.js +2 -0
- package/dist/jsx/nativeSheet.native.js.map +1 -1
- package/dist/jsx/useKeyboardControllerSheet.mjs +66 -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 +29 -0
- package/dist/jsx/webViewport.mjs.map +1 -0
- package/dist/jsx/webViewport.native.js +63 -0
- package/dist/jsx/webViewport.native.js.map +1 -0
- package/package.json +20 -20
- package/src/SheetImplementationCustom.tsx +207 -53
- package/src/SheetScrollView.tsx +36 -9
- package/src/createSheet.tsx +18 -6
- package/src/nativeSheet.tsx +2 -0
- package/src/types.tsx +11 -1
- package/src/useKeyboardControllerSheet.ts +123 -10
- package/src/useSheetProviderProps.tsx +10 -0
- package/src/useSheetScrollViewGestures.ts +23 -2
- package/src/webViewport.ts +81 -0
- package/types/SheetContext.d.ts +2 -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/createSheet.d.ts +12 -12
- package/types/createSheet.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 +2 -0
- package/types/useSheetProviderProps.d.ts.map +1 -1
- package/types/useSheetScrollViewGestures.d.ts.map +1 -1
- package/types/webViewport.d.ts +31 -0
- package/types/webViewport.d.ts.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamagui/sheet",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0-1780182832733",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"source": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -56,27 +56,27 @@
|
|
|
56
56
|
"clean": "tamagui-build clean"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@tamagui/adapt": "2.
|
|
60
|
-
"@tamagui/animate-presence": "2.
|
|
61
|
-
"@tamagui/animations-react-native": "2.
|
|
62
|
-
"@tamagui/compose-refs": "2.
|
|
63
|
-
"@tamagui/constants": "2.
|
|
64
|
-
"@tamagui/core": "2.
|
|
65
|
-
"@tamagui/create-context": "2.
|
|
66
|
-
"@tamagui/helpers": "2.
|
|
67
|
-
"@tamagui/native": "2.
|
|
68
|
-
"@tamagui/portal": "2.
|
|
69
|
-
"@tamagui/remove-scroll": "2.
|
|
70
|
-
"@tamagui/scroll-view": "2.
|
|
71
|
-
"@tamagui/stacks": "2.
|
|
72
|
-
"@tamagui/use-constant": "2.
|
|
73
|
-
"@tamagui/use-controllable-state": "2.
|
|
74
|
-
"@tamagui/use-did-finish-ssr": "2.
|
|
75
|
-
"@tamagui/use-keyboard-visible": "2.
|
|
76
|
-
"@tamagui/z-index-stack": "2.
|
|
59
|
+
"@tamagui/adapt": "2.1.0-1780182832733",
|
|
60
|
+
"@tamagui/animate-presence": "2.1.0-1780182832733",
|
|
61
|
+
"@tamagui/animations-react-native": "2.1.0-1780182832733",
|
|
62
|
+
"@tamagui/compose-refs": "2.1.0-1780182832733",
|
|
63
|
+
"@tamagui/constants": "2.1.0-1780182832733",
|
|
64
|
+
"@tamagui/core": "2.1.0-1780182832733",
|
|
65
|
+
"@tamagui/create-context": "2.1.0-1780182832733",
|
|
66
|
+
"@tamagui/helpers": "2.1.0-1780182832733",
|
|
67
|
+
"@tamagui/native": "2.1.0-1780182832733",
|
|
68
|
+
"@tamagui/portal": "2.1.0-1780182832733",
|
|
69
|
+
"@tamagui/remove-scroll": "2.1.0-1780182832733",
|
|
70
|
+
"@tamagui/scroll-view": "2.1.0-1780182832733",
|
|
71
|
+
"@tamagui/stacks": "2.1.0-1780182832733",
|
|
72
|
+
"@tamagui/use-constant": "2.1.0-1780182832733",
|
|
73
|
+
"@tamagui/use-controllable-state": "2.1.0-1780182832733",
|
|
74
|
+
"@tamagui/use-did-finish-ssr": "2.1.0-1780182832733",
|
|
75
|
+
"@tamagui/use-keyboard-visible": "2.1.0-1780182832733",
|
|
76
|
+
"@tamagui/z-index-stack": "2.1.0-1780182832733"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@tamagui/build": "2.
|
|
79
|
+
"@tamagui/build": "2.1.0-1780182832733",
|
|
80
80
|
"react": ">=19",
|
|
81
81
|
"react-native": "0.83.2",
|
|
82
82
|
"react-native-gesture-handler": "~2.30.0"
|
|
@@ -26,6 +26,12 @@ 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
|
+
getMaxViewportHeight,
|
|
31
|
+
getStableLayoutViewportHeight,
|
|
32
|
+
getWebKeyboardHeight,
|
|
33
|
+
MIN_KEYBOARD_HEIGHT,
|
|
34
|
+
} from './webViewport'
|
|
29
35
|
import { SheetProvider } from './SheetContext'
|
|
30
36
|
import type { SheetProps, SnapPointsMode } from './types'
|
|
31
37
|
import { useGestureHandlerPan } from './useGestureHandlerPan'
|
|
@@ -57,6 +63,19 @@ let sheetHiddenStyleSheet: HTMLStyleElement | null = null
|
|
|
57
63
|
// on web we are always relative to window, on to screen
|
|
58
64
|
const relativeDimensionTo = isWeb ? 'window' : 'screen'
|
|
59
65
|
|
|
66
|
+
// height of the viewport the sheet positions against. on web this MUST be the
|
|
67
|
+
// stable layout viewport and NOT Dimensions.get('window') — react-native-web's
|
|
68
|
+
// Dimensions tracks visualViewport, which shrinks by the soft keyboard. capping
|
|
69
|
+
// frameSize / maxContentSize against that shrinking value corrupts the fit-mode
|
|
70
|
+
// math (translateY = screenSize - frameSize), detaching the sheet's bottom from
|
|
71
|
+
// the screen edge when the keyboard opens. NOTE: window.innerHeight is NOT
|
|
72
|
+
// stable on real iOS Safari (it shrinks with the keyboard too), so we use the
|
|
73
|
+
// self-correcting baseline from webViewport instead. see getStableLayoutViewportHeight.
|
|
74
|
+
function getStableViewportHeight(): number {
|
|
75
|
+
if (isWeb && typeof window !== 'undefined') return getStableLayoutViewportHeight()
|
|
76
|
+
return Dimensions.get(relativeDimensionTo).height
|
|
77
|
+
}
|
|
78
|
+
|
|
60
79
|
export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
61
80
|
function SheetImplementationCustom(props, forwardedRef) {
|
|
62
81
|
const parentSheet = React.useContext(ParentSheetContext)
|
|
@@ -142,6 +161,19 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
142
161
|
setIsShowingInnerSheet(hasChild)
|
|
143
162
|
}, [])
|
|
144
163
|
|
|
164
|
+
// keyboard state tracking — just tracks height/visibility, no position animation.
|
|
165
|
+
// Position animation is handled via keyboard-adjusted positions below,
|
|
166
|
+
// matching the react-native-actions-sheet pattern.
|
|
167
|
+
const {
|
|
168
|
+
keyboardHeight,
|
|
169
|
+
isKeyboardVisible,
|
|
170
|
+
dismissKeyboard,
|
|
171
|
+
pauseKeyboardHandler,
|
|
172
|
+
flushPendingHide,
|
|
173
|
+
} = useKeyboardControllerSheet({
|
|
174
|
+
enabled: Boolean(moveOnKeyboardChange),
|
|
175
|
+
})
|
|
176
|
+
|
|
145
177
|
// FIX: Store stable frameSize to prevent recalculation during exit animation
|
|
146
178
|
const stableFrameSize = React.useRef(frameSize)
|
|
147
179
|
|
|
@@ -152,31 +184,42 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
152
184
|
}
|
|
153
185
|
}, [open, frameSize])
|
|
154
186
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
187
|
+
// WEB keyboard anchor freeze. on real iOS Safari opening the keyboard shrinks
|
|
188
|
+
// the visual viewport AND innerHeight AND the measured layout, which would
|
|
189
|
+
// re-derive screenSize/frameSize smaller, recompute the fit positions, and
|
|
190
|
+
// fly the frame up then back down ("goes back down after the keyboard opens").
|
|
191
|
+
// so we snapshot the pre-keyboard geometry — captured every render while the
|
|
192
|
+
// keyboard is CLOSED, which dodges the open-transition race where a shrunk
|
|
193
|
+
// onLayout lands before isKeyboardVisible flips — and use it for the anchor
|
|
194
|
+
// math while the keyboard is open. the sheet then stays put; only the scroll
|
|
195
|
+
// content shifts to clear the keyboard (keyboardOccludedHeight padding below
|
|
196
|
+
// + SheetScrollView's frozen height).
|
|
197
|
+
// this sheet is the kind the web keyboard anchor freeze is designed for — a
|
|
198
|
+
// fit-mode web sheet opted into keyboard handling. percent/constant sheets
|
|
199
|
+
// keep the live geometry (their height isn't pinned, so a frozen anchor
|
|
200
|
+
// would mismatch).
|
|
201
|
+
const isWebKbSheet = isWeb && hasFit && moveOnKeyboardChange
|
|
202
|
+
|
|
203
|
+
// the space the snap positions are built against. WEB: the stable layout
|
|
204
|
+
// viewport (document.documentElement.clientHeight), which the soft keyboard
|
|
205
|
+
// never shrinks (unlike the measured screenSize / visualViewport). NATIVE:
|
|
206
|
+
// the measured screenSize. positions are then shifted UP by keyboardHeight
|
|
207
|
+
// when the keyboard opens (activePositions, below) — the whole device-height
|
|
208
|
+
// frame slides up so its content clears the keyboard, capped at the safe area.
|
|
209
|
+
const effScreenSize = isWebKbSheet ? getStableViewportHeight() : screenSize
|
|
210
|
+
|
|
211
|
+
// use stableFrameSize when closing to prevent position jumps during the exit
|
|
212
|
+
// animation; while open use the live frameSize.
|
|
157
213
|
const effectiveFrameSize = open ? frameSize : stableFrameSize.current || frameSize
|
|
158
214
|
|
|
159
215
|
const positions = React.useMemo(
|
|
160
216
|
() =>
|
|
161
217
|
snapPoints.map((point) =>
|
|
162
|
-
getYPositions(snapPointsMode, point,
|
|
218
|
+
getYPositions(snapPointsMode, point, effScreenSize, effectiveFrameSize)
|
|
163
219
|
),
|
|
164
|
-
[
|
|
220
|
+
[effScreenSize, effectiveFrameSize, snapPoints, snapPointsMode]
|
|
165
221
|
)
|
|
166
222
|
|
|
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
223
|
const [isDragging, setIsDragging_] = React.useState(false)
|
|
181
224
|
|
|
182
225
|
// synchronous dragging ref — set BEFORE async state commits.
|
|
@@ -198,28 +241,30 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
198
241
|
[pauseKeyboardHandler, flushPendingHide]
|
|
199
242
|
)
|
|
200
243
|
|
|
201
|
-
// keyboard-adjusted
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
244
|
+
// keyboard-adjusted snap positions.
|
|
245
|
+
//
|
|
246
|
+
// WEB: the sheet stays ANCHORED at the bottom and keeps its full pre-keyboard
|
|
247
|
+
// height — the keyboard is an overlay, so the frame neither shifts nor resizes.
|
|
248
|
+
// Avoidance is handled below the frame: a bottom spacer (keyboardOccludedHeight
|
|
249
|
+
// = keyboardHeight) extends the scroll content so the lower content (e.g. the
|
|
250
|
+
// footer) can scroll up clear of the keyboard, and SheetScrollView scrolls the
|
|
251
|
+
// focused input above it. So activePositions === positions on web (no shift).
|
|
252
|
+
//
|
|
253
|
+
// NATIVE: shift snap points up by keyboard height (the native keyboard is
|
|
254
|
+
// opaque and pushes content), capped at the safe-area top inset.
|
|
206
255
|
//
|
|
207
|
-
// IMPORTANT: frozen during drag to prevent gesture handler recreation
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// The post-drag reconciliation effect handles animating to correct position after drag ends.
|
|
256
|
+
// IMPORTANT: frozen during drag to prevent gesture handler recreation —
|
|
257
|
+
// when a TextInput blurs mid-drag the keyboard state would otherwise revert
|
|
258
|
+
// and recreate the gesture useMemo, cancelling the active drag.
|
|
211
259
|
const activePositionsRef = React.useRef(positions)
|
|
212
260
|
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
261
|
if (isDragging || isDraggingRef.current) return activePositionsRef.current
|
|
217
262
|
|
|
218
263
|
let result: number[]
|
|
219
264
|
if (!isKeyboardVisible || keyboardHeight <= 0) {
|
|
220
265
|
result = positions
|
|
221
266
|
} else {
|
|
222
|
-
const safeAreaTop =
|
|
267
|
+
const safeAreaTop = getSafeAreaTopInset()
|
|
223
268
|
result = positions.map((p) => {
|
|
224
269
|
// don't adjust the off-screen/close position (from dismissOnSnapToBottom's 0% snap)
|
|
225
270
|
// — it must stay at screenSize so the user can drag between real snap points
|
|
@@ -232,14 +277,27 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
232
277
|
return result
|
|
233
278
|
}, [positions, isKeyboardVisible, keyboardHeight, screenSize, isDragging])
|
|
234
279
|
|
|
280
|
+
// bottom spacer for a sheet TALLER than the visible band: once the frame has
|
|
281
|
+
// shifted up as far as the safe-area cap allows, its lowest content can still
|
|
282
|
+
// sit behind the keyboard. the spacer = exactly that occluded height, so the
|
|
283
|
+
// footer can scroll up clear of the keyboard. when the sheet fits above the
|
|
284
|
+
// keyboard this returns 0 (no spacer, no over-scroll). same logic web + native.
|
|
235
285
|
const keyboardOccludedHeight = getKeyboardOccludedHeight({
|
|
236
|
-
frameSize,
|
|
237
|
-
isKeyboardVisible
|
|
286
|
+
frameSize: effectiveFrameSize,
|
|
287
|
+
isKeyboardVisible,
|
|
238
288
|
keyboardHeight,
|
|
239
|
-
screenSize,
|
|
289
|
+
screenSize: effScreenSize,
|
|
240
290
|
sheetY: position >= 0 ? activePositions[position] : undefined,
|
|
241
291
|
})
|
|
242
292
|
|
|
293
|
+
// pin the scroll view to the held (pre-keyboard) frame height while the
|
|
294
|
+
// keyboard is up on web. on older iOS the consumer's window-derived maxHeight
|
|
295
|
+
// shrinks with the keyboard, which would clip the scroll view (and the frame)
|
|
296
|
+
// smaller; this override keeps it at the full height so the frame only
|
|
297
|
+
// translates up, never resizes. 0 = no override (use the consumer maxHeight).
|
|
298
|
+
const keyboardStableFrameHeight =
|
|
299
|
+
isWebKbSheet && isKeyboardVisible && frameSize > 0 ? frameSize : 0
|
|
300
|
+
|
|
243
301
|
const { useAnimatedNumber, useAnimatedNumberStyle, useAnimatedNumberReaction } =
|
|
244
302
|
animationDriver
|
|
245
303
|
const AnimatedView = (animationDriver.View ?? TamaguiView) as typeof Animated.View
|
|
@@ -325,7 +383,19 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
325
383
|
const animateTo = useEvent((position: number, animationOverride?: any) => {
|
|
326
384
|
if (frameSize === 0) return
|
|
327
385
|
|
|
328
|
-
|
|
386
|
+
// use effScreenSize (the frozen anchor space the positions were built in) for
|
|
387
|
+
// the off-screen/close target too, so a close while the keyboard is still up
|
|
388
|
+
// animates fully out instead of to a mismatched live screenSize.
|
|
389
|
+
//
|
|
390
|
+
// web: clear the maximum the viewport can ever reveal, not just the current
|
|
391
|
+
// layout viewport. iOS Safari retracts its chrome on scroll and exposes area
|
|
392
|
+
// below the current viewport, so a sheet parked at effScreenSize would peek
|
|
393
|
+
// back in as the page scrolls. getMaxViewportHeight floors the target past
|
|
394
|
+
// anything Safari can expose.
|
|
395
|
+
const closeTarget = isWeb
|
|
396
|
+
? Math.max(effScreenSize, getMaxViewportHeight())
|
|
397
|
+
: effScreenSize
|
|
398
|
+
let toValue = isHidden || position === -1 ? closeTarget : activePositions[position]
|
|
329
399
|
|
|
330
400
|
if (at.current === toValue) return
|
|
331
401
|
|
|
@@ -417,6 +487,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
417
487
|
return
|
|
418
488
|
}
|
|
419
489
|
|
|
490
|
+
// never fight an active drag: the gesture owns the animated position. on
|
|
491
|
+
// web the AnimatedView's onLayout re-fires with sub-pixel jitter as the
|
|
492
|
+
// frame translates (frameSize 499.99996 <-> 500.00003), and since frameSize
|
|
493
|
+
// is a dep of this effect that would re-run it mid-pull and snap the sheet
|
|
494
|
+
// back to its resting snap point. read the live ref so drag-end (which the
|
|
495
|
+
// reconcile-after-drag effect handles) isn't gated by stale deps.
|
|
496
|
+
if (isDraggingRef.current) {
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
|
|
420
500
|
if (!frameSize || !screenSize || isHidden || (hasntMeasured && !open)) {
|
|
421
501
|
return
|
|
422
502
|
}
|
|
@@ -443,6 +523,10 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
443
523
|
scrollBridge.setScrollEnabled?.(false)
|
|
444
524
|
}
|
|
445
525
|
}
|
|
526
|
+
// NOTE: effScreenSize/effectiveFrameSize are intentionally NOT deps. With the
|
|
527
|
+
// spacer approach the frame's position target is frozen across keyboard
|
|
528
|
+
// open/close (same stable baseline), so it must NOT re-animate — keyboard
|
|
529
|
+
// avoidance is the bottom spacer + scroll, not a frame move.
|
|
446
530
|
}, [hasntMeasured, disableAnimation, isHidden, frameSize, screenSize, open, position])
|
|
447
531
|
|
|
448
532
|
const disableDrag = props.disableDrag ?? controller?.disableDrag
|
|
@@ -454,7 +538,11 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
454
538
|
if (!frameSize) return
|
|
455
539
|
if (isShowingInnerSheet) return
|
|
456
540
|
|
|
457
|
-
|
|
541
|
+
// use keyboard-adjusted positions (matches the RNGH path): when the
|
|
542
|
+
// keyboard is open the sheet sits at activePositions[0], so clamping drags
|
|
543
|
+
// against the un-adjusted positions[0] would rubber-band the sheet down to
|
|
544
|
+
// near the bottom on any drag.
|
|
545
|
+
const minY = activePositions[0]
|
|
458
546
|
scrollBridge.paneMinY = minY
|
|
459
547
|
let startY = at.current
|
|
460
548
|
|
|
@@ -497,8 +585,8 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
497
585
|
let closestPoint = 0
|
|
498
586
|
let dist = Number.POSITIVE_INFINITY
|
|
499
587
|
|
|
500
|
-
for (let i = 0; i <
|
|
501
|
-
const position =
|
|
588
|
+
for (let i = 0; i < activePositions.length; i++) {
|
|
589
|
+
const position = activePositions[i]
|
|
502
590
|
const curDist = end > position ? end - position : position - end
|
|
503
591
|
if (curDist < dist) {
|
|
504
592
|
dist = curDist
|
|
@@ -530,6 +618,16 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
530
618
|
return true
|
|
531
619
|
}
|
|
532
620
|
|
|
621
|
+
// touch is on the ScrollView node — the web scroll-view gesture hook
|
|
622
|
+
// owns it and drives drag/release through scrollBridge directly (it
|
|
623
|
+
// re-baselines via scrollBridge.startPanDrag on each pan handoff). if
|
|
624
|
+
// we also granted here, RNW's PanResponder would set the animated
|
|
625
|
+
// position from a second, differently-based offset every move and the
|
|
626
|
+
// sheet would jitter/jump. defer entirely to the hook.
|
|
627
|
+
if (scrollBridge.scrollNodeTouched) {
|
|
628
|
+
return false
|
|
629
|
+
}
|
|
630
|
+
|
|
533
631
|
if (scrollBridge.hasScrollableContent === true) {
|
|
534
632
|
if (scrollBridge.scrollLock) {
|
|
535
633
|
return false
|
|
@@ -575,6 +673,17 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
575
673
|
|
|
576
674
|
let isExternalDrag = false
|
|
577
675
|
|
|
676
|
+
// re-baseline a pan drag to the current animated position. the web
|
|
677
|
+
// scroll-view hook calls this on every transition INTO pan ownership
|
|
678
|
+
// (including handoffs back from scroll), so its panDragOffset — which it
|
|
679
|
+
// resets to 0 at each pan entry — is measured from where the sheet
|
|
680
|
+
// actually is now, not from where the gesture first grabbed it. without
|
|
681
|
+
// this the sheet would jump to a stale origin on a scroll→pan handoff.
|
|
682
|
+
scrollBridge.startPanDrag = () => {
|
|
683
|
+
isExternalDrag = true
|
|
684
|
+
grant()
|
|
685
|
+
}
|
|
686
|
+
|
|
578
687
|
scrollBridge.drag = (dy) => {
|
|
579
688
|
if (!isExternalDrag) {
|
|
580
689
|
isExternalDrag = true
|
|
@@ -616,13 +725,25 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
616
725
|
onPanResponderTerminate: finish,
|
|
617
726
|
onPanResponderRelease: finish,
|
|
618
727
|
})
|
|
619
|
-
}, [
|
|
620
|
-
|
|
621
|
-
|
|
728
|
+
}, [
|
|
729
|
+
disableDrag,
|
|
730
|
+
isShowingInnerSheet,
|
|
731
|
+
animateTo,
|
|
732
|
+
frameSize,
|
|
733
|
+
activePositions,
|
|
734
|
+
setPosition,
|
|
735
|
+
])
|
|
736
|
+
|
|
737
|
+
// animate to the keyboard-adjusted position when the keyboard state changes.
|
|
738
|
+
// both web and native shift the whole frame UP by the keyboard height (capped
|
|
739
|
+
// at the top safe area, in activePositions) so the content keeps its position
|
|
740
|
+
// relative to the visible area — it was resting on the device bottom, now it
|
|
741
|
+
// rests on the keyboard top. the frame is device-height, so sliding it up never
|
|
742
|
+
// reveals a gap below the content.
|
|
622
743
|
React.useEffect(() => {
|
|
623
744
|
if (isDragging || isHidden || !open || disableAnimation) return
|
|
624
745
|
if (!frameSize || !screenSize) return
|
|
625
|
-
//
|
|
746
|
+
// timing animation matches the iOS keyboard animation (~250ms)
|
|
626
747
|
animateTo(position, { type: 'timing', duration: 250 })
|
|
627
748
|
}, [isKeyboardVisible, keyboardHeight])
|
|
628
749
|
|
|
@@ -647,6 +768,11 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
647
768
|
React.useEffect(() => {
|
|
648
769
|
if (!open && isKeyboardVisible) {
|
|
649
770
|
dismissKeyboard()
|
|
771
|
+
// if the sheet was closed mid-drag the keyboard-hide handler was paused
|
|
772
|
+
// and a hide could be left pending — clear it so isKeyboardVisible can't
|
|
773
|
+
// stick true after the sheet is gone. (no-op for a normal close.)
|
|
774
|
+
pauseKeyboardHandler.current = false
|
|
775
|
+
flushPendingHide()
|
|
650
776
|
}
|
|
651
777
|
}, [open])
|
|
652
778
|
|
|
@@ -671,31 +797,57 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
671
797
|
pauseKeyboardHandler,
|
|
672
798
|
})
|
|
673
799
|
|
|
800
|
+
// ignore any layout measured while the soft keyboard is up (web +
|
|
801
|
+
// moveOnKeyboardChange): the visual viewport (which RN's web layout follows)
|
|
802
|
+
// shrinks, so the height would be the collapsed sheet — keeping it would
|
|
803
|
+
// recompute the fit anchor and fly the frame. a LIVE DOM check, NOT the
|
|
804
|
+
// isKeyboardVisible React state, is required: the state lags the resize, so
|
|
805
|
+
// the first shrunk onLayout lands before the flag flips. holding the measured
|
|
806
|
+
// sizes keeps the sheet anchored; the scroll content shifts to clear the kb.
|
|
807
|
+
const ignoreLayoutForKeyboard = useEvent(
|
|
808
|
+
() => isWeb && moveOnKeyboardChange && getWebKeyboardHeight() >= MIN_KEYBOARD_HEIGHT
|
|
809
|
+
)
|
|
810
|
+
|
|
674
811
|
const handleAnimationViewLayout = useEvent((e: LayoutChangeEvent) => {
|
|
675
812
|
// don't update frameSize during exit animation to prevent position jumps
|
|
676
813
|
if (!open && stableFrameSize.current !== 0) {
|
|
677
814
|
return
|
|
678
815
|
}
|
|
679
816
|
|
|
817
|
+
const layoutHeight = e.nativeEvent?.layout.height
|
|
818
|
+
// drop a layout measured while the keyboard is up: on older iOS the web
|
|
819
|
+
// viewport shrinks and the frame would resize. keep the pre-keyboard frame
|
|
820
|
+
// height so the frame just TRANSLATES up (activePositions) without resizing.
|
|
821
|
+
// exception: if we have no frame height yet (sheet opened with the keyboard
|
|
822
|
+
// already up), accept it so the sheet can appear at all.
|
|
823
|
+
if (ignoreLayoutForKeyboard() && frameSize > 0) return
|
|
824
|
+
|
|
680
825
|
// avoid bugs where it grows forever for whatever reason
|
|
681
826
|
// For inline mode (non-modal), don't cap at window height - use actual layout
|
|
682
|
-
const layoutHeight = e.nativeEvent?.layout.height
|
|
683
827
|
const next = modal
|
|
684
|
-
? Math.min(layoutHeight,
|
|
828
|
+
? Math.min(layoutHeight, getStableViewportHeight())
|
|
685
829
|
: layoutHeight
|
|
686
830
|
if (!next) return
|
|
687
|
-
|
|
831
|
+
// round: web onLayout reports sub-pixel heights (e.g. 499.99996) that jitter
|
|
832
|
+
// frame to frame as the view transforms; the raw float would re-fire every
|
|
833
|
+
// effect that depends on frameSize on each drag move.
|
|
834
|
+
setFrameSize(Math.round(next))
|
|
688
835
|
})
|
|
689
836
|
|
|
690
|
-
const handleMaxContentViewLayout = React.useCallback(
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
837
|
+
const handleMaxContentViewLayout = React.useCallback(
|
|
838
|
+
(e: LayoutChangeEvent) => {
|
|
839
|
+
// keep maxContentSize at the full pre-keyboard viewport: drop layouts
|
|
840
|
+
// measured while the keyboard is up (the shrunk viewport), unless we have
|
|
841
|
+
// none yet (keyboard-already-up open).
|
|
842
|
+
if (ignoreLayoutForKeyboard() && screenSize > 0) return
|
|
843
|
+
// avoid bugs where it grows forever for whatever reason
|
|
844
|
+
const next = Math.min(e.nativeEvent?.layout.height, getStableViewportHeight())
|
|
845
|
+
if (!next) return
|
|
846
|
+
// round to avoid sub-pixel churn re-firing size-dependent effects
|
|
847
|
+
setMaxContentSize(Math.round(next))
|
|
848
|
+
},
|
|
849
|
+
[ignoreLayoutForKeyboard, screenSize]
|
|
850
|
+
)
|
|
699
851
|
|
|
700
852
|
const getAnimatedNumberStyle = React.useCallback(
|
|
701
853
|
(val: number) => {
|
|
@@ -745,6 +897,8 @@ export const SheetImplementationCustom = React.forwardRef<View, SheetProps>(
|
|
|
745
897
|
<SheetProvider
|
|
746
898
|
{...providerProps}
|
|
747
899
|
keyboardOccludedHeight={keyboardOccludedHeight}
|
|
900
|
+
isKeyboardVisible={isKeyboardVisible}
|
|
901
|
+
keyboardStableFrameHeight={keyboardStableFrameHeight}
|
|
748
902
|
setHasScrollView={setHasScrollView}
|
|
749
903
|
>
|
|
750
904
|
<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,35 @@ 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
|
+
// when the keyboard is open, pin the scroll view to the sheet's pre-keyboard
|
|
71
|
+
// frame height (frozenFrameHeight), overriding any consumer maxHeight. on web
|
|
72
|
+
// that maxHeight is often tied to useWindowDimensions, which SHRINKS when the
|
|
73
|
+
// keyboard opens and would otherwise collapse the sheet. holding the height
|
|
74
|
+
// constant means the frame only TRANSLATES up (no resize, no jump). applied
|
|
75
|
+
// AFTER {...props} so it wins.
|
|
76
|
+
const keyboardFrozenOverride =
|
|
77
|
+
hasFit && isKeyboardVisible && frozenFrameHeight > 0
|
|
78
|
+
? { height: frozenFrameHeight, maxHeight: frozenFrameHeight }
|
|
79
|
+
: null
|
|
80
|
+
|
|
62
81
|
const panGestureRef = gestureContext?.panGestureRef
|
|
63
82
|
const { ScrollView: RNGHScrollView } = getGestureHandlerState()
|
|
64
83
|
const useRNGHScrollView = isGestureHandlerEnabled() && RNGHScrollView && panGestureRef
|
|
@@ -102,6 +121,12 @@ export const SheetScrollView = React.forwardRef<
|
|
|
102
121
|
}
|
|
103
122
|
}
|
|
104
123
|
|
|
124
|
+
// track the fit height for the scrollable-content check. the keyboard-freeze
|
|
125
|
+
// height is supplied by the sheet (frozenFrameHeight), not derived here.
|
|
126
|
+
const recordFitHeight = (height: number) => {
|
|
127
|
+
parentHeight.current = height
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
useEffect(() => {
|
|
106
131
|
scrollBridge.hasScrollableContent = hasScrollableContent
|
|
107
132
|
}, [hasScrollableContent])
|
|
@@ -148,7 +173,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
148
173
|
scrollEnabled={scrollEnabled}
|
|
149
174
|
simultaneousHandlers={[panGestureRef]}
|
|
150
175
|
onLayout={(e: any) => {
|
|
151
|
-
|
|
176
|
+
recordFitHeight(Math.ceil(e.nativeEvent.layout.height))
|
|
152
177
|
updateScrollable()
|
|
153
178
|
}}
|
|
154
179
|
onScroll={(e: any) => {
|
|
@@ -186,6 +211,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
186
211
|
keyboardShouldPersistTaps="always"
|
|
187
212
|
keyboardDismissMode="none"
|
|
188
213
|
{...props}
|
|
214
|
+
{...keyboardFrozenOverride}
|
|
189
215
|
>
|
|
190
216
|
{contentWrapper}
|
|
191
217
|
</RNGHComponent>
|
|
@@ -196,7 +222,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
196
222
|
return (
|
|
197
223
|
<ScrollView
|
|
198
224
|
onLayout={(e) => {
|
|
199
|
-
|
|
225
|
+
recordFitHeight(Math.ceil(e.nativeEvent.layout.height))
|
|
200
226
|
updateScrollable()
|
|
201
227
|
}}
|
|
202
228
|
ref={composeRefs(scrollRef as any, ref)}
|
|
@@ -213,6 +239,7 @@ export const SheetScrollView = React.forwardRef<
|
|
|
213
239
|
contentContainerStyle={{ minHeight: '100%' }}
|
|
214
240
|
{...gestureProps}
|
|
215
241
|
{...props}
|
|
242
|
+
{...keyboardFrozenOverride}
|
|
216
243
|
>
|
|
217
244
|
{contentWrapper}
|
|
218
245
|
</ScrollView>
|
package/src/createSheet.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useComposedRefs } from '@tamagui/compose-refs'
|
|
2
|
-
import { useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
2
|
+
import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/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
|
|
@@ -136,9 +137,9 @@ export function createSheet<
|
|
|
136
137
|
|
|
137
138
|
type ExtraFrameProps = {
|
|
138
139
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
140
|
+
* by default the sheet adds a view below its bottom that extends past the
|
|
141
|
+
* largest visible viewport height. this covers spring overshoot when opening
|
|
142
|
+
* so page content never shows through below the sheet.
|
|
142
143
|
*/
|
|
143
144
|
disableHideBottomOverflow?: boolean
|
|
144
145
|
|
|
@@ -225,14 +226,25 @@ export function createSheet<
|
|
|
225
226
|
<Frame
|
|
226
227
|
{...props}
|
|
227
228
|
componentName="SheetCover"
|
|
229
|
+
data-sheet-cover=""
|
|
228
230
|
children={null}
|
|
229
231
|
// Don't inherit testID - this is a visual helper element
|
|
230
232
|
testID={undefined}
|
|
231
233
|
id={undefined}
|
|
232
234
|
position="absolute"
|
|
233
|
-
bottom
|
|
235
|
+
// anchor the cover's top at the sheet's bottom edge (top: 100% of the
|
|
236
|
+
// container), then extend it downward. on web extend it past the
|
|
237
|
+
// largest viewport safari can reveal as its chrome retracts, so the
|
|
238
|
+
// sheet background covers the bottom instead of revealing the page.
|
|
239
|
+
// native keeps frameSize.
|
|
240
|
+
top="100%"
|
|
234
241
|
zIndex={-1}
|
|
235
|
-
height={
|
|
242
|
+
height={
|
|
243
|
+
isWeb
|
|
244
|
+
? Math.max(context.frameSize, getMaxViewportHeight())
|
|
245
|
+
: context.frameSize
|
|
246
|
+
}
|
|
247
|
+
maxHeight={isWeb ? 'none' : undefined}
|
|
236
248
|
left={0}
|
|
237
249
|
right={0}
|
|
238
250
|
borderWidth={0}
|