diff options
Diffstat (limited to 'src/components/forms')
-rw-r--r-- | src/components/forms/DateField/index.android.tsx | 83 | ||||
-rw-r--r-- | src/components/forms/DateField/index.shared.tsx | 99 | ||||
-rw-r--r-- | src/components/forms/DateField/index.tsx | 66 | ||||
-rw-r--r-- | src/components/forms/DateField/index.web.tsx | 10 | ||||
-rw-r--r-- | src/components/forms/DateField/types.ts | 1 | ||||
-rw-r--r-- | src/components/forms/FormError.tsx | 30 | ||||
-rw-r--r-- | src/components/forms/HostingProvider.tsx | 95 | ||||
-rw-r--r-- | src/components/forms/TextField.tsx | 14 |
8 files changed, 316 insertions, 82 deletions
diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index 451810a5e..700d15e6d 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -1,19 +1,11 @@ import React from 'react' -import {View, Pressable} from 'react-native' - -import {useTheme, atoms} from '#/alf' -import {Text} from '#/components/Typography' -import {useInteractionState} from '#/components/hooks/useInteractionState' -import * as TextField from '#/components/forms/TextField' -import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' +import DatePicker from 'react-native-date-picker' +import {useTheme} from '#/alf' import {DateFieldProps} from '#/components/forms/DateField/types' -import { - localizeDate, - toSimpleDateString, -} from '#/components/forms/DateField/utils' -import DatePicker from 'react-native-date-picker' -import {isAndroid} from 'platform/detection' +import {toSimpleDateString} from '#/components/forms/DateField/utils' +import * as TextField from '#/components/forms/TextField' +import {DateFieldButton} from './index.shared' export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label @@ -24,18 +16,10 @@ export function DateField({ label, isInvalid, testID, + accessibilityHint, }: DateFieldProps) { const t = useTheme() const [open, setOpen] = React.useState(false) - const { - state: pressed, - onIn: onPressIn, - onOut: onPressOut, - } = useInteractionState() - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - - const {chromeFocus, chromeError, chromeErrorHover} = - TextField.useSharedInputStyles() const onChangeInternal = React.useCallback( (date: Date) => { @@ -47,50 +31,29 @@ export function DateField({ [onChangeDate, setOpen], ) + const onPress = React.useCallback(() => { + setOpen(true) + }, []) + const onCancel = React.useCallback(() => { setOpen(false) }, []) return ( - <View style={[atoms.relative, atoms.w_full]}> - <Pressable - aria-label={label} - accessibilityLabel={label} - accessibilityHint={undefined} - onPress={() => setOpen(true)} - onPressIn={onPressIn} - onPressOut={onPressOut} - onFocus={onFocus} - onBlur={onBlur} - style={[ - { - paddingTop: 16, - paddingBottom: 16, - borderColor: 'transparent', - borderWidth: 2, - }, - atoms.flex_row, - atoms.flex_1, - atoms.w_full, - atoms.px_lg, - atoms.rounded_sm, - t.atoms.bg_contrast_50, - focused || pressed ? chromeFocus : {}, - isInvalid ? chromeError : {}, - isInvalid && (focused || pressed) ? chromeErrorHover : {}, - ]}> - <TextField.Icon icon={CalendarDays} /> - - <Text - style={[atoms.text_md, atoms.pl_xs, t.atoms.text, {paddingTop: 3}]}> - {localizeDate(value)} - </Text> - </Pressable> + <> + <DateFieldButton + label={label} + value={value} + onPress={onPress} + isInvalid={isInvalid} + accessibilityHint={accessibilityHint} + /> {open && ( <DatePicker - modal={isAndroid} - open={isAndroid} + modal + open + timeZoneOffsetInMinutes={0} theme={t.name === 'light' ? 'light' : 'dark'} date={new Date(value)} onConfirm={onChangeInternal} @@ -99,9 +62,9 @@ export function DateField({ testID={`${testID}-datepicker`} aria-label={label} accessibilityLabel={label} - accessibilityHint={undefined} + accessibilityHint={accessibilityHint} /> )} - </View> + </> ) } diff --git a/src/components/forms/DateField/index.shared.tsx b/src/components/forms/DateField/index.shared.tsx new file mode 100644 index 000000000..1f54bdc8b --- /dev/null +++ b/src/components/forms/DateField/index.shared.tsx @@ -0,0 +1,99 @@ +import React from 'react' +import {Pressable, View} from 'react-native' + +import {android, atoms as a, useTheme, web} from '#/alf' +import * as TextField from '#/components/forms/TextField' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' +import {Text} from '#/components/Typography' +import {localizeDate} from './utils' + +// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press +// iOS: open a dialog with an inline date picker +// Android: open the date picker modal + +export function DateFieldButton({ + label, + value, + onPress, + isInvalid, + accessibilityHint, +}: { + label: string + value: string + onPress: () => void + isInvalid?: boolean + accessibilityHint?: string +}) { + const t = useTheme() + + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = + TextField.useSharedInputStyles() + + return ( + <View + style={[a.relative, a.w_full]} + {...web({ + onMouseOver: onHoverIn, + onMouseOut: onHoverOut, + })}> + <Pressable + aria-label={label} + accessibilityLabel={label} + accessibilityHint={accessibilityHint} + onPress={onPress} + onPressIn={onPressIn} + onPressOut={onPressOut} + onFocus={onFocus} + onBlur={onBlur} + style={[ + { + paddingTop: 12, + paddingBottom: 12, + paddingLeft: 14, + paddingRight: 14, + borderColor: 'transparent', + borderWidth: 2, + }, + android({ + minHeight: 57.5, + }), + a.flex_row, + a.flex_1, + a.w_full, + a.rounded_sm, + t.atoms.bg_contrast_25, + a.align_center, + hovered ? chromeHover : {}, + focused || pressed ? chromeFocus : {}, + isInvalid || isInvalid ? chromeError : {}, + (isInvalid || isInvalid) && (hovered || focused) + ? chromeErrorHover + : {}, + ]}> + <TextField.Icon icon={CalendarDays} /> + <Text + style={[ + a.text_md, + a.pl_xs, + t.atoms.text, + {lineHeight: a.text_md.fontSize * 1.1875}, + ]}> + {localizeDate(value)} + </Text> + </Pressable> + </View> + ) +} diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx index 49e47a01e..5662bb594 100644 --- a/src/components/forms/DateField/index.tsx +++ b/src/components/forms/DateField/index.tsx @@ -1,11 +1,16 @@ import React from 'react' import {View} from 'react-native' +import DatePicker from 'react-native-date-picker' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' -import {useTheme, atoms} from '#/alf' -import * as TextField from '#/components/forms/TextField' -import {toSimpleDateString} from '#/components/forms/DateField/utils' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' import {DateFieldProps} from '#/components/forms/DateField/types' -import DatePicker from 'react-native-date-picker' +import {toSimpleDateString} from '#/components/forms/DateField/utils' +import * as TextField from '#/components/forms/TextField' +import {DateFieldButton} from './index.shared' export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label @@ -22,8 +27,12 @@ export function DateField({ onChangeDate, testID, label, + isInvalid, + accessibilityHint, }: DateFieldProps) { + const {_} = useLingui() const t = useTheme() + const control = Dialog.useDialogControl() const onChangeInternal = React.useCallback( (date: Date | undefined) => { @@ -36,17 +45,44 @@ export function DateField({ ) return ( - <View style={[atoms.relative, atoms.w_full]}> - <DatePicker - theme={t.name === 'light' ? 'light' : 'dark'} - date={new Date(value)} - onDateChange={onChangeInternal} - mode="date" - testID={`${testID}-datepicker`} - aria-label={label} - accessibilityLabel={label} - accessibilityHint={undefined} + <> + <DateFieldButton + label={label} + value={value} + onPress={control.open} + isInvalid={isInvalid} + accessibilityHint={accessibilityHint} /> - </View> + <Dialog.Outer control={control} testID={testID}> + <Dialog.Handle /> + <Dialog.Inner label={label}> + <View style={a.gap_lg}> + <View style={[a.relative, a.w_full, a.align_center]}> + <DatePicker + timeZoneOffsetInMinutes={0} + theme={t.name === 'light' ? 'light' : 'dark'} + date={new Date(value)} + onDateChange={onChangeInternal} + mode="date" + testID={`${testID}-datepicker`} + aria-label={label} + accessibilityLabel={label} + accessibilityHint={accessibilityHint} + /> + </View> + <Button + label={_(msg`Done`)} + onPress={() => control.close()} + size="medium" + color="primary" + variant="solid"> + <ButtonText> + <Trans>Done</Trans> + </ButtonText> + </Button> + </View> + </Dialog.Inner> + </Dialog.Outer> + </> ) } diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx index 32f38a5d1..982d32711 100644 --- a/src/components/forms/DateField/index.web.tsx +++ b/src/components/forms/DateField/index.web.tsx @@ -1,11 +1,12 @@ import React from 'react' -import {TextInput, TextInputProps, StyleSheet} from 'react-native' +import {StyleSheet, TextInput, TextInputProps} from 'react-native' // @ts-ignore import {unstable_createElement} from 'react-native-web' -import * as TextField from '#/components/forms/TextField' -import {toSimpleDateString} from '#/components/forms/DateField/utils' import {DateFieldProps} from '#/components/forms/DateField/types' +import {toSimpleDateString} from '#/components/forms/DateField/utils' +import * as TextField from '#/components/forms/TextField' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label @@ -37,6 +38,7 @@ export function DateField({ label, isInvalid, testID, + accessibilityHint, }: DateFieldProps) { const handleOnChange = React.useCallback( (e: any) => { @@ -52,12 +54,14 @@ export function DateField({ return ( <TextField.Root isInvalid={isInvalid}> + <TextField.Icon icon={CalendarDays} /> <Input value={value} label={label} onChange={handleOnChange} onChangeText={() => {}} testID={testID} + accessibilityHint={accessibilityHint} /> </TextField.Root> ) diff --git a/src/components/forms/DateField/types.ts b/src/components/forms/DateField/types.ts index 129f5672d..5400cf903 100644 --- a/src/components/forms/DateField/types.ts +++ b/src/components/forms/DateField/types.ts @@ -4,4 +4,5 @@ export type DateFieldProps = { label: string isInvalid?: boolean testID?: string + accessibilityHint?: string } diff --git a/src/components/forms/FormError.tsx b/src/components/forms/FormError.tsx new file mode 100644 index 000000000..9e72df879 --- /dev/null +++ b/src/components/forms/FormError.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' +import {Text} from '#/components/Typography' + +export function FormError({error}: {error?: string}) { + const t = useTheme() + + if (!error) return null + + return ( + <View + style={[ + {backgroundColor: t.palette.negative_400}, + a.flex_row, + a.rounded_sm, + a.p_md, + a.gap_sm, + ]}> + <Warning fill={t.palette.white} size="md" /> + <View> + <Text style={[{color: t.palette.white}, a.font_bold, a.leading_snug]}> + {error} + </Text> + </View> + </View> + ) +} diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx new file mode 100644 index 000000000..f2d11062a --- /dev/null +++ b/src/components/forms/HostingProvider.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import {Keyboard, View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {toNiceDomain} from '#/lib/strings/url-helpers' +import {isAndroid} from '#/platform/detection' +import {ServerInputDialog} from '#/view/com/auth/server-input' +import {atoms as a, useTheme} from '#/alf' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' +import {PencilLine_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' +import {Button} from '../Button' +import {useDialogControl} from '../Dialog' +import {Text} from '../Typography' + +export function HostingProvider({ + serviceUrl, + onSelectServiceUrl, + onOpenDialog, +}: { + serviceUrl: string + onSelectServiceUrl: (provider: string) => void + onOpenDialog?: () => void +}) { + const serverInputControl = useDialogControl() + const t = useTheme() + const {_} = useLingui() + + const onPressSelectService = React.useCallback(() => { + Keyboard.dismiss() + serverInputControl.open() + if (onOpenDialog) { + onOpenDialog() + } + }, [onOpenDialog, serverInputControl]) + + return ( + <> + <ServerInputDialog + control={serverInputControl} + onSelect={onSelectServiceUrl} + /> + <Button + label={toNiceDomain(serviceUrl)} + accessibilityHint={_(msg`Press to change hosting provider`)} + variant="solid" + color="secondary" + style={[ + a.w_full, + a.flex_row, + a.align_center, + a.rounded_sm, + a.px_md, + a.pr_sm, + a.gap_xs, + {paddingVertical: isAndroid ? 14 : 9}, + ]} + onPress={onPressSelectService}> + {({hovered, pressed}) => { + const interacted = hovered || pressed + return ( + <> + <View style={a.pr_xs}> + <Globe + size="md" + fill={ + interacted ? t.palette.contrast_800 : t.palette.contrast_500 + } + /> + </View> + <Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text> + <View + style={[ + a.rounded_sm, + interacted + ? t.atoms.bg_contrast_300 + : t.atoms.bg_contrast_100, + {marginLeft: 'auto', padding: 6}, + ]}> + <Pencil + size="sm" + style={{ + color: interacted + ? t.palette.contrast_800 + : t.palette.contrast_500, + }} + /> + </View> + </> + ) + }} + </Button> + </> + ) +} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index b37f4bfae..376883c9d 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -14,6 +14,7 @@ import {useTheme, atoms as a, web, android} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' import {Props as SVGIconProps} from '#/components/icons/common' +import {mergeRefs} from '#/lib/merge-refs' const Context = React.createContext<{ inputRef: React.RefObject<TextInput> | null @@ -125,9 +126,10 @@ export function useSharedInputStyles() { export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & { label: string - value: string - onChangeText: (value: string) => void + value?: string + onChangeText?: (value: string) => void isInvalid?: boolean + inputRef?: React.RefObject<TextInput> } export function createInput(Component: typeof TextInput) { @@ -137,6 +139,7 @@ export function createInput(Component: typeof TextInput) { value, onChangeText, isInvalid, + inputRef, ...rest }: InputProps) { const t = useTheme() @@ -161,19 +164,22 @@ export function createInput(Component: typeof TextInput) { ) } + const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) + return ( <> <Component accessibilityHint={undefined} {...rest} accessibilityLabel={label} - ref={ctx.inputRef} + ref={refs} value={value} onChangeText={onChangeText} onFocus={ctx.onFocus} onBlur={ctx.onBlur} placeholder={placeholder || label} placeholderTextColor={t.palette.contrast_500} + keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} hitSlop={HITSLOP_20} style={[ a.relative, @@ -271,7 +277,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) { <Comp size="md" style={[ - {color: t.palette.contrast_500, pointerEvents: 'none'}, + {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0}, ctx.hovered ? hover : {}, ctx.focused ? focus : {}, ctx.isInvalid && ctx.hovered ? errorHover : {}, |