about summary refs log tree commit diff
path: root/src/components/Dialog
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-10-04 13:24:12 -0700
committerGitHub <noreply@github.com>2024-10-04 13:24:12 -0700
commit00486e94991f344353ffb083dd631283a84c3ad3 (patch)
treea5dc4da5e5e71912d73a099e84761517fa8c62a9 /src/components/Dialog
parent9802ebe20d32dc1867a069dc377b3d4c43ce45f0 (diff)
downloadvoidsky-00486e94991f344353ffb083dd631283a84c3ad3.tar.zst
[Sheets] [Pt. 1] Root PR (#5557)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/components/Dialog')
-rw-r--r--src/components/Dialog/context.ts5
-rw-r--r--src/components/Dialog/index.tsx380
-rw-r--r--src/components/Dialog/index.web.tsx12
-rw-r--r--src/components/Dialog/sheet-wrapper.ts20
-rw-r--r--src/components/Dialog/types.ts13
5 files changed, 215 insertions, 215 deletions
diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts
index 859f8edd7..b479bc7f0 100644
--- a/src/components/Dialog/context.ts
+++ b/src/components/Dialog/context.ts
@@ -6,9 +6,14 @@ import {
   DialogControlRefProps,
   DialogOuterProps,
 } from '#/components/Dialog/types'
+import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
 
 export const Context = React.createContext<DialogContextProps>({
   close: () => {},
+  isNativeDialog: false,
+  nativeSnapPoint: BottomSheetSnapPoint.Hidden,
+  disableDrag: false,
+  setDisableDrag: () => {},
 })
 
 export function useDialogContext() {
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index d5d92048a..49b5e10b2 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -1,86 +1,48 @@
 import React, {useImperativeHandle} from 'react'
 import {
-  Dimensions,
-  Keyboard,
+  NativeScrollEvent,
+  NativeSyntheticEvent,
   Pressable,
+  ScrollView,
   StyleProp,
+  TextInput,
   View,
   ViewStyle,
 } from 'react-native'
-import Animated, {useAnimatedStyle} from 'react-native-reanimated'
+import {
+  KeyboardAwareScrollView,
+  useKeyboardHandler,
+} from 'react-native-keyboard-controller'
+import {runOnJS} from 'react-native-reanimated'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import BottomSheet, {
-  BottomSheetBackdropProps,
-  BottomSheetFlatList,
-  BottomSheetFlatListMethods,
-  BottomSheetScrollView,
-  BottomSheetScrollViewMethods,
-  BottomSheetTextInput,
-  BottomSheetView,
-  useBottomSheet,
-  WINDOW_HEIGHT,
-} from '@discord/bottom-sheet/src'
-import {BottomSheetFlatListProps} from '@discord/bottom-sheet/src/components/bottomSheetScrollable/types'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 import {logger} from '#/logger'
+import {isAndroid, isIOS} from '#/platform/detection'
+import {useA11y} from '#/state/a11y'
 import {useDialogStateControlContext} from '#/state/dialogs'
-import {atoms as a, flatten, useTheme} from '#/alf'
-import {Context} from '#/components/Dialog/context'
+import {List, ListMethods, ListProps} from '#/view/com/util/List'
+import {atoms as a, useTheme} from '#/alf'
+import {Context, useDialogContext} from '#/components/Dialog/context'
 import {
   DialogControlProps,
   DialogInnerProps,
   DialogOuterProps,
 } from '#/components/Dialog/types'
 import {createInput} from '#/components/forms/TextField'
-import {FullWindowOverlay} from '#/components/FullWindowOverlay'
-import {Portal} from '#/components/Portal'
+import {Portal as DefaultPortal} from '#/components/Portal'
+import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
+import {
+  BottomSheetSnapPointChangeEvent,
+  BottomSheetStateChangeEvent,
+} from '../../../modules/bottom-sheet/src/BottomSheet.types'
 
 export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
 export * from '#/components/Dialog/types'
 export * from '#/components/Dialog/utils'
 // @ts-ignore
-export const Input = createInput(BottomSheetTextInput)
-
-function Backdrop(props: BottomSheetBackdropProps) {
-  const t = useTheme()
-  const bottomSheet = useBottomSheet()
-
-  const animatedStyle = useAnimatedStyle(() => {
-    const opacity =
-      (Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000
-
-    return {
-      opacity: Math.min(Math.max(opacity, 0), 0.55),
-    }
-  })
-
-  const onPress = React.useCallback(() => {
-    bottomSheet.close()
-  }, [bottomSheet])
-
-  return (
-    <Animated.View
-      style={[
-        t.atoms.bg_contrast_300,
-        {
-          top: 0,
-          left: 0,
-          right: 0,
-          bottom: 0,
-          position: 'absolute',
-        },
-        animatedStyle,
-      ]}>
-      <Pressable
-        accessibilityRole="button"
-        accessibilityLabel="Dialog backdrop"
-        accessibilityHint="Press the backdrop to close the dialog"
-        style={{flex: 1}}
-        onPress={onPress}
-      />
-    </Animated.View>
-  )
-}
+export const Input = createInput(TextInput)
 
 export function Outer({
   children,
@@ -88,24 +50,22 @@ export function Outer({
   onClose,
   nativeOptions,
   testID,
+  Portal = DefaultPortal,
 }: React.PropsWithChildren<DialogOuterProps>) {
   const t = useTheme()
-  const sheet = React.useRef<BottomSheet>(null)
-  const sheetOptions = nativeOptions?.sheet || {}
-  const hasSnapPoints = !!sheetOptions.snapPoints
-  const insets = useSafeAreaInsets()
+  const ref = React.useRef<BottomSheet>(null)
   const closeCallbacks = React.useRef<(() => void)[]>([])
-  const {setDialogIsOpen} = useDialogStateControlContext()
+  const {setDialogIsOpen, setFullyExpandedCount} =
+    useDialogStateControlContext()
 
-  /*
-   * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
-   */
-  const [openIndex, setOpenIndex] = React.useState(-1)
+  const prevSnapPoint = React.useRef<BottomSheetSnapPoint>(
+    BottomSheetSnapPoint.Hidden,
+  )
 
-  /*
-   * `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open.
-   */
-  const isOpen = openIndex > -1
+  const [disableDrag, setDisableDrag] = React.useState(false)
+  const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>(
+    BottomSheetSnapPoint.Partial,
+  )
 
   const callQueuedCallbacks = React.useCallback(() => {
     for (const cb of closeCallbacks.current) {
@@ -119,25 +79,19 @@ export function Outer({
     closeCallbacks.current = []
   }, [])
 
-  const open = React.useCallback<DialogControlProps['open']>(
-    ({index} = {}) => {
-      // Run any leftover callbacks that might have been queued up before calling `.open()`
-      callQueuedCallbacks()
-
-      setDialogIsOpen(control.id, true)
-      // can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
-      setOpenIndex(index || 0)
-      sheet.current?.snapToIndex(index || 0)
-    },
-    [setDialogIsOpen, control.id, callQueuedCallbacks],
-  )
+  const open = React.useCallback<DialogControlProps['open']>(() => {
+    // Run any leftover callbacks that might have been queued up before calling `.open()`
+    callQueuedCallbacks()
+    setDialogIsOpen(control.id, true)
+    ref.current?.present()
+  }, [setDialogIsOpen, control.id, callQueuedCallbacks])
 
   // This is the function that we call when we want to dismiss the dialog.
   const close = React.useCallback<DialogControlProps['close']>(cb => {
     if (typeof cb === 'function') {
       closeCallbacks.current.push(cb)
     }
-    sheet.current?.close()
+    ref.current?.dismiss()
   }, [])
 
   // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
@@ -146,12 +100,39 @@ export function Outer({
     // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
     // tells us that we need to toggle the accessibility overlay setting
     setDialogIsOpen(control.id, false)
-    setOpenIndex(-1)
-
     callQueuedCallbacks()
     onClose?.()
   }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
 
+  const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => {
+    const {snapPoint} = e.nativeEvent
+    setSnapPoint(snapPoint)
+
+    if (
+      snapPoint === BottomSheetSnapPoint.Full &&
+      prevSnapPoint.current !== BottomSheetSnapPoint.Full
+    ) {
+      setFullyExpandedCount(c => c + 1)
+    } else if (
+      snapPoint !== BottomSheetSnapPoint.Full &&
+      prevSnapPoint.current === BottomSheetSnapPoint.Full
+    ) {
+      setFullyExpandedCount(c => c - 1)
+    }
+    prevSnapPoint.current = snapPoint
+  }
+
+  const onStateChange = (e: BottomSheetStateChangeEvent) => {
+    if (e.nativeEvent.state === 'closed') {
+      onCloseAnimationComplete()
+
+      if (prevSnapPoint.current === BottomSheetSnapPoint.Full) {
+        setFullyExpandedCount(c => c - 1)
+      }
+      prevSnapPoint.current = BottomSheetSnapPoint.Hidden
+    }
+  }
+
   useImperativeHandle(
     control.ref,
     () => ({
@@ -161,159 +142,144 @@ export function Outer({
     [open, close],
   )
 
-  React.useEffect(() => {
-    return () => {
-      setDialogIsOpen(control.id, false)
-    }
-  }, [control.id, setDialogIsOpen])
-
-  const context = React.useMemo(() => ({close}), [close])
+  const context = React.useMemo(
+    () => ({
+      close,
+      isNativeDialog: true,
+      nativeSnapPoint: snapPoint,
+      disableDrag,
+      setDisableDrag,
+    }),
+    [close, snapPoint, disableDrag, setDisableDrag],
+  )
 
   return (
-    isOpen && (
-      <Portal>
-        <FullWindowOverlay>
-          <View
-            // iOS
-            accessibilityViewIsModal
-            // Android
-            importantForAccessibility="yes"
-            style={[a.absolute, a.inset_0]}
-            testID={testID}
-            onTouchMove={() => Keyboard.dismiss()}>
-            <BottomSheet
-              enableDynamicSizing={!hasSnapPoints}
-              enablePanDownToClose
-              keyboardBehavior="interactive"
-              android_keyboardInputMode="adjustResize"
-              keyboardBlurBehavior="restore"
-              topInset={insets.top}
-              {...sheetOptions}
-              snapPoints={sheetOptions.snapPoints || ['100%']}
-              ref={sheet}
-              index={openIndex}
-              backgroundStyle={{backgroundColor: 'transparent'}}
-              backdropComponent={Backdrop}
-              handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
-              handleStyle={{display: 'none'}}
-              onClose={onCloseAnimationComplete}>
-              <Context.Provider value={context}>
-                <View
-                  style={[
-                    a.absolute,
-                    a.inset_0,
-                    t.atoms.bg,
-                    {
-                      borderTopLeftRadius: 40,
-                      borderTopRightRadius: 40,
-                      height: Dimensions.get('window').height * 2,
-                    },
-                  ]}
-                />
-                {children}
-              </Context.Provider>
-            </BottomSheet>
-          </View>
-        </FullWindowOverlay>
-      </Portal>
-    )
+    <Portal>
+      <Context.Provider value={context}>
+        <BottomSheet
+          ref={ref}
+          cornerRadius={20}
+          backgroundColor={t.atoms.bg.backgroundColor}
+          {...nativeOptions}
+          onSnapPointChange={onSnapPointChange}
+          onStateChange={onStateChange}
+          disableDrag={disableDrag}>
+          <View testID={testID}>{children}</View>
+        </BottomSheet>
+      </Context.Provider>
+    </Portal>
   )
 }
 
 export function Inner({children, style}: DialogInnerProps) {
   const insets = useSafeAreaInsets()
   return (
-    <BottomSheetView
+    <View
       style={[
-        a.py_xl,
+        a.pt_2xl,
         a.px_xl,
         {
-          paddingTop: 40,
-          borderTopLeftRadius: 40,
-          borderTopRightRadius: 40,
-          paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
+          paddingBottom: insets.bottom + insets.top,
         },
-        flatten(style),
+        style,
       ]}>
       {children}
-    </BottomSheetView>
+    </View>
   )
 }
 
-export const ScrollableInner = React.forwardRef<
-  BottomSheetScrollViewMethods,
-  DialogInnerProps
->(function ScrollableInner({children, style}, ref) {
-  const insets = useSafeAreaInsets()
-  return (
-    <BottomSheetScrollView
-      keyboardShouldPersistTaps="handled"
-      style={[
-        a.flex_1, // main diff is this
-        a.p_xl,
-        a.h_full,
-        {
-          paddingTop: 40,
-          borderTopLeftRadius: 40,
-          borderTopRightRadius: 40,
-        },
-        style,
-      ]}
-      contentContainerStyle={a.pb_4xl}
-      ref={ref}>
-      {children}
-      <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
-    </BottomSheetScrollView>
-  )
-})
+export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
+  function ScrollableInner({children, style, ...props}, ref) {
+    const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
+    const insets = useSafeAreaInsets()
+    const [keyboardHeight, setKeyboardHeight] = React.useState(0)
+    useKeyboardHandler({
+      onEnd: e => {
+        'worklet'
+        runOnJS(setKeyboardHeight)(e.height)
+      },
+    })
+
+    const basePading =
+      (isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
+    const fullPaddingBase = insets.bottom + insets.top + basePading
+    const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50
+
+    const paddingBottom =
+      nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading
+
+    const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
+      const {contentOffset} = e.nativeEvent
+      if (contentOffset.y > 0 && !disableDrag) {
+        setDisableDrag(true)
+      } else if (contentOffset.y <= 1 && disableDrag) {
+        setDisableDrag(false)
+      }
+    }
+
+    return (
+      <KeyboardAwareScrollView
+        style={[style]}
+        contentContainerStyle={[a.pt_2xl, a.px_xl, {paddingBottom}]}
+        ref={ref}
+        {...props}
+        bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
+        bottomOffset={30}
+        scrollEventThrottle={50}
+        onScroll={isAndroid ? onScroll : undefined}>
+        {children}
+      </KeyboardAwareScrollView>
+    )
+  },
+)
 
 export const InnerFlatList = React.forwardRef<
-  BottomSheetFlatListMethods,
-  BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
->(function InnerFlatList({style, contentContainerStyle, ...props}, ref) {
+  ListMethods,
+  ListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
+>(function InnerFlatList({style, ...props}, ref) {
   const insets = useSafeAreaInsets()
-
+  const {nativeSnapPoint} = useDialogContext()
   return (
-    <BottomSheetFlatList
+    <List
       keyboardShouldPersistTaps="handled"
-      contentContainerStyle={[a.pb_4xl, flatten(contentContainerStyle)]}
+      bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
       ListFooterComponent={
         <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
       }
       ref={ref}
       {...props}
-      style={[
-        a.flex_1,
-        a.p_xl,
-        a.pt_0,
-        a.h_full,
-        {
-          marginTop: 40,
-        },
-        flatten(style),
-      ]}
+      style={[style]}
     />
   )
 })
 
 export function Handle() {
   const t = useTheme()
+  const {_} = useLingui()
+  const {screenReaderEnabled} = useA11y()
+  const {close} = useDialogContext()
 
   return (
-    <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
-      <View
-        style={[
-          a.rounded_sm,
-          {
-            top: a.pt_lg.paddingTop,
-            width: 35,
-            height: 4,
-            alignSelf: 'center',
-            backgroundColor: t.palette.contrast_900,
-            opacity: 0.5,
-          },
-        ]}
-      />
+    <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 20}]}>
+      <Pressable
+        accessible={screenReaderEnabled}
+        onPress={() => close()}
+        accessibilityLabel={_(msg`Dismiss`)}
+        accessibilityHint={_(msg`Double tap to close the dialog`)}>
+        <View
+          style={[
+            a.rounded_sm,
+            {
+              top: 10,
+              width: 35,
+              height: 5,
+              alignSelf: 'center',
+              backgroundColor: t.palette.contrast_975,
+              opacity: 0.5,
+            },
+          ]}
+        />
+      </Pressable>
     </View>
   )
 }
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index bf20bd295..7b9cfb693 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -103,6 +103,10 @@ export function Outer({
   const context = React.useMemo(
     () => ({
       close,
+      isNativeDialog: false,
+      nativeSnapPoint: 0,
+      disableDrag: false,
+      setDisableDrag: () => {},
     }),
     [close],
   )
@@ -229,10 +233,6 @@ export const InnerFlatList = React.forwardRef<
   )
 })
 
-export function Handle() {
-  return null
-}
-
 export function Close() {
   const {_} = useLingui()
   const {close} = React.useContext(Context)
@@ -258,3 +258,7 @@ export function Close() {
     </View>
   )
 }
+
+export function Handle() {
+  return null
+}
diff --git a/src/components/Dialog/sheet-wrapper.ts b/src/components/Dialog/sheet-wrapper.ts
new file mode 100644
index 000000000..37c663383
--- /dev/null
+++ b/src/components/Dialog/sheet-wrapper.ts
@@ -0,0 +1,20 @@
+import {useCallback} from 'react'
+
+import {useDialogStateControlContext} from '#/state/dialogs'
+
+/**
+ * If we're calling a system API like the image picker that opens a sheet
+ * wrap it in this function to make sure the status bar is the correct color.
+ */
+export function useSheetWrapper() {
+  const {setFullyExpandedCount} = useDialogStateControlContext()
+  return useCallback(
+    async <T>(promise: Promise<T>): Promise<T> => {
+      setFullyExpandedCount(c => c + 1)
+      const res = await promise
+      setFullyExpandedCount(c => c - 1)
+      return res
+    },
+    [setFullyExpandedCount],
+  )
+}
diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts
index 1ddab02ee..caa787535 100644
--- a/src/components/Dialog/types.ts
+++ b/src/components/Dialog/types.ts
@@ -4,9 +4,11 @@ import type {
   GestureResponderEvent,
   ScrollViewProps,
 } from 'react-native'
-import {BottomSheetProps} from '@discord/bottom-sheet/src'
 
 import {ViewStyleProp} from '#/alf'
+import {PortalComponent} from '#/components/Portal'
+import {BottomSheetViewProps} from '../../../modules/bottom-sheet'
+import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
 
 type A11yProps = Required<AccessibilityProps>
 
@@ -37,6 +39,10 @@ export type DialogControlProps = DialogControlRefProps & {
 
 export type DialogContextProps = {
   close: DialogControlProps['close']
+  isNativeDialog: boolean
+  nativeSnapPoint: BottomSheetSnapPoint
+  disableDrag: boolean
+  setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
 }
 
 export type DialogControlOpenOptions = {
@@ -52,11 +58,10 @@ export type DialogControlOpenOptions = {
 export type DialogOuterProps = {
   control: DialogControlProps
   onClose?: () => void
-  nativeOptions?: {
-    sheet?: Omit<BottomSheetProps, 'children'>
-  }
+  nativeOptions?: Omit<BottomSheetViewProps, 'children'>
   webOptions?: {}
   testID?: string
+  Portal?: PortalComponent
 }
 
 type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T