import { useComposedRefs } from '@tamagui/compose-refs' import { useIsomorphicLayoutEffect } from '@tamagui/constants' import type { GetProps, ViewProps, TamaguiComponent, TamaguiComponentExpectingVariants, TamaguiElement, } from '@tamagui/core' import { View } from '@tamagui/core' import { composeEventHandlers, withStaticProperties } from '@tamagui/helpers' import { resolveViewZIndex } from '@tamagui/portal' import { RemoveScroll } from '@tamagui/remove-scroll' import { useDidFinishSSR } from '@tamagui/use-did-finish-ssr' import { StackZIndexContext } from '@tamagui/z-index-stack' import type { ForwardRefExoticComponent, FunctionComponent, RefAttributes } from 'react' import { forwardRef, memo, useMemo, useEffect, useRef } from 'react' import type { View as RNView } from 'react-native' import { Platform } from 'react-native' import { SHEET_HANDLE_NAME, SHEET_NAME, SHEET_OVERLAY_NAME } from './constants' import { getNativeSheet } from './nativeSheet' import { useSheetContext } from './SheetContext' import { SheetImplementationCustom } from './SheetImplementationCustom' import { SheetScrollView } from './SheetScrollView' import type { SheetProps, SheetScopedProps } from './types' import { useSheetController } from './useSheetController' import { useSheetOffscreenSize } from './useSheetOffscreenSize' type SharedSheetProps = { open?: boolean } type BaseProps = ViewProps & SharedSheetProps type SheetStyledComponent = TamaguiComponentExpectingVariants export function createSheet< H extends TamaguiComponent | SheetStyledComponent, F extends TamaguiComponent | SheetStyledComponent, O extends TamaguiComponent | SheetStyledComponent, >({ Handle, Frame, Overlay }: { Handle: H; Frame: F; Overlay: O }) { const SheetHandle = Handle.styleable( ( { __scopeSheet, ...props }: SheetScopedProps, forwardedRef ) => { const context = useSheetContext(SHEET_HANDLE_NAME, __scopeSheet) const composedRef = useComposedRefs(context.handleRef, forwardedRef) // track if sheet was being dragged to prevent onPress toggle after drag const wasDraggingRef = useRef(false) // subscribe to parent dragging changes to track if we dragged during this press useEffect(() => { if (!context.scrollBridge) return return context.scrollBridge.onParentDragging((isDragging: boolean) => { if (isDragging) { wasDraggingRef.current = true } }) }, [context.scrollBridge]) if (context.onlyShowFrame) { return null } return ( // @ts-ignore { // reset at start of new press wasDraggingRef.current = false }} onPress={() => { // skip toggle if this was a drag gesture if (wasDraggingRef.current) { wasDraggingRef.current = false return } // don't toggle to the bottom snap position when dismissOnSnapToBottom set const max = context.snapPoints.length + (context.dismissOnSnapToBottom ? -1 : 0) const nextPos = (context.position + 1) % max context.setPosition(nextPos) }} open={context.open} {...props} /> ) } ) /* ------------------------------------------------------------------------------------------------- * SheetOverlay * -----------------------------------------------------------------------------------------------*/ const SheetOverlay = Overlay.styleable>((propsIn, ref) => { const { __scopeSheet, ...props } = propsIn const context = useSheetContext(SHEET_OVERLAY_NAME, __scopeSheet) // this ones a bit weird for legacy reasons, we need to hoist it above AnimatedView // so we just pass it up to context const element = useMemo(() => { return ( // @ts-ignore { context.setOpen(false) } : undefined )} /> ) }, [props.onPress, props.opacity, context.dismissOnOverlayPress]) useIsomorphicLayoutEffect(() => { context.onOverlayComponent?.(element) }, [element]) if (context.onlyShowFrame) { return null } return null }) /* ------------------------------------------------------------------------------------------------- * Sheet * -----------------------------------------------------------------------------------------------*/ type ExtraFrameProps = { /** * By default the sheet adds a view below its bottom that extends down another 50%, * this is useful if your Sheet has a spring animation that bounces "past" the top when * opening, preventing it from showing the content underneath. */ disableHideBottomOverflow?: boolean /** * Adds padding accounting for the currently offscreen content, so if you put a flex element inside * the sheet, it will always flex to the height of the visible amount of the sheet. If this is not * turned on, the inner content is always set to the max height of the sheet. */ adjustPaddingForOffscreenContent?: boolean } const SheetFrame = Frame.styleable( ( { __scopeSheet, adjustPaddingForOffscreenContent, disableHideBottomOverflow, children, ...props }, forwardedRef ) => { const context = useSheetContext(SHEET_NAME, __scopeSheet) const { hasFit, disableRemoveScroll, frameSize, contentRef, open } = context const composedContentRef = useComposedRefs(forwardedRef, contentRef) const offscreenSize = useSheetOffscreenSize(context) // FIX: Store the frameSize when open for use during close animation const stableFrameSize = useRef(frameSize) useEffect(() => { if (open && frameSize) { stableFrameSize.current = frameSize } }, [open, frameSize]) const sheetContents = useMemo(() => { // FIX: Use fixed height during close animation to prevent content-driven resizing const shouldUseFixedHeight = hasFit && !open && stableFrameSize.current return ( // @ts-expect-error {children} {adjustPaddingForOffscreenContent && ( )} ) }, [ open, props, frameSize, offscreenSize, adjustPaddingForOffscreenContent, hasFit, ]) return ( <> {sheetContents} {/* below frame hide when bouncing past 100% */} {!disableHideBottomOverflow && ( // @ts-ignore )} ) } ) as any as ForwardRefExoticComponent< SheetScopedProps< Omit, keyof ExtraFrameProps> & ExtraFrameProps > > const Sheet = forwardRef(function Sheet(props, ref) { const hydrated = useDidFinishSSR() const { isShowingNonSheet } = useSheetController() let SheetImplementation = SheetImplementationCustom if (props.native && Platform.OS === 'ios') { if (process.env.TAMAGUI_TARGET === 'native') { const impl = getNativeSheet('ios') if (impl) { // @ts-expect-error accepting external sheet implementation SheetImplementation = impl } } } /** * Performance is sensitive here so avoid all the hooks below with this */ if (isShowingNonSheet || !hydrated) { return null } return }) const components = { Frame: SheetFrame, Overlay: SheetOverlay, Handle: SheetHandle, ScrollView: SheetScrollView, } const Controlled = withStaticProperties(Sheet, components) as any as FunctionComponent< Omit & RefAttributes > & typeof components return withStaticProperties(Sheet, { ...components, Controlled, }) } /* -------------------------------------------------------------------------------------------------*/