add detached variant to modal

This commit is contained in:
vineyardbovines
2026-03-05 11:57:19 -05:00
parent 1ae77aa997
commit aa1fbf4628
4 changed files with 144 additions and 7 deletions
+1
View File
@@ -23,6 +23,7 @@ export const Context = createContext<DialogContextProps>({
disableDrag: false,
setDisableDrag: () => {},
isWithinDialog: false,
type: 'sheet',
})
Context.displayName = 'DialogContext'
+124 -3
View File
@@ -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<DialogOuterProps>) {
if (props.type === 'alert') {
return <OuterAlert {...props} />
}
return <OuterSheet {...props} />
}
function OuterAlert({
children,
control,
onClose,
testID,
}: React.PropsWithChildren<DialogOuterProps>) {
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<DialogControlProps['open']>(() => {
callQueuedCallbacks()
setDialogIsOpen(control.id, true)
setIsOpen(true)
}, [setDialogIsOpen, control.id, callQueuedCallbacks])
const close = React.useCallback<DialogControlProps['close']>(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 (
<Modal
visible={isOpen}
transparent
animationType="fade"
onRequestClose={() => close()}
onDismiss={onDismiss}>
<Pressable
accessibilityRole="button"
onPress={() => close()}
style={[
a.flex_1,
a.justify_center,
a.align_center,
a.px_xl,
{backgroundColor: 'rgba(0,0,0,0.5)'},
]}>
<Pressable
accessibilityRole="button"
onPress={e => e.stopPropagation()}
style={[
t.atoms.bg,
a.w_full,
{borderRadius: 48, maxWidth: 320},
a.overflow_hidden,
]}>
<Context.Provider value={context}>
<View testID={testID}>{children}</View>
</Context.Provider>
</Pressable>
</Pressable>
</Modal>
)
}
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<ScrollView, DialogInnerProps>(
{children, contentContainerStyle, header, ...props},
ref,
) {
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
const {nativeSnapPoint, disableDrag, setDisableDrag, type} =
useDialogContext()
const insets = useSafeAreaInsets()
if (type === 'alert') {
return (
<View style={[a.p_2xl, contentContainerStyle]} {...props}>
{header}
{children}
</View>
)
}
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 (
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 20}]}>
+12 -4
View File
@@ -45,6 +45,7 @@ export function Outer({
control,
onClose,
webOptions,
type = 'sheet',
}: React.PropsWithChildren<DialogOuterProps>) {
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 (
<FocusScope.FocusScope loop asChild trapped>
@@ -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,
])}>
+7
View File
@@ -45,6 +45,7 @@ export type DialogContextProps = {
setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
// 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
}