diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index d613afbb18..40e751bf29 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -1,4 +1,4 @@ -import React, {useImperativeHandle} from 'react' +import React, {useImperativeHandle, useMemo, useRef, useState} from 'react' import { type LayoutChangeEvent, Modal, @@ -22,9 +22,7 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {ScrollProvider} from '#/lib/ScrollContext' -import {logger} from '#/logger' import {useA11y} from '#/state/a11y' -import {useDialogStateControlContext} from '#/state/dialogs' import {List, type ListMethods, type ListProps} from '#/view/com/util/List' import {android, atoms as a, ios, platform, tokens, useTheme} from '#/alf' import {useThemeName} from '#/alf/util/useColorModeTheme' @@ -34,6 +32,7 @@ import { type DialogInnerProps, type DialogOuterProps, } from '#/components/Dialog/types' +import {useDialogCallbackQueue} from '#/components/Dialog/utils' import {createInput} from '#/components/forms/TextField' import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS} from '#/env' import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' @@ -64,50 +63,29 @@ function OuterAlert({ testID, }: React.PropsWithChildren) { const t = useTheme() - const [isOpen, setIsOpen] = React.useState(false) - const closeCallbacks = React.useRef<(() => void)[]>([]) - const {setDialogIsOpen} = useDialogStateControlContext() - - const callQueuedCallbacks = React.useCallback(() => { - for (const cb of closeCallbacks.current) { - try { - cb() - } catch (e: any) { - logger.error(e || 'Error running close callback') - } - } - closeCallbacks.current = [] - }, []) - - const open = React.useCallback(() => { - callQueuedCallbacks() - setDialogIsOpen(control.id, true) - setIsOpen(true) - }, [setDialogIsOpen, control.id, callQueuedCallbacks]) - - const close = React.useCallback(cb => { - if (typeof cb === 'function') { - closeCallbacks.current.push(cb) - } - setIsOpen(false) - }, []) - - const onDismiss = React.useCallback(() => { - setDialogIsOpen(control.id, false) - callQueuedCallbacks() - onClose?.() - }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]) - - useImperativeHandle( - control.ref, - () => ({ - open, - close, - }), - [open, close], + const [isOpen, setIsOpen] = useState(false) + const {enqueueCallback, handleOpen, handleClose} = useDialogCallbackQueue( + control, + onClose, ) - const context = React.useMemo( + const open: DialogControlProps['open'] = () => { + handleOpen() + setIsOpen(true) + } + + const close: DialogControlProps['close'] = cb => { + enqueueCallback(cb) + setIsOpen(false) + } + + const onDismiss = () => { + handleClose() + } + + useImperativeHandle(control.ref, () => ({open, close}), [open, close]) + + const context = useMemo( () => ({ close, isNativeDialog: true, @@ -164,56 +142,33 @@ function OuterSheet({ }: React.PropsWithChildren) { const themeName = useThemeName() const t = useTheme(themeName) - const ref = React.useRef(null) - const closeCallbacks = React.useRef<(() => void)[]>([]) - const {setDialogIsOpen, setFullyExpandedCount} = - useDialogStateControlContext() + const ref = useRef(null) + const {enqueueCallback, handleOpen, handleClose, setFullyExpandedCount} = + useDialogCallbackQueue(control, onClose) - const prevSnapPoint = React.useRef( + const prevSnapPoint = useRef( BottomSheetSnapPoint.Hidden, ) - const [disableDrag, setDisableDrag] = React.useState(false) - const [snapPoint, setSnapPoint] = React.useState( + const [disableDrag, setDisableDrag] = useState(false) + const [snapPoint, setSnapPoint] = useState( BottomSheetSnapPoint.Partial, ) - const callQueuedCallbacks = React.useCallback(() => { - for (const cb of closeCallbacks.current) { - try { - cb() - } catch (e: any) { - logger.error(e || 'Error running close callback') - } - } - - closeCallbacks.current = [] - }, []) - - const open = React.useCallback(() => { - // Run any leftover callbacks that might have been queued up before calling `.open()` - callQueuedCallbacks() - setDialogIsOpen(control.id, true) + const open: DialogControlProps['open'] = () => { + handleOpen() 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(cb => { - if (typeof cb === 'function') { - closeCallbacks.current.push(cb) - } + const close: DialogControlProps['close'] = cb => { + enqueueCallback(cb) ref.current?.dismiss() - }, []) + } - // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to - // happen before we run this. It is passed to the `BottomSheet` component. - const onCloseAnimationComplete = React.useCallback(() => { - // 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) - callQueuedCallbacks() - onClose?.() - }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]) + // Runs after the bottom sheet close animation completes. + const onCloseAnimationComplete = () => { + handleClose() + } const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => { const {snapPoint} = e.nativeEvent @@ -244,16 +199,9 @@ function OuterSheet({ } } - useImperativeHandle( - control.ref, - () => ({ - open, - close, - }), - [open, close], - ) + useImperativeHandle(control.ref, () => ({open, close}), [open, close]) - const context = React.useMemo( + const context = useMemo( () => ({ close, isNativeDialog: true, diff --git a/src/components/Dialog/utils.ts b/src/components/Dialog/utils.ts index 812e8cca15..cb2d337b46 100644 --- a/src/components/Dialog/utils.ts +++ b/src/components/Dialog/utils.ts @@ -1,9 +1,55 @@ -import React from 'react' +import {useEffect, useRef} from 'react' +import {logger} from '#/logger' +import {useDialogStateControlContext} from '#/state/dialogs' import {type DialogControlProps} from '#/components/Dialog/types' +export function useDialogCallbackQueue( + control: DialogControlProps, + onClose?: () => void, +) { + const closeCallbacks = useRef<(() => void)[]>([]) + const {setDialogIsOpen, setFullyExpandedCount} = + useDialogStateControlContext() + + const callQueuedCallbacks = () => { + for (const cb of closeCallbacks.current) { + try { + cb() + } catch (e: any) { + logger.error(e || 'Error running close callback') + } + } + closeCallbacks.current = [] + } + + const enqueueCallback = (cb?: () => void) => { + if (typeof cb === 'function') { + closeCallbacks.current.push(cb) + } + } + + const handleOpen = () => { + callQueuedCallbacks() + setDialogIsOpen(control.id, true) + } + + const handleClose = () => { + setDialogIsOpen(control.id, false) + callQueuedCallbacks() + onClose?.() + } + + return { + enqueueCallback, + handleOpen, + handleClose, + setFullyExpandedCount, + } +} + export function useAutoOpen(control: DialogControlProps, showTimeout?: number) { - React.useEffect(() => { + useEffect(() => { if (showTimeout) { const timeout = setTimeout(() => { control.open()