Improve dialogs (#2933)
* Improve dialogs * Remove comment, revert storybook * Hacky fix * Comments
This commit is contained in:
parent
da62a77f05
commit
b52a742925
6 changed files with 123 additions and 85 deletions
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
5
src/components/icons/Times.tsx
Normal file
5
src/components/icons/Times.tsx
Normal 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',
|
||||||
|
})
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue