Add Prompt, Dialog refinement, mobile refinement

This commit is contained in:
Eric Bailey
2024-01-11 22:06:59 -06:00
parent 09874504af
commit b6b6c701fc
11 changed files with 305 additions and 58 deletions
+1
View File
@@ -5,6 +5,7 @@ import * as themes from '#/alf/themes'
export * as tokens from '#/alf/tokens'
export {atoms} from '#/alf/atoms'
export * from '#/alf/util/platform'
export * from '#/alf/util/flatten'
type BreakpointName = keyof typeof breakpoints
+24
View File
@@ -11,12 +11,36 @@ export const lightPalette = {
primary: tokens.color.blue_500,
positive: tokens.color.green_500,
negative: tokens.color.red_500,
contrast_0: tokens.color.gray_0,
contrast_100: tokens.color.gray_100,
contrast_200: tokens.color.gray_200,
contrast_300: tokens.color.gray_300,
contrast_400: tokens.color.gray_400,
contrast_500: tokens.color.gray_500,
contrast_600: tokens.color.gray_600,
contrast_700: tokens.color.gray_700,
contrast_800: tokens.color.gray_800,
contrast_900: tokens.color.gray_900,
contrast_1000: tokens.color.gray_1000,
} as const
export const darkPalette: Palette = {
primary: tokens.color.blue_500,
positive: tokens.color.green_400,
negative: tokens.color.red_400,
contrast_0: tokens.color.gray_1000,
contrast_100: tokens.color.gray_900,
contrast_200: tokens.color.gray_800,
contrast_300: tokens.color.gray_700,
contrast_400: tokens.color.gray_600,
contrast_500: tokens.color.gray_500,
contrast_600: tokens.color.gray_400,
contrast_700: tokens.color.gray_300,
contrast_800: tokens.color.gray_200,
contrast_900: tokens.color.gray_100,
contrast_1000: tokens.color.gray_0,
} as const
export const light = {
+3
View File
@@ -0,0 +1,3 @@
import {StyleSheet} from 'react-native'
export const flatten = StyleSheet.flatten
+1 -7
View File
@@ -284,13 +284,7 @@ export function ButtonText({
return (
<Text
{...rest}
style={[
atoms.flex_1,
atoms.font_semibold,
atoms.text_center,
...textStyles,
style,
]}>
style={[atoms.font_semibold, atoms.text_center, ...textStyles, style]}>
{children}
</Text>
)
+5 -1
View File
@@ -15,5 +15,9 @@ export function useDialogControl() {
close: () => {},
})
return control
return {
ref: control,
open: () => control.current.open(),
close: () => control.current.close(),
}
}
+26 -14
View File
@@ -12,12 +12,14 @@ import {
} from '#/view/com/Dialog/types'
import {Context} from '#/view/com/Dialog/context'
export {useDialogControl} from '#/view/com/Dialog/context'
export {useDialogControl, useDialogContext} from '#/view/com/Dialog/context'
export * from '#/view/com/Dialog/types'
export function Outer({
children,
control,
onClose,
children,
nativeOptions,
}: React.PropsWithChildren<DialogOuterProps>) {
const t = useTheme()
const sheet = React.useRef<BottomSheet>(null)
@@ -32,7 +34,7 @@ export function Outer({
}, [onClose])
useImperativeHandle(
control,
control.ref,
() => ({
open,
close,
@@ -45,13 +47,14 @@ export function Outer({
return (
<Portal>
<BottomSheet
ref={sheet}
index={-1}
snapPoints={['90%']}
enablePanDownToClose
keyboardBehavior="extend"
backgroundStyle={{backgroundColor: 'transparent'}}
android_keyboardInputMode="adjustResize"
{...(nativeOptions?.sheet || {})}
ref={sheet}
index={-1}
backgroundStyle={{backgroundColor: 'transparent'}}
backdropComponent={props => (
<BottomSheetBackdrop
appearsOnIndex={0}
@@ -61,28 +64,37 @@ export function Outer({
)}
handleIndicatorStyle={{backgroundColor: t.palette.primary}}
handleStyle={{display: 'none'}}
onChange={() => {}}
onClose={onClose}>
<Context.Provider value={context}>{children}</Context.Provider>
<Context.Provider value={context}>
<View
style={[
a.absolute,
a.inset_0,
t.atoms.bg,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
height: Dimensions.get('window').height * 2,
},
]}
/>
{children}
</Context.Provider>
</BottomSheet>
</Portal>
)
}
// TODO a11y props here, or is that handled by the sheet?
export function Inner(props: DialogInnerProps) {
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}
@@ -101,7 +113,7 @@ export function Handle() {
t.atoms.bg_contrast_200,
{
top: 12,
width: 80,
width: 50,
height: 6,
alignSelf: 'center',
},
+19 -5
View File
@@ -11,7 +11,8 @@ import {Button} from '#/view/com/Button'
import {DialogOuterProps, DialogInnerProps} from '#/view/com/Dialog/types'
import {Context, useDialogContext} from '#/view/com/Dialog/context'
export {useDialogControl} from '#/view/com/Dialog/context'
export {useDialogControl, useDialogContext} from '#/view/com/Dialog/context'
export * from '#/view/com/Dialog/types'
const stopPropagation = (e: any) => e.stopPropagation()
@@ -38,7 +39,7 @@ export function Outer({
}, [onClose, setIsOpen])
useImperativeHandle(
control,
control.ref,
() => ({
open,
close,
@@ -58,7 +59,12 @@ export function Outer({
return () => document.removeEventListener('keydown', handler)
}, [isOpen, close])
const context = React.useMemo(() => ({close}), [close])
const context = React.useMemo(
() => ({
close,
}),
[close],
)
return (
<>
@@ -111,12 +117,19 @@ export function Outer({
)
}
export function Inner({children, style}: DialogInnerProps) {
export function Inner({
children,
style,
accessibilityLabelledBy,
accessibilityDescribedBy,
}: DialogInnerProps) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
return (
<FocusScope loop enabled trapped>
<Animated.View
aria-labelledby={accessibilityLabelledBy}
aria-describedby={accessibilityDescribedBy}
aria-role="dialog"
// @ts-ignore web only -prf
onClick={stopPropagation}
@@ -128,9 +141,10 @@ export function Inner({children, style}: DialogInnerProps) {
a.relative,
a.rounded_md,
a.w_full,
a.border,
gtMobile ? a.p_xl : a.p_lg,
t.atoms.bg,
{maxWidth: 600},
{maxWidth: 600, borderColor: t.palette.contrast_300},
...(Array.isArray(style) ? style : [style || {}]),
]}>
{children}
+18 -3
View File
@@ -1,5 +1,8 @@
import React from 'react'
import type {ViewStyle} from 'react-native'
import type {ViewStyle, AccessibilityProps} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet'
type A11yProps = Required<AccessibilityProps>
export type DialogContextProps = {
close: () => void
@@ -11,8 +14,20 @@ export type DialogControlProps = {
}
export type DialogOuterProps = {
control: React.RefObject<DialogControlProps>
control: {
ref: React.RefObject<DialogControlProps>
open: (index?: number) => void
close: () => void
}
onClose?: () => void
nativeOptions?: {
sheet?: Omit<BottomSheetProps, 'children'>
}
webOptions?: {}
}
export type DialogInnerProps = React.PropsWithChildren<{style?: ViewStyle}>
export type DialogInnerProps = React.PropsWithChildren<{
style?: ViewStyle
accessibilityLabelledBy: A11yProps['aria-labelledby']
accessibilityDescribedBy: string
}>
+134
View File
@@ -0,0 +1,134 @@
import React from 'react'
import {View, PressableProps, LayoutChangeEvent} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useTheme, atoms as a} from '#/alf'
import {H2, P} from '#/view/com/Typography'
import {Button} from '#/view/com/Button'
import * as Dialog from '#/view/com/Dialog'
export {useDialogControl as usePromptControl} from '#/view/com/Dialog'
const Context = React.createContext<{
titleId: string
descriptionId: string
}>({
titleId: '',
descriptionId: '',
})
export function Outer({
children,
control,
}: React.PropsWithChildren<{
control: Dialog.DialogOuterProps['control']
}>) {
const insets = useSafeAreaInsets()
const titleId = React.useId()
const descriptionId = React.useId()
const [defaultSnapPoints, setDefaultSnapPoints] = React.useState<
string | number
>('25%')
const context = React.useMemo(
() => ({titleId, descriptionId}),
[titleId, descriptionId],
)
const measureDefaultSnapPoint = React.useCallback(
(e: LayoutChangeEvent) => {
setDefaultSnapPoints(e.nativeEvent.layout.height + insets.bottom + 50)
},
[insets, setDefaultSnapPoints],
)
return (
<Dialog.Outer
control={control}
nativeOptions={{
sheet: {
snapPoints: [defaultSnapPoints],
},
}}>
<Context.Provider value={context}>
<View onLayout={measureDefaultSnapPoint}>
<Dialog.Inner
accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId}
style={{width: 'auto', maxWidth: 400}}>
<Dialog.Handle />
{children}
</Dialog.Inner>
</View>
</Context.Provider>
</Dialog.Outer>
)
}
export function Title({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {titleId} = React.useContext(Context)
return (
<H2
nativeID={titleId}
style={[a.font_semibold, t.atoms.text_contrast_700, a.pb_sm]}>
{children}
</H2>
)
}
export function Description({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {descriptionId} = React.useContext(Context)
return (
<P nativeID={descriptionId} style={[t.atoms.text_contrast_700, a.pb_lg]}>
{children}
</P>
)
}
export function Actions({children}: React.PropsWithChildren<{}>) {
return (
<View style={[a.w_full, a.flex_row, a.gap_sm, a.justify_end]}>
{children}
</View>
)
}
export function Cancel({
children,
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
const {close} = Dialog.useDialogContext()
return (
<Button
type="secondary"
size="small"
accessibilityLabel="Cancel"
accessibilityHint="Cancel this action"
onPress={close}>
{children}
</Button>
)
}
export function Action({
children,
onPress,
}: React.PropsWithChildren<{onPress?: () => void}>) {
const {close} = Dialog.useDialogContext()
const handleOnPress = React.useCallback(() => {
close()
onPress?.()
}, [close, onPress])
return (
<Button
type="primary"
size="small"
accessibilityLabel="Confirm"
accessibilityHint="Confirm this action"
onPress={handleOnPress}>
{children}
</Button>
)
}
+27 -7
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {Text as RNText, TextProps} from 'react-native'
import {useTheme, atoms, web} from '#/alf'
import {useTheme, atoms, web, flatten} from '#/alf'
export function Text({style, ...rest}: TextProps) {
const t = useTheme()
@@ -18,7 +19,7 @@ export function H1({style, ...rest}: TextProps) {
<RNText
{...attr}
{...rest}
style={[atoms.text_xl, atoms.font_bold, t.atoms.text, style]}
style={[atoms.text_xl, atoms.font_bold, t.atoms.text, flatten(style)]}
/>
)
}
@@ -34,7 +35,7 @@ export function H2({style, ...rest}: TextProps) {
<RNText
{...attr}
{...rest}
style={[atoms.text_lg, atoms.font_bold, t.atoms.text, style]}
style={[atoms.text_lg, atoms.font_bold, t.atoms.text, flatten(style)]}
/>
)
}
@@ -50,7 +51,7 @@ export function H3({style, ...rest}: TextProps) {
<RNText
{...attr}
{...rest}
style={[atoms.text_md, atoms.font_bold, t.atoms.text, style]}
style={[atoms.text_md, atoms.font_bold, t.atoms.text, flatten(style)]}
/>
)
}
@@ -66,7 +67,7 @@ export function H4({style, ...rest}: TextProps) {
<RNText
{...attr}
{...rest}
style={[atoms.text_sm, atoms.font_bold, t.atoms.text, style]}
style={[atoms.text_sm, atoms.font_bold, t.atoms.text, flatten(style)]}
/>
)
}
@@ -82,7 +83,7 @@ export function H5({style, ...rest}: TextProps) {
<RNText
{...attr}
{...rest}
style={[atoms.text_xs, atoms.font_bold, t.atoms.text, style]}
style={[atoms.text_xs, atoms.font_bold, t.atoms.text, flatten(style)]}
/>
)
}
@@ -98,7 +99,26 @@ export function H6({style, ...rest}: TextProps) {
<RNText
{...attr}
{...rest}
style={[atoms.text_xxs, atoms.font_bold, t.atoms.text, style]}
style={[atoms.text_xxs, atoms.font_bold, t.atoms.text, flatten(style)]}
/>
)
}
export function P({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'paragraph',
}) || {}
const _style = flatten(style)
const lineHeight =
(_style?.lineHeight || atoms.text_md.lineHeight) *
atoms.leading_normal.lineHeight
return (
<RNText
{...attr}
{...rest}
style={[atoms.text_md, t.atoms.text, _style, {lineHeight}]}
/>
)
}
+47 -21
View File
@@ -8,32 +8,39 @@ import * as tokens from '#/alf/tokens'
import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf'
import {Button, ButtonText} from '#/view/com/Button'
import {Link} from '#/view/com/Link'
import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography'
import {Text, H1, H2, H3, H4, H5, H6, P} from '#/view/com/Typography'
import {InputText} from '#/view/com/forms/InputText'
import {InputDate, utils} from '#/view/com/forms/InputDate'
import {Logo} from '#/view/icons/Logo'
import * as Dialog from '#/view/com/Dialog'
import * as Prompt from '#/view/com/Prompt'
function ThemeSelector() {
const setColorMode = useSetColorMode()
return (
<View style={[a.flex_row, a.gap_md]}>
<View style={[a.flex_row, a.align_start, a.gap_md]}>
<Button
type="secondary"
size="small"
accessibilityLabel='Set theme to "system"'
accessibilityHint="Set theme to system default"
onPress={() => setColorMode('system')}>
System
</Button>
<Button
type="secondary"
size="small"
accessibilityLabel='Set theme to "system"'
accessibilityHint="Set theme to system default"
onPress={() => setColorMode('light')}>
Light
</Button>
<Button
type="secondary"
size="small"
accessibilityLabel='Set theme to "system"'
accessibilityHint="Set theme to system default"
onPress={() => setColorMode('dark')}>
Dark
</Button>
@@ -319,39 +326,58 @@ function Forms() {
function Dialogs() {
const control = Dialog.useDialogControl()
const prompt = Prompt.usePromptControl()
return (
<>
<Button
type="secondary"
size="small"
onPress={() => control.current.open()}
onPress={() => control.open()}
accessibilityLabel="Open basic dialog"
accessibilityHint="Open basic dialog">
Open basic dialog
</Button>
<Button
type="negative"
size="small"
onPress={() => prompt.open()}
accessibilityLabel="Open prompt"
accessibilityHint="Open prompt">
Open prompt
</Button>
<Prompt.Outer control={prompt}>
<Prompt.Title>Are you sure?</Prompt.Title>
<Prompt.Description>
This action cannot be undone. This action cannot be undone. This
action cannot be undone.
</Prompt.Description>
<Prompt.Actions>
<Prompt.Cancel>Cancel</Prompt.Cancel>
<Prompt.Action>Confirm</Prompt.Action>
</Prompt.Actions>
</Prompt.Outer>
<Dialog.Outer control={control}>
<Dialog.Inner style={{width: 'auto'}}>
<Dialog.Inner
accessibilityLabelledBy="dialog-title"
accessibilityDescribedBy="dialog-description">
<Dialog.Handle />
<View style={[a.gap_md]}>
<Button
type="primary"
size="small"
onPress={() => control.current.close()}
accessibilityLabel="Open basic dialog"
accessibilityHint="Open basic dialog">
Close basic dialog
</Button>
<View style={{height: 100}} />
<Button
type="secondary"
size="small"
onPress={() => control.current.close()}
accessibilityLabel="Open basic dialog"
accessibilityHint="Open basic dialog">
Some other control
</Button>
<H3 nativeID="dialog-title">Dialog</H3>
<P nativeID="dialog-description">Description</P>
<View style={[a.flex_row, a.justify_end]}>
<Button
type="primary"
size="small"
onPress={() => control.close()}
accessibilityLabel="Open basic dialog"
accessibilityHint="Open basic dialog">
Close basic dialog
</Button>
</View>
</View>
</Dialog.Inner>
</Dialog.Outer>