-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow nested sheets without boilerplate (#5660)
Co-authored-by: Hailey <[email protected]>
- Loading branch information
Showing
20 changed files
with
550 additions
and
509 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
}) |
103 changes: 103 additions & 0 deletions
103
modules/bottom-sheet/src/BottomSheetNativeComponent.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.