From cca344a3d1cdca3d4e63806a9bd5f7867f8961d4 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 9 Oct 2024 21:30:42 +0300 Subject: Allow nested sheets without boilerplate (#5660) Co-authored-by: Hailey --- modules/bottom-sheet/index.ts | 10 ++ modules/bottom-sheet/src/BottomSheet.tsx | 114 ++++----------------- .../src/BottomSheetNativeComponent.tsx | 103 +++++++++++++++++++ modules/bottom-sheet/src/BottomSheetPortal.tsx | 40 ++++++++ modules/bottom-sheet/src/lib/Portal.tsx | 67 ++++++++++++ 5 files changed, 239 insertions(+), 95 deletions(-) create mode 100644 modules/bottom-sheet/src/BottomSheetNativeComponent.tsx create mode 100644 modules/bottom-sheet/src/BottomSheetPortal.tsx create mode 100644 modules/bottom-sheet/src/lib/Portal.tsx (limited to 'modules') diff --git a/modules/bottom-sheet/index.ts b/modules/bottom-sheet/index.ts index 1fe3dac0e..4009f2ab2 100644 --- a/modules/bottom-sheet/index.ts +++ b/modules/bottom-sheet/index.ts @@ -4,9 +4,19 @@ import { BottomSheetState, BottomSheetViewProps, } from './src/BottomSheet.types' +import {BottomSheetNativeComponent} from './src/BottomSheetNativeComponent' +import { + BottomSheetOutlet, + BottomSheetPortalProvider, + BottomSheetProvider, +} from './src/BottomSheetPortal' export { BottomSheet, + BottomSheetNativeComponent, + BottomSheetOutlet, + BottomSheetPortalProvider, + BottomSheetProvider, BottomSheetSnapPoint, type BottomSheetState, type BottomSheetViewProps, diff --git a/modules/bottom-sheet/src/BottomSheet.tsx b/modules/bottom-sheet/src/BottomSheet.tsx index 9e7d0c209..bcc2c42ad 100644 --- a/modules/bottom-sheet/src/BottomSheet.tsx +++ b/modules/bottom-sheet/src/BottomSheet.tsx @@ -1,100 +1,24 @@ -import * as React from 'react' -import { - Dimensions, - NativeSyntheticEvent, - Platform, - StyleProp, - View, - ViewStyle, -} from 'react-native' -import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' +import React from 'react' -import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types' +import {BottomSheetViewProps} from './BottomSheet.types' +import {BottomSheetNativeComponent} from './BottomSheetNativeComponent' +import {useBottomSheetPortal_INTERNAL} from './BottomSheetPortal' -const screenHeight = Dimensions.get('screen').height +export const BottomSheet = React.forwardRef< + BottomSheetNativeComponent, + BottomSheetViewProps +>(function BottomSheet(props, ref) { + const Portal = useBottomSheetPortal_INTERNAL() -const NativeView: React.ComponentType< - BottomSheetViewProps & { - ref: React.RefObject - style: StyleProp - } -> = requireNativeViewManager('BottomSheet') - -const NativeModule = requireNativeModule('BottomSheet') - -export class BottomSheet extends React.Component< - BottomSheetViewProps, - { - open: boolean - } -> { - ref = React.createRef() - - constructor(props: BottomSheetViewProps) { - super(props) - this.state = { - open: false, - } - } - - present() { - this.setState({open: true}) - } - - dismiss() { - this.ref.current?.dismiss() - } - - private onStateChange = ( - event: NativeSyntheticEvent<{state: BottomSheetState}>, - ) => { - const {state} = event.nativeEvent - const isOpen = state !== 'closed' - this.setState({open: isOpen}) - this.props.onStateChange?.(event) - } - - private updateLayout = () => { - this.ref.current?.updateLayout() - } - - static dismissAll = async () => { - await NativeModule.dismissAll() - } - - render() { - const {children, backgroundColor, ...rest} = this.props - const cornerRadius = rest.cornerRadius ?? 0 - - if (!this.state.open) { - return null - } - - return ( - - - {children} - - + if (__DEV__ && !Portal) { + throw new Error( + 'BottomSheet: You need to wrap your component tree with a to use the bottom sheet.', ) } -} + + return ( + + + + ) +}) diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx new file mode 100644 index 000000000..eadd9b4a1 --- /dev/null +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -0,0 +1,103 @@ +import * as React from 'react' +import { + Dimensions, + NativeSyntheticEvent, + Platform, + StyleProp, + View, + ViewStyle, +} from 'react-native' +import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' + +import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types' +import {BottomSheetPortalProvider} from './BottomSheetPortal' + +const screenHeight = Dimensions.get('screen').height + +const NativeView: React.ComponentType< + BottomSheetViewProps & { + ref: React.RefObject + style: StyleProp + } +> = requireNativeViewManager('BottomSheet') + +const NativeModule = requireNativeModule('BottomSheet') + +export class BottomSheetNativeComponent extends React.Component< + BottomSheetViewProps, + { + open: boolean + } +> { + ref = React.createRef() + + constructor(props: BottomSheetViewProps) { + super(props) + this.state = { + open: false, + } + } + + present() { + this.setState({open: true}) + } + + dismiss() { + this.ref.current?.dismiss() + } + + private onStateChange = ( + event: NativeSyntheticEvent<{state: BottomSheetState}>, + ) => { + const {state} = event.nativeEvent + const isOpen = state !== 'closed' + this.setState({open: isOpen}) + this.props.onStateChange?.(event) + } + + private updateLayout = () => { + this.ref.current?.updateLayout() + } + + static dismissAll = async () => { + await NativeModule.dismissAll() + } + + render() { + const {children, backgroundColor, ...rest} = this.props + const cornerRadius = rest.cornerRadius ?? 0 + + if (!this.state.open) { + return null + } + + return ( + + + + {children} + + + + ) + } +} diff --git a/modules/bottom-sheet/src/BottomSheetPortal.tsx b/modules/bottom-sheet/src/BottomSheetPortal.tsx new file mode 100644 index 000000000..da14cfa77 --- /dev/null +++ b/modules/bottom-sheet/src/BottomSheetPortal.tsx @@ -0,0 +1,40 @@ +import React from 'react' + +import {createPortalGroup_INTERNAL} from './lib/Portal' + +type PortalContext = React.ElementType<{children: React.ReactNode}> + +const Context = React.createContext({} as PortalContext) + +export const useBottomSheetPortal_INTERNAL = () => React.useContext(Context) + +export function BottomSheetPortalProvider({ + children, +}: { + children: React.ReactNode +}) { + const portal = React.useMemo(() => { + return createPortalGroup_INTERNAL() + }, []) + + return ( + + + {children} + + + + ) +} + +const defaultPortal = createPortalGroup_INTERNAL() + +export const BottomSheetOutlet = defaultPortal.Outlet + +export function BottomSheetProvider({children}: {children: React.ReactNode}) { + return ( + + {children} + + ) +} diff --git a/modules/bottom-sheet/src/lib/Portal.tsx b/modules/bottom-sheet/src/lib/Portal.tsx new file mode 100644 index 000000000..dd1bc4c13 --- /dev/null +++ b/modules/bottom-sheet/src/lib/Portal.tsx @@ -0,0 +1,67 @@ +import React from 'react' + +type Component = React.ReactElement + +type ContextType = { + outlet: Component | null + append(id: string, component: Component): void + remove(id: string): void +} + +type ComponentMap = { + [id: string]: Component +} + +export function createPortalGroup_INTERNAL() { + const Context = React.createContext({ + outlet: null, + append: () => {}, + remove: () => {}, + }) + + function Provider(props: React.PropsWithChildren<{}>) { + const map = React.useRef({}) + const [outlet, setOutlet] = React.useState(null) + + const append = React.useCallback((id, component) => { + if (map.current[id]) return + map.current[id] = {component} + setOutlet(<>{Object.values(map.current)}) + }, []) + + const remove = React.useCallback(id => { + delete map.current[id] + setOutlet(<>{Object.values(map.current)}) + }, []) + + const contextValue = React.useMemo( + () => ({ + outlet, + append, + remove, + }), + [outlet, append, remove], + ) + + return ( + {props.children} + ) + } + + function Outlet() { + const ctx = React.useContext(Context) + return ctx.outlet + } + + function Portal({children}: React.PropsWithChildren<{}>) { + const {append, remove} = React.useContext(Context) + const id = React.useId() + React.useEffect(() => { + append(id, children as Component) + return () => remove(id) + }, [id, children, append, remove]) + return null + } + + return {Provider, Outlet, Portal} +} -- cgit 1.4.1