Native dialog

This commit is contained in:
Eric Bailey
2024-01-10 19:19:35 -06:00
parent 78a0f65234
commit ad6d9c4d9f
4 changed files with 211 additions and 75 deletions
+113 -7
View File
@@ -1,17 +1,123 @@
import React from 'react'
import React, {useImperativeHandle} from 'react'
import {View, Dimensions} from 'react-native'
import BottomSheet, {BottomSheetBackdrop} from '@gorhom/bottom-sheet'
import {DialogProps} from '#/view/com/Dialog/types'
import {useTheme, atoms as a} from '#/alf'
import {Portal} from '#/view/com/Portal'
export function Outer(props: React.PropsWithChildren<DialogProps>) {
return null
import {DialogProps, DialogControl} from '#/view/com/Dialog/types'
const Context = React.createContext<{
close: () => void
}>({
close: () => {},
})
export function useDialogControl() {
const control = React.useRef<DialogControl>({
open: () => {},
close: () => {},
})
return control
}
export function useDialog() {
return React.useContext(Context)
}
export function Outer({
control,
onClose,
children,
}: React.PropsWithChildren<DialogProps>) {
const t = useTheme()
const sheet = React.useRef<BottomSheet>(null)
const open = React.useCallback<DialogControl['open']>((i = 0) => {
sheet.current?.snapToIndex(i)
}, [])
const close = React.useCallback(() => {
sheet.current?.close()
onClose?.()
}, [onClose])
useImperativeHandle(
control,
() => ({
open,
close,
}),
[open, close],
)
return (
<Portal>
<BottomSheet
ref={sheet}
index={-1}
snapPoints={['90%']}
enablePanDownToClose
keyboardBehavior="extend"
backgroundStyle={{backgroundColor: 'transparent'}}
android_keyboardInputMode="adjustResize"
backdropComponent={props => (
<BottomSheetBackdrop
appearsOnIndex={0}
disappearsOnIndex={-1}
{...props}
/>
)}
handleIndicatorStyle={{backgroundColor: t.palette.primary}}
handleStyle={{display: 'none'}}
onChange={() => {}}
onClose={onClose}>
{children}
</BottomSheet>
</Portal>
)
}
export function Inner(props: React.PropsWithChildren<{}>) {
return null
const t = useTheme()
return (
<View
style={[
a.absolute,
a.inset_0,
a.p_lg,
a.pt_xxl,
t.atoms.bg,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
height: Dimensions.get('window').height * 2,
},
]}>
{props.children}
</View>
)
}
export function Header(props: React.PropsWithChildren<{ title: string }>) {
return null
export function Handle() {
const t = useTheme()
return (
<View
style={[
a.absolute,
a.rounded_sm,
a.z_10,
t.atoms.bg_contrast_200,
{
top: 12,
width: 80,
height: 6,
alignSelf: 'center',
},
]}
/>
)
}
export function Close() {
+71 -55
View File
@@ -1,48 +1,73 @@
import React from 'react'
import React, {useImperativeHandle} from 'react'
import {View, TouchableWithoutFeedback, DimensionValue} from 'react-native'
import {FocusScope} from '@tamagui/focus-scope'
import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {EventStopper} from '#/view/com/util/EventStopper'
import {H3, Text} from '#/view/com/Typography'
import {Text} from '#/view/com/Typography'
import {Portal} from '#/view/com/Portal'
import {DialogProps} from '#/view/com/Dialog/types'
import {Button} from '#/view/com/Button'
import {DialogProps, DialogControl} from '#/view/com/Dialog/types'
const Context = React.createContext<{
dismiss: () => void
close: () => void
}>({
dismiss: () => {},
close: () => {},
})
export function useDialogControl() {
const control = React.useRef<DialogControl>({
open: () => {},
close: () => {},
})
return control
}
export function useDialog() {
return React.useContext(Context)
}
export function Outer({
isOpen,
onDismiss,
control,
onClose,
children,
}: React.PropsWithChildren<DialogProps>) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
const [isOpen, setIsOpen] = React.useState(false)
const dismiss = React.useCallback(() => {
onDismiss()
}, [onDismiss])
const open = React.useCallback(() => {
setIsOpen(true)
}, [setIsOpen])
const close = React.useCallback(() => {
setIsOpen(false)
onClose?.()
}, [onClose, setIsOpen])
useImperativeHandle(
control,
() => ({
open,
close,
}),
[open, close],
)
React.useEffect(() => {
function handler(e: KeyboardEvent) {
if (e.key === 'Escape') dismiss()
if (e.key === 'Escape') close()
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [dismiss])
}, [close])
const context = React.useMemo(() => ({dismiss}), [dismiss])
const context = React.useMemo(() => ({close}), [close])
return (
<>
@@ -51,7 +76,7 @@ export function Outer({
<Context.Provider value={context}>
<TouchableWithoutFeedback
accessibilityRole="button"
onPress={onDismiss}>
onPress={close}>
<View
style={[
a.absolute,
@@ -84,11 +109,8 @@ export function Outer({
<Animated.View
entering={FadeInDown.duration(200)}
exiting={FadeOut.duration(200)}
aria-role="dialog"
>
<EventStopper>
{children}
</EventStopper>
aria-role="dialog">
<EventStopper>{children}</EventStopper>
</Animated.View>
</FocusScope>
</View>
@@ -104,52 +126,46 @@ export function Outer({
export function Inner(props: React.PropsWithChildren<{}>) {
const t = useTheme()
return (
<View style={[
a.relative,
a.rounded_md,
a.p_xl,
t.atoms.bg,
]}>
<View style={[a.relative, a.rounded_md, a.p_xl, t.atoms.bg]}>
{props.children}
</View>
)
}
export function Header({ children, title }: React.PropsWithChildren<{ title: string }>) {
const t = useTheme()
return (
<View style={[a.flex_row, a.justify_between, a.mb_lg]}>
<H3 style={[t.atoms.text_contrast_500, a.pr_lg]}>{title}</H3>
{children}
</View>
)
export function Handle() {
return null
}
export function Close() {
const t = useTheme()
const {dismiss} = useDialog()
const {close} = useDialog()
return (
<View style={[
a.absolute,
a.z_10,
{
top: a.pt_lg.paddingTop,
right: a.pr_lg.paddingRight,
}
]}>
<Button onPress={dismiss} accessibilityLabel='Close dialog' accessibilityHint='Clicking this button will close the current dialog.'>
{({ state}) => (
<View style={[
a.justify_center,
a.align_center,
a.rounded_full,
t.atoms.bg_contrast_200,
{
pointerEvents: 'none',
height: 32,
width: 32,
}
]}>
<View
style={[
a.absolute,
a.z_10,
{
top: a.pt_lg.paddingTop,
right: a.pr_lg.paddingRight,
},
]}>
<Button
onPress={close}
accessibilityLabel="Close dialog"
accessibilityHint="Clicking this button will close the current dialog.">
{() => (
<View
style={[
a.justify_center,
a.align_center,
a.rounded_full,
t.atoms.bg_contrast_200,
{
pointerEvents: 'none',
height: 32,
width: 32,
},
]}>
<Text>X</Text>
</View>
)}
+10 -3
View File
@@ -1,4 +1,11 @@
export type DialogProps = {
isOpen: boolean
onDismiss: () => void
import React from 'react'
export type DialogControl = {
open: (index?: number) => void
close: () => void
}
export type DialogProps = {
control: React.RefObject<DialogControl>
onClose?: () => void
}
+17 -10
View File
@@ -318,22 +318,29 @@ function Forms() {
}
function Dialogs() {
const [isOpen, setIsOpen] = React.useState(false)
const control = Dialog.useDialogControl()
return (
<>
<Button type='secondary' size='small' onPress={() => setIsOpen(true)} accessibilityLabel='Open basic dialog' accessibilityHint='Open basic dialog'>
<Button
type="secondary"
size="small"
onPress={() => control.current.open()}
accessibilityLabel="Open basic dialog"
accessibilityHint="Open basic dialog">
Open basic dialog
</Button>
<Dialog.Outer isOpen={isOpen} onDismiss={() => setIsOpen(false)}>
<Dialog.Inner>
<Dialog.Close />
<Dialog.Header title='Basic dialog' />
<Button type='primary' size='small' onPress={() => setIsOpen(false)} accessibilityLabel='Open basic dialog' accessibilityHint='Open basic dialog'>
Close basic dialog
</Button>
<Button type='secondary' size='small' onPress={() => setIsOpen(false)} accessibilityLabel='Open basic dialog' accessibilityHint='Open basic dialog'>
<Dialog.Outer control={control}>
<Dialog.Inner>
<Dialog.Handle />
<Dialog.Close />
<Button
type="primary"
size="small"
onPress={() => control.current.close()}
accessibilityLabel="Open basic dialog"
accessibilityHint="Open basic dialog">
Close basic dialog
</Button>
</Dialog.Inner>