Native dialog
This commit is contained in:
@@ -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>) {
|
import {DialogProps, DialogControl} from '#/view/com/Dialog/types'
|
||||||
return null
|
|
||||||
|
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<{}>) {
|
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 }>) {
|
export function Handle() {
|
||||||
return null
|
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() {
|
export function Close() {
|
||||||
|
|||||||
@@ -1,48 +1,73 @@
|
|||||||
import React from 'react'
|
import React, {useImperativeHandle} from 'react'
|
||||||
import {View, TouchableWithoutFeedback, DimensionValue} from 'react-native'
|
import {View, TouchableWithoutFeedback, DimensionValue} from 'react-native'
|
||||||
import {FocusScope} from '@tamagui/focus-scope'
|
import {FocusScope} from '@tamagui/focus-scope'
|
||||||
import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated'
|
import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated'
|
||||||
|
|
||||||
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
|
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
|
||||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
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 {Portal} from '#/view/com/Portal'
|
||||||
import {DialogProps} from '#/view/com/Dialog/types'
|
|
||||||
import {Button} from '#/view/com/Button'
|
import {Button} from '#/view/com/Button'
|
||||||
|
|
||||||
|
import {DialogProps, DialogControl} from '#/view/com/Dialog/types'
|
||||||
|
|
||||||
const Context = React.createContext<{
|
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() {
|
export function useDialog() {
|
||||||
return React.useContext(Context)
|
return React.useContext(Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Outer({
|
export function Outer({
|
||||||
isOpen,
|
control,
|
||||||
onDismiss,
|
onClose,
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<DialogProps>) {
|
}: React.PropsWithChildren<DialogProps>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false)
|
||||||
|
|
||||||
const dismiss = React.useCallback(() => {
|
const open = React.useCallback(() => {
|
||||||
onDismiss()
|
setIsOpen(true)
|
||||||
}, [onDismiss])
|
}, [setIsOpen])
|
||||||
|
|
||||||
|
const close = React.useCallback(() => {
|
||||||
|
setIsOpen(false)
|
||||||
|
onClose?.()
|
||||||
|
}, [onClose, setIsOpen])
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
control,
|
||||||
|
() => ({
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
}),
|
||||||
|
[open, close],
|
||||||
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function handler(e: KeyboardEvent) {
|
function handler(e: KeyboardEvent) {
|
||||||
if (e.key === 'Escape') dismiss()
|
if (e.key === 'Escape') close()
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', handler)
|
document.addEventListener('keydown', handler)
|
||||||
|
|
||||||
return () => document.removeEventListener('keydown', handler)
|
return () => document.removeEventListener('keydown', handler)
|
||||||
}, [dismiss])
|
}, [close])
|
||||||
|
|
||||||
const context = React.useMemo(() => ({dismiss}), [dismiss])
|
const context = React.useMemo(() => ({close}), [close])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -51,7 +76,7 @@ export function Outer({
|
|||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={onDismiss}>
|
onPress={close}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.absolute,
|
a.absolute,
|
||||||
@@ -84,11 +109,8 @@ export function Outer({
|
|||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInDown.duration(200)}
|
entering={FadeInDown.duration(200)}
|
||||||
exiting={FadeOut.duration(200)}
|
exiting={FadeOut.duration(200)}
|
||||||
aria-role="dialog"
|
aria-role="dialog">
|
||||||
>
|
<EventStopper>{children}</EventStopper>
|
||||||
<EventStopper>
|
|
||||||
{children}
|
|
||||||
</EventStopper>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</FocusScope>
|
</FocusScope>
|
||||||
</View>
|
</View>
|
||||||
@@ -104,52 +126,46 @@ export function Outer({
|
|||||||
export function Inner(props: React.PropsWithChildren<{}>) {
|
export function Inner(props: React.PropsWithChildren<{}>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<View style={[
|
<View style={[a.relative, a.rounded_md, a.p_xl, t.atoms.bg]}>
|
||||||
a.relative,
|
|
||||||
a.rounded_md,
|
|
||||||
a.p_xl,
|
|
||||||
t.atoms.bg,
|
|
||||||
]}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ children, title }: React.PropsWithChildren<{ title: string }>) {
|
export function Handle() {
|
||||||
const t = useTheme()
|
return null
|
||||||
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 Close() {
|
export function Close() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {dismiss} = useDialog()
|
const {close} = useDialog()
|
||||||
return (
|
return (
|
||||||
<View style={[
|
<View
|
||||||
a.absolute,
|
style={[
|
||||||
a.z_10,
|
a.absolute,
|
||||||
{
|
a.z_10,
|
||||||
top: a.pt_lg.paddingTop,
|
{
|
||||||
right: a.pr_lg.paddingRight,
|
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}) => (
|
<Button
|
||||||
<View style={[
|
onPress={close}
|
||||||
a.justify_center,
|
accessibilityLabel="Close dialog"
|
||||||
a.align_center,
|
accessibilityHint="Clicking this button will close the current dialog.">
|
||||||
a.rounded_full,
|
{() => (
|
||||||
t.atoms.bg_contrast_200,
|
<View
|
||||||
{
|
style={[
|
||||||
pointerEvents: 'none',
|
a.justify_center,
|
||||||
height: 32,
|
a.align_center,
|
||||||
width: 32,
|
a.rounded_full,
|
||||||
}
|
t.atoms.bg_contrast_200,
|
||||||
]}>
|
{
|
||||||
|
pointerEvents: 'none',
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
<Text>X</Text>
|
<Text>X</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
export type DialogProps = {
|
import React from 'react'
|
||||||
isOpen: boolean
|
|
||||||
onDismiss: () => void
|
export type DialogControl = {
|
||||||
|
open: (index?: number) => void
|
||||||
|
close: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DialogProps = {
|
||||||
|
control: React.RefObject<DialogControl>
|
||||||
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,22 +318,29 @@ function Forms() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Dialogs() {
|
function Dialogs() {
|
||||||
const [isOpen, setIsOpen] = React.useState(false)
|
const control = Dialog.useDialogControl()
|
||||||
|
|
||||||
return (
|
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
|
Open basic dialog
|
||||||
</Button>
|
</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'>
|
<Dialog.Outer control={control}>
|
||||||
Close basic dialog
|
<Dialog.Inner>
|
||||||
</Button>
|
<Dialog.Handle />
|
||||||
<Button type='secondary' size='small' onPress={() => setIsOpen(false)} accessibilityLabel='Open basic dialog' accessibilityHint='Open basic dialog'>
|
<Dialog.Close />
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onPress={() => control.current.close()}
|
||||||
|
accessibilityLabel="Open basic dialog"
|
||||||
|
accessibilityHint="Open basic dialog">
|
||||||
Close basic dialog
|
Close basic dialog
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Inner>
|
</Dialog.Inner>
|
||||||
|
|||||||
Reference in New Issue
Block a user