@tamagui/sheet 3.0.0-beta.924.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/dist/cjs/Sheet.cjs +48 -16
  2. package/dist/cjs/Sheet.native.cjs +46 -17
  3. package/dist/cjs/Sheet.native.js +46 -17
  4. package/dist/cjs/Sheet.native.js.map +1 -1
  5. package/dist/cjs/SheetDragSurface.cjs +50 -0
  6. package/dist/{jsx/GestureDetectorWrapper.native.js → cjs/SheetDragSurface.native.cjs} +24 -12
  7. package/dist/cjs/{GestureDetectorWrapper.native.js → SheetDragSurface.native.js} +25 -11
  8. package/dist/cjs/SheetDragSurface.native.js.map +1 -0
  9. package/dist/cjs/SheetImplementationCustom.cjs +41 -58
  10. package/dist/cjs/SheetImplementationCustom.native.cjs +43 -59
  11. package/dist/cjs/SheetImplementationCustom.native.js +43 -59
  12. package/dist/cjs/SheetImplementationCustom.native.js.map +1 -1
  13. package/dist/cjs/SheetScrollView.cjs +15 -3
  14. package/dist/cjs/SheetScrollView.native.cjs +17 -4
  15. package/dist/cjs/SheetScrollView.native.js +17 -4
  16. package/dist/cjs/SheetScrollView.native.js.map +1 -1
  17. package/dist/cjs/nativeSheet.cjs +3 -59
  18. package/dist/cjs/nativeSheet.native.cjs +70 -58
  19. package/dist/cjs/nativeSheet.native.js +70 -58
  20. package/dist/cjs/nativeSheet.native.js.map +1 -1
  21. package/dist/cjs/stableViewportHeight.cjs +27 -0
  22. package/dist/cjs/stableViewportHeight.native.cjs +29 -0
  23. package/dist/cjs/stableViewportHeight.native.js +31 -0
  24. package/dist/cjs/stableViewportHeight.native.js.map +1 -0
  25. package/dist/cjs/useSheetOffscreenSize.cjs +3 -1
  26. package/dist/cjs/useSheetOffscreenSize.native.cjs +3 -2
  27. package/dist/cjs/useSheetOffscreenSize.native.js +3 -2
  28. package/dist/cjs/useSheetOffscreenSize.native.js.map +1 -1
  29. package/dist/cjs/useSheetOpenState.cjs +13 -6
  30. package/dist/cjs/useSheetOpenState.native.cjs +15 -5
  31. package/dist/cjs/useSheetOpenState.native.js +15 -5
  32. package/dist/cjs/useSheetOpenState.native.js.map +1 -1
  33. package/dist/cjs/useSheetProviderProps.cjs +5 -94
  34. package/dist/cjs/useSheetProviderProps.native.cjs +5 -103
  35. package/dist/cjs/useSheetProviderProps.native.js +5 -103
  36. package/dist/cjs/useSheetProviderProps.native.js.map +1 -1
  37. package/dist/cjs/useSheetState.cjs +120 -0
  38. package/dist/cjs/useSheetState.native.cjs +131 -0
  39. package/dist/cjs/useSheetState.native.js +133 -0
  40. package/dist/cjs/useSheetState.native.js.map +1 -0
  41. package/dist/esm/Sheet.mjs +48 -15
  42. package/dist/esm/Sheet.mjs.map +1 -1
  43. package/dist/esm/Sheet.native.js +46 -17
  44. package/dist/esm/Sheet.native.js.map +1 -1
  45. package/dist/esm/SheetDragSurface.mjs +25 -0
  46. package/dist/esm/SheetDragSurface.mjs.map +1 -0
  47. package/dist/esm/SheetDragSurface.native.js +34 -0
  48. package/dist/esm/SheetDragSurface.native.js.map +1 -0
  49. package/dist/esm/SheetImplementationCustom.mjs +39 -56
  50. package/dist/esm/SheetImplementationCustom.mjs.map +1 -1
  51. package/dist/esm/SheetImplementationCustom.native.js +41 -57
  52. package/dist/esm/SheetImplementationCustom.native.js.map +1 -1
  53. package/dist/esm/SheetScrollView.mjs +16 -4
  54. package/dist/esm/SheetScrollView.mjs.map +1 -1
  55. package/dist/esm/SheetScrollView.native.js +18 -5
  56. package/dist/esm/SheetScrollView.native.js.map +1 -1
  57. package/dist/esm/nativeSheet.mjs +3 -60
  58. package/dist/esm/nativeSheet.mjs.map +1 -1
  59. package/dist/esm/nativeSheet.native.js +70 -58
  60. package/dist/esm/nativeSheet.native.js.map +1 -1
  61. package/dist/esm/stableViewportHeight.mjs +8 -0
  62. package/dist/esm/stableViewportHeight.mjs.map +1 -0
  63. package/dist/esm/stableViewportHeight.native.js +8 -0
  64. package/dist/esm/stableViewportHeight.native.js.map +1 -0
  65. package/dist/esm/useKeyboardControllerSheet.native.js +0 -5
  66. package/dist/esm/useSheetOffscreenSize.mjs +3 -1
  67. package/dist/esm/useSheetOffscreenSize.mjs.map +1 -1
  68. package/dist/esm/useSheetOffscreenSize.native.js +3 -2
  69. package/dist/esm/useSheetOffscreenSize.native.js.map +1 -1
  70. package/dist/esm/useSheetOpenState.mjs +14 -7
  71. package/dist/esm/useSheetOpenState.mjs.map +1 -1
  72. package/dist/esm/useSheetOpenState.native.js +16 -6
  73. package/dist/esm/useSheetOpenState.native.js.map +1 -1
  74. package/dist/esm/useSheetProviderProps.mjs +5 -88
  75. package/dist/esm/useSheetProviderProps.mjs.map +1 -1
  76. package/dist/esm/useSheetProviderProps.native.js +5 -97
  77. package/dist/esm/useSheetProviderProps.native.js.map +1 -1
  78. package/dist/esm/useSheetState.mjs +95 -0
  79. package/dist/esm/useSheetState.mjs.map +1 -0
  80. package/dist/esm/useSheetState.native.js +104 -0
  81. package/dist/esm/useSheetState.native.js.map +1 -0
  82. package/dist/jsx/Sheet.mjs +48 -15
  83. package/dist/jsx/Sheet.mjs.map +1 -1
  84. package/dist/jsx/Sheet.native.cjs +46 -17
  85. package/dist/jsx/Sheet.native.js +46 -17
  86. package/dist/jsx/Sheet.native.js.map +1 -1
  87. package/dist/jsx/SheetDragSurface.mjs +25 -0
  88. package/dist/jsx/SheetDragSurface.mjs.map +1 -0
  89. package/dist/{cjs/GestureDetectorWrapper.native.cjs → jsx/SheetDragSurface.native.cjs} +24 -10
  90. package/dist/jsx/{GestureDetectorWrapper.native.cjs → SheetDragSurface.native.js} +26 -10
  91. package/dist/jsx/SheetDragSurface.native.js.map +1 -0
  92. package/dist/jsx/SheetImplementationCustom.mjs +39 -56
  93. package/dist/jsx/SheetImplementationCustom.mjs.map +1 -1
  94. package/dist/jsx/SheetImplementationCustom.native.cjs +43 -59
  95. package/dist/jsx/SheetImplementationCustom.native.js +43 -59
  96. package/dist/jsx/SheetImplementationCustom.native.js.map +1 -1
  97. package/dist/jsx/SheetScrollView.mjs +16 -4
  98. package/dist/jsx/SheetScrollView.mjs.map +1 -1
  99. package/dist/jsx/SheetScrollView.native.cjs +17 -4
  100. package/dist/jsx/SheetScrollView.native.js +17 -4
  101. package/dist/jsx/SheetScrollView.native.js.map +1 -1
  102. package/dist/jsx/nativeSheet.mjs +3 -60
  103. package/dist/jsx/nativeSheet.mjs.map +1 -1
  104. package/dist/jsx/nativeSheet.native.cjs +70 -58
  105. package/dist/jsx/nativeSheet.native.js +70 -58
  106. package/dist/jsx/nativeSheet.native.js.map +1 -1
  107. package/dist/jsx/stableViewportHeight.mjs +8 -0
  108. package/dist/jsx/stableViewportHeight.mjs.map +1 -0
  109. package/dist/jsx/stableViewportHeight.native.cjs +29 -0
  110. package/dist/jsx/stableViewportHeight.native.js +31 -0
  111. package/dist/jsx/stableViewportHeight.native.js.map +1 -0
  112. package/dist/jsx/useSheetOffscreenSize.mjs +3 -1
  113. package/dist/jsx/useSheetOffscreenSize.mjs.map +1 -1
  114. package/dist/jsx/useSheetOffscreenSize.native.cjs +3 -2
  115. package/dist/jsx/useSheetOffscreenSize.native.js +3 -2
  116. package/dist/jsx/useSheetOffscreenSize.native.js.map +1 -1
  117. package/dist/jsx/useSheetOpenState.mjs +14 -7
  118. package/dist/jsx/useSheetOpenState.mjs.map +1 -1
  119. package/dist/jsx/useSheetOpenState.native.cjs +15 -5
  120. package/dist/jsx/useSheetOpenState.native.js +15 -5
  121. package/dist/jsx/useSheetOpenState.native.js.map +1 -1
  122. package/dist/jsx/useSheetProviderProps.mjs +5 -88
  123. package/dist/jsx/useSheetProviderProps.mjs.map +1 -1
  124. package/dist/jsx/useSheetProviderProps.native.cjs +5 -103
  125. package/dist/jsx/useSheetProviderProps.native.js +5 -103
  126. package/dist/jsx/useSheetProviderProps.native.js.map +1 -1
  127. package/dist/jsx/useSheetState.mjs +95 -0
  128. package/dist/jsx/useSheetState.mjs.map +1 -0
  129. package/dist/jsx/useSheetState.native.cjs +131 -0
  130. package/dist/jsx/useSheetState.native.js +133 -0
  131. package/dist/jsx/useSheetState.native.js.map +1 -0
  132. package/package.json +27 -22
  133. package/src/Sheet.tsx +45 -18
  134. package/src/SheetDragSurface.native.tsx +42 -0
  135. package/src/SheetDragSurface.tsx +40 -0
  136. package/src/SheetImplementationCustom.tsx +67 -92
  137. package/src/SheetScrollView.tsx +26 -4
  138. package/src/nativeSheet.native.tsx +110 -0
  139. package/src/nativeSheet.ts +16 -0
  140. package/src/stableViewportHeight.native.ts +9 -0
  141. package/src/stableViewportHeight.ts +15 -0
  142. package/src/types.tsx +44 -1
  143. package/src/useSheetOffscreenSize.tsx +3 -7
  144. package/src/useSheetOpenState.tsx +18 -8
  145. package/src/useSheetProviderProps.tsx +18 -149
  146. package/src/useSheetScrollViewGestures.native.ts +4 -1
  147. package/src/useSheetState.tsx +139 -0
  148. package/types/Sheet.d.ts +71 -167
  149. package/types/Sheet.d.ts.map +1 -1
  150. package/types/SheetDragSurface.d.ts +11 -0
  151. package/types/SheetDragSurface.d.ts.map +1 -0
  152. package/types/SheetDragSurface.native.d.ts +11 -0
  153. package/types/SheetDragSurface.native.d.ts.map +1 -0
  154. package/types/SheetImplementationCustom.d.ts +1 -2
  155. package/types/SheetImplementationCustom.d.ts.map +1 -1
  156. package/types/SheetScrollView.d.ts +3 -3
  157. package/types/SheetScrollView.d.ts.map +1 -1
  158. package/types/nativeSheet.d.ts +8 -9
  159. package/types/nativeSheet.d.ts.map +1 -1
  160. package/types/nativeSheet.native.d.ts +4 -0
  161. package/types/nativeSheet.native.d.ts.map +1 -0
  162. package/types/stableViewportHeight.d.ts +12 -0
  163. package/types/stableViewportHeight.d.ts.map +1 -0
  164. package/types/stableViewportHeight.native.d.ts +6 -0
  165. package/types/stableViewportHeight.native.d.ts.map +1 -0
  166. package/types/types.d.ts +34 -1
  167. package/types/types.d.ts.map +1 -1
  168. package/types/useSheetOffscreenSize.d.ts +1 -1
  169. package/types/useSheetOffscreenSize.d.ts.map +1 -1
  170. package/types/useSheetOpenState.d.ts +3 -1
  171. package/types/useSheetOpenState.d.ts.map +1 -1
  172. package/types/useSheetProviderProps.d.ts +15 -13
  173. package/types/useSheetProviderProps.d.ts.map +1 -1
  174. package/types/useSheetScrollViewGestures.native.d.ts +1 -1
  175. package/types/useSheetScrollViewGestures.native.d.ts.map +1 -1
  176. package/types/useSheetState.d.ts +26 -0
  177. package/types/useSheetState.d.ts.map +1 -0
  178. package/dist/cjs/GestureDetectorWrapper.cjs +0 -44
  179. package/dist/cjs/GestureDetectorWrapper.native.js.map +0 -1
  180. package/dist/esm/GestureDetectorWrapper.mjs +0 -25
  181. package/dist/esm/GestureDetectorWrapper.mjs.map +0 -1
  182. package/dist/esm/GestureDetectorWrapper.native.js +0 -26
  183. package/dist/esm/GestureDetectorWrapper.native.js.map +0 -1
  184. package/dist/jsx/GestureDetectorWrapper.mjs +0 -25
  185. package/dist/jsx/GestureDetectorWrapper.mjs.map +0 -1
  186. package/dist/jsx/GestureDetectorWrapper.native.js.map +0 -1
  187. package/src/GestureDetectorWrapper.tsx +0 -38
  188. package/src/nativeSheet.tsx +0 -86
  189. package/types/GestureDetectorWrapper.d.ts +0 -14
  190. package/types/GestureDetectorWrapper.d.ts.map +0 -1
