@tamagui/sheet 1.20.6 → 1.21.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 (144) hide show
  1. package/dist/cjs/Sheet.js +124 -210
  2. package/dist/cjs/Sheet.js.map +2 -2
  3. package/dist/cjs/SheetController.js +49 -0
  4. package/dist/cjs/SheetController.js.map +6 -0
  5. package/dist/cjs/getNativeSheet.js +39 -0
  6. package/dist/cjs/getNativeSheet.js.map +6 -0
  7. package/dist/cjs/index.js +14 -1
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/native-ios.js +5 -0
  10. package/dist/cjs/native-ios.js.map +6 -0
  11. package/dist/cjs/nativeSheet.js +58 -0
  12. package/dist/cjs/nativeSheet.js.map +6 -0
  13. package/dist/cjs/useSheetChildren.js +68 -0
  14. package/dist/cjs/useSheetChildren.js.map +6 -0
  15. package/dist/cjs/useSheetContoller.js +44 -0
  16. package/dist/cjs/useSheetContoller.js.map +6 -0
  17. package/dist/cjs/useSheetController.js +44 -0
  18. package/dist/cjs/useSheetController.js.map +6 -0
  19. package/dist/cjs/useSheetOpenState.js +51 -0
  20. package/dist/cjs/useSheetOpenState.js.map +6 -0
  21. package/dist/cjs/useSheetProviderProps.js +98 -0
  22. package/dist/cjs/useSheetProviderProps.js.map +6 -0
  23. package/dist/esm/Sheet.js +123 -200
  24. package/dist/esm/Sheet.js.map +2 -2
  25. package/dist/esm/Sheet.mjs +123 -200
  26. package/dist/esm/Sheet.mjs.map +2 -2
  27. package/dist/esm/SheetController.js +25 -0
  28. package/dist/esm/SheetController.js.map +6 -0
  29. package/dist/esm/SheetController.mjs +25 -0
  30. package/dist/esm/SheetController.mjs.map +6 -0
  31. package/dist/esm/getNativeSheet.js +14 -0
  32. package/dist/esm/getNativeSheet.js.map +6 -0
  33. package/dist/esm/getNativeSheet.mjs +14 -0
  34. package/dist/esm/getNativeSheet.mjs.map +6 -0
  35. package/dist/esm/index.js +6 -0
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/index.mjs +6 -0
  38. package/dist/esm/index.mjs.map +1 -1
  39. package/dist/esm/native-ios.js +4 -0
  40. package/dist/esm/native-ios.js.map +6 -0
  41. package/dist/esm/native-ios.mjs +4 -0
  42. package/dist/esm/native-ios.mjs.map +6 -0
  43. package/dist/esm/nativeSheet.js +33 -0
  44. package/dist/esm/nativeSheet.js.map +6 -0
  45. package/dist/esm/nativeSheet.mjs +33 -0
  46. package/dist/esm/nativeSheet.mjs.map +6 -0
  47. package/dist/esm/useSheetChildren.js +34 -0
  48. package/dist/esm/useSheetChildren.js.map +6 -0
  49. package/dist/esm/useSheetChildren.mjs +34 -0
  50. package/dist/esm/useSheetChildren.mjs.map +6 -0
  51. package/dist/esm/useSheetContoller.js +19 -0
  52. package/dist/esm/useSheetContoller.js.map +6 -0
  53. package/dist/esm/useSheetContoller.mjs +19 -0
  54. package/dist/esm/useSheetContoller.mjs.map +6 -0
  55. package/dist/esm/useSheetController.js +19 -0
  56. package/dist/esm/useSheetController.js.map +6 -0
  57. package/dist/esm/useSheetController.mjs +19 -0
  58. package/dist/esm/useSheetController.mjs.map +6 -0
  59. package/dist/esm/useSheetOpenState.js +27 -0
  60. package/dist/esm/useSheetOpenState.js.map +6 -0
  61. package/dist/esm/useSheetOpenState.mjs +27 -0
  62. package/dist/esm/useSheetOpenState.mjs.map +6 -0
  63. package/dist/esm/useSheetProviderProps.js +64 -0
  64. package/dist/esm/useSheetProviderProps.js.map +6 -0
  65. package/dist/esm/useSheetProviderProps.mjs +64 -0
  66. package/dist/esm/useSheetProviderProps.mjs.map +6 -0
  67. package/dist/jsx/Sheet.js +88 -159
  68. package/dist/jsx/Sheet.js.map +2 -2
  69. package/dist/jsx/Sheet.mjs +88 -159
  70. package/dist/jsx/Sheet.mjs.map +2 -2
  71. package/dist/jsx/SheetController.js +24 -0
  72. package/dist/jsx/SheetController.js.map +6 -0
  73. package/dist/jsx/SheetController.mjs +24 -0
  74. package/dist/jsx/SheetController.mjs.map +6 -0
  75. package/dist/jsx/getNativeSheet.js +14 -0
  76. package/dist/jsx/getNativeSheet.js.map +6 -0
  77. package/dist/jsx/getNativeSheet.mjs +14 -0
  78. package/dist/jsx/getNativeSheet.mjs.map +6 -0
  79. package/dist/jsx/index.js +6 -0
  80. package/dist/jsx/index.js.map +1 -1
  81. package/dist/jsx/index.mjs +6 -0
  82. package/dist/jsx/index.mjs.map +1 -1
  83. package/dist/jsx/native-ios.js +4 -0
  84. package/dist/jsx/native-ios.js.map +6 -0
  85. package/dist/jsx/native-ios.mjs +4 -0
  86. package/dist/jsx/native-ios.mjs.map +6 -0
  87. package/dist/jsx/nativeSheet.js +31 -0
  88. package/dist/jsx/nativeSheet.js.map +6 -0
  89. package/dist/jsx/nativeSheet.mjs +31 -0
  90. package/dist/jsx/nativeSheet.mjs.map +6 -0
  91. package/dist/jsx/useSheetChildren.js +33 -0
  92. package/dist/jsx/useSheetChildren.js.map +6 -0
  93. package/dist/jsx/useSheetChildren.mjs +33 -0
  94. package/dist/jsx/useSheetChildren.mjs.map +6 -0
  95. package/dist/jsx/useSheetContoller.js +19 -0
  96. package/dist/jsx/useSheetContoller.js.map +6 -0
  97. package/dist/jsx/useSheetContoller.mjs +19 -0
  98. package/dist/jsx/useSheetContoller.mjs.map +6 -0
  99. package/dist/jsx/useSheetController.js +19 -0
  100. package/dist/jsx/useSheetController.js.map +6 -0
  101. package/dist/jsx/useSheetController.mjs +19 -0
  102. package/dist/jsx/useSheetController.mjs.map +6 -0
  103. package/dist/jsx/useSheetOpenState.js +26 -0
  104. package/dist/jsx/useSheetOpenState.js.map +6 -0
  105. package/dist/jsx/useSheetOpenState.mjs +26 -0
  106. package/dist/jsx/useSheetOpenState.mjs.map +6 -0
  107. package/dist/jsx/useSheetProviderProps.js +64 -0
  108. package/dist/jsx/useSheetProviderProps.js.map +6 -0
  109. package/dist/jsx/useSheetProviderProps.mjs +64 -0
  110. package/dist/jsx/useSheetProviderProps.mjs.map +6 -0
  111. package/package.json +14 -14
  112. package/src/Sheet.tsx +97 -183
  113. package/src/SheetController.tsx +28 -0
  114. package/src/index.ts +3 -0
  115. package/src/nativeSheet.tsx +42 -0
  116. package/src/types.tsx +6 -0
  117. package/src/useSheetChildren.tsx +33 -0
  118. package/src/useSheetController.tsx +24 -0
  119. package/src/useSheetOpenState.tsx +30 -0
  120. package/src/useSheetProviderProps.tsx +71 -0
  121. package/types/Sheet.d.ts +31 -42
  122. package/types/Sheet.d.ts.map +1 -1
  123. package/types/SheetController.d.ts +6 -0
  124. package/types/SheetController.d.ts.map +1 -0
  125. package/types/getNativeSheet.d.ts +3 -0
  126. package/types/getNativeSheet.d.ts.map +1 -0
  127. package/types/index.d.ts +3 -0
  128. package/types/index.d.ts.map +1 -1
  129. package/types/native-ios.d.ts +2 -0
  130. package/types/native-ios.d.ts.map +1 -0
  131. package/types/nativeSheet.d.ts +7 -0
  132. package/types/nativeSheet.d.ts.map +1 -0
  133. package/types/types.d.ts +4 -0
  134. package/types/types.d.ts.map +1 -1
  135. package/types/useSheetChildren.d.ts +6 -0
  136. package/types/useSheetChildren.d.ts.map +1 -0
  137. package/types/useSheetContoller.d.ts +14 -0
  138. package/types/useSheetContoller.d.ts.map +1 -0
  139. package/types/useSheetController.d.ts +14 -0
  140. package/types/useSheetController.d.ts.map +1 -0
  141. package/types/useSheetOpenState.d.ts +10 -0
  142. package/types/useSheetOpenState.d.ts.map +1 -0
  143. package/types/useSheetProviderProps.d.ts +22 -0
  144. package/types/useSheetProviderProps.d.ts.map +1 -0
