@tamagui/sheet 1.26.0 → 1.27.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 (53) hide show
  1. package/dist/cjs/Sheet.js +23 -425
  2. package/dist/cjs/Sheet.js.map +2 -2
  3. package/dist/cjs/StyledSheet.js +132 -0
  4. package/dist/cjs/StyledSheet.js.map +6 -0
  5. package/dist/cjs/constants.js +3 -0
  6. package/dist/cjs/constants.js.map +1 -1
  7. package/dist/cjs/createSheet.js +439 -0
  8. package/dist/cjs/createSheet.js.map +6 -0
  9. package/dist/cjs/index.js +2 -0
  10. package/dist/cjs/index.js.map +1 -1
  11. package/dist/esm/Sheet.js +20 -447
  12. package/dist/esm/Sheet.js.map +2 -2
  13. package/dist/esm/StyledSheet.js +100 -0
  14. package/dist/esm/StyledSheet.js.map +6 -0
  15. package/dist/esm/constants.js +2 -0
  16. package/dist/esm/constants.js.map +1 -1
  17. package/dist/esm/createSheet.js +441 -0
  18. package/dist/esm/createSheet.js.map +6 -0
  19. package/dist/esm/index.js +1 -0
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/jsx/Sheet.js +20 -434
  22. package/dist/jsx/Sheet.js.map +2 -2
  23. package/dist/jsx/Sheet.mjs +20 -434
  24. package/dist/jsx/Sheet.mjs.map +2 -2
  25. package/dist/jsx/StyledSheet.js +100 -0
  26. package/dist/jsx/StyledSheet.js.map +6 -0
  27. package/dist/jsx/StyledSheet.mjs +100 -0
  28. package/dist/jsx/StyledSheet.mjs.map +6 -0
  29. package/dist/jsx/constants.js +2 -0
  30. package/dist/jsx/constants.js.map +1 -1
  31. package/dist/jsx/constants.mjs +2 -0
  32. package/dist/jsx/constants.mjs.map +1 -1
  33. package/dist/jsx/createSheet.js +428 -0
  34. package/dist/jsx/createSheet.js.map +6 -0
  35. package/dist/jsx/createSheet.mjs +428 -0
  36. package/dist/jsx/createSheet.mjs.map +6 -0
  37. package/dist/jsx/index.js +1 -0
  38. package/dist/jsx/index.js.map +1 -1
  39. package/dist/jsx/index.mjs +1 -0
  40. package/dist/jsx/index.mjs.map +1 -1
  41. package/package.json +13 -13
  42. package/src/Sheet.tsx +20 -543
  43. package/src/constants.tsx +1 -0
  44. package/src/createSheet.tsx +565 -0
  45. package/src/index.ts +1 -0
  46. package/types/Sheet.d.ts +159 -80
  47. package/types/Sheet.d.ts.map +1 -1
  48. package/types/constants.d.ts +1 -0
  49. package/types/constants.d.ts.map +1 -1
  50. package/types/createSheet.d.ts +66 -0
  51. package/types/createSheet.d.ts.map +1 -0
  52. package/types/index.d.ts +1 -0
  53. package/types/index.d.ts.map +1 -1