@@ -0,0 +1,40 @@
1
+ import type { TamaguiElement } from '@tamagui/core'
2
+ import { View } from '@tamagui/core'
3
+ import { PanResponder } from '@tamagui/react-native-pan-responder'
4
+ import { useResponderEvents } from '@tamagui/react-native-use-responder-events'
5
+ import * as React from 'react'
6
+
7
+ import type { SheetDragSurfaceProps } from './types'
8
+
9
+ /**
10
+ * The surface the sheet is dragged by.
11
+ *
12
+ * Web runs the same PanResponder react-native does, over the same responder
13
+ * system, so the drag behaves as it did when this was a react-native `View`.
14
+ * `gestureHandler` is native-only, so the props for it are ignored here.
15
+ */
16
+ export function SheetDragSurface({ panConfig, children }: SheetDragSurfaceProps) {
17
+ const ref = React.useRef<TamaguiElement>(null)
18
+
19
+ const panHandlers = React.useMemo(
20
+ () => (panConfig ? PanResponder.create(panConfig).panHandlers : undefined),
21
+ [panConfig]
22
+ )
23
+
24
+ const { onClickCapture, ...responderHandlers } = panHandlers ?? {}
25
+ useResponderEvents(ref, responderHandlers)
26
+
27
+ // onClickCapture is a DOM prop: tamagui forwards it to the element untouched
28
+ // but only types react-native's prop surface
29
+ const domProps = { onClickCapture } as Record<string, unknown>
30
+
31
+ return (
32
+ // flexBasis auto, like the container frame: under the web style compat
33
+ // `flex={1}` means a basis of 0, which collapses this surface inside the
34
+ // animated wrapper (that wrapper has no height of its own in fit mode), and
35
+ // a collapsed surface leaves the frame unmeasured and parked off-screen
36
+ <View ref={ref} {...domProps} flex={1} flexBasis="auto" width="100%" height="100%">
37
+ {children}
38
+ </View>
39
+ )
40
+ }
@@ -1,5 +1,7 @@
1
1
  import { ProvideAdaptContext, useAdaptContext } from '@tamagui/adapt'
