-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: add autocomplete prop to rac datefield + datepicker #7773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
// which is why in the code below we first set each segment to validate it before committing the new value. | ||
// However, in DatePickerState, since we have to be able to commit values from the Calendar popover, we are also able to | ||
// set a new value when the field itself is empty. | ||
if ('setSegment' in state) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a little unsure if this is the direction we want to go. i left some comments in the code to explain what's going on but essentially the issue is that depending if the state is DateFieldState or DatePickerState, the setValue method behaves a bit differently.
i've tried validating each segment inside setValue in useDateFieldState but the problem is that we aren't able to differentiate between a newValue that is the placeholder date versus something external (like in this case). as a result, we end up committing the placeholder value as an actual value that we want to display. in any case, it causes some really strange behavior when trying to validate segments inside setValue so it worked better to do it here inside HiddenDateInput
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this was always inside a DateInput rather than sometimes in a DatePicker would it be easier? Both DateField and DatePicker include a DateInput, and this would simplify it to only need to set the value of the DateField which in turn would set the value of the DatePicker.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be good to add a test to make sure the hidden input doesn't end up in the submitted form data if that's possible.
Where did we land on that again btw?
let dateSegments = ['day', 'month', 'year']; | ||
let timeSegments = ['hour', 'minute', 'second']; | ||
let granularityMap = {'hour': 1, 'minute': 2, 'second': 3}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these look like constants, better to declare them once at the module level
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good. i kept time segments tho since i actually change that based on the granularity
useEffect(() => { | ||
if (state.value == null) { | ||
setDateValue(''); | ||
} else { | ||
setDateValue(state.value.toString()); | ||
} | ||
}, [state.value]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any way we can derive the state in render instead of causing a second render anytime state.value
changes?
it's a little hacky but we set as for a unit test, i couldn't think of a good way to do it. the value of a hidden date input should match the value of the input we submit in a form. the only way to test if it is coming from the right input is to purposefully mismatch the value of the hidden date input and the "actual" input and confirm that we are getting the value from the "actual" value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks pretty good to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested as requested with Dashlane, it does not work, but it also doesn't work for Picker/Select for the state, so it (Dashlane) has some issues
Build successful! 🎉 |
export interface SpectrumDateRangePickerProps<T extends DateValue> extends Omit<AriaDateRangePickerProps<T>, 'isInvalid' | 'validationState'>, Omit<SpectrumDatePickerBase<T>, 'validate'> {} | ||
export interface SpectrumDateFieldProps<T extends DateValue> extends Omit<AriaDateFieldProps<T>, 'isInvalid' | 'validationState'>, SpectrumDateFieldBase<T> {} | ||
export interface SpectrumDateFieldProps<T extends DateValue> extends Omit<AriaDateFieldProps<T>, 'isInvalid' | 'validationState' | 'autoComplete'>, SpectrumDateFieldBase<T> {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not supported in Spectrum? Only RAC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figured we just do add it to RAC since that's where it was asked for but it should be pretty easy to add to Spectrum if we want to
@@ -110,6 +111,11 @@ export const DateField = /*#__PURE__*/ (forwardRef as forwardRefType)(function D | |||
slot={props.slot || undefined} | |||
data-invalid={state.isInvalid || undefined} | |||
data-disabled={state.isDisabled || undefined} /> | |||
<HiddenDateInput |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this could be combined with the existing hidden input in DateInput which is used for form submission? Seems like we could potentially only have one, assuming the formats are equivalent. Did you try that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I just tried that and one issue is that in order for autocomplete to work in Safari, the input cannot have <input hidden />
but that's what we have on the existing hidden input. So we'd have to change it so that it was visually hidden instead. I feel like this is the main issue since autocomplete with dates only works on Safari from what I've found.
The other issue I encountered is if <input type="text"/>
(which our existing hidden input is) and the current date is filled, I can't use autocomplete to change the date. I'd have to delete parts of the date such that it becomes undefined in order to use autocomplete.
If we're willing to make these changes then we could probably combine them into one but not sure if that's what we'd want to do
onChange: (e) => { | ||
let targetString = e.target.value.toString(); | ||
if (targetString) { | ||
let targetValue: CalendarDateTime | CalendarDate = parseDateTime(targetString); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably add a try...catch around these parsing functions in case the value is somehow invalid.
// which is why in the code below we first set each segment to validate it before committing the new value. | ||
// However, in DatePickerState, since we have to be able to commit values from the Calendar popover, we are also able to | ||
// set a new value when the field itself is empty. | ||
if ('setSegment' in state) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this was always inside a DateInput rather than sometimes in a DatePicker would it be easier? Both DateField and DatePicker include a DateInput, and this would simplify it to only need to set the value of the DateField which in turn would set the value of the DatePicker.
Build successful! 🎉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's get this in!
// which is why in the code below we first set each segment to validate it before committing the new value. | ||
// However, in DatePickerState, since we have to be able to commit values from the Calendar popover, we are also able to | ||
// set a new value when the field itself is empty. | ||
if ('setSegment' in state) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably make state.setValue
a bit smarter so it sets all the segments as valid when called from the outside. We can do that later.
type: inputType, | ||
// We set the form prop to an empty string to prevent the hidden date input's value from being submitted | ||
form: '', | ||
name, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need the name then if the input won't be submitted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm good point. yeah i don't think we need it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay nvm just tested it in safari and if we remove name
, it doesn't autofill so i'll just leave it in even though it won't be submitted
Build successful! 🎉 |
## API Changes
react-aria-components/react-aria-components:DateField DateField <T extends DateValue> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
+ autoComplete?: string
autoFocus?: boolean
children?: ChildrenOrFunction<DateFieldRenderProps>
className?: ClassNameOrFunction<DateFieldRenderProps>
defaultValue?: DateValue | null
granularity?: Granularity
hideTimeZone?: boolean = false
hourCycle?: number | number
id?: string
isDateUnavailable?: (DateValue) => boolean
isDisabled?: boolean
isInvalid?: boolean
isReadOnly?: boolean
isRequired?: boolean
maxValue?: DateValue | null
minValue?: DateValue | null
name?: string
onBlur?: (FocusEvent<Target>) => void
onChange?: (MappedDateValue<DateValue> | null) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
placeholderValue?: DateValue | null
shouldForceLeadingZeros?: boolean
slot?: string | null
style?: StyleOrFunction<DateFieldRenderProps>
validate?: (MappedDateValue<DateValue>) => ValidationError | boolean | null | undefined
validationBehavior?: 'native' | 'aria' = 'native'
value?: DateValue | null
} /react-aria-components:DatePicker DatePicker <T extends DateValue> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
+ autoComplete?: string
autoFocus?: boolean
children?: ChildrenOrFunction<DatePickerRenderProps>
className?: ClassNameOrFunction<DatePickerRenderProps>
defaultOpen?: boolean
firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
form?: string
granularity?: Granularity
hideTimeZone?: boolean = false
hourCycle?: number | number
id?: string
isDateUnavailable?: (DateValue) => boolean
isDisabled?: boolean
isInvalid?: boolean
isOpen?: boolean
isReadOnly?: boolean
isRequired?: boolean
maxValue?: DateValue | null
minValue?: DateValue | null
name?: string
onBlur?: (FocusEvent<Target>) => void
onChange?: (MappedDateValue<DateValue> | null) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
onOpenChange?: (boolean) => void
pageBehavior?: PageBehavior = visible
placeholderValue?: DateValue | null
shouldCloseOnSelect?: boolean | () => boolean = true
shouldForceLeadingZeros?: boolean
slot?: string | null
style?: StyleOrFunction<DatePickerRenderProps>
validate?: (MappedDateValue<DateValue>) => ValidationError | boolean | null | undefined
validationBehavior?: 'native' | 'aria' = 'native'
value?: DateValue | null
} /react-aria-components:DateFieldProps DateFieldProps <T extends DateValue> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
+ autoComplete?: string
autoFocus?: boolean
children?: ChildrenOrFunction<DateFieldRenderProps>
className?: ClassNameOrFunction<DateFieldRenderProps>
defaultValue?: DateValue | null
granularity?: Granularity
hideTimeZone?: boolean = false
hourCycle?: number | number
id?: string
isDateUnavailable?: (DateValue) => boolean
isDisabled?: boolean
isInvalid?: boolean
isReadOnly?: boolean
isRequired?: boolean
maxValue?: DateValue | null
minValue?: DateValue | null
name?: string
onBlur?: (FocusEvent<Target>) => void
onChange?: (MappedDateValue<DateValue> | null) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
placeholderValue?: DateValue | null
shouldForceLeadingZeros?: boolean
slot?: string | null
style?: StyleOrFunction<DateFieldRenderProps>
validate?: (MappedDateValue<DateValue>) => ValidationError | boolean | null | undefined
validationBehavior?: 'native' | 'aria' = 'native'
value?: DateValue | null
} /react-aria-components:DatePickerProps DatePickerProps <T extends DateValue> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
+ autoComplete?: string
autoFocus?: boolean
children?: ChildrenOrFunction<DatePickerRenderProps>
className?: ClassNameOrFunction<DatePickerRenderProps>
defaultOpen?: boolean
firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
form?: string
granularity?: Granularity
hideTimeZone?: boolean = false
hourCycle?: number | number
id?: string
isDateUnavailable?: (DateValue) => boolean
isDisabled?: boolean
isInvalid?: boolean
isOpen?: boolean
isReadOnly?: boolean
isRequired?: boolean
maxValue?: DateValue | null
minValue?: DateValue | null
name?: string
onBlur?: (FocusEvent<Target>) => void
onChange?: (MappedDateValue<DateValue> | null) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
onOpenChange?: (boolean) => void
pageBehavior?: PageBehavior = visible
placeholderValue?: DateValue | null
shouldCloseOnSelect?: boolean | () => boolean = true
shouldForceLeadingZeros?: boolean
slot?: string | null
style?: StyleOrFunction<DatePickerRenderProps>
validate?: (MappedDateValue<DateValue>) => ValidationError | boolean | null | undefined
validationBehavior?: 'native' | 'aria' = 'native'
value?: DateValue | null
} @react-aria/datepicker/@react-aria/datepicker:AriaDateFieldProps AriaDateFieldProps <T extends DateValue> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
+ autoComplete?: string
autoFocus?: boolean
defaultValue?: DateValue | null
description?: ReactNode
errorMessage?: ReactNode | (ValidationResult) => ReactNode
granularity?: Granularity
hideTimeZone?: boolean = false
hourCycle?: number | number
id?: string
isDateUnavailable?: (DateValue) => boolean
isDisabled?: boolean
isInvalid?: boolean
isReadOnly?: boolean
isRequired?: boolean
label?: ReactNode
maxValue?: DateValue | null
minValue?: DateValue | null
name?: string
onBlur?: (FocusEvent<Target>) => void
onChange?: (MappedDateValue<DateValue> | null) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
placeholderValue?: DateValue | null
shouldForceLeadingZeros?: boolean
validate?: (MappedDateValue<DateValue>) => ValidationError | boolean | null | undefined
validationBehavior?: 'aria' | 'native' = 'aria'
value?: DateValue | null
} /@react-aria/datepicker:AriaDatePickerProps AriaDatePickerProps <T extends DateValue> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
+ autoComplete?: string
autoFocus?: boolean
defaultOpen?: boolean
defaultValue?: DateValue | null
description?: ReactNode
firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
form?: string
granularity?: Granularity
hideTimeZone?: boolean = false
hourCycle?: number | number
id?: string
isDateUnavailable?: (DateValue) => boolean
isDisabled?: boolean
isInvalid?: boolean
isOpen?: boolean
isReadOnly?: boolean
isRequired?: boolean
label?: ReactNode
maxValue?: DateValue | null
minValue?: DateValue | null
name?: string
onBlur?: (FocusEvent<Target>) => void
onChange?: (MappedDateValue<DateValue> | null) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
onOpenChange?: (boolean) => void
pageBehavior?: PageBehavior = visible
placeholderValue?: DateValue | null
shouldForceLeadingZeros?: boolean
validate?: (MappedDateValue<DateValue>) => ValidationError | boolean | null | undefined
validationBehavior?: 'aria' | 'native' = 'aria'
value?: DateValue | null
} /@react-aria/datepicker:AriaDateFieldOptions AriaDateFieldOptions <T extends DateValue> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
+ autoComplete?: string
autoFocus?: boolean
description?: ReactNode
errorMessage?: ReactNode | (ValidationResult) => ReactNode
form?: string
hideTimeZone?: boolean = false
hourCycle?: number | number
id?: string
inputRef?: RefObject<HTMLInputElement | null>
isDateUnavailable?: (DateValue) => boolean
isDisabled?: boolean
isInvalid?: boolean
isReadOnly?: boolean
isRequired?: boolean
label?: ReactNode
name?: string
onBlur?: (FocusEvent<Target>) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
shouldForceLeadingZeros?: boolean
validationBehavior?: 'aria' | 'native' = 'aria'
} @react-stately/datepicker/@react-stately/datepicker:DateFieldStateOptions DateFieldStateOptions <T extends DateValue = DateValue> {
+ autoComplete?: string
autoFocus?: boolean
createCalendar: (CalendarIdentifier) => Calendar
defaultOpen?: boolean
defaultValue?: DateValue | null
errorMessage?: ReactNode | (ValidationResult) => ReactNode
firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
granularity?: Granularity
hideTimeZone?: boolean = false
hourCycle?: number | number
isDateUnavailable?: (DateValue) => boolean
isDisabled?: boolean
isInvalid?: boolean
isOpen?: boolean
isReadOnly?: boolean
isRequired?: boolean
label?: ReactNode
locale: string
maxGranularity?: 'year' | 'month' | Granularity = 'year'
maxValue?: DateValue | null
minValue?: DateValue | null
onBlur?: (FocusEvent<Target>) => void
onChange?: (MappedDateValue<DateValue> | null) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
onOpenChange?: (boolean) => void
pageBehavior?: PageBehavior = visible
placeholderValue?: DateValue | null
shouldForceLeadingZeros?: boolean
validate?: (MappedDateValue<DateValue>) => ValidationError | boolean | null | undefined
validationBehavior?: 'aria' | 'native' = 'aria'
value?: DateValue | null
} /@react-stately/datepicker:DatePickerStateOptions DatePickerStateOptions <T extends DateValue> {
+ autoComplete?: string
autoFocus?: boolean
defaultOpen?: boolean
defaultValue?: DateValue | null
description?: ReactNode
firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
granularity?: Granularity
hideTimeZone?: boolean = false
hourCycle?: number | number
isDateUnavailable?: (DateValue) => boolean
isDisabled?: boolean
isInvalid?: boolean
isOpen?: boolean
isReadOnly?: boolean
isRequired?: boolean
label?: ReactNode
maxValue?: DateValue | null
minValue?: DateValue | null
onBlur?: (FocusEvent<Target>) => void
onChange?: (MappedDateValue<DateValue> | null) => void
onFocus?: (FocusEvent<Target>) => void
onFocusChange?: (boolean) => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
onOpenChange?: (boolean) => void
pageBehavior?: PageBehavior = visible
placeholderValue?: DateValue | null
shouldCloseOnSelect?: boolean | () => boolean = true
shouldForceLeadingZeros?: boolean
validate?: (MappedDateValue<DateValue>) => ValidationError | boolean | null | undefined
validationBehavior?: 'aria' | 'native' = 'aria'
value?: DateValue | null
} |
Closes #7167
Excludes adding autocomplete prop to DateRangePicker since the only date spec that is really supported is "bday" and those don't really come in ranges. See spec: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill
This is just a RAC change but we could add it to Spectrum probably.
✅ Pull Request Checklist:
📝 Test Instructions:
Personally, I've only been able to get this working in Safari. You need to ensure that you have your contact information filled out for it to work. Browser support for date autofill is spotty at best. See https://www.w3.org/WAI/GL/wiki/WCAG_2.1_Implementations/JF/research
When you type into the textfield, (you might need to click in the textfield a couple times), a popover should appear that looks something like this. Click the option with that says "fill birthday" and it should autofill