diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index b7e3c78d5e..252fd2f468 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -23,6 +23,7 @@ export const Context = createContext({ disableDrag: false, setDisableDrag: () => {}, isWithinDialog: false, + type: 'sheet', }) Context.displayName = 'DialogContext' diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 7cb870d86d..d613afbb18 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -1,6 +1,7 @@ import React, {useImperativeHandle} from 'react' import { type LayoutChangeEvent, + Modal, type NativeScrollEvent, type NativeSyntheticEvent, Pressable, @@ -49,7 +50,112 @@ export * from '#/components/Dialog/utils' export const Input = createInput(TextInput) -export function Outer({ +export function Outer(props: React.PropsWithChildren) { + if (props.type === 'alert') { + return + } + return +} + +function OuterAlert({ + children, + control, + onClose, + 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 context = React.useMemo( + () => ({ + close, + isNativeDialog: true, + nativeSnapPoint: BottomSheetSnapPoint.Hidden, + disableDrag: false, + setDisableDrag: () => {}, + isWithinDialog: true, + type: 'alert' as const, + }), + [close], + ) + + return ( + close()} + onDismiss={onDismiss}> + close()} + style={[ + a.flex_1, + a.justify_center, + a.align_center, + a.px_xl, + {backgroundColor: 'rgba(0,0,0,0.5)'}, + ]}> + e.stopPropagation()} + style={[ + t.atoms.bg, + a.w_full, + {borderRadius: 48, maxWidth: 320}, + a.overflow_hidden, + ]}> + + {children} + + + + + ) +} + +function OuterSheet({ children, control, onClose, @@ -155,6 +261,7 @@ export function Outer({ disableDrag, setDisableDrag, isWithinDialog: true, + type: 'sheet' as const, }), [close, snapPoint, disableDrag, setDisableDrag], ) @@ -206,8 +313,18 @@ export const ScrollableInner = React.forwardRef( {children, contentContainerStyle, header, ...props}, ref, ) { - const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() + const {nativeSnapPoint, disableDrag, setDisableDrag, type} = + useDialogContext() const insets = useSafeAreaInsets() + + if (type === 'alert') { + return ( + + {header} + {children} + + ) + } const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full let paddingBottom = 0 @@ -375,7 +492,11 @@ export function Handle({ const t = useTheme() const {_} = useLingui() const {screenReaderEnabled} = useA11y() - const {close} = useDialogContext() + const {close, type} = useDialogContext() + + if (type === 'alert') { + return null + } return ( diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 6470f09c6f..b85c678fb5 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -45,6 +45,7 @@ export function Outer({ control, onClose, webOptions, + type = 'sheet', }: React.PropsWithChildren) { const {_} = useLingui() const {gtMobile} = useBreakpoints() @@ -96,6 +97,8 @@ export function Outer({ [close, open], ) + const isAlert = type === 'alert' + const context = React.useMemo( () => ({ close, @@ -104,8 +107,9 @@ export function Outer({ disableDrag: false, setDisableDrag: () => {}, isWithinDialog: true, + type, }), - [close], + [close, type], ) return ( @@ -124,7 +128,9 @@ export function Outer({ a.inset_0, a.z_10, a.px_xl, - webOptions?.alignCenter ? a.justify_center : undefined, + webOptions?.alignCenter || isAlert + ? a.justify_center + : undefined, a.align_center, { overflowY: 'auto', @@ -165,9 +171,10 @@ export function Inner({ contentContainerStyle, }: DialogInnerProps) { const t = useTheme() - const {close} = React.useContext(Context) + const {close, type} = React.useContext(Context) const {gtMobile} = useBreakpoints() const {reduceMotionEnabled} = useA11y() + const isAlert = type === 'alert' FocusGuards.useFocusGuards() return ( @@ -189,12 +196,13 @@ export function Inner({ a.border, t.atoms.bg, { - maxWidth: 600, + maxWidth: isAlert ? 320 : 600, borderColor: t.palette.contrast_200, shadowColor: t.palette.black, shadowOpacity: t.name === 'light' ? 0.1 : 0.4, shadowRadius: 30, }, + isAlert && {borderRadius: 36}, !reduceMotionEnabled && a.zoom_fade_in, style, ])}> diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index 938d7c744d..5ecc1fada8 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -45,6 +45,7 @@ export type DialogContextProps = { setDisableDrag: React.Dispatch> // in the event that the hook is used outside of a dialog isWithinDialog: boolean + type: 'sheet' | 'alert' } export type DialogControlOpenOptions = { @@ -65,6 +66,12 @@ export type DialogOuterProps = { alignCenter?: boolean onBackgroundPress?: (e: GestureResponderEvent) => void } + /** + * The presentation style of the dialog. + * - `sheet` (default): Bottom sheet on native, standard modal on web. + * - `alert`: Centered alert modal on all platforms. + */ + type?: 'sheet' | 'alert' testID?: string }