2
2
  import { AnimatePresence } from '@tamagui/animate-presence'
3
+ // leaf subpath: the grammar itself belongs to the drivers, not to every sheet
4
+ import { getTransitionResolver } from '@tamagui/animation-helpers/transitionResolver'
3
5
  import { useComposedRefs } from '@tamagui/compose-refs'
4
6
  import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
5
7
  import {
@@ -16,17 +18,18 @@ import {
16
18
  } from '@tamagui/core'
17
19
  import { needsPortalRepropagation, Portal } from '@tamagui/portal'
18
20
  import React, { useState } from 'react'
21
+ import type { PanResponderConfig } from '@tamagui/react-native-pan-responder'
19
22
  import type {
20
- Animated,
21
23
  GestureResponderEvent,
22
24
  LayoutChangeEvent,
23
25
  PanResponderGestureState,
24
- } from 'react-native'
25
- import { Dimensions, PanResponder, View } from 'react-native'
26
+ ViewInstance as View,
27
+ } from '@tamagui/react-native-types'
26
28
  import { ParentSheetContext, SheetInsideSheetContext } from './contexts'
27
29
  import { SHEET_OVERLAY_MARKER } from './constants'
28
- import { GestureDetectorWrapper } from './GestureDetectorWrapper'
29
30
  import { getGestureHandlerState } from './gestureState'
