diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-10-09 21:30:42 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-09 11:30:42 -0700 |
commit | cca344a3d1cdca3d4e63806a9bd5f7867f8961d4 (patch) | |
tree | 999d7dffe5d53989b7e217db13f451c6d019ff57 /modules | |
parent | b3ade19bbe3da3caf07bf9561cebb11dac4b6afc (diff) | |
download | voidsky-cca344a3d1cdca3d4e63806a9bd5f7867f8961d4.tar.zst |
Allow nested sheets without boilerplate (#5660)
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'modules')
-rw-r--r-- | modules/bottom-sheet/index.ts | 10 | ||||
-rw-r--r-- | modules/bottom-sheet/src/BottomSheet.tsx | 114 | ||||
-rw-r--r-- | modules/bottom-sheet/src/BottomSheetNativeComponent.tsx | 103 | ||||
-rw-r--r-- | modules/bottom-sheet/src/BottomSheetPortal.tsx | 40 | ||||
-rw-r--r-- | modules/bottom-sheet/src/lib/Portal.tsx | 67 |
5 files changed, 239 insertions, 95 deletions
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<any> - style: StyleProp<ViewStyle> - } -> = requireNativeViewManager('BottomSheet') - -const NativeModule = requireNativeModule('BottomSheet') - -export class BottomSheet extends React.Component< - BottomSheetViewProps, - { - open: boolean - } -> { - ref = React.createRef<any>() - - 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 ( - <NativeView - {...rest} - onStateChange={this.onStateChange} - ref={this.ref} - style={{ - position: 'absolute', - height: screenHeight, - width: '100%', - }} - containerBackgroundColor={backgroundColor}> - <View - style={[ - { - flex: 1, - backgroundColor, - }, - Platform.OS === 'android' && { - borderTopLeftRadius: cornerRadius, - borderTopRightRadius: cornerRadius, - }, - ]}> - <View onLayout={this.updateLayout}>{children}</View> - </View> - </NativeView> + if (__DEV__ && !Portal) { + throw new Error( + 'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.', ) } -} + + return ( + <Portal> + <BottomSheetNativeComponent {...props} ref={ref} /> + </Portal> + ) +}) 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<any> + style: StyleProp<ViewStyle> + } +> = requireNativeViewManager('BottomSheet') + +const NativeModule = requireNativeModule('BottomSheet') + +export class BottomSheetNativeComponent extends React.Component< + BottomSheetViewProps, + { + open: boolean + } +> { + ref = React.createRef<any>() + + 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 ( + <NativeView + {...rest} + onStateChange={this.onStateChange} + ref={this.ref} + style={{ + position: 'absolute', + height: screenHeight, + width: '100%', + }} + containerBackgroundColor={backgroundColor}> + <View + style={[ + { + flex: 1, + backgroundColor, + }, + Platform.OS === 'android' && { + borderTopLeftRadius: cornerRadius, + borderTopRightRadius: cornerRadius, + }, + ]}> + <View onLayout={this.updateLayout}> + <BottomSheetPortalProvider>{children}</BottomSheetPortalProvider> + </View> + </View> + </NativeView> + ) + } +} 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 ( + <Context.Provider value={portal.Portal}> + <portal.Provider> + {children} + <portal.Outlet /> + </portal.Provider> + </Context.Provider> + ) +} + +const defaultPortal = createPortalGroup_INTERNAL() + +export const BottomSheetOutlet = defaultPortal.Outlet + +export function BottomSheetProvider({children}: {children: React.ReactNode}) { + return ( + <Context.Provider value={defaultPortal.Portal}> + <defaultPortal.Provider>{children}</defaultPortal.Provider> + </Context.Provider> + ) +} 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<ContextType>({ + outlet: null, + append: () => {}, + remove: () => {}, + }) + + function Provider(props: React.PropsWithChildren<{}>) { + const map = React.useRef<ComponentMap>({}) + const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null) + + const append = React.useCallback<ContextType['append']>((id, component) => { + if (map.current[id]) return + map.current[id] = <React.Fragment key={id}>{component}</React.Fragment> + setOutlet(<>{Object.values(map.current)}</>) + }, []) + + const remove = React.useCallback<ContextType['remove']>(id => { + delete map.current[id] + setOutlet(<>{Object.values(map.current)}</>) + }, []) + + const contextValue = React.useMemo( + () => ({ + outlet, + append, + remove, + }), + [outlet, append, remove], + ) + + return ( + <Context.Provider value={contextValue}>{props.children}</Context.Provider> + ) + } + + 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} +} |