diff --git a/packages/material/src/controls/MaterialDateControl.tsx b/packages/material/src/controls/MaterialDateControl.tsx index 0daf295b2..2bdafbd45 100644 --- a/packages/material/src/controls/MaterialDateControl.tsx +++ b/packages/material/src/controls/MaterialDateControl.tsx @@ -38,7 +38,12 @@ import { LocalizationProvider } from '@mui/lab'; import AdapterDayjs from '@mui/lab/AdapterDayjs'; -import { createOnChangeHandler, getData, useFocus } from '../util'; +import { + createOnChangeHandler, + getData, + useFocus, + useParsedDateSynchronizer, +} from '../util'; export const MaterialDateControl = (props: ControlProps)=> { const [focused, onFocus, onBlur] = useFocus(); @@ -79,6 +84,7 @@ export const MaterialDateControl = (props: ControlProps)=> { handleChange, saveFormat ),[path, handleChange, saveFormat]); + const parsedDateSynchronizer = useParsedDateSynchronizer({ data, onBlur }); return ( @@ -103,10 +109,15 @@ export const MaterialDateControl = (props: ControlProps)=> { autoFocus={appliedUiSchemaOptions.focus} error={!isValid} fullWidth={!appliedUiSchemaOptions.trim} - inputProps={{ ...params.inputProps, type: 'text' }} + inputProps={{ + ...params.inputProps, + type: 'text', + value: parsedDateSynchronizer.value, + onChange: parsedDateSynchronizer.createOnChangeHandler(params.inputProps.onChange), + }} InputLabelProps={data ? { shrink: true } : undefined} onFocus={onFocus} - onBlur={onBlur} + onBlur={parsedDateSynchronizer.onBlur} variant={'standard'} /> )} diff --git a/packages/material/src/controls/MaterialDateTimeControl.tsx b/packages/material/src/controls/MaterialDateTimeControl.tsx index 76b7de3e5..956759b16 100644 --- a/packages/material/src/controls/MaterialDateTimeControl.tsx +++ b/packages/material/src/controls/MaterialDateTimeControl.tsx @@ -38,7 +38,12 @@ import { LocalizationProvider } from '@mui/lab'; import AdapterDayjs from '@mui/lab/AdapterDayjs'; -import { createOnChangeHandler, getData, useFocus } from '../util'; +import { + createOnChangeHandler, + getData, + useFocus, + useParsedDateSynchronizer, +} from '../util'; export const MaterialDateTimeControl = (props: ControlProps) => { const [focused, onFocus, onBlur] = useFocus(); @@ -82,6 +87,8 @@ export const MaterialDateTimeControl = (props: ControlProps) => { saveFormat ),[path, handleChange, saveFormat]); + const parsedDateSynchronizer = useParsedDateSynchronizer({ data, onBlur }); + return ( @@ -106,10 +113,15 @@ export const MaterialDateTimeControl = (props: ControlProps) => { autoFocus={appliedUiSchemaOptions.focus} error={!isValid} fullWidth={!appliedUiSchemaOptions.trim} - inputProps={{ ...params.inputProps, type: 'text' }} + inputProps={{ + ...params.inputProps, + type: 'text', + value: parsedDateSynchronizer.value, + onChange: parsedDateSynchronizer.createOnChangeHandler(params.inputProps.onChange), + }} InputLabelProps={data ? { shrink: true } : undefined} onFocus={onFocus} - onBlur={onBlur} + onBlur={parsedDateSynchronizer.onBlur} variant={'standard'} /> )} diff --git a/packages/material/src/controls/MaterialTimeControl.tsx b/packages/material/src/controls/MaterialTimeControl.tsx index 16b3b7952..f4f73fc62 100644 --- a/packages/material/src/controls/MaterialTimeControl.tsx +++ b/packages/material/src/controls/MaterialTimeControl.tsx @@ -38,7 +38,12 @@ import { LocalizationProvider } from '@mui/lab'; import AdapterDayjs from '@mui/lab/AdapterDayjs'; -import { createOnChangeHandler, getData, useFocus } from '../util'; +import { + createOnChangeHandler, + getData, + useFocus, + useParsedDateSynchronizer, +} from '../util'; export const MaterialTimeControl = (props: ControlProps) => { const [focused, onFocus, onBlur] = useFocus(); @@ -82,6 +87,8 @@ export const MaterialTimeControl = (props: ControlProps) => { saveFormat ),[path, handleChange, saveFormat]); + const parsedDateSynchronizer = useParsedDateSynchronizer({ data, onBlur }); + return ( @@ -106,10 +113,15 @@ export const MaterialTimeControl = (props: ControlProps) => { autoFocus={appliedUiSchemaOptions.focus} error={!isValid} fullWidth={!appliedUiSchemaOptions.trim} - inputProps={{ ...params.inputProps, type: 'text' }} + inputProps={{ + ...params.inputProps, + type: 'text', + value: parsedDateSynchronizer.value, + onChange: parsedDateSynchronizer.createOnChangeHandler(params.inputProps.onChange), + }} InputLabelProps={data ? { shrink: true } : undefined} onFocus={onFocus} - onBlur={onBlur} + onBlur={parsedDateSynchronizer.onBlur} variant={'standard'} /> )} diff --git a/packages/material/src/util/datejs.ts b/packages/material/src/util/datejs.ts index 505763c82..76be9b799 100644 --- a/packages/material/src/util/datejs.ts +++ b/packages/material/src/util/datejs.ts @@ -1,5 +1,6 @@ import dayjs from 'dayjs'; import customParsing from 'dayjs/plugin/customParseFormat'; +import { useState, useMemo, FormEvent, FormEventHandler } from 'react'; // required for the custom save formats in the date, time and date-time pickers dayjs.extend(customParsing); @@ -30,3 +31,38 @@ export const getData = ( } return dayjsData; }; + +type DateInputFormEvent = FormEvent; + +/** + * Improves the UX of date fields by controlling the rendered input value. + * When a user enters a date value that ends up being different than the + * the value of `data`, then on blur we sync the rendered input value. + * + * @param data The parsed date value. + * @param onBlur Additional handler to run after input value is sync'd. + * @returns Props to pass to the rendered input element. + */ +export const useParsedDateSynchronizer = (props: { + data: any; + onBlur: FormEventHandler | undefined; +}) => { + const [value, setValue] = useState(props.data); + + const onBlur = useMemo( + () => (event: DateInputFormEvent) => { + setValue(props.data); + if (props.onBlur) props.onBlur(event); + }, + [props.data, props.onBlur] + ); + + const createOnChangeHandler = ( + onChange: (event: DateInputFormEvent) => void + ) => (event: DateInputFormEvent) => { + setValue((event.target as HTMLInputElement | HTMLTextAreaElement).value); + if (onChange) onChange(event); + }; + + return { value, onBlur, createOnChangeHandler }; +};