diff --git a/CLAUDE.md b/CLAUDE.md index 37e6a1c2c5..ea7c1fc9f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -164,12 +164,45 @@ related code together and gives us a better visual cue that there are probably other files contained within this "macro" feature, whereas `Component.tsx` on its own looks more like a single component file. -### Documentation and Tests Within Features +### Comments Comment code when necessary to explain the “why” behind something; avoid comments that simply describe the code. Avoid Unicode characters in comments, e.g., use `-` not `—`. +Always use docblock (`/** */`) syntax for comments that document a type, type +member, method, function, or variable. These are the comments a reader expects +to find attached to a named declaration, and the docblock form makes that intent +clear and surfaces nicely in editor tooltips. + +```tsx +type DateFieldProps = { + /** + * An empty string renders the placeholder and opens the picker at today (or + * maximumDate, if earlier). + */ + value: string | Date +} + +/** + * Date-only input. Accepts a string in the format YYYY-MM-DD, or a Date object. + */ +export function DateField() {} +``` + +More generally, any multiline comment should use the `/* */` block syntax rather +than stacked `//` lines. Reserve `//` for short, single-line comments. + +```tsx +/* + * The picker requires a valid date, so when value is empty we fall back to + * maximumDate (if set) or today. + */ +const fallbackDate = maximumDate ? toSimpleDateString(maximumDate) : today +``` + +### Documentation and Tests Within Features + For larger features or components, it's helpful to include a README.md file within the directory that explains the purpose of the feature, how it works, and any important implementation details. The `/Component/index.tsx` pattern lends diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index d8d95aac79..b7f594281a 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -16,6 +16,8 @@ export function DateField({ value, inputRef, onChangeDate, + onConfirm, + placeholder, label, isInvalid, testID, @@ -26,14 +28,28 @@ export function DateField({ const t = useTheme() const [open, setOpen] = useState(false) + /* + * The picker requires a valid date, so when value is empty we open at + * maximumDate (if set) or today. Normalize through toSimpleDateString so a + * date-only value is parsed as UTC midnight, consistent with the picker's + * timeZoneOffsetInMinutes={0} and the maximumDate below. + */ + const initialDate = + value === '' + ? maximumDate + ? new Date(toSimpleDateString(maximumDate)) + : new Date() + : new Date(toSimpleDateString(value)) + const onChangeInternal = useCallback( (date: Date) => { setOpen(false) const formatted = toSimpleDateString(date) onChangeDate(formatted) + onConfirm?.(formatted) }, - [onChangeDate, setOpen], + [onChangeDate, onConfirm, setOpen], ) useImperativeHandle( @@ -63,6 +79,7 @@ export function DateField({ void isInvalid?: boolean accessibilityHint?: string @@ -78,20 +80,18 @@ export function DateFieldButton({ a.align_center, hovered ? chromeHover : {}, focused || pressed ? chromeFocus : {}, - isInvalid || isInvalid ? chromeError : {}, - (isInvalid || isInvalid) && (hovered || focused) - ? chromeErrorHover - : {}, + isInvalid ? chromeError : {}, + isInvalid && (hovered || focused) ? chromeErrorHover : {}, ]}> - {i18n.date(value, {timeZone: 'UTC'})} + {value === '' ? placeholder : i18n.date(value, {timeZone: 'UTC'})} diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx index 4236650e5e..0d014fa877 100644 --- a/src/components/forms/DateField/index.tsx +++ b/src/components/forms/DateField/index.tsx @@ -1,4 +1,4 @@ -import {useCallback, useImperativeHandle} from 'react' +import {useCallback, useImperativeHandle, useState} from 'react' import {Keyboard, View} from 'react-native' import DatePicker from 'react-native-date-picker' import {msg} from '@lingui/core/macro' @@ -28,6 +28,8 @@ export function DateField({ value, inputRef, onChangeDate, + onConfirm, + placeholder, testID, label, isInvalid, @@ -38,10 +40,23 @@ export function DateField({ const t = useTheme() const control = Dialog.useDialogControl() + /* + * The picker requires a valid date, so when value is empty we fall back to + * maximumDate (if set) or today. Draft state lets the picker scroll even when + * the parent does not echo value back (e.g. a clearable field). + */ + const fallbackDate = maximumDate + ? toSimpleDateString(maximumDate) + : toSimpleDateString(new Date()) + const [draft, setDraft] = useState(() => + value === '' ? fallbackDate : toSimpleDateString(value), + ) + const onChangeInternal = useCallback( (date: Date | undefined) => { if (date) { const formatted = toSimpleDateString(date) + setDraft(formatted) onChangeDate(formatted) } }, @@ -53,13 +68,14 @@ export function DateField({ () => ({ focus: () => { Keyboard.dismiss() + setDraft(value === '' ? fallbackDate : toSimpleDateString(value)) control.open() }, blur: () => { control.close() }, }), - [control], + [control, value, fallbackDate], ) return ( @@ -67,8 +83,10 @@ export function DateField({ { Keyboard.dismiss() + setDraft(value === '' ? fallbackDate : toSimpleDateString(value)) control.open() }} isInvalid={isInvalid} @@ -85,7 +103,7 @@ export function DateField({