From ead02d766c4db751612bf78d35347f533072070e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 13:14:18 -0600 Subject: [PATCH] Refactor DateField, web done --- .../forms/DateField/index.android.tsx | 161 ++++++++++++++++++ src/components/forms/DateField/index.tsx | 63 +++++++ src/components/forms/DateField/index.web.tsx | 71 ++++++++ src/components/forms/DateField/types.ts | 10 ++ src/components/forms/DateField/utils.ts | 16 ++ src/components/forms/TextField.tsx | 16 +- src/view/screens/Storybook/Forms.tsx | 18 +- 7 files changed, 342 insertions(+), 13 deletions(-) create mode 100644 src/components/forms/DateField/index.android.tsx create mode 100644 src/components/forms/DateField/index.tsx create mode 100644 src/components/forms/DateField/index.web.tsx create mode 100644 src/components/forms/DateField/types.ts create mode 100644 src/components/forms/DateField/utils.ts diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx new file mode 100644 index 0000000000..3547557841 --- /dev/null +++ b/src/components/forms/DateField/index.android.tsx @@ -0,0 +1,161 @@ +import React from 'react' +import {View, TextStyle, Pressable} from 'react-native' +import DateTimePicker, { + BaseProps as DateTimePickerProps, +} from '@react-native-community/datetimepicker' + +import {Logo} from '#/view/icons/Logo' +import {useTheme, atoms, tokens} from '#/alf' +import {Text} from '#/components/Typography' +import {useInteractionState} from '#/components/hooks/useInteractionState' + +import {InputDateProps} from '#/components/forms/InputDate/types' +import { + localizeDate, + toSimpleDateString, +} from '#/components/forms/InputDate/utils' + +export * as utils from '#/components/forms/InputDate/utils' + +export function InputDate({ + value: initialValue, + onChange, + testID, + label, + hasError, + accessibilityLabel, + accessibilityHint, + ...props +}: InputDateProps) { + const labelId = React.useId() + const t = useTheme() + const [open, setOpen] = React.useState(false) + const [value, setValue] = React.useState(initialValue) + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const {inputStyles, iconStyles} = React.useMemo(() => { + const input: TextStyle[] = [ + { + paddingLeft: 40, + }, + ] + const icon: TextStyle[] = [] + + if (hasError) { + input.push({ + borderColor: tokens.color.red_200, + }) + icon.push({ + color: tokens.color.red_400, + }) + } + + if (focused) { + input.push({ + borderColor: t.atoms.border_contrast.borderColor, + }) + + if (hasError) { + input.push({ + borderColor: tokens.color.red_500, + }) + } + } + + return {inputStyles: input, iconStyles: icon} + }, [t, focused, hasError]) + + const onChangeInternal = React.useCallback< + Required['onChange'] + >( + (_event, date) => { + setOpen(false) + + if (date) { + const formatted = toSimpleDateString(date) + onChange(formatted) + setValue(formatted) + } + }, + [onChange, setOpen, setValue], + ) + + return ( + + {label && ( + + {label} + + )} + + setOpen(true)} + onFocus={onFocus} + onBlur={onBlur} + style={[ + { + paddingTop: atoms.pt_md.paddingTop + 2, + }, + atoms.w_full, + atoms.px_lg, + atoms.pb_md, + atoms.rounded_sm, + t.atoms.bg_contrast_100, + ...inputStyles, + ]}> + {localizeDate(value)} + + + + + + + {open && ( + + )} + + ) +} diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx new file mode 100644 index 0000000000..dcf509b7d8 --- /dev/null +++ b/src/components/forms/DateField/index.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import {View} from 'react-native' +import DateTimePicker, { + DateTimePickerEvent, +} from '@react-native-community/datetimepicker' + +import {useTheme, atoms} from '#/alf' +import {toSimpleDateString} from '#/components/forms/InputDate/utils' +import {InputDateProps} from '#/components/forms/InputDate/types' +import TextField from '#/components/forms/TextField' + +export * as utils from '#/components/forms/InputDate/utils' +export const Label = TextField.Label + +/** + * Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date + * changes in the same format. + * + * For dates of unknown format, convert with the + * `utils.toSimpleDateString(Date)` export of this file. + */ +export function DateField({ + value: initialValue, + onChange, + testID, + label, + accessibilityLabel, + accessibilityHint, +}: InputDateProps) { + const labelId = React.useId() + const t = useTheme() + const [value, setValue] = React.useState(initialValue) + + const onChangeInternal = React.useCallback( + (event: DateTimePickerEvent, date: Date | undefined) => { + if (date) { + const formatted = toSimpleDateString(date) + onChange(formatted) + setValue(formatted) + } + }, + [onChange], + ) + + return ( + + + + ) +} diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx new file mode 100644 index 0000000000..f8809cb7ca --- /dev/null +++ b/src/components/forms/DateField/index.web.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import {TextInput, TextInputProps, StyleSheet} from 'react-native' +// @ts-ignore +import {unstable_createElement} from 'react-native-web' + +import TextField, {createInput} from '#/components/forms/TextField' + +import {toSimpleDateString} from '#/components/forms/InputDate/utils' + +export * as utils from '#/components/forms/InputDate/utils' +export const Label = TextField.Label + +const InputBase = React.forwardRef( + ({style, ...props}, ref) => { + return unstable_createElement('input', { + ...props, + ref, + type: 'date', + style: [ + StyleSheet.flatten(style), + { + background: 'transparent', + border: 0, + }, + ], + }) + }, +) + +InputBase.displayName = 'InputBase' + +const Input = createInput(InputBase as unknown as typeof TextInput) + +export function DateField({ + value, + onChange, + label, + isInvalid, + testID, +}: { + value: string + onChange: (value: string) => void + label: string + isInvalid?: boolean + testID?: string +}) { + const handleOnChange = React.useCallback( + (e: any) => { + const date = e.target.valueAsDate || e.target.value + + if (date) { + const formatted = toSimpleDateString(date) + onChange(formatted) + } + }, + [onChange], + ) + + return ( + + {}} + isInvalid={isInvalid} + testID={testID} + /> + + ) +} diff --git a/src/components/forms/DateField/types.ts b/src/components/forms/DateField/types.ts new file mode 100644 index 0000000000..2ef0a4b3ba --- /dev/null +++ b/src/components/forms/DateField/types.ts @@ -0,0 +1,10 @@ +import {TextInputProps} from 'react-native' + +import {BaseProps} from '#/components/forms/types' + +export type InputDateProps = BaseProps & { + /** + * **NOTE:** Available only on web + */ + autoFocus?: TextInputProps['autoFocus'] +} diff --git a/src/components/forms/DateField/utils.ts b/src/components/forms/DateField/utils.ts new file mode 100644 index 0000000000..c787272fe8 --- /dev/null +++ b/src/components/forms/DateField/utils.ts @@ -0,0 +1,16 @@ +import {getLocales} from 'expo-localization' + +const LOCALE = getLocales()[0] + +// we need the date in the form yyyy-MM-dd to pass to the input +export function toSimpleDateString(date: Date | string): string { + const _date = typeof date === 'string' ? new Date(date) : date + return _date.toISOString().split('T')[0] +} + +export function localizeDate(date: Date | string): string { + const _date = typeof date === 'string' ? new Date(date) : date + return new Intl.DateTimeFormat(LOCALE.languageTag, { + timeZone: 'UTC', + }).format(_date) +} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index bcf7dafe0d..cc3aae2e59 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -94,7 +94,7 @@ function Root({children, isInvalid = false}: RootProps) { paddingVertical: 14, }, ]} - onPressIn={() => inputRef.current?.focus()} + onPress={() => inputRef.current?.focus()} onHoverIn={onHoverIn} onHoverOut={onHoverOut}> {children} @@ -103,7 +103,7 @@ function Root({children, isInvalid = false}: RootProps) { ) } -function useSharedInputStyles() { +export function useSharedInputStyles() { const t = useTheme() return React.useMemo(() => { const hover: ViewStyle[] = [ @@ -228,6 +228,15 @@ export function createInput(Component: typeof TextInput) { const Input = createInput(TextInput) +function Label({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + return ( + + {children} + + ) +} + function Icon({icon: Comp}: {icon: React.ComponentType}) { const t = useTheme() const ctx = React.useContext(Context) @@ -266,7 +275,7 @@ function Icon({icon: Comp}: {icon: React.ComponentType}) { @@ -51,16 +52,13 @@ export function Forms() {

InputDate

- console.log(date)} - label="Input" - /> - console.log(date)} + value={date} + onChange={date => { + console.log(date) + setDate(date) + }} label="Input" />