@@ -0,0 +1,565 @@
1
+ import { AdaptParentContext } from '@tamagui/adapt'
2
+ import { useComposedRefs } from '@tamagui/compose-refs'
3
+ import {
4
+ GetProps,
5
+ StackProps,
6
+ TamaguiComponentExpectingVariants,
7
+ Theme,
8
+ isClient,
9
+ isTouchable,
10
+ isWeb,
11
+ mergeEvent,
12
+ themeable,
13
+ useAnimationDriver,
14
+ useDidFinishSSR,
15
+ useEvent,
16
+ useIsomorphicLayoutEffect,
17
+ useThemeName,
18
+ withStaticProperties,
19
+ } from '@tamagui/core'
20
+ import { Portal } from '@tamagui/portal'
21
+ import { RemoveScroll } from '@tamagui/remove-scroll'
22
+ import { useKeyboardVisible } from '@tamagui/use-keyboard-visible'
23
+ import {
24
+ FunctionComponent,
25
+ RefAttributes,
26
+ createContext,
27
+ forwardRef,
28
+ useCallback,
29
+ useContext,
30
+ useEffect,
31
+ useMemo,
32
+ useRef,
33
+ useState,
34
+ } from 'react'
35
+ import {
36
+ Animated,
37
+ GestureResponderEvent,
38
+ Keyboard,
39
+ PanResponder,
40
+ PanResponderGestureState,
41
+ Platform,
42
+ View,
43
+ } from 'react-native'
44
+
45
+ import { SHEET_HANDLE_NAME, SHEET_NAME, SHEET_OVERLAY_NAME } from './constants'
46
+ import { getNativeSheet } from './nativeSheet'
47
+ import { SheetProvider, useSheetContext } from './SheetContext'
48
+ import { SheetScrollView } from './SheetScrollView'
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'
54
+ import { useSheetSnapPoints } from './useSheetSnapPoints'
55
+
56
+ type SharedSheetProps = {
57
+ open?: boolean
58
+ }
59
+
60
+ type BaseProps = StackProps & SharedSheetProps
61
+
62
+ export type CreateSheetProps = {
63
+ Frame: TamaguiComponentExpectingVariants<BaseProps, SharedSheetProps>
64
+ Handle: TamaguiComponentExpectingVariants<BaseProps, SharedSheetProps>
65
+ Overlay: TamaguiComponentExpectingVariants<BaseProps, SharedSheetProps>
66
+ }
67
+
68
+ const selectionStyleSheet = isClient ? document.createElement('style') : null
69
+ if (selectionStyleSheet) {
70
+ document.head.appendChild(selectionStyleSheet)
71
+ }
72
+
73
+ export function createSheet({ Handle, Frame, Overlay }: CreateSheetProps) {
74
+ const SheetHandle = Handle.extractable(
75
+ ({ __scopeSheet, ...props }: SheetScopedProps<GetProps<typeof Handle>>) => {
76
+ const context = useSheetContext(SHEET_HANDLE_NAME, __scopeSheet)
77
+ return (
78
+ <Handle
79
+ onPress={() => {
80
+ // don't toggle to the bottom snap position when dismissOnSnapToBottom set
81
+ const max =
82
+ context.snapPoints.length + (context.dismissOnSnapToBottom ? -1 : 0)
83
+ const nextPos = (context.position + 1) % max
84
+ context.setPosition(nextPos)
85
+ }}
86
+ open={context.open}
87
+ {...props}
88
+ />
89
+ )
90
+ }
91
+ )
92
+
93
+ /* -------------------------------------------------------------------------------------------------
94
+ * SheetOverlay
95
+ * -----------------------------------------------------------------------------------------------*/
96
+
97
+ const SheetOverlay = Overlay.extractable(
98
+ ({ __scopeSheet, ...props }: SheetScopedProps<GetProps<typeof Overlay>>) => {
99
+ const context = useSheetContext(SHEET_OVERLAY_NAME, __scopeSheet)
100
+ return (
101
+ <Overlay
102
+ open={context.open && !context.hidden}
103
+ {...props}
104
+ onPress={mergeEvent(
105
+ props.onPress as any,
106
+ context.dismissOnOverlayPress
107
+ ? () => {
108
+ context.setOpen(false)
109
+ }
110
+ : undefined
111
+ )}
112
+ />
113
+ )
114
+ }
115
+ )
116
+
117
+ /* -------------------------------------------------------------------------------------------------
118
+ * Sheet
119
+ * -----------------------------------------------------------------------------------------------*/
120
+
121
+ const SheetFrame = Frame.extractable(
122
+ forwardRef(
123
+ (
124
+ { __scopeSheet, ...props }: SheetScopedProps<GetProps<typeof Frame>>,
125
+ forwardedRef
126
+ ) => {
127
+ const context = useSheetContext(SHEET_NAME, __scopeSheet)
128
+ const composedContentRef = useComposedRefs(forwardedRef, context.contentRef)
129
+ return <Frame ref={composedContentRef} {...props} />
130
+ }
131
+ )
132
+ )
133
+
134
+ const Sheet = forwardRef<View, SheetProps>(function Sheet(props, ref) {
135
+ const hydrated = useDidFinishSSR()
136
+ const { isShowingNonSheet } = useSheetController()
137
+
138
+ let SheetImplementation = SheetImplementationCustom
139
+
140
+ if (props.native && Platform.OS === 'ios') {
141
+ if (process.env.TAMAGUI_TARGET === 'native') {
142
+ const impl = getNativeSheet('ios')
143
+ if (impl) {
144
+ SheetImplementation = impl
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Performance is sensitive here so avoid all the hooks below with this
151
+ */
152
+ if (isShowingNonSheet || !hydrated) {
153
+ return null
154
+ }
155
+
156
+ return <SheetImplementation ref={ref} {...props} />
157
+ })
158
+
159
+ const components = {
160
+ Frame: SheetFrame,
161
+ Overlay: SheetOverlay,
162
+ Handle: SheetHandle,
163
+ ScrollView: SheetScrollView,
164
+ }
165
+
166
+ const Controlled = withStaticProperties(Sheet, components) as any as FunctionComponent<
167
+ Omit<SheetProps, 'open' | 'onOpenChange'> & RefAttributes<View>
168
+ > &
169
+ typeof components
170
+
171
+ return withStaticProperties(Sheet, {
172
+ ...components,
173
+ Controlled,
174
+ })
175
+ }
176
+
177
+ // set all the way off screen
178
+ const HIDDEN_SIZE = 10_000
179
+
180
+ const ParentSheetContext = createContext({
181
+ zIndex: 100_000,
182
+ })
183
+
184
+ const SheetImplementationCustom = themeable(
185
+ forwardRef<View, SheetProps>(function SheetImplementationCustom(props, forwardedRef) {
186
+ const parentSheet = useContext(ParentSheetContext)
187
+
188
+ const {
189
+ animationConfig,
190
+ forceRemoveScrollEnabled = null,
191
+ disableDrag: disableDragProp,
192
+ modal = false,
193
+ zIndex = parentSheet.zIndex + 1,
194
+ moveOnKeyboardChange = false,
195
+ portalProps,
196
+ } = props
197
+
198
+ const state = useSheetOpenState(props)
199
+ const providerProps = useSheetProviderProps(props, state)
200
+ const { positions, maxSnapPoint, screenSize } = useSheetSnapPoints(providerProps)
201
+
202
+ const { position, contentRef, setPosition, scrollBridge, frameSize, setFrameSize } =
203
+ providerProps
204
+
205
+ const { open, isHidden, controller } = state
206
+
207
+ const { frameComponent, handleComponent, bottomCoverComponent, overlayComponent } =
208
+ useSheetChildren(props.children)
209
+
210
+ const sheetRef = useRef<View>(null)
211
+ const ref = useComposedRefs(forwardedRef, sheetRef)
212
+
213
+ const driver = useAnimationDriver()
214
+ if (!driver) {
215
+ throw new Error('Must set animations in tamagui.config.ts')
216
+ }
217
+
218
+ const disableDrag = disableDragProp ?? controller?.disableDrag
219
+ const keyboardIsVisible = useKeyboardVisible()
220
+ const themeName = useThemeName()
221
+
222
+ const { useAnimatedNumber, useAnimatedNumberReaction, useAnimatedNumberStyle } =
223
+ driver
224
+
225
+ const animatedNumber = useAnimatedNumber(HIDDEN_SIZE)
226
+
227
+ // native only fix
228
+ const at = useRef(0)
229
+
230
+ useAnimatedNumberReaction(
231
+ {
232
+ value: animatedNumber,
233
+ hostRef: sheetRef,
234
+ },
235
+ (value) => {
236
+ if (!driver.isReactNative) return
237
+ at.current = value
238
+ scrollBridge.paneY = value
239
+ }
240
+ )
241
+
242
+ function stopSpring() {
243
+ animatedNumber.stop()
244
+ if (scrollBridge.onFinishAnimate) {
245
+ scrollBridge.onFinishAnimate()
246
+ scrollBridge.onFinishAnimate = undefined
247
+ }
248
+ }
249
+
250
+ // we need to set this *after* fully closed to 0, to avoid it overlapping
251
+ // the page when resizing quickly on web for example
252
+ const [opacity, setOpacity] = useState(open ? 1 : 0)
253
+ if (open && opacity === 0) {
254
+ setOpacity(1)
255
+ }
256
+ useEffect(() => {
257
+ if (!open) {
258
+ // need to wait for animation complete, for now lets just do it naively
259
+ const tm = setTimeout(() => {
260
+ setOpacity(0)
261
+ }, 400)
262
+ return () => {
263
+ clearTimeout(tm)
264
+ }
265
+ }
266
+ }, [open])
267
+
268
+ const animateTo = useEvent((position: number) => {
269
+ const current = animatedNumber.getValue()
270
+ if (isHidden && open) return
271
+ if (!current) return
272
+ if (frameSize === 0) return
273
+ const hiddenValue = frameSize === 0 ? HIDDEN_SIZE : screenSize
274
+ const toValue = isHidden || position === -1 ? hiddenValue : positions[position]
275
+ if (at.current === toValue) return
276
+ stopSpring()
277
+ if (isHidden) {
278
+ animatedNumber.setValue(toValue, {
279
+ type: 'timing',
280
+ duration: 0,
281
+ })
282
+ at.current = toValue
283
+ return
284
+ }
285
+ // dont bounce on initial measure to bottom
286
+ const overshootClamping = at.current === HIDDEN_SIZE
287
+ animatedNumber.setValue(toValue, {
288
+ ...animationConfig,
289
+ type: 'spring',
290
+ overshootClamping,
291
+ })
292
+ })
293
+
294
+ useIsomorphicLayoutEffect(() => {
295
+ animateTo(position)
296
+ }, [isHidden, frameSize, position, animateTo])
297
+
298
+ /**
299
+ * This is a hacky workaround for native:
300
+ */
301
+ const [isShowingInnerSheet, setIsShowingInnerSheet] = useState(false)
302
+ const shouldHideParentSheet = !isWeb && modal && isShowingInnerSheet
303
+ const parentSheetContext = useContext(SheetInsideSheetContext)
304
+ const onInnerSheet = useCallback((hasChild: boolean) => {
305
+ setIsShowingInnerSheet(hasChild)
306
+ }, [])
307
+
308
+ const panResponder = useMemo(
309
+ () => {
310
+ if (disableDrag) return
311
+ if (!frameSize) return
312
+ if (isShowingInnerSheet) return
313
+
314
+ const minY = positions[0]
315
+ scrollBridge.paneMinY = minY
316
+ let startY = at.current
317
+
318
+ function makeUnselectable(val: boolean) {
319
+ if (!selectionStyleSheet) return
320
+ if (!val) {
321
+ selectionStyleSheet.innerText = ''
322
+ } else {
323
+ selectionStyleSheet.innerText =
324
+ ':root * { user-select: none !important; -webkit-user-select: none !important; }'
325
+ }
326
+ }
327
+
328
+ const release = ({ vy, dragAt }: { dragAt: number; vy: number }) => {
329
+ isExternalDrag = false
330
+ previouslyScrolling = false
331
+ makeUnselectable(false)
332
+ const at = dragAt + startY
333
+ // seems liky vy goes up to about 4 at the very most (+ is down, - is up)
334
+ // lets base our multiplier on the total layout height
335
+ const end = at + frameSize * vy * 0.2
336
+ let closestPoint = 0
337
+ let dist = Infinity
338
+ for (let i = 0; i < positions.length; i++) {
339
+ const position = positions[i]
340
+ const curDist = end > position ? end - position : position - end
341
+ if (curDist < dist) {
342
+ dist = curDist
343
+ closestPoint = i
344
+ }
345
+ }
346
+ // have to call both because state may not change but need to snap back
347
+ setPosition(closestPoint)
348
+ animateTo(closestPoint)
349
+ }
350
+
351
+ const finish = (_e: GestureResponderEvent, state: PanResponderGestureState) => {
352
+ release({
353
+ vy: state.vy,
354
+ dragAt: state.dy,
355
+ })
356
+ }
357
+
358
+ let previouslyScrolling = false
359
+
360
+ const onMoveShouldSet = (
361
+ _e: GestureResponderEvent,
362
+ { dy }: PanResponderGestureState
363
+ ) => {
364
+ const isScrolled = scrollBridge.y !== 0
365
+ const isDraggingUp = dy < 0
366
+ // we can treat near top instead of exactly to avoid trouble with springs
367
+ const isNearTop = scrollBridge.paneY - 5 <= scrollBridge.paneMinY
368
+ if (isScrolled) {
369
+ previouslyScrolling = true
370
+ return false
371
+ }
372
+ // prevent drag once at top and pulling up
373
+ if (isNearTop) {
374
+ if (!isScrolled && isDraggingUp) {
375
+ return false
376
+ }
377
+ }
378
+ // we could do some detection of other touchables and cancel here..
379
+ return Math.abs(dy) > 5
380
+ }
381
+
382
+ const grant = () => {
383
+ makeUnselectable(true)
384
+ stopSpring()
385
+ startY = at.current
386
+ }
387
+
388
+ let isExternalDrag = false
389
+
390
+ scrollBridge.drag = (dy) => {
391
+ if (!isExternalDrag) {
392
+ isExternalDrag = true
393
+ grant()
394
+ }
395
+ const to = dy + startY
396
+ animatedNumber.setValue(resisted(to, minY), { type: 'direct' })
397
+ }
398
+
399
+ scrollBridge.release = release
400
+
401
+ return PanResponder.create({
402
+ onMoveShouldSetPanResponder: onMoveShouldSet,
403
+ onPanResponderGrant: grant,
404
+ onPanResponderMove: (_e, { dy }) => {
405
+ const toFull = dy + startY
406
+ const to = resisted(toFull, minY)
407
+ animatedNumber.setValue(to, { type: 'direct' })
408
+ },
409
+ onPanResponderEnd: finish,
410
+ onPanResponderTerminate: finish,
411
+ onPanResponderRelease: finish,
412
+ })
413
+ },
414
+ // eslint-disable-next-line react-hooks/exhaustive-deps
415
+ [disableDrag, isShowingInnerSheet, animateTo, frameSize, positions, setPosition]
416
+ )
417
+
418
+ const animatedStyle = useAnimatedNumberStyle(animatedNumber, (val) => {
419
+ 'worklet'
420
+ const translateY = frameSize === 0 ? HIDDEN_SIZE : val
421
+ return {
422
+ transform: [{ translateY }],
423
+ }
424
+ })
425
+
426
+ const sizeBeforeKeyboard = useRef<number | null>(null)
427
+ useEffect(() => {
428
+ if (isWeb || !moveOnKeyboardChange) return
429
+ const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', (e) => {
430
+ if (sizeBeforeKeyboard.current !== null) return
431
+ sizeBeforeKeyboard.current = animatedNumber.getValue()
432
+ animatedNumber.setValue(
433
+ Math.max(animatedNumber.getValue() - e.endCoordinates.height, 0)
434
+ )
435
+ })
436
+ const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
437
+ if (sizeBeforeKeyboard.current === null) return
438
+ animatedNumber.setValue(sizeBeforeKeyboard.current)
439
+ sizeBeforeKeyboard.current = null
440
+ })
441
+
442
+ return () => {
443
+ keyboardDidHideListener.remove()
444
+ keyboardDidShowListener.remove()
445
+ }
446
+ }, [])
447
+
448
+ // temp until reanimated useAnimatedNumber fix
449
+ const AnimatedView = (driver['NumberView'] ?? driver.View) as typeof Animated.View
450
+
451
+ useIsomorphicLayoutEffect(() => {
452
+ if (!(parentSheetContext && open)) return
453
+ parentSheetContext(true)
454
+ return () => {
455
+ parentSheetContext(false)
456
+ }
457
+ }, [parentSheetContext, open])
458
+
459
+ const nextParentContext = useMemo(
460
+ () => ({
461
+ zIndex,
462
+ }),
463
+ [zIndex]
464
+ )
465
+
466
+ const handleLayout = useCallback(
467
+ (e) => {
468
+ let next = e.nativeEvent?.layout.height
469
+ if (isWeb && isTouchable && !open) {
470
+ // temp fix ios bug where it doesn't go below dynamic bottom...
471
+ next += 100
472
+ }
473
+ if (!next) return
474
+ setFrameSize(() => next)
475
+ },
476
+ [keyboardIsVisible]
477
+ )
478
+
479
+ const removeScrollEnabled = forceRemoveScrollEnabled ?? (open && modal)
480
+
481
+ const contents = (
482
+ <ParentSheetContext.Provider value={nextParentContext}>
483
+ <SheetProvider {...providerProps}>
484
+ {shouldHideParentSheet ? null : overlayComponent}
485
+
486
+ <AnimatedView
487
+ ref={ref}
488
+ {...panResponder?.panHandlers}
489
+ onLayout={handleLayout}
490
+ pointerEvents={open && !shouldHideParentSheet ? 'auto' : 'none'}
491
+ // @ts-ignore
492
+ animation={props.animation}
493
+ style={[
494
+ {
495
+ position: 'absolute',
496
+ zIndex,
497
+ width: '100%',
498
+ height: `${maxSnapPoint}%`,
499
+ opacity,
500
+ },
501
+ animatedStyle,
502
+ ]}
503
+ >
504
+ {handleComponent}
505
+
506
+ {bottomCoverComponent}
507
+ {/* @ts-ignore */}
508
+ <RemoveScroll
509
+ forwardProps
510
+ enabled={removeScrollEnabled}
511
+ allowPinchZoom
512
+ shards={[contentRef]}
513
+ // causes lots of bugs on touch web on site
514
+ removeScrollBar={false}
515
+ >
516
+ {frameComponent}
517
+ </RemoveScroll>
518
+ </AnimatedView>
519
+ </SheetProvider>
520
+ </ParentSheetContext.Provider>
521
+ )
522
+
523
+ const adaptContext = useContext(AdaptParentContext)
524
+
525
+ if (modal) {
526
+ const modalContents = (
527
+ <Portal zIndex={zIndex} {...portalProps}>
528
+ <Theme forceClassName name={themeName}>
529
+ <AdaptParentContext.Provider value={adaptContext}>
530
+ {contents}
531
+ </AdaptParentContext.Provider>
532
+ </Theme>
533
+ </Portal>
534
+ )
535
+
536
+ if (isWeb) {
537
+ return modalContents
538
+ }
539
+
540
+ // on native we don't support multiple modals yet... fix for now is to hide outer one
541
+ return (
542
+ <SheetInsideSheetContext.Provider value={onInnerSheet}>
543
+ {modalContents}
544
+ </SheetInsideSheetContext.Provider>
545
+ )
546
+ }
547
+
548
+ return contents
549
+ })
550
+ )
551
+
552
+ const SheetInsideSheetContext = createContext<((hasChild: boolean) => void) | null>(null)
553
+
554
+ /* -------------------------------------------------------------------------------------------------*/
555
+
556
+ function resisted(y: number, minY: number, maxOverflow = 25) {
557
+ if (y < minY) {
558
+ const past = minY - y
559
+ const pctPast = Math.min(maxOverflow, past) / maxOverflow
560
+ const diminishBy = 1.1 - Math.pow(0.1, pctPast)
561
+ const extra = -diminishBy * maxOverflow
562
+ return minY + extra
563
+ }
564
+ return y
565
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './Sheet'
2
+ export * from './createSheet'
2
3
  export * from './SheetController'
3
4
  export * from './useSheetController'
4
5
  export { setupNativeSheet } from './nativeSheet'