Improve dialogs (#2933)

* Improve dialogs

* Remove comment, revert storybook

* Hacky fix

* Comments
This commit is contained in:
Eric Bailey 2024-02-19 18:18:13 -06:00 committed by GitHub
parent da62a77f05
commit b52a742925
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 123 additions and 85 deletions

View file

@ -8,7 +8,7 @@ import BottomSheet, {
} from '@gorhom/bottom-sheet' } from '@gorhom/bottom-sheet'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useTheme, atoms as a} from '#/alf' import {useTheme, atoms as a, flatten} from '#/alf'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {createInput} from '#/components/forms/TextField' import {createInput} from '#/components/forms/TextField'
@ -36,9 +36,23 @@ export function Outer({
const hasSnapPoints = !!sheetOptions.snapPoints const hasSnapPoints = !!sheetOptions.snapPoints
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const open = React.useCallback<DialogControlProps['open']>((i = 0) => { /*
sheet.current?.snapToIndex(i) * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
}, []) */
const [openIndex, setOpenIndex] = React.useState(-1)
/*
* `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open.
*/
const isOpen = openIndex > -1
const open = React.useCallback<DialogControlProps['open']>(
({index} = {}) => {
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
setOpenIndex(index || 0)
},
[setOpenIndex],
)
const close = React.useCallback(() => { const close = React.useCallback(() => {
sheet.current?.close() sheet.current?.close()
@ -57,77 +71,80 @@ export function Outer({
(index: number) => { (index: number) => {
if (index === -1) { if (index === -1) {
onClose?.() onClose?.()
setOpenIndex(-1)
} }
}, },
[onClose], [onClose, setOpenIndex],
) )
const context = React.useMemo(() => ({close}), [close]) const context = React.useMemo(() => ({close}), [close])
return ( return (
<Portal> isOpen && (
<BottomSheet <Portal>
enableDynamicSizing={!hasSnapPoints} <BottomSheet
enablePanDownToClose enableDynamicSizing={!hasSnapPoints}
keyboardBehavior="interactive" enablePanDownToClose
android_keyboardInputMode="adjustResize" keyboardBehavior="interactive"
keyboardBlurBehavior="restore" android_keyboardInputMode="adjustResize"
topInset={insets.top} keyboardBlurBehavior="restore"
{...sheetOptions} topInset={insets.top}
ref={sheet} {...sheetOptions}
index={-1} ref={sheet}
backgroundStyle={{backgroundColor: 'transparent'}} index={openIndex}
backdropComponent={props => ( backgroundStyle={{backgroundColor: 'transparent'}}
<BottomSheetBackdrop backdropComponent={props => (
opacity={0.4} <BottomSheetBackdrop
appearsOnIndex={0} opacity={0.4}
disappearsOnIndex={-1} appearsOnIndex={0}
{...props} disappearsOnIndex={-1}
/> {...props}
)} />
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} )}
handleStyle={{display: 'none'}} handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
onChange={onChange}> handleStyle={{display: 'none'}}
<Context.Provider value={context}> onChange={onChange}>
<View <Context.Provider value={context}>
style={[ <View
a.absolute, style={[
a.inset_0, a.absolute,
t.atoms.bg, a.inset_0,
{ t.atoms.bg,
borderTopLeftRadius: 40, {
borderTopRightRadius: 40, borderTopLeftRadius: 40,
height: Dimensions.get('window').height * 2, borderTopRightRadius: 40,
}, height: Dimensions.get('window').height * 2,
]} },
/> ]}
{children} />
</Context.Provider> {hasSnapPoints ? children : <View>{children}</View>}
</BottomSheet> </Context.Provider>
</Portal> </BottomSheet>
</Portal>
)
) )
} }
// TODO a11y props here, or is that handled by the sheet? export function Inner({children, style}: DialogInnerProps) {
export function Inner(props: DialogInnerProps) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
return ( return (
<BottomSheetView <BottomSheetView
style={[ style={[
a.p_lg, a.p_xl,
{ {
paddingTop: 40, paddingTop: 40,
borderTopLeftRadius: 40, borderTopLeftRadius: 40,
borderTopRightRadius: 40, borderTopRightRadius: 40,
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
}, },
flatten(style),
]}> ]}>
{props.children} {children}
</BottomSheetView> </BottomSheetView>
) )
} }
export function ScrollableInner(props: DialogInnerProps) { export function ScrollableInner({children, style}: DialogInnerProps) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
return ( return (
<BottomSheetScrollView <BottomSheetScrollView
@ -136,13 +153,15 @@ export function ScrollableInner(props: DialogInnerProps) {
style={[ style={[
a.flex_1, // main diff is this a.flex_1, // main diff is this
a.p_xl, a.p_xl,
a.h_full,
{ {
paddingTop: 40, paddingTop: 40,
borderTopLeftRadius: 40, borderTopLeftRadius: 40,
borderTopRightRadius: 40, borderTopRightRadius: 40,
}, },
flatten(style),
]}> ]}>
{props.children} {children}
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
</BottomSheetScrollView> </BottomSheetScrollView>
) )

View file

@ -5,11 +5,13 @@ import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useTheme, atoms as a, useBreakpoints, web} from '#/alf' import {useTheme, atoms as a, useBreakpoints, web, flatten} from '#/alf'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context' import {Context} from '#/components/Dialog/context'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
@ -18,9 +20,9 @@ export {Input} from '#/components/forms/TextField'
const stopPropagation = (e: any) => e.stopPropagation() const stopPropagation = (e: any) => e.stopPropagation()
export function Outer({ export function Outer({
children,
control, control,
onClose, onClose,
children,
}: React.PropsWithChildren<DialogOuterProps>) { }: React.PropsWithChildren<DialogOuterProps>) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@ -147,7 +149,7 @@ export function Inner({
a.rounded_md, a.rounded_md,
a.w_full, a.w_full,
a.border, a.border,
gtMobile ? a.p_xl : a.p_lg, gtMobile ? a.p_2xl : a.p_xl,
t.atoms.bg, t.atoms.bg,
{ {
maxWidth: 600, maxWidth: 600,
@ -156,7 +158,7 @@ export function Inner({
shadowOpacity: t.name === 'light' ? 0.1 : 0.4, shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
shadowRadius: 30, shadowRadius: 30,
}, },
...(Array.isArray(style) ? style : [style || {}]), flatten(style),
]}> ]}>
{children} {children}
</Animated.View> </Animated.View>
@ -170,25 +172,28 @@ export function Handle() {
return null return null
} }
/** export function Close() {
* TODO(eric) unused rn const {_} = useLingui()
*/ const {close} = React.useContext(Context)
// export function Close() { return (
// const {_} = useLingui() <View
// const t = useTheme() style={[
// const {close} = useDialogContext() a.absolute,
// return ( a.z_10,
// <View {
// style={[ top: a.pt_md.paddingTop,
// a.absolute, right: a.pr_md.paddingRight,
// a.z_10, },
// { ]}>
// top: a.pt_lg.paddingTop, <Button
// right: a.pr_lg.paddingRight, size="small"
// }, variant="ghost"
// ]}> color="primary"
// <Button onPress={close} label={_(msg`Close active dialog`)}> shape="round"
// </Button> onPress={close}
// </View> label={_(msg`Close active dialog`)}>
// ) <ButtonIcon icon={X} size="md" />
// } </Button>
</View>
)
}

View file

@ -1,15 +1,27 @@
import React from 'react' import React from 'react'
import type {ViewStyle, AccessibilityProps} from 'react-native' import type {AccessibilityProps} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet' import {BottomSheetProps} from '@gorhom/bottom-sheet'
import {ViewStyleProp} from '#/alf'
type A11yProps = Required<AccessibilityProps> type A11yProps = Required<AccessibilityProps>
export type DialogContextProps = { export type DialogContextProps = {
close: () => void close: () => void
} }
export type DialogControlOpenOptions = {
/**
* NATIVE ONLY
*
* Optional index of the snap point to open the bottom sheet to. Defaults to
* 0, which is the first snap point (i.e. "open").
*/
index?: number
}
export type DialogControlProps = { export type DialogControlProps = {
open: (index?: number) => void open: (options?: DialogControlOpenOptions) => void
close: () => void close: () => void
} }
@ -26,10 +38,7 @@ export type DialogOuterProps = {
webOptions?: {} webOptions?: {}
} }
type DialogInnerPropsBase<T> = React.PropsWithChildren<{ type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
style?: ViewStyle
}> &
T
export type DialogInnerProps = export type DialogInnerProps =
| DialogInnerPropsBase<{ | DialogInnerPropsBase<{
label?: undefined label?: undefined

View file

@ -41,7 +41,7 @@ export function Outer({
<Dialog.Inner <Dialog.Inner
accessibilityLabelledBy={titleId} accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId} accessibilityDescribedBy={descriptionId}
style={{width: 'auto', maxWidth: 400}}> style={[{width: 'auto', maxWidth: 400}]}>
{children} {children}
</Dialog.Inner> </Dialog.Inner>
</Context.Provider> </Context.Provider>

View file

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const TimesLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4.293 4.293a1 1 0 0 1 1.414 0L12 10.586l6.293-6.293a1 1 0 1 1 1.414 1.414L13.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414L12 13.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L10.586 12 4.293 5.707a1 1 0 0 1 0-1.414Z',
})

View file

@ -50,7 +50,7 @@ export function Dialogs() {
<Dialog.Outer <Dialog.Outer
control={control} control={control}
nativeOptions={{sheet: {snapPoints: ['90%']}}}> nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.ScrollableInner <Dialog.ScrollableInner