31
+ import { SheetDragSurface } from './SheetDragSurface'
32
+ import { getStableViewportHeight } from './stableViewportHeight'
30
33
  import { GestureSheetProvider } from './GestureSheetContext'
31
34
  import { resisted } from './helpers'
32
35
  import {
@@ -37,7 +40,6 @@ import {
37
40
  import {
38
41
  getWebKeyboardResizeHeight,
39
42
  getMaxViewportHeight,
40
- getStableLayoutViewportHeight,
41
43
  getWebVisualViewportOffsetTop,
42
44
  MIN_KEYBOARD_HEIGHT,
43
45
  } from './webViewport'
@@ -75,22 +77,6 @@ let sheetHiddenStyleSheet: HTMLStyleElement | null = null
75
77
  // deepest sheet sits last), popped on close/unmount.
76
78
  const sheetEscapeLayers: object[] = []
77
79
 
78
- // on web we are always relative to window, on to screen
79
- const relativeDimensionTo = isWeb ? 'window' : 'screen'
80
-
81
- // height of the viewport the sheet positions against. on web this MUST be the
82
- // stable layout viewport and NOT Dimensions.get('window') — react-native-web's
83
- // Dimensions tracks visualViewport, which shrinks by the soft keyboard. capping
84
- // frameSize / maxContentSize against that shrinking value corrupts the fit-mode
85
- // math (translateY = screenSize - frameSize), detaching the sheet's bottom from
86
- // the screen edge when the keyboard opens. NOTE: window.innerHeight is NOT
87
- // stable on real iOS Safari (it shrinks with the keyboard too), so we use the
88
- // self-correcting baseline from webViewport instead. see getStableLayoutViewportHeight.
89
- function getStableViewportHeight(): number {
90
- if (isWeb && typeof window !== 'undefined') return getStableLayoutViewportHeight()
91
- return Dimensions.get(relativeDimensionTo).height
92
- }
93
-
94
80
  export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
95
81
  function SheetImplementationCustom(props, forwardedRef) {
96
82
  const parentSheet = React.useContext(ParentSheetContext)
@@ -150,29 +136,32 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
150
136
  return transitionConfigProp
151
137
  }
152
138
 
153
- const [animationProp, animationPropConfig] = !transition
154
- ? []
155
- : Array.isArray(transition)
156
- ? transition
157
- : ([transition] as const)
158
-
159
- // look up named animation config from driver if available
160
- const namedConfig =
161
- animationProp && animationDriver.animations?.[animationProp as string]
162
- if (namedConfig) {
163
- // css stores string configs (e.g. '150ms ease-out'); object-spreading a
164
- // string produces char-indexed junk. the css visual is driven by the DOM
165
- // `transition` prop, so pass only any explicit override object through.
166
- if (typeof namedConfig === 'string') {
167
- return animationPropConfig ?? null
168
- }
169
- return {
170
- ...namedConfig,
171
- ...animationPropConfig,
172
- }
139
+ // the sheet drives its position through an animated number rather than a
140
+ // style prop, so it resolves the authored transition itself. it goes
141
+ // through the same grammar every driver and the compiler use, so a config
142
+ // entry never means one thing here and another in a style prop. the raw
143
+ // config is the wrong thing to spread: presets are authored as
144
+ // `{ duration, bounce }` in milliseconds, and motion reads `duration` in
145
+ // seconds.
146
+ const resolved = transition
147
+ ? getTransitionResolver()?.resolve(transition, {
148
+ animations: animationDriver.animations,
149
+ })
150
+ : null
151
+ const timing = resolved?.all?.timing
152
+ if (!timing) return null
153
+ if (timing.kind === 'timing') {
154
+ return { type: 'timing', duration: timing.durationMs }
155
+ }
156
+ // stiffness/damping/mass is the spelling every spring driver takes, and
157
+ // the resolver derives it from `{ duration, bounce }` when that is what
158
+ // the config was written in
159
+ return {
160
+ type: 'spring',
161
+ stiffness: timing.stiffness,
162
+ damping: timing.damping,
163
+ mass: timing.mass,
173
164
  }
174
-
175
- return null
176
165
  })()
177
166
 
178
167
  /**
@@ -353,7 +342,7 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
353
342
  ? preKeyboardFrameSize.current
354
343
  : 0
355
344
 
356
- const AnimatedView = (animationDriver.View ?? TamaguiView) as typeof Animated.View
345
+ const AnimatedView = (animationDriver.View ?? TamaguiView) as React.ComponentType<any>
357
346
 
358
347
  useIsomorphicLayoutEffect(() => {
359
348
  if (!(sheetInsideSheet && open)) return
@@ -600,20 +589,15 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
600
589
 
601
590
  if (hasntMeasured && screenSize && frameSize) {
602
591
  at.current = screenSize
603
- animatedNumber.setValue(
604
- screenSize,
605
- {
606
- type: 'timing',
607
- duration: 0,
608
- },
609
- () => {
610
- syncAnimatedPosition(screenSize)
611
- // imperfect but struggling to render properly here
612
- setTimeout(() => {
613
- setDisableAnimation(false)
614
- }, 10)
615
- }
616
- )
592
+ // jump without waiting on a driver completion callback: drivers can drop
593
+ // onFinish (lost UI/JS hop under load) and the sheet would stay parked
594
+ // offscreen behind its overlay forever
595
+ animatedNumber.setValue(screenSize, { type: 'direct' })
596
+ syncAnimatedPosition(screenSize)
597
+ // imperfect but struggling to render properly here
598
+ setTimeout(() => {
599
+ setDisableAnimation(false)
600
+ }, 10)
617
601
  return
618
602
  }
619
603
 
@@ -667,10 +651,12 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
667
651
  const themeName = useThemeName()
668
652
  const [blockPan, setBlockPan] = React.useState(false)
669
653
 
670
- const panResponder = React.useMemo(() => {
671
- if (disableDrag) return
672
- if (!frameSize) return
673
- if (isShowingInnerSheet) return
654
+ // the drag surface turns this into a PanResponder. it is built here because
655
+ // the handlers close over the sheet's live drag state and the scroll bridge.
656
+ const panConfig = React.useMemo((): PanResponderConfig | null => {
657
+ if (disableDrag) return null
658
+ if (!frameSize) return null
659
+ if (isShowingInnerSheet) return null
674
660
 
675
661
  // use keyboard-adjusted positions (matches the RNGH path): when the
676
662
  // keyboard is open the sheet sits at activePositions[0], so clamping drags
@@ -754,7 +740,7 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
754
740
  // touch is on the ScrollView node — the web scroll-view gesture hook
755
741
  // owns it and drives drag/release through scrollBridge directly (it
756
742
  // re-baselines via scrollBridge.startPanDrag on each pan handoff). if
757
- // we also granted here, RNW's PanResponder would set the animated
743
+ // we also granted here, the PanResponder would set the animated
758
744
  // position from a second, differently-based offset every move and the
759
745
  // sheet would jitter/jump. defer entirely to the hook.
760
746
  if (scrollBridge.scrollNodeTouched) {
@@ -837,7 +823,7 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
837
823
  animateTo(positionIndex)
838
824
  }
839
825
 
840
- return PanResponder.create({
826
+ return {
841
827
  onMoveShouldSetPanResponder: onMoveShouldSet,
842
828
  // once we own the drag, don't yield it to another responder
843
829
  // (re-renders during the drag were cooperatively terminating it under
@@ -861,7 +847,7 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
861
847
  onPanResponderEnd: finish,
862
848
  onPanResponderTerminate: finish,
863
849
  onPanResponderRelease: finish,
864
- })
850
+ }
865
851
  }, [
866
852
  disableDrag,
867
853
  isShowingInnerSheet,
@@ -1110,16 +1096,11 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
1110
1096
  </SheetOverlayLayerContext.Provider>
1111
1097
 
1112
1098
  {snapPointsMode !== 'percent' && (
1113
- <View
1114
- style={{
1115
- opacity: 0,
1116
- position: 'absolute',
1117
- top: 0,
1118
- left: 0,
1119
- right: 0,
1120
- bottom: 0,
1121
- pointerEvents: 'none',
1122
- }}
1099
+ <TamaguiView
1100
+ opacity={0}
1101
+ position="absolute"
1102
+ inset={0}
1103
+ pointerEvents="none"
1123
1104
  onLayout={handleMaxContentViewLayout}
1124
1105
  />
1125
1106
  )}
@@ -1147,19 +1128,13 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
1147
1128
  animatedStyle,
1148
1129
  ]}
1149
1130
  >
1150
- {/* wrap children with plain RN View for panResponder - tamagui views no longer handle responder events on web */}
1151
- {gestureHandlerEnabled && panGesture ? (
1152
- <GestureDetectorWrapper gesture={panGesture} style={{ flex: 1 }}>
1153
- {animatedChildren}
1154
- </GestureDetectorWrapper>
1155
- ) : (
1156
- <View
1157
- {...panResponder?.panHandlers}
1158
- style={{ flex: 1, width: '100%', height: '100%' }}
1159
- >
1160
- {animatedChildren}
1161
- </View>
1162
- )}
1131
+ <SheetDragSurface
1132
+ gestureHandlerEnabled={gestureHandlerEnabled}
1133
+ panGesture={panGesture}
1134
+ panConfig={panConfig}
1135
+ >
1136
+ {animatedChildren}
1137
+ </SheetDragSurface>
1163
1138
  </AnimatedView>
