Add placeholder, empty-value, and onConfirm support to DateField (#10993)

Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Eric Bailey
2026-06-25 18:04:54 -05:00
committed by GitHub
parent fcf17a482f
commit 308693762c
7 changed files with 162 additions and 18 deletions
@@ -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({
<DateFieldButton
label={label}
value={value}
placeholder={placeholder}
onPress={onPress}
isInvalid={isInvalid}
accessibilityHint={accessibilityHint}
@@ -77,7 +94,7 @@ export function DateField({
theme={t.scheme}
// @ts-ignore TODO
buttonColor={t.name === 'light' ? '#000000' : '#ffffff'}
date={new Date(value)}
date={initialDate}
onConfirm={onChangeInternal}
onCancel={onCancel}
mode="date"
@@ -14,12 +14,14 @@ import {Text} from '#/components/Typography'
export function DateFieldButton({
label,
value,
placeholder,
onPress,
isInvalid,
accessibilityHint,
}: {
label: string
value: string | Date
placeholder?: string
onPress: () => 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 : {},
]}>
<TextField.Icon icon={CalendarDays} />
<Text
style={[
a.text_md,
a.pl_xs,
t.atoms.text,
value === '' ? t.atoms.text_contrast_low : t.atoms.text,
{lineHeight: a.text_md.fontSize * 1.1875},
]}>
{i18n.date(value, {timeZone: 'UTC'})}
{value === '' ? placeholder : i18n.date(value, {timeZone: 'UTC'})}
</Text>
</Pressable>
</View>
+33 -4
View File
@@ -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({
<DateFieldButton
label={label}
value={value}
placeholder={placeholder}
onPress={() => {
Keyboard.dismiss()
setDraft(value === '' ? fallbackDate : toSimpleDateString(value))
control.open()
}}
isInvalid={isInvalid}
@@ -85,7 +103,7 @@ export function DateField({
<DatePicker
timeZoneOffsetInMinutes={0}
theme={t.scheme}
date={new Date(toSimpleDateString(value))}
date={new Date(draft)}
onDateChange={onChangeInternal}
mode="date"
locale={i18n.locale}
@@ -102,7 +120,18 @@ export function DateField({
</View>
<Button
label={_(msg`Done`)}
onPress={() => control.close()}
onPress={() => {
/*
* Commit the currently shown date even if the user never
* scrolled (onDateChange only fires on scroll). This keeps
* onChangeDate firing alongside onConfirm, matching Android and
* web, so an empty field confirmed without scrolling does not
* report a date via onConfirm while onChangeDate stays silent.
*/
onChangeDate(draft)
onConfirm?.(draft)
control.close()
}}
size="large"
color="primary"
variant="solid">
+4 -2
View File
@@ -36,6 +36,7 @@ export function DateField({
value,
inputRef,
onChangeDate,
onConfirm,
label,
isInvalid,
testID,
@@ -49,16 +50,17 @@ export function DateField({
if (date) {
const formatted = toSimpleDateString(date)
onChangeDate(formatted)
onConfirm?.(formatted)
}
},
[onChangeDate],
[onChangeDate, onConfirm],
)
return (
<TextField.Root isInvalid={isInvalid}>
<TextField.Icon icon={CalendarDays} />
<Input
value={toSimpleDateString(value)}
value={value === '' ? '' : toSimpleDateString(value)}
inputRef={inputRef as React.Ref<TextInput>}
label={label}
onChange={handleOnChange}
+15
View File
@@ -3,8 +3,23 @@ export type DateFieldRef = {
blur: () => void
}
export type DateFieldProps = {
/**
* An empty string renders the placeholder and opens the picker at today (or
* maximumDate, if earlier).
*/
value: string | Date
onChangeDate: (date: string) => void
/**
* Fired when the user commits a date: iOS "Done", Android confirm, or web
* input change. Distinct from onChangeDate, which on iOS fires on every
* scroll tick.
*/
onConfirm?: (date: string) => void
/**
* Shown on native when value is empty. Web uses the browser's native date
* placeholder.
*/
placeholder?: string
label: string
inputRef?: React.Ref<DateFieldRef>
isInvalid?: boolean