about summary refs log tree commit diff
path: root/modules
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-10-09 21:30:42 +0300
committerGitHub <noreply@github.com>2024-10-09 11:30:42 -0700
commitcca344a3d1cdca3d4e63806a9bd5f7867f8961d4 (patch)
tree999d7dffe5d53989b7e217db13f451c6d019ff57 /modules
parentb3ade19bbe3da3caf07bf9561cebb11dac4b6afc (diff)
downloadvoidsky-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.ts10
-rw-r--r--modules/bottom-sheet/src/BottomSheet.tsx114
-rw-r--r--modules/bottom-sheet/src/BottomSheetNativeComponent.tsx103
-rw-r--r--modules/bottom-sheet/src/BottomSheetPortal.tsx40
-rw-r--r--modules/bottom-sheet/src/lib/Portal.tsx67
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}
+}