Add Prompt, Dialog refinement, mobile refinement
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import {StyleSheet} from 'react-native'
|
||||
|
||||
export const flatten = StyleSheet.flatten
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -15,5 +15,9 @@ export function useDialogControl() {
|
||||
close: () => {},
|
||||
})
|
||||
|
||||
return control
|
||||
return {
|
||||
ref: control,
|
||||
open: () => control.current.open(),
|
||||
close: () => control.current.close(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user