From 279d8a9a0b089c5ff490c9dc9e890b86eb345000 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Mon, 11 Apr 2022 11:55:22 +0200 Subject: [PATCH] [time picker] Only update time when updating input in TimePicker (#4398) --- .../DesktopTimePicker.test.tsx | 31 ++++++++++++++++ .../DesktopTimePicker/DesktopTimePicker.tsx | 7 ++++ .../src/MobileTimePicker/MobileTimePicker.tsx | 7 ++++ .../src/internals/hooks/useMaskedInput.tsx | 1 + .../src/internals/hooks/usePickerState.ts | 35 +++++++++++++++++-- 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.test.tsx index f4cd69c09746..f155e52b10cc 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.test.tsx @@ -219,6 +219,37 @@ describe('', () => { ); }); + it('should only update the time change editing through the input', () => { + const handleChange = spy(); + render( + } + value={adapterToUse.date('2019-01-01T04:20:00.000')} + />, + ); + + // call `onChange` with an invalid time + fireEvent.change(screen.getByRole('textbox'), { + target: { value: ':00 pm' }, + }); + + expect(handleChange.callCount).to.equal(1); + expect(adapterToUse.isValid(handleChange.lastCall.args[0])).to.equal(false); + + // call `onChange` with a valid time + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '07:00 pm' }, + }); + + expect(handleChange.callCount).to.equal(2); + expect(handleChange.lastCall.args[0]).toEqualDateTime( + adapterToUse.date('2019-01-01T19:00:00.000'), + ); + }); + context('input validation', () => { const shouldDisableTime: TimePickerProps['shouldDisableTime'] = (value) => value === 10; diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx index a157d14902f7..030914a0da1d 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx @@ -18,6 +18,13 @@ const valueManager: PickerStateValueManager = { parseInput: parsePickerInputValue, areValuesEqual: (utils: MuiPickersAdapter, a: unknown, b: unknown) => utils.isEqual(a, b), + valueReducer: (utils, prevValue, newValue) => { + if (prevValue == null) { + return newValue; + } + + return utils.mergeDateAndTime(prevValue, newValue); + }, }; export interface DesktopTimePickerProps diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx index e12caa65ad69..b7d95ca09dc6 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx @@ -15,6 +15,13 @@ const valueManager: PickerStateValueManager = { parseInput: parsePickerInputValue, areValuesEqual: (utils: MuiPickersAdapter, a: unknown, b: unknown) => utils.isEqual(a, b), + valueReducer: (utils, prevValue, newValue) => { + if (prevValue == null) { + return newValue; + } + + return utils.mergeDateAndTime(prevValue, newValue); + }, }; export interface MobileTimePickerProps diff --git a/packages/x-date-pickers/src/internals/hooks/useMaskedInput.tsx b/packages/x-date-pickers/src/internals/hooks/useMaskedInput.tsx index 4c9be132c889..afeb66f5b29c 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMaskedInput.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useMaskedInput.tsx @@ -79,6 +79,7 @@ export const useMaskedInput = ({ setInnerInputValue(finalString); const date = finalString === null ? null : utils.parse(finalString, inputFormat); + if (ignoreInvalidInputs && !utils.isValid(date)) { return; } diff --git a/packages/x-date-pickers/src/internals/hooks/usePickerState.ts b/packages/x-date-pickers/src/internals/hooks/usePickerState.ts index 980fd67ea7e3..c261c59dfde9 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePickerState.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePickerState.ts @@ -12,6 +12,11 @@ export interface PickerStateValueManager { ) => boolean; emptyValue: TDateValue; parseInput: (utils: MuiPickersAdapter, value: TInputValue) => TDateValue; + valueReducer?: ( + utils: MuiPickersAdapter, + prevValue: TDateValue | null, + value: TDateValue, + ) => TDateValue; } export type PickerSelectionState = 'partial' | 'shallow' | 'finish'; @@ -49,7 +54,21 @@ export const usePickerState = ( return { committed: date, draft: date }; } - const parsedDateValue = valueManager.parseInput(utils, value); + const parsedDateValue = React.useMemo( + () => valueManager.parseInput(utils, value), + [valueManager, utils, value], + ); + + const [lastValidDateValue, setLastValidDateValue] = React.useState( + parsedDateValue, + ); + + React.useEffect(() => { + if (parsedDateValue != null) { + setLastValidDateValue(parsedDateValue); + } + }, [parsedDateValue]); + const [draftState, dispatch] = React.useReducer( (state: Draftable, action: DraftAction): Draftable => { switch (action.type) { @@ -141,14 +160,24 @@ export const usePickerState = ( [acceptDate, disableCloseOnSelect, isMobileKeyboardViewOpen, draftState.draft], ); + const handleInputChange = React.useCallback( + (date: TDateValue, keyboardInputValue?: string) => { + const cleanDate = valueManager.valueReducer + ? valueManager.valueReducer(utils, lastValidDateValue, date) + : date; + onChange(cleanDate, keyboardInputValue); + }, + [onChange, valueManager, lastValidDateValue, utils], + ); + const inputProps = React.useMemo( () => ({ - onChange, + onChange: handleInputChange, open: isOpen, rawValue: value, openPicker: () => setIsOpen(true), }), - [onChange, isOpen, value, setIsOpen], + [handleInputChange, isOpen, value, setIsOpen], ); const pickerState = { pickerProps, inputProps, wrapperProps };