diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index 3547557841..b1116411cc 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -1,69 +1,41 @@ import React from 'react' -import {View, TextStyle, Pressable} from 'react-native' +import {View, 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 {useTheme, atoms} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' +import TextField, {useSharedInputStyles} from '#/components/forms/TextField' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' -import {InputDateProps} from '#/components/forms/InputDate/types' +import {DateFieldProps} from '#/components/forms/DateField/types' import { localizeDate, toSimpleDateString, -} from '#/components/forms/InputDate/utils' +} from '#/components/forms/DateField/utils' -export * as utils from '#/components/forms/InputDate/utils' +export * as utils from '#/components/forms/DateField/utils' +export const Label = TextField.Label -export function InputDate({ - value: initialValue, - onChange, - testID, +export function DateField({ + value, + onChangeDate, label, - hasError, - accessibilityLabel, - accessibilityHint, - ...props -}: InputDateProps) { - const labelId = React.useId() + isInvalid, + testID, +}: DateFieldProps) { const t = useTheme() const [open, setOpen] = React.useState(false) - const [value, setValue] = React.useState(initialValue) + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() 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 {chromeFocus, chromeError, chromeErrorHover} = useSharedInputStyles() const onChangeInternal = React.useCallback< Required['onChange'] @@ -73,75 +45,53 @@ export function InputDate({ if (date) { const formatted = toSimpleDateString(date) - onChange(formatted) - setValue(formatted) + onChangeDate(formatted) } }, - [onChange, setOpen, setValue], + [onChangeDate, setOpen], ) return ( - {label && ( - - {label} - - )} - setOpen(true)} + onPressIn={onPressIn} + onPressOut={onPressOut} onFocus={onFocus} onBlur={onBlur} style={[ { - paddingTop: atoms.pt_md.paddingTop + 2, + paddingTop: 16, + paddingBottom: 16, + borderColor: 'transparent', + borderWidth: 2, }, + atoms.flex_row, + atoms.flex_1, atoms.w_full, atoms.px_lg, - atoms.pb_md, atoms.rounded_sm, - t.atoms.bg_contrast_100, - ...inputStyles, + t.atoms.bg_contrast_50, + focused || pressed ? chromeFocus : {}, + isInvalid ? chromeError : {}, + isInvalid && (focused || pressed) ? chromeErrorHover : {}, ]}> - {localizeDate(value)} - + - - - + + {localizeDate(value)} + + {open && ( )} diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx index dcf509b7d8..8a8db876b2 100644 --- a/src/components/forms/DateField/index.tsx +++ b/src/components/forms/DateField/index.tsx @@ -5,11 +5,11 @@ import DateTimePicker, { } 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' +import {toSimpleDateString} from '#/components/forms/DateField/utils' +import {DateFieldProps} from '#/components/forms/DateField/types' -export * as utils from '#/components/forms/InputDate/utils' +export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label /** @@ -20,43 +20,36 @@ export const Label = TextField.Label * `utils.toSimpleDateString(Date)` export of this file. */ export function DateField({ - value: initialValue, - onChange, + value, + onChangeDate, testID, label, - accessibilityLabel, - accessibilityHint, -}: InputDateProps) { - const labelId = React.useId() +}: DateFieldProps) { 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) + onChangeDate(formatted) } }, - [onChange], + [onChangeDate], ) return ( ) diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx index f8809cb7ca..4dd1980393 100644 --- a/src/components/forms/DateField/index.web.tsx +++ b/src/components/forms/DateField/index.web.tsx @@ -4,10 +4,10 @@ import {TextInput, TextInputProps, StyleSheet} from 'react-native' import {unstable_createElement} from 'react-native-web' import TextField, {createInput} from '#/components/forms/TextField' +import {toSimpleDateString} from '#/components/forms/DateField/utils' +import {DateFieldProps} from '#/components/forms/DateField/types' -import {toSimpleDateString} from '#/components/forms/InputDate/utils' - -export * as utils from '#/components/forms/InputDate/utils' +export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label const InputBase = React.forwardRef( @@ -33,37 +33,30 @@ const Input = createInput(InputBase as unknown as typeof TextInput) export function DateField({ value, - onChange, + onChangeDate, label, isInvalid, testID, -}: { - value: string - onChange: (value: string) => void - label: string - isInvalid?: boolean - testID?: string -}) { +}: DateFieldProps) { const handleOnChange = React.useCallback( (e: any) => { const date = e.target.valueAsDate || e.target.value if (date) { const formatted = toSimpleDateString(date) - onChange(formatted) + onChangeDate(formatted) } }, - [onChange], + [onChangeDate], ) return ( - + {}} - isInvalid={isInvalid} testID={testID} /> diff --git a/src/components/forms/DateField/types.ts b/src/components/forms/DateField/types.ts index 2ef0a4b3ba..129f5672d4 100644 --- a/src/components/forms/DateField/types.ts +++ b/src/components/forms/DateField/types.ts @@ -1,10 +1,7 @@ -import {TextInputProps} from 'react-native' - -import {BaseProps} from '#/components/forms/types' - -export type InputDateProps = BaseProps & { - /** - * **NOTE:** Available only on web - */ - autoFocus?: TextInputProps['autoFocus'] +export type DateFieldProps = { + value: string + onChangeDate: (date: string) => void + label: string + isInvalid?: boolean + testID?: string } diff --git a/src/components/forms/InputDate/index.android.tsx b/src/components/forms/InputDate/index.android.tsx deleted file mode 100644 index 3547557841..0000000000 --- a/src/components/forms/InputDate/index.android.tsx +++ /dev/null @@ -1,161 +0,0 @@ -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/InputDate/index.tsx b/src/components/forms/InputDate/index.tsx deleted file mode 100644 index 727dcd6736..0000000000 --- a/src/components/forms/InputDate/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import DateTimePicker, { - DateTimePickerEvent, -} from '@react-native-community/datetimepicker' - -import {useTheme, atoms} from '#/alf' -import {Text} from '#/components/Typography' -import {toSimpleDateString} from '#/components/forms/InputDate/utils' - -import {InputDateProps} from '#/components/forms/InputDate/types' -export * as utils from '#/components/forms/InputDate/utils' - -/** - * 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 InputDate({ - 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 ( - - {label && ( - - {label} - - )} - - - - ) -} diff --git a/src/components/forms/InputDate/index.web.tsx b/src/components/forms/InputDate/index.web.tsx deleted file mode 100644 index 95a4d9e1b4..0000000000 --- a/src/components/forms/InputDate/index.web.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React from 'react' -import {View, TextStyle} from 'react-native' -// @ts-ignore -import {unstable_createElement} from 'react-native-web' - -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 {toSimpleDateString} from '#/components/forms/InputDate/utils' - -export * as utils from '#/components/forms/InputDate/utils' - -export function InputDate({ - label, - hasError, - testID, - value: initialValue, - onChange, - accessibilityLabel, - accessibilityHint, - ...props -}: InputDateProps) { - const labelId = React.useId() - const t = useTheme() - const [value, setValue] = React.useState(initialValue) - const { - state: hovered, - onIn: onHoverIn, - onOut: onHoverOut, - } = useInteractionState() - 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 (hovered || focused) { - input.push({ - borderColor: t.atoms.border_contrast.borderColor, - }) - - if (hasError) { - input.push({ - borderColor: tokens.color.red_500, - }) - } - } - - return {inputStyles: input, iconStyles: icon} - }, [t, hovered, focused, hasError]) - - const handleOnChange = React.useCallback( - (e: any) => { - const date = e.currentTarget.valueAsDate - - if (date) { - const formatted = toSimpleDateString(date) - onChange(formatted) - setValue(formatted) - } - }, - [onChange, setValue], - ) - - return ( - - {label && ( - - {label} - - )} - - {unstable_createElement('input', { - ...props, - testID: `${testID}-datepicker`, - 'aria-labelledby': labelId, - 'aria-label': label, - accessibilityLabel: accessibilityLabel, - accessibilityHint: accessibilityHint, - type: 'date', - value: value, - onFocus: onFocus, - onBlur: onBlur, - onChange: handleOnChange, - onMouseEnter: onHoverIn, - onMouseLeave: onHoverOut, - style: [ - { - outline: 0, - border: 0, - appearance: 'none', - boxSizing: 'border-box', - lineHeight: atoms.text_md.lineHeight * 1.1875, - paddingTop: atoms.pt_md.paddingTop - 1, - }, - atoms.w_full, - atoms.px_lg, - atoms.pb_md, - atoms.rounded_sm, - atoms.text_md, - t.atoms.bg_contrast_100, - t.atoms.text, - ...inputStyles, - ], - })} - - - - - - ) -} diff --git a/src/components/forms/InputDate/types.ts b/src/components/forms/InputDate/types.ts deleted file mode 100644 index 2ef0a4b3ba..0000000000 --- a/src/components/forms/InputDate/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -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/InputDate/utils.ts b/src/components/forms/InputDate/utils.ts deleted file mode 100644 index c787272fe8..0000000000 --- a/src/components/forms/InputDate/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 cc3aae2e59..941c16bd30 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -215,8 +215,8 @@ export function createInput(Component: typeof TextInput) { {borderColor: 'transparent', borderWidth: 2}, ctx.hovered ? chromeHover : {}, ctx.focused ? chromeFocus : {}, - ctx.isInvalid ? chromeError : {}, - ctx.isInvalid && (ctx.hovered || ctx.focused) + ctx.isInvalid || isInvalid ? chromeError : {}, + (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) ? chromeErrorHover : {}, ]} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index 4339f389d3..0e9bf54b01 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -41,7 +41,7 @@ export function Forms() { /> - + Text field @@ -54,14 +54,26 @@ export function Forms() { + + Textarea + + +

DateField

- + { + onChangeDate={date => { console.log(date) setDate(date) }}