Open flaviendelangle opened 2 years ago
Related to https://github.com/mui/mui-x/issues/6736. I've spent 8+ hours now trying to figure out a solution that correctly covers onAccept
, onBlur
, and on onChange
, works in both the desktop and mobile pickers with and without the accept
button, doesn't break input validation, etc.. I want the users of my date picker to be listen for real, "committed" updates to the date without worrying about whether that came from the keyboard input or calendar picker, and without firing a bunch of intermediate dates (or firing with the same date twice) while editing or selecting in the modal view before pressing "accept." It's turned out to be quite tricky to make that work.
After mucking around a bit, I think the date picker should keep its own internal state about the currently selected date, and fire onAccept
when:
onBlur
).enter
key.After much trial-and-error, here's my current attempt (which also solves the AdapterDateFns input leniency problem):
import React, {KeyboardEvent, useEffect, useState} from 'react'
import TextField from '@mui/material/TextField'
import {DatePicker} from '@mui/x-date-pickers/DatePicker'
import {AdapterDateFns} from '@mui/x-date-pickers/AdapterDateFns'
import {LocalizationProvider} from '@mui/x-date-pickers/LocalizationProvider'
export type DatePickerProps = {
value: Date | null
onUpdate: ((date: Date | null) => void)
}
const PARSER = {
format: "yyyy-MM-dd",
mask: "____-__-__",
regex: /^(\d{4})-(\d{2})-(\d{2})$/
}
function isValidDate(date: any): boolean {
return date instanceof Date && !isNaN(date.valueOf())
}
function isValidDateString(input: string): boolean {
return PARSER.regex.test(input) && isValidDate(new Date(input))
}
export default function MyFunnyDatePicker({
onUpdate,
value,
...pickerProps
}: DatePickerProps) {
const [date, setDate] = useState<Date | null>(value)
useEffect(() => setDate(value), [value])
const shouldUpdateDate =
date.toLocaleDateString() !== value?.toLocaleDateString()
const handleChange =
(newDate: Date | null, rawInput?: string) => {
if (!rawInput && isValidDate(date)) {
setDate(newDate)
} else if (rawInput && rawInput?.length && isValidDateString(rawInput)) {
setDate(newDate)
}
}
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && shouldUpdateDate) {
onUpdate(date)
}
}
const onAccept = () => {
if (shouldUpdateDate) {
onUpdate(date)
}
}
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
inputFormat={PARSER.format}
mask={PARSER.mask}
value={date}
InputAdornmentProps={{position: 'end'}}
onAccept={onAccept}
onChange={handleChange}
{...pickerProps}
renderInput={(params) =>
<TextField
{...params}
size='small'
onKeyDown={onKeyDown}
onBlur={onAccept}
/>
}
/>
</LocalizationProvider>
)
}
I've also tried debouncing the text input so that updates will fire after some delay, but that makes implementation trickier and it's unclear that is a better user experience than waiting for the user to trigger an update with onBlur
or enter
.
This solution still doesn't handle the accept
button correctly in desktop view (when I manually add it) ... apparently that works differently than in mobile view?
onAccept
and onChange
semantics are wrong in the DateTimePicker
. onChange
should be the callback that is called when the selection is confirmed (for example the user clicks the OK button, or the popup is closed in some other way that assumes that the user has confirmed the selection).
Internal changes in the popup should trigger some other callback prop.
Agree with @croraf, sadly still an issue.
Duplicates
Latest version
Summary 💡
With the current pickers, users have to control the value (i.e to pass a
value
andonChange
props).The
onChange
prop is then called every time the selected date changes to a valid value.selectionState = 'partial' | 'finish'
). For aDateTimePicker
it will be fired when picking the day, then when picking the hour etc...We also expose an
onAccept
prop that is called when the user finishes his date selection. Which can happen when:If a developer wants to only care about the date selected at the end of the date selection, he can't easily do it.
On the new fields / pickers, developers will be able to only provide a
defaultValue
instead of controlling. But it does not solve the whole issue. Right now, the old input (and new field) new triggeredonAccept
. And withonChange
, there is no way to easily now if it was an acceptance call in the view. I am unclear about the actual solution, adding it to the "To Explore" board.Related issues
4560
4593
4599
5891
4473
Somehow related to #5774
Examples 🌈
No response
Motivation 🔦
No response
Order ID 💳 (optional)
No response