package/src/Sheet.tsx CHANGED
@@ -2,8 +2,6 @@ import { AdaptParentContext } from '@tamagui/adapt'
2
2
  import { useComposedRefs } from '@tamagui/compose-refs'
3
3
  import {
4
4
  GetProps,
5
- Slot,
6
- TamaguiElement,
7
5
  Theme,
8
6
  isClient,
9
7
  isTouchable,
@@ -21,15 +19,12 @@ import {
21
19
  import { Portal } from '@tamagui/portal'
22
20
  import { RemoveScroll } from '@tamagui/remove-scroll'
23
21
  import { ThemeableStack, XStack, XStackProps, YStack, YStackProps } from '@tamagui/stacks'
24
- import { useConstant } from '@tamagui/use-constant'
25
- import { useControllableState } from '@tamagui/use-controllable-state'
26
22
  import { useKeyboardVisible } from '@tamagui/use-keyboard-visible'
27
23
  import React, {
28
24
  FunctionComponent,
29
25
  RefAttributes,
30
26
  createContext,
31
27
  forwardRef,
32
- isValidElement,
33
28
  useCallback,
34
29
  useContext,
35
30
  useEffect,
@@ -43,15 +38,22 @@ import {
43
38
  Keyboard,
44
39
  PanResponder,
45
40
  PanResponderGestureState,
41
+ Platform,
46
42
  View,
47
43
  } from 'react-native'
48
44
 
49
45
  import { SHEET_HANDLE_NAME, SHEET_NAME } from './constants'
46
+ import { getNativeSheet } from './nativeSheet'
50
47
  import { SheetProvider, useSheetContext } from './SheetContext'
51
48
  import { SheetScrollView } from './SheetScrollView'
52
- import { ScrollBridge, SheetProps, SheetScopedProps } from './types'
49
+ import { SheetProps, SheetScopedProps } from './types'
50
+ import { useSheetChildren } from './useSheetChildren'
51
+ import { useSheetController } from './useSheetController'
52
+ import { useSheetOpenState } from './useSheetOpenState'
53
+ import { useSheetProviderProps } from './useSheetProviderProps'
53
54
 
54
55
  export { createSheetScope } from './SheetContext'
56
+ export * from './types'
55
57
 
56
58
  /* -------------------------------------------------------------------------------------------------
57
59
  * SheetHandle
@@ -59,17 +61,6 @@ export { createSheetScope } from './SheetContext'
59
61
 
60
62
  export const SheetHandleFrame = styled(XStack, {
61
63
  name: SHEET_HANDLE_NAME,
62
- height: 10,
63
- borderRadius: 100,
64
- backgroundColor: '$background',
65
- zIndex: 10,
66
- marginHorizontal: '35%',
67
- marginBottom: '$2',
68
- opacity: 0.5,
69
-
70
- hoverStyle: {
71
- opacity: 0.7,
72
- },
73
64
 
74
65
  variants: {
75
66
  open: {
@@ -81,7 +72,27 @@ export const SheetHandleFrame = styled(XStack, {
81
72
  pointerEvents: 'none',
82
73
  },
83
74
  },
75
+
76
+ unstyled: {
77
+ false: {
78
+ height: 10,
79
+ borderRadius: 100,
80
+ backgroundColor: '$background',
81
+ zIndex: 10,
82
+ marginHorizontal: '35%',
83
+ marginBottom: '$2',
84
+ opacity: 0.5,
85
+
86
+ hoverStyle: {
87
+ opacity: 0.7,
88
+ },
89
+ },
90
+ },
84
91
  } as const,
92
+
93
+ defaultVariants: {
94
+ unstyled: false,
95
+ },
85
96
  })
86
97
 
87
98
  export const SheetHandle = SheetHandleFrame.extractable(
@@ -110,10 +121,6 @@ const SHEET_OVERLAY_NAME = 'SheetOverlay'
110
121
 
111
122
  export const SheetOverlayFrame = styled(ThemeableStack, {
112
123
  name: SHEET_OVERLAY_NAME,
113
- fullscreen: true,
114
- backgrounded: true,
115
- opacity: 0.5,
116
- zIndex: 100_000,
117
124
 
118
125
  variants: {
119
126
  closed: {
@@ -125,7 +132,20 @@ export const SheetOverlayFrame = styled(ThemeableStack, {
125
132
  pointerEvents: 'auto',
126
133
  },
127
134
  },
135
+
136
+ unstyled: {
137
+ false: {
138
+ fullscreen: true,
139
+ position: 'absolute',
140
+ backgrounded: true,
141
+ zIndex: 100_000,
142
+ },
143
+ },
128
144
  } as const,
145
+
146
+ defaultVariants: {
147
+ unstyled: false,
148
+ },
129
149
  })
130
150
 
131
151
  export type SheetOverlayProps = GetProps<typeof SheetOverlayFrame>
@@ -161,14 +181,25 @@ if (selectionStyleSheet) {
161
181
 
162
182
  export const SheetFrameFrame = styled(YStack, {
163
183
  name: SHEET_NAME,
164
- flex: 1,
165
- backgroundColor: '$background',
166
- borderTopLeftRadius: '$true',
167
- borderTopRightRadius: '$true',
168
- width: '100%',
169
- maxHeight: '100%',
170
- overflow: 'hidden',
171
- pointerEvents: 'auto',
184
+
185
+ variants: {
186
+ unstyled: {
187
+ false: {
188
+ flex: 1,
189
+ backgroundColor: '$background',
190
+ borderTopLeftRadius: '$true',
191
+ borderTopRightRadius: '$true',
192
+ width: '100%',
193
+ maxHeight: '100%',
194
+ overflow: 'hidden',
195
+ pointerEvents: 'auto',
196
+ },
197
+ },
198
+ } as const,
199
+
200
+ defaultVariants: {
201
+ unstyled: false,
202
+ },
172
203
  })
173
204
 
174
205
  export const SheetFrame = SheetFrameFrame.extractable(
@@ -195,21 +226,21 @@ const ParentSheetContext = createContext({
195
226
  zIndex: 100_000,
196
227
  })
197
228
 
198
- const useSheetContoller = () => {
199
- const controller = useContext(SheetControllerContext)
200
- const isHidden = controller?.hidden
201
- const isShowingNonSheet = isHidden && controller?.open
202
- return {
203
- controller,
204
- isHidden,
205
- isShowingNonSheet,
206
- }
207
- }
208
-
209
229
  export const Sheet = withStaticProperties(
210
230
  forwardRef<View, SheetProps>(function Sheet(props, ref) {
211
231
  const hydrated = useDidFinishSSR()
212
- const { isShowingNonSheet } = useSheetContoller()
232
+ const { isShowingNonSheet } = useSheetController()
233
+
234
+ let SheetImplementation = SheetImplementationCustom
235
+
236
+ if (props.native && Platform.OS === 'ios') {
237
+ if (process.env.TAMAGUI_TARGET === 'native') {
238
+ const impl = getNativeSheet('ios')
239
+ if (impl) {
240
+ SheetImplementation = impl
241
+ }
242
+ }
243
+ }
213
244
 
214
245
  /**
215
246
  * Performance is sensitive here so avoid all the hooks below with this
@@ -223,22 +254,11 @@ export const Sheet = withStaticProperties(
223
254
  sheetComponents
224
255
  )
225
256
 
226
- const SheetImplementation = themeable(
227
- forwardRef<View, SheetProps>(function SheetImplementation(props, forwardedRef) {
257
+ const SheetImplementationCustom = themeable(
258
+ forwardRef<View, SheetProps>(function SheetImplementationCustom(props, forwardedRef) {
228
259
  const parentSheet = useContext(ParentSheetContext)
229
- const { isHidden, controller } = useSheetContoller()
230
260
 
231
261
  const {
232
- __scopeSheet,
233
- snapPoints: snapPointsProp = [80],
234
- open: openProp,
235
- defaultOpen,
236
- children: childrenProp,
237
- position: positionProp,
238
- onPositionChange,
239
- onOpenChange,
240
- defaultPosition,
241
- dismissOnOverlayPress = true,
242
262
  animationConfig,
243
263
  dismissOnSnapToBottom = false,
244
264
  forceRemoveScrollEnabled = null,
@@ -249,8 +269,28 @@ const SheetImplementation = themeable(
249
269
  portalProps,
250
270
  } = props
251
271
 
272
+ const state = useSheetOpenState(props)
273
+ const providerProps = useSheetProviderProps(props, state)
274
+
275
+ const {
276
+ snapPoints,
277
+ position,
278
+ contentRef,
279
+ setPosition,
280
+ setPositionImmediate,
281
+ scrollBridge,
282
+ frameSize,
283
+ setFrameSize,
284
+ } = providerProps
285
+
286
+ const { open, isHidden, controller } = state
287
+
288
+ const { frameComponent, handleComponent, overlayComponent } = useSheetChildren(
289
+ props.children
290
+ )
291
+
252
292
  if (process.env.NODE_ENV === 'development') {
253
- if (snapPointsProp.some((p) => p < 0 || p > 100)) {
293
+ if (snapPoints.some((p) => p < 0 || p > 100)) {
254
294
  console.warn(
255
295
  '⚠️ Invalid snapPoint given, snapPoints must be between 0 and 100, equal to percent height of frame'
256
296
  )
@@ -268,66 +308,12 @@ const SheetImplementation = themeable(
268
308
  const disableDrag = disableDragProp ?? controller?.disableDrag
269
309
  const keyboardIsVisible = useKeyboardVisible()
270
310
  const themeName = useThemeName()
271
- const contentRef = React.useRef<TamaguiElement>(null)
272
- const scrollBridge = useConstant<ScrollBridge>(() => ({
273
- enabled: false,
274
- y: 0,
275
- paneY: 0,
276
- paneMinY: 0,
277
- scrollStartY: -1,
278
- drag: () => {},
279
- release: () => {},
280
- scrollLock: false,
281
- }))
282
-
283
- const onOpenChangeInternal = (val: boolean) => {
284
- controller?.onOpenChange?.(val)
285
- onOpenChange?.(val)
286
- }
287
-
288
- const [open, setOpen] = useControllableState({
289
- prop: controller?.open ?? openProp,
290
- defaultProp: true,
291
- onChange: onOpenChangeInternal,
292
- strategy: 'most-recent-wins',
293
- transition: true,
294
- })
295
-
296
- const [frameSize, setFrameSize] = useState<number>(0)
297
-
298
- const snapPoints = useMemo(
299
- () => (dismissOnSnapToBottom ? [...snapPointsProp, 0] : snapPointsProp),
300
- // eslint-disable-next-line react-hooks/exhaustive-deps
301
- [JSON.stringify(snapPointsProp), dismissOnSnapToBottom]
302
- )
303
-
304
- // lets set -1 to be always the "open = false" position
305
- const [position_, setPosition_] = useControllableState({
306
- prop: positionProp,
307
- defaultProp: defaultPosition || (open ? 0 : -1),
308
- onChange: onPositionChange,
309
- strategy: 'most-recent-wins',
310
- transition: true,
311
- })
312
- const position = open === false ? -1 : position_
313
311
 
314
312
  // reset position to fully open on re-open after dismissOnSnapToBottom
315
313
  if (open && dismissOnSnapToBottom && position === snapPoints.length - 1) {
316
- setPosition_(0)
314
+ setPositionImmediate(0)
317
315
  }
318
316
 
319
- const setPosition = useCallback(
320
- (next: number) => {
321
- // close on dismissOnSnapToBottom (and set position so it animates)
322
- if (dismissOnSnapToBottom && next === snapPoints.length - 1) {
323
- setOpen(false)
324
- } else {
325
- setPosition_(next)
326
- }
327
- },
328
- [dismissOnSnapToBottom, snapPoints.length, setPosition_, setOpen]
329
- )
330
-
331
317
  const { useAnimatedNumber, useAnimatedNumberReaction, useAnimatedNumberStyle } =
332
318
  driver
333
319
 
@@ -540,30 +526,6 @@ const SheetImplementation = themeable(
540
526
  [disableDrag, isShowingInnerSheet, animateTo, frameSize, positions, setPosition]
541
527
  )
542
528
 
543
- let handleComponent: React.ReactElement | null = null
544
- let overlayComponent: React.ReactElement | null = null
545
- let frameComponent: React.ReactElement | null = null
546
-
547
- // TODO do more radix-like and don't require direct children descendents
548
- React.Children.forEach(childrenProp, (child) => {
549
- if (isValidElement(child)) {
550
- const name = child.type?.['staticConfig']?.componentName
551
- switch (name) {
552
- case 'SheetHandle':
553
- handleComponent = child
554
- break
555
- case 'Sheet':
556
- frameComponent = child
557
- break
558
- case 'SheetOverlay':
559
- overlayComponent = child
560
- break
561
- default:
562
- console.warn('Warning: passed invalid child to Sheet', child)
563
- }
564
- }
565
- })
566
-
567
529
  const animatedStyle = useAnimatedNumberStyle(animatedNumber, (val) => {
568
530
  'worklet'
569
531
  const translateY = frameSize === 0 ? HIDDEN_SIZE : val
@@ -629,21 +591,7 @@ const SheetImplementation = themeable(
629
591
 
630
592
  const contents = (
631
593
  <ParentSheetContext.Provider value={nextParentContext}>
632
- <SheetProvider
633
- modal={modal}
634
- contentRef={contentRef}
635
- frameSize={frameSize}
636
- dismissOnOverlayPress={dismissOnOverlayPress}
637
- dismissOnSnapToBottom={dismissOnSnapToBottom}
638
- open={open}
639
- hidden={!!isHidden}
640
- scope={__scopeSheet}
641
- position={position}
642
- snapPoints={snapPoints}
643
- setPosition={setPosition}
644
- setOpen={setOpen}
645
- scrollBridge={scrollBridge}
646
- >
594
+ <SheetProvider {...providerProps}>
647
595
  {shouldHideParentSheet ? null : overlayComponent}
648
596
 
649
597
  <AnimatedView
@@ -742,37 +690,3 @@ function resisted(y: number, minY: number, maxOverflow = 25) {
742
690
  }
743
691
  return y
744
692
  }
745
-
746
- type SheetControllerContextValue = {
747
- disableDrag?: boolean
748
- open?: boolean
749
- // hide without "closing" to prevent re-animation when shown again
750
- hidden?: boolean
751
- onOpenChange?: React.Dispatch<React.SetStateAction<boolean>> | ((val: boolean) => void)
752
- }
753
-
754
- const SheetControllerContext = createContext<SheetControllerContextValue | null>(null)
755
-
756
- export const SheetController = ({
757
- children,
758
- onOpenChange: onOpenChangeProp,
759
- ...value
760
- }: Partial<SheetControllerContextValue> & { children?: React.ReactNode }) => {
761
- const onOpenChange = useEvent(onOpenChangeProp)
762
-
763
- const memoValue = useMemo(
764
- () => ({
765
- open: value.open,
766
- hidden: value.hidden,
767
- disableDrag: value.disableDrag,
768
- onOpenChange,
769
- }),
770
- [onOpenChange, value.open, value.hidden, value.disableDrag]
771
- )
772
-
773
- return (
774
- <SheetControllerContext.Provider value={memoValue}>
775
- {children}
776
- </SheetControllerContext.Provider>
777
- )
778
- }
@@ -0,0 +1,28 @@
1
+ import { useEvent } from '@tamagui/core'
2
+ import React, { useMemo } from 'react'
3
+
4
+ import { SheetControllerContext, SheetControllerContextValue } from './useSheetController'
5
+
6
+ export const SheetController = ({
7
+ children,
8
+ onOpenChange: onOpenChangeProp,
9
+ ...value
10
+ }: Partial<SheetControllerContextValue> & { children?: React.ReactNode }) => {
11
+ const onOpenChange = useEvent(onOpenChangeProp)
12
+
13
+ const memoValue = useMemo(
14
+ () => ({
15
+ open: value.open,
16
+ hidden: value.hidden,
17
+ disableDrag: value.disableDrag,
18
+ onOpenChange,
19
+ }),
20
+ [onOpenChange, value.open, value.hidden, value.disableDrag]
21
+ )
22
+
23
+ return (
24
+ <SheetControllerContext.Provider value={memoValue}>
25
+ {children}
26
+ </SheetControllerContext.Provider>
27
+ )
28
+ }
package/src/index.ts CHANGED
@@ -1 +1,4 @@
1
1
  export * from './Sheet'
2
+ export * from './SheetController'
3
+ export * from './useSheetController'
4
+ export { setupNativeSheet } from './nativeSheet'
@@ -0,0 +1,42 @@
1
+ import { FunctionComponent, useEffect, useRef } from 'react'
2
+
3
+ import { SheetProvider } from './SheetContext'
4
+ import { SheetProps } from './types'
5
+ import { useSheetChildren } from './useSheetChildren'
6
+ import { useSheetOpenState } from './useSheetOpenState'
7
+ import { useSheetProviderProps } from './useSheetProviderProps'
8
+
9
+ type SheetNativePlatforms = 'ios'
10
+
11
+ const nativeSheets: Record<SheetNativePlatforms, FunctionComponent<SheetProps> | null> = {
12
+ ios: null,
13
+ }
14
+
15
+ export function getNativeSheet(platform: SheetNativePlatforms) {
16
+ return nativeSheets[platform]
17
+ }
18
+
19
+ export function setupNativeSheet(platform: SheetNativePlatforms, Implementation: any) {
20
+ if (platform === 'ios') {
21
+ nativeSheets[platform] = (props: SheetProps) => {
22
+ const state = useSheetOpenState(props)
23
+ const providerProps = useSheetProviderProps(props, state)
24
+ const { frameComponent } = useSheetChildren(props.children)
25
+
26
+ const { open, setOpen } = state
27
+ const ref = useRef<{
28
+ setVisibility: Function
29
+ }>()
30
+
31
+ useEffect(() => {
32
+ ref.current?.setVisibility(open)
33
+ }, [open])
34
+
35
+ return (
36
+ <Implementation ref={ref} onModalDismiss={() => setOpen(false)}>
37
+ <SheetProvider {...providerProps}>{frameComponent}</SheetProvider>
38
+ </Implementation>
39
+ )
40
+ }
41
+ }
42
+ }
package/src/types.tsx CHANGED
@@ -18,6 +18,12 @@ export type SheetProps = ScopedProps<
18
18
  dismissOnSnapToBottom?: boolean
19
19
  forceRemoveScrollEnabled?: boolean
20
20
  animationConfig?: AnimatedNumberStrategy
21
+
22
+ /**
23
+ * Adapts the sheet to use native sheet on the given platform (if available)
24
+ */
25
+ native?: 'ios'[] | boolean
26
+
21
27
  /**
22
28
  * Pass if you're using the CSS animation driver
23
29
  */
@@ -0,0 +1,33 @@
1
+ import React, { isValidElement } from 'react'
2
+
3
+ export function useSheetChildren(childrenProp: any) {
4
+ let handleComponent: React.ReactElement | null = null
5
+ let overlayComponent: React.ReactElement | null = null
6
+ let frameComponent: React.ReactElement | null = null
7
+
8
+ // TODO do more radix-like and don't require direct children descendents
9
+ React.Children.forEach(childrenProp, (child) => {
10
+ if (isValidElement(child)) {
11
+ const name = child.type?.['staticConfig']?.componentName
12
+ switch (name) {
13
+ case 'SheetHandle':
14
+ handleComponent = child
15
+ break
16
+ case 'Sheet':
17
+ frameComponent = child
18
+ break
19
+ case 'SheetOverlay':
20
+ overlayComponent = child
21
+ break
22
+ default:
23
+ console.warn('Warning: passed invalid child to Sheet', child)
24
+ }
25
+ }
26
+ })
27
+
28
+ return {
29
+ handleComponent,
30
+ overlayComponent,
31
+ frameComponent,
32
+ }
33
+ }
@@ -0,0 +1,24 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ export const useSheetController = () => {
4
+ const controller = useContext(SheetControllerContext)
5
+ const isHidden = controller?.hidden
6
+ const isShowingNonSheet = isHidden && controller?.open
7
+ return {
8
+ controller,
9
+ isHidden,
10
+ isShowingNonSheet,
11
+ }
12
+ }
13
+
14
+ export const SheetControllerContext = createContext<SheetControllerContextValue | null>(
15
+ null
16
+ )
17
+
18
+ export type SheetControllerContextValue = {
19
+ disableDrag?: boolean
20
+ open?: boolean
21
+ // hide without "closing" to prevent re-animation when shown again
22
+ hidden?: boolean
23
+ onOpenChange?: React.Dispatch<React.SetStateAction<boolean>> | ((val: boolean) => void)
24
+ }
@@ -0,0 +1,30 @@
1
+ import { useControllableState } from '@tamagui/use-controllable-state'
2
+
3
+ import { SheetProps } from './types'
4
+ import { useSheetController } from './useSheetController'
5
+
6
+ export const useSheetOpenState = (props: SheetProps) => {
7
+ const { isHidden, controller } = useSheetController()
8
+
9
+ const onOpenChangeInternal = (val: boolean) => {
10
+ controller?.onOpenChange?.(val)
11
+ props.onOpenChange?.(val)
12
+ }
13
+
14
+ const [open, setOpen] = useControllableState({
15
+ prop: controller?.open ?? props.open,
16
+ defaultProp: props.defaultOpen ?? false,
17
+ onChange: onOpenChangeInternal,
18
+ strategy: 'most-recent-wins',
19
+ transition: true,
20
+ })
21
+
22
+ return {
23
+ open,
24
+ setOpen,
25
+ isHidden,
26
+ controller,
27
+ }
28
+ }
29
+
30
+ export type SheetOpenState = ReturnType<typeof useSheetOpenState>
@@ -0,0 +1,71 @@
1
+ import { TamaguiElement } from '@tamagui/core'
2
+ import { useConstant } from '@tamagui/use-constant'
3
+ import { useControllableState } from '@tamagui/use-controllable-state'
4
+ import React, { useCallback, useMemo, useState } from 'react'
5
+
6
+ import { ScrollBridge, SheetProps } from './types'
7
+ import { SheetOpenState } from './useSheetOpenState'
8
+
9
+ export function useSheetProviderProps(props: SheetProps, state: SheetOpenState) {
10
+ const contentRef = React.useRef<TamaguiElement>(null)
11
+ const [frameSize, setFrameSize] = useState<number>(0)
12
+ const snapPointsProp = props.snapPoints || [80]
13
+
14
+ const snapPoints = useMemo(
15
+ () => (props.dismissOnSnapToBottom ? [...snapPointsProp, 0] : snapPointsProp),
16
+ // eslint-disable-next-line react-hooks/exhaustive-deps
17
+ [JSON.stringify(snapPointsProp), props.dismissOnSnapToBottom]
18
+ )
19
+
20
+ // lets set -1 to be always the "open = false" position
21
+ const [position_, setPositionImmediate] = useControllableState({
22
+ prop: props.position,
23
+ defaultProp: props.defaultPosition || (state.open ? 0 : -1),
24
+ onChange: props.onPositionChange,
25
+ strategy: 'most-recent-wins',
26
+ transition: true,
27
+ })
28
+
29
+ const position = state.open === false ? -1 : position_
30
+
31
+ const setPosition = useCallback(
32
+ (next: number) => {
33
+ // close on dismissOnSnapToBottom (and set position so it animates)
34
+ if (props.dismissOnSnapToBottom && next === snapPoints.length - 1) {
35
+ state.setOpen(false)
36
+ } else {
37
+ setPositionImmediate(next)
38
+ }
39
+ },
40
+ [props.dismissOnSnapToBottom, snapPoints.length, setPositionImmediate, state.setOpen]
41
+ )
42
+
43
+ const scrollBridge = useConstant<ScrollBridge>(() => ({
44
+ enabled: false,
45
+ y: 0,
46
+ paneY: 0,
47
+ paneMinY: 0,
48
+ scrollStartY: -1,
49
+ drag: () => {},
50
+ release: () => {},
51
+ scrollLock: false,
52
+ }))
53
+
54
+ return {
55
+ modal: !!props.modal,
56
+ open: state.open,
57
+ setOpen: state.setOpen,
58
+ hidden: !!state.isHidden,
59
+ contentRef,
60
+ frameSize,
61
+ setFrameSize,
62
+ dismissOnOverlayPress: props.dismissOnOverlayPress ?? true,
63
+ dismissOnSnapToBottom: props.dismissOnSnapToBottom ?? false,
64
+ scope: props.__scopeSheet,
65
+ position,
66
+ snapPoints,
67
+ setPosition,
68
+ setPositionImmediate,
69
+ scrollBridge,
70
+ }
71
+ }