1164
1139
  </SheetAnimatedPositionContext.Provider>
1165
1140
  </GestureSheetProvider>
@@ -1187,6 +1162,8 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
1187
1162
  const shouldMountChildren = unmountChildrenWhenHidden ? !isFullyClosed : true
1188
1163
 
1189
1164
  if (modal) {
1165
+ // an empty portal still mounts a full-screen wrapper in the native root host
1166
+ if (!shouldMountChildren) return null
1190
1167
  // a modal sheet is teleported through <Portal> to the root portal host.
1191
1168
  // that host is mounted by TamaguiProvider, which may sit ABOVE the app's
1192
1169
  // GestureHandlerRootView - so the teleported content lands outside any
@@ -1200,12 +1177,10 @@ export const SheetImplementationCustom = createRefComponent<View, SheetProps>(
1200
1177
  // collapses to 0 height while closed so it occupies no hit area - see
1201
1178
  // rnghRootStyleOpen/Closed above for why pointerEvents can't be used.
1202
1179
  const RNGHRoot = getGestureHandlerState().RootView
1203
- const mountedContents = shouldMountChildren ? (
1204
- <ContainerComponent>{contents}</ContainerComponent>
1205
- ) : null
1180
+ const mountedContents = <ContainerComponent>{contents}</ContainerComponent>
1206
1181
  const modalContents = (
1207
1182
  <Portal stackZIndex={zIndex} hidden={!isVisuallyActive} {...portalProps}>
1208
- {mountedContents && RNGHRoot ? (
1183
+ {RNGHRoot ? (
1209
1184
  <RNGHRoot style={open ? rnghRootStyleOpen : rnghRootStyleClosed}>
1210
1185
  {mountedContents}
1211
1186
  </RNGHRoot>
@@ -1,5 +1,11 @@
1
1
  import { composeRefs } from '@tamagui/compose-refs'
2
- import { createStyledHOC, isWeb, style, View, type GetProps } from '@tamagui/core'
2
+ import {
3
+ createRefComponent,
4
+ createStyledHOC,
5
+ isWeb,
6
+ View,
7
+ type GetProps,
8
+ } from '@tamagui/core'
3
9
  import type { ScrollViewRef } from '@tamagui/scroll-view'
4
10
  import { ScrollView } from '@tamagui/scroll-view'
5
11
  import { useControllableState } from '@tamagui/use-controllable-state'
@@ -18,7 +24,10 @@ import {
18
24
  } from './webViewport'
19
25
 
20
26
  const SHEET_SCROLL_VIEW_NAME = 'SheetScrollView'
21
- const sheetContentStyle = style({ minHeight: '100%' })
27
+ // plain object, not a style piece: these values are static (no theme tokens),
28
+ // and style() warns when a lazily-required module first evaluates during a
29
+ // render — which is exactly when a route-level sheet module loads.
30
+ const sheetContentStyle = { minHeight: '100%' }
22
31
 
23
32
  type SheetScrollViewBaseProps = GetProps<typeof ScrollView>
24
33
 
@@ -30,8 +39,7 @@ type SheetScrollViewProps = SheetScopedProps<
30
39
  }
31
40
  >
32
41
 
33
- export const SheetScrollView = createStyledHOC(
34
- ScrollView,
42
+ const SheetScrollViewCustom = createRefComponent<ScrollViewRef, SheetScrollViewProps>(
35
43
  (
36
44
  {
37
45
  scope,
@@ -44,6 +52,8 @@ export const SheetScrollView = createStyledHOC(
44
52
  ref
45
53
  ) => {
46
54
  const context = useSheetContext(scope)
55
+ if (context.onlyShowContainer)
56
+ throw new Error('Custom Sheet scrolling requires a custom Sheet')
47
57
  const gestureContext = useGestureSheetContext()
48
58
  const { scrollBridge, setHasScrollView, hasFit, screenSize } = context
49
59
  const keyboardOccludedHeight = Math.max(0, context.keyboardOccludedHeight || 0)
@@ -376,6 +386,18 @@ export const SheetScrollView = createStyledHOC(
376
386
  {contentWrapper}
377
387
  </ScrollView>
378
388
  )
389
+ }
390
+ )
391
+
392
+ export const SheetScrollView = createStyledHOC(
393
+ ScrollView,
394
+ ({ scope, ...props }: SheetScrollViewProps, ref) => {
395
+ const context = useSheetContext(scope)
396
+ return context.onlyShowContainer ? (
397
+ <ScrollView {...props} ref={ref} />
398
+ ) : (
399
+ <SheetScrollViewCustom {...props} scope={scope} ref={ref} />
400
+ )
379
401
  },
380
402
  {
381
403
  disableTheme: true,
@@ -0,0 +1,110 @@
1
+ import { createRefComponent } from '@tamagui/core'
2
+ import type { ViewInstance as View } from '@tamagui/react-native-types'
3
+ import { useMemo, useRef, useState } from 'react'
4
+ import { Platform } from 'react-native'
5
+ import { SheetNativeSystemContext, SheetProvider } from './SheetContext'
6
+ import type {
7
+ NativeSheetRenderer,
8
+ NativeSheetSnapPoint,
9
+ SheetNativePlatforms,
10
+ SheetProps,
11
+ } from './types'
12
+ import { useSheetOpenState } from './useSheetOpenState'
13
+ import { useSheetState } from './useSheetState'
14
+
15
+ const nativeSheets: Partial<Record<SheetNativePlatforms, NativeSheetImplementation>> = {}
16
+ type NativeSheetImplementation = ReturnType<typeof createRefComponent<View, SheetProps>>
17
+
18
+ export function getNativeSheet(platform: SheetNativePlatforms) {
19
+ if (Platform.OS !== platform) return null
20
+ return nativeSheets[platform] ?? null
21
+ }
22
+
23
+ export function setupNativeSheet(
24
+ platform: SheetNativePlatforms,
25
+ Renderer: NativeSheetRenderer
26
+ ) {
27
+ nativeSheets[platform] = createRefComponent<View, SheetProps>(
28
+ function NativeSheet(props, ref) {
29
+ // a native request cannot replace a controlled parent's accepted value.
30
+ const state = useSheetOpenState(props, 'prop-wins')
31
+ const { maxContentSize, ...sheetState } = useSheetState(props, state)
32
+ const { open, setOpen } = state
33
+ const openRef = useRef(open)
34
+ openRef.current = open
35
+ const [dismissed, setDismissed] = useState(!open)
36
+ const [previousOpen, setPreviousOpen] = useState(open)
37
+ if (previousOpen !== open) {
38
+ setPreviousOpen(open)
39
+ if (open) setDismissed(false)
40
+ }
41
+ const { snapPoints, snapPointsMode } = sheetState
42
+ const points = useMemo(() => {
43
+ const authored = props.dismissOnSnapToBottom
44
+ ? snapPoints.slice(0, -1)
45
+ : snapPoints
46
+ return authored.map((point): NativeSheetSnapPoint => {
47
+ if (point === 'fit' && (snapPointsMode === 'fit' || snapPointsMode === 'mixed'))
48
+ return { type: 'fit' }
49
+ const percent =
50
+ snapPointsMode === 'percent' ||
51
+ (snapPointsMode === 'mixed' &&
52
+ typeof point === 'string' &&
53
+ point.endsWith('%'))
54
+ const value =
55
+ percent && typeof point === 'string' ? Number(point.slice(0, -1)) : point
56
+ if (
57
+ typeof value !== 'number' ||
58
+ !Number.isFinite(value) ||
59
+ value < 0 ||
60
+ (percent && value > 100)
61
+ ) {
62
+ throw new Error(`Invalid native Sheet snap point: ${String(point)}`)
63
+ }
64
+ if (snapPointsMode === 'fit')
65
+ throw new Error('Native Sheet fit mode requires a fit snap point')
66
+ return { type: percent ? 'percent' : 'height', value }
67
+ })
68
+ }, [snapPoints, snapPointsMode, props.dismissOnSnapToBottom])
69
+ if (!points.length) throw new Error('Native Sheet requires a snap point')
70
+ const position =
71
+ open &&
72
+ (sheetState.position < 0 ||
73
+ (props.dismissOnSnapToBottom && sheetState.position === points.length))
74
+ ? 0
75
+ : sheetState.position
76
+ if (open && (!Number.isInteger(position) || position >= points.length))
77
+ throw new Error('Native Sheet position must select an authored snap point')
78
+ const {
79
+ defaultOpen,
80
+ defaultPosition,
81
+ unmountChildrenWhenHidden,
82
+ children,
83
+ ...rendererProps
84
+ } = props
85
+ return (
86
+ <SheetNativeSystemContext.Provider value>
87
+ <SheetProvider {...sheetState} onlyShowContainer>
88
+ <Renderer
89
+ {...rendererProps}
90
+ ref={ref}
91
+ unmountChildrenWhenHidden={unmountChildrenWhenHidden}
92
+ open={open}
93
+ onOpenChange={setOpen}
94
+ position={position}
95
+ onPositionChange={sheetState.setPosition}
96
+ snapPoints={points}
97
+ onDismiss={() => {
98
+ if (openRef.current) return
99
+ setDismissed(true)
100
+ state.onNativeDismiss?.()
101
+ }}
102
+ >
103
+ {unmountChildrenWhenHidden && dismissed ? null : children}
104
+ </Renderer>
105
+ </SheetProvider>
106
+ </SheetNativeSystemContext.Provider>
107
+ )
108
+ }
109
+ )
110
+ }
@@ -0,0 +1,16 @@
1
+ import type { Ref } from 'react'
2
+ import type { ViewInstance as View } from '@tamagui/react-native-types'
3
+ import type { ComponentType } from 'react'
4
+ import type { NativeSheetRenderer, SheetNativePlatforms, SheetProps } from './types'
5
+
6
+ // registration is native-only; web always uses Tamagui's own sheet.
7
+ export function getNativeSheet(
8
+ _platform: SheetNativePlatforms
9
+ ): ComponentType<SheetProps & { ref?: Ref<View> }> | null {
10
+ return null
11
+ }
12
+
13
+ export function setupNativeSheet(
14
+ _platform: SheetNativePlatforms,
15
+ _Renderer: NativeSheetRenderer
16
+ ) {}
@@ -0,0 +1,9 @@
1
+ import { Dimensions } from 'react-native'
2
+
3
+ /**
4
+ * Height of the viewport the sheet positions against. Native measures against
5
+ * the screen rather than the window, so it does not move with the keyboard.
6
+ */
7
+ export function getStableViewportHeight(): number {
8
+ return Dimensions.get('screen').height
9
+ }
@@ -0,0 +1,15 @@
1
+ import { getStableLayoutViewportHeight } from './webViewport'
2
+
3
+ /**
4
+ * Height of the viewport the sheet positions against.
5
+ *
6
+ * This MUST be the stable layout viewport: the visual viewport shrinks by the
7
+ * soft keyboard, and capping frameSize / maxContentSize against a shrinking
8
+ * value corrupts the fit-mode math (translateY = screenSize - frameSize),
9
+ * detaching the sheet's bottom from the screen edge when the keyboard opens.
10
+ * NOTE: window.innerHeight is NOT stable on real iOS Safari (it shrinks with
11
+ * the keyboard too), hence the self-correcting baseline in webViewport.
12
+ */
13
+ export function getStableViewportHeight(): number {
14
+ return getStableLayoutViewportHeight()
15
+ }
package/src/types.tsx CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { AnimatedNumberStrategy, TransitionProp } from '@tamagui/core'
2
2
  import type { PortalProps } from '@tamagui/portal'
3
+ import type { PanResponderConfig } from '@tamagui/react-native-pan-responder'
3
4
  import type { RemoveScroll } from '@tamagui/remove-scroll'
4
- import type { ReactNode } from 'react'
5
+ import type { ComponentType, ReactNode, Ref } from 'react'
6
+ import type { ViewInstance as View } from '@tamagui/react-native-types'
5
7
  import type React from 'react'
6
8
 
7
9
  export type SheetScopes = string
@@ -210,3 +212,44 @@ export interface KeyboardControllerSheetResult {
210
212
  */
211
213
  flushPendingHide: () => void
212
214
  }
215
+
216
+ export type SheetDragSurfaceProps = {
217
+ /** react-native-gesture-handler is set up and owns the drag (native only) */
218
+ gestureHandlerEnabled: boolean
219
+ /** the `Gesture.Pan` to attach when gesture handler owns the drag */
220
+ panGesture: any
221
+ /** the PanResponder config to drive the drag with otherwise, null when dragging is off */
222
+ panConfig: PanResponderConfig | null
223
+ children?: ReactNode
224
+ }
225
+
226
+ export type SheetNativePlatforms = 'ios'
227
+
228
+ export type NativeSheetSnapPoint =
229
+ | { type: 'percent'; value: number }
230
+ | { type: 'height'; value: number }
231
+ | { type: 'fit' }
232
+
233
+ export type NativeSheetRendererProps = Omit<
234
+ SheetProps,
235
+ | 'open'
236
+ | 'defaultOpen'
237
+ | 'onOpenChange'
238
+ | 'snapPoints'
239
+ | 'position'
240
+ | 'defaultPosition'
241
+ | 'onPositionChange'
242
+ | 'children'
243
+ > & {
244
+ open: boolean
245
+ onOpenChange: (open: boolean) => void
246
+ snapPoints: readonly NativeSheetSnapPoint[]
247
+ position: number
248
+ onPositionChange: (position: number) => void
249
+ /** report physical removal after accepted close; never report an interrupted close. */
250
+ onDismiss: () => void
251
+ children: ReactNode
252
+ ref?: Ref<View>
253
+ }
254
+
255
+ export type NativeSheetRenderer = ComponentType<NativeSheetRendererProps>
@@ -1,12 +1,8 @@
1
1
  import type { SheetContextValue } from './useSheetProviderProps'
2
2
 
3
- export const useSheetOffscreenSize = ({
4
- snapPoints,
5
- position,
6
- screenSize,
7
- frameSize,
8
- snapPointsMode,
9
- }: SheetContextValue) => {
3
+ export const useSheetOffscreenSize = (context: SheetContextValue) => {
4
+ if (context.onlyShowContainer) return 0
5
+ const { snapPoints, position, screenSize, frameSize, snapPointsMode } = context
10
6
  if (snapPointsMode === 'fit') {
11
7
  return 0
12
8
  }
@@ -1,4 +1,4 @@
1
- import { useAdaptContext, useAdaptTarget } from '@tamagui/adapt'
1
+ import { useAdaptContext, useAdaptTarget, useAdaptTargetScope } from '@tamagui/adapt'
2
2
  import React from 'react'
3
3
  import { useControllableState } from '@tamagui/use-controllable-state'
4
4
 
@@ -6,14 +6,19 @@ import type { SheetProps } from './types'
6
6
  import type { SheetControllerContextValue } from './useSheetController'
7
7
  import { useSheetController } from './useSheetController'
8
8
 
9
- export const useSheetOpenState = (props: SheetProps) => {
10
- const adaptContext = useAdaptContext()
11
- const adapt = useAdaptTarget()
9
+ export const useSheetOpenState = (
10
+ props: SheetProps,
11
+ strategy: 'prop-wins' | 'most-recent-wins' = 'most-recent-wins'
12
+ ) => {
13
+ const adaptTargetScope = useAdaptTargetScope()
14
+ const adapt = useAdaptTarget(adaptTargetScope ?? undefined)
15
+ const adaptContext = useAdaptContext(adaptTargetScope ?? undefined)
12
16
  const { isHidden: controllerIsHidden, controller: legacyController } =
13
17
  useSheetController(props.scope)
14
- const shouldUseAdapt = Boolean(
15
- adaptContext.open !== undefined || adaptContext.onOpenChange
16
- )
18
+ // an adapt parent can wrap unrelated sheets; only its <Adapt> child owns the handoff.
19
+ const shouldUseAdapt =
20
+ adaptTargetScope === adaptContext.scopeName &&
21
+ Boolean(adaptContext.open !== undefined || adaptContext.onOpenChange)
17
22
  // Dialog no longer mounts SheetController while Adapt is inactive, so the
18
23
  // hidden state must come from the Adapt parent context even before a target
19
24
  // can register through useAdaptTarget().
@@ -65,7 +70,7 @@ export const useSheetOpenState = (props: SheetProps) => {
65
70
  prop: propVal,
66
71
  defaultProp: props.defaultOpen ?? false,
67
72
  onChange: onOpenChangeInternal,
68
- strategy: 'most-recent-wins',
73
+ strategy,
69
74
  })
70
75
 
71
76
  return {
@@ -73,6 +78,11 @@ export const useSheetOpenState = (props: SheetProps) => {
73
78
  setOpen,
74
79
  isHidden,
75
80
  controller,
81
+ strategy,
82
+ onNativeDismiss: shouldUseAdapt
83
+ ? () =>
84
+ adapt?.handoff.onTransition({ phase: 'end', cause: 'close', finished: true })
85
+ : undefined,
76
86
  }
77
87
  }
78
88