From 58f1859119c4df451bf80db1c40d40ce0c9c1db8 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Thu, 22 Feb 2024 14:38:40 +0100 Subject: [PATCH] [fields] Use the `PickersTextField` component in the fields (#10649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Flavien DELANGLE Signed-off-by: Lukas Co-authored-by: Lukas Co-authored-by: José Rodolfo Freitas --- ...rWithBrowserField.js => BrowserV6Field.js} | 9 +- ...ithBrowserField.tsx => BrowserV6Field.tsx} | 20 +- ...tsx.preview => BrowserV6Field.tsx.preview} | 0 ...ld.js => BrowserV6MultiInputRangeField.js} | 9 +- ....tsx => BrowserV6MultiInputRangeField.tsx} | 21 +- ...BrowserV6MultiInputRangeField.tsx.preview} | 0 ...d.js => BrowserV6SingleInputRangeField.js} | 9 +- ...tsx => BrowserV6SingleInputRangeField.tsx} | 49 +- ...rowserV6SingleInputRangeField.tsx.preview} | 0 .../custom-field/BrowserV7Field.js | 119 ++ .../custom-field/BrowserV7Field.tsx | 156 ++ .../custom-field/BrowserV7Field.tsx.preview | 5 + .../BrowserV7MultiInputRangeField.js | 172 +++ .../BrowserV7MultiInputRangeField.tsx | 221 +++ .../BrowserV7MultiInputRangeField.tsx.preview | 1 + .../BrowserV7SingleInputRangeField.js | 168 +++ .../BrowserV7SingleInputRangeField.tsx | 218 +++ ...BrowserV7SingleInputRangeField.tsx.preview | 5 + .../DateRangePickerWithButtonField.tsx | 6 +- .../{PickerWithJoyField.js => JoyV6Field.js} | 11 +- ...{PickerWithJoyField.tsx => JoyV6Field.tsx} | 18 +- .../custom-field/JoyV6Field.tsx.preview | 12 + ...yField.js => JoyV6MultiInputRangeField.js} | 5 +- ...ield.tsx => JoyV6MultiInputRangeField.tsx} | 15 +- ... => JoyV6MultiInputRangeField.tsx.preview} | 0 ...Field.js => JoyV6SingleInputRangeField.js} | 9 +- ...eld.tsx => JoyV6SingleInputRangeField.tsx} | 41 +- ...=> JoyV6SingleInputRangeField.tsx.preview} | 0 .../custom-field/MaterialV6Field.js | 17 + .../custom-field/MaterialV6Field.tsx | 17 + .../custom-field/MaterialV6Field.tsx.preview | 2 + .../custom-field/MaterialV6FieldWrapped.js | 22 + .../custom-field/MaterialV6FieldWrapped.tsx | 24 + .../MaterialV6FieldWrapped.tsx.preview | 2 + .../custom-field/MaterialV7Field.js | 17 + .../custom-field/MaterialV7Field.tsx | 17 + .../custom-field/MaterialV7Field.tsx.preview | 2 + .../custom-field/MaterialV7FieldWrapped.js | 28 + .../custom-field/MaterialV7FieldWrapped.tsx | 33 + .../MaterialV7FieldWrapped.tsx.preview | 8 + .../PickerWithAutocompleteField.js | 4 +- .../PickerWithAutocompleteField.tsx | 7 +- .../custom-field/PickerWithButtonField.js | 4 +- .../custom-field/PickerWithButtonField.tsx | 7 +- ...s => SingleInputDateRangePickerWrapped.js} | 2 +- ... => SingleInputDateRangePickerWrapped.tsx} | 5 +- ...leInputDateRangePickerWrapped.tsx.preview} | 0 .../date-pickers/custom-field/custom-field.md | 77 +- .../fields/BasicV6DOMStructure.js | 15 + .../fields/BasicV6DOMStructure.tsx | 15 + .../fields/BasicV6DOMStructure.tsx.preview | 1 + .../fields/BasicV7DOMStructure.js | 15 + .../fields/BasicV7DOMStructure.tsx | 15 + .../fields/BasicV7DOMStructure.tsx.preview | 1 + docs/data/date-pickers/fields/fields.md | 195 +++ .../migration-pickers-v6.md | 63 + docs/pages/x/api/date-pickers/date-field.json | 2 +- .../pages/x/api/date-pickers/date-picker.json | 6 +- .../x/api/date-pickers/date-range-picker.json | 6 +- .../x/api/date-pickers/date-time-field.json | 2 +- .../x/api/date-pickers/date-time-picker.json | 6 +- .../date-pickers/date-time-range-picker.json | 10 +- .../api/date-pickers/desktop-date-picker.json | 6 +- .../desktop-date-range-picker.json | 6 +- .../desktop-date-time-picker.json | 6 +- .../desktop-date-time-range-picker.json | 10 +- .../api/date-pickers/desktop-time-picker.json | 6 +- .../api/date-pickers/mobile-date-picker.json | 6 +- .../mobile-date-range-picker.json | 6 +- .../date-pickers/mobile-date-time-picker.json | 6 +- .../mobile-date-time-range-picker.json | 10 +- .../api/date-pickers/mobile-time-picker.json | 6 +- .../multi-input-date-range-field.json | 3 +- .../multi-input-date-time-range-field.json | 3 +- .../multi-input-time-range-field.json | 3 +- .../single-input-date-range-field.json | 2 +- .../single-input-date-time-range-field.json | 2 +- .../single-input-time-range-field.json | 2 +- docs/pages/x/api/date-pickers/time-field.json | 2 +- .../pages/x/api/date-pickers/time-picker.json | 6 +- .../date-pickers/date-field/date-field.json | 2 +- .../date-pickers/date-picker/date-picker.json | 4 +- .../date-range-picker/date-range-picker.json | 4 +- .../date-time-field/date-time-field.json | 2 +- .../date-time-picker/date-time-picker.json | 4 +- .../date-time-range-picker.json | 4 +- .../desktop-date-picker.json | 4 +- .../desktop-date-range-picker.json | 4 +- .../desktop-date-time-picker.json | 4 +- .../desktop-date-time-range-picker.json | 4 +- .../desktop-time-picker.json | 4 +- .../mobile-date-picker.json | 4 +- .../mobile-date-range-picker.json | 4 +- .../mobile-date-time-picker.json | 4 +- .../mobile-date-time-range-picker.json | 4 +- .../mobile-time-picker.json | 4 +- .../multi-input-date-range-field.json | 5 +- .../multi-input-date-time-range-field.json | 5 +- .../multi-input-time-range-field.json | 5 +- .../date-pickers/pickers-sections-list.json | 22 + .../single-input-date-range-field.json | 2 +- .../single-input-date-time-range-field.json | 2 +- .../single-input-time-range-field.json | 2 +- .../date-pickers/time-field/time-field.json | 2 +- .../date-pickers/time-picker/time-picker.json | 4 +- .../DateRangePicker/DateRangePicker.test.tsx | 30 +- .../src/DateRangePicker/DateRangePicker.tsx | 31 +- .../DateRangePicker/DateRangePicker.types.ts | 18 +- .../DateTimeRangePicker.tsx | 38 +- .../DateTimeRangePicker.types.ts | 18 +- .../DesktopDateRangePicker.tsx | 39 +- .../DesktopDateRangePicker.types.ts | 21 +- .../tests/DesktopDateRangePicker.test.tsx | 119 +- .../describes.DesktopDateRangePicker.test.tsx | 62 +- .../DesktopDateTimeRangePicker.tsx | 63 +- .../DesktopDateTimeRangePicker.types.ts | 25 +- .../MobileDateRangePicker.tsx | 39 +- .../MobileDateRangePicker.types.ts | 18 +- .../tests/MobileDateRangePicker.test.tsx | 32 +- .../describes.MobileDateRangePicker.test.tsx | 30 +- .../MobileDateTimeRangePicker.tsx | 63 +- .../MobileDateTimeRangePicker.types.ts | 25 +- .../MultiInputDateRangeField.tsx | 89 +- .../MultiInputDateRangeField.types.ts | 65 +- ...tiInputDateRangeField.conformance.test.tsx | 2 +- ...ltiInputDateRangeField.validation.test.tsx | 16 +- .../MultiInputDateTimeRangeField.tsx | 89 +- .../MultiInputDateTimeRangeField.types.ts | 72 +- ...putDateTimeRangeField.conformance.test.tsx | 2 +- ...nputDateTimeRangeField.validation.test.tsx | 18 +- .../MultiInputTimeRangeField.tsx | 89 +- .../MultiInputTimeRangeField.types.ts | 76 +- ...tiInputTimeRangeField.conformance.test.tsx | 2 +- ...ltiInputTimeRangeField.validation.test.tsx | 18 +- .../SingleInputDateRangeField.tsx | 59 +- .../SingleInputDateRangeField.types.ts | 88 +- .../src/SingleInputDateRangeField/index.ts | 1 - ...scribes.SingleInputDateRangeField.test.tsx | 6 +- ...editing.SingleInputDateRangeField.test.tsx | 486 +++++-- ...lection.SingleInputDateRangeField.test.tsx | 268 +++- .../useSingleInputDateRangeField.ts | 51 +- .../SingleInputDateTimeRangeField.tsx | 61 +- .../SingleInputDateTimeRangeField.types.ts | 84 +- .../SingleInputDateTimeRangeField/index.ts | 1 - ...bes.SingleInputDateTimeRangeField.test.tsx | 2 +- .../useSingleInputDateTimeRangeField.ts | 63 +- .../SingleInputTimeRangeField.tsx | 57 +- .../SingleInputTimeRangeField.types.ts | 87 +- .../src/SingleInputTimeRangeField/index.ts | 1 - ...scribes.SingleInputTimeRangeField.test.tsx | 2 +- .../useSingleInputTimeRangeField.ts | 49 +- packages/x-date-pickers-pro/src/index.ts | 5 - .../internals/hooks/models/useRangePicker.ts | 13 +- .../useDesktopRangePicker.tsx | 85 +- .../useDesktopRangePicker.types.ts | 18 +- .../hooks/useEnrichedRangePickerFieldProps.ts | 179 ++- .../useMobileRangePicker.tsx | 84 +- .../useMobileRangePicker.types.ts | 18 +- .../useMultiInputFieldSelectedSections.ts | 74 + .../useMultiInputDateRangeField.ts | 67 +- .../useMultiInputDateTimeRangeField.ts | 96 +- .../useMultiInputRangeField.types.ts | 11 +- .../useMultiInputTimeRangeField.ts | 90 +- .../src/internals/hooks/useRangePosition.ts | 15 +- .../useStaticRangePicker.tsx | 4 +- .../useStaticRangePicker.types.ts | 3 +- .../src/internals/models/dateRange.ts | 40 +- .../src/internals/models/dateTimeRange.ts | 34 +- .../src/internals/models/fields.ts | 67 - .../src/internals/models/index.ts | 1 - .../src/internals/models/timeRange.ts | 30 +- .../src/internals/utils/date-fields-utils.ts | 2 +- .../src/internals/utils/valueManagers.ts | 41 +- .../x-date-pickers-pro/src/models/fields.ts | 103 ++ .../x-date-pickers-pro/src/models/index.ts | 1 + .../src/themeAugmentation/props.d.ts | 24 +- .../AdapterDateFns/AdapterDateFns.test.tsx | 29 +- .../AdapterDateFnsJalali.test.tsx | 31 +- .../src/AdapterDayjs/AdapterDayjs.test.tsx | 29 +- .../src/AdapterLuxon/AdapterLuxon.test.tsx | 54 +- .../src/AdapterMoment/AdapterMoment.test.tsx | 29 +- .../AdapterMomentHijri.test.tsx | 29 +- .../AdapterMomentJalaali.test.tsx | 29 +- .../src/DateField/DateField.tsx | 53 +- .../src/DateField/DateField.types.ts | 76 +- .../x-date-pickers/src/DateField/index.ts | 1 - .../tests/describes.DateField.test.tsx | 33 +- .../tests/editing.DateField.test.tsx | 1275 +++++++++++++---- .../DateField/tests/format.DateField.test.tsx | 254 +++- .../tests/selection.DateField.test.tsx | 309 ++-- .../src/DateField/useDateField.ts | 52 +- .../src/DatePicker/DatePicker.tsx | 31 +- .../src/DatePicker/DatePicker.types.ts | 18 +- .../src/DatePicker/tests/DatePicker.test.tsx | 5 +- .../src/DateTimeField/DateTimeField.tsx | 53 +- .../src/DateTimeField/DateTimeField.types.ts | 76 +- .../x-date-pickers/src/DateTimeField/index.ts | 1 - .../tests/describes.DateTimeField.test.tsx | 39 +- .../tests/editing.DateTimeField.test.tsx | 96 +- .../tests/timezone.DateTimeField.test.tsx | 100 +- .../src/DateTimeField/useDateTimeField.ts | 60 +- .../src/DateTimePicker/DateTimePicker.tsx | 31 +- .../DateTimePicker/DateTimePicker.types.ts | 29 +- .../tests/DateTimePicker.test.tsx | 5 +- .../DesktopDatePicker/DesktopDatePicker.tsx | 45 +- .../DesktopDatePicker.types.ts | 18 +- .../tests/DesktopDatePicker.test.tsx | 24 +- .../describes.DesktopDatePicker.test.tsx | 25 +- .../tests/field.DesktopDatePicker.test.tsx | 108 +- .../DesktopDateTimePicker.tsx | 39 +- .../DesktopDateTimePicker.types.ts | 22 +- .../describes.DesktopDateTimePicker.test.tsx | 65 +- .../field.DesktopDateTimePicker.test.tsx | 48 +- .../DesktopTimePicker/DesktopTimePicker.tsx | 39 +- .../DesktopTimePicker.types.ts | 22 +- .../describes.DesktopTimePicker.test.tsx | 41 +- .../tests/field.DesktopTimePicker.test.tsx | 48 +- .../src/MobileDatePicker/MobileDatePicker.tsx | 46 +- .../MobileDatePicker.types.ts | 18 +- .../tests/MobileDatePicker.test.tsx | 74 +- .../tests/describes.MobileDatePicker.test.tsx | 25 +- .../MobileDateTimePicker.tsx | 39 +- .../MobileDateTimePicker.types.ts | 12 +- .../tests/MobileDateTimePicker.test.tsx | 20 +- .../describes.MobileDateTimePicker.test.tsx | 31 +- .../tests/field.MobileDateTimePicker.test.tsx | 26 +- .../src/MobileTimePicker/MobileTimePicker.tsx | 40 +- .../MobileTimePicker.types.ts | 12 +- .../tests/MobileTimePicker.test.tsx | 9 +- .../tests/describes.MobileTimePicker.test.tsx | 27 +- .../tests/field.MobileTimePicker.test.tsx | 26 +- .../PickersSectionList/PickersSectionList.tsx | 3 +- .../PickersFilledInput/PickersFilledInput.tsx | 9 +- .../PickersInput/PickersInput.tsx | 9 +- .../PickersInputBase/PickersInputBase.tsx | 15 +- .../PickersInputBase.types.ts | 1 + .../PickersOutlinedInput.tsx | 9 +- .../src/PickersTextField/PickersTextField.tsx | 11 +- .../StaticTimePicker.test.tsx | 2 +- .../src/TimeField/TimeField.tsx | 53 +- .../src/TimeField/TimeField.types.ts | 76 +- .../x-date-pickers/src/TimeField/index.ts | 1 - .../tests/describes.TimeField.test.tsx | 27 +- .../tests/editing.TimeField.test.tsx | 210 ++- .../src/TimeField/useTimeField.ts | 51 +- .../src/TimePicker/TimePicker.tsx | 31 +- .../src/TimePicker/TimePicker.types.ts | 21 +- .../src/TimePicker/tests/TimePicker.test.tsx | 5 +- .../YearCalendar/tests/YearCalendar.test.tsx | 2 +- packages/x-date-pickers/src/hooks/index.tsx | 7 +- .../src/hooks/useClearableField.tsx | 16 +- .../src/internals/demo/DemoContainer.tsx | 18 +- .../internals/hooks/defaultizedFieldProps.ts | 95 ++ .../useDesktopPicker/useDesktopPicker.tsx | 80 +- .../useDesktopPicker.types.ts | 47 +- .../hooks/useField/buildSectionsFromFormat.ts | 316 ++++ .../src/internals/hooks/useField/index.ts | 7 +- .../src/internals/hooks/useField/useField.ts | 497 ++----- .../hooks/useField/useField.types.ts | 236 ++- .../hooks/useField/useField.utils.ts | 344 +---- .../useField/useFieldCharacterEditing.ts | 28 +- .../internals/hooks/useField/useFieldState.ts | 195 +-- .../hooks/useField/useFieldV6TextField.ts | 445 ++++++ .../hooks/useField/useFieldV7TextField.ts | 493 +++++++ .../hooks/useMobilePicker/useMobilePicker.tsx | 69 +- .../useMobilePicker/useMobilePicker.types.ts | 44 +- .../internals/hooks/usePicker/usePicker.ts | 7 +- .../hooks/usePicker/usePicker.types.ts | 11 +- .../hooks/usePicker/usePickerValue.ts | 31 +- .../hooks/usePicker/usePickerValue.types.ts | 20 +- .../hooks/usePicker/usePickerViews.ts | 16 +- .../hooks/useStaticPicker/useStaticPicker.tsx | 1 + .../useStaticPicker/useStaticPicker.types.ts | 1 + .../x-date-pickers/src/internals/index.ts | 14 +- .../src/internals/models/fields.ts | 24 +- .../src/internals/models/helpers.ts | 6 + .../models/props/basePickerProps.tsx | 17 +- ...nvertFieldResponseIntoMuiTextFieldProps.ts | 18 +- .../src/internals/utils/fields.ts | 4 +- .../src/internals/utils/valueManagers.ts | 18 +- packages/x-date-pickers/src/models/fields.ts | 129 +- .../tests/fieldKeyboardInteraction.test.tsx | 177 ++- .../src/themeAugmentation/props.d.ts | 6 +- scripts/x-date-pickers-pro.exports.json | 27 +- scripts/x-date-pickers.exports.json | 15 +- .../DatePicker/BasicDesktopDatePicker.tsx | 6 +- .../BasicDesktopDateRangePicker.tsx | 2 +- .../DatePicker/BasicDesktopDateTimePicker.tsx | 2 +- .../DatePicker/BasicMobileDatePicker.tsx | 2 +- .../DesktopDateTimePickerNoTimeRenderers.tsx | 1 + test/e2e/index.test.ts | 132 +- test/utils/pickers/assertions.ts | 13 +- .../pickers/describePicker/describePicker.tsx | 4 +- .../describeRangeValidation.types.ts | 3 +- .../testDayViewRangeValidation.tsx | 14 +- .../testTextFieldKeyboardRangeValidation.tsx | 90 +- .../testTextFieldRangeValidation.tsx | 30 +- .../testDayViewValidation.tsx | 12 +- .../testTextFieldValidation.tsx | 116 +- .../testYearViewValidation.tsx | 52 +- .../pickers/describeValue/describeValue.tsx | 31 +- .../describeValue/describeValue.types.ts | 7 +- .../testControlledUnControlled.tsx | 162 ++- .../describeValue/testPickerActionBar.tsx | 16 +- .../testPickerOpenCloseLifeCycle.tsx | 198 ++- .../pickers/describeValue/testShortcuts.tsx | 3 + test/utils/pickers/fields.tsx | 281 +++- test/utils/pickers/openPicker.ts | 25 +- 308 files changed, 10345 insertions(+), 4474 deletions(-) rename docs/data/date-pickers/custom-field/{PickerWithBrowserField.js => BrowserV6Field.js} (89%) rename docs/data/date-pickers/custom-field/{PickerWithBrowserField.tsx => BrowserV6Field.tsx} (84%) rename docs/data/date-pickers/custom-field/{PickerWithBrowserField.tsx.preview => BrowserV6Field.tsx.preview} (100%) rename docs/data/date-pickers/custom-field/{RangePickerWithBrowserField.js => BrowserV6MultiInputRangeField.js} (91%) rename docs/data/date-pickers/custom-field/{RangePickerWithBrowserField.tsx => BrowserV6MultiInputRangeField.tsx} (88%) rename docs/data/date-pickers/custom-field/{RangePickerWithBrowserField.tsx.preview => BrowserV6MultiInputRangeField.tsx.preview} (100%) rename docs/data/date-pickers/custom-field/{RangePickerWithSingleInputBrowserField.js => BrowserV6SingleInputRangeField.js} (92%) rename docs/data/date-pickers/custom-field/{RangePickerWithSingleInputBrowserField.tsx => BrowserV6SingleInputRangeField.tsx} (79%) rename docs/data/date-pickers/custom-field/{RangePickerWithSingleInputBrowserField.tsx.preview => BrowserV6SingleInputRangeField.tsx.preview} (100%) create mode 100644 docs/data/date-pickers/custom-field/BrowserV7Field.js create mode 100644 docs/data/date-pickers/custom-field/BrowserV7Field.tsx create mode 100644 docs/data/date-pickers/custom-field/BrowserV7Field.tsx.preview create mode 100644 docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js create mode 100644 docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx create mode 100644 docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx.preview create mode 100644 docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js create mode 100644 docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx create mode 100644 docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx.preview rename docs/data/date-pickers/custom-field/{PickerWithJoyField.js => JoyV6Field.js} (92%) rename docs/data/date-pickers/custom-field/{PickerWithJoyField.tsx => JoyV6Field.tsx} (89%) create mode 100644 docs/data/date-pickers/custom-field/JoyV6Field.tsx.preview rename docs/data/date-pickers/custom-field/{RangePickerWithJoyField.js => JoyV6MultiInputRangeField.js} (96%) rename docs/data/date-pickers/custom-field/{RangePickerWithJoyField.tsx => JoyV6MultiInputRangeField.tsx} (94%) rename docs/data/date-pickers/custom-field/{RangePickerWithJoyField.tsx.preview => JoyV6MultiInputRangeField.tsx.preview} (100%) rename docs/data/date-pickers/custom-field/{RangePickerWithSingleInputJoyField.js => JoyV6SingleInputRangeField.js} (94%) rename docs/data/date-pickers/custom-field/{RangePickerWithSingleInputJoyField.tsx => JoyV6SingleInputRangeField.tsx} (84%) rename docs/data/date-pickers/custom-field/{RangePickerWithSingleInputJoyField.tsx.preview => JoyV6SingleInputRangeField.tsx.preview} (100%) create mode 100644 docs/data/date-pickers/custom-field/MaterialV6Field.js create mode 100644 docs/data/date-pickers/custom-field/MaterialV6Field.tsx create mode 100644 docs/data/date-pickers/custom-field/MaterialV6Field.tsx.preview create mode 100644 docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.js create mode 100644 docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.tsx create mode 100644 docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.tsx.preview create mode 100644 docs/data/date-pickers/custom-field/MaterialV7Field.js create mode 100644 docs/data/date-pickers/custom-field/MaterialV7Field.tsx create mode 100644 docs/data/date-pickers/custom-field/MaterialV7Field.tsx.preview create mode 100644 docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.js create mode 100644 docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.tsx create mode 100644 docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.tsx.preview rename docs/data/date-pickers/custom-field/{WrappedSingleInputDateRangePicker.js => SingleInputDateRangePickerWrapped.js} (93%) rename docs/data/date-pickers/custom-field/{WrappedSingleInputDateRangePicker.tsx => SingleInputDateRangePickerWrapped.tsx} (88%) rename docs/data/date-pickers/custom-field/{WrappedSingleInputDateRangePicker.tsx.preview => SingleInputDateRangePickerWrapped.tsx.preview} (100%) create mode 100644 docs/data/date-pickers/fields/BasicV6DOMStructure.js create mode 100644 docs/data/date-pickers/fields/BasicV6DOMStructure.tsx create mode 100644 docs/data/date-pickers/fields/BasicV6DOMStructure.tsx.preview create mode 100644 docs/data/date-pickers/fields/BasicV7DOMStructure.js create mode 100644 docs/data/date-pickers/fields/BasicV7DOMStructure.tsx create mode 100644 docs/data/date-pickers/fields/BasicV7DOMStructure.tsx.preview create mode 100644 docs/translations/api-docs/date-pickers/pickers-sections-list.json create mode 100644 packages/x-date-pickers-pro/src/internals/hooks/useMultiInputFieldSelectedSections.ts delete mode 100644 packages/x-date-pickers-pro/src/internals/models/fields.ts create mode 100644 packages/x-date-pickers-pro/src/models/fields.ts create mode 100644 packages/x-date-pickers/src/internals/hooks/defaultizedFieldProps.ts create mode 100644 packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts create mode 100644 packages/x-date-pickers/src/internals/hooks/useField/useFieldV6TextField.ts create mode 100644 packages/x-date-pickers/src/internals/hooks/useField/useFieldV7TextField.ts diff --git a/docs/data/date-pickers/custom-field/PickerWithBrowserField.js b/docs/data/date-pickers/custom-field/BrowserV6Field.js similarity index 89% rename from docs/data/date-pickers/custom-field/PickerWithBrowserField.js rename to docs/data/date-pickers/custom-field/BrowserV6Field.js index b5a8aa1a476a6..f3a8f6cc98d17 100644 --- a/docs/data/date-pickers/custom-field/PickerWithBrowserField.js +++ b/docs/data/date-pickers/custom-field/BrowserV6Field.js @@ -10,6 +10,8 @@ import { useClearableField } from '@mui/x-date-pickers/hooks'; const BrowserField = React.forwardRef((props, ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, disabled, id, label, @@ -41,7 +43,10 @@ const BrowserField = React.forwardRef((props, ref) => { const BrowserDateField = React.forwardRef((props, ref) => { const { slots, slotProps, ...textFieldProps } = props; - const fieldResponse = useDateField(textFieldProps); + const fieldResponse = useDateField({ + ...textFieldProps, + enableAccessibleFieldDOMStructure: false, + }); /* If you don't need a clear button, you can skip the use of this hook */ const processedFieldProps = useClearableField({ @@ -63,7 +68,7 @@ const BrowserDatePicker = React.forwardRef((props, ref) => { ); }); -export default function PickerWithBrowserField() { +export default function BrowserV6Field() { return ( , 'size'> { @@ -29,6 +29,7 @@ interface BrowserFieldProps focused?: boolean; ownerState?: any; sx?: any; + enableAccessibleFieldDOMStructure: boolean; } type BrowserFieldComponent = (( @@ -38,6 +39,9 @@ type BrowserFieldComponent = (( const BrowserField = React.forwardRef( (props: BrowserFieldProps, ref: React.Ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, + disabled, id, label, @@ -68,11 +72,12 @@ const BrowserField = React.forwardRef( ) as BrowserFieldComponent; interface BrowserDateFieldProps - extends UseDateFieldProps, + extends UseDateFieldProps, BaseSingleInputFieldProps< Dayjs | null, Dayjs, FieldSection, + false, DateValidationError > {} @@ -80,7 +85,10 @@ const BrowserDateField = React.forwardRef( (props: BrowserDateFieldProps, ref: React.Ref) => { const { slots, slotProps, ...textFieldProps } = props; - const fieldResponse = useDateField(textFieldProps); + const fieldResponse = useDateField({ + ...textFieldProps, + enableAccessibleFieldDOMStructure: false, + }); /* If you don't need a clear button, you can skip the use of this hook */ const processedFieldProps = useClearableField({ @@ -94,9 +102,9 @@ const BrowserDateField = React.forwardRef( ); const BrowserDatePicker = React.forwardRef( - (props: DatePickerProps, ref: React.Ref) => { + (props: DatePickerProps, ref: React.Ref) => { return ( - ref={ref} {...props} slots={{ ...props.slots, field: BrowserDateField }} @@ -105,7 +113,7 @@ const BrowserDatePicker = React.forwardRef( }, ); -export default function PickerWithBrowserField() { +export default function BrowserV6Field() { return ( { const { + // Should be ignored + enableAccessibleFieldDOMStructure, disabled, id, label, @@ -57,6 +59,8 @@ const BrowserMultiInputDateRangeField = React.forwardRef((props, ref) => { selectedSections, onSelectedSectionsChange, className, + unstableStartFieldRef, + unstableEndFieldRef, } = props; const startTextFieldProps = useSlotProps({ @@ -87,9 +91,12 @@ const BrowserMultiInputDateRangeField = React.forwardRef((props, ref) => { disablePast, selectedSections, onSelectedSectionsChange, + enableAccessibleFieldDOMStructure: false, }, startTextFieldProps, endTextFieldProps, + unstableStartFieldRef, + unstableEndFieldRef, }); return ( @@ -117,7 +124,7 @@ const BrowserDateRangePicker = React.forwardRef((props, ref) => { ); }); -export default function RangePickerWithBrowserField() { +export default function BrowserV6MultiInputRangeField() { return ( diff --git a/docs/data/date-pickers/custom-field/RangePickerWithBrowserField.tsx b/docs/data/date-pickers/custom-field/BrowserV6MultiInputRangeField.tsx similarity index 88% rename from docs/data/date-pickers/custom-field/RangePickerWithBrowserField.tsx rename to docs/data/date-pickers/custom-field/BrowserV6MultiInputRangeField.tsx index 2bdd06ef5d0c5..78049576322c3 100644 --- a/docs/data/date-pickers/custom-field/RangePickerWithBrowserField.tsx +++ b/docs/data/date-pickers/custom-field/BrowserV6MultiInputRangeField.tsx @@ -15,10 +15,10 @@ import { BaseMultiInputFieldProps, DateRange, DateRangeValidationError, - UseDateRangeFieldProps, MultiInputFieldSlotTextFieldProps, RangeFieldSection, -} from '@mui/x-date-pickers-pro'; +} from '@mui/x-date-pickers-pro/models'; +import { UseDateRangeFieldProps } from '@mui/x-date-pickers-pro'; interface BrowserFieldProps extends Omit, 'size'> { @@ -33,6 +33,7 @@ interface BrowserFieldProps focused?: boolean; ownerState?: any; sx?: any; + enableAccessibleFieldDOMStructure: boolean; } type BrowserFieldComponent = (( @@ -42,6 +43,9 @@ type BrowserFieldComponent = (( const BrowserField = React.forwardRef( (props: BrowserFieldProps, ref: React.Ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, + disabled, id, label, @@ -72,11 +76,12 @@ const BrowserField = React.forwardRef( ) as BrowserFieldComponent; interface BrowserMultiInputDateRangeFieldProps - extends UseDateRangeFieldProps, + extends UseDateRangeFieldProps, BaseMultiInputFieldProps< DateRange, Dayjs, RangeFieldSection, + false, DateRangeValidationError > {} @@ -103,6 +108,8 @@ const BrowserMultiInputDateRangeField = React.forwardRef( selectedSections, onSelectedSectionsChange, className, + unstableStartFieldRef, + unstableEndFieldRef, } = props; const startTextFieldProps = useSlotProps({ @@ -119,6 +126,7 @@ const BrowserMultiInputDateRangeField = React.forwardRef( const fieldResponse = useMultiInputDateRangeField< Dayjs, + false, MultiInputFieldSlotTextFieldProps >({ sharedProps: { @@ -136,9 +144,12 @@ const BrowserMultiInputDateRangeField = React.forwardRef( disablePast, selectedSections, onSelectedSectionsChange, + enableAccessibleFieldDOMStructure: false, }, startTextFieldProps, endTextFieldProps, + unstableStartFieldRef, + unstableEndFieldRef, }); return ( @@ -158,7 +169,7 @@ const BrowserMultiInputDateRangeField = React.forwardRef( ) as BrowserMultiInputDateRangeFieldComponent; const BrowserDateRangePicker = React.forwardRef( - (props: DateRangePickerProps, ref: React.Ref) => { + (props: DateRangePickerProps, ref: React.Ref) => { return ( diff --git a/docs/data/date-pickers/custom-field/RangePickerWithBrowserField.tsx.preview b/docs/data/date-pickers/custom-field/BrowserV6MultiInputRangeField.tsx.preview similarity index 100% rename from docs/data/date-pickers/custom-field/RangePickerWithBrowserField.tsx.preview rename to docs/data/date-pickers/custom-field/BrowserV6MultiInputRangeField.tsx.preview diff --git a/docs/data/date-pickers/custom-field/RangePickerWithSingleInputBrowserField.js b/docs/data/date-pickers/custom-field/BrowserV6SingleInputRangeField.js similarity index 92% rename from docs/data/date-pickers/custom-field/RangePickerWithSingleInputBrowserField.js rename to docs/data/date-pickers/custom-field/BrowserV6SingleInputRangeField.js index a3f68577c8587..5761a6ea83bd0 100644 --- a/docs/data/date-pickers/custom-field/RangePickerWithSingleInputBrowserField.js +++ b/docs/data/date-pickers/custom-field/BrowserV6SingleInputRangeField.js @@ -14,6 +14,8 @@ import { useClearableField } from '@mui/x-date-pickers/hooks'; const BrowserField = React.forwardRef((props, ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, disabled, id, label, @@ -63,7 +65,10 @@ const BrowserSingleInputDateRangeField = React.forwardRef((props, ref) => { ), }; - const fieldResponse = useSingleInputDateRangeField(textFieldProps); + const fieldResponse = useSingleInputDateRangeField({ + ...textFieldProps, + enableAccessibleFieldDOMStructure: false, + }); /* If you don't need a clear button, you can skip the use of this hook */ const processedFieldProps = useClearableField({ @@ -113,7 +118,7 @@ const BrowserSingleInputDateRangePicker = React.forwardRef((props, ref) => { ); }); -export default function RangePickerWithSingleInputBrowserField() { +export default function BrowserV6SingleInputRangeField() { return ( , 'size'> { @@ -31,6 +39,7 @@ interface BrowserFieldProps focused?: boolean; ownerState?: any; sx?: any; + enableAccessibleFieldDOMStructure: boolean; } type BrowserFieldComponent = (( @@ -40,6 +49,9 @@ type BrowserFieldComponent = (( const BrowserField = React.forwardRef( (props: BrowserFieldProps, ref: React.Ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, + disabled, id, label, @@ -70,27 +82,32 @@ const BrowserField = React.forwardRef( ) as BrowserFieldComponent; interface BrowserSingleInputDateRangeFieldProps - extends SingleInputDateRangeFieldProps< - Dayjs, - Omit, 'size'> - > { + extends UseSingleInputDateRangeFieldProps, + BaseSingleInputFieldProps< + DateRange, + Dayjs, + RangeFieldSection, + false, + DateRangeValidationError + > { onAdornmentClick?: () => void; } type BrowserSingleInputDateRangeFieldComponent = (( props: BrowserSingleInputDateRangeFieldProps & React.RefAttributes, -) => React.JSX.Element) & { fieldType?: string }; +) => React.JSX.Element) & { fieldType?: FieldType }; const BrowserSingleInputDateRangeField = React.forwardRef( (props: BrowserSingleInputDateRangeFieldProps, ref: React.Ref) => { const { slots, slotProps, onAdornmentClick, ...other } = props; - const textFieldProps: SingleInputDateRangeFieldProps = useSlotProps({ - elementType: 'input', - externalSlotProps: slotProps?.textField, - externalForwardedProps: other, - ownerState: props as any, - }); + const textFieldProps: SingleInputDateRangeFieldProps = + useSlotProps({ + elementType: 'input', + externalSlotProps: slotProps?.textField, + externalForwardedProps: other, + ownerState: props as any, + }); textFieldProps.InputProps = { ...textFieldProps.InputProps, @@ -103,9 +120,11 @@ const BrowserSingleInputDateRangeField = React.forwardRef( ), }; - const fieldResponse = useSingleInputDateRangeField( - textFieldProps, - ); + const fieldResponse = useSingleInputDateRangeField< + Dayjs, + false, + typeof textFieldProps + >({ ...textFieldProps, enableAccessibleFieldDOMStructure: false }); /* If you don't need a clear button, you can skip the use of this hook */ const processedFieldProps = useClearableField({ @@ -158,7 +177,7 @@ const BrowserSingleInputDateRangePicker = React.forwardRef( }, ); -export default function RangePickerWithSingleInputBrowserField() { +export default function BrowserV6SingleInputRangeField() { return ( { + const { + // Should be ignored + enableAccessibleFieldDOMStructure, + // Should be passed to the PickersSectionList component + elements, + sectionListRef, + contentEditable, + onFocus, + onBlur, + tabIndex, + onInput, + onPaste, + onKeyDown, + // Can be passed to a hidden element + onChange, + value, + // Can be used to render a custom label + label, + // Can be used to style the component + areAllSectionsEmpty, + disabled, + readOnly, + focused, + error, + InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, + // The rest can be passed to the root element + ...other + } = props; + + const handleRef = useForkRef(InputPropsRef, ref); + + return ( + + {startAdornment} + + + + {endAdornment} + + ); +}); + +const BrowserDateField = React.forwardRef((props, ref) => { + const { slots, slotProps, ...textFieldProps } = props; + + const fieldResponse = useDateField({ + ...textFieldProps, + enableAccessibleFieldDOMStructure: true, + }); + + /* If you don't need a clear button, you can skip the use of this hook */ + const processedFieldProps = useClearableField({ + ...fieldResponse, + slots, + slotProps, + }); + + return ; +}); + +const BrowserDatePicker = React.forwardRef((props, ref) => { + return ( + + ); +}); + +export default function BrowserV7Field() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/BrowserV7Field.tsx b/docs/data/date-pickers/custom-field/BrowserV7Field.tsx new file mode 100644 index 0000000000000..a84e535d4340a --- /dev/null +++ b/docs/data/date-pickers/custom-field/BrowserV7Field.tsx @@ -0,0 +1,156 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import useForkRef from '@mui/utils/useForkRef'; +import { styled } from '@mui/material/styles'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DatePicker, DatePickerProps } from '@mui/x-date-pickers/DatePicker'; +import { + unstable_useDateField as useDateField, + UseDateFieldProps, +} from '@mui/x-date-pickers/DateField'; +import { useClearableField } from '@mui/x-date-pickers/hooks'; +import { + BaseSingleInputPickersTextFieldProps, + BaseSingleInputFieldProps, + DateValidationError, + FieldSection, +} from '@mui/x-date-pickers/models'; +import { Unstable_PickersSectionList as PickersSectionList } from '@mui/x-date-pickers/PickersSectionList'; + +const BrowserFieldRoot = styled('div', { name: 'BrowserField', slot: 'Root' })({ + display: 'flex', + alignItems: 'center', +}); + +const BrowserFieldContent = styled('div', { name: 'BrowserField', slot: 'Content' })( + { + border: '1px solid grey', + fontSize: 13.33333, + lineHeight: 'normal', + padding: '1px 2px', + whiteSpace: 'nowrap', + }, +); + +interface BrowserTextFieldProps + extends BaseSingleInputPickersTextFieldProps, + Omit< + React.HTMLAttributes, + keyof BaseSingleInputPickersTextFieldProps + > {} + +const BrowserTextField = React.forwardRef( + (props: BrowserTextFieldProps, ref: React.Ref) => { + const { + // Should be ignored + enableAccessibleFieldDOMStructure, + + // Should be passed to the PickersSectionList component + elements, + sectionListRef, + contentEditable, + onFocus, + onBlur, + tabIndex, + onInput, + onPaste, + onKeyDown, + + // Can be passed to a hidden element + onChange, + value, + + // Can be used to render a custom label + label, + + // Can be used to style the component + areAllSectionsEmpty, + disabled, + readOnly, + focused, + error, + + InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, + + // The rest can be passed to the root element + ...other + } = props; + + const handleRef = useForkRef(InputPropsRef, ref); + + return ( + + {startAdornment} + + + + {endAdornment} + + ); + }, +); + +interface BrowserDateFieldProps + extends UseDateFieldProps, + BaseSingleInputFieldProps< + Dayjs | null, + Dayjs, + FieldSection, + true, + DateValidationError + > {} + +const BrowserDateField = React.forwardRef( + (props: BrowserDateFieldProps, ref: React.Ref) => { + const { slots, slotProps, ...textFieldProps } = props; + + const fieldResponse = useDateField({ + ...textFieldProps, + enableAccessibleFieldDOMStructure: true, + }); + + /* If you don't need a clear button, you can skip the use of this hook */ + const processedFieldProps = useClearableField({ + ...fieldResponse, + slots, + slotProps, + }); + + return ; + }, +); + +const BrowserDatePicker = React.forwardRef( + (props: DatePickerProps, ref: React.Ref) => { + return ( + + ); + }, +); + +export default function BrowserV7Field() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/BrowserV7Field.tsx.preview b/docs/data/date-pickers/custom-field/BrowserV7Field.tsx.preview new file mode 100644 index 0000000000000..0bb9e399d3cb4 --- /dev/null +++ b/docs/data/date-pickers/custom-field/BrowserV7Field.tsx.preview @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js new file mode 100644 index 0000000000000..76d9cceb5c6aa --- /dev/null +++ b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js @@ -0,0 +1,172 @@ +import * as React from 'react'; + +import useForkRef from '@mui/utils/useForkRef'; +import { useSlotProps } from '@mui/base/utils'; +import { styled } from '@mui/material/styles'; +import Stack from '@mui/material/Stack'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker'; +import { unstable_useMultiInputDateRangeField as useMultiInputDateRangeField } from '@mui/x-date-pickers-pro/MultiInputDateRangeField'; +import { Unstable_PickersSectionList as PickersSectionList } from '@mui/x-date-pickers/PickersSectionList'; + +const BrowserFieldRoot = styled('div', { name: 'BrowserField', slot: 'Root' })({ + display: 'flex', + alignItems: 'center', +}); + +const BrowserFieldContent = styled('div', { name: 'BrowserField', slot: 'Content' })( + { + border: '1px solid grey', + fontSize: 13.33333, + lineHeight: 'normal', + padding: '1px 2px', + whiteSpace: 'nowrap', + }, +); + +// This demo uses `BasePickersTextFieldProps` instead of `BaseMultiInputPickersTextFieldProps`, +// That way you can reuse the same `BrowserTextField` for all your pickers, range or not. +const BrowserTextField = React.forwardRef((props, ref) => { + const { + // Should be ignored + enableAccessibleFieldDOMStructure, + // Should be passed to the PickersSectionList component + elements, + sectionListRef, + contentEditable, + onFocus, + onBlur, + tabIndex, + onInput, + onPaste, + onKeyDown, + // Can be passed to a hidden element + onChange, + value, + // Can be used to render a custom label + label, + // Can be used to style the component + areAllSectionsEmpty, + disabled, + readOnly, + focused, + error, + InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, + // The rest can be passed to the root element + ...other + } = props; + + const handleRef = useForkRef(InputPropsRef, ref); + + return ( + + {startAdornment} + + + + {endAdornment} + + ); +}); + +const BrowserMultiInputDateRangeField = React.forwardRef((props, ref) => { + const { + slotProps, + value, + defaultValue, + format, + onChange, + readOnly, + disabled, + onError, + shouldDisableDate, + minDate, + maxDate, + disableFuture, + disablePast, + selectedSections, + onSelectedSectionsChange, + className, + unstableStartFieldRef, + unstableEndFieldRef, + } = props; + + const startTextFieldProps = useSlotProps({ + elementType: 'input', + externalSlotProps: slotProps?.textField, + ownerState: { ...props, position: 'start' }, + }); + + const endTextFieldProps = useSlotProps({ + elementType: 'input', + externalSlotProps: slotProps?.textField, + ownerState: { ...props, position: 'end' }, + }); + + const fieldResponse = useMultiInputDateRangeField({ + sharedProps: { + value, + defaultValue, + format, + onChange, + readOnly, + disabled, + onError, + shouldDisableDate, + minDate, + maxDate, + disableFuture, + disablePast, + selectedSections, + onSelectedSectionsChange, + enableAccessibleFieldDOMStructure: true, + }, + startTextFieldProps, + endTextFieldProps, + unstableStartFieldRef, + unstableEndFieldRef, + }); + + return ( + + + + + + ); +}); + +const BrowserDateRangePicker = React.forwardRef((props, ref) => { + return ( + + ); +}); + +export default function BrowserV7MultiInputRangeField() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx new file mode 100644 index 0000000000000..09e0c7eda30c6 --- /dev/null +++ b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx @@ -0,0 +1,221 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import useForkRef from '@mui/utils/useForkRef'; +import { useSlotProps } from '@mui/base/utils'; +import { styled } from '@mui/material/styles'; +import Stack from '@mui/material/Stack'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { + DateRangePicker, + DateRangePickerProps, +} from '@mui/x-date-pickers-pro/DateRangePicker'; +import { unstable_useMultiInputDateRangeField as useMultiInputDateRangeField } from '@mui/x-date-pickers-pro/MultiInputDateRangeField'; +import { Unstable_PickersSectionList as PickersSectionList } from '@mui/x-date-pickers/PickersSectionList'; +import { + RangeFieldSection, + BaseMultiInputFieldProps, + BasePickersTextFieldProps, + MultiInputFieldSlotTextFieldProps, + DateRangeValidationError, + DateRange, +} from '@mui/x-date-pickers-pro/models'; +import { UseDateRangeFieldProps } from '@mui/x-date-pickers-pro'; + +const BrowserFieldRoot = styled('div', { name: 'BrowserField', slot: 'Root' })({ + display: 'flex', + alignItems: 'center', +}); + +const BrowserFieldContent = styled('div', { name: 'BrowserField', slot: 'Content' })( + { + border: '1px solid grey', + fontSize: 13.33333, + lineHeight: 'normal', + padding: '1px 2px', + whiteSpace: 'nowrap', + }, +); + +interface BrowserTextFieldProps + extends BasePickersTextFieldProps, + Omit< + React.HTMLAttributes, + keyof BasePickersTextFieldProps + > {} + +// This demo uses `BasePickersTextFieldProps` instead of `BaseMultiInputPickersTextFieldProps`, +// That way you can reuse the same `BrowserTextField` for all your pickers, range or not. +const BrowserTextField = React.forwardRef( + (props: BrowserTextFieldProps, ref: React.Ref) => { + const { + // Should be ignored + enableAccessibleFieldDOMStructure, + + // Should be passed to the PickersSectionList component + elements, + sectionListRef, + contentEditable, + onFocus, + onBlur, + tabIndex, + onInput, + onPaste, + onKeyDown, + + // Can be passed to a hidden element + onChange, + value, + + // Can be used to render a custom label + label, + + // Can be used to style the component + areAllSectionsEmpty, + disabled, + readOnly, + focused, + error, + + InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, + + // The rest can be passed to the root element + ...other + } = props; + + const handleRef = useForkRef(InputPropsRef, ref); + + return ( + + {startAdornment} + + + + {endAdornment} + + ); + }, +); + +interface BrowserMultiInputDateRangeFieldProps + extends UseDateRangeFieldProps, + BaseMultiInputFieldProps< + DateRange, + Dayjs, + RangeFieldSection, + true, + DateRangeValidationError + > {} + +type BrowserMultiInputDateRangeFieldComponent = (( + props: BrowserMultiInputDateRangeFieldProps & React.RefAttributes, +) => React.JSX.Element) & { propTypes?: any }; + +const BrowserMultiInputDateRangeField = React.forwardRef( + (props: BrowserMultiInputDateRangeFieldProps, ref: React.Ref) => { + const { + slotProps, + value, + defaultValue, + format, + onChange, + readOnly, + disabled, + onError, + shouldDisableDate, + minDate, + maxDate, + disableFuture, + disablePast, + selectedSections, + onSelectedSectionsChange, + className, + unstableStartFieldRef, + unstableEndFieldRef, + } = props; + + const startTextFieldProps = useSlotProps({ + elementType: 'input', + externalSlotProps: slotProps?.textField, + ownerState: { ...props, position: 'start' }, + }) as MultiInputFieldSlotTextFieldProps; + + const endTextFieldProps = useSlotProps({ + elementType: 'input', + externalSlotProps: slotProps?.textField, + ownerState: { ...props, position: 'end' }, + }) as MultiInputFieldSlotTextFieldProps; + + const fieldResponse = useMultiInputDateRangeField< + Dayjs, + true, + MultiInputFieldSlotTextFieldProps + >({ + sharedProps: { + value, + defaultValue, + format, + onChange, + readOnly, + disabled, + onError, + shouldDisableDate, + minDate, + maxDate, + disableFuture, + disablePast, + selectedSections, + onSelectedSectionsChange, + enableAccessibleFieldDOMStructure: true, + }, + startTextFieldProps, + endTextFieldProps, + unstableStartFieldRef, + unstableEndFieldRef, + }); + + return ( + + + + + + ); + }, +) as BrowserMultiInputDateRangeFieldComponent; + +const BrowserDateRangePicker = React.forwardRef( + (props: DateRangePickerProps, ref: React.Ref) => { + return ( + + ); + }, +); + +export default function BrowserV7MultiInputRangeField() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx.preview b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx.preview new file mode 100644 index 0000000000000..d797406fa9997 --- /dev/null +++ b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js new file mode 100644 index 0000000000000..2eeede71d126d --- /dev/null +++ b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js @@ -0,0 +1,168 @@ +import * as React from 'react'; + +import useForkRef from '@mui/utils/useForkRef'; +import { useSlotProps } from '@mui/base/utils'; +import { styled } from '@mui/material/styles'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import { DateRangeIcon } from '@mui/x-date-pickers/icons'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker'; +import { unstable_useSingleInputDateRangeField as useSingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; +import { useClearableField } from '@mui/x-date-pickers/hooks'; +import { Unstable_PickersSectionList as PickersSectionList } from '@mui/x-date-pickers/PickersSectionList'; + +const BrowserFieldRoot = styled('div', { name: 'BrowserField', slot: 'Root' })({ + display: 'flex', + alignItems: 'center', +}); + +const BrowserFieldContent = styled('div', { name: 'BrowserField', slot: 'Content' })( + { + border: '1px solid grey', + fontSize: 13.33333, + lineHeight: 'normal', + padding: '1px 2px', + whiteSpace: 'nowrap', + }, +); + +const BrowserTextField = React.forwardRef((props, ref) => { + const { + // Should be ignored + enableAccessibleFieldDOMStructure, + // Should be passed to the PickersSectionList component + elements, + sectionListRef, + contentEditable, + onFocus, + onBlur, + tabIndex, + onInput, + onPaste, + onKeyDown, + // Can be passed to a hidden element + onChange, + value, + // Can be used to render a custom label + label, + // Can be used to style the component + areAllSectionsEmpty, + disabled, + readOnly, + focused, + error, + InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, + // The rest can be passed to the root element + ...other + } = props; + + const handleRef = useForkRef(InputPropsRef, ref); + + return ( + + {startAdornment} + + + + {endAdornment} + + ); +}); + +const BrowserSingleInputDateRangeField = React.forwardRef((props, ref) => { + const { slots, slotProps, onAdornmentClick, ...other } = props; + + const textFieldProps = useSlotProps({ + elementType: 'input', + externalSlotProps: slotProps?.textField, + externalForwardedProps: other, + ownerState: props, + }); + + textFieldProps.InputProps = { + ...textFieldProps.InputProps, + endAdornment: ( + + + + + + ), + }; + + const fieldResponse = useSingleInputDateRangeField({ + ...textFieldProps, + enableAccessibleFieldDOMStructure: true, + }); + + /* If you don't need a clear button, you can skip the use of this hook */ + const processedFieldProps = useClearableField({ + ...fieldResponse, + slots, + slotProps, + }); + + return ( + + ); +}); + +BrowserSingleInputDateRangeField.fieldType = 'single-input'; + +const BrowserSingleInputDateRangePicker = React.forwardRef((props, ref) => { + const [isOpen, setIsOpen] = React.useState(false); + + const toggleOpen = () => setIsOpen((currentOpen) => !currentOpen); + + const handleOpen = () => setIsOpen(true); + + const handleClose = () => setIsOpen(false); + + return ( + + ); +}); + +export default function BrowserV7SingleInputRangeField() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx new file mode 100644 index 0000000000000..eac9ebb26854a --- /dev/null +++ b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx @@ -0,0 +1,218 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import useForkRef from '@mui/utils/useForkRef'; +import { useSlotProps } from '@mui/base/utils'; +import { styled } from '@mui/material/styles'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import { DateRangeIcon } from '@mui/x-date-pickers/icons'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { + DateRangePicker, + DateRangePickerProps, +} from '@mui/x-date-pickers-pro/DateRangePicker'; +import { + unstable_useSingleInputDateRangeField as useSingleInputDateRangeField, + UseSingleInputDateRangeFieldProps, +} from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; +import { useClearableField } from '@mui/x-date-pickers/hooks'; +import { Unstable_PickersSectionList as PickersSectionList } from '@mui/x-date-pickers/PickersSectionList'; +import { + BasePickersTextFieldProps, + DateRangeValidationError, + RangeFieldSection, + DateRange, + FieldType, +} from '@mui/x-date-pickers-pro/models'; +import { BaseSingleInputFieldProps } from '@mui/x-date-pickers'; + +const BrowserFieldRoot = styled('div', { name: 'BrowserField', slot: 'Root' })({ + display: 'flex', + alignItems: 'center', +}); + +const BrowserFieldContent = styled('div', { name: 'BrowserField', slot: 'Content' })( + { + border: '1px solid grey', + fontSize: 13.33333, + lineHeight: 'normal', + padding: '1px 2px', + whiteSpace: 'nowrap', + }, +); + +interface BrowserTextFieldProps + extends BasePickersTextFieldProps, + Omit< + React.HTMLAttributes, + keyof BasePickersTextFieldProps + > {} + +const BrowserTextField = React.forwardRef( + (props: BrowserTextFieldProps, ref: React.Ref) => { + const { + // Should be ignored + enableAccessibleFieldDOMStructure, + + // Should be passed to the PickersSectionList component + elements, + sectionListRef, + contentEditable, + onFocus, + onBlur, + tabIndex, + onInput, + onPaste, + onKeyDown, + + // Can be passed to a hidden element + onChange, + value, + + // Can be used to render a custom label + label, + + // Can be used to style the component + areAllSectionsEmpty, + disabled, + readOnly, + focused, + error, + + InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, + + // The rest can be passed to the root element + ...other + } = props; + + const handleRef = useForkRef(InputPropsRef, ref); + + return ( + + {startAdornment} + + + + {endAdornment} + + ); + }, +); + +interface BrowserSingleInputDateRangeFieldProps + extends UseSingleInputDateRangeFieldProps, + BaseSingleInputFieldProps< + DateRange, + Dayjs, + RangeFieldSection, + true, + DateRangeValidationError + > { + onAdornmentClick?: () => void; +} + +type BrowserSingleInputDateRangeFieldComponent = (( + props: BrowserSingleInputDateRangeFieldProps & React.RefAttributes, +) => React.JSX.Element) & { fieldType?: FieldType }; + +const BrowserSingleInputDateRangeField = React.forwardRef( + (props: BrowserSingleInputDateRangeFieldProps, ref: React.Ref) => { + const { slots, slotProps, onAdornmentClick, ...other } = props; + + const textFieldProps: typeof props = useSlotProps({ + elementType: 'input', + externalSlotProps: slotProps?.textField, + externalForwardedProps: other, + ownerState: props as any, + }); + + textFieldProps.InputProps = { + ...textFieldProps.InputProps, + endAdornment: ( + + + + + + ), + }; + + const fieldResponse = useSingleInputDateRangeField< + Dayjs, + true, + typeof textFieldProps + >({ ...textFieldProps, enableAccessibleFieldDOMStructure: true }); + + /* If you don't need a clear button, you can skip the use of this hook */ + const processedFieldProps = useClearableField({ + ...fieldResponse, + slots, + slotProps, + }); + + return ( + + ); + }, +) as BrowserSingleInputDateRangeFieldComponent; + +BrowserSingleInputDateRangeField.fieldType = 'single-input'; + +const BrowserSingleInputDateRangePicker = React.forwardRef( + (props: DateRangePickerProps, ref: React.Ref) => { + const [isOpen, setIsOpen] = React.useState(false); + + const toggleOpen = () => setIsOpen((currentOpen) => !currentOpen); + + const handleOpen = () => setIsOpen(true); + + const handleClose = () => setIsOpen(false); + + return ( + + ); + }, +); + +export default function BrowserV7SingleInputRangeField() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx.preview b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx.preview new file mode 100644 index 0000000000000..bcaf8043948fb --- /dev/null +++ b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx.preview @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx b/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx index 3268f758c80f8..1b894f55e6a15 100644 --- a/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx +++ b/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Dayjs } from 'dayjs'; import Button from '@mui/material/Button'; import useForkRef from '@mui/utils/useForkRef'; -import { DateRange } from '@mui/x-date-pickers-pro/models'; +import { DateRange, FieldType } from '@mui/x-date-pickers-pro/models'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { @@ -17,10 +17,10 @@ interface DateRangeButtonFieldProps extends SingleInputDateRangeFieldProps, -) => React.JSX.Element) & { fieldType?: string }; +) => React.JSX.Element) & { fieldType?: FieldType }; const DateRangeButtonField = React.forwardRef( - (props: DateRangeButtonFieldProps, ref: React.Ref) => { + (props: DateRangeButtonFieldProps, ref: React.Ref) => { const { setOpen, label, diff --git a/docs/data/date-pickers/custom-field/PickerWithJoyField.js b/docs/data/date-pickers/custom-field/JoyV6Field.js similarity index 92% rename from docs/data/date-pickers/custom-field/PickerWithJoyField.js rename to docs/data/date-pickers/custom-field/JoyV6Field.js index c685d14d25b9c..29f4e14ce2918 100644 --- a/docs/data/date-pickers/custom-field/PickerWithJoyField.js +++ b/docs/data/date-pickers/custom-field/JoyV6Field.js @@ -24,6 +24,8 @@ const joyTheme = extendJoyTheme(); const JoyField = React.forwardRef((props, ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, disabled, id, label, @@ -73,7 +75,10 @@ const JoyField = React.forwardRef((props, ref) => { const JoyDateField = React.forwardRef((props, ref) => { const { slots, slotProps, ...textFieldProps } = props; - const fieldResponse = useDateField(textFieldProps); + const fieldResponse = useDateField({ + ...textFieldProps, + enableAccessibleFieldDOMStructure: false, + }); /* If you don't need a clear button, you can skip the use of this hook */ const processedFieldProps = useClearableField({ @@ -82,7 +87,7 @@ const JoyDateField = React.forwardRef((props, ref) => { slotProps, }); - return ; + return ; }); const JoyDatePicker = React.forwardRef((props, ref) => { @@ -118,7 +123,7 @@ function SyncThemeMode({ mode }) { return null; } -export default function PickerWithJoyField() { +export default function JoyV6Field() { const materialTheme = useMaterialTheme(); return ( diff --git a/docs/data/date-pickers/custom-field/PickerWithJoyField.tsx b/docs/data/date-pickers/custom-field/JoyV6Field.tsx similarity index 89% rename from docs/data/date-pickers/custom-field/PickerWithJoyField.tsx rename to docs/data/date-pickers/custom-field/JoyV6Field.tsx index 306a5adec5f66..05c311984ebf4 100644 --- a/docs/data/date-pickers/custom-field/PickerWithJoyField.tsx +++ b/docs/data/date-pickers/custom-field/JoyV6Field.tsx @@ -33,6 +33,7 @@ const joyTheme = extendJoyTheme(); interface JoyFieldProps extends InputProps { label?: React.ReactNode; inputRef?: React.Ref; + enableAccessibleFieldDOMStructure?: boolean; InputProps?: { ref?: React.Ref; endAdornment?: React.ReactNode; @@ -48,6 +49,9 @@ type JoyFieldComponent = (( const JoyField = React.forwardRef( (props: JoyFieldProps, ref: React.Ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, + disabled, id, label, @@ -96,11 +100,12 @@ const JoyField = React.forwardRef( ) as JoyFieldComponent; interface JoyDateFieldProps - extends UseDateFieldProps, + extends UseDateFieldProps, BaseSingleInputFieldProps< Dayjs | null, Dayjs, FieldSection, + false, DateValidationError > {} @@ -108,7 +113,10 @@ const JoyDateField = React.forwardRef( (props: JoyDateFieldProps, ref: React.Ref) => { const { slots, slotProps, ...textFieldProps } = props; - const fieldResponse = useDateField(textFieldProps); + const fieldResponse = useDateField({ + ...textFieldProps, + enableAccessibleFieldDOMStructure: false, + }); /* If you don't need a clear button, you can skip the use of this hook */ const processedFieldProps = useClearableField({ @@ -117,12 +125,12 @@ const JoyDateField = React.forwardRef( slotProps, }); - return ; + return ; }, ); const JoyDatePicker = React.forwardRef( - (props: DatePickerProps, ref: React.Ref) => { + (props: DatePickerProps, ref: React.Ref) => { return ( diff --git a/docs/data/date-pickers/custom-field/JoyV6Field.tsx.preview b/docs/data/date-pickers/custom-field/JoyV6Field.tsx.preview new file mode 100644 index 0000000000000..cff735f8a7af0 --- /dev/null +++ b/docs/data/date-pickers/custom-field/JoyV6Field.tsx.preview @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/RangePickerWithJoyField.js b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js similarity index 96% rename from docs/data/date-pickers/custom-field/RangePickerWithJoyField.js rename to docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js index 200b4229807eb..40b15af9295f9 100644 --- a/docs/data/date-pickers/custom-field/RangePickerWithJoyField.js +++ b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js @@ -27,6 +27,8 @@ const joyTheme = extendJoyTheme(); const JoyField = React.forwardRef((props, ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, disabled, id, label, @@ -148,6 +150,7 @@ const JoyMultiInputDateRangeField = React.forwardRef((props, ref) => { disablePast, selectedSections, onSelectedSectionsChange, + enableAccessibleFieldDOMStructure: false, }, startTextFieldProps, endTextFieldProps, @@ -186,7 +189,7 @@ function SyncThemeMode({ mode }) { return null; } -export default function RangePickerWithJoyField() { +export default function JoyV6MultiInputRangeField() { const materialTheme = useMaterialTheme(); return ( diff --git a/docs/data/date-pickers/custom-field/RangePickerWithJoyField.tsx b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx similarity index 94% rename from docs/data/date-pickers/custom-field/RangePickerWithJoyField.tsx rename to docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx index bf1c1bdd5ca82..0790a3d607365 100644 --- a/docs/data/date-pickers/custom-field/RangePickerWithJoyField.tsx +++ b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx @@ -29,16 +29,17 @@ import { BaseMultiInputFieldProps, DateRange, DateRangeValidationError, - UseDateRangeFieldProps, MultiInputFieldSlotTextFieldProps, RangeFieldSection, -} from '@mui/x-date-pickers-pro'; +} from '@mui/x-date-pickers-pro/models'; +import { UseDateRangeFieldProps } from '@mui/x-date-pickers-pro'; const joyTheme = extendJoyTheme(); interface JoyFieldProps extends InputProps { label?: React.ReactNode; inputRef?: React.Ref; + enableAccessibleFieldDOMStructure?: boolean; InputProps?: { ref?: React.Ref; endAdornment?: React.ReactNode; @@ -53,6 +54,9 @@ type JoyFieldComponent = (( const JoyField = React.forwardRef( (props: JoyFieldProps, ref: React.Ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, + disabled, id, label, @@ -128,11 +132,12 @@ const MultiInputJoyDateRangeFieldSeparator = styled( )({ marginTop: '25px' }); interface JoyMultiInputDateRangeFieldProps - extends UseDateRangeFieldProps, + extends UseDateRangeFieldProps, BaseMultiInputFieldProps< DateRange, Dayjs, RangeFieldSection, + false, DateRangeValidationError > {} @@ -175,6 +180,7 @@ const JoyMultiInputDateRangeField = React.forwardRef( const fieldResponse = useMultiInputDateRangeField< Dayjs, + false, MultiInputFieldSlotTextFieldProps >({ sharedProps: { @@ -192,6 +198,7 @@ const JoyMultiInputDateRangeField = React.forwardRef( disablePast, selectedSections, onSelectedSectionsChange, + enableAccessibleFieldDOMStructure: false, }, startTextFieldProps, endTextFieldProps, @@ -233,7 +240,7 @@ function SyncThemeMode({ mode }: { mode: 'light' | 'dark' }) { return null; } -export default function RangePickerWithJoyField() { +export default function JoyV6MultiInputRangeField() { const materialTheme = useMaterialTheme(); return ( diff --git a/docs/data/date-pickers/custom-field/RangePickerWithJoyField.tsx.preview b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx.preview similarity index 100% rename from docs/data/date-pickers/custom-field/RangePickerWithJoyField.tsx.preview rename to docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx.preview diff --git a/docs/data/date-pickers/custom-field/RangePickerWithSingleInputJoyField.js b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js similarity index 94% rename from docs/data/date-pickers/custom-field/RangePickerWithSingleInputJoyField.js rename to docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js index 1c8d0719826ed..7183f8d9c4a37 100644 --- a/docs/data/date-pickers/custom-field/RangePickerWithSingleInputJoyField.js +++ b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js @@ -27,6 +27,8 @@ const joyTheme = extendJoyTheme(); const JoyField = React.forwardRef((props, ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, disabled, id, label, @@ -77,7 +79,10 @@ const JoySingleInputDateRangeField = React.forwardRef((props, ref) => { ownerState: props, }); - const fieldResponse = useSingleInputDateRangeField(textFieldProps); + const fieldResponse = useSingleInputDateRangeField({ + ...textFieldProps, + enableAccessibleFieldDOMStructure: false, + }); /* If you don't need a clear button, you can skip the use of this hook */ const processedFieldProps = useClearableField({ @@ -152,7 +157,7 @@ function SyncThemeMode({ mode }) { return null; } -export default function RangePickerWithSingleInputJoyField() { +export default function JoyV6SingleInputRangeField() { const materialTheme = useMaterialTheme(); return ( diff --git a/docs/data/date-pickers/custom-field/RangePickerWithSingleInputJoyField.tsx b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx similarity index 84% rename from docs/data/date-pickers/custom-field/RangePickerWithSingleInputJoyField.tsx rename to docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx index 817129b9a4589..d7e26765a1a0b 100644 --- a/docs/data/date-pickers/custom-field/RangePickerWithSingleInputJoyField.tsx +++ b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx @@ -25,15 +25,23 @@ import { } from '@mui/x-date-pickers-pro/DateRangePicker'; import { unstable_useSingleInputDateRangeField as useSingleInputDateRangeField, - SingleInputDateRangeFieldProps, + UseSingleInputDateRangeFieldProps, } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; import { useClearableField } from '@mui/x-date-pickers/hooks'; +import { BaseSingleInputFieldProps } from '@mui/x-date-pickers/models'; +import { + RangeFieldSection, + DateRange, + DateRangeValidationError, + FieldType, +} from '@mui/x-date-pickers-pro/models'; const joyTheme = extendJoyTheme(); interface JoyFieldProps extends InputProps { label?: React.ReactNode; inputRef?: React.Ref; + enableAccessibleFieldDOMStructure?: boolean; InputProps?: { ref?: React.Ref; endAdornment?: React.ReactNode; @@ -48,6 +56,9 @@ type JoyFieldComponent = (( const JoyField = React.forwardRef( (props: JoyFieldProps, ref: React.Ref) => { const { + // Should be ignored + enableAccessibleFieldDOMStructure, + disabled, id, label, @@ -90,31 +101,37 @@ const JoyField = React.forwardRef( ) as JoyFieldComponent; interface JoySingleInputDateRangeFieldProps - extends SingleInputDateRangeFieldProps { + extends UseSingleInputDateRangeFieldProps, + BaseSingleInputFieldProps< + DateRange, + Dayjs, + RangeFieldSection, + false, + DateRangeValidationError + > { onAdornmentClick?: () => void; } type JoySingleInputDateRangeFieldComponent = (( props: JoySingleInputDateRangeFieldProps & React.RefAttributes, -) => React.JSX.Element) & { fieldType?: string }; +) => React.JSX.Element) & { fieldType?: FieldType }; const JoySingleInputDateRangeField = React.forwardRef( (props: JoySingleInputDateRangeFieldProps, ref: React.Ref) => { const { slots, slotProps, onAdornmentClick, ...other } = props; - const textFieldProps: SingleInputDateRangeFieldProps< - Dayjs, - JoyFieldProps & { inputRef: React.Ref } - > = useSlotProps({ + const textFieldProps: JoySingleInputDateRangeFieldProps = useSlotProps({ elementType: FormControl, externalSlotProps: slotProps?.textField, externalForwardedProps: other, ownerState: props as any, }); - const fieldResponse = useSingleInputDateRangeField( - textFieldProps, - ); + const fieldResponse = useSingleInputDateRangeField< + Dayjs, + false, + JoySingleInputDateRangeFieldProps + >({ ...textFieldProps, enableAccessibleFieldDOMStructure: false }); /* If you don't need a clear button, you can skip the use of this hook */ const processedFieldProps = useClearableField({ @@ -145,7 +162,7 @@ const JoySingleInputDateRangeField = React.forwardRef( JoySingleInputDateRangeField.fieldType = 'single-input'; const JoySingleInputDateRangePicker = React.forwardRef( - (props: DateRangePickerProps, ref: React.Ref) => { + (props: DateRangePickerProps, ref: React.Ref) => { const [isOpen, setIsOpen] = React.useState(false); const toggleOpen = (event: React.PointerEvent) => { @@ -192,7 +209,7 @@ function SyncThemeMode({ mode }: { mode: 'light' | 'dark' }) { return null; } -export default function RangePickerWithSingleInputJoyField() { +export default function JoyV6SingleInputRangeField() { const materialTheme = useMaterialTheme(); return ( diff --git a/docs/data/date-pickers/custom-field/RangePickerWithSingleInputJoyField.tsx.preview b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx.preview similarity index 100% rename from docs/data/date-pickers/custom-field/RangePickerWithSingleInputJoyField.tsx.preview rename to docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx.preview diff --git a/docs/data/date-pickers/custom-field/MaterialV6Field.js b/docs/data/date-pickers/custom-field/MaterialV6Field.js new file mode 100644 index 0000000000000..35d4e0ca73ead --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV6Field.js @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; + +export default function MaterialV6Field() { + return ( + + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/MaterialV6Field.tsx b/docs/data/date-pickers/custom-field/MaterialV6Field.tsx new file mode 100644 index 0000000000000..35d4e0ca73ead --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV6Field.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; + +export default function MaterialV6Field() { + return ( + + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/MaterialV6Field.tsx.preview b/docs/data/date-pickers/custom-field/MaterialV6Field.tsx.preview new file mode 100644 index 0000000000000..027cb7beacfac --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV6Field.tsx.preview @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.js b/docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.js new file mode 100644 index 0000000000000..df1b5053a7451 --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.js @@ -0,0 +1,22 @@ +import * as React from 'react'; +import MuiTextField from '@mui/material/TextField'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; + +const TextField = React.forwardRef((props, ref) => ( + +)); + +export default function MaterialV6FieldWrapped() { + return ( + + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.tsx b/docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.tsx new file mode 100644 index 0000000000000..0c90908a7d4b5 --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import MuiTextField, { TextFieldProps } from '@mui/material/TextField'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; + +const TextField = React.forwardRef( + (props: TextFieldProps, ref: React.Ref) => ( + + ), +); + +export default function MaterialV6FieldWrapped() { + return ( + + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.tsx.preview b/docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.tsx.preview new file mode 100644 index 0000000000000..cd5ff731abbcd --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV6FieldWrapped.tsx.preview @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/MaterialV7Field.js b/docs/data/date-pickers/custom-field/MaterialV7Field.js new file mode 100644 index 0000000000000..f7c4f157b96a8 --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV7Field.js @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { DateField } from '@mui/x-date-pickers/DateField'; + +export default function MaterialV7Field() { + return ( + + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/MaterialV7Field.tsx b/docs/data/date-pickers/custom-field/MaterialV7Field.tsx new file mode 100644 index 0000000000000..f7c4f157b96a8 --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV7Field.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { DateField } from '@mui/x-date-pickers/DateField'; + +export default function MaterialV7Field() { + return ( + + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/MaterialV7Field.tsx.preview b/docs/data/date-pickers/custom-field/MaterialV7Field.tsx.preview new file mode 100644 index 0000000000000..9708ed359729e --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV7Field.tsx.preview @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.js b/docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.js new file mode 100644 index 0000000000000..d125a8479b5c8 --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.js @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; + +const MyPickersTextField = React.forwardRef((props, ref) => ( + +)); + +export default function MaterialV7FieldWrapped() { + return ( + + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.tsx b/docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.tsx new file mode 100644 index 0000000000000..82e06da7316b3 --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { + PickersTextField, + PickersTextFieldProps, +} from '@mui/x-date-pickers/PickersTextField'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; + +const MyPickersTextField = React.forwardRef( + (props: PickersTextFieldProps, ref: React.Ref) => ( + + ), +); + +export default function MaterialV7FieldWrapped() { + return ( + + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.tsx.preview b/docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.tsx.preview new file mode 100644 index 0000000000000..17535291f5f3b --- /dev/null +++ b/docs/data/date-pickers/custom-field/MaterialV7FieldWrapped.tsx.preview @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/PickerWithAutocompleteField.js b/docs/data/date-pickers/custom-field/PickerWithAutocompleteField.js index 12b3a98d85078..ea7512ace1590 100644 --- a/docs/data/date-pickers/custom-field/PickerWithAutocompleteField.js +++ b/docs/data/date-pickers/custom-field/PickerWithAutocompleteField.js @@ -97,8 +97,8 @@ function AutocompleteDatePicker(props) { return ( !optionsLookup[date.startOf('day').toISOString()]} {...other} /> diff --git a/docs/data/date-pickers/custom-field/PickerWithAutocompleteField.tsx b/docs/data/date-pickers/custom-field/PickerWithAutocompleteField.tsx index 6d5cfc980154a..1aac33d076c0d 100644 --- a/docs/data/date-pickers/custom-field/PickerWithAutocompleteField.tsx +++ b/docs/data/date-pickers/custom-field/PickerWithAutocompleteField.tsx @@ -14,11 +14,12 @@ import { } from '@mui/x-date-pickers/models'; interface AutoCompleteFieldProps - extends UseDateFieldProps, + extends UseDateFieldProps, BaseSingleInputFieldProps< Dayjs | null, Dayjs, FieldSection, + false, DateValidationError > { /** @@ -127,8 +128,8 @@ function AutocompleteDatePicker(props: AutocompleteDatePickerProps) { return ( - slots={{ field: AutocompleteField, ...props.slots }} - slotProps={{ field: { options } as any }} + slots={{ ...props.slots, field: AutocompleteField }} + slotProps={{ ...props.slotProps, field: { options } as any }} shouldDisableDate={(date) => !optionsLookup[date.startOf('day').toISOString()]} {...other} /> diff --git a/docs/data/date-pickers/custom-field/PickerWithButtonField.js b/docs/data/date-pickers/custom-field/PickerWithButtonField.js index 255784386528b..f13ca0315b2bb 100644 --- a/docs/data/date-pickers/custom-field/PickerWithButtonField.js +++ b/docs/data/date-pickers/custom-field/PickerWithButtonField.js @@ -34,8 +34,8 @@ function ButtonDatePicker(props) { return ( setOpen(false)} diff --git a/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx b/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx index c8cfc01a1964e..aa09717fb29ce 100644 --- a/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx +++ b/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx @@ -12,11 +12,12 @@ import { } from '@mui/x-date-pickers/models'; interface ButtonFieldProps - extends UseDateFieldProps, + extends UseDateFieldProps, BaseSingleInputFieldProps< Dayjs | null, Dayjs, FieldSection, + false, DateValidationError > { setOpen?: React.Dispatch>; @@ -53,8 +54,8 @@ function ButtonDatePicker( return ( setOpen(false)} diff --git a/docs/data/date-pickers/custom-field/WrappedSingleInputDateRangePicker.js b/docs/data/date-pickers/custom-field/SingleInputDateRangePickerWrapped.js similarity index 93% rename from docs/data/date-pickers/custom-field/WrappedSingleInputDateRangePicker.js rename to docs/data/date-pickers/custom-field/SingleInputDateRangePickerWrapped.js index 389bd55f5822c..aa20bda0c4275 100644 --- a/docs/data/date-pickers/custom-field/WrappedSingleInputDateRangePicker.js +++ b/docs/data/date-pickers/custom-field/SingleInputDateRangePickerWrapped.js @@ -12,7 +12,7 @@ const WrappedSingleInputDateRangeField = React.forwardRef((props, ref) => { WrappedSingleInputDateRangeField.fieldType = 'single-input'; -export default function WrappedSingleInputDateRangePicker() { +export default function SingleInputDateRangePickerWrapped() { return ( diff --git a/docs/data/date-pickers/custom-field/WrappedSingleInputDateRangePicker.tsx b/docs/data/date-pickers/custom-field/SingleInputDateRangePickerWrapped.tsx similarity index 88% rename from docs/data/date-pickers/custom-field/WrappedSingleInputDateRangePicker.tsx rename to docs/data/date-pickers/custom-field/SingleInputDateRangePickerWrapped.tsx index a7f000683fc7c..21ea29ee17dd7 100644 --- a/docs/data/date-pickers/custom-field/WrappedSingleInputDateRangePicker.tsx +++ b/docs/data/date-pickers/custom-field/SingleInputDateRangePickerWrapped.tsx @@ -9,11 +9,12 @@ import { } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { FieldType } from '@mui/x-date-pickers-pro/models'; type FieldComponent = (( props: SingleInputDateRangeFieldProps & React.RefAttributes, -) => React.JSX.Element) & { fieldType?: string }; +) => React.JSX.Element) & { fieldType?: FieldType }; const WrappedSingleInputDateRangeField = React.forwardRef( ( @@ -26,7 +27,7 @@ const WrappedSingleInputDateRangeField = React.forwardRef( WrappedSingleInputDateRangeField.fieldType = 'single-input'; -export default function WrappedSingleInputDateRangePicker() { +export default function SingleInputDateRangePickerWrapped() { return ( diff --git a/docs/data/date-pickers/custom-field/WrappedSingleInputDateRangePicker.tsx.preview b/docs/data/date-pickers/custom-field/SingleInputDateRangePickerWrapped.tsx.preview similarity index 100% rename from docs/data/date-pickers/custom-field/WrappedSingleInputDateRangePicker.tsx.preview rename to docs/data/date-pickers/custom-field/SingleInputDateRangePickerWrapped.tsx.preview diff --git a/docs/data/date-pickers/custom-field/custom-field.md b/docs/data/date-pickers/custom-field/custom-field.md index 947d98059e12d..302cde45d08c8 100644 --- a/docs/data/date-pickers/custom-field/custom-field.md +++ b/docs/data/date-pickers/custom-field/custom-field.md @@ -39,7 +39,7 @@ You can pass the single input fields to the range picker to use it for keyboard If you want to create a wrapper around the field, make sure to set the `fieldType` static property to `'single-input'`. Otherwise, the picker won't know your field is a single input one and use the multi input event listeners: -{{"demo": "WrappedSingleInputDateRangePicker.js", "defaultCodeOpen": false}} +{{"demo": "SingleInputDateRangePickerWrapped.js", "defaultCodeOpen": false}} You can manually add an `endAdornment` if you want your range picker to look exactly like on a simple picker: @@ -56,11 +56,44 @@ Setting `formatDensity` to `"spacious"` will add a space before and after each ` {{"demo": "FieldFormatDensity.js"}} -## Commonly used custom field +## Usage with Material UI -### Using another input +### Using Material `TextField` -#### With the Joy UI input +You can import the `TextField` component to create custom wrappers: + +{{"demo": "MaterialV6FieldWrapped.js"}} + +:::success +This approach is only recommended if you need complex customizations on your `TextField`, +or if you already have a wrapper also used outside the Date and Time Pickers. + +If you just need to set some default props, you can use [the `slotProps` prop](/x/react-date-pickers/custom-field/#customize-the-textfield). +::: + +### Using Material `PickersTextField` + +Pass the `enableAccessibleFieldDOMStructure` to any Field or Picker component to enable the accessible DOM structure: + +{{"demo": "MaterialV7Field.js"}} + +:::success +Learn more about the [accessible DOM structure](/x/react-date-pickers/fields/#accessible-dom-structure). +::: + +You can import the `PickersTextField` component to create custom wrappers: + +{{"demo": "MaterialV7FieldWrapped.js"}} + +:::success +This approach is only recommended if you need complex customizations on your `PickersTextField`. + +If you just need to set some default props, you can use [the `slotProps` prop](/x/react-date-pickers/custom-field/#customize-the-textfield). +::: + +## Usage with Joy UI + +### Using Joy `Input` You can use the [Joy UI](https://mui.com/joy-ui/getting-started/) components instead of the Material UI ones: @@ -68,26 +101,46 @@ You can use the [Joy UI](https://mui.com/joy-ui/getting-started/) components in A higher-level solution for _Joy UI_ will be provided in the near future for even simpler usage. ::: -{{"demo": "PickerWithJoyField.js", "defaultCodeOpen": false}} +{{"demo": "JoyV6Field.js", "defaultCodeOpen": false}} -{{"demo": "RangePickerWithSingleInputJoyField.js", "defaultCodeOpen": false}} +{{"demo": "JoyV6SingleInputRangeField.js", "defaultCodeOpen": false}} -{{"demo": "RangePickerWithJoyField.js", "defaultCodeOpen": false}} +{{"demo": "JoyV6MultiInputRangeField.js", "defaultCodeOpen": false}} -#### With the browser input +### Using Joy `PickersTextField` -You can also use any other input: +:::warning +This component is not available yet. +::: -{{"demo": "PickerWithBrowserField.js", "defaultCodeOpen": false}} +## Usage with an unstyled input -{{"demo": "RangePickerWithSingleInputBrowserField.js", "defaultCodeOpen": false}} +### Using the browser input -{{"demo": "RangePickerWithBrowserField.js", "defaultCodeOpen": false}} +{{"demo": "BrowserV6Field.js", "defaultCodeOpen": false}} + +{{"demo": "BrowserV6SingleInputRangeField.js", "defaultCodeOpen": false}} + +{{"demo": "BrowserV6MultiInputRangeField.js", "defaultCodeOpen": false}} :::warning You will need to use a component that supports the `sx` prop as a wrapper for your input, in order to be able to benefit from the **hover** and **focus** behavior of the clear button. You will have access to the `clearable` and `onClear` props using native HTML elements, but the on **focus** and **hover** behavior depends on styles applied via the `sx` prop. ::: +### Using custom `PickersTextField` + +:::success +Learn more about the accessible DOM structure and its difference compared to the current one on the [dedicated doc section](/x/react-date-pickers/fields/#accessible-dom-structure). +::: + +{{"demo": "BrowserV7Field.js", "defaultCodeOpen": false}} + +{{"demo": "BrowserV7SingleInputRangeField.js", "defaultCodeOpen": false}} + +{{"demo": "BrowserV7MultiInputRangeField.js", "defaultCodeOpen": false}} + +## Usage with another UI + ### Using an `Autocomplete` If your user can only select a value in a small list of available dates, diff --git a/docs/data/date-pickers/fields/BasicV6DOMStructure.js b/docs/data/date-pickers/fields/BasicV6DOMStructure.js new file mode 100644 index 0000000000000..20a6c9ae844be --- /dev/null +++ b/docs/data/date-pickers/fields/BasicV6DOMStructure.js @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; + +export default function BasicV6DOMStructure() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/fields/BasicV6DOMStructure.tsx b/docs/data/date-pickers/fields/BasicV6DOMStructure.tsx new file mode 100644 index 0000000000000..20a6c9ae844be --- /dev/null +++ b/docs/data/date-pickers/fields/BasicV6DOMStructure.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; + +export default function BasicV6DOMStructure() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/fields/BasicV6DOMStructure.tsx.preview b/docs/data/date-pickers/fields/BasicV6DOMStructure.tsx.preview new file mode 100644 index 0000000000000..2dc079345c091 --- /dev/null +++ b/docs/data/date-pickers/fields/BasicV6DOMStructure.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/fields/BasicV7DOMStructure.js b/docs/data/date-pickers/fields/BasicV7DOMStructure.js new file mode 100644 index 0000000000000..1f32eca967da6 --- /dev/null +++ b/docs/data/date-pickers/fields/BasicV7DOMStructure.js @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; + +export default function BasicV7DOMStructure() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/fields/BasicV7DOMStructure.tsx b/docs/data/date-pickers/fields/BasicV7DOMStructure.tsx new file mode 100644 index 0000000000000..1f32eca967da6 --- /dev/null +++ b/docs/data/date-pickers/fields/BasicV7DOMStructure.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; + +export default function BasicV7DOMStructure() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/fields/BasicV7DOMStructure.tsx.preview b/docs/data/date-pickers/fields/BasicV7DOMStructure.tsx.preview new file mode 100644 index 0000000000000..f33bc6c8fdc7e --- /dev/null +++ b/docs/data/date-pickers/fields/BasicV7DOMStructure.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/fields/fields.md b/docs/data/date-pickers/fields/fields.md index 9bd7260fff468..5f54b1411f66e 100644 --- a/docs/data/date-pickers/fields/fields.md +++ b/docs/data/date-pickers/fields/fields.md @@ -25,6 +25,201 @@ All fields to edit a range are available in a single input version and in a mult {{"demo": "DateRangeFieldExamples.js", "defaultCodeOpen": false}} +## Accessible DOM structure + +By default, the fields' DOM structure consists of an ``, which holds the whole value for the component, but unfortunately presents a few limitations in terms of accessibility when managing multiple section values. + +From v7 version, you can opt-in for a new and experimental DOM structure on any field or picker component using the `enableAccessibleFieldDOMStructure` prop. + +```tsx + + + +``` + +This new feature allows the field component to set aria attributes on individual sections, providing a far better experience with screen readers. + +{{"demo": "BasicV7DOMStructure.js", "defaultCodeOpen": false }} + +### Usage with `slotProps.field` + +When using `slotProps.field` to pass props to your field component, +the field consumes some props (e.g: `shouldRespectLeadingZeros`) and forwards the rest to the `TextField`. + +- For the props consumed by the field, the behavior should remain exactly the same with both DOM structures. + + Both components below will respect the leading zeroes on digit sections: + + ```js + + + ``` + +- For the props forwarded to the `TextField`, + you can have a look at the next section to see how the migration impact them. + + Both components below will render a small size UI: + + ```js + + + ``` + +### Usage with `slotProps.textField` + +If you are passing props to `slotProps.textField`, +these props will now be received by `PickersTextField` and should keep working the same way as before. + +Both components below will render a small size UI: + +```js + + +``` + +:::info +If you are passing `inputProps` to `slotProps.textField`, +these props will now be passed to the hidden `` element. +::: + +### Usage with `slots.field` + +If you are passing a custom field component to your pickers, you need to create a new one that is using the accessible DOM structure. +This new component will need to use the `PickersSectionList` component instead of an `` HTML element. + +You can have a look at the [custom PickersTextField](/x/react-date-pickers/custom-field/#using-custom-pickerstextfield) to have a concrete example. + +:::info +If your custom field was used to create a Joy UI design component, +you may want to wait a few weeks for the release of an out-of-the-box Joy `PickersTextField` component instead of implementing it yourself. +::: + +### Usage with `slots.textField` + +If you are passing a custom `TextField` component to your fields and pickers, +you need to create a new one that is using the accessible DOM structure. + +You can have a look at the second demo of the [Material PickersTextField section](/x/react-date-pickers/custom-field/#using-material-pickerstextfield) to have a concrete example. + +:::info +If your custom `TextField` was used to apply a totally different input that did not use `@mui/material/TextField`, +please consider having a look at the [custom PickersTextField](/x/react-date-pickers/custom-field/#using-custom-pickerstextfield) section which uses `slots.field`. +This approach can be more appropriate for deeper changes. +::: + +### Usage with theme + +If you are using the theme to customize `MuiTextField`, +you need to pass the same config to `MuiPickersTextField`: + +```js +const theme = createTheme({ + components: { + MuiTextField: { + defaultProps: { + variant: 'outlined', + }, + styleOverrides: { + root: { + '& .MuiInputLabel-outlined.Mui-focused': { + color: 'red', + }, + }, + }, + }, + MuiPickersTextField: { + defaultProps: { + variant: 'outlined', + }, + styleOverrides: { + root: { + '& .MuiInputLabel-outlined.Mui-focused': { + color: 'red', + }, + }, + }, + }, + }, +}); +``` + +If you are using the theme to customize `MuiInput`, `MuiOutlinedInput` or `MuiFilledInput`, +you need to pass the same config to `MuiPickersInput`, `MuiPickersOutlinedInput` or `MuiPickersFilledInput`: + +```js +const theme = createTheme({ + components: { + // Replace with `MuiOutlinedInput` or `MuiFilledInput` if needed + MuiInput: { + defaultProps: { + margin: 'dense', + }, + styleOverrides: { + root: { + color: 'red', + }, + }, + }, + // Replace with `MuiPickersOutlinedInput` or `MuiPickersFilledInput` if needed + MuiPickersInput: { + defaultProps: { + margin: 'dense', + }, + styleOverrides: { + root: { + color: 'red', + }, + }, + }, + }, +}); +``` + +If you are using the theme to customize `MuiInputBase`, +you need to pass the same config to `MuiPickersInputBase`: + +```js +const theme = createTheme({ + components: { + MuiInputBase: { + defaultProps: { + margin: 'dense', + }, + styleOverrides: { + root: { + color: 'red', + }, + }, + }, + MuiPickersInputBase: { + defaultProps: { + margin: 'dense', + }, + styleOverrides: { + root: { + color: 'red', + }, + }, + }, + }, +}); +``` + ## Advanced ### What is a section? diff --git a/docs/data/migration/migration-pickers-v6/migration-pickers-v6.md b/docs/data/migration/migration-pickers-v6/migration-pickers-v6.md index 61844f3741c6f..f9d09cc46fafa 100644 --- a/docs/data/migration/migration-pickers-v6/migration-pickers-v6.md +++ b/docs/data/migration/migration-pickers-v6/migration-pickers-v6.md @@ -243,6 +243,23 @@ The string argument of the `dayOfWeekFormatter` prop has been replaced in favor ## Field components +### Update the format of `selectedSections` + +The `selectedSections` prop no longer accepts start and end indexes. +When selecting several — but not all — sections, +the field components were not behaving correctly, you can now only select one or all sections: + +```diff + +``` + ### Replace the section `hasLeadingZeros` property :::success @@ -358,6 +375,41 @@ If your custom field is based on one of the examples of the [Custom field](/x/re then you can look at the page to see all the examples improved and updated to use the new simplified API. ::: +#### Do not forward the `enableAccessibleFieldDOMStructure` prop to the DOM + +The headless field hooks (e.g.: `useDateField`) now return a new prop called `enableAccessibleFieldDOMStructure`. +This is used to know if the current UI expected is built using the accessible DOM structure or not. +Learn more about this new [accessible DOM structure](/x/react-date-pickers/fields/#accessible-dom-structure). + +When building a custom UI, you are most-likely only supporting one DOM structure, so you can remove `enableAccessibleFieldDOMStructure` before it is passed to the DOM: + +```diff + function MyCustomTextField(props) { + const { ++ // Should be ignored ++ enableAccessibleFieldDOMStructure, + + // ... rest of the props you are using + } + + return ( /* Some UI to edit the date */ ) + } + + function MyCustomField(props) { + const fieldResponse = useDateField({ + ...props, ++ // If you only support one DOM structure, we advise you to hardcode it here to avoid unwanted switches in your application ++ enableAccessibleFieldDOMStructure: false, + }); + + return ; + } + + function App() { + return ; + } +``` + ## Date management ### Use localized week with luxon @@ -852,3 +904,14 @@ Which is the same type as the one accepted by the components `value` prop. ``` + +## Removed internal types + +The following internal types were exported by mistake and have been removed from the public API: + +- `UseDateFieldDefaultizedProps` +- `UseTimeFieldDefaultizedProps` +- `UseDateTimeFieldDefaultizedProps` +- `UseSingleInputDateRangeFieldComponentProps` +- `UseSingleInputTimeRangeFieldComponentProps` +- `UseSingleInputDateTimeRangeFieldComponentProps` diff --git a/docs/pages/x/api/date-pickers/date-field.json b/docs/pages/x/api/date-pickers/date-field.json index ac12fc860e072..61aff54380b49 100644 --- a/docs/pages/x/api/date-pickers/date-field.json +++ b/docs/pages/x/api/date-pickers/date-field.json @@ -70,7 +70,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { diff --git a/docs/pages/x/api/date-pickers/date-picker.json b/docs/pages/x/api/date-pickers/date-picker.json index ebbef0911f56d..c615567fe8291 100644 --- a/docs/pages/x/api/date-pickers/date-picker.json +++ b/docs/pages/x/api/date-pickers/date-picker.json @@ -111,7 +111,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -280,8 +280,8 @@ }, { "name": "textField", - "description": "Form control with an input to render the value inside the default field.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render the value inside the default field.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/date-range-picker.json b/docs/pages/x/api/date-pickers/date-range-picker.json index 787f387f624ba..99612ca1ca76e 100644 --- a/docs/pages/x/api/date-pickers/date-range-picker.json +++ b/docs/pages/x/api/date-pickers/date-range-picker.json @@ -112,7 +112,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -237,8 +237,8 @@ }, { "name": "textField", - "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/date-time-field.json b/docs/pages/x/api/date-pickers/date-time-field.json index 6c12c32f7960c..c7778236192ff 100644 --- a/docs/pages/x/api/date-pickers/date-time-field.json +++ b/docs/pages/x/api/date-pickers/date-time-field.json @@ -77,7 +77,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { diff --git a/docs/pages/x/api/date-pickers/date-time-picker.json b/docs/pages/x/api/date-pickers/date-time-picker.json index 7dca2a90b23d1..1588c746f832d 100644 --- a/docs/pages/x/api/date-pickers/date-time-picker.json +++ b/docs/pages/x/api/date-pickers/date-time-picker.json @@ -119,7 +119,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -314,8 +314,8 @@ }, { "name": "textField", - "description": "Form control with an input to render the value inside the default field.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render the value inside the default field.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/date-time-range-picker.json b/docs/pages/x/api/date-pickers/date-time-range-picker.json index 99884583aa0a9..d08722fb1fd14 100644 --- a/docs/pages/x/api/date-pickers/date-time-range-picker.json +++ b/docs/pages/x/api/date-pickers/date-time-range-picker.json @@ -47,9 +47,7 @@ "type": { "name": "enum", "description": "'dense'
| 'spacious'" }, "default": "\"dense\"" }, - "inputRef": { - "type": { "name": "union", "description": "func
| { current?: object }" } - }, + "inputRef": { "type": { "name": "custom", "description": "ref" } }, "label": { "type": { "name": "node" } }, "loading": { "type": { "name": "bool" }, "default": "false" }, "localeText": { "type": { "name": "object" } }, @@ -131,7 +129,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -308,8 +306,8 @@ }, { "name": "textField", - "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/desktop-date-picker.json b/docs/pages/x/api/date-pickers/desktop-date-picker.json index 226272c1cac99..8054ed48893cc 100644 --- a/docs/pages/x/api/date-pickers/desktop-date-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-date-picker.json @@ -107,7 +107,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -276,8 +276,8 @@ }, { "name": "textField", - "description": "Form control with an input to render the value inside the default field.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render the value inside the default field.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/desktop-date-range-picker.json b/docs/pages/x/api/date-pickers/desktop-date-range-picker.json index fac92678028fe..56ef916cd278e 100644 --- a/docs/pages/x/api/date-pickers/desktop-date-range-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-date-range-picker.json @@ -108,7 +108,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -233,8 +233,8 @@ }, { "name": "textField", - "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/desktop-date-time-picker.json b/docs/pages/x/api/date-pickers/desktop-date-time-picker.json index 86be59e1023cd..bdc527c14f28d 100644 --- a/docs/pages/x/api/date-pickers/desktop-date-time-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-date-time-picker.json @@ -115,7 +115,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -310,8 +310,8 @@ }, { "name": "textField", - "description": "Form control with an input to render the value inside the default field.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render the value inside the default field.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/desktop-date-time-range-picker.json b/docs/pages/x/api/date-pickers/desktop-date-time-range-picker.json index a5be8be300f56..5370342674072 100644 --- a/docs/pages/x/api/date-pickers/desktop-date-time-range-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-date-time-range-picker.json @@ -43,9 +43,7 @@ "type": { "name": "enum", "description": "'dense'
| 'spacious'" }, "default": "\"dense\"" }, - "inputRef": { - "type": { "name": "union", "description": "func
| { current?: object }" } - }, + "inputRef": { "type": { "name": "custom", "description": "ref" } }, "label": { "type": { "name": "node" } }, "loading": { "type": { "name": "bool" }, "default": "false" }, "localeText": { "type": { "name": "object" } }, @@ -127,7 +125,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -304,8 +302,8 @@ }, { "name": "textField", - "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/desktop-time-picker.json b/docs/pages/x/api/date-pickers/desktop-time-picker.json index 8273e26e92d90..3437b5b664ee0 100644 --- a/docs/pages/x/api/date-pickers/desktop-time-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-time-picker.json @@ -77,7 +77,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableTime": { @@ -213,8 +213,8 @@ }, { "name": "textField", - "description": "Form control with an input to render the value inside the default field.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render the value inside the default field.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/mobile-date-picker.json b/docs/pages/x/api/date-pickers/mobile-date-picker.json index bda77b1be01bd..9c2d12c3887c0 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-picker.json @@ -107,7 +107,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -258,8 +258,8 @@ }, { "name": "textField", - "description": "Form control with an input to render the value inside the default field.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render the value inside the default field.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/mobile-date-range-picker.json b/docs/pages/x/api/date-pickers/mobile-date-range-picker.json index 0c98429fe9ab3..2c0481a80fd63 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-range-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-range-picker.json @@ -104,7 +104,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -229,8 +229,8 @@ }, { "name": "textField", - "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/mobile-date-time-picker.json b/docs/pages/x/api/date-pickers/mobile-date-time-picker.json index 4ef39117304fa..a31d556420df3 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-time-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-time-picker.json @@ -115,7 +115,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -283,8 +283,8 @@ }, { "name": "textField", - "description": "Form control with an input to render the value inside the default field.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render the value inside the default field.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/mobile-date-time-range-picker.json b/docs/pages/x/api/date-pickers/mobile-date-time-range-picker.json index 732bb95151b6a..4b62aadfa72e5 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-time-range-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-time-range-picker.json @@ -39,9 +39,7 @@ "type": { "name": "enum", "description": "'dense'
| 'spacious'" }, "default": "\"dense\"" }, - "inputRef": { - "type": { "name": "union", "description": "func
| { current?: object }" } - }, + "inputRef": { "type": { "name": "custom", "description": "ref" } }, "label": { "type": { "name": "node" } }, "loading": { "type": { "name": "bool" }, "default": "false" }, "localeText": { "type": { "name": "object" } }, @@ -123,7 +121,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { @@ -300,8 +298,8 @@ }, { "name": "textField", - "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render a date or time inside the default field.\nIt is rendered twice: once for the start element and once for the end element.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/mobile-time-picker.json b/docs/pages/x/api/date-pickers/mobile-time-picker.json index ff478590ca5df..c1cc22aecfafd 100644 --- a/docs/pages/x/api/date-pickers/mobile-time-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-time-picker.json @@ -77,7 +77,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableTime": { @@ -183,8 +183,8 @@ }, { "name": "textField", - "description": "Form control with an input to render the value inside the default field.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render the value inside the default field.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/pages/x/api/date-pickers/multi-input-date-range-field.json b/docs/pages/x/api/date-pickers/multi-input-date-range-field.json index ab581f1f6f69f..81aa049a9cf74 100644 --- a/docs/pages/x/api/date-pickers/multi-input-date-range-field.json +++ b/docs/pages/x/api/date-pickers/multi-input-date-range-field.json @@ -1,5 +1,6 @@ { "props": { + "autoFocus": { "type": { "name": "bool" } }, "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "defaultValue": { "type": { "name": "arrayOf", "description": "Array<object>" } }, "direction": { @@ -49,7 +50,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { diff --git a/docs/pages/x/api/date-pickers/multi-input-date-time-range-field.json b/docs/pages/x/api/date-pickers/multi-input-date-time-range-field.json index 3ad6a4c4bc6bc..7c3409b379817 100644 --- a/docs/pages/x/api/date-pickers/multi-input-date-time-range-field.json +++ b/docs/pages/x/api/date-pickers/multi-input-date-time-range-field.json @@ -1,6 +1,7 @@ { "props": { "ampm": { "type": { "name": "bool" }, "default": "`utils.is12HourCycleInCurrentLocale()`" }, + "autoFocus": { "type": { "name": "bool" } }, "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "defaultValue": { "type": { "name": "arrayOf", "description": "Array<object>" } }, "direction": { @@ -56,7 +57,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { diff --git a/docs/pages/x/api/date-pickers/multi-input-time-range-field.json b/docs/pages/x/api/date-pickers/multi-input-time-range-field.json index 230921861f532..7eaa36103979b 100644 --- a/docs/pages/x/api/date-pickers/multi-input-time-range-field.json +++ b/docs/pages/x/api/date-pickers/multi-input-time-range-field.json @@ -1,6 +1,7 @@ { "props": { "ampm": { "type": { "name": "bool" }, "default": "`utils.is12HourCycleInCurrentLocale()`" }, + "autoFocus": { "type": { "name": "bool" } }, "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "defaultValue": { "type": { "name": "arrayOf", "description": "Array<object>" } }, "direction": { @@ -52,7 +53,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableTime": { diff --git a/docs/pages/x/api/date-pickers/single-input-date-range-field.json b/docs/pages/x/api/date-pickers/single-input-date-range-field.json index 0b8a151594e24..edcc1144f2fa5 100644 --- a/docs/pages/x/api/date-pickers/single-input-date-range-field.json +++ b/docs/pages/x/api/date-pickers/single-input-date-range-field.json @@ -70,7 +70,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { diff --git a/docs/pages/x/api/date-pickers/single-input-date-time-range-field.json b/docs/pages/x/api/date-pickers/single-input-date-time-range-field.json index 5ea7d709048a7..34b899ca07dad 100644 --- a/docs/pages/x/api/date-pickers/single-input-date-time-range-field.json +++ b/docs/pages/x/api/date-pickers/single-input-date-time-range-field.json @@ -77,7 +77,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableDate": { diff --git a/docs/pages/x/api/date-pickers/single-input-time-range-field.json b/docs/pages/x/api/date-pickers/single-input-time-range-field.json index 3c00fc18fb75d..5a450b178c85d 100644 --- a/docs/pages/x/api/date-pickers/single-input-time-range-field.json +++ b/docs/pages/x/api/date-pickers/single-input-time-range-field.json @@ -73,7 +73,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableTime": { diff --git a/docs/pages/x/api/date-pickers/time-field.json b/docs/pages/x/api/date-pickers/time-field.json index 8f87d61b8c3ad..641529f95fd4f 100644 --- a/docs/pages/x/api/date-pickers/time-field.json +++ b/docs/pages/x/api/date-pickers/time-field.json @@ -73,7 +73,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableTime": { diff --git a/docs/pages/x/api/date-pickers/time-picker.json b/docs/pages/x/api/date-pickers/time-picker.json index d3386734875f2..1fd742141aa65 100644 --- a/docs/pages/x/api/date-pickers/time-picker.json +++ b/docs/pages/x/api/date-pickers/time-picker.json @@ -81,7 +81,7 @@ "selectedSections": { "type": { "name": "union", - "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number
| { endIndex: number, startIndex: number }" + "description": "'all'
| 'day'
| 'empty'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'weekDay'
| 'year'
| number" } }, "shouldDisableTime": { @@ -217,8 +217,8 @@ }, { "name": "textField", - "description": "Form control with an input to render the value inside the default field.\nReceives the same props as `@mui/material/TextField`.", - "default": "TextField from '@mui/material'", + "description": "Form control with an input to render the value inside the default field.", + "default": "TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`.", "class": null }, { diff --git a/docs/translations/api-docs/date-pickers/date-field/date-field.json b/docs/translations/api-docs/date-pickers/date-field/date-field.json index 21890db624548..8b90f9e6a9bf4 100644 --- a/docs/translations/api-docs/date-pickers/date-field/date-field.json +++ b/docs/translations/api-docs/date-pickers/date-field/date-field.json @@ -86,7 +86,7 @@ "description": "If true, the label is displayed as required and the input element is required." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", diff --git a/docs/translations/api-docs/date-pickers/date-picker/date-picker.json b/docs/translations/api-docs/date-pickers/date-picker/date-picker.json index 0e15c5cd2bde4..3e108cea87188 100644 --- a/docs/translations/api-docs/date-pickers/date-picker/date-picker.json +++ b/docs/translations/api-docs/date-pickers/date-picker/date-picker.json @@ -113,7 +113,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -184,7 +184,7 @@ "shortcuts": "Custom component for the shortcuts.", "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", - "textField": "Form control with an input to render the value inside the default field. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render the value inside the default field.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/date-range-picker/date-range-picker.json b/docs/translations/api-docs/date-pickers/date-range-picker/date-range-picker.json index eb22b1e23a009..a13177b244d05 100644 --- a/docs/translations/api-docs/date-pickers/date-range-picker/date-range-picker.json +++ b/docs/translations/api-docs/date-pickers/date-range-picker/date-range-picker.json @@ -122,7 +122,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -174,7 +174,7 @@ "shortcuts": "Custom component for the shortcuts.", "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", - "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/date-time-field/date-time-field.json b/docs/translations/api-docs/date-pickers/date-time-field/date-time-field.json index 039cb389ead33..bcabb346acaa9 100644 --- a/docs/translations/api-docs/date-pickers/date-time-field/date-time-field.json +++ b/docs/translations/api-docs/date-pickers/date-time-field/date-time-field.json @@ -103,7 +103,7 @@ "description": "If true, the label is displayed as required and the input element is required." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", diff --git a/docs/translations/api-docs/date-pickers/date-time-picker/date-time-picker.json b/docs/translations/api-docs/date-pickers/date-time-picker/date-time-picker.json index 13371c238fdd8..2b5be2cd11918 100644 --- a/docs/translations/api-docs/date-pickers/date-time-picker/date-time-picker.json +++ b/docs/translations/api-docs/date-pickers/date-time-picker/date-time-picker.json @@ -133,7 +133,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -224,7 +224,7 @@ "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", "tabs": "Tabs enabling toggling between date and time pickers.", - "textField": "Form control with an input to render the value inside the default field. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render the value inside the default field.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/date-time-range-picker/date-time-range-picker.json b/docs/translations/api-docs/date-pickers/date-time-range-picker/date-time-range-picker.json index 59370c227246f..7a822c7ee04f0 100644 --- a/docs/translations/api-docs/date-pickers/date-time-range-picker/date-time-range-picker.json +++ b/docs/translations/api-docs/date-pickers/date-time-range-picker/date-time-range-picker.json @@ -146,7 +146,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -222,7 +222,7 @@ "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", "tabs": "Tabs enabling toggling between date and time pickers.", - "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/desktop-date-picker/desktop-date-picker.json b/docs/translations/api-docs/date-pickers/desktop-date-picker/desktop-date-picker.json index 7417406bf04b4..e97a6a69024fc 100644 --- a/docs/translations/api-docs/date-pickers/desktop-date-picker/desktop-date-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-date-picker/desktop-date-picker.json @@ -110,7 +110,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -178,7 +178,7 @@ "shortcuts": "Custom component for the shortcuts.", "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", - "textField": "Form control with an input to render the value inside the default field. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render the value inside the default field.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/desktop-date-range-picker/desktop-date-range-picker.json b/docs/translations/api-docs/date-pickers/desktop-date-range-picker/desktop-date-range-picker.json index e8ff63d1170d2..619d7999ae43a 100644 --- a/docs/translations/api-docs/date-pickers/desktop-date-range-picker/desktop-date-range-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-date-range-picker/desktop-date-range-picker.json @@ -119,7 +119,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -168,7 +168,7 @@ "shortcuts": "Custom component for the shortcuts.", "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", - "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/desktop-date-time-picker/desktop-date-time-picker.json b/docs/translations/api-docs/date-pickers/desktop-date-time-picker/desktop-date-time-picker.json index d5f526e8584f3..5b1bd9b0032a2 100644 --- a/docs/translations/api-docs/date-pickers/desktop-date-time-picker/desktop-date-time-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-date-time-picker/desktop-date-time-picker.json @@ -130,7 +130,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -218,7 +218,7 @@ "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", "tabs": "Tabs enabling toggling between date and time pickers.", - "textField": "Form control with an input to render the value inside the default field. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render the value inside the default field.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/desktop-date-time-range-picker/desktop-date-time-range-picker.json b/docs/translations/api-docs/date-pickers/desktop-date-time-range-picker/desktop-date-time-range-picker.json index 6d7cc6b7db13b..84238b6d79e37 100644 --- a/docs/translations/api-docs/date-pickers/desktop-date-time-range-picker/desktop-date-time-range-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-date-time-range-picker/desktop-date-time-range-picker.json @@ -143,7 +143,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -216,7 +216,7 @@ "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", "tabs": "Tabs enabling toggling between date and time pickers.", - "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/desktop-time-picker/desktop-time-picker.json b/docs/translations/api-docs/date-pickers/desktop-time-picker/desktop-time-picker.json index 7e6c1133b42cc..897981187fe05 100644 --- a/docs/translations/api-docs/date-pickers/desktop-time-picker/desktop-time-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-time-picker/desktop-time-picker.json @@ -90,7 +90,7 @@ "description": "The date used to generate the new value when both value and defaultValue are empty." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableTime": { "description": "Disable specific time.", @@ -148,7 +148,7 @@ "previousIconButton": "Button allowing to switch to the left view.", "rightArrowIcon": "Icon displayed in the right view switch button.", "shortcuts": "Custom component for the shortcuts.", - "textField": "Form control with an input to render the value inside the default field. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render the value inside the default field.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/mobile-date-picker/mobile-date-picker.json b/docs/translations/api-docs/date-pickers/mobile-date-picker/mobile-date-picker.json index c8fce2d4fb486..bec2bd8dfdde7 100644 --- a/docs/translations/api-docs/date-pickers/mobile-date-picker/mobile-date-picker.json +++ b/docs/translations/api-docs/date-pickers/mobile-date-picker/mobile-date-picker.json @@ -110,7 +110,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -172,7 +172,7 @@ "shortcuts": "Custom component for the shortcuts.", "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", - "textField": "Form control with an input to render the value inside the default field. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render the value inside the default field.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/mobile-date-range-picker/mobile-date-range-picker.json b/docs/translations/api-docs/date-pickers/mobile-date-range-picker/mobile-date-range-picker.json index 01bb3ecedd3dc..8f7d5c9375e2f 100644 --- a/docs/translations/api-docs/date-pickers/mobile-date-range-picker/mobile-date-range-picker.json +++ b/docs/translations/api-docs/date-pickers/mobile-date-range-picker/mobile-date-range-picker.json @@ -116,7 +116,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -164,7 +164,7 @@ "shortcuts": "Custom component for the shortcuts.", "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", - "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/mobile-date-time-picker/mobile-date-time-picker.json b/docs/translations/api-docs/date-pickers/mobile-date-time-picker/mobile-date-time-picker.json index 39aca2fc70467..d5ba4d1d7dad4 100644 --- a/docs/translations/api-docs/date-pickers/mobile-date-time-picker/mobile-date-time-picker.json +++ b/docs/translations/api-docs/date-pickers/mobile-date-time-picker/mobile-date-time-picker.json @@ -130,7 +130,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -201,7 +201,7 @@ "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", "tabs": "Tabs enabling toggling between date and time pickers.", - "textField": "Form control with an input to render the value inside the default field. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render the value inside the default field.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/mobile-date-time-range-picker/mobile-date-time-range-picker.json b/docs/translations/api-docs/date-pickers/mobile-date-time-range-picker/mobile-date-time-range-picker.json index 81d3254bc4b8b..b2f99f529dbcb 100644 --- a/docs/translations/api-docs/date-pickers/mobile-date-time-range-picker/mobile-date-time-range-picker.json +++ b/docs/translations/api-docs/date-pickers/mobile-date-time-range-picker/mobile-date-time-range-picker.json @@ -140,7 +140,7 @@ "typeDescriptions": { "React.ReactNode": "The node to render when loading." } }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", @@ -212,7 +212,7 @@ "switchViewButton": "Button displayed to switch between different calendar views.", "switchViewIcon": "Icon displayed in the SwitchViewButton. Rotated by 180° when the open view is 'year'.", "tabs": "Tabs enabling toggling between date and time pickers.", - "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render a date or time inside the default field. It is rendered twice: once for the start element and once for the end element.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/mobile-time-picker/mobile-time-picker.json b/docs/translations/api-docs/date-pickers/mobile-time-picker/mobile-time-picker.json index b74c526f7afda..80fd21ef71fd8 100644 --- a/docs/translations/api-docs/date-pickers/mobile-time-picker/mobile-time-picker.json +++ b/docs/translations/api-docs/date-pickers/mobile-time-picker/mobile-time-picker.json @@ -90,7 +90,7 @@ "description": "The date used to generate the new value when both value and defaultValue are empty." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableTime": { "description": "Disable specific time.", @@ -131,7 +131,7 @@ "previousIconButton": "Button allowing to switch to the left view.", "rightArrowIcon": "Icon displayed in the right view switch button.", "shortcuts": "Custom component for the shortcuts.", - "textField": "Form control with an input to render the value inside the default field. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render the value inside the default field.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/docs/translations/api-docs/date-pickers/multi-input-date-range-field/multi-input-date-range-field.json b/docs/translations/api-docs/date-pickers/multi-input-date-range-field/multi-input-date-range-field.json index 70fc7d15b6f48..fde01a7e1b8d1 100644 --- a/docs/translations/api-docs/date-pickers/multi-input-date-range-field/multi-input-date-range-field.json +++ b/docs/translations/api-docs/date-pickers/multi-input-date-range-field/multi-input-date-range-field.json @@ -1,6 +1,9 @@ { "componentDescription": "", "propDescriptions": { + "autoFocus": { + "description": "If true, the input element is focused during the first mount." + }, "classes": { "description": "Override or extend the styles applied to the component." }, "defaultValue": { "description": "The default value. Use when the component is not controlled." @@ -47,7 +50,7 @@ "description": "The date used to generate a part of the new value that is not present in the format when both value and defaultValue are empty. For example, on time fields it will be used to determine the date to set." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", diff --git a/docs/translations/api-docs/date-pickers/multi-input-date-time-range-field/multi-input-date-time-range-field.json b/docs/translations/api-docs/date-pickers/multi-input-date-time-range-field/multi-input-date-time-range-field.json index ed82c72587041..c06fd6bdb2613 100644 --- a/docs/translations/api-docs/date-pickers/multi-input-date-time-range-field/multi-input-date-time-range-field.json +++ b/docs/translations/api-docs/date-pickers/multi-input-date-time-range-field/multi-input-date-time-range-field.json @@ -2,6 +2,9 @@ "componentDescription": "", "propDescriptions": { "ampm": { "description": "12h/24h view for hour selection clock." }, + "autoFocus": { + "description": "If true, the input element is focused during the first mount." + }, "classes": { "description": "Override or extend the styles applied to the component." }, "defaultValue": { "description": "The default value. Use when the component is not controlled." @@ -64,7 +67,7 @@ "description": "The date used to generate a part of the new value that is not present in the format when both value and defaultValue are empty. For example, on time fields it will be used to determine the date to set." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", diff --git a/docs/translations/api-docs/date-pickers/multi-input-time-range-field/multi-input-time-range-field.json b/docs/translations/api-docs/date-pickers/multi-input-time-range-field/multi-input-time-range-field.json index 4032ca4f894b2..4adf72573ec25 100644 --- a/docs/translations/api-docs/date-pickers/multi-input-time-range-field/multi-input-time-range-field.json +++ b/docs/translations/api-docs/date-pickers/multi-input-time-range-field/multi-input-time-range-field.json @@ -2,6 +2,9 @@ "componentDescription": "", "propDescriptions": { "ampm": { "description": "12h/24h view for hour selection clock." }, + "autoFocus": { + "description": "If true, the input element is focused during the first mount." + }, "classes": { "description": "Override or extend the styles applied to the component." }, "defaultValue": { "description": "The default value. Use when the component is not controlled." @@ -56,7 +59,7 @@ "description": "The date used to generate a part of the new value that is not present in the format when both value and defaultValue are empty. For example, on time fields it will be used to determine the date to set." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableTime": { "description": "Disable specific time.", diff --git a/docs/translations/api-docs/date-pickers/pickers-sections-list.json b/docs/translations/api-docs/date-pickers/pickers-sections-list.json new file mode 100644 index 0000000000000..ebdaf7ab80b83 --- /dev/null +++ b/docs/translations/api-docs/date-pickers/pickers-sections-list.json @@ -0,0 +1,22 @@ +{ + "componentDescription": "", + "propDescriptions": { + "slotProps": { + "description": "The props used for each component slot.", + "deprecated": "", + "typeDescriptions": {} + }, + "slots": { + "description": "Overridable component slots.", + "deprecated": "", + "typeDescriptions": {} + } + }, + "classDescriptions": { + "sectionContent": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the content of a section" + } + }, + "slotDescriptions": { "root": "", "section": "", "sectionContent": "", "sectionSeparator": "" } +} diff --git a/docs/translations/api-docs/date-pickers/single-input-date-range-field/single-input-date-range-field.json b/docs/translations/api-docs/date-pickers/single-input-date-range-field/single-input-date-range-field.json index 7c070143e8f42..04844904a07e3 100644 --- a/docs/translations/api-docs/date-pickers/single-input-date-range-field/single-input-date-range-field.json +++ b/docs/translations/api-docs/date-pickers/single-input-date-range-field/single-input-date-range-field.json @@ -86,7 +86,7 @@ "description": "If true, the label is displayed as required and the input element is required." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", diff --git a/docs/translations/api-docs/date-pickers/single-input-date-time-range-field/single-input-date-time-range-field.json b/docs/translations/api-docs/date-pickers/single-input-date-time-range-field/single-input-date-time-range-field.json index 29e4d03fa583e..61340a97f9b64 100644 --- a/docs/translations/api-docs/date-pickers/single-input-date-time-range-field/single-input-date-time-range-field.json +++ b/docs/translations/api-docs/date-pickers/single-input-date-time-range-field/single-input-date-time-range-field.json @@ -103,7 +103,7 @@ "description": "If true, the label is displayed as required and the input element is required." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableDate": { "description": "Disable specific date.
Warning: This function can be called multiple times (e.g. when rendering date calendar, checking if focus can be moved to a certain date, etc.). Expensive computations can impact performance.", diff --git a/docs/translations/api-docs/date-pickers/single-input-time-range-field/single-input-time-range-field.json b/docs/translations/api-docs/date-pickers/single-input-time-range-field/single-input-time-range-field.json index 61b335bea6c21..afe1273696503 100644 --- a/docs/translations/api-docs/date-pickers/single-input-time-range-field/single-input-time-range-field.json +++ b/docs/translations/api-docs/date-pickers/single-input-time-range-field/single-input-time-range-field.json @@ -95,7 +95,7 @@ "description": "If true, the label is displayed as required and the input element is required." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableTime": { "description": "Disable specific time.", diff --git a/docs/translations/api-docs/date-pickers/time-field/time-field.json b/docs/translations/api-docs/date-pickers/time-field/time-field.json index 61b335bea6c21..afe1273696503 100644 --- a/docs/translations/api-docs/date-pickers/time-field/time-field.json +++ b/docs/translations/api-docs/date-pickers/time-field/time-field.json @@ -95,7 +95,7 @@ "description": "If true, the label is displayed as required and the input element is required." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableTime": { "description": "Disable specific time.", diff --git a/docs/translations/api-docs/date-pickers/time-picker/time-picker.json b/docs/translations/api-docs/date-pickers/time-picker/time-picker.json index a31efe7936275..c766c72c5bb59 100644 --- a/docs/translations/api-docs/date-pickers/time-picker/time-picker.json +++ b/docs/translations/api-docs/date-pickers/time-picker/time-picker.json @@ -93,7 +93,7 @@ "description": "The date used to generate the new value when both value and defaultValue are empty." }, "selectedSections": { - "description": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally." + "description": "The currently selected sections. This prop accepts four formats: 1. If a number is provided, the section at this index will be selected. 2. If a string of type FieldSectionType is provided, the first section with that name will be selected. 3. If "all" is provided, all the sections will be selected. 4. If null is provided, no section will be selected. If not provided, the selected sections will be handled internally." }, "shouldDisableTime": { "description": "Disable specific time.", @@ -154,7 +154,7 @@ "previousIconButton": "Button allowing to switch to the left view.", "rightArrowIcon": "Icon displayed in the right view switch button.", "shortcuts": "Custom component for the shortcuts.", - "textField": "Form control with an input to render the value inside the default field. Receives the same props as @mui/material/TextField.", + "textField": "Form control with an input to render the value inside the default field.", "toolbar": "Custom component for the toolbar rendered above the views." } } diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.test.tsx index 35d62b9a5d613..c7931687484f2 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.test.tsx +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.test.tsx @@ -2,7 +2,12 @@ import * as React from 'react'; import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker'; import { fireEvent, screen } from '@mui-internal/test-utils/createRenderer'; import { expect } from 'chai'; -import { createPickerRenderer, stubMatchMedia } from 'test/utils/pickers'; +import { + buildFieldInteractions, + createPickerRenderer, + getFieldInputRoot, + stubMatchMedia, +} from 'test/utils/pickers'; describe('', () => { const { render, clock } = createPickerRenderer({ @@ -10,8 +15,25 @@ describe('', () => { clockConfig: new Date(2018, 0, 1, 0, 0, 0, 0), }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: DateRangePicker, + }); + it('should not open mobile picker dialog when clicked on input', () => { - render(); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + fireEvent.click(getFieldInputRoot()); + clock.runToLast(); + + expect(screen.queryByRole('tooltip')).not.to.equal(null); + expect(screen.queryByRole('dialog')).to.equal(null); + + v7Response.unmount(); + + // Test with v6 input + renderWithProps({ enableAccessibleFieldDOMStructure: false }); fireEvent.click(screen.getAllByRole('textbox')[0]); clock.runToLast(); @@ -23,8 +45,8 @@ describe('', () => { const originalMatchMedia = window.matchMedia; window.matchMedia = stubMatchMedia(false); - render(); - fireEvent.click(screen.getAllByRole('textbox')[0]); + render(); + fireEvent.click(getFieldInputRoot()); clock.runToLast(); expect(screen.getByRole('dialog')).not.to.equal(null); diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.tsx b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.tsx index 448d9e82238c0..30bcf5403022a 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.tsx @@ -8,8 +8,12 @@ import { DesktopDateRangePicker } from '../DesktopDateRangePicker'; import { MobileDateRangePicker } from '../MobileDateRangePicker'; import { DateRangePickerProps } from './DateRangePicker.types'; -type DatePickerComponent = (( - props: DateRangePickerProps & React.RefAttributes, +type DatePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DateRangePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -22,8 +26,11 @@ type DatePickerComponent = (( * * - [DateRangePicker API](https://mui.com/x/api/date-pickers/date-range-picker/) */ -const DateRangePicker = React.forwardRef(function DateRangePicker( - inProps: DateRangePickerProps, +const DateRangePicker = React.forwardRef(function DateRangePicker< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DateRangePickerProps, ref: React.Ref, ) { const props = useThemeProps({ props: inProps, name: 'MuiDateRangePicker' }); @@ -131,6 +138,10 @@ DateRangePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -260,11 +271,11 @@ DateRangePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -281,10 +292,6 @@ DateRangePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.types.ts b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.types.ts index 619d929549386..cc56765af3668 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.types.ts @@ -14,13 +14,17 @@ export interface DateRangePickerSlots extends DesktopDateRangePickerSlots, MobileDateRangePickerSlots {} -export interface DateRangePickerSlotProps - extends DesktopDateRangePickerSlotProps, - MobileDateRangePickerSlotProps {} +export interface DateRangePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends DesktopDateRangePickerSlotProps, + MobileDateRangePickerSlotProps {} -export interface DateRangePickerProps - extends DesktopDateRangePickerProps, - MobileDateRangePickerProps { +export interface DateRangePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends DesktopDateRangePickerProps, + MobileDateRangePickerProps { /** * CSS media query when `Mobile` mode will be changed to `Desktop`. * @default '@media (pointer: fine)' @@ -36,5 +40,5 @@ export interface DateRangePickerProps * The props used for each component slot. * @default {} */ - slotProps?: DateRangePickerSlotProps; + slotProps?: DateRangePickerSlotProps; } diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.tsx b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.tsx index 73dcf309e8a2a..2f9d4dedd783b 100644 --- a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; +import { refType } from '@mui/utils'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useThemeProps } from '@mui/material/styles'; import { PickerValidDate } from '@mui/x-date-pickers/models'; @@ -7,13 +8,21 @@ import { DateTimeRangePickerProps } from './DateTimeRangePicker.types'; import { DesktopDateTimeRangePicker } from '../DesktopDateTimeRangePicker'; import { MobileDateTimeRangePicker } from '../MobileDateTimeRangePicker'; -type DateTimeRangePickerComponent = (( - props: DateTimeRangePickerProps & React.RefAttributes, +type DateTimeRangePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DateTimeRangePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; const DateTimeRangePicker = React.forwardRef(function DateTimeRangePicker< TDate extends PickerValidDate, ->(inProps: DateTimeRangePickerProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DateTimeRangePickerProps, + ref: React.Ref, +) { const props = useThemeProps({ props: inProps, name: 'MuiDateTimeRangePicker' }); const { desktopModeMediaQuery = '@media (pointer: fine)', ...other } = props; @@ -129,6 +138,10 @@ DateTimeRangePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -149,12 +162,7 @@ DateTimeRangePicker.propTypes = { * Pass a ref to the `input` element. * Ignored if the field has several inputs. */ - inputRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.object, - }), - ]), + inputRef: refType, /** * The label content. * Ignored if the field has several inputs. @@ -298,11 +306,11 @@ DateTimeRangePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -319,10 +327,6 @@ DateTimeRangePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.types.ts b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.types.ts index ef4308ee99016..109b4715c9638 100644 --- a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.types.ts @@ -14,13 +14,17 @@ export interface DateTimeRangePickerSlots extends DesktopDateTimeRangePickerSlots, MobileDateTimeRangePickerSlots {} -export interface DateTimeRangePickerSlotProps - extends DesktopDateTimeRangePickerSlotProps, - MobileDateTimeRangePickerSlotProps {} +export interface DateTimeRangePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends DesktopDateTimeRangePickerSlotProps, + MobileDateTimeRangePickerSlotProps {} -export interface DateTimeRangePickerProps - extends DesktopDateTimeRangePickerProps, - MobileDateTimeRangePickerProps { +export interface DateTimeRangePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends DesktopDateTimeRangePickerProps, + MobileDateTimeRangePickerProps { /** * CSS media query when `Mobile` mode will be changed to `Desktop`. * @default '@media (pointer: fine)' @@ -36,5 +40,5 @@ export interface DateTimeRangePickerProps * The props used for each component slot. * @default {} */ - slotProps?: DateTimeRangePickerSlotProps; + slotProps?: DateTimeRangePickerSlotProps; } diff --git a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx index 9db38329f9da7..53b40e695207e 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx @@ -13,8 +13,12 @@ import { useDesktopRangePicker } from '../internals/hooks/useDesktopRangePicker' import { validateDateRange } from '../internals/utils/validation/validateDateRange'; import { DateRange } from '../models'; -type DesktopDateRangePickerComponent = (( - props: DesktopDateRangePickerProps & React.RefAttributes, +type DesktopDateRangePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DesktopDateRangePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -29,11 +33,15 @@ type DesktopDateRangePickerComponent = (( */ const DesktopDateRangePicker = React.forwardRef(function DesktopDateRangePicker< TDate extends PickerValidDate, ->(inProps: DesktopDateRangePickerProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DesktopDateRangePickerProps, + ref: React.Ref, +) { // Props with the default values common to all date time pickers const defaultizedProps = useDateRangePickerDefaultizedProps< TDate, - DesktopDateRangePickerProps + DesktopDateRangePickerProps >(inProps, 'MuiDesktopDateRangePicker'); const viewRenderers: PickerViewRendererLookup, 'day', any, {}> = { @@ -65,7 +73,12 @@ const DesktopDateRangePicker = React.forwardRef(function DesktopDateRangePicker< }, }; - const { renderPicker } = useDesktopRangePicker({ + const { renderPicker } = useDesktopRangePicker< + TDate, + 'day', + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: rangeValueManager, valueType: 'date', @@ -160,6 +173,10 @@ DesktopDateRangePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -289,11 +306,11 @@ DesktopDateRangePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -310,10 +327,6 @@ DesktopDateRangePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.types.ts b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.types.ts index 364776776e06d..40957130e402b 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.types.ts @@ -15,13 +15,20 @@ export interface DesktopDateRangePickerSlots extends BaseDateRangePickerSlots, MakeOptional, 'field'> {} -export interface DesktopDateRangePickerSlotProps - extends BaseDateRangePickerSlotProps, - Omit, 'tabs'> {} +export interface DesktopDateRangePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends BaseDateRangePickerSlotProps, + Omit< + UseDesktopRangePickerSlotProps, + 'tabs' + > {} -export interface DesktopDateRangePickerProps - extends BaseDateRangePickerProps, - DesktopRangeOnlyPickerProps { +export interface DesktopDateRangePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends BaseDateRangePickerProps, + DesktopRangeOnlyPickerProps { /** * The number of calendars to render on **desktop**. * @default 2 @@ -36,5 +43,5 @@ export interface DesktopDateRangePickerProps * The props used for each component slot. * @default {} */ - slotProps?: DesktopDateRangePickerSlotProps; + slotProps?: DesktopDateRangePickerSlotProps; } diff --git a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/tests/DesktopDateRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/tests/DesktopDateRangePicker.test.tsx index c684edfa95a72..d0238aae4a5eb 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/tests/DesktopDateRangePicker.test.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/tests/DesktopDateRangePicker.test.tsx @@ -10,8 +10,11 @@ import { adapterToUse, AdapterClassToUse, openPicker, + getFieldSectionsContainer, } from 'test/utils/pickers'; +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + const getPickerDay = (name: string, picker = 'January 2018'): HTMLButtonElement => getByRole(screen.getByText(picker)?.parentElement?.parentElement!, 'gridcell', { name }); @@ -24,6 +27,7 @@ describe('', () => { it('should scroll current month to the active selection when focusing appropriate field', () => { render( , @@ -42,7 +46,10 @@ describe('', () => { it(`should not crash when opening picker with invalid date value`, async () => { render( - , + , ); openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); @@ -64,6 +71,7 @@ describe('', () => { ', () => { const handleTouchStart = spy(); render( ', () => { it('should open when clicking the start input', () => { const onOpen = spy(); - render(); + render(); openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); @@ -121,7 +130,7 @@ describe('', () => { it('should open when clicking the end input', () => { const onOpen = spy(); - render(); + render(); openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'end' }); @@ -133,11 +142,12 @@ describe('', () => { it(`should open when pressing "${key}" in the start input`, () => { const onOpen = spy(); - render(); + render(); - const startInput = screen.getAllByRole('textbox')[0]; + const startInput = getFieldSectionsContainer(); act(() => startInput.focus()); - fireEvent.keyDown(startInput, { key }); + // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target + fireEvent.keyDown(document.activeElement!, { key }); expect(onOpen.callCount).to.equal(1); expect(screen.getByRole('tooltip')).toBeVisible(); @@ -148,11 +158,12 @@ describe('', () => { it(`should open when pressing "${key}" in the end input`, () => { const onOpen = spy(); - render(); + render(); - const endInput = screen.getAllByRole('textbox')[1]; + const endInput = getFieldSectionsContainer(1); act(() => endInput.focus()); - fireEvent.keyDown(endInput, { key }); + // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target + fireEvent.keyDown(document.activeElement!, { key }); expect(onOpen.callCount).to.equal(1); expect(screen.getByRole('tooltip')).toBeVisible(); @@ -170,6 +181,7 @@ describe('', () => { render( ', () => { render( ', () => { render( ', () => { render( ', () => { render(
- +
, ); @@ -336,6 +356,7 @@ describe('', () => { render(
', () => { expect(onChange.callCount).to.equal(1); // Start date change expect(onAccept.callCount).to.equal(1); - // expect(onAccept.lastCall.args[0][0]).toEqualDateTime(new Date(2018, 0, 3)); - // expect(onAccept.lastCall.args[0][1]).toEqualDateTime(defaultValue[1]); - // expect(onClose.callCount).to.equal(1); + expect(onAccept.lastCall.args[0][0]).toEqualDateTime(new Date(2018, 0, 3)); + expect(onAccept.lastCall.args[0][1]).toEqualDateTime(defaultValue[1]); + expect(onClose.callCount).to.equal(1); }); it('should not call onClose or onAccept when clicking outside of the picker if not opened', () => { @@ -374,7 +395,14 @@ describe('', () => { const onAccept = spy(); const onClose = spy(); - render(); + render( + , + ); // Dismiss the picker userEvent.mousePress(document.body); @@ -383,24 +411,35 @@ describe('', () => { expect(onClose.callCount).to.equal(0); }); - it('should call onClose when blur the current field without prior change', () => { + it('should call onClose when blur the current field without prior change', function test() { + // test:unit does not call `blur` when focusing another element. + if (isJSDOM) { + this.skip(); + } + const onChange = spy(); const onAccept = spy(); const onClose = spy(); render( - - + + , ); openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); expect(screen.getByRole('tooltip')).toBeVisible(); - act(() => { - screen.getAllByRole('textbox')[0].blur(); - }); + document.querySelector('#test')!.focus(); clock.runToLast(); expect(onChange.callCount).to.equal(0); @@ -418,12 +457,16 @@ describe('', () => { ]; render( - , +
+ +
, ); openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); @@ -434,7 +477,7 @@ describe('', () => { clock.runToLast(); act(() => { - screen.getAllByRole('textbox')[1].blur(); + document.querySelector('#test')!.focus(); }); clock.runToLast(); @@ -456,6 +499,7 @@ describe('', () => { render( ', () => { render( ', () => { render( ', () => { render( ', () => { describe('disabled dates', () => { it('should respect the disablePast prop', () => { - render(); + render(); openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); @@ -565,7 +612,7 @@ describe('', () => { }); it('should respect the disableFuture prop', () => { - render(); + render(); openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); @@ -577,7 +624,12 @@ describe('', () => { }); it('should respect the minDate prop', () => { - render(); + render( + , + ); openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); @@ -589,7 +641,12 @@ describe('', () => { }); it('should respect the maxDate prop', () => { - render(); + render( + , + ); openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); diff --git a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/tests/describes.DesktopDateRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/tests/describes.DesktopDateRangePicker.test.tsx index a28dc1a2e8145..c4915573c45d0 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/tests/describes.DesktopDateRangePicker.test.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/tests/describes.DesktopDateRangePicker.test.tsx @@ -4,12 +4,12 @@ import { adapterToUse, createPickerRenderer, wrapPickerMount, - getTextbox, - expectInputPlaceholder, - expectInputValue, + expectFieldValueV7, describePicker, describeValue, describeRangeValidation, + getFieldInputRoot, + getFieldSectionsContainer, } from 'test/utils/pickers'; import { DesktopDateRangePicker } from '@mui/x-date-pickers-pro/DesktopDateRangePicker'; import { SingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; @@ -30,7 +30,7 @@ describe(' - Describes', () => { views: ['day'], })); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, render, muiName: 'MuiDesktopDateRangePicker', @@ -64,16 +64,22 @@ describe(' - Describes', () => { ], emptyValue: [null, null], assertRenderedValue: (expectedValues: any[]) => { - const textBoxes: HTMLInputElement[] = screen.getAllByRole('textbox'); - expectedValues.forEach((value, index) => { - const input = textBoxes[index]; - if (!value) { - expectInputPlaceholder(input, 'MM/DD/YYYY'); - } - expectInputValue(input, value ? adapterToUse.format(value, 'keyboardDate') : ''); - }); + const startSectionsContainer = getFieldSectionsContainer(0); + const expectedStartValueStr = expectedValues[0] + ? adapterToUse.format(expectedValues[0], 'keyboardDate') + : 'MM/DD/YYYY'; + expectFieldValueV7(startSectionsContainer, expectedStartValueStr); + + const endSectionsContainer = getFieldSectionsContainer(1); + const expectedEndValueStr = expectedValues[1] + ? adapterToUse.format(expectedValues[1], 'keyboardDate') + : 'MM/DD/YYYY'; + expectFieldValueV7(endSectionsContainer, expectedEndValueStr); }, - setNewValue: (value, { isOpened, applySameValue, setEndDate = false, selectSection }) => { + setNewValue: ( + value, + { isOpened, applySameValue, setEndDate = false, selectSection, pressKey }, + ) => { let newValue: any[]; if (applySameValue) { newValue = value; @@ -91,8 +97,7 @@ describe(' - Describes', () => { ); } else { selectSection('day'); - const input = screen.getAllByRole('textbox')[0]; - userEvent.keyPress(input, { key: 'ArrowUp' }); + pressKey(undefined, 'ArrowUp'); } return newValue; @@ -119,20 +124,24 @@ describe(' - Describes', () => { ], emptyValue: [null, null], assertRenderedValue: (expectedValues: any[]) => { - const input = screen.getByRole('textbox'); - const expectedValueStr = expectedValues - .map((value) => (value == null ? 'MM/DD/YYYY' : adapterToUse.format(value, 'keyboardDate'))) - .join(' – '); + const fieldRoot = getFieldInputRoot(0); - const isEmpty = expectedValues[0] == null && expectedValues[1] == null; + const expectedStartValueStr = expectedValues[0] + ? adapterToUse.format(expectedValues[0], 'keyboardDate') + : 'MM/DD/YYYY'; - if (isEmpty) { - expectInputPlaceholder(input, expectedValueStr); - } + const expectedEndValueStr = expectedValues[1] + ? adapterToUse.format(expectedValues[1], 'keyboardDate') + : 'MM/DD/YYYY'; + + const expectedValueStr = `${expectedStartValueStr} – ${expectedEndValueStr}`; - expectInputValue(input, isEmpty ? '' : expectedValueStr); + expectFieldValueV7(fieldRoot, expectedValueStr); }, - setNewValue: (value, { isOpened, applySameValue, setEndDate = false, selectSection }) => { + setNewValue: ( + value, + { isOpened, applySameValue, setEndDate = false, selectSection, pressKey }, + ) => { let newValue: any[]; if (applySameValue) { newValue = value; @@ -150,8 +159,7 @@ describe(' - Describes', () => { ); } else { selectSection('day'); - const input = getTextbox(); - userEvent.keyPress(input, { key: 'ArrowUp' }); + pressKey(undefined, 'ArrowUp'); } return newValue; diff --git a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx index bb879ab87a70a..3d189efa0a049 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx @@ -10,6 +10,7 @@ import { } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { resolveComponentProps } from '@mui/base/utils'; +import { refType } from '@mui/utils'; import { renderDigitalClockTimeView, renderMultiSectionDigitalClockTimeView, @@ -37,11 +38,23 @@ import { DateTimeRangePickerTimeWrapper } from '../DateTimeRangePicker/DateTimeR import { RANGE_VIEW_HEIGHT } from '../internals/constants/dimensions'; import { DesktopDateTimeRangePickerLayout } from './DesktopDateTimeRangePickerLayout'; -const rendererInterceptor = function rendererInterceptor( +const rendererInterceptor = function rendererInterceptor< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +>( inViewRenderers: PickerViewRendererLookup, DateTimeRangePickerView, any, any>, popperView: DateTimeRangePickerView, rendererProps: DefaultizedProps< - Omit, 'onChange'>, + Omit< + UseDesktopRangePickerProps< + TDate, + DateTimeRangePickerView, + TEnableAccessibleFieldDOMStructure, + any, + any + >, + 'onChange' + >, 'rangePosition' | 'onRangePositionChange' | 'openTo' >, ) { @@ -89,17 +102,25 @@ const rendererInterceptor = function rendererInterceptor( - props: DesktopDateTimeRangePickerProps & React.RefAttributes, +type DesktopDateRangePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DesktopDateTimeRangePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; const DesktopDateTimeRangePicker = React.forwardRef(function DesktopDateTimeRangePicker< TDate extends PickerValidDate, ->(inProps: DesktopDateTimeRangePickerProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DesktopDateTimeRangePickerProps, + ref: React.Ref, +) { // Props with the default values common to all date time range pickers const defaultizedProps = useDateTimeRangePickerDefaultizedProps< TDate, - DesktopDateTimeRangePickerProps + DesktopDateTimeRangePickerProps >(inProps, 'MuiDesktopDateTimeRangePicker'); const renderTimeView = defaultizedProps.shouldRenderTimeInASingleColumn @@ -164,7 +185,12 @@ const DesktopDateTimeRangePicker = React.forwardRef(function DesktopDateTimeRang }, }; - const { renderPicker } = useDesktopRangePicker({ + const { renderPicker } = useDesktopRangePicker< + TDate, + DateTimeRangePickerView, + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: rangeValueManager, valueType: 'date-time', @@ -270,6 +296,10 @@ DesktopDateTimeRangePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -290,12 +320,7 @@ DesktopDateTimeRangePicker.propTypes = { * Pass a ref to the `input` element. * Ignored if the field has several inputs. */ - inputRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.object, - }), - ]), + inputRef: refType, /** * The label content. * Ignored if the field has several inputs. @@ -439,11 +464,11 @@ DesktopDateTimeRangePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -460,10 +485,6 @@ DesktopDateTimeRangePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.types.ts b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.types.ts index a692b4307fe44..b778e12dabc52 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.types.ts @@ -16,13 +16,24 @@ export interface DesktopDateTimeRangePickerSlots extends BaseDateTimeRangePickerSlots, MakeOptional, 'field'> {} -export interface DesktopDateTimeRangePickerSlotProps - extends BaseDateTimeRangePickerSlotProps, - Omit, 'tabs' | 'toolbar'> {} +export interface DesktopDateTimeRangePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends BaseDateTimeRangePickerSlotProps, + Omit< + UseDesktopRangePickerSlotProps< + TDate, + DateTimeRangePickerView, + TEnableAccessibleFieldDOMStructure + >, + 'tabs' | 'toolbar' + > {} -export interface DesktopDateTimeRangePickerProps - extends BaseDateTimeRangePickerProps, - DesktopRangeOnlyPickerProps { +export interface DesktopDateTimeRangePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends BaseDateTimeRangePickerProps, + DesktopRangeOnlyPickerProps { /** * The number of calendars to render on **desktop**. * @default 1 @@ -37,5 +48,5 @@ export interface DesktopDateTimeRangePickerProps * The props used for each component slot. * @default {} */ - slotProps?: DesktopDateTimeRangePickerSlotProps; + slotProps?: DesktopDateTimeRangePickerSlotProps; } diff --git a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx index 1d6779f0a20c7..fbb6cc4ec78cd 100644 --- a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx @@ -13,8 +13,12 @@ import { useMobileRangePicker } from '../internals/hooks/useMobileRangePicker'; import { validateDateRange } from '../internals/utils/validation/validateDateRange'; import { DateRange } from '../models'; -type MobileDateRangePickerComponent = (( - props: MobileDateRangePickerProps & React.RefAttributes, +type MobileDateRangePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: MobileDateRangePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -29,11 +33,15 @@ type MobileDateRangePickerComponent = (( */ const MobileDateRangePicker = React.forwardRef(function MobileDateRangePicker< TDate extends PickerValidDate, ->(inProps: MobileDateRangePickerProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: MobileDateRangePickerProps, + ref: React.Ref, +) { // Props with the default values common to all date time pickers const defaultizedProps = useDateRangePickerDefaultizedProps< TDate, - MobileDateRangePickerProps + MobileDateRangePickerProps >(inProps, 'MuiMobileDateRangePicker'); const viewRenderers: PickerViewRendererLookup, 'day', any, {}> = { @@ -66,7 +74,12 @@ const MobileDateRangePicker = React.forwardRef(function MobileDateRangePicker< }, }; - const { renderPicker } = useMobileRangePicker({ + const { renderPicker } = useMobileRangePicker< + TDate, + 'day', + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: rangeValueManager, valueType: 'date', @@ -156,6 +169,10 @@ MobileDateRangePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -285,11 +302,11 @@ MobileDateRangePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -306,10 +323,6 @@ MobileDateRangePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.types.ts b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.types.ts index 9bb677638253b..0edee69d176c8 100644 --- a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.types.ts @@ -15,13 +15,17 @@ export interface MobileDateRangePickerSlots extends BaseDateRangePickerSlots, MakeOptional, 'field'> {} -export interface MobileDateRangePickerSlotProps - extends BaseDateRangePickerSlotProps, - Omit, 'tabs'> {} +export interface MobileDateRangePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends BaseDateRangePickerSlotProps, + Omit, 'tabs'> {} -export interface MobileDateRangePickerProps - extends BaseDateRangePickerProps, - MobileRangeOnlyPickerProps { +export interface MobileDateRangePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends BaseDateRangePickerProps, + MobileRangeOnlyPickerProps { /** * Overridable component slots. * @default {} @@ -31,5 +35,5 @@ export interface MobileDateRangePickerProps * The props used for each component slot. * @default {} */ - slotProps?: MobileDateRangePickerSlotProps; + slotProps?: MobileDateRangePickerSlotProps; } diff --git a/packages/x-date-pickers-pro/src/MobileDateRangePicker/tests/MobileDateRangePicker.test.tsx b/packages/x-date-pickers-pro/src/MobileDateRangePicker/tests/MobileDateRangePicker.test.tsx index eae38b8466045..076f95ed07af5 100644 --- a/packages/x-date-pickers-pro/src/MobileDateRangePicker/tests/MobileDateRangePicker.test.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateRangePicker/tests/MobileDateRangePicker.test.tsx @@ -3,7 +3,12 @@ import { spy } from 'sinon'; import { expect } from 'chai'; import { screen, userEvent, fireEvent } from '@mui-internal/test-utils'; import { MobileDateRangePicker } from '@mui/x-date-pickers-pro/MobileDateRangePicker'; -import { createPickerRenderer, adapterToUse, openPicker } from 'test/utils/pickers'; +import { + createPickerRenderer, + adapterToUse, + openPicker, + getFieldSectionsContainer, +} from 'test/utils/pickers'; import { DateRange } from '@mui/x-date-pickers-pro/models'; describe('', () => { @@ -13,7 +18,7 @@ describe('', () => { it('should open when focusing the start input', () => { const onOpen = spy(); - render(); + render(); openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'start' }); @@ -24,7 +29,7 @@ describe('', () => { it('should open when focusing the end input', () => { const onOpen = spy(); - render(); + render(); openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'end' }); @@ -43,6 +48,7 @@ describe('', () => { render( ', () => { render( ', () => { render( ', () => { render( ', () => { render( ', () => { render( ', () => { render( ', () => { }); it('should correctly set focused styles when input is focused', () => { - render(); + render(); - const firstInput = screen.getAllByRole('textbox')[0]; - fireEvent.focus(firstInput); + const startSectionsContainer = getFieldSectionsContainer(); + fireEvent.focus(startSectionsContainer); expect(screen.getByText('Start', { selector: 'label' })).to.have.class('Mui-focused'); }); - - it('should render "readonly" input elements', () => { - render(); - - screen.getAllByRole('textbox').forEach((input) => { - expect(input).to.have.attribute('readonly'); - }); - }); }); // TODO: Write test diff --git a/packages/x-date-pickers-pro/src/MobileDateRangePicker/tests/describes.MobileDateRangePicker.test.tsx b/packages/x-date-pickers-pro/src/MobileDateRangePicker/tests/describes.MobileDateRangePicker.test.tsx index 4ebc728ce436c..f2458c04dc86a 100644 --- a/packages/x-date-pickers-pro/src/MobileDateRangePicker/tests/describes.MobileDateRangePicker.test.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateRangePicker/tests/describes.MobileDateRangePicker.test.tsx @@ -6,11 +6,11 @@ import { createPickerRenderer, wrapPickerMount, openPicker, - expectInputPlaceholder, - expectInputValue, + expectFieldValueV7, describeRangeValidation, describeValue, describePicker, + getFieldSectionsContainer, } from 'test/utils/pickers'; import { describeConformance } from 'test/utils/describeConformance'; @@ -30,7 +30,7 @@ describe(' - Describes', () => { variant: 'mobile', })); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, render, muiName: 'MuiMobileDateRangePicker', @@ -64,19 +64,17 @@ describe(' - Describes', () => { ], emptyValue: [null, null], assertRenderedValue: (expectedValues: any[]) => { - // `getAllByRole('textbox')` does not work here, because inputs are `readonly` - const textBoxes: HTMLInputElement[] = [ - screen.getByLabelText('Start'), - screen.getByLabelText('End'), - ]; - expectedValues.forEach((value, index) => { - const input = textBoxes[index]; - // TODO: Support single range input - if (!value) { - expectInputPlaceholder(input, 'MM/DD/YYYY'); - } - expectInputValue(input, value ? adapterToUse.format(value, 'keyboardDate') : ''); - }); + const startSectionsContainer = getFieldSectionsContainer(0); + const expectedStartValueStr = expectedValues[0] + ? adapterToUse.format(expectedValues[0], 'keyboardDate') + : 'MM/DD/YYYY'; + expectFieldValueV7(startSectionsContainer, expectedStartValueStr); + + const endFieldRoot = getFieldSectionsContainer(1); + const expectedEndValueStr = expectedValues[1] + ? adapterToUse.format(expectedValues[1], 'keyboardDate') + : 'MM/DD/YYYY'; + expectFieldValueV7(endFieldRoot, expectedEndValueStr); }, setNewValue: (value, { isOpened, applySameValue, setEndDate = false }) => { let newValue: any[]; diff --git a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx index ec2091dc9d2f6..5346acf574457 100644 --- a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; +import { refType } from '@mui/utils'; import { DIALOG_WIDTH, VIEW_HEIGHT, @@ -36,11 +37,23 @@ import { MultiInputDateTimeRangeField } from '../MultiInputDateTimeRangeField'; import { DateTimeRangePickerTimeWrapper } from '../DateTimeRangePicker/DateTimeRangePickerTimeWrapper'; import { RANGE_VIEW_HEIGHT } from '../internals/constants/dimensions'; -const rendererInterceptor = function rendererInterceptor( +const rendererInterceptor = function rendererInterceptor< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +>( inViewRenderers: PickerViewRendererLookup, DateTimeRangePickerView, any, any>, popperView: DateTimeRangePickerView, rendererProps: DefaultizedProps< - Omit, 'onChange'>, + Omit< + UseMobileRangePickerProps< + TDate, + DateTimeRangePickerView, + TEnableAccessibleFieldDOMStructure, + any, + any + >, + 'onChange' + >, 'rangePosition' | 'onRangePositionChange' | 'openTo' >, ) { @@ -103,17 +116,25 @@ const rendererInterceptor = function rendererInterceptor( - props: MobileDateTimeRangePickerProps & React.RefAttributes, +type MobileDateRangePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: MobileDateTimeRangePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; const MobileDateTimeRangePicker = React.forwardRef(function MobileDateTimeRangePicker< TDate extends PickerValidDate, ->(inProps: MobileDateTimeRangePickerProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: MobileDateTimeRangePickerProps, + ref: React.Ref, +) { // Props with the default values common to all date time range pickers const defaultizedProps = useDateTimeRangePickerDefaultizedProps< TDate, - MobileDateTimeRangePickerProps + MobileDateTimeRangePickerProps >(inProps, 'MuiMobileDateTimeRangePicker'); const renderTimeView = defaultizedProps.shouldRenderTimeInASingleColumn @@ -164,7 +185,12 @@ const MobileDateTimeRangePicker = React.forwardRef(function MobileDateTimeRangeP }, }; - const { renderPicker } = useMobileRangePicker({ + const { renderPicker } = useMobileRangePicker< + TDate, + DateTimeRangePickerView, + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: rangeValueManager, valueType: 'date-time', @@ -265,6 +291,10 @@ MobileDateTimeRangePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -285,12 +315,7 @@ MobileDateTimeRangePicker.propTypes = { * Pass a ref to the `input` element. * Ignored if the field has several inputs. */ - inputRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.object, - }), - ]), + inputRef: refType, /** * The label content. * Ignored if the field has several inputs. @@ -434,11 +459,11 @@ MobileDateTimeRangePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -455,10 +480,6 @@ MobileDateTimeRangePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.types.ts b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.types.ts index de00d8476319d..38ca47cfd319d 100644 --- a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.types.ts @@ -16,13 +16,24 @@ export interface MobileDateTimeRangePickerSlots extends BaseDateTimeRangePickerSlots, MakeOptional, 'field'> {} -export interface MobileDateTimeRangePickerSlotProps - extends BaseDateTimeRangePickerSlotProps, - Omit, 'tabs' | 'toolbar'> {} +export interface MobileDateTimeRangePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends BaseDateTimeRangePickerSlotProps, + Omit< + UseMobileRangePickerSlotProps< + TDate, + DateTimeRangePickerView, + TEnableAccessibleFieldDOMStructure + >, + 'tabs' | 'toolbar' + > {} -export interface MobileDateTimeRangePickerProps - extends BaseDateTimeRangePickerProps, - MobileRangeOnlyPickerProps { +export interface MobileDateTimeRangePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends BaseDateTimeRangePickerProps, + MobileRangeOnlyPickerProps { /** * Overridable component slots. * @default {} @@ -32,5 +43,5 @@ export interface MobileDateTimeRangePickerProps * The props used for each component slot. * @default {} */ - slotProps?: MobileDateTimeRangePickerSlotProps; + slotProps?: MobileDateTimeRangePickerSlotProps; } diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx index 6cc3b8d20e970..640f42f6036b9 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx @@ -13,14 +13,17 @@ import { } from '@mui/utils'; import { splitFieldInternalAndForwardedProps, - FieldsTextFieldProps, convertFieldResponseIntoMuiTextFieldProps, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { MultiInputDateRangeFieldProps } from './MultiInputDateRangeField.types'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; +import { + MultiInputDateRangeFieldProps, + MultiInputDateRangeFieldSlotProps, +} from './MultiInputDateRangeField.types'; import { useMultiInputDateRangeField } from '../internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField'; import { UseDateRangeFieldProps } from '../internals/models/dateRange'; -import { MultiInputRangeFieldClasses } from '../models'; +import { MultiInputRangeFieldClasses, RangePosition } from '../models'; export const multiInputDateRangeFieldClasses: MultiInputRangeFieldClasses = generateUtilityClasses( 'MuiMultiInputDateRangeField', @@ -30,7 +33,7 @@ export const multiInputDateRangeFieldClasses: MultiInputRangeFieldClasses = gene export const getMultiInputDateRangeFieldUtilityClass = (slot: string) => generateUtilityClass('MuiMultiInputDateRangeField', slot); -const useUtilityClasses = (ownerState: MultiInputDateRangeFieldProps) => { +const useUtilityClasses = (ownerState: MultiInputDateRangeFieldProps) => { const { classes } = ownerState; const slots = { root: ['root'], @@ -60,8 +63,12 @@ const MultiInputDateRangeFieldSeparator = styled( }, )({}); -type MultiInputDateRangeFieldComponent = (( - props: MultiInputDateRangeFieldProps & React.RefAttributes, +type MultiInputDateRangeFieldComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: MultiInputDateRangeFieldProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -76,26 +83,24 @@ type MultiInputDateRangeFieldComponent = (( */ const MultiInputDateRangeField = React.forwardRef(function MultiInputDateRangeField< TDate extends PickerValidDate, ->(inProps: MultiInputDateRangeFieldProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: MultiInputDateRangeFieldProps, + ref: React.Ref, +) { const themeProps = useThemeProps({ props: inProps, name: 'MuiMultiInputDateRangeField', }); - const { internalProps: dateFieldInternalProps, forwardedProps } = - splitFieldInternalAndForwardedProps< - typeof themeProps, - keyof Omit< - UseDateRangeFieldProps, - 'unstableFieldRef' | 'disabled' | 'clearable' | 'onClear' - > - >(themeProps, 'date'); + const { internalProps, forwardedProps } = splitFieldInternalAndForwardedProps< + typeof themeProps, + keyof Omit, 'clearable' | 'onClear'> + >(themeProps, 'date'); const { slots, slotProps, - disabled, - autoFocus, unstableStartFieldRef, unstableEndFieldRef, className, @@ -117,14 +122,29 @@ const MultiInputDateRangeField = React.forwardRef(function MultiInputDateRangeFi className: clsx(className, classes.root), }); - const TextField = slots?.textField ?? MuiTextField; - const startTextFieldProps: FieldsTextFieldProps = useSlotProps({ + const TextField = + slots?.textField ?? + (inProps.enableAccessibleFieldDOMStructure ? PickersTextField : MuiTextField); + const startTextFieldProps = useSlotProps< + typeof TextField, + MultiInputDateRangeFieldSlotProps['textField'], + {}, + MultiInputDateRangeFieldProps & { + position: RangePosition; + } + >({ elementType: TextField, externalSlotProps: slotProps?.textField, - additionalProps: { autoFocus }, ownerState: { ...ownerState, position: 'start' }, }); - const endTextFieldProps: FieldsTextFieldProps = useSlotProps({ + const endTextFieldProps = useSlotProps< + typeof TextField, + MultiInputDateRangeFieldSlotProps['textField'], + {}, + MultiInputDateRangeFieldProps & { + position: RangePosition; + } + >({ elementType: TextField, externalSlotProps: slotProps?.textField, ownerState: { ...ownerState, position: 'end' }, @@ -138,8 +158,12 @@ const MultiInputDateRangeField = React.forwardRef(function MultiInputDateRangeFi className: classes.separator, }); - const fieldResponse = useMultiInputDateRangeField({ - sharedProps: { ...dateFieldInternalProps, disabled }, + const fieldResponse = useMultiInputDateRangeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof startTextFieldProps + >({ + sharedProps: internalProps, startTextFieldProps, endTextFieldProps, unstableStartFieldRef, @@ -163,6 +187,9 @@ MultiInputDateRangeField.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- + /** + * If `true`, the `input` element is focused during the first mount. + */ autoFocus: PropTypes.bool, /** * Override or extend the styles applied to the component. @@ -203,6 +230,10 @@ MultiInputDateRangeField.propTypes = { * Add an element between each child. */ divider: PropTypes.node, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.bool, /** * Format of the date when rendered in the input(s). */ @@ -256,11 +287,11 @@ MultiInputDateRangeField.propTypes = { referenceDate: PropTypes.object, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -277,10 +308,6 @@ MultiInputDateRangeField.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.types.ts b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.types.ts index 8b741a80e9201..5485e46e6220f 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.types.ts @@ -6,26 +6,44 @@ import Stack, { StackProps } from '@mui/material/Stack'; import TextField from '@mui/material/TextField'; import { UseDateRangeFieldProps } from '../internals/models/dateRange'; import { UseMultiInputRangeFieldParams } from '../internals/hooks/useMultiInputRangeField/useMultiInputRangeField.types'; -import { MultiInputFieldRefs } from '../internals/models/fields'; -import { MultiInputRangeFieldClasses, RangePosition } from '../models'; +import { MultiInputFieldRefs, MultiInputRangeFieldClasses, RangePosition } from '../models'; export type UseMultiInputDateRangeFieldParams< TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TTextFieldSlotProps extends {}, -> = UseMultiInputRangeFieldParams, TTextFieldSlotProps>; +> = UseMultiInputRangeFieldParams< + UseMultiInputDateRangeFieldProps, + TTextFieldSlotProps +>; -export interface UseMultiInputDateRangeFieldProps - extends Omit, 'unstableFieldRef' | 'clearable' | 'onClear'>, +export interface UseMultiInputDateRangeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends Omit< + UseDateRangeFieldProps, + 'unstableFieldRef' | 'clearable' | 'onClear' + >, MultiInputFieldRefs {} export type UseMultiInputDateRangeFieldComponentProps< TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TChildProps extends {}, -> = Omit> & - UseMultiInputDateRangeFieldProps; +> = Omit< + TChildProps, + keyof UseMultiInputDateRangeFieldProps +> & + UseMultiInputDateRangeFieldProps; -export interface MultiInputDateRangeFieldProps - extends UseMultiInputDateRangeFieldComponentProps> { +export interface MultiInputDateRangeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends UseMultiInputDateRangeFieldComponentProps< + TDate, + TEnableAccessibleFieldDOMStructure, + Omit + > { autoFocus?: boolean; /** * Override or extend the styles applied to the component. @@ -40,12 +58,9 @@ export interface MultiInputDateRangeFieldProps * The props used for each component slot. * @default {} */ - slotProps?: MultiInputDateRangeFieldSlotProps; + slotProps?: MultiInputDateRangeFieldSlotProps; } -export type MultiInputDateRangeFieldOwnerState = - MultiInputDateRangeFieldProps; - export interface MultiInputDateRangeFieldSlots { /** * Element rendered at the root. @@ -55,8 +70,7 @@ export interface MultiInputDateRangeFieldSlots { /** * Form control with an input to render a date. * It is rendered twice: once for the start date and once for the end date. - * Receives the same props as `@mui/material/TextField`. - * @default TextField from '@mui/material' + * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ textField?: React.ElementType; /** @@ -66,12 +80,25 @@ export interface MultiInputDateRangeFieldSlots { separator?: React.ElementType; } -export interface MultiInputDateRangeFieldSlotProps { - root?: SlotComponentProps>; +export interface MultiInputDateRangeFieldSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> { + root?: SlotComponentProps< + typeof Stack, + {}, + MultiInputDateRangeFieldProps + >; textField?: SlotComponentProps< typeof TextField, {}, - MultiInputDateRangeFieldOwnerState & { position: RangePosition } + MultiInputDateRangeFieldProps & { + position: RangePosition; + } + >; + separator?: SlotComponentProps< + typeof Typography, + {}, + MultiInputDateRangeFieldProps >; - separator?: SlotComponentProps>; } diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/tests/MultiInputDateRangeField.conformance.test.tsx b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/tests/MultiInputDateRangeField.conformance.test.tsx index 4ae82e23ec61f..d0a04e59dd147 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/tests/MultiInputDateRangeField.conformance.test.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/tests/MultiInputDateRangeField.conformance.test.tsx @@ -6,7 +6,7 @@ import { describeConformance } from 'test/utils/describeConformance'; describe('', () => { const { render } = createPickerRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, inheritComponent: 'div', render, diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/tests/MultiInputDateRangeField.validation.test.tsx b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/tests/MultiInputDateRangeField.validation.test.tsx index 56348ce63bb1a..8552a54ddb71d 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/tests/MultiInputDateRangeField.validation.test.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/tests/MultiInputDateRangeField.validation.test.tsx @@ -1,7 +1,10 @@ -import { screen } from '@mui-internal/test-utils'; -import { fireEvent } from '@mui-internal/test-utils/createRenderer'; import { MultiInputDateRangeField } from '@mui/x-date-pickers-pro/MultiInputDateRangeField'; -import { createPickerRenderer, adapterToUse, describeRangeValidation } from 'test/utils/pickers'; +import { + createPickerRenderer, + adapterToUse, + describeRangeValidation, + setValueOnFieldInput, +} from 'test/utils/pickers'; describe('', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); @@ -11,11 +14,8 @@ describe('', () => { clock, componentFamily: 'field', views: ['year', 'month', 'day'], - inputValue: (value, { setEndDate } = {}) => { - const inputs = screen.getAllByRole('textbox'); - const input = inputs[setEndDate ? 1 : 0]; - input.focus(); - fireEvent.change(input, { target: { value: adapterToUse.format(value, 'keyboardDate') } }); + setValue: (value, { setEndDate } = {}) => { + setValueOnFieldInput(adapterToUse.format(value, 'keyboardDate'), setEndDate ? 1 : 0); }, })); }); diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx index 64ec39d23c327..28ace8cfa6ad2 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx @@ -13,14 +13,17 @@ import { } from '@mui/utils'; import { splitFieldInternalAndForwardedProps, - FieldsTextFieldProps, convertFieldResponseIntoMuiTextFieldProps, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { MultiInputDateTimeRangeFieldProps } from './MultiInputDateTimeRangeField.types'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; +import { + MultiInputDateTimeRangeFieldProps, + MultiInputDateTimeRangeFieldSlotProps, +} from './MultiInputDateTimeRangeField.types'; import { useMultiInputDateTimeRangeField } from '../internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField'; import { UseDateTimeRangeFieldProps } from '../internals/models/dateTimeRange'; -import { MultiInputRangeFieldClasses } from '../models'; +import { MultiInputRangeFieldClasses, RangePosition } from '../models'; export const multiInputDateTimeRangeFieldClasses: MultiInputRangeFieldClasses = generateUtilityClasses('MuiMultiInputDateTimeRangeField', ['root', 'separator']); @@ -28,7 +31,7 @@ export const multiInputDateTimeRangeFieldClasses: MultiInputRangeFieldClasses = export const getMultiInputDateTimeRangeFieldUtilityClass = (slot: string) => generateUtilityClass('MuiMultiInputDateTimeRangeField', slot); -const useUtilityClasses = (ownerState: MultiInputDateTimeRangeFieldProps) => { +const useUtilityClasses = (ownerState: MultiInputDateTimeRangeFieldProps) => { const { classes } = ownerState; const slots = { root: ['root'], @@ -58,8 +61,12 @@ const MultiInputDateTimeRangeFieldSeparator = styled( }, )({}); -type MultiInputDateTimeRangeFieldComponent = (( - props: MultiInputDateTimeRangeFieldProps & React.RefAttributes, +type MultiInputDateTimeRangeFieldComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: MultiInputDateTimeRangeFieldProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -74,26 +81,24 @@ type MultiInputDateTimeRangeFieldComponent = (( */ const MultiInputDateTimeRangeField = React.forwardRef(function MultiInputDateTimeRangeField< TDate extends PickerValidDate, ->(inProps: MultiInputDateTimeRangeFieldProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: MultiInputDateTimeRangeFieldProps, + ref: React.Ref, +) { const themeProps = useThemeProps({ props: inProps, name: 'MuiMultiInputDateTimeRangeField', }); - const { internalProps: dateTimeFieldInternalProps, forwardedProps } = - splitFieldInternalAndForwardedProps< - typeof themeProps, - keyof Omit< - UseDateTimeRangeFieldProps, - 'unstableFieldRef' | 'disabled' | 'clearable' | 'onClear' - > - >(themeProps, 'date-time'); + const { internalProps, forwardedProps } = splitFieldInternalAndForwardedProps< + typeof themeProps, + keyof Omit, 'clearable' | 'onClear'> + >(themeProps, 'date-time'); const { slots, slotProps, - disabled, - autoFocus, unstableStartFieldRef, unstableEndFieldRef, className, @@ -115,14 +120,29 @@ const MultiInputDateTimeRangeField = React.forwardRef(function MultiInputDateTim className: clsx(className, classes.root), }); - const TextField = slots?.textField ?? MuiTextField; - const startTextFieldProps: FieldsTextFieldProps = useSlotProps({ + const TextField = + slots?.textField ?? + (inProps.enableAccessibleFieldDOMStructure ? PickersTextField : MuiTextField); + const startTextFieldProps = useSlotProps< + typeof TextField, + MultiInputDateTimeRangeFieldSlotProps['textField'], + {}, + MultiInputDateTimeRangeFieldProps & { + position: RangePosition; + } + >({ elementType: TextField, externalSlotProps: slotProps?.textField, - additionalProps: { autoFocus }, ownerState: { ...ownerState, position: 'start' }, }); - const endTextFieldProps: FieldsTextFieldProps = useSlotProps({ + const endTextFieldProps = useSlotProps< + typeof TextField, + MultiInputDateTimeRangeFieldSlotProps['textField'], + {}, + MultiInputDateTimeRangeFieldProps & { + position: RangePosition; + } + >({ elementType: TextField, externalSlotProps: slotProps?.textField, ownerState: { ...ownerState, position: 'end' }, @@ -136,8 +156,12 @@ const MultiInputDateTimeRangeField = React.forwardRef(function MultiInputDateTim className: classes.separator, }); - const fieldResponse = useMultiInputDateTimeRangeField({ - sharedProps: { ...dateTimeFieldInternalProps, disabled }, + const fieldResponse = useMultiInputDateTimeRangeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof startTextFieldProps + >({ + sharedProps: internalProps, startTextFieldProps, endTextFieldProps, unstableStartFieldRef, @@ -166,6 +190,9 @@ MultiInputDateTimeRangeField.propTypes = { * @default `utils.is12HourCycleInCurrentLocale()` */ ampm: PropTypes.bool, + /** + * If `true`, the `input` element is focused during the first mount. + */ autoFocus: PropTypes.bool, /** * Override or extend the styles applied to the component. @@ -211,6 +238,10 @@ MultiInputDateTimeRangeField.propTypes = { * Add an element between each child. */ divider: PropTypes.node, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.bool, /** * Format of the date when rendered in the input(s). */ @@ -287,11 +318,11 @@ MultiInputDateTimeRangeField.propTypes = { referenceDate: PropTypes.object, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -308,10 +339,6 @@ MultiInputDateTimeRangeField.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts index f83594e58fb1f..6199fe64b64b2 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts @@ -4,31 +4,46 @@ import Typography from '@mui/material/Typography'; import Stack, { StackProps } from '@mui/material/Stack'; import TextField from '@mui/material/TextField'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { - UseDateTimeRangeFieldDefaultizedProps, - UseDateTimeRangeFieldProps, -} from '../internals/models/dateTimeRange'; -import { MultiInputFieldRefs } from '../internals/models/fields'; +import { UseDateTimeRangeFieldProps } from '../internals/models/dateTimeRange'; import { UseMultiInputRangeFieldParams } from '../internals/hooks/useMultiInputRangeField/useMultiInputRangeField.types'; -import { MultiInputRangeFieldClasses, RangePosition } from '../models'; +import { MultiInputFieldRefs, MultiInputRangeFieldClasses, RangePosition } from '../models'; export type UseMultiInputDateTimeRangeFieldParams< TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TTextFieldSlotProps extends {}, -> = UseMultiInputRangeFieldParams, TTextFieldSlotProps>; +> = UseMultiInputRangeFieldParams< + UseMultiInputDateTimeRangeFieldProps, + TTextFieldSlotProps +>; -export interface UseMultiInputDateTimeRangeFieldProps - extends Omit, 'unstableFieldRef' | 'clearable' | 'onClear'>, +export interface UseMultiInputDateTimeRangeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends Omit< + UseDateTimeRangeFieldProps, + 'unstableFieldRef' | 'clearable' | 'onClear' + >, MultiInputFieldRefs {} export type UseMultiInputDateTimeRangeFieldComponentProps< TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TChildProps extends {}, -> = Omit> & - UseMultiInputDateTimeRangeFieldProps; +> = Omit< + TChildProps, + keyof UseMultiInputDateTimeRangeFieldProps +> & + UseMultiInputDateTimeRangeFieldProps; -export interface MultiInputDateTimeRangeFieldProps - extends UseMultiInputDateTimeRangeFieldComponentProps> { +export interface MultiInputDateTimeRangeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends UseMultiInputDateTimeRangeFieldComponentProps< + TDate, + TEnableAccessibleFieldDOMStructure, + Omit + > { autoFocus?: boolean; /** * Override or extend the styles applied to the component. @@ -43,12 +58,9 @@ export interface MultiInputDateTimeRangeFieldProps; + slotProps?: MultiInputDateTimeRangeFieldSlotProps; } -export type MultiInputDateTimeRangeFieldOwnerState = - MultiInputDateTimeRangeFieldProps; - export interface MultiInputDateTimeRangeFieldSlots { /** * Element rendered at the root. @@ -58,8 +70,7 @@ export interface MultiInputDateTimeRangeFieldSlots { /** * Form control with an input to render a date and time. * It is rendered twice: once for the start date time and once for the end date time. - * Receives the same props as `@mui/material/TextField`. - * @default TextField from '@mui/material' + * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ textField?: React.ElementType; /** @@ -69,22 +80,25 @@ export interface MultiInputDateTimeRangeFieldSlots { separator?: React.ElementType; } -export interface MultiInputDateTimeRangeFieldSlotProps { - root?: SlotComponentProps>; +export interface MultiInputDateTimeRangeFieldSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> { + root?: SlotComponentProps< + typeof Stack, + {}, + MultiInputDateTimeRangeFieldProps + >; textField?: SlotComponentProps< typeof TextField, {}, - MultiInputDateTimeRangeFieldOwnerState & { position: RangePosition } + MultiInputDateTimeRangeFieldProps & { + position: RangePosition; + } >; separator?: SlotComponentProps< typeof Typography, {}, - MultiInputDateTimeRangeFieldOwnerState + MultiInputDateTimeRangeFieldProps >; } - -export type UseMultiInputDateTimeRangeFieldDefaultizedProps< - TDate extends PickerValidDate, - AdditionalProps extends {}, -> = UseDateTimeRangeFieldDefaultizedProps & - Omit; diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.conformance.test.tsx b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.conformance.test.tsx index d2b46d40e7dcc..a0ebd8203fc14 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.conformance.test.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.conformance.test.tsx @@ -6,7 +6,7 @@ import { describeConformance } from 'test/utils/describeConformance'; describe('', () => { const { render } = createPickerRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, inheritComponent: 'div', render, diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.validation.test.tsx b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.validation.test.tsx index d31a5dbcedc31..4fd962807ba7f 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.validation.test.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.validation.test.tsx @@ -1,7 +1,10 @@ -import { screen } from '@mui-internal/test-utils'; -import { fireEvent } from '@mui-internal/test-utils/createRenderer'; import { MultiInputDateTimeRangeField } from '@mui/x-date-pickers-pro/MultiInputDateTimeRangeField'; -import { createPickerRenderer, adapterToUse, describeRangeValidation } from 'test/utils/pickers'; +import { + createPickerRenderer, + adapterToUse, + describeRangeValidation, + setValueOnFieldInput, +} from 'test/utils/pickers'; describe('', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); @@ -11,13 +14,8 @@ describe('', () => { clock, componentFamily: 'field', views: ['year', 'month', 'day', 'hours', 'minutes'], - inputValue: (value, { setEndDate } = {}) => { - const inputs = screen.getAllByRole('textbox'); - const input = inputs[setEndDate ? 1 : 0]; - input.focus(); - fireEvent.change(input, { - target: { value: adapterToUse.format(value, 'keyboardDateTime') }, - }); + setValue: (value, { setEndDate } = {}) => { + setValueOnFieldInput(adapterToUse.format(value, 'keyboardDateTime'), setEndDate ? 1 : 0); }, })); }); diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx index 03ad3860bce26..593a224064a80 100644 --- a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx @@ -13,14 +13,17 @@ import { } from '@mui/utils'; import { splitFieldInternalAndForwardedProps, - FieldsTextFieldProps, convertFieldResponseIntoMuiTextFieldProps, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { MultiInputTimeRangeFieldProps } from './MultiInputTimeRangeField.types'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; +import { + MultiInputTimeRangeFieldProps, + MultiInputTimeRangeFieldSlotProps, +} from './MultiInputTimeRangeField.types'; import { useMultiInputTimeRangeField } from '../internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField'; import { UseTimeRangeFieldProps } from '../internals/models/timeRange'; -import { MultiInputRangeFieldClasses } from '../models'; +import { MultiInputRangeFieldClasses, RangePosition } from '../models'; export const multiInputTimeRangeFieldClasses: MultiInputRangeFieldClasses = generateUtilityClasses( 'MuiMultiInputTimeRangeField', @@ -30,7 +33,7 @@ export const multiInputTimeRangeFieldClasses: MultiInputRangeFieldClasses = gene export const getMultiInputTimeRangeFieldUtilityClass = (slot: string) => generateUtilityClass('MuiMultiInputTimeRangeField', slot); -const useUtilityClasses = (ownerState: MultiInputTimeRangeFieldProps) => { +const useUtilityClasses = (ownerState: MultiInputTimeRangeFieldProps) => { const { classes } = ownerState; const slots = { root: ['root'], @@ -60,8 +63,12 @@ const MultiInputTimeRangeFieldSeparator = styled( }, )({}); -type MultiInputTimeRangeFieldComponent = (( - props: MultiInputTimeRangeFieldProps & React.RefAttributes, +type MultiInputTimeRangeFieldComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: MultiInputTimeRangeFieldProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -76,26 +83,24 @@ type MultiInputTimeRangeFieldComponent = (( */ const MultiInputTimeRangeField = React.forwardRef(function MultiInputTimeRangeField< TDate extends PickerValidDate, ->(inProps: MultiInputTimeRangeFieldProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: MultiInputTimeRangeFieldProps, + ref: React.Ref, +) { const themeProps = useThemeProps({ props: inProps, name: 'MuiMultiInputTimeRangeField', }); - const { internalProps: timeFieldInternalProps, forwardedProps } = - splitFieldInternalAndForwardedProps< - typeof themeProps, - keyof Omit< - UseTimeRangeFieldProps, - 'unstableFieldRef' | 'disabled' | 'clearable' | 'onClear' - > - >(themeProps, 'time'); + const { internalProps, forwardedProps } = splitFieldInternalAndForwardedProps< + typeof themeProps, + keyof Omit, 'clearable' | 'onClear'> + >(themeProps, 'time'); const { slots, slotProps, - disabled, - autoFocus, unstableStartFieldRef, unstableEndFieldRef, className, @@ -117,15 +122,30 @@ const MultiInputTimeRangeField = React.forwardRef(function MultiInputTimeRangeFi className: clsx(className, classes.root), }); - const TextField = slots?.textField ?? MuiTextField; - const startTextFieldProps: FieldsTextFieldProps = useSlotProps({ + const TextField = + slots?.textField ?? + (inProps.enableAccessibleFieldDOMStructure ? PickersTextField : MuiTextField); + const startTextFieldProps = useSlotProps< + typeof TextField, + MultiInputTimeRangeFieldSlotProps['textField'], + {}, + MultiInputTimeRangeFieldProps & { + position: RangePosition; + } + >({ elementType: TextField, externalSlotProps: slotProps?.textField, - additionalProps: { autoFocus }, ownerState: { ...ownerState, position: 'start' }, }); - const endTextFieldProps: FieldsTextFieldProps = useSlotProps({ + const endTextFieldProps = useSlotProps< + typeof TextField, + MultiInputTimeRangeFieldSlotProps['textField'], + {}, + MultiInputTimeRangeFieldProps & { + position: RangePosition; + } + >({ elementType: TextField, externalSlotProps: slotProps?.textField, ownerState: { ...ownerState, position: 'end' }, @@ -139,8 +159,12 @@ const MultiInputTimeRangeField = React.forwardRef(function MultiInputTimeRangeFi className: classes.separator, }); - const fieldResponse = useMultiInputTimeRangeField({ - sharedProps: { ...timeFieldInternalProps, disabled }, + const fieldResponse = useMultiInputTimeRangeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof startTextFieldProps + >({ + sharedProps: internalProps, startTextFieldProps, endTextFieldProps, unstableStartFieldRef, @@ -169,6 +193,9 @@ MultiInputTimeRangeField.propTypes = { * @default `utils.is12HourCycleInCurrentLocale()` */ ampm: PropTypes.bool, + /** + * If `true`, the `input` element is focused during the first mount. + */ autoFocus: PropTypes.bool, /** * Override or extend the styles applied to the component. @@ -214,6 +241,10 @@ MultiInputTimeRangeField.propTypes = { * Add an element between each child. */ divider: PropTypes.node, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.bool, /** * Format of the date when rendered in the input(s). */ @@ -274,11 +305,11 @@ MultiInputTimeRangeField.propTypes = { referenceDate: PropTypes.object, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -295,10 +326,6 @@ MultiInputTimeRangeField.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific time. diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts index 3dde1f4009337..aaedafde0864b 100644 --- a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts @@ -4,31 +4,46 @@ import Typography from '@mui/material/Typography'; import Stack, { StackProps } from '@mui/material/Stack'; import TextField from '@mui/material/TextField'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { - UseTimeRangeFieldDefaultizedProps, - UseTimeRangeFieldProps, -} from '../internals/models/timeRange'; +import { UseTimeRangeFieldProps } from '../internals/models/timeRange'; import { UseMultiInputRangeFieldParams } from '../internals/hooks/useMultiInputRangeField/useMultiInputRangeField.types'; -import { MultiInputFieldRefs } from '../internals/models/fields'; -import { MultiInputRangeFieldClasses, RangePosition } from '../models'; +import { MultiInputFieldRefs, MultiInputRangeFieldClasses, RangePosition } from '../models'; export type UseMultiInputTimeRangeFieldParams< TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TTextFieldSlotProps extends {}, -> = UseMultiInputRangeFieldParams, TTextFieldSlotProps>; +> = UseMultiInputRangeFieldParams< + UseMultiInputTimeRangeFieldProps, + TTextFieldSlotProps +>; -export interface UseMultiInputTimeRangeFieldProps - extends Omit, 'unstableFieldRef' | 'clearable' | 'onClear'>, +export interface UseMultiInputTimeRangeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends Omit< + UseTimeRangeFieldProps, + 'unstableFieldRef' | 'clearable' | 'onClear' + >, MultiInputFieldRefs {} export type UseMultiInputTimeRangeFieldComponentProps< TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TChildProps extends {}, -> = Omit> & - UseMultiInputTimeRangeFieldProps; +> = Omit< + TChildProps, + keyof UseMultiInputTimeRangeFieldProps +> & + UseMultiInputTimeRangeFieldProps; -export interface MultiInputTimeRangeFieldProps - extends UseMultiInputTimeRangeFieldComponentProps> { +export interface MultiInputTimeRangeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends UseMultiInputTimeRangeFieldComponentProps< + TDate, + TEnableAccessibleFieldDOMStructure, + Omit + > { autoFocus?: boolean; /** * Override or extend the styles applied to the component. @@ -43,12 +58,9 @@ export interface MultiInputTimeRangeFieldProps * The props used for each component slot. * @default {} */ - slotProps?: MultiInputTimeRangeFieldSlotProps; + slotProps?: MultiInputTimeRangeFieldSlotProps; } -export type MultiInputTimeRangeFieldOwnerState = - MultiInputTimeRangeFieldProps; - export interface MultiInputTimeRangeFieldSlots { /** * Element rendered at the root. @@ -58,8 +70,7 @@ export interface MultiInputTimeRangeFieldSlots { /** * Form control with an input to render a time. * It is rendered twice: once for the start time and once for the end time. - * Receives the same props as `@mui/material/TextField`. - * @default TextField from '@mui/material' + * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ textField?: React.ElementType; /** @@ -69,18 +80,25 @@ export interface MultiInputTimeRangeFieldSlots { separator?: React.ElementType; } -export interface MultiInputTimeRangeFieldSlotProps { - root?: SlotComponentProps>; +export interface MultiInputTimeRangeFieldSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> { + root?: SlotComponentProps< + typeof Stack, + {}, + MultiInputTimeRangeFieldProps + >; textField?: SlotComponentProps< typeof TextField, {}, - MultiInputTimeRangeFieldOwnerState & { position: RangePosition } + MultiInputTimeRangeFieldProps & { + position: RangePosition; + } + >; + separator?: SlotComponentProps< + typeof Typography, + {}, + MultiInputTimeRangeFieldProps >; - separator?: SlotComponentProps>; } - -export type UseMultiInputTimeRangeFieldDefaultizedProps< - TDate extends PickerValidDate, - AdditionalProps extends {}, -> = UseTimeRangeFieldDefaultizedProps & - Omit; diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.conformance.test.tsx b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.conformance.test.tsx index a7da491cbbcc6..e5efca7026ec6 100644 --- a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.conformance.test.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.conformance.test.tsx @@ -6,7 +6,7 @@ import { describeConformance } from 'test/utils/describeConformance'; describe('', () => { const { render } = createPickerRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, inheritComponent: 'div', render, diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.validation.test.tsx b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.validation.test.tsx index 18980367909e3..a84da9d27b0a9 100644 --- a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.validation.test.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.validation.test.tsx @@ -1,7 +1,10 @@ -import { screen } from '@mui-internal/test-utils'; -import { fireEvent } from '@mui-internal/test-utils/createRenderer'; import { MultiInputTimeRangeField } from '@mui/x-date-pickers-pro/MultiInputTimeRangeField'; -import { createPickerRenderer, adapterToUse, describeRangeValidation } from 'test/utils/pickers'; +import { + createPickerRenderer, + adapterToUse, + describeRangeValidation, + setValueOnFieldInput, +} from 'test/utils/pickers'; describe('', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); @@ -11,13 +14,8 @@ describe('', () => { clock, componentFamily: 'field', views: ['hours', 'minutes'], - inputValue: (value, { setEndDate } = {}) => { - const inputs = screen.getAllByRole('textbox'); - const input = inputs[setEndDate ? 1 : 0]; - input.focus(); - fireEvent.change(input, { - target: { value: adapterToUse.format(value, 'fullTime') }, - }); + setValue: (value, { setEndDate } = {}) => { + setValueOnFieldInput(adapterToUse.format(value, 'fullTime'), setEndDate ? 1 : 0); }, })); }); diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx index ca3db91f8151f..d758ed6ea40fc 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx @@ -3,19 +3,21 @@ import PropTypes from 'prop-types'; import MuiTextField from '@mui/material/TextField'; import { useThemeProps } from '@mui/material/styles'; import { useSlotProps } from '@mui/base/utils'; +import { refType } from '@mui/utils'; import { useClearableField } from '@mui/x-date-pickers/hooks'; import { convertFieldResponseIntoMuiTextFieldProps } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { refType } from '@mui/utils'; -import { - SingleInputDateRangeFieldProps, - SingleInputDateRangeFieldSlotProps, -} from './SingleInputDateRangeField.types'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; +import { SingleInputDateRangeFieldProps } from './SingleInputDateRangeField.types'; import { useSingleInputDateRangeField } from './useSingleInputDateRangeField'; -import { FieldType } from '../internals/models'; +import { FieldType } from '../models'; -type DateRangeFieldComponent = (( - props: SingleInputDateRangeFieldProps & React.RefAttributes, +type DateRangeFieldComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: SingleInputDateRangeFieldProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any; fieldType?: FieldType }; /** @@ -30,7 +32,11 @@ type DateRangeFieldComponent = (( */ const SingleInputDateRangeField = React.forwardRef(function SingleInputDateRangeField< TDate extends PickerValidDate, ->(inProps: SingleInputDateRangeFieldProps, inRef: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: SingleInputDateRangeFieldProps, + inRef: React.Ref, +) { const themeProps = useThemeProps({ props: inProps, name: 'MuiSingleInputDateRangeField', @@ -40,13 +46,10 @@ const SingleInputDateRangeField = React.forwardRef(function SingleInputDateRange const ownerState = themeProps; - const TextField = slots?.textField ?? MuiTextField; - const textFieldProps: SingleInputDateRangeFieldProps = useSlotProps< - typeof TextField, - SingleInputDateRangeFieldSlotProps['textField'], - SingleInputDateRangeFieldProps, - SingleInputDateRangeFieldProps - >({ + const TextField = + slots?.textField ?? + (inProps.enableAccessibleFieldDOMStructure ? PickersTextField : MuiTextField); + const textFieldProps = useSlotProps({ elementType: TextField, externalSlotProps: slotProps?.textField, externalForwardedProps: other, @@ -54,13 +57,17 @@ const SingleInputDateRangeField = React.forwardRef(function SingleInputDateRange additionalProps: { ref: inRef, }, - }); + }) as SingleInputDateRangeFieldProps; // TODO: Remove when mui/material-ui#35088 will be merged textFieldProps.inputProps = { ...inputProps, ...textFieldProps.inputProps }; textFieldProps.InputProps = { ...InputProps, ...textFieldProps.InputProps }; - const fieldResponse = useSingleInputDateRangeField(textFieldProps); + const fieldResponse = useSingleInputDateRangeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof textFieldProps + >(textFieldProps); const convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); const processedFieldProps = useClearableField({ @@ -117,6 +124,10 @@ SingleInputDateRangeField.propTypes = { * @default false */ disablePast: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.bool, /** * If `true`, the component is displayed in focused state. */ @@ -243,11 +254,11 @@ SingleInputDateRangeField.propTypes = { required: PropTypes.bool, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -264,10 +275,6 @@ SingleInputDateRangeField.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts index 8e022305bf596..e53a975bd6743 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts @@ -1,55 +1,67 @@ import * as React from 'react'; import { SlotComponentProps } from '@mui/base/utils'; import TextField from '@mui/material/TextField'; -import { FieldsTextFieldProps } from '@mui/x-date-pickers/internals'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { UseClearableFieldSlots, UseClearableFieldSlotProps } from '@mui/x-date-pickers/hooks'; -import { UseDateRangeFieldDefaultizedProps, UseDateRangeFieldProps } from '../internals/models'; +import { UseFieldInternalProps } from '@mui/x-date-pickers/internals'; +import { BuiltInFieldTextFieldProps, PickerValidDate } from '@mui/x-date-pickers/models'; +import { + ExportedUseClearableFieldProps, + UseClearableFieldSlots, + UseClearableFieldSlotProps, +} from '@mui/x-date-pickers/hooks'; +import { UseDateRangeFieldProps } from '../internals/models'; +import type { DateRange, RangeFieldSection, DateRangeValidationError } from '../models'; -export interface UseSingleInputDateRangeFieldProps - extends UseDateRangeFieldProps {} - -export type UseSingleInputDateRangeFieldDefaultizedProps< - TDate extends PickerValidDate, - AdditionalProps extends {}, -> = UseDateRangeFieldDefaultizedProps & - Omit; - -export type UseSingleInputDateRangeFieldComponentProps< +export interface UseSingleInputDateRangeFieldProps< TDate extends PickerValidDate, - TChildProps extends {}, -> = Omit> & - UseSingleInputDateRangeFieldProps; + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseDateRangeFieldProps, + ExportedUseClearableFieldProps, + Pick< + UseFieldInternalProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + DateRangeValidationError + >, + 'unstableFieldRef' + > {} export type SingleInputDateRangeFieldProps< TDate extends PickerValidDate, - TChildProps extends {} = FieldsTextFieldProps, -> = UseSingleInputDateRangeFieldComponentProps & { - /** - * Overridable component slots. - * @default {} - */ - slots?: SingleInputDateRangeFieldSlots; - /** - * The props used for each component slot. - * @default {} - */ - slotProps?: SingleInputDateRangeFieldSlotProps; -}; - -export type SingleInputDateRangeFieldOwnerState = - SingleInputDateRangeFieldProps; + TEnableAccessibleFieldDOMStructure extends boolean = false, +> = Omit< + BuiltInFieldTextFieldProps, + keyof UseSingleInputDateRangeFieldProps +> & + UseSingleInputDateRangeFieldProps & { + /** + * Overridable component slots. + * @default {} + */ + slots?: SingleInputDateRangeFieldSlots; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: SingleInputDateRangeFieldSlotProps; + }; export interface SingleInputDateRangeFieldSlots extends UseClearableFieldSlots { /** * Form control with an input to render the value. - * Receives the same props as `@mui/material/TextField`. - * @default TextField from '@mui/material' + * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ textField?: React.ElementType; } -export interface SingleInputDateRangeFieldSlotProps - extends UseClearableFieldSlotProps { - textField?: SlotComponentProps>; +export interface SingleInputDateRangeFieldSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseClearableFieldSlotProps { + textField?: SlotComponentProps< + typeof TextField, + {}, + SingleInputDateRangeFieldProps + >; } diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/index.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/index.ts index fd4c20cd97d1e..01056dcdfb086 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/index.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/index.ts @@ -2,6 +2,5 @@ export { SingleInputDateRangeField } from './SingleInputDateRangeField'; export { useSingleInputDateRangeField as unstable_useSingleInputDateRangeField } from './useSingleInputDateRangeField'; export type { UseSingleInputDateRangeFieldProps, - UseSingleInputDateRangeFieldComponentProps, SingleInputDateRangeFieldProps, } from './SingleInputDateRangeField.types'; diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/describes.SingleInputDateRangeField.test.tsx b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/describes.SingleInputDateRangeField.test.tsx index dd7e5073e1e89..57704368d4a31 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/describes.SingleInputDateRangeField.test.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/describes.SingleInputDateRangeField.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import TextField from '@mui/material/TextField'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; import { SingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; import { createPickerRenderer, wrapPickerMount, describeRangeValidation } from 'test/utils/pickers'; import { describeConformance } from 'test/utils/describeConformance'; @@ -7,9 +7,9 @@ import { describeConformance } from 'test/utils/describeConformance'; describe(' - Describes', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, - inheritComponent: TextField, + inheritComponent: PickersTextField, render, muiName: 'MuiSingleInputDateRangeField', wrapMount: wrapPickerMount, diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/editing.SingleInputDateRangeField.test.tsx b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/editing.SingleInputDateRangeField.test.tsx index f24be9b99be94..3ac365c240edf 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/editing.SingleInputDateRangeField.test.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/editing.SingleInputDateRangeField.test.tsx @@ -1,193 +1,493 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import { SingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; -import { userEvent, fireEvent } from '@mui-internal/test-utils'; -import { expectInputValue, describeAdapters } from 'test/utils/pickers'; +import { fireEvent } from '@mui-internal/test-utils'; +import { + expectFieldValueV7, + expectFieldValueV6, + describeAdapters, + getTextbox, +} from 'test/utils/pickers'; describe(' - Editing', () => { describeAdapters(`key: Delete`, SingleInputDateRangeField, ({ adapter, renderWithProps }) => { it('should clear all the sections when all sections are selected and all sections are completed', () => { - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], format: `${adapter.formats.month} ${adapter.formats.year}`, }); - selectSection('month'); + v7Response.selectSection('month'); // Select all sections - userEvent.keyPress(input, { key: 'a', ctrlKey: true }); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); - userEvent.keyPress(input, { key: 'Delete' }); - expectInputValue(input, 'MMMM YYYY – MMMM YYYY'); + fireEvent.keyDown(v7Response.getSectionsContainer(), { key: 'Delete' }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY – MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], + format: `${adapter.formats.month} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + + fireEvent.keyDown(input, { key: 'Delete' }); + expectFieldValueV6(input, 'MMMM YYYY – MMMM YYYY'); }); it('should clear all the sections when all sections are selected and not all sections are completed', () => { - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + format: `${adapter.formats.month} ${adapter.formats.year}`, + }); + + v7Response.selectSection('month'); + + // Set a value for the "month" section + fireEvent.input(v7Response.getActiveSection(0), { target: { innerHTML: 'j' } }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'January YYYY – MMMM YYYY'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + fireEvent.keyDown(v7Response.getSectionsContainer(), { key: 'Delete' }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY – MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ format: `${adapter.formats.month} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, }); - selectSection('month'); + const input = getTextbox(); + v6Response.selectSection('month'); // Set a value for the "month" section fireEvent.change(input, { target: { value: 'j YYYY – MMMM YYYY' }, }); // Press "j" - expectInputValue(input, 'January YYYY – MMMM YYYY'); + expectFieldValueV6(input, 'January YYYY – MMMM YYYY'); // Select all sections - userEvent.keyPress(input, { key: 'a', ctrlKey: true }); + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); - userEvent.keyPress(input, { key: 'Delete' }); - expectInputValue(input, 'MMMM YYYY – MMMM YYYY'); + fireEvent.keyDown(input, { key: 'Delete' }); + expectFieldValueV6(input, 'MMMM YYYY – MMMM YYYY'); }); it('should not call `onChange` when clearing all sections and both dates are already empty', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, - defaultValue: [null, null], - onChange, + onChange: onChangeV7, }); - selectSection('month'); + v7Response.selectSection('month'); // Select all sections - userEvent.keyPress(input, { key: 'a', ctrlKey: true }); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); - userEvent.keyPress(input, { key: 'Delete' }); - expect(onChange.callCount).to.equal(0); - }); + fireEvent.keyDown(v7Response.getSectionsContainer(), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(0); - it('should call `onChange` when clearing the each section of each date', () => { - const handleChange = spy(); + v7Response.unmount(); - const { selectSection, input } = renderWithProps({ + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ format: `${adapter.formats.month} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + onChange: onChangeV6, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + + fireEvent.keyDown(input, { key: 'Delete' }); + expect(onChangeV6.callCount).to.equal(0); + }); + + it('should call `onChange` when clearing the first and last section of each date', () => { + // Test with v7 input + const onChangeV7 = spy(); + + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], - onChange: handleChange, + onChange: onChangeV7, }); - selectSection('month'); + v7Response.selectSection('month'); // Start date - userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(1); - userEvent.keyPress(input, { key: 'ArrowRight' }); - userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(2); - expect(handleChange.lastCall.firstArg[0]).to.equal(null); - expect(handleChange.lastCall.firstArg[1]).toEqualDateTime( - adapter.addYears(adapter.date(), 1), - ); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(1); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(1); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(2), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(2); + expect(onChangeV7.lastCall.firstArg[0]).to.equal(null); + expect(onChangeV7.lastCall.firstArg[1]).toEqualDateTime(adapter.addYears(adapter.date(), 1)); // End date - userEvent.keyPress(input, { key: 'ArrowRight' }); - userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(3); - userEvent.keyPress(input, { key: 'ArrowRight' }); - userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(4); - expect(handleChange.lastCall.firstArg[0]).to.equal(null); - expect(handleChange.lastCall.firstArg[1]).to.equal(null); + fireEvent.keyDown(v7Response.getActiveSection(2), { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(3), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(3); + fireEvent.keyDown(v7Response.getActiveSection(3), { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(4), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(3); + fireEvent.keyDown(v7Response.getActiveSection(4), { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(5), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(4); + expect(onChangeV7.lastCall.firstArg[0]).to.equal(null); + expect(onChangeV7.lastCall.firstArg[1]).to.equal(null); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], + onChange: onChangeV6, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + // Start date + fireEvent.keyDown(input, { key: 'Delete' }); + expect(onChangeV6.callCount).to.equal(1); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.keyDown(input, { key: 'Delete' }); + expect(onChangeV6.callCount).to.equal(1); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.keyDown(input, { key: 'Delete' }); + expect(onChangeV6.callCount).to.equal(2); + expect(onChangeV6.lastCall.firstArg[0]).to.equal(null); + expect(onChangeV6.lastCall.firstArg[1]).toEqualDateTime(adapter.addYears(adapter.date(), 1)); + + // End date + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.keyDown(input, { key: 'Delete' }); + expect(onChangeV6.callCount).to.equal(3); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.keyDown(input, { key: 'Delete' }); + expect(onChangeV6.callCount).to.equal(3); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.keyDown(input, { key: 'Delete' }); + expect(onChangeV6.callCount).to.equal(4); + expect(onChangeV6.lastCall.firstArg[0]).to.equal(null); + expect(onChangeV6.lastCall.firstArg[1]).to.equal(null); }); it('should not call `onChange` if the section is already empty', () => { - const handleChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { selectSection, input } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], - onChange: handleChange, + onChange: onChangeV7, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(1); + v7Response.selectSection('month'); + + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(1); + + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(1); + + v7Response.unmount(); - userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(1); + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `${adapter.formats.month} ${adapter.formats.year}`, + defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], + onChange: onChangeV6, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + fireEvent.keyDown(input, { key: 'Delete' }); + expect(onChangeV6.callCount).to.equal(1); + + fireEvent.keyDown(input, { key: 'Delete' }); + expect(onChangeV6.callCount).to.equal(1); }); }); describeAdapters( `Backspace editing`, SingleInputDateRangeField, - ({ adapter, renderWithProps, testFieldChange }) => { + ({ adapter, renderWithProps }) => { it('should clear all the sections when all sections are selected and all sections are completed (Backspace)', () => { - testFieldChange({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], format: `${adapter.formats.month} ${adapter.formats.year}`, - keyStrokes: [{ value: '', expected: 'MMMM YYYY – MMMM YYYY' }], }); + + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + v7Response.pressKey(null, ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY – MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], + format: `${adapter.formats.month} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + + fireEvent.change(input, { target: { value: '' } }); + expectFieldValueV6(input, 'MMMM YYYY – MMMM YYYY'); }); it('should clear all the sections when all sections are selected and not all sections are completed (Backspace)', () => { - testFieldChange({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, - keyStrokes: [ - { value: 'j YYYY – MMMM YYYY', expected: 'January YYYY – MMMM YYYY' }, - { value: '', expected: 'MMMM YYYY – MMMM YYYY' }, - ], }); + + v7Response.selectSection('month'); + + // Set a value for the "month" section + fireEvent.input(v7Response.getActiveSection(0), { target: { innerHTML: 'j' } }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'January YYYY – MMMM YYYY'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + v7Response.pressKey(null, ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY – MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + format: `${adapter.formats.month} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + // Set a value for the "month" section + fireEvent.change(input, { + target: { value: 'j YYYY – MMMM YYYY' }, + }); // Press "j" + expectFieldValueV6(input, 'January YYYY – MMMM YYYY'); + + // Select all sections + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + + fireEvent.change(input, { target: { value: '' } }); + expectFieldValueV6(input, 'MMMM YYYY – MMMM YYYY'); }); it('should not call `onChange` when clearing all sections and both dates are already empty (Backspace)', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - testFieldChange({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, - keyStrokes: [{ value: '', expected: 'MMMM YYYY – MMMM YYYY' }], + onChange: onChangeV7, }); - expect(onChange.callCount).to.equal(0); - }); + v7Response.selectSection('month'); - it('should call `onChange` when clearing the each section of each date (Backspace)', () => { - const onChange = spy(); + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); - const { selectSection, input } = renderWithProps({ + v7Response.pressKey(null, ''); + expect(onChangeV7.callCount).to.equal(0); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ format: `${adapter.formats.month} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + onChange: onChangeV6, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + + fireEvent.change(input, { target: { value: 'Delete' } }); + expect(onChangeV6.callCount).to.equal(0); + }); + + it('should call `onChange` when clearing the first and last section of each date (Backspace)', () => { + // Test with v7 input + const onChangeV7 = spy(); + + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], - onChange, + onChange: onChangeV7, }); - selectSection('month'); + v7Response.selectSection('month'); // Start date - fireEvent.change(input, { target: { value: ' 2022 – June 2023' } }); - expect(onChange.callCount).to.equal(1); - userEvent.keyPress(input, { key: 'ArrowRight' }); - fireEvent.change(input, { target: { value: 'MMMM – June 2023' } }); - expect(onChange.callCount).to.equal(2); - expect(onChange.lastCall.firstArg[0]).to.equal(null); - expect(onChange.lastCall.firstArg[1]).toEqualDateTime(adapter.addYears(adapter.date(), 1)); + v7Response.pressKey(0, ''); + expect(onChangeV7.callCount).to.equal(1); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowRight' }); + v7Response.pressKey(1, ''); + expect(onChangeV7.callCount).to.equal(1); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'ArrowRight' }); + v7Response.pressKey(2, ''); + expect(onChangeV7.callCount).to.equal(2); + expect(onChangeV7.lastCall.firstArg[0]).to.equal(null); + expect(onChangeV7.lastCall.firstArg[1]).toEqualDateTime( + adapter.addYears(adapter.date(), 1), + ); // End date - userEvent.keyPress(input, { key: 'ArrowRight' }); - fireEvent.change(input, { target: { value: 'MMMM YYYY – 2023' } }); - expect(onChange.callCount).to.equal(3); - userEvent.keyPress(input, { key: 'ArrowRight' }); - fireEvent.change(input, { target: { value: 'MMMM YYYY – MMMM ' } }); - expect(onChange.callCount).to.equal(4); - expect(onChange.lastCall.firstArg[0]).to.equal(null); - expect(onChange.lastCall.firstArg[1]).to.equal(null); + fireEvent.keyDown(v7Response.getActiveSection(2), { key: 'ArrowRight' }); + v7Response.pressKey(3, ''); + expect(onChangeV7.callCount).to.equal(3); + fireEvent.keyDown(v7Response.getActiveSection(3), { key: 'ArrowRight' }); + v7Response.pressKey(4, ''); + expect(onChangeV7.callCount).to.equal(3); + fireEvent.keyDown(v7Response.getActiveSection(4), { key: 'ArrowRight' }); + v7Response.pressKey(5, ''); + expect(onChangeV7.callCount).to.equal(4); + expect(onChangeV7.lastCall.firstArg[0]).to.equal(null); + expect(onChangeV7.lastCall.firstArg[1]).to.equal(null); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], + onChange: onChangeV6, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + // Start date + fireEvent.change(input, { target: { value: '/15/2022 – 06/15/2023' } }); + expect(onChangeV6.callCount).to.equal(1); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.change(input, { target: { value: 'MM//2022 – 06/15/2023' } }); + expect(onChangeV6.callCount).to.equal(1); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.change(input, { target: { value: 'MM/DD/ – 06/15/2023' } }); + expect(onChangeV6.callCount).to.equal(2); + expect(onChangeV6.lastCall.firstArg[0]).to.equal(null); + expect(onChangeV6.lastCall.firstArg[1]).toEqualDateTime( + adapter.addYears(adapter.date(), 1), + ); + + // End date + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.change(input, { target: { value: 'MM/DD/YYYY – /15/2023' } }); + expect(onChangeV6.callCount).to.equal(3); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.change(input, { target: { value: 'MM/DD/YYYY – MM//2023' } }); + expect(onChangeV6.callCount).to.equal(3); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + fireEvent.change(input, { target: { value: 'MM/DD/YYYY – MM/DD/' } }); + expect(onChangeV6.callCount).to.equal(4); + expect(onChangeV6.lastCall.firstArg[0]).to.equal(null); + expect(onChangeV6.lastCall.firstArg[1]).to.equal(null); }); it('should not call `onChange` if the section is already empty (Backspace)', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - testFieldChange({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], - onChange, - keyStrokes: [ - { value: ' 2022 – June 2023', expected: 'MMMM 2022 – June 2023' }, - { value: ' 2022 – June 2023', expected: 'MMMM 2022 – June 2023' }, - ], + onChange: onChangeV7, }); - expect(onChange.callCount).to.equal(1); + v7Response.selectSection('month'); + + v7Response.pressKey(0, ''); + expect(onChangeV7.callCount).to.equal(1); + + v7Response.pressKey(0, ''); + expect(onChangeV7.callCount).to.equal(1); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `${adapter.formats.month} ${adapter.formats.year}`, + defaultValue: [adapter.date(), adapter.addYears(adapter.date(), 1)], + onChange: onChangeV6, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + fireEvent.change(input, { target: { value: ' 2022 – June 2023' } }); + expect(onChangeV6.callCount).to.equal(1); + + fireEvent.change(input, { target: { value: ' 2022 – June 2023' } }); + expect(onChangeV6.callCount).to.equal(1); }); }, ); diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/selection.SingleInputDateRangeField.test.tsx b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/selection.SingleInputDateRangeField.test.tsx index ae5fcfe3f3446..4ab094a9aab08 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/selection.SingleInputDateRangeField.test.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/tests/selection.SingleInputDateRangeField.test.tsx @@ -1,14 +1,14 @@ -import * as React from 'react'; import { expect } from 'chai'; import { SingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; -import { act, userEvent } from '@mui-internal/test-utils'; +import { act, fireEvent } from '@mui-internal/test-utils'; import { adapterToUse, buildFieldInteractions, getCleanedSelectedContent, getTextbox, createPickerRenderer, - expectInputValue, + expectFieldValueV7, + expectFieldValueV6, } from 'test/utils/pickers'; describe(' - Selection', () => { @@ -20,16 +20,27 @@ describe(' - Selection', () => { }); describe('Focus', () => { - it('should select all on mount focus (`autoFocus = true`)', () => { - render(); - const input = getTextbox(); + it('should select 1st section (v7) / all sections (v6) on mount focus (`autoFocus = true`)', () => { + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + autoFocus: true, + }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY – MM/DD/YYYY'); + expect(getCleanedSelectedContent()).to.equal('MM'); - expectInputValue(input, 'MM/DD/YYYY – MM/DD/YYYY'); - expect(getCleanedSelectedContent(input)).to.equal('MM/DD/YYYY – MM/DD/YYYY'); + v7Response.unmount(); + + // Test with v6 input + renderWithProps({ autoFocus: true, enableAccessibleFieldDOMStructure: false }); + const input = getTextbox(); + expectFieldValueV6(input, 'MM/DD/YYYY – MM/DD/YYYY'); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY – MM/DD/YYYY'); }); - it('should select all on focus', () => { - render(); + it('should select all on focus (v6 only)', () => { + // Test with v6 input + renderWithProps({ enableAccessibleFieldDOMStructure: false }); const input = getTextbox(); // Simulate a focus interaction on desktop act(() => { @@ -38,115 +49,236 @@ describe(' - Selection', () => { clock.runToLast(); input.select(); - expectInputValue(input, 'MM/DD/YYYY – MM/DD/YYYY'); - expect(getCleanedSelectedContent(input)).to.equal('MM/DD/YYYY – MM/DD/YYYY'); + expectFieldValueV6(input, 'MM/DD/YYYY – MM/DD/YYYY'); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY – MM/DD/YYYY'); }); }); describe('Click', () => { it('should select the clicked selection when the input is already focused', () => { - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, value: [null, adapterToUse.date('2022-02-24')], }); // Start date - selectSection('day'); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + v7Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); - selectSection('month'); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + v7Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); // End date - selectSection('month', 'last'); - expect(getCleanedSelectedContent(input)).to.equal('02'); + v7Response.selectSection('month', 'last'); + expect(getCleanedSelectedContent()).to.equal('02'); - selectSection('day', 'last'); - expect(getCleanedSelectedContent(input)).to.equal('24'); + v7Response.selectSection('day', 'last'); + expect(getCleanedSelectedContent()).to.equal('24'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + value: [null, adapterToUse.date('2022-02-24')], + }); + + // Start date + v6Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + + v6Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); + + // End date + v6Response.selectSection('month', 'last'); + expect(getCleanedSelectedContent()).to.equal('02'); + + v6Response.selectSection('day', 'last'); + expect(getCleanedSelectedContent()).to.equal('24'); }); it('should not change the selection when clicking on the only already selected section', () => { - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: [null, adapterToUse.date('2022-02-24')], + }); + + // Start date + v7Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + + v7Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + + // End date + v7Response.selectSection('day', 'last'); + expect(getCleanedSelectedContent()).to.equal('24'); + + v7Response.selectSection('day', 'last'); + expect(getCleanedSelectedContent()).to.equal('24'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, value: [null, adapterToUse.date('2022-02-24')], }); // Start date - selectSection('day'); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + v6Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); - selectSection('day'); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + v6Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); // End date - selectSection('day', 'last'); - expect(getCleanedSelectedContent(input)).to.equal('24'); + v6Response.selectSection('day', 'last'); + expect(getCleanedSelectedContent()).to.equal('24'); - selectSection('day', 'last'); - expect(getCleanedSelectedContent(input)).to.equal('24'); + v6Response.selectSection('day', 'last'); + expect(getCleanedSelectedContent()).to.equal('24'); }); }); describe('key: ArrowRight', () => { - it('should allows to move from left to right with ArrowRight', () => { - const { input, selectSection } = renderWithProps({}); + it('should allow to move from left to right with ArrowRight', () => { + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + + v7Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); - selectSection('month'); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('DD'); - userEvent.keyPress(input, { key: 'ArrowRight' }); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); - userEvent.keyPress(input, { key: 'ArrowRight' }); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); + fireEvent.keyDown(v7Response.getActiveSection(2), { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('MM'); - userEvent.keyPress(input, { key: 'ArrowRight' }); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + fireEvent.keyDown(v7Response.getActiveSection(3), { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('DD'); - userEvent.keyPress(input, { key: 'ArrowRight' }); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + fireEvent.keyDown(v7Response.getActiveSection(4), { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); - userEvent.keyPress(input, { key: 'ArrowRight' }); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + + const input = getTextbox(); + v6Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); + + fireEvent.keyDown(input, { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('DD'); + + fireEvent.keyDown(input, { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + + fireEvent.keyDown(input, { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('MM'); + + fireEvent.keyDown(input, { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('DD'); + + fireEvent.keyDown(input, { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); }); it('should stay on the current section when the last section is selected', () => { - const { input, selectSection } = renderWithProps({}); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + + v7Response.selectSection('year', 'last'); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + fireEvent.keyDown(v7Response.getActiveSection(5), { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); - selectSection('year', 'last'); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); - userEvent.keyPress(input, { key: 'ArrowRight' }); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + + const input = getTextbox(); + v6Response.selectSection('year', 'last'); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); }); }); describe('key: ArrowLeft', () => { - it('should allows to move from right to left with ArrowLeft', () => { - const { input, selectSection } = renderWithProps({}); + it('should allow to move from right to left with ArrowLeft', () => { + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); - selectSection('year', 'last'); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); - userEvent.keyPress(input, { key: 'ArrowLeft' }); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + v7Response.selectSection('year', 'last'); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + fireEvent.keyDown(v7Response.getActiveSection(5), { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('DD'); - userEvent.keyPress(input, { key: 'ArrowLeft' }); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + fireEvent.keyDown(v7Response.getActiveSection(4), { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); - userEvent.keyPress(input, { key: 'ArrowLeft' }); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); + fireEvent.keyDown(v7Response.getActiveSection(3), { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); - userEvent.keyPress(input, { key: 'ArrowLeft' }); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + fireEvent.keyDown(v7Response.getActiveSection(2), { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('DD'); - userEvent.keyPress(input, { key: 'ArrowLeft' }); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + + const input = getTextbox(); + v6Response.selectSection('year', 'last'); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + fireEvent.keyDown(input, { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('DD'); + + fireEvent.keyDown(input, { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); + + fireEvent.keyDown(input, { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + + fireEvent.keyDown(input, { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('DD'); + + fireEvent.keyDown(input, { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); }); it('should stay on the current section when the first section is selected', () => { - const { input, selectSection } = renderWithProps({}); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + + v7Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); - selectSection('month'); - expect(getCleanedSelectedContent(input)).to.equal('MM'); - userEvent.keyPress(input, { key: 'ArrowLeft' }); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + + const input = getTextbox(); + v6Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); + fireEvent.keyDown(input, { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); }); }); }); diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts index 267533708dcb5..d328ad1d1d7f1 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts @@ -1,49 +1,40 @@ import { - useUtils, - useDefaultDates, - applyDefaultDate, useField, splitFieldInternalAndForwardedProps, + useDefaultizedDateField, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { - UseSingleInputDateRangeFieldComponentProps, - UseSingleInputDateRangeFieldDefaultizedProps, - UseSingleInputDateRangeFieldProps, -} from './SingleInputDateRangeField.types'; +import { UseSingleInputDateRangeFieldProps } from './SingleInputDateRangeField.types'; import { rangeValueManager, rangeFieldValueManager } from '../internals/utils/valueManagers'; import { validateDateRange } from '../internals/utils/validation/validateDateRange'; +import { RangeFieldSection, DateRange } from '../models'; -export const useDefaultizedDateRangeFieldProps = < +export const useSingleInputDateRangeField = < TDate extends PickerValidDate, - AdditionalProps extends {}, + TEnableAccessibleFieldDOMStructure extends boolean, + TAllProps extends UseSingleInputDateRangeFieldProps, >( - props: UseSingleInputDateRangeFieldProps, -): UseSingleInputDateRangeFieldDefaultizedProps => { - const utils = useUtils(); - const defaultDates = useDefaultDates(); - - return { - ...props, - disablePast: props.disablePast ?? false, - disableFuture: props.disableFuture ?? false, - format: props.format ?? utils.formats.keyboardDate, - minDate: applyDefaultDate(utils, props.minDate, defaultDates.minDate), - maxDate: applyDefaultDate(utils, props.maxDate, defaultDates.maxDate), - } as any; -}; - -export const useSingleInputDateRangeField = ( - inProps: UseSingleInputDateRangeFieldComponentProps, + inProps: TAllProps, ) => { - const props = useDefaultizedDateRangeFieldProps(inProps); + const props = useDefaultizedDateField< + TDate, + UseSingleInputDateRangeFieldProps, + TAllProps + >(inProps); const { forwardedProps, internalProps } = splitFieldInternalAndForwardedProps< typeof props, - keyof UseSingleInputDateRangeFieldProps + keyof UseSingleInputDateRangeFieldProps >(props, 'date'); - return useField({ + return useField< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + typeof forwardedProps, + typeof internalProps + >({ forwardedProps, internalProps, valueManager: rangeValueManager, diff --git a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx index f2635d79d128a..7b86fc27045bb 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx @@ -2,20 +2,22 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import MuiTextField from '@mui/material/TextField'; import { convertFieldResponseIntoMuiTextFieldProps } from '@mui/x-date-pickers/internals'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; import { useThemeProps } from '@mui/material/styles'; +import { refType } from '@mui/utils'; import { useSlotProps } from '@mui/base/utils'; import { useClearableField } from '@mui/x-date-pickers/hooks'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { refType } from '@mui/utils'; -import { - SingleInputDateTimeRangeFieldProps, - SingleInputDateTimeRangeFieldSlotProps, -} from './SingleInputDateTimeRangeField.types'; +import { SingleInputDateTimeRangeFieldProps } from './SingleInputDateTimeRangeField.types'; import { useSingleInputDateTimeRangeField } from './useSingleInputDateTimeRangeField'; -import { FieldType } from '../internals/models'; +import { FieldType } from '../models'; -type DateRangeFieldComponent = (( - props: SingleInputDateTimeRangeFieldProps & React.RefAttributes, +type DateRangeFieldComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: SingleInputDateTimeRangeFieldProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any; fieldType?: FieldType }; /** @@ -30,7 +32,11 @@ type DateRangeFieldComponent = (( */ const SingleInputDateTimeRangeField = React.forwardRef(function SingleInputDateTimeRangeField< TDate extends PickerValidDate, ->(inProps: SingleInputDateTimeRangeFieldProps, inRef: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: SingleInputDateTimeRangeFieldProps, + inRef: React.Ref, +) { const themeProps = useThemeProps({ props: inProps, name: 'MuiSingleInputDateTimeRangeField', @@ -40,13 +46,10 @@ const SingleInputDateTimeRangeField = React.forwardRef(function SingleInputDateT const ownerState = themeProps; - const TextField = slots?.textField ?? MuiTextField; - const textFieldProps: SingleInputDateTimeRangeFieldProps = useSlotProps< - typeof TextField, - SingleInputDateTimeRangeFieldSlotProps['textField'], - SingleInputDateTimeRangeFieldProps, - SingleInputDateTimeRangeFieldProps - >({ + const TextField = + slots?.textField ?? + (inProps.enableAccessibleFieldDOMStructure ? PickersTextField : MuiTextField); + const textFieldProps = useSlotProps({ elementType: TextField, externalSlotProps: slotProps?.textField, externalForwardedProps: other, @@ -54,15 +57,17 @@ const SingleInputDateTimeRangeField = React.forwardRef(function SingleInputDateT additionalProps: { ref: inRef, }, - }); + }) as SingleInputDateTimeRangeFieldProps; // TODO: Remove when mui/material-ui#35088 will be merged textFieldProps.inputProps = { ...inputProps, ...textFieldProps.inputProps }; textFieldProps.InputProps = { ...InputProps, ...textFieldProps.InputProps }; - const fieldResponse = useSingleInputDateTimeRangeField( - textFieldProps, - ); + const fieldResponse = useSingleInputDateTimeRangeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof textFieldProps + >(textFieldProps); const convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); const processedFieldProps = useClearableField({ @@ -129,6 +134,10 @@ SingleInputDateTimeRangeField.propTypes = { * @default false */ disablePast: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.bool, /** * If `true`, the component is displayed in focused state. */ @@ -278,11 +287,11 @@ SingleInputDateTimeRangeField.propTypes = { required: PropTypes.bool, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -299,10 +308,6 @@ SingleInputDateTimeRangeField.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.types.ts b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.types.ts index 9a6b6b88d7fc0..9e2e93d3323c6 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.types.ts @@ -1,59 +1,67 @@ import * as React from 'react'; import { SlotComponentProps } from '@mui/base/utils'; import TextField from '@mui/material/TextField'; -import { FieldsTextFieldProps } from '@mui/x-date-pickers/internals'; -import { UseClearableFieldSlots, UseClearableFieldSlotProps } from '@mui/x-date-pickers/hooks'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { UseFieldInternalProps } from '@mui/x-date-pickers/internals'; +import { BuiltInFieldTextFieldProps, PickerValidDate } from '@mui/x-date-pickers/models'; import { - UseDateTimeRangeFieldDefaultizedProps, - UseDateTimeRangeFieldProps, -} from '../internals/models'; + ExportedUseClearableFieldProps, + UseClearableFieldSlots, + UseClearableFieldSlotProps, +} from '@mui/x-date-pickers/hooks'; +import { UseDateTimeRangeFieldProps } from '../internals/models'; +import { DateRange, RangeFieldSection, DateTimeRangeValidationError } from '../models'; -export interface UseSingleInputDateTimeRangeFieldProps - extends UseDateTimeRangeFieldProps {} - -export type UseSingleInputDateTimeRangeFieldDefaultizedProps< +export interface UseSingleInputDateTimeRangeFieldProps< TDate extends PickerValidDate, - AdditionalProps extends {}, -> = UseDateTimeRangeFieldDefaultizedProps & AdditionalProps; + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseDateTimeRangeFieldProps, + ExportedUseClearableFieldProps, + Pick< + UseFieldInternalProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + DateTimeRangeValidationError + >, + 'unstableFieldRef' + > {} -export type UseSingleInputDateTimeRangeFieldComponentProps< +export type SingleInputDateTimeRangeFieldProps< TDate extends PickerValidDate, - TChildProps extends {}, -> = Omit> & - UseSingleInputDateTimeRangeFieldProps; - -export interface SingleInputDateTimeRangeFieldProps - extends UseSingleInputDateTimeRangeFieldComponentProps { - /** - * Overridable component slots. - * @default {} - */ - slots?: SingleInputDateTimeRangeFieldSlots; - /** - * The props used for each component slot. - * @default {} - */ - slotProps?: SingleInputDateTimeRangeFieldSlotProps; -} - -export type SingleInputDateTimeRangeFieldOwnerState = - SingleInputDateTimeRangeFieldProps; + TEnableAccessibleFieldDOMStructure extends boolean = false, +> = Omit< + BuiltInFieldTextFieldProps, + keyof UseSingleInputDateTimeRangeFieldProps +> & + UseSingleInputDateTimeRangeFieldProps & { + /** + * Overridable component slots. + * @default {} + */ + slots?: SingleInputDateTimeRangeFieldSlots; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: SingleInputDateTimeRangeFieldSlotProps; + }; export interface SingleInputDateTimeRangeFieldSlots extends UseClearableFieldSlots { /** * Form control with an input to render the value. - * Receives the same props as `@mui/material/TextField`. - * @default TextField from '@mui/material' + * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ textField?: React.ElementType; } -export interface SingleInputDateTimeRangeFieldSlotProps - extends UseClearableFieldSlotProps { +export interface SingleInputDateTimeRangeFieldSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseClearableFieldSlotProps { textField?: SlotComponentProps< typeof TextField, {}, - SingleInputDateTimeRangeFieldOwnerState + SingleInputDateTimeRangeFieldProps >; } diff --git a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/index.ts b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/index.ts index 32b1a90ee8fb3..bfcf5404fa7c3 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/index.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/index.ts @@ -2,6 +2,5 @@ export { SingleInputDateTimeRangeField } from './SingleInputDateTimeRangeField'; export { useSingleInputDateTimeRangeField as unstable_useSingleInputDateTimeRangeField } from './useSingleInputDateTimeRangeField'; export type { UseSingleInputDateTimeRangeFieldProps, - UseSingleInputDateTimeRangeFieldComponentProps, SingleInputDateTimeRangeFieldProps, } from './SingleInputDateTimeRangeField.types'; diff --git a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/tests/describes.SingleInputDateTimeRangeField.test.tsx b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/tests/describes.SingleInputDateTimeRangeField.test.tsx index c960b9a903894..219e51370d938 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/tests/describes.SingleInputDateTimeRangeField.test.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/tests/describes.SingleInputDateTimeRangeField.test.tsx @@ -6,7 +6,7 @@ import { describeConformance } from 'test/utils/describeConformance'; describe(' - Describes', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, inheritComponent: 'div', render, diff --git a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts index d6afd05119409..b7c796ba8abad 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts @@ -1,60 +1,43 @@ import { - useUtils, useField, - applyDefaultDate, - useDefaultDates, splitFieldInternalAndForwardedProps, + useDefaultizedDateTimeField, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { - UseSingleInputDateTimeRangeFieldComponentProps, - UseSingleInputDateTimeRangeFieldDefaultizedProps, - UseSingleInputDateTimeRangeFieldProps, -} from './SingleInputDateTimeRangeField.types'; +import { UseSingleInputDateTimeRangeFieldProps } from './SingleInputDateTimeRangeField.types'; import { rangeValueManager, rangeFieldValueManager } from '../internals/utils/valueManagers'; import { validateDateTimeRange } from '../internals/utils/validation/validateDateTimeRange'; - -export const useDefaultizedTimeRangeFieldProps = < - TDate extends PickerValidDate, - AdditionalProps extends {}, ->( - props: UseSingleInputDateTimeRangeFieldProps, -): UseSingleInputDateTimeRangeFieldDefaultizedProps => { - const utils = useUtils(); - const defaultDates = useDefaultDates(); - - const ampm = props.ampm ?? utils.is12HourCycleInCurrentLocale(); - const defaultFormat = ampm - ? utils.formats.keyboardDateTime12h - : utils.formats.keyboardDateTime24h; - - return { - ...props, - disablePast: props.disablePast ?? false, - disableFuture: props.disableFuture ?? false, - format: props.format ?? defaultFormat, - minDate: applyDefaultDate(utils, props.minDateTime ?? props.minDate, defaultDates.minDate), - maxDate: applyDefaultDate(utils, props.maxDateTime ?? props.maxDate, defaultDates.maxDate), - minTime: props.minDateTime ?? props.minTime, - maxTime: props.maxDateTime ?? props.maxTime, - disableIgnoringDatePartForTimeValidation: Boolean(props.minDateTime || props.maxDateTime), - } as any; -}; +import { RangeFieldSection, DateRange } from '../models'; export const useSingleInputDateTimeRangeField = < TDate extends PickerValidDate, - TChildProps extends {}, + TEnableAccessibleFieldDOMStructure extends boolean, + TAllProps extends UseSingleInputDateTimeRangeFieldProps< + TDate, + TEnableAccessibleFieldDOMStructure + >, >( - inProps: UseSingleInputDateTimeRangeFieldComponentProps, + inProps: TAllProps, ) => { - const props = useDefaultizedTimeRangeFieldProps(inProps); + const props = useDefaultizedDateTimeField< + TDate, + UseSingleInputDateTimeRangeFieldProps, + TAllProps + >(inProps); const { forwardedProps, internalProps } = splitFieldInternalAndForwardedProps< typeof props, - keyof UseSingleInputDateTimeRangeFieldProps + keyof UseSingleInputDateTimeRangeFieldProps >(props, 'date-time'); - return useField({ + return useField< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + typeof forwardedProps, + typeof internalProps + >({ forwardedProps, internalProps, valueManager: rangeValueManager, diff --git a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx index c719820f233f7..a9789bd0e0473 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx @@ -4,18 +4,20 @@ import MuiTextField from '@mui/material/TextField'; import { useClearableField } from '@mui/x-date-pickers/hooks'; import { convertFieldResponseIntoMuiTextFieldProps } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; import { useThemeProps } from '@mui/material/styles'; import { useSlotProps } from '@mui/base/utils'; import { refType } from '@mui/utils'; -import { - SingleInputTimeRangeFieldProps, - SingleInputTimeRangeFieldSlotProps, -} from './SingleInputTimeRangeField.types'; +import { SingleInputTimeRangeFieldProps } from './SingleInputTimeRangeField.types'; import { useSingleInputTimeRangeField } from './useSingleInputTimeRangeField'; -import { FieldType } from '../internals/models'; +import { FieldType } from '../models'; -type DateRangeFieldComponent = (( - props: SingleInputTimeRangeFieldProps & React.RefAttributes, +type DateRangeFieldComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: SingleInputTimeRangeFieldProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any; fieldType?: FieldType }; /** @@ -30,7 +32,11 @@ type DateRangeFieldComponent = (( */ const SingleInputTimeRangeField = React.forwardRef(function SingleInputTimeRangeField< TDate extends PickerValidDate, ->(inProps: SingleInputTimeRangeFieldProps, inRef: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: SingleInputTimeRangeFieldProps, + inRef: React.Ref, +) { const themeProps = useThemeProps({ props: inProps, name: 'MuiSingleInputTimeRangeField', @@ -40,13 +46,10 @@ const SingleInputTimeRangeField = React.forwardRef(function SingleInputTimeRange const ownerState = themeProps; - const TextField = slots?.textField ?? MuiTextField; - const textFieldProps: SingleInputTimeRangeFieldProps = useSlotProps< - typeof TextField, - SingleInputTimeRangeFieldSlotProps['textField'], - SingleInputTimeRangeFieldProps, - SingleInputTimeRangeFieldProps - >({ + const TextField = + slots?.textField ?? + (inProps.enableAccessibleFieldDOMStructure ? PickersTextField : MuiTextField); + const textFieldProps = useSlotProps({ elementType: TextField, externalSlotProps: slotProps?.textField, externalForwardedProps: other, @@ -54,13 +57,17 @@ const SingleInputTimeRangeField = React.forwardRef(function SingleInputTimeRange additionalProps: { ref: inRef, }, - }); + }) as SingleInputTimeRangeFieldProps; // TODO: Remove when mui/material-ui#35088 will be merged textFieldProps.inputProps = { ...inputProps, ...textFieldProps.inputProps }; textFieldProps.InputProps = { ...InputProps, ...textFieldProps.InputProps }; - const fieldResponse = useSingleInputTimeRangeField(textFieldProps); + const fieldResponse = useSingleInputTimeRangeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof textFieldProps + >(textFieldProps); const convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); const processedFieldProps = useClearableField({ @@ -127,6 +134,10 @@ SingleInputTimeRangeField.propTypes = { * @default false */ disablePast: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.bool, /** * If `true`, the component is displayed in focused state. */ @@ -260,11 +271,11 @@ SingleInputTimeRangeField.propTypes = { required: PropTypes.bool, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -281,10 +292,6 @@ SingleInputTimeRangeField.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific time. diff --git a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.types.ts b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.types.ts index 2145337104a91..e6623980cc2af 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.types.ts @@ -1,52 +1,67 @@ import * as React from 'react'; import { SlotComponentProps } from '@mui/base/utils'; import TextField from '@mui/material/TextField'; -import { FieldsTextFieldProps } from '@mui/x-date-pickers/internals'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { UseClearableFieldSlots, UseClearableFieldSlotProps } from '@mui/x-date-pickers/hooks'; -import { UseTimeRangeFieldDefaultizedProps, UseTimeRangeFieldProps } from '../internals/models'; +import { UseFieldInternalProps } from '@mui/x-date-pickers/internals'; +import { PickerValidDate, BuiltInFieldTextFieldProps } from '@mui/x-date-pickers/models'; +import { + ExportedUseClearableFieldProps, + UseClearableFieldSlots, + UseClearableFieldSlotProps, +} from '@mui/x-date-pickers/hooks'; +import { UseTimeRangeFieldProps } from '../internals/models'; +import { DateRange, RangeFieldSection, TimeRangeValidationError } from '../models'; -export interface UseSingleInputTimeRangeFieldProps - extends UseTimeRangeFieldProps {} - -export type UseSingleInputTimeRangeFieldDefaultizedProps< +export interface UseSingleInputTimeRangeFieldProps< TDate extends PickerValidDate, - AdditionalProps extends {}, -> = UseTimeRangeFieldDefaultizedProps & AdditionalProps; + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseTimeRangeFieldProps, + ExportedUseClearableFieldProps, + Pick< + UseFieldInternalProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + TimeRangeValidationError + >, + 'unstableFieldRef' + > {} -export type UseSingleInputTimeRangeFieldComponentProps< +export type SingleInputTimeRangeFieldProps< TDate extends PickerValidDate, - TChildProps extends {}, -> = Omit> & - UseSingleInputTimeRangeFieldProps; - -export interface SingleInputTimeRangeFieldProps - extends UseSingleInputTimeRangeFieldComponentProps { - /** - * Overridable component slots. - * @default {} - */ - slots?: SingleInputTimeRangeFieldSlots; - /** - * The props used for each component slot. - * @default {} - */ - slotProps?: SingleInputTimeRangeFieldSlotProps; -} - -export type SingleInputTimeRangeFieldOwnerState = - SingleInputTimeRangeFieldProps; + TEnableAccessibleFieldDOMStructure extends boolean = false, +> = Omit< + BuiltInFieldTextFieldProps, + keyof UseSingleInputTimeRangeFieldProps +> & + UseSingleInputTimeRangeFieldProps & { + /** + * Overridable component slots. + * @default {} + */ + slots?: SingleInputTimeRangeFieldSlots; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: SingleInputTimeRangeFieldSlotProps; + }; export interface SingleInputTimeRangeFieldSlots extends UseClearableFieldSlots { /** * Form control with an input to render the value. - * Receives the same props as `@mui/material/TextField`. - * @default TextField from '@mui/material' + * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ textField?: React.ElementType; } -export interface SingleInputTimeRangeFieldSlotProps - extends UseClearableFieldSlotProps { - textField?: SlotComponentProps>; +export interface SingleInputTimeRangeFieldSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseClearableFieldSlotProps { + textField?: SlotComponentProps< + typeof TextField, + {}, + SingleInputTimeRangeFieldProps + >; } diff --git a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/index.ts b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/index.ts index a1e05b8e5531b..630409df6eda9 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/index.ts +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/index.ts @@ -2,6 +2,5 @@ export { SingleInputTimeRangeField } from './SingleInputTimeRangeField'; export { useSingleInputTimeRangeField as unstable_useSingleInputTimeRangeField } from './useSingleInputTimeRangeField'; export type { UseSingleInputTimeRangeFieldProps, - UseSingleInputTimeRangeFieldComponentProps, SingleInputTimeRangeFieldProps, } from './SingleInputTimeRangeField.types'; diff --git a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/tests/describes.SingleInputTimeRangeField.test.tsx b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/tests/describes.SingleInputTimeRangeField.test.tsx index ab43a0cede0e4..684786c4d54d2 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/tests/describes.SingleInputTimeRangeField.test.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/tests/describes.SingleInputTimeRangeField.test.tsx @@ -6,7 +6,7 @@ import { describeConformance } from 'test/utils/describeConformance'; describe(' - Describes', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, inheritComponent: 'div', render, diff --git a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts index 92de900e89e66..0b8da45b33146 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts @@ -1,47 +1,40 @@ import { - useUtils, useField, splitFieldInternalAndForwardedProps, + useDefaultizedTimeField, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { - UseSingleInputTimeRangeFieldComponentProps, - UseSingleInputTimeRangeFieldDefaultizedProps, - UseSingleInputTimeRangeFieldProps, -} from './SingleInputTimeRangeField.types'; +import { UseSingleInputTimeRangeFieldProps } from './SingleInputTimeRangeField.types'; import { rangeValueManager, rangeFieldValueManager } from '../internals/utils/valueManagers'; import { validateTimeRange } from '../internals/utils/validation/validateTimeRange'; +import { RangeFieldSection, DateRange } from '../models'; -export const useDefaultizedTimeRangeFieldProps = < +export const useSingleInputTimeRangeField = < TDate extends PickerValidDate, - AdditionalProps extends {}, + TEnableAccessibleFieldDOMStructure extends boolean, + TAllProps extends UseSingleInputTimeRangeFieldProps, >( - props: UseSingleInputTimeRangeFieldProps, -): UseSingleInputTimeRangeFieldDefaultizedProps => { - const utils = useUtils(); - - const ampm = props.ampm ?? utils.is12HourCycleInCurrentLocale(); - const defaultFormat = ampm ? utils.formats.fullTime12h : utils.formats.fullTime24h; - - return { - ...props, - disablePast: props.disablePast ?? false, - disableFuture: props.disableFuture ?? false, - format: props.format ?? defaultFormat, - } as any; -}; - -export const useSingleInputTimeRangeField = ( - inProps: UseSingleInputTimeRangeFieldComponentProps, + inProps: TAllProps, ) => { - const props = useDefaultizedTimeRangeFieldProps(inProps); + const props = useDefaultizedTimeField< + TDate, + UseSingleInputTimeRangeFieldProps, + TAllProps + >(inProps); const { forwardedProps, internalProps } = splitFieldInternalAndForwardedProps< typeof props, - keyof UseSingleInputTimeRangeFieldProps + keyof UseSingleInputTimeRangeFieldProps >(props, 'time'); - return useField({ + return useField< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + typeof forwardedProps, + typeof internalProps + >({ forwardedProps, internalProps, valueManager: rangeValueManager, diff --git a/packages/x-date-pickers-pro/src/index.ts b/packages/x-date-pickers-pro/src/index.ts index b38a8da338127..c87ecd38be82a 100644 --- a/packages/x-date-pickers-pro/src/index.ts +++ b/packages/x-date-pickers-pro/src/index.ts @@ -16,11 +16,6 @@ export * from './MultiInputDateTimeRangeField'; export * from './SingleInputDateRangeField'; export * from './SingleInputTimeRangeField'; export * from './SingleInputDateTimeRangeField'; -export type { - RangeFieldSection, - BaseMultiInputFieldProps, - MultiInputFieldSlotTextFieldProps, -} from './internals/models/fields'; // Calendars export * from './DateRangeCalendar'; diff --git a/packages/x-date-pickers-pro/src/internals/hooks/models/useRangePicker.ts b/packages/x-date-pickers-pro/src/internals/hooks/models/useRangePicker.ts index 9c78331a02851..f78eda4c15195 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/models/useRangePicker.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/models/useRangePicker.ts @@ -14,13 +14,13 @@ import { ExportedPickersLayoutSlots, ExportedPickersLayoutSlotProps, } from '@mui/x-date-pickers/PickersLayout'; -import { RangeFieldSection, BaseRangeNonStaticPickerProps } from '../../models'; +import { BaseRangeNonStaticPickerProps } from '../../models'; import { UseRangePositionProps, UseRangePositionResponse } from '../useRangePosition'; import { RangePickerFieldSlots, RangePickerFieldSlotProps, } from '../useEnrichedRangePickerFieldProps'; -import { DateRange } from '../../../models'; +import { DateRange, RangeFieldSection } from '../../../models'; export interface UseRangePickerSlots< TDate extends PickerValidDate, @@ -31,15 +31,16 @@ export interface UseRangePickerSlots< export interface UseRangePickerSlotProps< TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure extends boolean, > extends ExportedPickersLayoutSlotProps, TDate, TView>, - RangePickerFieldSlotProps { + RangePickerFieldSlotProps { tabs?: ExportedBaseTabsProps; toolbar?: ExportedBaseToolbarProps; } -export interface RangeOnlyPickerProps +export interface RangeOnlyPickerProps extends BaseNonStaticPickerProps, - UsePickerValueNonStaticProps, RangeFieldSection>, + UsePickerValueNonStaticProps, UsePickerViewsNonStaticProps, BaseRangeNonStaticPickerProps, UseRangePositionProps {} @@ -50,7 +51,7 @@ export interface UseRangePickerProps< TError, TExternalProps extends UsePickerViewsProps, TAdditionalViewProps extends {}, -> extends RangeOnlyPickerProps, +> extends RangeOnlyPickerProps, BasePickerProps, TDate, TView, TError, TExternalProps, TAdditionalViewProps> {} export interface RangePickerAdditionalViewProps diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx index 52bf5c8e3e0d5..fb40acd431f98 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx @@ -10,12 +10,10 @@ import { PickersPopper, InferError, ExportedBaseToolbarProps, - BaseFieldProps, DateOrTimeViewWithMeridiem, - UsePickerValueFieldResponse, ExportedBaseTabsProps, } from '@mui/x-date-pickers/internals'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickerValidDate, FieldRef, BaseSingleInputFieldProps } from '@mui/x-date-pickers/models'; import { DesktopRangePickerAdditionalViewProps, UseDesktopRangePickerParams, @@ -24,8 +22,7 @@ import { } from './useDesktopRangePicker.types'; import { useEnrichedRangePickerFieldProps } from '../useEnrichedRangePickerFieldProps'; import { getReleaseInfo } from '../../utils/releaseInfo'; -import { DateRange } from '../../../models'; -import { RangeFieldSection } from '../../models/fields'; +import { DateRange, BaseMultiInputFieldProps, RangeFieldSection } from '../../../models'; import { useRangePosition } from '../useRangePosition'; const releaseInfo = getReleaseInfo(); @@ -33,11 +30,23 @@ const releaseInfo = getReleaseInfo(); export const useDesktopRangePicker = < TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, - TExternalProps extends UseDesktopRangePickerProps, + TEnableAccessibleFieldDOMStructure extends boolean, + TExternalProps extends UseDesktopRangePickerProps< + TDate, + TView, + TEnableAccessibleFieldDOMStructure, + any, + TExternalProps + >, >({ props, ...pickerParams -}: UseDesktopRangePickerParams) => { +}: UseDesktopRangePickerParams< + TDate, + TView, + TEnableAccessibleFieldDOMStructure, + TExternalProps +>) => { useLicenseVerifier('x-date-pickers-pro', releaseInfo); const { @@ -47,6 +56,9 @@ export const useDesktopRangePicker = < sx, format, formatDensity, + enableAccessibleFieldDOMStructure, + selectedSections, + onSelectedSectionsChange, timezone, label, inputRef, @@ -62,9 +74,15 @@ export const useDesktopRangePicker = < const fieldContainerRef = React.useRef(null); const anchorRef = React.useRef(null); const popperRef = React.useRef(null); + const startFieldRef = React.useRef>(null); + const endFieldRef = React.useRef>(null); const initialView = React.useRef(props.openTo ?? null); - const { rangePosition, onRangePositionChange, singleInputFieldRef } = useRangePosition(props); + const fieldType = (slots.field as any).fieldType ?? 'multi-input'; + const { rangePosition, onRangePositionChange } = useRangePosition( + props, + fieldType === 'single-input' ? startFieldRef : undefined, + ); const { open, @@ -85,6 +103,7 @@ export const useDesktopRangePicker = < props, wrapperVariant: 'desktop', autoFocusView: false, + fieldRef: rangePosition === 'start' ? startFieldRef : endFieldRef, additionalViewProps: { rangePosition, onRangePositionChange, @@ -112,30 +131,25 @@ export const useDesktopRangePicker = < }; const Field = slots.field; - const fieldType = (Field as any).fieldType ?? 'multi-input'; - - const fieldProps: BaseFieldProps< - DateRange, - TDate, - RangeFieldSection, - InferError - > = useSlotProps< + const fieldProps = useSlotProps< typeof Field, - UseDesktopRangePickerSlotProps['field'], - UsePickerValueFieldResponse, RangeFieldSection, InferError> & - Partial< - Pick< - UseDesktopRangePickerProps, - | 'readOnly' - | 'disabled' - | 'className' - | 'sx' - | 'format' - | 'formatDensity' - | 'timezone' - | 'label' - | 'name' - | 'autoFocus' + UseDesktopRangePickerSlotProps['field'], + | Partial< + BaseSingleInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + InferError + > + > + | Partial< + BaseMultiInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + InferError > >, TExternalProps @@ -150,10 +164,13 @@ export const useDesktopRangePicker = < sx, format, formatDensity, + enableAccessibleFieldDOMStructure, + selectedSections, + onSelectedSectionsChange, timezone, autoFocus: autoFocus && !props.open, ref: fieldContainerRef, - ...(fieldType === 'single-input' && { inputRef, name }), + ...(inputRef ? { inputRef, name } : {}), }, ownerState: props, }); @@ -161,6 +178,7 @@ export const useDesktopRangePicker = < const enrichedFieldProps = useEnrichedRangePickerFieldProps< TDate, TView, + TEnableAccessibleFieldDOMStructure, InferError >({ wrapperVariant: 'desktop', @@ -174,11 +192,12 @@ export const useDesktopRangePicker = < onBlur: handleBlur, rangePosition, onRangePositionChange, - singleInputFieldRef, pickerSlotProps: slotProps, pickerSlots: slots, fieldProps, anchorRef, + startFieldRef, + endFieldRef, currentView: layoutProps.view !== props.openTo ? layoutProps.view : undefined, initialView: initialView.current ?? undefined, onViewChange: layoutProps.onViewChange, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.types.ts b/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.types.ts index a98872b25d9eb..47649e3d352bd 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.types.ts @@ -23,11 +23,11 @@ export interface UseDesktopRangePickerSlots< export interface UseDesktopRangePickerSlotProps< TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, -> extends UseRangePickerSlotProps, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseRangePickerSlotProps, PickersPopperSlotProps {} -export interface DesktopRangeOnlyPickerProps - extends RangeOnlyPickerProps { +export interface DesktopRangeOnlyPickerProps extends RangeOnlyPickerProps { /** * If `true`, the start `input` element is focused during the first mount. */ @@ -37,6 +37,7 @@ export interface DesktopRangeOnlyPickerProps export interface UseDesktopRangePickerProps< TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure extends boolean, TError, TExternalProps extends UsePickerViewsProps, > extends UseRangePickerProps< @@ -55,7 +56,7 @@ export interface UseDesktopRangePickerProps< * The props used for each component slot. * @default {} */ - slotProps?: UseDesktopRangePickerSlotProps; + slotProps?: UseDesktopRangePickerSlotProps; } export interface DesktopRangePickerAdditionalViewProps extends RangePickerAdditionalViewProps {} @@ -63,7 +64,14 @@ export interface DesktopRangePickerAdditionalViewProps extends RangePickerAdditi export interface UseDesktopRangePickerParams< TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, - TExternalProps extends UseDesktopRangePickerProps, + TEnableAccessibleFieldDOMStructure extends boolean, + TExternalProps extends UseDesktopRangePickerProps< + TDate, + TView, + TEnableAccessibleFieldDOMStructure, + any, + TExternalProps + >, > extends UseRangePickerParams< TDate, TView, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerFieldProps.ts b/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerFieldProps.ts index 9fb579a2e3af9..cba1ca9319b0c 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerFieldProps.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerFieldProps.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import Stack, { StackProps } from '@mui/material/Stack'; import Typography, { TypographyProps } from '@mui/material/Typography'; -import TextField, { TextFieldProps } from '@mui/material/TextField'; +import TextField from '@mui/material/TextField'; import { resolveComponentProps, SlotComponentProps } from '@mui/base/utils'; import useEventCallback from '@mui/utils/useEventCallback'; import useForkRef from '@mui/utils/useForkRef'; @@ -20,18 +20,19 @@ import { UsePickerResponse, WrapperVariant, UsePickerProps, - getActiveElement, + SlotComponentPropsFromProps, DateOrTimeViewWithMeridiem, } from '@mui/x-date-pickers/internals'; +import { UseDateRangeFieldProps } from '../models'; import { BaseMultiInputFieldProps, MultiInputFieldSlotRootProps, MultiInputFieldSlotTextFieldProps, RangeFieldSection, - UseDateRangeFieldProps, + DateRange, + RangePosition, FieldType, -} from '../models'; -import { DateRange, RangePosition } from '../../models'; +} from '../../models'; import { UseRangePositionResponse } from './useRangePosition'; export interface RangePickerFieldSlots extends UseClearableFieldSlots { @@ -49,41 +50,53 @@ export interface RangePickerFieldSlots extends UseClearableFieldSlots { /** * Form control with an input to render a date or time inside the default field. * It is rendered twice: once for the start element and once for the end element. - * Receives the same props as `@mui/material/TextField`. - * @default TextField from '@mui/material' + * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ - textField?: React.ElementType; + textField?: React.ElementType; } -export interface RangePickerFieldSlotProps - extends UseClearableFieldSlotProps { - field?: SlotComponentProps< - React.ElementType< - BaseMultiInputFieldProps, TDate, RangeFieldSection, unknown> +export interface RangePickerFieldSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseClearableFieldSlotProps { + field?: SlotComponentPropsFromProps< + BaseMultiInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + unknown >, {}, - UsePickerProps, TDate, any, RangeFieldSection, any, any, any> + UsePickerProps, TDate, any, any, any, any> >; fieldRoot?: SlotComponentProps>; fieldSeparator?: SlotComponentProps>; - textField?: SlotComponentProps< typeof TextField, {}, - UseDateRangeFieldProps & { position?: RangePosition } + UseDateRangeFieldProps & { position?: RangePosition } >; } export interface UseEnrichedRangePickerFieldPropsParams< TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure extends boolean, TError, FieldProps extends BaseFieldProps< DateRange, TDate, RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + TError + > = BaseFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, TError - > = BaseFieldProps, TDate, RangeFieldSection, TError>, + >, > extends Pick< UsePickerResponse, TView, RangeFieldSection, any>, 'open' | 'actions' @@ -95,21 +108,23 @@ export interface UseEnrichedRangePickerFieldPropsParams< labelId?: string; disableOpenPicker?: boolean; onBlur?: () => void; - inputRef?: React.Ref; label?: React.ReactNode; localeText: PickersInputLocaleText | undefined; - pickerSlotProps: RangePickerFieldSlotProps | undefined; + pickerSlotProps: RangePickerFieldSlotProps | undefined; pickerSlots: RangePickerFieldSlots | undefined; fieldProps: FieldProps; anchorRef?: React.Ref; currentView?: TView | null; initialView?: TView; onViewChange?: (view: TView) => void; + startFieldRef: React.RefObject>; + endFieldRef: React.RefObject>; } const useMultiInputFieldSlotProps = < TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure extends boolean, TError, >({ wrapperVariant, @@ -129,21 +144,33 @@ const useMultiInputFieldSlotProps = < currentView, initialView, onViewChange, + startFieldRef, + endFieldRef, }: UseEnrichedRangePickerFieldPropsParams< TDate, TView, + TEnableAccessibleFieldDOMStructure, TError, - BaseMultiInputFieldProps, TDate, RangeFieldSection, TError> + BaseMultiInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + TError + > >) => { - type ReturnType = BaseMultiInputFieldProps, TDate, RangeFieldSection, TError>; + type ReturnType = BaseMultiInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + TError + >; const localeText = useLocaleText(); - const startRef = React.useRef(null); - const endRef = React.useRef(null); - const startFieldRef = React.useRef>(null); - const endFieldRef = React.useRef>(null); const handleStartFieldRef = useForkRef(fieldProps.unstableStartFieldRef, startFieldRef); - const handleEndFieldRef = useForkRef(fieldProps.unstableFieldRef, endFieldRef); + const handleEndFieldRef = useForkRef(fieldProps.unstableEndFieldRef, endFieldRef); + const previousRangePosition = React.useRef(rangePosition); React.useEffect(() => { @@ -151,25 +178,23 @@ const useMultiInputFieldSlotProps = < return; } - const currentRef = rangePosition === 'start' ? startRef : endRef; - currentRef.current?.focus(); const currentFieldRef = rangePosition === 'start' ? startFieldRef : endFieldRef; + currentFieldRef.current?.focusField(); if (!currentFieldRef.current || !currentView) { // could happen when the user is switching between the inputs previousRangePosition.current = rangePosition; return; } + // bring back focus to the field currentFieldRef.current.setSelectedSections( // use the current view or `0` when the range position has just been swapped previousRangePosition.current === rangePosition ? currentView : 0, ); previousRangePosition.current = rangePosition; - }, [rangePosition, open, currentView]); + }, [rangePosition, open, currentView, startFieldRef, endFieldRef]); - const openRangeStartSelection = ( - event: React.MouseEvent | React.KeyboardEvent, - ) => { + const openRangeStartSelection: React.UIEventHandler = (event) => { event.stopPropagation(); onRangePositionChange('start'); if (!readOnly && !disableOpenPicker) { @@ -177,9 +202,7 @@ const useMultiInputFieldSlotProps = < } }; - const openRangeEndSelection = ( - event: React.MouseEvent | React.KeyboardEvent, - ) => { + const openRangeEndSelection: React.UIEventHandler = (event) => { event.stopPropagation(); onRangePositionChange('end'); if (!readOnly && !disableOpenPicker) { @@ -218,11 +241,10 @@ const useMultiInputFieldSlotProps = < ...fieldProps.slotProps, textField: (ownerState) => { const resolvedComponentProps = resolveComponentProps(pickerSlotProps?.textField, ownerState); - let inputProps: MultiInputFieldSlotTextFieldProps; + let textFieldProps: MultiInputFieldSlotTextFieldProps; let InputProps: MultiInputFieldSlotTextFieldProps['InputProps']; if (ownerState.position === 'start') { - inputProps = { - inputRef: startRef, + textFieldProps = { label: inLocaleText?.start ?? localeText.start, onKeyDown: onSpaceOrEnter(openRangeStartSelection), onFocus: handleFocusStart, @@ -239,8 +261,7 @@ const useMultiInputFieldSlotProps = < }; } } else { - inputProps = { - inputRef: endRef, + textFieldProps = { label: inLocaleText?.end ?? localeText.end, onKeyDown: onSpaceOrEnter(openRangeEndSelection), onFocus: handleFocusEnd, @@ -255,7 +276,7 @@ const useMultiInputFieldSlotProps = < return { ...(labelId != null && { id: `${labelId}-${ownerState.position!}` }), - ...inputProps, + ...textFieldProps, ...resolveComponentProps(pickerSlotProps?.textField, ownerState), InputProps, }; @@ -274,14 +295,14 @@ const useMultiInputFieldSlotProps = < }; /* TODO: remove this when a clearable behavior for multiple input range fields is implemented */ - const { clearable, onClear, ...restFieldProps } = fieldProps; + const { clearable, onClear, ...restFieldProps } = fieldProps as any; const enrichedFieldProps: ReturnType = { ...restFieldProps, - slots, - slotProps, unstableStartFieldRef: handleStartFieldRef, unstableEndFieldRef: handleEndFieldRef, + slots, + slotProps, }; return enrichedFieldProps; @@ -290,20 +311,21 @@ const useMultiInputFieldSlotProps = < const useSingleInputFieldSlotProps = < TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure extends boolean, TError, >({ wrapperVariant, open, actions, readOnly, - inputRef: inInputRef, labelId, disableOpenPicker, label, onBlur, rangePosition, onRangePositionChange, - singleInputFieldRef, + startFieldRef, + endFieldRef, pickerSlots, pickerSlotProps, fieldProps, @@ -312,39 +334,53 @@ const useSingleInputFieldSlotProps = < }: UseEnrichedRangePickerFieldPropsParams< TDate, TView, + TEnableAccessibleFieldDOMStructure, TError, - BaseSingleInputFieldProps, TDate, RangeFieldSection, TError> + BaseSingleInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + TError + > >) => { - type ReturnType = BaseSingleInputFieldProps, TDate, RangeFieldSection, TError>; - - const inputRef = React.useRef(null); - const handleInputRef = useForkRef(inInputRef, inputRef); + type ReturnType = BaseSingleInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + TError + >; - const handleFieldRef = useForkRef(fieldProps.unstableFieldRef, singleInputFieldRef); + const handleFieldRef = useForkRef(fieldProps.unstableFieldRef, startFieldRef, endFieldRef); React.useEffect(() => { - if (!open) { + if (!open || !startFieldRef.current) { return; } - inputRef.current?.focus(); - if (!singleInputFieldRef.current || !currentView) { + if (startFieldRef.current.isFieldFocused()) { return; } - const sections = singleInputFieldRef.current.getSections().map((section) => section.type); + // bring back focus to the field with the current view section selected - singleInputFieldRef.current.setSelectedSections( - rangePosition === 'start' ? sections.indexOf(currentView) : sections.lastIndexOf(currentView), - ); - }, [rangePosition, open, currentView, singleInputFieldRef]); + if (currentView) { + const sections = startFieldRef.current.getSections().map((section) => section.type); + const newSelectedSection = + rangePosition === 'start' + ? sections.indexOf(currentView) + : sections.lastIndexOf(currentView); + startFieldRef.current?.focusField(newSelectedSection); + } + }, [rangePosition, open, currentView, startFieldRef]); const updateRangePosition = () => { - if (!singleInputFieldRef.current || inputRef.current !== getActiveElement(document)) { + if (!startFieldRef.current?.isFieldFocused()) { return; } - const sections = singleInputFieldRef.current.getSections(); - const activeSectionIndex = singleInputFieldRef.current?.getActiveSectionIndex(); + const sections = startFieldRef.current.getSections(); + const activeSectionIndex = startFieldRef.current?.getActiveSectionIndex(); const domRangePosition = activeSectionIndex == null || activeSectionIndex < sections.length / 2 ? 'start' : 'end'; @@ -354,9 +390,9 @@ const useSingleInputFieldSlotProps = < }; const handleSelectedSectionsChange = useEventCallback( - (selectedSections: FieldSelectedSections) => { + (selectedSection: FieldSelectedSections) => { setTimeout(updateRangePosition); - fieldProps.onSelectedSectionsChange?.(selectedSections); + fieldProps.onSelectedSectionsChange?.(selectedSection); }, ); @@ -368,14 +404,14 @@ const useSingleInputFieldSlotProps = < } }; - const slots: ReturnType['slots'] = { + const slots = { ...fieldProps.slots, textField: pickerSlots?.textField, clearButton: pickerSlots?.clearButton, clearIcon: pickerSlots?.clearIcon, }; - const slotProps: ReturnType['slotProps'] = { + const slotProps = { ...fieldProps.slotProps, textField: pickerSlotProps?.textField, clearButton: pickerSlots?.clearButton, @@ -388,7 +424,6 @@ const useSingleInputFieldSlotProps = < slotProps, label, unstableFieldRef: handleFieldRef, - inputRef: handleInputRef, onKeyDown: onSpaceOrEnter(openPicker, fieldProps.onKeyDown), onSelectedSectionsChange: handleSelectedSectionsChange, onBlur, @@ -410,9 +445,15 @@ const useSingleInputFieldSlotProps = < export const useEnrichedRangePickerFieldProps = < TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure extends boolean, TError, >( - params: UseEnrichedRangePickerFieldPropsParams, + params: UseEnrichedRangePickerFieldPropsParams< + TDate, + TView, + TEnableAccessibleFieldDOMStructure, + TError + >, ) => { /* eslint-disable react-hooks/rules-of-hooks */ if (process.env.NODE_ENV !== 'production') { diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx index 7acf65d0aa523..b0095be48249f 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx @@ -10,10 +10,9 @@ import { ExportedBaseToolbarProps, useLocaleText, DateOrTimeViewWithMeridiem, - UsePickerValueFieldResponse, ExportedBaseTabsProps, } from '@mui/x-date-pickers/internals'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickerValidDate, FieldRef, BaseSingleInputFieldProps } from '@mui/x-date-pickers/models'; import useId from '@mui/utils/useId'; import { MobileRangePickerAdditionalViewProps, @@ -23,8 +22,7 @@ import { } from './useMobileRangePicker.types'; import { useEnrichedRangePickerFieldProps } from '../useEnrichedRangePickerFieldProps'; import { getReleaseInfo } from '../../utils/releaseInfo'; -import { DateRange } from '../../../models'; -import { BaseMultiInputFieldProps, RangeFieldSection } from '../../models/fields'; +import { DateRange, BaseMultiInputFieldProps, RangeFieldSection } from '../../../models'; import { useRangePosition } from '../useRangePosition'; const releaseInfo = getReleaseInfo(); @@ -32,11 +30,23 @@ const releaseInfo = getReleaseInfo(); export const useMobileRangePicker = < TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, - TExternalProps extends UseMobileRangePickerProps, + TEnableAccessibleFieldDOMStructure extends boolean, + TExternalProps extends UseMobileRangePickerProps< + TDate, + TView, + TEnableAccessibleFieldDOMStructure, + any, + TExternalProps + >, >({ props, ...pickerParams -}: UseMobileRangePickerParams) => { +}: UseMobileRangePickerParams< + TDate, + TView, + TEnableAccessibleFieldDOMStructure, + TExternalProps +>) => { useLicenseVerifier('x-date-pickers-pro', releaseInfo); const { @@ -46,6 +56,9 @@ export const useMobileRangePicker = < sx, format, formatDensity, + enableAccessibleFieldDOMStructure, + selectedSections, + onSelectedSectionsChange, timezone, label, inputRef, @@ -56,7 +69,14 @@ export const useMobileRangePicker = < localeText, } = props; - const { rangePosition, onRangePositionChange, singleInputFieldRef } = useRangePosition(props); + const startFieldRef = React.useRef>(null); + const endFieldRef = React.useRef>(null); + + const fieldType = (slots.field as any).fieldType ?? 'multi-input'; + const { rangePosition, onRangePositionChange } = useRangePosition( + props, + fieldType === 'single-input' ? startFieldRef : undefined, + ); const labelId = useId(); const contextLocaleText = useLocaleText(); @@ -78,6 +98,7 @@ export const useMobileRangePicker = < props, wrapperVariant: 'mobile', autoFocusView: true, + fieldRef: rangePosition === 'start' ? startFieldRef : endFieldRef, additionalViewProps: { rangePosition, onRangePositionChange, @@ -85,30 +106,26 @@ export const useMobileRangePicker = < }); const Field = slots.field; - const fieldType = (Field as any).fieldType ?? 'multi-input'; - const fieldProps: BaseMultiInputFieldProps< - DateRange, - TDate, - RangeFieldSection, - InferError - > = useSlotProps< + const fieldProps = useSlotProps< typeof Field, - UseMobileRangePickerSlotProps['field'], - UsePickerValueFieldResponse, RangeFieldSection, InferError> & - Partial< - Pick< - UseMobileRangePickerProps, - | 'readOnly' - | 'disabled' - | 'className' - | 'sx' - | 'format' - | 'formatDensity' - | 'timezone' - | 'label' - | 'name' - | 'autoFocus' + UseMobileRangePickerSlotProps['field'], + | Partial< + BaseSingleInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + InferError + > + > + | Partial< + BaseMultiInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + InferError > >, TExternalProps @@ -123,8 +140,11 @@ export const useMobileRangePicker = < sx, format, formatDensity, + enableAccessibleFieldDOMStructure, + selectedSections, + onSelectedSectionsChange, timezone, - ...(fieldType === 'single-input' && { inputRef, name }), + ...(inputRef ? { inputRef, name } : {}), }, ownerState: props, }); @@ -134,6 +154,7 @@ export const useMobileRangePicker = < const enrichedFieldProps = useEnrichedRangePickerFieldProps< TDate, TView, + TEnableAccessibleFieldDOMStructure, InferError >({ wrapperVariant: 'mobile', @@ -147,10 +168,11 @@ export const useMobileRangePicker = < localeText, rangePosition, onRangePositionChange, - singleInputFieldRef, pickerSlots: slots, pickerSlotProps: innerSlotProps, fieldProps, + startFieldRef, + endFieldRef, }); const slotPropsForLayout: PickersLayoutSlotProps, TDate, TView> = { diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.types.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.types.ts index 39235ce0b0d5a..7305b45164706 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.types.ts @@ -23,15 +23,16 @@ export interface UseMobileRangePickerSlots< export interface UseMobileRangePickerSlotProps< TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, -> extends UseRangePickerSlotProps, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseRangePickerSlotProps, PickersModalDialogSlotProps {} -export interface MobileRangeOnlyPickerProps - extends RangeOnlyPickerProps {} +export interface MobileRangeOnlyPickerProps extends RangeOnlyPickerProps {} export interface UseMobileRangePickerProps< TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure extends boolean, TError, TExternalProps extends UsePickerViewsProps, > extends UseRangePickerProps< @@ -50,7 +51,7 @@ export interface UseMobileRangePickerProps< * The props used for each component slot. * @default {} */ - slotProps?: UseMobileRangePickerSlotProps; + slotProps?: UseMobileRangePickerSlotProps; } export interface MobileRangePickerAdditionalViewProps extends RangePickerAdditionalViewProps {} @@ -58,7 +59,14 @@ export interface MobileRangePickerAdditionalViewProps extends RangePickerAdditio export interface UseMobileRangePickerParams< TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, - TExternalProps extends UseMobileRangePickerProps, + TEnableAccessibleFieldDOMStructure extends boolean, + TExternalProps extends UseMobileRangePickerProps< + TDate, + TView, + TEnableAccessibleFieldDOMStructure, + any, + TExternalProps + >, > extends UseRangePickerParams< TDate, TView, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputFieldSelectedSections.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputFieldSelectedSections.ts new file mode 100644 index 0000000000000..4f6987aa23612 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputFieldSelectedSections.ts @@ -0,0 +1,74 @@ +import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { UseFieldInternalProps } from '@mui/x-date-pickers/internals'; +import { FieldRef, FieldSelectedSections } from '@mui/x-date-pickers/models'; +import { RangeFieldSection } from '../../models'; + +interface UseMultiInputFieldSelectedSectionsParams + extends Pick< + UseFieldInternalProps, + 'selectedSections' | 'onSelectedSectionsChange' + > { + unstableStartFieldRef?: React.Ref>; + unstableEndFieldRef?: React.Ref>; +} + +export const useMultiInputFieldSelectedSections = ( + params: UseMultiInputFieldSelectedSectionsParams, +) => { + const unstableEndFieldRef = React.useRef>(null); + const handleUnstableEndFieldRef = useForkRef(params.unstableEndFieldRef, unstableEndFieldRef); + + const [startSelectedSection, setStartSelectedSection] = React.useState( + params.selectedSections ?? null, + ); + const [endSelectedSection, setEndSelectedSection] = React.useState(null); + + const getActiveField = () => { + if (unstableEndFieldRef.current && unstableEndFieldRef.current.isFieldFocused()) { + return 'end'; + } + + return 'start'; + }; + + const handleStartSelectedSectionChange = useEventCallback( + (newSelectedSections: FieldSelectedSections) => { + setStartSelectedSection(newSelectedSections); + if (getActiveField() === 'start') { + params.onSelectedSectionsChange?.(newSelectedSections); + } + }, + ); + + const handleEndSelectedSectionChange = useEventCallback( + (newSelectedSections: FieldSelectedSections) => { + setEndSelectedSection(newSelectedSections); + if (getActiveField() === 'end') { + params.onSelectedSectionsChange?.(newSelectedSections); + } + }, + ); + + const activeField = getActiveField(); + + return { + start: { + unstableFieldRef: params.unstableStartFieldRef, + selectedSections: + activeField === 'start' && params.selectedSections !== undefined + ? params.selectedSections + : startSelectedSection, + onSelectedSectionsChange: handleStartSelectedSectionChange, + }, + end: { + unstableFieldRef: handleUnstableEndFieldRef, + selectedSections: + activeField === 'end' && params.selectedSections !== undefined + ? params.selectedSections + : endSelectedSection, + onSelectedSectionsChange: handleEndSelectedSectionChange, + }, + }; +}; diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts index 47088f346682e..84dfaeeb86312 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts @@ -2,8 +2,6 @@ import useEventCallback from '@mui/utils/useEventCallback'; import { unstable_useDateField as useDateField, UseDateFieldComponentProps, - UseDateFieldProps, - UseDateFieldDefaultizedProps, } from '@mui/x-date-pickers/DateField'; import { useLocalizationContext, @@ -12,10 +10,13 @@ import { FieldChangeHandlerContext, UseFieldResponse, useControlledValueWithTimezone, + useDefaultizedDateField, } from '@mui/x-date-pickers/internals'; import { DateValidationError, PickerValidDate } from '@mui/x-date-pickers/models'; -import { useDefaultizedDateRangeFieldProps } from '../../../SingleInputDateRangeField/useSingleInputDateRangeField'; -import { UseMultiInputDateRangeFieldParams } from '../../../MultiInputDateRangeField/MultiInputDateRangeField.types'; +import { + UseMultiInputDateRangeFieldParams, + UseMultiInputDateRangeFieldProps, +} from '../../../MultiInputDateRangeField/MultiInputDateRangeField.types'; import { DateRangeComponentValidationProps, validateDateRange, @@ -24,9 +25,11 @@ import { rangeValueManager } from '../../utils/valueManagers'; import type { UseMultiInputRangeFieldResponse } from './useMultiInputRangeField.types'; import { DateRangeValidationError, DateRange } from '../../../models'; import { excludeProps } from './shared'; +import { useMultiInputFieldSelectedSections } from '../useMultiInputFieldSelectedSections'; export const useMultiInputDateRangeField = < TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TTextFieldSlotProps extends {}, >({ sharedProps: inSharedProps, @@ -36,11 +39,15 @@ export const useMultiInputDateRangeField = < unstableEndFieldRef, }: UseMultiInputDateRangeFieldParams< TDate, + TEnableAccessibleFieldDOMStructure, TTextFieldSlotProps ->): UseMultiInputRangeFieldResponse => { - const sharedProps = useDefaultizedDateRangeFieldProps>( - inSharedProps, - ); +>): UseMultiInputRangeFieldResponse => { + const sharedProps = useDefaultizedDateField< + TDate, + UseMultiInputDateRangeFieldProps, + typeof inSharedProps + >(inSharedProps); + const adapter = useLocalizationContext(); const { @@ -55,6 +62,8 @@ export const useMultiInputDateRangeField = < selectedSections, onSelectedSectionsChange, timezone: timezoneProp, + enableAccessibleFieldDOMStructure, + autoFocus, } = sharedProps; const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ @@ -102,43 +111,65 @@ export const useMultiInputDateRangeField = < rangeValueManager.defaultErrorState, ); - const startFieldProps: UseDateFieldComponentProps> = { + const selectedSectionsResponse = useMultiInputFieldSelectedSections({ + selectedSections, + onSelectedSectionsChange, + unstableStartFieldRef, + unstableEndFieldRef, + }); + + const startFieldProps: UseDateFieldComponentProps< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof sharedProps + > = { error: !!validationError[0], ...startTextFieldProps, + ...selectedSectionsResponse.start, disabled, readOnly, format, formatDensity, shouldRespectLeadingZeros, timezone, - unstableFieldRef: unstableStartFieldRef, value: valueProp === undefined ? undefined : valueProp[0], defaultValue: defaultValue === undefined ? undefined : defaultValue[0], onChange: handleStartDateChange, - selectedSections, - onSelectedSectionsChange, + enableAccessibleFieldDOMStructure, + autoFocus, // Do not add on end field. }; - const endFieldProps: UseDateFieldComponentProps> = { + const endFieldProps: UseDateFieldComponentProps< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof sharedProps + > = { error: !!validationError[1], ...endTextFieldProps, + ...selectedSectionsResponse.end, format, formatDensity, shouldRespectLeadingZeros, disabled, readOnly, timezone, - unstableFieldRef: unstableEndFieldRef, value: valueProp === undefined ? undefined : valueProp[1], defaultValue: defaultValue === undefined ? undefined : defaultValue[1], onChange: handleEndDateChange, - selectedSections, - onSelectedSectionsChange, + enableAccessibleFieldDOMStructure, }; - const startDateResponse = useDateField(startFieldProps) as UseFieldResponse; + const startDateResponse = useDateField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof startFieldProps + >(startFieldProps) as UseFieldResponse; - const endDateResponse = useDateField(endFieldProps) as UseFieldResponse; + const endDateResponse = useDateField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof endFieldProps + >(endFieldProps) as UseFieldResponse; /* TODO: Undo this change when a clearable behavior for multiple input range fields is implemented */ return { diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts index d398a8b5e1ab5..cecc16b5cc6eb 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts @@ -2,23 +2,18 @@ import useEventCallback from '@mui/utils/useEventCallback'; import { unstable_useDateTimeField as useDateTimeField, UseDateTimeFieldComponentProps, - UseDateTimeFieldProps, - UseDateTimeFieldDefaultizedProps, } from '@mui/x-date-pickers/DateTimeField'; import { - applyDefaultDate, - useDefaultDates, useLocalizationContext, - useUtils, useValidation, FieldChangeHandler, FieldChangeHandlerContext, UseFieldResponse, useControlledValueWithTimezone, + useDefaultizedDateTimeField, } from '@mui/x-date-pickers/internals'; import { DateTimeValidationError, PickerValidDate } from '@mui/x-date-pickers/models'; import type { - UseMultiInputDateTimeRangeFieldDefaultizedProps, UseMultiInputDateTimeRangeFieldParams, UseMultiInputDateTimeRangeFieldProps, } from '../../../MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types'; @@ -30,36 +25,11 @@ import { import { rangeValueManager } from '../../utils/valueManagers'; import type { UseMultiInputRangeFieldResponse } from './useMultiInputRangeField.types'; import { excludeProps } from './shared'; - -export const useDefaultizedDateTimeRangeFieldProps = < - TDate extends PickerValidDate, - AdditionalProps extends {}, ->( - props: UseMultiInputDateTimeRangeFieldProps, -): UseMultiInputDateTimeRangeFieldDefaultizedProps => { - const utils = useUtils(); - const defaultDates = useDefaultDates(); - - const ampm = props.ampm ?? utils.is12HourCycleInCurrentLocale(); - const defaultFormat = ampm - ? utils.formats.keyboardDateTime12h - : utils.formats.keyboardDateTime24h; - - return { - ...props, - disablePast: props.disablePast ?? false, - disableFuture: props.disableFuture ?? false, - format: props.format ?? defaultFormat, - minDate: applyDefaultDate(utils, props.minDateTime ?? props.minDate, defaultDates.minDate), - maxDate: applyDefaultDate(utils, props.maxDateTime ?? props.maxDate, defaultDates.maxDate), - minTime: props.minDateTime ?? props.minTime, - maxTime: props.maxDateTime ?? props.maxTime, - disableIgnoringDatePartForTimeValidation: Boolean(props.minDateTime || props.maxDateTime), - } as any; -}; +import { useMultiInputFieldSelectedSections } from '../useMultiInputFieldSelectedSections'; export const useMultiInputDateTimeRangeField = < TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TTextFieldSlotProps extends {}, >({ sharedProps: inSharedProps, @@ -69,24 +39,30 @@ export const useMultiInputDateTimeRangeField = < unstableEndFieldRef, }: UseMultiInputDateTimeRangeFieldParams< TDate, + TEnableAccessibleFieldDOMStructure, TTextFieldSlotProps ->): UseMultiInputRangeFieldResponse => { - const sharedProps = useDefaultizedDateTimeRangeFieldProps>( - inSharedProps, - ); +>): UseMultiInputRangeFieldResponse => { + const sharedProps = useDefaultizedDateTimeField< + TDate, + UseMultiInputDateTimeRangeFieldProps, + typeof inSharedProps + >(inSharedProps); const adapter = useLocalizationContext(); const { value: valueProp, defaultValue, format, + formatDensity, shouldRespectLeadingZeros, - timezone: timezoneProp, onChange, disabled, readOnly, selectedSections, onSelectedSectionsChange, + timezone: timezoneProp, + enableAccessibleFieldDOMStructure, + autoFocus, } = sharedProps; const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ @@ -134,49 +110,65 @@ export const useMultiInputDateTimeRangeField = < rangeValueManager.defaultErrorState, ); + const selectedSectionsResponse = useMultiInputFieldSelectedSections({ + selectedSections, + onSelectedSectionsChange, + unstableStartFieldRef, + unstableEndFieldRef, + }); + const startFieldProps: UseDateTimeFieldComponentProps< TDate, - UseDateTimeFieldDefaultizedProps + TEnableAccessibleFieldDOMStructure, + typeof sharedProps > = { error: !!validationError[0], ...startTextFieldProps, - format, - shouldRespectLeadingZeros, + ...selectedSectionsResponse.start, disabled, readOnly, + format, + formatDensity, + shouldRespectLeadingZeros, timezone, - unstableFieldRef: unstableStartFieldRef, value: valueProp === undefined ? undefined : valueProp[0], defaultValue: defaultValue === undefined ? undefined : defaultValue[0], onChange: handleStartDateChange, - selectedSections, - onSelectedSectionsChange, + enableAccessibleFieldDOMStructure, + autoFocus, // Do not add on end field. }; const endFieldProps: UseDateTimeFieldComponentProps< TDate, - UseDateTimeFieldDefaultizedProps + TEnableAccessibleFieldDOMStructure, + typeof sharedProps > = { error: !!validationError[1], ...endTextFieldProps, + ...selectedSectionsResponse.end, format, + formatDensity, shouldRespectLeadingZeros, disabled, readOnly, timezone, - unstableFieldRef: unstableEndFieldRef, value: valueProp === undefined ? undefined : valueProp[1], defaultValue: defaultValue === undefined ? undefined : defaultValue[1], onChange: handleEndDateChange, - selectedSections, - onSelectedSectionsChange, + enableAccessibleFieldDOMStructure, }; - const startDateResponse = useDateTimeField( - startFieldProps, - ) as UseFieldResponse; + const startDateResponse = useDateTimeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof startFieldProps + >(startFieldProps) as UseFieldResponse; - const endDateResponse = useDateTimeField(endFieldProps) as UseFieldResponse; + const endDateResponse = useDateTimeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof endFieldProps + >(endFieldProps) as UseFieldResponse; /* TODO: Undo this change when a clearable behavior for multiple input range fields is implemented */ return { diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputRangeField.types.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputRangeField.types.ts index d64d18c327fef..b2a961644c39a 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputRangeField.types.ts @@ -1,5 +1,5 @@ import { UseFieldResponse } from '@mui/x-date-pickers/internals'; -import { MultiInputFieldRefs } from '../../models/fields'; +import { MultiInputFieldRefs } from '../../../models'; export interface UseMultiInputRangeFieldParams< TSharedProps extends {}, @@ -10,7 +10,10 @@ export interface UseMultiInputRangeFieldParams< endTextFieldProps: TTextFieldSlotProps; } -export interface UseMultiInputRangeFieldResponse { - startDate: UseFieldResponse; - endDate: UseFieldResponse; +export interface UseMultiInputRangeFieldResponse< + TEnableAccessibleFieldDOMStructure extends boolean, + TForwardedProps extends {}, +> { + startDate: UseFieldResponse; + endDate: UseFieldResponse; } diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts index 8656009ce3f2e..a27dcb41a897f 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts @@ -2,17 +2,15 @@ import useEventCallback from '@mui/utils/useEventCallback'; import { unstable_useTimeField as useTimeField, UseTimeFieldComponentProps, - UseTimeFieldProps, - UseTimeFieldDefaultizedProps, } from '@mui/x-date-pickers/TimeField'; import { useLocalizationContext, - useUtils, useValidation, FieldChangeHandler, FieldChangeHandlerContext, UseFieldResponse, useControlledValueWithTimezone, + useDefaultizedTimeField, } from '@mui/x-date-pickers/internals'; import { PickerValidDate, TimeValidationError } from '@mui/x-date-pickers/models'; import { @@ -21,35 +19,17 @@ import { } from '../../utils/validation/validateTimeRange'; import { TimeRangeValidationError, DateRange } from '../../../models'; import type { - UseMultiInputTimeRangeFieldDefaultizedProps, UseMultiInputTimeRangeFieldParams, UseMultiInputTimeRangeFieldProps, } from '../../../MultiInputTimeRangeField/MultiInputTimeRangeField.types'; import { rangeValueManager } from '../../utils/valueManagers'; import type { UseMultiInputRangeFieldResponse } from './useMultiInputRangeField.types'; import { excludeProps } from './shared'; - -export const useDefaultizedTimeRangeFieldProps = < - TDate extends PickerValidDate, - AdditionalProps extends {}, ->( - props: UseMultiInputTimeRangeFieldProps, -): UseMultiInputTimeRangeFieldDefaultizedProps => { - const utils = useUtils(); - - const ampm = props.ampm ?? utils.is12HourCycleInCurrentLocale(); - const defaultFormat = ampm ? utils.formats.fullTime12h : utils.formats.fullTime24h; - - return { - ...props, - disablePast: props.disablePast ?? false, - disableFuture: props.disableFuture ?? false, - format: props.format ?? defaultFormat, - } as any; -}; +import { useMultiInputFieldSelectedSections } from '../useMultiInputFieldSelectedSections'; export const useMultiInputTimeRangeField = < TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TTextFieldSlotProps extends {}, >({ sharedProps: inSharedProps, @@ -59,24 +39,30 @@ export const useMultiInputTimeRangeField = < unstableEndFieldRef, }: UseMultiInputTimeRangeFieldParams< TDate, + TEnableAccessibleFieldDOMStructure, TTextFieldSlotProps ->): UseMultiInputRangeFieldResponse => { - const sharedProps = useDefaultizedTimeRangeFieldProps>( - inSharedProps, - ); +>): UseMultiInputRangeFieldResponse => { + const sharedProps = useDefaultizedTimeField< + TDate, + UseMultiInputTimeRangeFieldProps, + typeof inSharedProps + >(inSharedProps); const adapter = useLocalizationContext(); const { value: valueProp, defaultValue, format, + formatDensity, shouldRespectLeadingZeros, - timezone: timezoneProp, onChange, disabled, readOnly, selectedSections, onSelectedSectionsChange, + timezone: timezoneProp, + enableAccessibleFieldDOMStructure, + autoFocus, } = sharedProps; const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ @@ -124,41 +110,65 @@ export const useMultiInputTimeRangeField = < rangeValueManager.defaultErrorState, ); - const startFieldProps: UseTimeFieldComponentProps> = { + const selectedSectionsResponse = useMultiInputFieldSelectedSections({ + selectedSections, + onSelectedSectionsChange, + unstableStartFieldRef, + unstableEndFieldRef, + }); + + const startFieldProps: UseTimeFieldComponentProps< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof sharedProps + > = { error: !!validationError[0], ...startTextFieldProps, - format, - shouldRespectLeadingZeros, + ...selectedSectionsResponse.start, disabled, readOnly, + format, + formatDensity, + shouldRespectLeadingZeros, timezone, - unstableFieldRef: unstableStartFieldRef, value: valueProp === undefined ? undefined : valueProp[0], defaultValue: defaultValue === undefined ? undefined : defaultValue[0], onChange: handleStartDateChange, - selectedSections, - onSelectedSectionsChange, + enableAccessibleFieldDOMStructure, + autoFocus, // Do not add on end field. }; - const endFieldProps: UseTimeFieldComponentProps> = { + const endFieldProps: UseTimeFieldComponentProps< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof sharedProps + > = { error: !!validationError[1], ...endTextFieldProps, + ...selectedSectionsResponse.end, format, + formatDensity, shouldRespectLeadingZeros, disabled, readOnly, timezone, - unstableFieldRef: unstableEndFieldRef, value: valueProp === undefined ? undefined : valueProp[1], defaultValue: defaultValue === undefined ? undefined : defaultValue[1], onChange: handleEndDateChange, - selectedSections, - onSelectedSectionsChange, + enableAccessibleFieldDOMStructure, }; - const startDateResponse = useTimeField(startFieldProps) as UseFieldResponse; + const startDateResponse = useTimeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof startFieldProps + >(startFieldProps) as UseFieldResponse; - const endDateResponse = useTimeField(endFieldProps) as UseFieldResponse; + const endDateResponse = useTimeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof endFieldProps + >(endFieldProps) as UseFieldResponse; /* TODO: Undo this change when a clearable behavior for multiple input range fields is implemented */ return { diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useRangePosition.ts b/packages/x-date-pickers-pro/src/internals/hooks/useRangePosition.ts index 630f7ea701a1f..c5029471e4e88 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useRangePosition.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useRangePosition.ts @@ -2,8 +2,7 @@ import * as React from 'react'; import useControlled from '@mui/utils/useControlled'; import useEventCallback from '@mui/utils/useEventCallback'; import { FieldRef } from '@mui/x-date-pickers/models'; -import { RangePosition } from '../../models'; -import { RangeFieldSection } from '../models/fields'; +import { RangePosition, RangeFieldSection } from '../../models'; export interface UseRangePositionProps { /** @@ -27,12 +26,12 @@ export interface UseRangePositionProps { export interface UseRangePositionResponse { rangePosition: RangePosition; onRangePositionChange: (newPosition: RangePosition) => void; - singleInputFieldRef: React.MutableRefObject | undefined>; } -export const useRangePosition = (props: UseRangePositionProps): UseRangePositionResponse => { - const singleInputFieldRef = React.useRef>(); - +export const useRangePosition = ( + props: UseRangePositionProps, + singleInputFieldRef?: React.RefObject>, +): UseRangePositionResponse => { const [rangePosition, setRangePosition] = useControlled({ name: 'useRangePosition', state: 'rangePosition', @@ -43,7 +42,7 @@ export const useRangePosition = (props: UseRangePositionProps): UseRangePosition // When using a single input field, // we want to select the 1st section of the edited date when updating the range position. const syncRangePositionWithSingleInputField = (newRangePosition: RangePosition) => { - if (singleInputFieldRef.current == null) { + if (singleInputFieldRef?.current == null) { return; } @@ -58,5 +57,5 @@ export const useRangePosition = (props: UseRangePositionProps): UseRangePosition syncRangePositionWithSingleInputField(newRangePosition); }); - return { rangePosition, onRangePositionChange: handleRangePositionChange, singleInputFieldRef }; + return { rangePosition, onRangePositionChange: handleRangePositionChange }; }; diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx index 75b4ec220d76b..fbb9d3a4f76cb 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx @@ -14,9 +14,8 @@ import { UseStaticRangePickerParams, UseStaticRangePickerProps, } from './useStaticRangePicker.types'; -import { DateRange } from '../../../models'; +import { DateRange, RangeFieldSection } from '../../../models'; import { useRangePosition } from '../useRangePosition'; -import { RangeFieldSection } from '../../models/fields'; const PickerStaticLayout = styled(PickersLayout)(({ theme }) => ({ overflow: 'hidden', @@ -52,6 +51,7 @@ export const useStaticRangePicker = < ...pickerParams, props, autoFocusView: autoFocus ?? false, + fieldRef: undefined, additionalViewProps: { rangePosition, onRangePositionChange, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.types.ts b/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.types.ts index 2ff34eabfa9ca..dbabeb05d587a 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.types.ts @@ -11,9 +11,8 @@ import { ExportedPickersLayoutSlots, ExportedPickersLayoutSlotProps, } from '@mui/x-date-pickers/PickersLayout'; -import { DateRange } from '../../../models'; +import { RangeFieldSection, DateRange } from '../../../models'; import { UseRangePositionProps } from '../useRangePosition'; -import { RangeFieldSection } from '../../models/fields'; export interface UseStaticRangePickerSlots< TDate extends PickerValidDate, diff --git a/packages/x-date-pickers-pro/src/internals/models/dateRange.ts b/packages/x-date-pickers-pro/src/internals/models/dateRange.ts index 422d777db1251..f1ac7d14580ba 100644 --- a/packages/x-date-pickers-pro/src/internals/models/dateRange.ts +++ b/packages/x-date-pickers-pro/src/internals/models/dateRange.ts @@ -1,12 +1,10 @@ import { BaseDateValidationProps, - DefaultizedProps, MakeOptional, UseFieldInternalProps, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import type { DateRangeValidationError, DateRange } from '../../models'; -import { RangeFieldSection } from './fields'; +import type { DateRangeValidationError, RangeFieldSection, DateRange } from '../../models'; /** * Props used to validate a day value in range pickers. @@ -25,27 +23,21 @@ export interface DayRangeValidationProps { shouldDisableDate?: (day: TDate, position: 'start' | 'end') => boolean; } -/** - * Props used in every range picker. - */ -export interface BaseRangeProps { - /** - * If `true`, the component is disabled. - * @default false - */ - disabled?: boolean; -} - -export interface UseDateRangeFieldProps - extends MakeOptional< - UseFieldInternalProps, TDate, RangeFieldSection, DateRangeValidationError>, +export interface UseDateRangeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends MakeOptional< + Omit< + UseFieldInternalProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + DateRangeValidationError + >, + 'unstableFieldRef' + >, 'format' >, DayRangeValidationProps, - BaseDateValidationProps, - BaseRangeProps {} - -export type UseDateRangeFieldDefaultizedProps = DefaultizedProps< - UseDateRangeFieldProps, - keyof BaseDateValidationProps | 'format' ->; + BaseDateValidationProps {} diff --git a/packages/x-date-pickers-pro/src/internals/models/dateTimeRange.ts b/packages/x-date-pickers-pro/src/internals/models/dateTimeRange.ts index 3ed96dab9e6bd..aeb49c239f773 100644 --- a/packages/x-date-pickers-pro/src/internals/models/dateTimeRange.ts +++ b/packages/x-date-pickers-pro/src/internals/models/dateTimeRange.ts @@ -1,32 +1,35 @@ import { BaseDateValidationProps, TimeValidationProps, - DefaultizedProps, MakeOptional, UseFieldInternalProps, DateTimeValidationProps, DateOrTimeViewWithMeridiem, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { BaseRangeProps, DayRangeValidationProps } from './dateRange'; -import { DateTimeRangeValidationError, DateRange } from '../../models'; -import { RangeFieldSection } from './fields'; +import { DayRangeValidationProps } from './dateRange'; +import { DateTimeRangeValidationError, RangeFieldSection, DateRange } from '../../models'; -export interface UseDateTimeRangeFieldProps - extends MakeOptional< - UseFieldInternalProps< - DateRange, - TDate, - RangeFieldSection, - DateTimeRangeValidationError +export interface UseDateTimeRangeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends MakeOptional< + Omit< + UseFieldInternalProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + DateTimeRangeValidationError + >, + 'unstableFieldRef' >, 'format' >, DayRangeValidationProps, TimeValidationProps, BaseDateValidationProps, - DateTimeValidationProps, - BaseRangeProps { + DateTimeValidationProps { /** * 12h/24h view for hour selection clock. * @default `utils.is12HourCycleInCurrentLocale()` @@ -34,11 +37,6 @@ export interface UseDateTimeRangeFieldProps ampm?: boolean; } -export type UseDateTimeRangeFieldDefaultizedProps = DefaultizedProps< - UseDateTimeRangeFieldProps, - keyof BaseDateValidationProps | 'format' | 'disableIgnoringDatePartForTimeValidation' ->; - export type DateTimeRangePickerView = Exclude; export type DateTimeRangePickerViewExternal = Exclude; diff --git a/packages/x-date-pickers-pro/src/internals/models/fields.ts b/packages/x-date-pickers-pro/src/internals/models/fields.ts deleted file mode 100644 index 893c4484c6da9..0000000000000 --- a/packages/x-date-pickers-pro/src/internals/models/fields.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import { SlotComponentProps } from '@mui/base/utils'; -import { BaseFieldProps, FieldsTextFieldProps } from '@mui/x-date-pickers/internals'; -import { FieldSection, FieldRef, PickerValidDate } from '@mui/x-date-pickers/models'; - -export interface RangeFieldSection extends FieldSection { - dateName: 'start' | 'end'; -} - -export type FieldType = 'single-input' | 'multi-input'; - -/** - * Props the `textField` slot of the multi input field can receive when used inside a picker. - */ -export interface MultiInputFieldSlotTextFieldProps { - inputRef?: React.Ref; - disabled?: boolean; - readOnly?: boolean; - id?: string; - label?: React.ReactNode; - onKeyDown?: React.KeyboardEventHandler; - onFocus?: React.FocusEventHandler; - focused?: boolean; - InputProps?: Partial; -} - -/** - * Props the `root` slot of the multi input field can receive when used inside a picker. - */ -export interface MultiInputFieldSlotRootProps { - onBlur?: React.FocusEventHandler; -} - -export interface MultiInputFieldRefs { - unstableStartFieldRef?: React.Ref>; - unstableEndFieldRef?: React.Ref>; -} - -/** - * Props the multi input field can receive when used inside a picker. - * Only contains what the MUI component are passing to the field, not what users can pass using the `props.slotProps.field`. - */ -export interface BaseMultiInputFieldProps< - TValue, - TDate extends PickerValidDate, - TSection extends FieldSection, - TError, -> extends BaseFieldProps, - MultiInputFieldRefs { - slots?: { - root?: React.ElementType; - separator?: React.ElementType; - textField?: React.ElementType; - }; - slotProps?: { - root?: SlotComponentProps< - React.ElementType, - {}, - Record - >; - textField?: SlotComponentProps< - React.ElementType, - {}, - { position?: 'start' | 'end' } & Record - >; - }; -} diff --git a/packages/x-date-pickers-pro/src/internals/models/index.ts b/packages/x-date-pickers-pro/src/internals/models/index.ts index 8eded7e3e5011..58cda92f0c10d 100644 --- a/packages/x-date-pickers-pro/src/internals/models/index.ts +++ b/packages/x-date-pickers-pro/src/internals/models/index.ts @@ -1,5 +1,4 @@ export * from './dateRange'; export * from './dateTimeRange'; export * from './timeRange'; -export * from './fields'; export * from './rangePickerProps'; diff --git a/packages/x-date-pickers-pro/src/internals/models/timeRange.ts b/packages/x-date-pickers-pro/src/internals/models/timeRange.ts index 9ec9db5c39232..09cf4f179dd5f 100644 --- a/packages/x-date-pickers-pro/src/internals/models/timeRange.ts +++ b/packages/x-date-pickers-pro/src/internals/models/timeRange.ts @@ -1,31 +1,33 @@ import { BaseTimeValidationProps, TimeValidationProps, - DefaultizedProps, MakeOptional, UseFieldInternalProps, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { TimeRangeValidationError, DateRange } from '../../models'; -import { BaseRangeProps } from './dateRange'; -import { RangeFieldSection } from './fields'; +import { TimeRangeValidationError, RangeFieldSection, DateRange } from '../../models'; -export interface UseTimeRangeFieldProps - extends MakeOptional< - UseFieldInternalProps, TDate, RangeFieldSection, TimeRangeValidationError>, +export interface UseTimeRangeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends MakeOptional< + Omit< + UseFieldInternalProps< + DateRange, + TDate, + RangeFieldSection, + TEnableAccessibleFieldDOMStructure, + TimeRangeValidationError + >, + 'unstableFieldRef' + >, 'format' >, TimeValidationProps, - BaseTimeValidationProps, - BaseRangeProps { + BaseTimeValidationProps { /** * 12h/24h view for hour selection clock. * @default `utils.is12HourCycleInCurrentLocale()` */ ampm?: boolean; } - -export type UseTimeRangeFieldDefaultizedProps = DefaultizedProps< - UseTimeRangeFieldProps, - keyof BaseTimeValidationProps | 'format' ->; diff --git a/packages/x-date-pickers-pro/src/internals/utils/date-fields-utils.ts b/packages/x-date-pickers-pro/src/internals/utils/date-fields-utils.ts index 580088f556417..9210c310244de 100644 --- a/packages/x-date-pickers-pro/src/internals/utils/date-fields-utils.ts +++ b/packages/x-date-pickers-pro/src/internals/utils/date-fields-utils.ts @@ -1,4 +1,4 @@ -import { RangeFieldSection } from '../models/fields'; +import { RangeFieldSection } from '../../models'; export const splitDateRangeSections = (sections: RangeFieldSection[]) => { const startDateSections: RangeFieldSection[] = []; diff --git a/packages/x-date-pickers-pro/src/internals/utils/valueManagers.ts b/packages/x-date-pickers-pro/src/internals/utils/valueManagers.ts index 82649345b5d47..9fe081690daf2 100644 --- a/packages/x-date-pickers-pro/src/internals/utils/valueManagers.ts +++ b/packages/x-date-pickers-pro/src/internals/utils/valueManagers.ts @@ -2,8 +2,8 @@ import { PickerValueManager, replaceInvalidDateByNull, FieldValueManager, - addPositionPropertiesToSections, - createDateStrForInputFromSections, + createDateStrForV7HiddenInputFromSections, + createDateStrForV6InputFromSections, areDatesEqual, getTodayDate, getDefaultReferenceDate, @@ -14,10 +14,10 @@ import type { DateRangeValidationError, DateTimeRangeValidationError, TimeRangeValidationError, + RangeFieldSection, DateRange, RangePosition, } from '../../models'; -import { RangeFieldSection } from '../models/fields'; export type RangePickerValueManager< TValue = [any, any], @@ -93,14 +93,7 @@ export const rangeFieldValueManager: FieldValueManager, any, Rang return [prevReferenceValue[1], value[1]]; }, - getSectionsFromValue: ( - utils, - [start, end], - fallbackSections, - localizedDigits, - isRTL, - getSectionsFromDate, - ) => { + getSectionsFromValue: (utils, [start, end], fallbackSections, getSectionsFromDate) => { const separatedFallbackSections = fallbackSections == null ? { startDate: null, endDate: null } @@ -123,7 +116,8 @@ export const rangeFieldValueManager: FieldValueManager, any, Rang return { ...section, dateName: position, - endSeparator: `${section.endSeparator}${isRTL ? '\u2069 – \u2066' : ' – '}`, + // TODO: Check if RTL still works + endSeparator: `${section.endSeparator} – `, }; } @@ -134,18 +128,21 @@ export const rangeFieldValueManager: FieldValueManager, any, Rang }); }; - return addPositionPropertiesToSections( - [ - ...getSections(start, separatedFallbackSections.startDate, 'start'), - ...getSections(end, separatedFallbackSections.endDate, 'end'), - ], - localizedDigits, - isRTL, - ); + return [ + ...getSections(start, separatedFallbackSections.startDate, 'start'), + ...getSections(end, separatedFallbackSections.endDate, 'end'), + ]; + }, + getV7HiddenInputValueFromSections: (sections) => { + const dateRangeSections = splitDateRangeSections(sections); + return createDateStrForV7HiddenInputFromSections([ + ...dateRangeSections.startDate, + ...dateRangeSections.endDate, + ]); }, - getValueStrFromSections: (sections, localizedDigits, isRTL) => { + getV6InputValueFromSections: (sections, localizedDigits, isRTL) => { const dateRangeSections = splitDateRangeSections(sections); - return createDateStrForInputFromSections( + return createDateStrForV6InputFromSections( [...dateRangeSections.startDate, ...dateRangeSections.endDate], localizedDigits, isRTL, diff --git a/packages/x-date-pickers-pro/src/models/fields.ts b/packages/x-date-pickers-pro/src/models/fields.ts new file mode 100644 index 0000000000000..8bc89688a769f --- /dev/null +++ b/packages/x-date-pickers-pro/src/models/fields.ts @@ -0,0 +1,103 @@ +import * as React from 'react'; +import { SlotComponentProps } from '@mui/base/utils'; +import { BaseFieldProps, UseFieldResponse } from '@mui/x-date-pickers/internals'; +import { + BaseSingleInputPickersTextFieldProps, + FieldRef, + FieldSection, + PickerValidDate, +} from '@mui/x-date-pickers/models'; +import { UseClearableFieldResponse } from '@mui/x-date-pickers/hooks'; +import { SxProps } from '@mui/material/styles'; + +export interface RangeFieldSection extends FieldSection { + dateName: 'start' | 'end'; +} + +export type FieldType = 'single-input' | 'multi-input'; + +/** + * Props the `textField` slot of the multi input field can receive when used inside a picker. + */ +export interface MultiInputFieldSlotTextFieldProps { + label?: React.ReactNode; + id?: string; + disabled?: boolean; + readOnly?: boolean; + onKeyDown?: React.KeyboardEventHandler; + onClick?: React.MouseEventHandler; + onFocus?: React.FocusEventHandler; + focused?: boolean; + InputProps?: { + ref?: React.Ref; + endAdornment?: React.ReactNode; + startAdornment?: React.ReactNode; + }; +} + +/** + * Props the `root` slot of the multi input field can receive when used inside a picker. + */ +export interface MultiInputFieldSlotRootProps { + onBlur?: React.FocusEventHandler; +} + +export interface MultiInputFieldRefs { + unstableStartFieldRef?: React.Ref>; + unstableEndFieldRef?: React.Ref>; +} + +/** + * Props the multi input field can receive when used inside a picker. + * Only contains what the MUI components are passing to the field, + * not what users can pass using the `props.slotProps.field`. + */ +export interface BaseMultiInputFieldProps< + TValue, + TDate extends PickerValidDate, + TSection extends FieldSection, + TEnableAccessibleFieldDOMStructure extends boolean, + TError, +> extends Omit< + BaseFieldProps, + 'unstableFieldRef' + > { + sx?: SxProps; + unstableStartFieldRef?: React.Ref>; + unstableEndFieldRef?: React.Ref>; + slots?: { + root?: React.ElementType; + separator?: React.ElementType; + textField?: React.ElementType; + }; + slotProps?: { + root?: SlotComponentProps< + React.ElementType, + {}, + Record + >; + textField?: SlotComponentProps< + React.ElementType, + {}, + { position?: 'start' | 'end' } & Record + >; + }; +} + +/** + * Props the text field receives when used with a multi input picker. + * Only contains what the MUI components are passing to the text field, not what users can pass using the `props.slotProps.textField`. + */ +export type BaseMultiInputPickersTextFieldProps< + TEnableAccessibleFieldDOMStructure extends boolean, +> = UseClearableFieldResponse< + UseFieldResponse +>; + +/** + * Props the text field receives when used with a single or multi input picker. + * Only contains what the MUI components are passing to the text field, not what users can pass using the `props.slotProps.field` or `props.slotProps.textField`. + */ +export type BasePickersTextFieldProps = + BaseSingleInputPickersTextFieldProps & + BaseMultiInputPickersTextFieldProps; diff --git a/packages/x-date-pickers-pro/src/models/index.ts b/packages/x-date-pickers-pro/src/models/index.ts index c367db76916fe..7a709045ff250 100644 --- a/packages/x-date-pickers-pro/src/models/index.ts +++ b/packages/x-date-pickers-pro/src/models/index.ts @@ -1,3 +1,4 @@ +export * from './fields'; export * from './range'; export * from './validation'; export * from './multiInputRangeFieldClasses'; diff --git a/packages/x-date-pickers-pro/src/themeAugmentation/props.d.ts b/packages/x-date-pickers-pro/src/themeAugmentation/props.d.ts index c43cf07e5baa0..8b4ab840ae1d6 100644 --- a/packages/x-date-pickers-pro/src/themeAugmentation/props.d.ts +++ b/packages/x-date-pickers-pro/src/themeAugmentation/props.d.ts @@ -25,25 +25,25 @@ export interface PickersProComponentsPropsList { MuiDateTimeRangePickerToolbar: ExportedDateTimeRangePickerToolbarProps; // Multi input range fields - MuiMultiInputDateRangeField: MultiInputDateRangeFieldProps; - MuiMultiInputDateTimeRangeField: MultiInputDateTimeRangeFieldProps; - MuiMultiInputTimeRangeField: MultiInputTimeRangeFieldProps; + MuiMultiInputDateRangeField: MultiInputDateRangeFieldProps; + MuiMultiInputDateTimeRangeField: MultiInputDateTimeRangeFieldProps; + MuiMultiInputTimeRangeField: MultiInputTimeRangeFieldProps; // Single input range fields - MuiSingleInputDateRangeField: SingleInputDateRangeFieldProps; - MuiSingleInputDateTimeRangeField: SingleInputDateTimeRangeFieldProps; - MuiSingleInputTimeRangeField: SingleInputTimeRangeFieldProps; + MuiSingleInputDateRangeField: SingleInputDateRangeFieldProps; + MuiSingleInputDateTimeRangeField: SingleInputDateTimeRangeFieldProps; + MuiSingleInputTimeRangeField: SingleInputTimeRangeFieldProps; // Date Range Pickers - MuiDateRangePicker: DateRangePickerProps; - MuiDesktopDateRangePicker: DesktopDateRangePickerProps; - MuiMobileDateRangePicker: MobileDateRangePickerProps; + MuiDateRangePicker: DateRangePickerProps; + MuiDesktopDateRangePicker: DesktopDateRangePickerProps; + MuiMobileDateRangePicker: MobileDateRangePickerProps; MuiStaticDateRangePicker: StaticDateRangePickerProps; // Date Time Range Pickers - MuiDateTimeRangePicker: DateTimeRangePickerProps; - MuiDesktopDateTimeRangePicker: DesktopDateTimeRangePickerProps; - MuiMobileDateTimeRangePicker: MobileDateTimeRangePickerProps; + MuiDateTimeRangePicker: DateTimeRangePickerProps; + MuiDesktopDateTimeRangePicker: DesktopDateTimeRangePickerProps; + MuiMobileDateTimeRangePicker: MobileDateTimeRangePickerProps; } declare module '@mui/material/styles' { diff --git a/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.test.tsx b/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.test.tsx index b844cbbc195e7..130c14f288215 100644 --- a/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.test.tsx +++ b/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.test.tsx @@ -1,15 +1,13 @@ -import * as React from 'react'; -import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { DateTimeField } from '@mui/x-date-pickers/DateTimeField'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { AdapterFormats } from '@mui/x-date-pickers/models'; -import { screen } from '@mui-internal/test-utils/createRenderer'; import { expect } from 'chai'; import { createPickerRenderer, - expectInputPlaceholder, - expectInputValue, + expectFieldValueV7, describeGregorianAdapter, TEST_DATE_ISO_STRING, + buildFieldInteractions, } from 'test/utils/pickers'; import { enUS, fr, de, ru } from 'date-fns/locale'; @@ -98,25 +96,34 @@ describe('', () => { const localeObject = localeKey === 'undefined' ? undefined : { fr, de }[localeKey]; describe(`test with the ${localeName} locale`, () => { - const { render, adapter } = createPickerRenderer({ + const { render, clock, adapter } = createPickerRenderer({ clock: 'fake', adapterName: 'date-fns', locale: localeObject, }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: DateTimeField, + }); + it('should have correct placeholder', () => { - render(); + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); - expectInputPlaceholder( - screen.getByRole('textbox'), + expectFieldValueV7( + v7Response.getSectionsContainer(), localizedTexts[localeKey].placeholder, ); }); it('should have well formatted value', () => { - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date(testDate), + }); - expectInputValue(screen.getByRole('textbox'), localizedTexts[localeKey].value); + expectFieldValueV7(v7Response.getSectionsContainer(), localizedTexts[localeKey].value); }); }); }); diff --git a/packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.test.tsx b/packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.test.tsx index 268f1aeeaf6f8..a49981020364b 100644 --- a/packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.test.tsx +++ b/packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.test.tsx @@ -1,19 +1,17 @@ -import * as React from 'react'; import { expect } from 'chai'; -import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { DateTimeField } from '@mui/x-date-pickers'; import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalali'; -import { screen } from '@mui-internal/test-utils/createRenderer'; import { createPickerRenderer, - expectInputPlaceholder, - expectInputValue, + expectFieldValueV7, describeJalaliAdapter, + buildFieldInteractions, } from 'test/utils/pickers'; import { enUS } from 'date-fns/locale'; import faIR from 'date-fns-jalali/locale/fa-IR'; import faJalaliIR from 'date-fns-jalali/locale/fa-jalali-IR'; import { AdapterMomentJalaali } from '@mui/x-date-pickers/AdapterMomentJalaali'; -import { AdapterFormats } from '@mui/x-date-pickers'; +import { AdapterFormats } from '@mui/x-date-pickers/models'; describe('', () => { describeJalaliAdapter(AdapterDateFnsJalali, {}); @@ -62,25 +60,34 @@ describe('', () => { }[localeKey]; describe(`test with the "${localeKey}" locale`, () => { - const { render, adapter } = createPickerRenderer({ + const { render, adapter, clock } = createPickerRenderer({ clock: 'fake', adapterName: 'date-fns-jalali', locale: localeObject, }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: DateTimeField, + }); + it('should have correct placeholder', () => { - render(); + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); - expectInputPlaceholder( - screen.getByRole('textbox'), + expectFieldValueV7( + v7Response.getSectionsContainer(), localizedTexts[localeKey].placeholder, ); }); it('should have well formatted value', () => { - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date(testDate), + }); - expectInputValue(screen.getByRole('textbox'), localizedTexts[localeKey].value); + expectFieldValueV7(v7Response.getSectionsContainer(), localizedTexts[localeKey].value); }); }); }); diff --git a/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.test.tsx b/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.test.tsx index 94a2acb529a32..e9345793b08db 100644 --- a/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.test.tsx +++ b/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.test.tsx @@ -1,16 +1,14 @@ -import * as React from 'react'; import dayjs, { Dayjs } from 'dayjs'; -import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { DateTimeField } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { AdapterFormats } from '@mui/x-date-pickers/models'; -import { screen } from '@mui-internal/test-utils'; import { expect } from 'chai'; import { - expectInputPlaceholder, - expectInputValue, + expectFieldValueV7, createPickerRenderer, describeGregorianAdapter, TEST_DATE_ISO_STRING, + buildFieldInteractions, } from 'test/utils/pickers'; import 'dayjs/locale/fr'; import 'dayjs/locale/de'; @@ -126,25 +124,34 @@ describe('', () => { const localeObject = localeKey === 'undefined' ? undefined : { code: localeKey }; describe(`test with the ${localeName} locale`, () => { - const { render, adapter } = createPickerRenderer({ + const { render, clock, adapter } = createPickerRenderer({ clock: 'fake', adapterName: 'dayjs', locale: localeObject, }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: DateTimeField, + }); + it('should have correct placeholder', () => { - render(); + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); - expectInputPlaceholder( - screen.getByRole('textbox'), + expectFieldValueV7( + v7Response.getSectionsContainer(), localizedTexts[localeKey].placeholder, ); }); it('should have well formatted value', () => { - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date(testDate), + }); - expectInputValue(screen.getByRole('textbox'), localizedTexts[localeKey].value); + expectFieldValueV7(v7Response.getSectionsContainer(), localizedTexts[localeKey].value); }); }); }); diff --git a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.test.tsx b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.test.tsx index db6f0190580c7..7fca1db9ba2a7 100644 --- a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.test.tsx +++ b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.test.tsx @@ -1,17 +1,15 @@ -import * as React from 'react'; import { expect } from 'chai'; import { DateTime, Settings } from 'luxon'; -import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { DateTimeField } from '@mui/x-date-pickers'; import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; import { AdapterFormats } from '@mui/x-date-pickers/models'; -import { screen } from '@mui-internal/test-utils/createRenderer'; import { cleanText, createPickerRenderer, - expectInputPlaceholder, - expectInputValue, + expectFieldValueV7, describeGregorianAdapter, TEST_DATE_ISO_STRING, + buildFieldInteractions, } from 'test/utils/pickers'; describe('', () => { @@ -96,25 +94,34 @@ describe('', () => { const localeObject = localeKey === 'undefined' ? undefined : { code: localeKey }; describe(`test with the ${localeName} locale`, () => { - const { render, adapter } = createPickerRenderer({ + const { render, clock, adapter } = createPickerRenderer({ clock: 'fake', adapterName: 'luxon', locale: localeObject, }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: DateTimeField, + }); + it('should have correct placeholder', () => { - render(); + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); - expectInputPlaceholder( - screen.getByRole('textbox'), + expectFieldValueV7( + v7Response.getSectionsContainer(), localizedTexts[localeKey].placeholder, ); }); it('should have well formatted value', () => { - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date(testDate), + }); - expectInputValue(screen.getByRole('textbox'), localizedTexts[localeKey].value); + expectFieldValueV7(v7Response.getSectionsContainer(), localizedTexts[localeKey].value); }); }); }); @@ -146,25 +153,38 @@ describe('', () => { const localeObject = localeKey === 'undefined' ? undefined : { code: localeKey }; describe(`test with the ${localeName} locale`, () => { - const { render, adapter } = createPickerRenderer({ + const { render, adapter, clock } = createPickerRenderer({ clock: 'fake', adapterName: 'luxon', locale: localeObject, }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: DateTimeField, + }); + it('should have correct placeholder', () => { - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + format: 'DD', + }); - expectInputPlaceholder( - screen.getByRole('textbox'), + expectFieldValueV7( + v7Response.getSectionsContainer(), localizedTexts[localeKey].placeholder, ); }); it('should have well formatted value', () => { - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date(testDate), + format: 'DD', + }); - expectInputValue(screen.getByRole('textbox'), localizedTexts[localeKey].value); + expectFieldValueV7(v7Response.getSectionsContainer(), localizedTexts[localeKey].value); }); }); }); diff --git a/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.test.tsx b/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.test.tsx index e6107a3b3f04f..0d5c15689d0a3 100644 --- a/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.test.tsx +++ b/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.test.tsx @@ -1,18 +1,16 @@ -import * as React from 'react'; import moment, { Moment } from 'moment'; import momentTZ from 'moment-timezone'; -import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { DateTimeField } from '@mui/x-date-pickers'; import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; import { AdapterFormats } from '@mui/x-date-pickers/models'; -import { screen } from '@mui-internal/test-utils/createRenderer'; import { expect } from 'chai'; import { spy } from 'sinon'; import { createPickerRenderer, - expectInputPlaceholder, - expectInputValue, + expectFieldValueV7, describeGregorianAdapter, TEST_DATE_ISO_STRING, + buildFieldInteractions, } from 'test/utils/pickers'; import 'moment/locale/de'; import 'moment/locale/fr'; @@ -150,25 +148,34 @@ describe('', () => { const localeObject = { code: localeKey }; describe(`test with the locale "${localeKey}"`, () => { - const { render, adapter } = createPickerRenderer({ + const { render, clock, adapter } = createPickerRenderer({ clock: 'fake', adapterName: 'moment', locale: localeObject, }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: DateTimeField, + }); + it('should have correct placeholder', () => { - render(); + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); - expectInputPlaceholder( - screen.getByRole('textbox'), + expectFieldValueV7( + v7Response.getSectionsContainer(), localizedTexts[localeKey].placeholder, ); }); it('should have well formatted value', () => { - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date(testDate), + }); - expectInputValue(screen.getByRole('textbox'), localizedTexts[localeKey].value); + expectFieldValueV7(v7Response.getSectionsContainer(), localizedTexts[localeKey].value); }); }); }); diff --git a/packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.test.tsx b/packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.test.tsx index b2d0857b831a3..1e0b2499d0c5a 100644 --- a/packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.test.tsx +++ b/packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.test.tsx @@ -1,15 +1,13 @@ -import * as React from 'react'; import moment from 'moment'; import { expect } from 'chai'; -import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { DateTimeField } from '@mui/x-date-pickers'; import { AdapterMomentHijri } from '@mui/x-date-pickers/AdapterMomentHijri'; import { AdapterFormats } from '@mui/x-date-pickers/models'; -import { screen } from '@mui-internal/test-utils/createRenderer'; import { createPickerRenderer, - expectInputPlaceholder, - expectInputValue, + expectFieldValueV7, describeHijriAdapter, + buildFieldInteractions, } from 'test/utils/pickers'; import 'moment/locale/ar'; @@ -64,25 +62,34 @@ describe('', () => { const localeObject = { code: localeKey }; describe(`test with the locale "${localeKey}"`, () => { - const { render, adapter } = createPickerRenderer({ + const { render, clock, adapter } = createPickerRenderer({ clock: 'fake', adapterName: 'moment-hijri', locale: localeObject, }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: DateTimeField, + }); + it('should have correct placeholder', () => { - render(); + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); - expectInputPlaceholder( - screen.getByRole('textbox'), + expectFieldValueV7( + v7Response.getSectionsContainer(), localizedTexts[localeKey].placeholder, ); }); it('should have well formatted value', () => { - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date(testDate), + }); - expectInputValue(screen.getByRole('textbox'), localizedTexts[localeKey].value); + expectFieldValueV7(v7Response.getSectionsContainer(), localizedTexts[localeKey].value); }); }); }); diff --git a/packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.test.tsx b/packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.test.tsx index 4c5033ca1d429..1d326ff9e3257 100644 --- a/packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.test.tsx +++ b/packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.test.tsx @@ -1,15 +1,13 @@ -import * as React from 'react'; import { expect } from 'chai'; import moment from 'moment'; import jMoment from 'moment-jalaali'; -import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { DateTimeField } from '@mui/x-date-pickers/DateTimeField'; import { AdapterMomentJalaali } from '@mui/x-date-pickers/AdapterMomentJalaali'; -import { screen } from '@mui-internal/test-utils/createRenderer'; import { createPickerRenderer, - expectInputPlaceholder, - expectInputValue, + expectFieldValueV7, describeJalaliAdapter, + buildFieldInteractions, } from 'test/utils/pickers'; import { AdapterFormats } from '@mui/x-date-pickers/models'; import 'moment/locale/fa'; @@ -71,25 +69,34 @@ describe('', () => { const localeObject = { code: localeKey }; describe(`test with the locale "${localeKey}"`, () => { - const { render, adapter } = createPickerRenderer({ + const { render, clock, adapter } = createPickerRenderer({ clock: 'fake', adapterName: 'moment-jalaali', locale: localeObject, }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: DateTimeField, + }); + it('should have correct placeholder', () => { - render(); + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); - expectInputPlaceholder( - screen.getByRole('textbox'), + expectFieldValueV7( + v7Response.getSectionsContainer(), localizedTexts[localeKey].placeholder, ); }); it('should have well formatted value', () => { - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date(testDate), + }); - expectInputValue(screen.getByRole('textbox'), localizedTexts[localeKey].value); + expectFieldValueV7(v7Response.getSectionsContainer(), localizedTexts[localeKey].value); }); }); }); diff --git a/packages/x-date-pickers/src/DateField/DateField.tsx b/packages/x-date-pickers/src/DateField/DateField.tsx index cf4218512439d..61abeceed5ca6 100644 --- a/packages/x-date-pickers/src/DateField/DateField.tsx +++ b/packages/x-date-pickers/src/DateField/DateField.tsx @@ -4,14 +4,19 @@ import MuiTextField from '@mui/material/TextField'; import { useThemeProps } from '@mui/material/styles'; import { useSlotProps } from '@mui/base/utils'; import { refType } from '@mui/utils'; -import { DateFieldProps, DateFieldSlotProps } from './DateField.types'; +import { DateFieldProps } from './DateField.types'; import { useDateField } from './useDateField'; import { useClearableField } from '../hooks'; +import { PickersTextField } from '../PickersTextField'; import { convertFieldResponseIntoMuiTextFieldProps } from '../internals/utils/convertFieldResponseIntoMuiTextFieldProps'; import { PickerValidDate } from '../models'; -type DateFieldComponent = (( - props: DateFieldProps & React.RefAttributes, +type DateFieldComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DateFieldProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -24,8 +29,11 @@ type DateFieldComponent = (( * * - [DateField API](https://mui.com/x/api/date-pickers/date-field/) */ -const DateField = React.forwardRef(function DateField( - inProps: DateFieldProps, +const DateField = React.forwardRef(function DateField< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DateFieldProps, inRef: React.Ref, ) { const themeProps = useThemeProps({ @@ -37,13 +45,10 @@ const DateField = React.forwardRef(function DateField = useSlotProps< - typeof TextField, - DateFieldSlotProps['textField'], - DateFieldProps, - DateFieldProps - >({ + const TextField = + slots?.textField ?? + (inProps.enableAccessibleFieldDOMStructure ? PickersTextField : MuiTextField); + const textFieldProps = useSlotProps({ elementType: TextField, externalSlotProps: slotProps?.textField, externalForwardedProps: other, @@ -51,13 +56,17 @@ const DateField = React.forwardRef(function DateField; // TODO: Remove when mui/material-ui#35088 will be merged textFieldProps.inputProps = { ...inputProps, ...textFieldProps.inputProps }; textFieldProps.InputProps = { ...InputProps, ...textFieldProps.InputProps }; - const fieldResponse = useDateField(textFieldProps); + const fieldResponse = useDateField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof textFieldProps + >(textFieldProps); const convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); const processedFieldProps = useClearableField({ @@ -112,6 +121,10 @@ DateField.propTypes = { * @default false */ disablePast: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.bool, /** * If `true`, the component is displayed in focused state. */ @@ -238,11 +251,11 @@ DateField.propTypes = { required: PropTypes.bool, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -259,10 +272,6 @@ DateField.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers/src/DateField/DateField.types.ts b/packages/x-date-pickers/src/DateField/DateField.types.ts index 51a00deb3a8ab..ef75f2b560905 100644 --- a/packages/x-date-pickers/src/DateField/DateField.types.ts +++ b/packages/x-date-pickers/src/DateField/DateField.types.ts @@ -1,40 +1,60 @@ import * as React from 'react'; import { SlotComponentProps } from '@mui/base/utils'; import TextField from '@mui/material/TextField'; -import { UseClearableFieldSlots, UseClearableFieldSlotProps } from '../hooks/useClearableField'; -import { DateValidationError, FieldSection, PickerValidDate } from '../models'; +import { + ExportedUseClearableFieldProps, + UseClearableFieldSlots, + UseClearableFieldSlotProps, +} from '../hooks/useClearableField'; +import { + DateValidationError, + FieldSection, + PickerValidDate, + BuiltInFieldTextFieldProps, +} from '../models'; import { UseFieldInternalProps } from '../internals/hooks/useField'; -import { DefaultizedProps, MakeOptional } from '../internals/models/helpers'; +import { MakeOptional } from '../internals/models/helpers'; import { BaseDateValidationProps, DayValidationProps, MonthValidationProps, YearValidationProps, } from '../internals/models/validation'; -import { FieldsTextFieldProps } from '../internals/models/fields'; -export interface UseDateFieldProps - extends MakeOptional< - UseFieldInternalProps, +export interface UseDateFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends MakeOptional< + UseFieldInternalProps< + TDate | null, + TDate, + FieldSection, + TEnableAccessibleFieldDOMStructure, + DateValidationError + >, 'format' >, DayValidationProps, MonthValidationProps, YearValidationProps, - BaseDateValidationProps {} - -export type UseDateFieldDefaultizedProps = DefaultizedProps< - UseDateFieldProps, - keyof BaseDateValidationProps | 'format' ->; + BaseDateValidationProps, + ExportedUseClearableFieldProps {} export type UseDateFieldComponentProps< TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TChildProps extends {}, -> = Omit> & UseDateFieldProps; +> = Omit> & + UseDateFieldProps; -export interface DateFieldProps - extends UseDateFieldComponentProps { +export type DateFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> = UseDateFieldComponentProps< + TDate, + TEnableAccessibleFieldDOMStructure, + BuiltInFieldTextFieldProps +> & { /** * Overridable component slots. * @default {} @@ -44,21 +64,29 @@ export interface DateFieldProps * The props used for each component slot. * @default {} */ - slotProps?: DateFieldSlotProps; -} + slotProps?: DateFieldSlotProps; +}; -export type DateFieldOwnerState = DateFieldProps; +export type DateFieldOwnerState< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> = DateFieldProps; export interface DateFieldSlots extends UseClearableFieldSlots { /** * Form control with an input to render the value. - * Receives the same props as `@mui/material/TextField`. - * @default TextField from '@mui/material' + * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ textField?: React.ElementType; } -export interface DateFieldSlotProps - extends UseClearableFieldSlotProps { - textField?: SlotComponentProps>; +export interface DateFieldSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseClearableFieldSlotProps { + textField?: SlotComponentProps< + typeof TextField, + {}, + DateFieldOwnerState + >; } diff --git a/packages/x-date-pickers/src/DateField/index.ts b/packages/x-date-pickers/src/DateField/index.ts index 6fd6dcef27329..cd6119e98a255 100644 --- a/packages/x-date-pickers/src/DateField/index.ts +++ b/packages/x-date-pickers/src/DateField/index.ts @@ -4,5 +4,4 @@ export type { UseDateFieldProps, UseDateFieldComponentProps, DateFieldProps, - UseDateFieldDefaultizedProps, } from './DateField.types'; diff --git a/packages/x-date-pickers/src/DateField/tests/describes.DateField.test.tsx b/packages/x-date-pickers/src/DateField/tests/describes.DateField.test.tsx index 3b191484b51cf..1c26eeda56fbe 100644 --- a/packages/x-date-pickers/src/DateField/tests/describes.DateField.test.tsx +++ b/packages/x-date-pickers/src/DateField/tests/describes.DateField.test.tsx @@ -1,16 +1,14 @@ import * as React from 'react'; -import { userEvent } from '@mui-internal/test-utils'; -import TextField from '@mui/material/TextField'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; import { DateField } from '@mui/x-date-pickers/DateField'; import { createPickerRenderer, wrapPickerMount, - expectInputValue, - expectInputPlaceholder, + expectFieldValueV7, adapterToUse, - getTextbox, describeValidation, describeValue, + getFieldInputRoot, } from 'test/utils/pickers'; import { describeConformance } from 'test/utils/describeConformance'; @@ -24,9 +22,9 @@ describe(' - Describes', () => { componentFamily: 'field', })); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, - inheritComponent: TextField, + inheritComponent: PickersTextField, render, muiName: 'MuiDateField', wrapMount: wrapPickerMount, @@ -49,20 +47,19 @@ describe(' - Describes', () => { emptyValue: null, clock, assertRenderedValue: (expectedValue: any) => { - const input = getTextbox(); - if (!expectedValue) { - expectInputPlaceholder(input, 'MM/DD/YYYY'); - } - expectInputValue( - input, - expectedValue ? adapterToUse.format(expectedValue, 'keyboardDate') : '', - ); + const fieldRoot = getFieldInputRoot(); + + const expectedValueStr = expectedValue + ? adapterToUse.format(expectedValue, 'keyboardDate') + : 'MM/DD/YYYY'; + + expectFieldValueV7(fieldRoot, expectedValueStr); }, - setNewValue: (value, { selectSection }) => { + setNewValue: (value, { selectSection, pressKey }) => { const newValue = adapterToUse.addDays(value, 1); selectSection('day'); - const input = getTextbox(); - userEvent.keyPress(input, { key: 'ArrowUp' }); + pressKey(undefined, 'ArrowUp'); + return newValue; }, })); diff --git a/packages/x-date-pickers/src/DateField/tests/editing.DateField.test.tsx b/packages/x-date-pickers/src/DateField/tests/editing.DateField.test.tsx index a26b58f6a522b..16777b15972a0 100644 --- a/packages/x-date-pickers/src/DateField/tests/editing.DateField.test.tsx +++ b/packages/x-date-pickers/src/DateField/tests/editing.DateField.test.tsx @@ -1,9 +1,13 @@ -import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import { DateField } from '@mui/x-date-pickers/DateField'; import { act, userEvent, fireEvent } from '@mui-internal/test-utils'; -import { expectInputValue, getTextbox, describeAdapters } from 'test/utils/pickers'; +import { + expectFieldValueV7, + getTextbox, + describeAdapters, + expectFieldValueV6, +} from 'test/utils/pickers'; describe(' - Editing', () => { describeAdapters('key: ArrowDown', DateField, ({ adapter, testFieldKeyPress }) => { @@ -208,19 +212,40 @@ describe(' - Editing', () => { describeAdapters(`key: Delete`, DateField, ({ adapter, testFieldKeyPress, renderWithProps }) => { it('should clear the selected section when only this section is completed', () => { - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, }); - selectSection('month'); + + v7Response.selectSection('month'); + + // Set a value for the "month" section + v7Response.pressKey(0, 'j'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'January YYYY'); + + userEvent.keyPress(v7Response.getActiveSection(0), { key: 'Delete' }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `${adapter.formats.month} ${adapter.formats.year}`, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); // Set a value for the "month" section fireEvent.change(input, { target: { value: 'j YYYY' }, }); // press "j" - expectInputValue(input, 'January YYYY'); + expectFieldValueV6(input, 'January YYYY'); userEvent.keyPress(input, { key: 'Delete' }); - expectInputValue(input, 'MMMM YYYY'); + expectFieldValueV6(input, 'MMMM YYYY'); }); it('should clear the selected section when all sections are completed', () => { @@ -233,55 +258,120 @@ describe(' - Editing', () => { }); it('should clear all the sections when all sections are selected and all sections are completed', () => { - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, defaultValue: adapter.date(), }); - selectSection('month'); + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + userEvent.keyPress(v7Response.getSectionsContainer(), { key: 'Delete' }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `${adapter.formats.month} ${adapter.formats.year}`, + defaultValue: adapter.date(), + }); + + const input = getTextbox(); + v6Response.selectSection('month'); // Select all sections userEvent.keyPress(input, { key: 'a', ctrlKey: true }); userEvent.keyPress(input, { key: 'Delete' }); - expectInputValue(input, 'MMMM YYYY'); + expectFieldValueV6(input, 'MMMM YYYY'); }); it('should clear all the sections when all sections are selected and not all sections are completed', () => { - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, }); - selectSection('month'); + v7Response.selectSection('month'); + + // Set a value for the "month" section + v7Response.pressKey(0, 'j'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'January YYYY'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + userEvent.keyPress(v7Response.getSectionsContainer(), { key: 'Delete' }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `${adapter.formats.month} ${adapter.formats.year}`, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); // Set a value for the "month" section fireEvent.change(input, { target: { value: 'j YYYY' }, }); // Press "j" - expectInputValue(input, 'January YYYY'); + expectFieldValueV6(input, 'January YYYY'); // Select all sections userEvent.keyPress(input, { key: 'a', ctrlKey: true }); userEvent.keyPress(input, { key: 'Delete' }); - expectInputValue(input, 'MMMM YYYY'); + expectFieldValueV6(input, 'MMMM YYYY'); }); it('should not keep query after typing again on a cleared section', () => { - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: adapter.formats.year, }); - selectSection('year'); + v7Response.selectSection('year'); + + v7Response.pressKey(0, '2'); + expectFieldValueV7(v7Response.getSectionsContainer(), '0002'); + + userEvent.keyPress(v7Response.getActiveSection(0), { key: 'Delete' }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'YYYY'); + + v7Response.pressKey(0, '2'); + expectFieldValueV7(v7Response.getSectionsContainer(), '0002'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: adapter.formats.year, + }); + + const input = getTextbox(); + v6Response.selectSection('year'); fireEvent.change(input, { target: { value: '2' } }); // press "2" - expectInputValue(input, '0002'); + expectFieldValueV6(input, '0002'); userEvent.keyPress(input, { key: 'Delete' }); - expectInputValue(input, 'YYYY'); + expectFieldValueV6(input, 'YYYY'); fireEvent.change(input, { target: { value: '2' } }); // press "2" - expectInputValue(input, '0002'); + expectFieldValueV6(input, '0002'); }); it('should not clear the sections when props.readOnly = true', () => { @@ -295,63 +385,138 @@ describe(' - Editing', () => { }); it('should not call `onChange` when clearing all sections and both dates are already empty', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, - onChange, + onChange: onChangeV7, }); - selectSection('month'); + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + userEvent.keyPress(v7Response.getSectionsContainer(), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(0); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `${adapter.formats.month} ${adapter.formats.year}`, + onChange: onChangeV6, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); // Select all sections userEvent.keyPress(input, { key: 'a', ctrlKey: true }); userEvent.keyPress(input, { key: 'Delete' }); - expect(onChange.callCount).to.equal(0); + expect(onChangeV6.callCount).to.equal(0); }); it('should call `onChange` when clearing the first and last section', () => { - const handleChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); + + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + format: `${adapter.formats.month} ${adapter.formats.year}`, + defaultValue: adapter.date(), + onChange: onChangeV7, + }); + + v7Response.selectSection('month'); + + userEvent.keyPress(v7Response.getActiveSection(0), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(1); + expect(onChangeV7.lastCall.args[1].validationError).to.equal('invalidDate'); + + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowRight' }); + + userEvent.keyPress(v7Response.getActiveSection(1), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(2); + expect(onChangeV7.lastCall.firstArg).to.equal(null); + expect(onChangeV7.lastCall.args[1].validationError).to.equal(null); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); - const { selectSection, input } = renderWithProps({ + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, format: `${adapter.formats.month} ${adapter.formats.year}`, defaultValue: adapter.date(), - onChange: handleChange, + onChange: onChangeV6, }); - selectSection('month'); + const input = getTextbox(); + v6Response.selectSection('month'); + userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.lastCall.args[1].validationError).to.equal('invalidDate'); + expect(onChangeV6.callCount).to.equal(1); + expect(onChangeV6.lastCall.args[1].validationError).to.equal('invalidDate'); userEvent.keyPress(input, { key: 'ArrowRight' }); userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(2); - expect(handleChange.lastCall.firstArg).to.equal(null); - expect(handleChange.lastCall.args[1].validationError).to.equal(null); + expect(onChangeV6.callCount).to.equal(2); + expect(onChangeV6.lastCall.firstArg).to.equal(null); + expect(onChangeV6.lastCall.args[1].validationError).to.equal(null); }); it('should not call `onChange` if the section is already empty', () => { - const handleChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { selectSection, input } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, defaultValue: adapter.date(), - onChange: handleChange, + onChange: onChangeV7, }); - selectSection('month'); + v7Response.selectSection('month'); + + userEvent.keyPress(v7Response.getActiveSection(0), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(1); + + userEvent.keyPress(v7Response.getActiveSection(0), { key: 'Delete' }); + expect(onChangeV7.callCount).to.equal(1); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `${adapter.formats.month} ${adapter.formats.year}`, + defaultValue: adapter.date(), + onChange: onChangeV6, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(1); + expect(onChangeV6.callCount).to.equal(1); userEvent.keyPress(input, { key: 'Delete' }); - expect(handleChange.callCount).to.equal(1); + expect(onChangeV6.callCount).to.equal(1); }); }); - describeAdapters('Digit editing', DateField, ({ adapter, testFieldChange }) => { + describeAdapters('Digit editing', DateField, ({ adapter, testFieldChange, renderWithProps }) => { it('should set the day to the digit pressed when no digit no value is provided', () => { testFieldChange({ format: adapter.formats.dayOfMonth, @@ -513,18 +678,66 @@ describe(' - Editing', () => { }); it('should allow to type the date 29th of February for leap years', () => { - testFieldChange({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: adapter.formats.keyboardDate, - keyStrokes: [ - { value: '2/DD/YYYY', expected: '02/DD/YYYY' }, - { value: '02/2/YYYY', expected: '02/02/YYYY' }, - { value: '02/9/YYYY', expected: '02/29/YYYY' }, - { value: '02/29/1', expected: '02/29/0001' }, - { value: '02/29/9', expected: '02/29/0019' }, - { value: '02/29/8', expected: '02/29/0198' }, - { value: '02/29/8', expected: '02/29/1988' }, - ], }); + + v7Response.selectSection('month'); + + v7Response.pressKey(0, '2'); + expectFieldValueV7(v7Response.getSectionsContainer(), '02/DD/YYYY'); + + v7Response.pressKey(1, '2'); + expectFieldValueV7(v7Response.getSectionsContainer(), '02/02/YYYY'); + + v7Response.pressKey(1, '9'); + expectFieldValueV7(v7Response.getSectionsContainer(), '02/29/YYYY'); + + v7Response.pressKey(2, '1'); + expectFieldValueV7(v7Response.getSectionsContainer(), '02/29/0001'); + + v7Response.pressKey(2, '9'); + expectFieldValueV7(v7Response.getSectionsContainer(), '02/29/0019'); + + v7Response.pressKey(2, '8'); + expectFieldValueV7(v7Response.getSectionsContainer(), '02/29/0198'); + + v7Response.pressKey(2, '8'); + expectFieldValueV7(v7Response.getSectionsContainer(), '02/29/1988'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: adapter.formats.keyboardDate, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + fireEvent.change(input, { target: { value: '2/DD/YYYY' } }); + expectFieldValueV6(input, '02/DD/YYYY'); + + fireEvent.change(input, { target: { value: '02/2/YYYY' } }); + expectFieldValueV6(input, '02/02/YYYY'); + + fireEvent.change(input, { target: { value: '02/9/YYYY' } }); + expectFieldValueV6(input, '02/29/YYYY'); + + fireEvent.change(input, { target: { value: '02/29/1' } }); + expectFieldValueV6(input, '02/29/0001'); + + fireEvent.change(input, { target: { value: '02/29/9' } }); + expectFieldValueV6(input, '02/29/0019'); + + fireEvent.change(input, { target: { value: '02/29/8' } }); + expectFieldValueV6(input, '02/29/0198'); + + fireEvent.change(input, { target: { value: '02/29/8' } }); + expectFieldValueV6(input, '02/29/1988'); }); it('should not edit when props.readOnly = true and no value is provided', () => { @@ -637,39 +850,135 @@ describe(' - Editing', () => { DateField, ({ adapter, renderWithProps, testFieldChange }) => { it('should clear the selected section when only this section is completed (Backspace)', () => { - testFieldChange({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, - keyStrokes: [ - { value: 'j YYYY', expected: 'January YYYY' }, - { value: ' YYYY', expected: 'MMMM YYYY' }, - ], }); + + v7Response.selectSection('month'); + v7Response.pressKey(0, 'j'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'January YYYY'); + + v7Response.pressKey(0, ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + format: `${adapter.formats.month} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + fireEvent.change(input, { target: { value: 'j YYYY' } }); + expectFieldValueV6(input, 'January YYYY'); + + fireEvent.change(input, { target: { value: ' YYYY' } }); + expectFieldValueV6(input, 'MMMM YYYY'); }); it('should clear the selected section when all sections are completed (Backspace)', () => { - testFieldChange({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + format: `${adapter.formats.month} ${adapter.formats.year}`, + defaultValue: adapter.date(), + }); + + v7Response.selectSection('month'); + + v7Response.pressKey(0, ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM 2022'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ format: `${adapter.formats.month} ${adapter.formats.year}`, defaultValue: adapter.date(), - keyStrokes: [{ value: ' 2022', expected: 'MMMM 2022' }], + enableAccessibleFieldDOMStructure: false, }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + fireEvent.change(input, { target: { value: ' 2022' } }); + expectFieldValueV6(input, 'MMMM 2022'); }); it('should clear all the sections when all sections are selected and all sections are completed (Backspace)', () => { - testFieldChange({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, defaultValue: adapter.date(), - keyStrokes: [{ value: '', expected: 'MMMM YYYY' }], }); + + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + v7Response.pressKey(null, ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + format: `${adapter.formats.month} ${adapter.formats.year}`, + defaultValue: adapter.date(), + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + + fireEvent.change(input, { target: { value: '' } }); + expectFieldValueV6(input, 'MMMM YYYY'); }); it('should clear all the sections when all sections are selected and not all sections are completed (Backspace)', () => { - testFieldChange({ + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, - keyStrokes: [ - { value: 'j YYYY', expected: 'January YYYY' }, - { value: '', expected: 'MMMM YYYY' }, - ], }); + + v7Response.selectSection('month'); + v7Response.pressKey(0, 'j'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'January YYYY'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + v7Response.pressKey(null, ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + format: `${adapter.formats.month} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + fireEvent.change(input, { target: { value: 'j YYYY' } }); + expectFieldValueV6(input, 'January YYYY'); + + // Select all sections + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + + fireEvent.change(input, { target: { value: '' } }); + expectFieldValueV6(input, 'MMMM YYYY'); }); it('should not keep query after typing again on a cleared section (Backspace)', () => { @@ -688,7 +997,7 @@ describe(' - Editing', () => { format: adapter.formats.year, defaultValue: adapter.date(), readOnly: true, - keyStrokes: [{ value: '2', expected: '2022' }], + keyStrokes: [{ value: '', expected: '2022' }], }); }); @@ -705,25 +1014,51 @@ describe(' - Editing', () => { }); it('should call `onChange` when clearing the first and last section (Backspace)', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { selectSection, input } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `${adapter.formats.month} ${adapter.formats.year}`, defaultValue: adapter.date(), - onChange, + onChange: onChangeV7, + }); + + v7Response.selectSection('month'); + v7Response.pressKey(0, ''); + expect(onChangeV7.callCount).to.equal(1); + expect(onChangeV7.lastCall.args[1].validationError).to.equal('invalidDate'); + + v7Response.selectSection('year'); + v7Response.pressKey(1, ''); + expect(onChangeV7.callCount).to.equal(2); + expect(onChangeV7.lastCall.firstArg).to.equal(null); + expect(onChangeV7.lastCall.args[1].validationError).to.equal(null); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `${adapter.formats.month} ${adapter.formats.year}`, + defaultValue: adapter.date(), + onChange: onChangeV6, }); - selectSection('month'); + const input = getTextbox(); + v6Response.selectSection('month'); fireEvent.change(input, { target: { value: ' 2022' } }); - expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[1].validationError).to.equal('invalidDate'); + expect(onChangeV6.callCount).to.equal(1); + expect(onChangeV6.lastCall.args[1].validationError).to.equal('invalidDate'); userEvent.keyPress(input, { key: 'ArrowRight' }); fireEvent.change(input, { target: { value: 'MMMM ' } }); - expect(onChange.callCount).to.equal(2); - expect(onChange.lastCall.firstArg).to.equal(null); - expect(onChange.lastCall.args[1].validationError).to.equal(null); + expect(onChangeV6.callCount).to.equal(2); + expect(onChangeV6.lastCall.firstArg).to.equal(null); + expect(onChangeV6.lastCall.args[1].validationError).to.equal(null); }); it('should not call `onChange` if the section is already empty (Backspace)', () => { @@ -739,13 +1074,36 @@ describe(' - Editing', () => { onChange, }); - expect(onChange.callCount).to.equal(1); + // 1 for v7 and 1 for v7 input + expect(onChange.callCount).to.equal(2); }); }, ); - describeAdapters('Pasting', DateField, ({ adapter, render, renderWithProps, clickOnInput }) => { - const firePasteEvent = (input: HTMLInputElement, pastedValue?: string, rawValue?: string) => { + describeAdapters('Pasting', DateField, ({ adapter, renderWithProps }) => { + const firePasteEventV7 = (element: HTMLElement, pastedValue: string) => { + act(() => { + const clipboardEvent = new window.Event('paste', { + bubbles: true, + cancelable: true, + composed: true, + }); + + // @ts-ignore + clipboardEvent.clipboardData = { + getData: () => pastedValue, + }; + // canContinue is `false` if default have been prevented + const canContinue = element.dispatchEvent(clipboardEvent); + if (!canContinue) { + return; + } + + fireEvent.input(element, { target: { textContent: pastedValue } }); + }); + }; + + const firePasteEventV6 = (input: HTMLInputElement, pastedValue?: string, rawValue?: string) => { act(() => { const clipboardEvent = new window.Event('paste', { bubbles: true, @@ -777,177 +1135,385 @@ describe(' - Editing', () => { }; it('should set the date when all sections are selected, the pasted value is valid and a value is provided', () => { - const onChange = spy(); - - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const onChangeV7 = spy(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, defaultValue: adapter.date(), - onChange, + onChange: onChangeV7, }); + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + firePasteEventV7(v7Response.getSectionsContainer(), '09/16/2022'); + + expect(onChangeV7.callCount).to.equal(1); + expect(onChangeV7.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16)); - selectSection('month'); + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + const v6Response = renderWithProps({ + defaultValue: adapter.date(), + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); + v6Response.selectSection('month'); // Select all sections userEvent.keyPress(input, { key: 'a', ctrlKey: true }); - firePasteEvent(input, '09/16/2022'); + firePasteEventV6(input, '09/16/2022'); - expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16)); + expect(onChangeV6.callCount).to.equal(1); + expect(onChangeV6.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16)); }); it('should set the date when all sections are selected, the pasted value is valid and no value is provided', () => { - const onChange = spy(); - - const { input, selectSection } = renderWithProps({ - onChange, + // Test with v7 input + const onChangeV7 = spy(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + onChange: onChangeV7, }); + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); - selectSection('month'); + firePasteEventV7(v7Response.getSectionsContainer(), '09/16/2022'); + + expect(onChangeV7.callCount).to.equal(1); + expect(onChangeV7.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16)); + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + const v6Response = renderWithProps({ + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); + v6Response.selectSection('month'); // Select all sections userEvent.keyPress(input, { key: 'a', ctrlKey: true }); - firePasteEvent(input, '09/16/2022'); + firePasteEventV6(input, '09/16/2022'); - expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16)); + expect(onChangeV6.callCount).to.equal(1); + expect(onChangeV6.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16)); }); it('should not set the date when all sections are selected and the pasted value is not valid', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + onChange: onChangeV7, + }); + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + firePasteEventV7(v7Response.getSectionsContainer(), 'Some invalid content'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY'); + v7Response.unmount(); - const { input, selectSection } = renderWithProps({ onChange }); - selectSection('month'); + // Test with v6 input + const onChangeV6 = spy(); + const v6Response = renderWithProps({ + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); + v6Response.selectSection('month'); // Select all sections userEvent.keyPress(input, { key: 'a', ctrlKey: true }); - firePasteEvent(input, 'Some invalid content'); - expectInputValue(input, 'MM/DD/YYYY'); + firePasteEventV6(input, 'Some invalid content'); + expectFieldValueV6(input, 'MM/DD/YYYY'); }); it('should set the date when all sections are selected and the format contains escaped characters', () => { const { start: startChar, end: endChar } = adapter.escapedCharacters; - const onChange = spy(); - render( - , - ); + + // Test with v7 input + const onChangeV7 = spy(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + onChange: onChangeV7, + format: `${startChar}Escaped${endChar} ${adapter.formats.year}`, + }); + + v7Response.selectSection('year'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + firePasteEventV7(v7Response.getSectionsContainer(), `Escaped 2014`); + expect(onChangeV7.callCount).to.equal(1); + expect(adapter.getYear(onChangeV7.lastCall.firstArg)).to.equal(2014); + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + const v6Response = renderWithProps({ + onChange: onChangeV6, + format: `${startChar}Escaped${endChar} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); - clickOnInput(input, 1); + v6Response.selectSection('year'); // Select all sections userEvent.keyPress(input, { key: 'a', ctrlKey: true }); - firePasteEvent(input, `Escaped 2014`); - expect(onChange.callCount).to.equal(1); - expect(adapter.getYear(onChange.lastCall.firstArg)).to.equal(2014); + firePasteEventV6(input, `Escaped 2014`); + expect(onChangeV6.callCount).to.equal(1); + expect(adapter.getYear(onChangeV6.lastCall.firstArg)).to.equal(2014); }); it('should not set the date when all sections are selected and props.readOnly = true', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { input, selectSection } = renderWithProps({ - onChange, + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + onChange: onChangeV7, readOnly: true, }); - selectSection('month'); + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + firePasteEventV7(v7Response.getSectionsContainer(), '09/16/2022'); + expect(onChangeV7.callCount).to.equal(0); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + onChange: onChangeV6, + readOnly: true, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); // Select all sections userEvent.keyPress(input, { key: 'a', ctrlKey: true }); - firePasteEvent(input, '09/16/2022'); - expect(onChange.callCount).to.equal(0); + firePasteEventV6(input, '09/16/2022'); + expect(onChangeV6.callCount).to.equal(0); }); it('should set the section when one section is selected, the pasted value has the correct type and no value is provided', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { input, selectSection } = renderWithProps({ - onChange, + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + onChange: onChangeV7, }); - selectSection('month'); + v7Response.selectSection('month'); + + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY'); + firePasteEventV7(v7Response.getActiveSection(0), '12'); - expectInputValue(input, 'MM/DD/YYYY'); - firePasteEvent(input, '12'); + expect(onChangeV7.callCount).to.equal(1); + expectFieldValueV7(v7Response.getSectionsContainer(), '12/DD/YYYY'); - expect(onChange.callCount).to.equal(1); - expectInputValue(input, '12/DD/YYYY'); + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + expectFieldValueV6(input, 'MM/DD/YYYY'); + firePasteEventV6(input, '12'); + + expect(onChangeV6.callCount).to.equal(1); + expectFieldValueV6(input, '12/DD/YYYY'); }); it('should set the section when one section is selected, the pasted value has the correct type and value is provided', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, defaultValue: adapter.date('2018-01-13'), - onChange, + onChange: onChangeV7, }); - selectSection('month'); + v7Response.selectSection('month'); + + expectFieldValueV7(v7Response.getSectionsContainer(), '01/13/2018'); + firePasteEventV7(v7Response.getActiveSection(0), '12'); + expectFieldValueV7(v7Response.getSectionsContainer(), '12/13/2018'); + expect(onChangeV7.callCount).to.equal(1); + expect(onChangeV7.lastCall.firstArg).toEqualDateTime(new Date(2018, 11, 13)); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); - expectInputValue(input, '01/13/2018'); - firePasteEvent(input, '12'); - expectInputValue(input, '12/13/2018'); - expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 11, 13)); + const v6Response = renderWithProps({ + defaultValue: adapter.date('2018-01-13'), + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + expectFieldValueV6(input, '01/13/2018'); + firePasteEventV6(input, '12'); + expectFieldValueV6(input, '12/13/2018'); + expect(onChangeV6.callCount).to.equal(1); + expect(onChangeV6.lastCall.firstArg).toEqualDateTime(new Date(2018, 11, 13)); }); it('should not update the section when one section is selected and the pasted value has incorrect type', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, defaultValue: adapter.date('2018-01-13'), - onChange, + onChange: onChangeV7, }); - selectSection('month'); + v7Response.selectSection('month'); + + expectFieldValueV7(v7Response.getSectionsContainer(), '01/13/2018'); + firePasteEventV7(v7Response.getActiveSection(0), 'Jun'); + expectFieldValueV7(v7Response.getSectionsContainer(), '01/13/2018'); + expect(onChangeV7.callCount).to.equal(0); - expectInputValue(input, '01/13/2018'); - firePasteEvent(input, 'Jun'); - expectInputValue(input, '01/13/2018'); - expect(onChange.callCount).to.equal(0); + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + defaultValue: adapter.date('2018-01-13'), + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + expectFieldValueV6(input, '01/13/2018'); + firePasteEventV6(input, 'Jun'); + expectFieldValueV6(input, '01/13/2018'); + expect(onChangeV6.callCount).to.equal(0); }); it('should reset sections internal state when pasting', () => { - const onChange = spy(); + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + defaultValue: adapter.date('2018-12-05'), + }); + + v7Response.selectSection('day'); + + v7Response.pressKey(1, '2'); + expectFieldValueV7(v7Response.getSectionsContainer(), '12/02/2018'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'a', ctrlKey: true }); - const { input, selectSection } = renderWithProps({ + firePasteEventV7(v7Response.getSectionsContainer(), '09/16/2022'); + expectFieldValueV7(v7Response.getSectionsContainer(), '09/16/2022'); + + v7Response.selectSection('day'); + + v7Response.pressKey(1, '2'); // Press 2 + expectFieldValueV7(v7Response.getSectionsContainer(), '09/02/2022'); // If internal state is not reset it would be 22 instead of 02 + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ defaultValue: adapter.date('2018-12-05'), - onChange, + enableAccessibleFieldDOMStructure: false, }); - selectSection('day'); + const input = getTextbox(); + v6Response.selectSection('day'); fireEvent.change(input, { target: { value: '12/2/2018' } }); // Press 2 - expectInputValue(input, '12/02/2018'); + expectFieldValueV6(input, '12/02/2018'); - firePasteEvent(input, '09/16/2022'); - expectInputValue(input, '09/16/2022'); + firePasteEventV6(input, '09/16/2022'); + expectFieldValueV6(input, '09/16/2022'); fireEvent.change(input, { target: { value: '09/2/2022' } }); // Press 2 - expectInputValue(input, '09/02/2022'); // If internal state is not reset it would be 22 instead of 02 + expectFieldValueV6(input, '09/02/2022'); // If internal state is not reset it would be 22 instead of 02 }); it('should allow pasting a section', () => { - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + defaultValue: adapter.date('2018-12-05'), + }); + + v7Response.selectSection('month'); + + v7Response.pressKey(0, '1'); // Press 1 + expectFieldValueV7(v7Response.getSectionsContainer(), '01/05/2018'); + + firePasteEventV7(v7Response.getActiveSection(0), '05'); + expectFieldValueV7(v7Response.getSectionsContainer(), '05/05/2018'); + + v7Response.selectSection('month'); // move back to month section + v7Response.pressKey(0, '2'); // check that the search query has been cleared after pasting + expectFieldValueV7(v7Response.getSectionsContainer(), '02/05/2018'); // If internal state is not reset it would be 12 instead of 02 + + v7Response.unmount(); + + const v6Response = renderWithProps({ defaultValue: adapter.date('2018-12-05'), + enableAccessibleFieldDOMStructure: false, }); - selectSection('month'); + const input = getTextbox(); + v6Response.selectSection('month'); fireEvent.change(input, { target: { value: '1/05/2018' } }); // initiate search query on month section - expectInputValue(input, '01/05/2018'); + expectFieldValueV6(input, '01/05/2018'); - firePasteEvent(input, undefined, '05'); - expectInputValue(input, '05/05/2018'); + firePasteEventV6(input, undefined, '05'); + expectFieldValueV6(input, '05/05/2018'); - selectSection('month'); // move back to month section + v6Response.selectSection('month'); // move back to month section fireEvent.change(input, { target: { value: '2/05/2018' } }); // check that the search query has been cleared after pasting - expectInputValue(input, '02/05/2018'); // If internal state is not reset it would be 12 instead of 02 + expectFieldValueV6(input, '02/05/2018'); // If internal state is not reset it would be 12 instead of 02 }); }); @@ -956,79 +1522,166 @@ describe(' - Editing', () => { DateField, ({ adapter, renderWithProps }) => { it('should not loose time information when a value is provided', () => { - const onChange = spy(); - - const { input, selectSection } = renderWithProps({ + // Test with v7 input + const onChangeV7 = spy(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, defaultValue: adapter.date('2010-04-03T03:03:03'), - onChange, + onChange: onChangeV7, }); + v7Response.selectSection('year'); + fireEvent.keyDown(v7Response.getActiveSection(2), { key: 'ArrowDown' }); + expect(onChangeV7.lastCall.firstArg).toEqualDateTime(new Date(2009, 3, 3, 3, 3, 3)); - selectSection('year'); - userEvent.keyPress(input, { key: 'ArrowDown' }); + v7Response.unmount(); - expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2009, 3, 3, 3, 3, 3)); + // Test with v6 input + const onChangeV6 = spy(); + const v6Response = renderWithProps({ + defaultValue: adapter.date('2010-04-03T03:03:03'), + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); + v6Response.selectSection('year'); + userEvent.keyPress(input, { key: 'ArrowDown' }); + expect(onChangeV6.lastCall.firstArg).toEqualDateTime(new Date(2009, 3, 3, 3, 3, 3)); }); it('should not loose time information when cleaning the date then filling it again', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, defaultValue: adapter.date('2010-04-03T03:03:03'), - onChange, + onChange: onChangeV7, }); - selectSection('month'); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + v7Response.pressKey(null, ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY'); + v7Response.selectSection('month'); + + v7Response.pressKey(0, '1'); + expectFieldValueV7(v7Response.getSectionsContainer(), '01/DD/YYYY'); + + v7Response.pressKey(0, '1'); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/DD/YYYY'); + + v7Response.pressKey(1, '2'); + v7Response.pressKey(1, '5'); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/25/YYYY'); + + v7Response.pressKey(2, '2'); + v7Response.pressKey(2, '0'); + v7Response.pressKey(2, '0'); + v7Response.pressKey(2, '9'); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/25/2009'); + expect(onChangeV7.lastCall.firstArg).toEqualDateTime(new Date(2009, 10, 25, 3, 3, 3)); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + defaultValue: adapter.date('2010-04-03T03:03:03'), + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('month'); userEvent.keyPress(input, { key: 'a', ctrlKey: true }); fireEvent.change(input, { target: { value: '' } }); userEvent.keyPress(input, { key: 'ArrowLeft' }); fireEvent.change(input, { target: { value: '1/DD/YYYY' } }); // Press "1" - expectInputValue(input, '01/DD/YYYY'); + expectFieldValueV6(input, '01/DD/YYYY'); fireEvent.change(input, { target: { value: '11/DD/YYYY' } }); // Press "1" - expectInputValue(input, '11/DD/YYYY'); + expectFieldValueV6(input, '11/DD/YYYY'); fireEvent.change(input, { target: { value: '11/2/YYYY' } }); // Press "2" fireEvent.change(input, { target: { value: '11/5/YYYY' } }); // Press "5" - expectInputValue(input, '11/25/YYYY'); + expectFieldValueV6(input, '11/25/YYYY'); fireEvent.change(input, { target: { value: '11/25/2' } }); // Press "2" fireEvent.change(input, { target: { value: '11/25/0' } }); // Press "0" fireEvent.change(input, { target: { value: '11/25/0' } }); // Press "0" fireEvent.change(input, { target: { value: '11/25/9' } }); // Press "9" - expectInputValue(input, '11/25/2009'); - expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2009, 10, 25, 3, 3, 3)); + expectFieldValueV6(input, '11/25/2009'); + expect(onChangeV6.lastCall.firstArg).toEqualDateTime(new Date(2009, 10, 25, 3, 3, 3)); }); it('should not loose date information when using the year format and value is provided', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: adapter.formats.year, defaultValue: adapter.date('2010-04-03T03:03:03'), - onChange, + onChange: onChangeV7, }); - selectSection('year'); + v7Response.selectSection('year'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowDown' }); + + expect(onChangeV7.lastCall.firstArg).toEqualDateTime(new Date(2009, 3, 3, 3, 3, 3)); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + const v6Response = renderWithProps({ + format: adapter.formats.year, + defaultValue: adapter.date('2010-04-03T03:03:03'), + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); + + const input = getTextbox(); + v6Response.selectSection('year'); userEvent.keyPress(input, { key: 'ArrowDown' }); - expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2009, 3, 3, 3, 3, 3)); + expect(onChangeV6.lastCall.firstArg).toEqualDateTime(new Date(2009, 3, 3, 3, 3, 3)); }); it('should not loose date information when using the month format and value is provided', () => { - const onChange = spy(); + // Test with v7 input + const onChangeV7 = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: adapter.formats.month, defaultValue: adapter.date('2010-04-03T03:03:03'), - onChange, + onChange: onChangeV7, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'ArrowDown' }); + v7Response.selectSection('month'); + userEvent.keyPress(v7Response.getActiveSection(0), { key: 'ArrowDown' }); + expect(onChangeV7.lastCall.firstArg).toEqualDateTime(new Date(2010, 2, 3, 3, 3, 3)); + + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + + const v6Response = renderWithProps({ + format: adapter.formats.month, + defaultValue: adapter.date('2010-04-03T03:03:03'), + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); - expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2010, 2, 3, 3, 3, 3)); + v6Response.selectSection('month'); + const input = getTextbox(); + userEvent.keyPress(input, { key: 'ArrowDown' }); + expect(onChangeV6.lastCall.firstArg).toEqualDateTime(new Date(2010, 2, 3, 3, 3, 3)); }); }, ); @@ -1036,89 +1689,73 @@ describe(' - Editing', () => { describeAdapters( 'Imperative change (without any section selected)', DateField, - ({ adapter, render }) => { + ({ adapter, renderWithProps }) => { it('should set the date when the change value is valid and no value is provided', () => { - const onChange = spy(); - render(); - const input = getTextbox(); - fireEvent.change(input, { target: { value: '09/16/2022' } }); + // Test with v7 input + const onChangeV7 = spy(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + onChange: onChangeV7, + }); + fireEvent.change(v7Response.getHiddenInput(), { target: { value: '09/16/2022' } }); - expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16)); - }); + expect(onChangeV7.callCount).to.equal(1); + expect(onChangeV7.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16)); - it('should set the date when the change value is valid and a value is provided', () => { - const onChange = spy(); - render( - , - ); + v7Response.unmount(); + + // Test with v6 input + const onChangeV6 = spy(); + renderWithProps({ + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); const input = getTextbox(); fireEvent.change(input, { target: { value: '09/16/2022' } }); - expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16, 3, 3, 3)); + expect(onChangeV6.callCount).to.equal(1); + expect(onChangeV6.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16)); }); - }, - ); - describeAdapters('Editing from the outside', DateField, ({ adapter, render, clickOnInput }) => { - it('should be able to reset the value from the outside', () => { - const { setProps } = render(); - const input = getTextbox(); - expectInputValue(input, '11/23/2022'); - - setProps({ value: null }); + it('should set the date when the change value is valid and a value is provided', () => { + // Test with v7 input + const onChangeV7 = spy(); - clickOnInput(input, 0); - expectInputValue(input, 'MM/DD/YYYY'); - }); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + defaultValue: adapter.date('2010-04-03T03:03:03'), + onChange: onChangeV7, + }); - it('should reset the input query state on an unfocused field', () => { - const { setProps } = render(); - const input = getTextbox(); + fireEvent.change(v7Response.getHiddenInput(), { target: { value: '09/16/2022' } }); - clickOnInput(input, 0); + expect(onChangeV7.callCount).to.equal(1); + expect(onChangeV7.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16, 3, 3, 3)); - fireEvent.change(input, { target: { value: '1/DD/YYYY' } }); // Press "1" - expectInputValue(input, '01/DD/YYYY'); + v7Response.unmount(); - fireEvent.change(input, { target: { value: '11/DD/YYYY' } }); // Press "1" - expectInputValue(input, '11/DD/YYYY'); + // Test with v6 input + const onChangeV6 = spy(); - fireEvent.change(input, { target: { value: '11/2/YYYY' } }); // Press "2" - fireEvent.change(input, { target: { value: '11/5/YYYY' } }); // Press "5" - expectInputValue(input, '11/25/YYYY'); + renderWithProps({ + defaultValue: adapter.date('2010-04-03T03:03:03'), + onChange: onChangeV6, + enableAccessibleFieldDOMStructure: false, + }); - fireEvent.change(input, { target: { value: '11/25/2' } }); // Press "2" - fireEvent.change(input, { target: { value: '11/25/0' } }); // Press "0" - expectInputValue(input, '11/25/0020'); + const input = getTextbox(); + fireEvent.change(input, { target: { value: '09/16/2022' } }); - act(() => { - input.blur(); + expect(onChangeV6.callCount).to.equal(1); + expect(onChangeV6.lastCall.firstArg).toEqualDateTime(new Date(2022, 8, 16, 3, 3, 3)); }); - - setProps({ value: adapter.date('2022-11-23') }); - expectInputValue(input, '11/23/2022'); - - // not using clickOnInput here because it will call `runLast` on the fake timer - act(() => { - fireEvent.mouseDown(input); - fireEvent.mouseUp(input); - input.setSelectionRange(6, 9); - fireEvent.click(input); - }); - - fireEvent.change(input, { target: { value: '11/23/2' } }); // Press "2" - expectInputValue(input, '11/23/0002'); - fireEvent.change(input, { target: { value: '11/23/1' } }); // Press "0" - expectInputValue(input, '11/23/0021'); - }); - }); + }, + ); describeAdapters( - 'Android editing', + 'Android editing ( textfield DOM structure only)', DateField, - ({ adapter, render, renderWithProps, clickOnInput }) => { + ({ adapter, renderWithProps }) => { let originalUserAgent: string = ''; beforeEach(() => { @@ -1139,13 +1776,15 @@ describe(' - Editing', () => { }); it('should support digit editing', () => { - render(); + const v6Response = renderWithProps({ + defaultValue: adapter.date('2022-11-23'), + enableAccessibleFieldDOMStructure: false, + }); const input = getTextbox(); const initialValueStr = input.value; - const sectionStart = initialValueStr.indexOf('2'); - clickOnInput(input, sectionStart, sectionStart + 1); + v6Response.selectSection('day'); act(() => { // Remove the selected section @@ -1163,16 +1802,19 @@ describe(' - Editing', () => { fireEvent.change(input, { target: { value: initialValueStr.replace('23', '1') } }); }); - expectInputValue(input, '11/21/2022'); + expectFieldValueV6(input, '11/21/2022'); }); it('should support letter editing', () => { - const { input, selectSection } = renderWithProps({ + // Test with v6 input + const v6Response = renderWithProps({ defaultValue: adapter.date('2022-05-16'), format: `${adapter.formats.month} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, }); - selectSection('month'); + const input = getTextbox(); + v6Response.selectSection('month'); act(() => { // Remove the selected section @@ -1190,15 +1832,140 @@ describe(' - Editing', () => { fireEvent.change(input, { target: { value: 'u 2022' } }); }); - expectInputValue(input, 'June 2022'); + expectFieldValueV6(input, 'June 2022'); }); }, ); + describeAdapters('Editing from the outside', DateField, ({ adapter, renderWithProps, clock }) => { + it('should be able to reset the value from the outside', () => { + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date('2022-11-23'), + }); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/23/2022'); + + v7Response.setProps({ value: null }); + + v7Response.selectSection('month'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + value: adapter.date('2022-11-23'), + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); + expectFieldValueV6(input, '11/23/2022'); + + v6Response.setProps({ value: null }); + + v6Response.selectSection('month'); + expectFieldValueV6(input, 'MM/DD/YYYY'); + }); + + it('should reset the input query state on an unfocused field', () => { + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true, value: null }); + + v7Response.selectSection('month'); + + v7Response.pressKey(0, '1'); + expectFieldValueV7(v7Response.getSectionsContainer(), '01/DD/YYYY'); + + v7Response.pressKey(0, '1'); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/DD/YYYY'); + + v7Response.pressKey(1, '2'); + v7Response.pressKey(1, '5'); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/25/YYYY'); + + v7Response.pressKey(2, '2'); + v7Response.pressKey(2, '0'); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/25/0020'); + + act(() => { + v7Response.getSectionsContainer().blur(); + }); + + clock.runToLast(); + + v7Response.setProps({ value: adapter.date('2022-11-23') }); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/23/2022'); + + v7Response.selectSection('year'); + + v7Response.pressKey(2, '2'); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/23/0002'); + v7Response.pressKey(2, '1'); + expectFieldValueV7(v7Response.getSectionsContainer(), '11/23/0021'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false, value: null }); + + const input = getTextbox(); + v6Response.selectSection('month'); + + fireEvent.change(input, { target: { value: '1/DD/YYYY' } }); // Press "1" + expectFieldValueV6(input, '01/DD/YYYY'); + + fireEvent.change(input, { target: { value: '11/DD/YYYY' } }); // Press "1" + expectFieldValueV6(input, '11/DD/YYYY'); + + fireEvent.change(input, { target: { value: '11/2/YYYY' } }); // Press "2" + fireEvent.change(input, { target: { value: '11/5/YYYY' } }); // Press "5" + expectFieldValueV6(input, '11/25/YYYY'); + + fireEvent.change(input, { target: { value: '11/25/2' } }); // Press "2" + fireEvent.change(input, { target: { value: '11/25/0' } }); // Press "0" + expectFieldValueV6(input, '11/25/0020'); + + act(() => { + input.blur(); + }); + + v6Response.setProps({ value: adapter.date('2022-11-23') }); + expectFieldValueV6(input, '11/23/2022'); + + act(() => { + fireEvent.mouseDown(input); + fireEvent.mouseUp(input); + input.setSelectionRange(6, 9); + fireEvent.click(input); + }); + + fireEvent.change(input, { target: { value: '11/23/2' } }); // Press "2" + expectFieldValueV6(input, '11/23/0002'); + fireEvent.change(input, { target: { value: '11/23/1' } }); // Press "0" + expectFieldValueV6(input, '11/23/0021'); + }); + }); + describeAdapters('Select all', DateField, ({ renderWithProps }) => { it('should edit the 1st section when all sections are selected', () => { - const { input, selectSection } = renderWithProps({}); - selectSection('month'); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + + // When all sections are selected, the value only contains the key pressed + v7Response.pressKey(null, '9'); + + expectFieldValueV7(v7Response.getSectionsContainer(), '09/DD/YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + v6Response.selectSection('month'); + const input = getTextbox(); // Select all sections userEvent.keyPress(input, { key: 'a', ctrlKey: true }); @@ -1206,7 +1973,7 @@ describe(' - Editing', () => { // When all sections are selected, the value only contains the key pressed fireEvent.change(input, { target: { value: '9' } }); - expectInputValue(input, '09/DD/YYYY'); + expectFieldValueV6(input, '09/DD/YYYY'); }); }); }); diff --git a/packages/x-date-pickers/src/DateField/tests/format.DateField.test.tsx b/packages/x-date-pickers/src/DateField/tests/format.DateField.test.tsx index 7652e776dafb4..51e71600acee8 100644 --- a/packages/x-date-pickers/src/DateField/tests/format.DateField.test.tsx +++ b/packages/x-date-pickers/src/DateField/tests/format.DateField.test.tsx @@ -1,39 +1,71 @@ -import * as React from 'react'; import { - expectInputPlaceholder, - expectInputValue, + expectFieldPlaceholderV6, + expectFieldValueV6, + expectFieldValueV7, getTextbox, describeAdapters, } from 'test/utils/pickers'; import { DateField } from '@mui/x-date-pickers/DateField'; -describeAdapters(' - Format', DateField, ({ render, adapter }) => { +describeAdapters(' - Format', DateField, ({ adapter, renderWithProps }) => { it('should support escaped characters in start separator', () => { const { start: startChar, end: endChar } = adapter.escapedCharacters; - // For Day.js: "[Escaped] YYYY" - const { setProps } = render( - , - ); + + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + // For Day.js: "[Escaped] YYYY" + format: `${startChar}Escaped${endChar} ${adapter.formats.year}`, + }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'Escaped YYYY'); + + v7Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'Escaped 2019'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + // For Day.js: "[Escaped] YYYY" + format: `${startChar}Escaped${endChar} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); const input = getTextbox(); - expectInputPlaceholder(input, 'Escaped YYYY'); + expectFieldPlaceholderV6(input, 'Escaped YYYY'); - setProps({ value: adapter.date('2019-01-01') }); - expectInputValue(input, 'Escaped 2019'); + v6Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV6(input, 'Escaped 2019'); }); it('should support escaped characters between sections separator', () => { const { start: startChar, end: endChar } = adapter.escapedCharacters; - // For Day.js: "MMMM [Escaped] YYYY" - const { setProps } = render( - , - ); + + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + // For Day.js: "MMMM [Escaped] YYYY" + format: `${adapter.formats.month} ${startChar}Escaped${endChar} ${adapter.formats.year}`, + }); + + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM Escaped YYYY'); + + v7Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'January Escaped 2019'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + // For Day.js: "MMMM [Escaped] YYYY" + format: `${adapter.formats.month} ${startChar}Escaped${endChar} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); - expectInputPlaceholder(input, 'MMMM Escaped YYYY'); + expectFieldPlaceholderV6(input, 'MMMM Escaped YYYY'); - setProps({ value: adapter.date('2019-01-01') }); - expectInputValue(input, 'January Escaped 2019'); + v6Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV6(input, 'January Escaped 2019'); }); it('should support nested escaped characters', function test() { @@ -44,78 +76,172 @@ describeAdapters(' - Format', DateField, ({ render, adapter }) => { this.skip(); } - // For Day.js: "MMMM [Escaped[] YYYY" - const { setProps } = render( - , - ); + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + // For Day.js: "MMMM [Escaped[] YYYY" + format: `${adapter.formats.month} ${startChar}Escaped ${startChar}${endChar} ${adapter.formats.year}`, + }); + + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM Escaped [ YYYY'); + + v7Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'January Escaped [ 2019'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + // For Day.js: "MMMM [Escaped[] YYYY" + format: `${adapter.formats.month} ${startChar}Escaped ${startChar}${endChar} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); - expectInputPlaceholder(input, 'MMMM Escaped [ YYYY'); + expectFieldPlaceholderV6(input, 'MMMM Escaped [ YYYY'); - setProps({ value: adapter.date('2019-01-01') }); - expectInputValue(input, 'January Escaped [ 2019'); + v6Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV6(input, 'January Escaped [ 2019'); }); - it('should support several escaped parts', function test() { + it('should support several escaped parts', () => { const { start: startChar, end: endChar } = adapter.escapedCharacters; - // For Day.js: "[Escaped] MMMM [Escaped] YYYY" - const { setProps } = render( - , - ); + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + // For Day.js: "[Escaped] MMMM [Escaped] YYYY" + format: `${startChar}Escaped${endChar} ${adapter.formats.month} ${startChar}Escaped${endChar} ${adapter.formats.year}`, + }); + + expectFieldValueV7(v7Response.getSectionsContainer(), 'Escaped MMMM Escaped YYYY'); + + v7Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'Escaped January Escaped 2019'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + // For Day.js: "[Escaped] MMMM [Escaped] YYYY" + format: `${startChar}Escaped${endChar} ${adapter.formats.month} ${startChar}Escaped${endChar} ${adapter.formats.year}`, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); - expectInputPlaceholder(input, 'Escaped MMMM Escaped YYYY'); + expectFieldPlaceholderV6(input, 'Escaped MMMM Escaped YYYY'); - setProps({ value: adapter.date('2019-01-01') }); - expectInputValue(input, 'Escaped January Escaped 2019'); + v6Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV6(input, 'Escaped January Escaped 2019'); }); it('should support format with only escaped parts', function test() { const { start: startChar, end: endChar } = adapter.escapedCharacters; - // For Day.js: "[Escaped] [Escaped]" - render(); + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + // For Day.js: "[Escaped] [Escaped]" + format: `${startChar}Escaped${endChar} ${startChar}Escaped${endChar}`, + }); + + expectFieldValueV7(v7Response.getSectionsContainer(), 'Escaped Escaped'); + + v7Response.unmount(); + + // Test with v6 input + renderWithProps({ + // For Day.js: "[Escaped] [Escaped]" + format: `${startChar}Escaped${endChar} ${startChar}Escaped${endChar}`, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); - expectInputPlaceholder(input, 'Escaped Escaped'); + expectFieldPlaceholderV6(input, 'Escaped Escaped'); }); it('should add spaces around `/` when `formatDensity = "spacious"`', () => { - const { setProps } = render(); + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + formatDensity: `spacious`, + }); + + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM / DD / YYYY'); + + v7Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV7(v7Response.getSectionsContainer(), '01 / 01 / 2019'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + formatDensity: `spacious`, + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); - expectInputPlaceholder(input, 'MM / DD / YYYY'); + expectFieldPlaceholderV6(input, 'MM / DD / YYYY'); - setProps({ value: adapter.date('2019-01-01') }); - expectInputValue(input, '01 / 01 / 2019'); + v6Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV6(input, '01 / 01 / 2019'); }); it('should add spaces around `.` when `formatDensity = "spacious"`', () => { - const { setProps } = render( - , - ); + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + formatDensity: `spacious`, + format: adapter.expandFormat(adapter.formats.keyboardDate).replace(/\//g, '.'), + }); + + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM . DD . YYYY'); + + v7Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV7(v7Response.getSectionsContainer(), '01 . 01 . 2019'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + formatDensity: `spacious`, + format: adapter.expandFormat(adapter.formats.keyboardDate).replace(/\//g, '.'), + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); - expectInputPlaceholder(input, 'MM . DD . YYYY'); + expectFieldPlaceholderV6(input, 'MM . DD . YYYY'); - setProps({ value: adapter.date('2019-01-01') }); - expectInputValue(input, '01 . 01 . 2019'); + v6Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV6(input, '01 . 01 . 2019'); }); it('should add spaces around `-` when `formatDensity = "spacious"`', () => { - const { setProps } = render( - , - ); + // Test with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + formatDensity: `spacious`, + format: adapter.expandFormat(adapter.formats.keyboardDate).replace(/\//g, '-'), + }); + + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM - DD - YYYY'); + + v7Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV7(v7Response.getSectionsContainer(), '01 - 01 - 2019'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ + formatDensity: `spacious`, + format: adapter.expandFormat(adapter.formats.keyboardDate).replace(/\//g, '-'), + enableAccessibleFieldDOMStructure: false, + }); + const input = getTextbox(); - expectInputPlaceholder(input, 'MM - DD - YYYY'); + expectFieldPlaceholderV6(input, 'MM - DD - YYYY'); - setProps({ value: adapter.date('2019-01-01') }); - expectInputValue(input, '01 - 01 - 2019'); + v6Response.setProps({ value: adapter.date('2019-01-01') }); + expectFieldValueV6(input, '01 - 01 - 2019'); }); }); diff --git a/packages/x-date-pickers/src/DateField/tests/selection.DateField.test.tsx b/packages/x-date-pickers/src/DateField/tests/selection.DateField.test.tsx index 935fb69cce371..80a08a1ba58f9 100644 --- a/packages/x-date-pickers/src/DateField/tests/selection.DateField.test.tsx +++ b/packages/x-date-pickers/src/DateField/tests/selection.DateField.test.tsx @@ -1,10 +1,10 @@ -import * as React from 'react'; import { expect } from 'chai'; import { DateField } from '@mui/x-date-pickers/DateField'; -import { act, userEvent } from '@mui-internal/test-utils'; +import { act, fireEvent } from '@mui-internal/test-utils'; import { createPickerRenderer, - expectInputValue, + expectFieldValueV7, + expectFieldValueV6, getCleanedSelectedContent, getTextbox, buildFieldInteractions, @@ -16,25 +16,50 @@ describe(' - Selection', () => { const { renderWithProps } = buildFieldInteractions({ clock, render, Component: DateField }); describe('Focus', () => { - it('should select all on mount focus (`autoFocus = true`)', () => { - render(); - const input = getTextbox(); + it('should select 1st section (v7) / all sections (v6) on mount focus (`autoFocus = true`)', () => { + // Text with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + autoFocus: true, + }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY'); + expect(getCleanedSelectedContent()).to.equal('MM'); + v7Response.unmount(); - expectInputValue(input, 'MM/DD/YYYY'); - expect(getCleanedSelectedContent(input)).to.equal('MM/DD/YYYY'); + // Text with v6 input + renderWithProps({ enableAccessibleFieldDOMStructure: false, autoFocus: true }); + const input = getTextbox(); + expectFieldValueV6(input, 'MM/DD/YYYY'); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY'); }); - it('should select all on mount focus (`autoFocus = true`) with start separator', () => { - render(); + it('should select 1st section (v7) / all sections (v6) (`autoFocus = true`) with start separator', () => { + // Text with v7 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + autoFocus: true, + format: `- ${adapterToUse.formats.year}`, + }); + expectFieldValueV7(v7Response.getSectionsContainer(), '- YYYY'); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + v7Response.unmount(); + + // Text with v6 input + renderWithProps({ + enableAccessibleFieldDOMStructure: false, + autoFocus: true, + format: `- ${adapterToUse.formats.year}`, + }); const input = getTextbox(); - - expectInputValue(input, '- YYYY'); - expect(getCleanedSelectedContent(input)).to.equal('- YYYY'); + expectFieldValueV6(input, '- YYYY'); + expect(getCleanedSelectedContent()).to.equal('- YYYY'); }); - it('should select all on focus', () => { - render(); + it('should select all on focus (v6 only)', () => { + // Text with v6 input + renderWithProps({ enableAccessibleFieldDOMStructure: false }); const input = getTextbox(); + // Simulate a focus interaction on desktop act(() => { input.focus(); @@ -42,13 +67,18 @@ describe(' - Selection', () => { clock.runToLast(); input.select(); - expectInputValue(input, 'MM/DD/YYYY'); - expect(getCleanedSelectedContent(input)).to.equal('MM/DD/YYYY'); + expectFieldValueV6(input, 'MM/DD/YYYY'); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY'); }); - it('should select all on focus with start separator', () => { - render(); + it('should select all on focus with start separator (v6 only)', () => { + // Text with v6 input + renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `- ${adapterToUse.formats.year}`, + }); const input = getTextbox(); + // Simulate a focus interaction on desktop act(() => { input.focus(); @@ -56,135 +86,254 @@ describe(' - Selection', () => { clock.runToLast(); input.select(); - expectInputValue(input, '- YYYY'); - expect(getCleanedSelectedContent(input)).to.equal('- YYYY'); + expectFieldValueV6(input, '- YYYY'); + expect(getCleanedSelectedContent()).to.equal('- YYYY'); }); - it('should select day on mobile', () => { - render(); + it('should select day on mobile (v6 only)', () => { + // Test with v6 input + renderWithProps({ enableAccessibleFieldDOMStructure: false }); + const input = getTextbox(); // Simulate a touch focus interaction on mobile act(() => { input.focus(); }); clock.runToLast(); - expectInputValue(input, 'MM/DD/YYYY'); + expectFieldValueV6(input, 'MM/DD/YYYY'); input.setSelectionRange(3, 5); expect(input.selectionStart).to.equal(3); expect(input.selectionEnd).to.equal(5); }); - it('should select day on desktop', () => { - const { input, selectSection } = renderWithProps({}); - render(); + it('should select day on desktop (v6 only)', () => { + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); - selectSection('day'); + const input = getTextbox(); + v6Response.selectSection('day'); - expectInputValue(input, 'MM/DD/YYYY'); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + expectFieldValueV6(input, 'MM/DD/YYYY'); + expect(getCleanedSelectedContent()).to.equal('DD'); }); }); describe('Click', () => { it('should select the clicked selection when the input is already focused', () => { - const { input, selectSection } = renderWithProps({}); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + + v7Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + + v7Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); + + v7Response.unmount(); - selectSection('day'); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); - selectSection('month'); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + v6Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + + v6Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); }); it('should not change the selection when clicking on the only already selected section', () => { - const { input, selectSection } = renderWithProps({}); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + + v7Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + + v7Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); - selectSection('day'); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + v6Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); - selectSection('day'); - expect(getCleanedSelectedContent(input)).to.equal('DD'); + v6Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); }); }); describe('key: Ctrl + A', () => { it('should select all sections', () => { - const { input, selectSection } = renderWithProps({}); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY'); - selectSection('month'); - userEvent.keyPress(input, { key: 'a', ctrlKey: true }); - expect(getCleanedSelectedContent(input)).to.equal('MM/DD/YYYY'); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + const input = getTextbox(); + v6Response.selectSection('month'); + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY'); }); it('should select all sections with start separator', () => { - const { input, selectSection } = renderWithProps({ + // Test with v6 input + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, format: `- ${adapterToUse.formats.year}`, }); + v7Response.selectSection('year'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + expect(getCleanedSelectedContent()).to.equal('- YYYY'); + + v7Response.unmount(); - selectSection('year'); - userEvent.keyPress(input, { key: 'a', ctrlKey: true }); - expect(getCleanedSelectedContent(input)).to.equal('- YYYY'); + // Test with v6 input + const v6Response = renderWithProps({ + enableAccessibleFieldDOMStructure: false, + format: `- ${adapterToUse.formats.year}`, + }); + const input = getTextbox(); + v6Response.selectSection('year'); + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + expect(getCleanedSelectedContent()).to.equal('- YYYY'); }); }); describe('key: ArrowRight', () => { it('should move selection to the next section when one section is selected', () => { - const { input, selectSection } = renderWithProps({}); - selectSection('day'); - expect(getCleanedSelectedContent(input)).to.equal('DD'); - userEvent.keyPress(input, { key: 'ArrowRight' }); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + v7Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + const input = getTextbox(); + v6Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); }); it('should stay on the current section when the last section is selected', () => { - const { input, selectSection } = renderWithProps({}); - selectSection('year'); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); - userEvent.keyPress(input, { key: 'ArrowRight' }); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + v7Response.selectSection('year'); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + fireEvent.keyDown(v7Response.getActiveSection(2), { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + const input = getTextbox(); + v6Response.selectSection('year'); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); }); it('should select the last section when all the sections are selected', () => { - const { input, selectSection } = renderWithProps({}); - selectSection('month'); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY'); + + fireEvent.keyDown(v7Response.getSectionsContainer(), { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + const input = getTextbox(); + v6Response.selectSection('month'); // Select all sections - userEvent.keyPress(input, { key: 'a', ctrlKey: true }); - expect(getCleanedSelectedContent(input)).to.equal('MM/DD/YYYY'); + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY'); - userEvent.keyPress(input, { key: 'ArrowRight' }); - expect(getCleanedSelectedContent(input)).to.equal('YYYY'); + fireEvent.keyDown(input, { key: 'ArrowRight' }); + expect(getCleanedSelectedContent()).to.equal('YYYY'); }); }); describe('key: ArrowLeft', () => { it('should move selection to the previous section when one section is selected', () => { - const { input, selectSection } = renderWithProps({}); - selectSection('day'); - expect(getCleanedSelectedContent(input)).to.equal('DD'); - userEvent.keyPress(input, { key: 'ArrowLeft' }); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + v7Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + const input = getTextbox(); + v6Response.selectSection('day'); + expect(getCleanedSelectedContent()).to.equal('DD'); + fireEvent.keyDown(input, { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); }); it('should stay on the current section when the first section is selected', () => { - const { input, selectSection } = renderWithProps({}); - selectSection('month'); - expect(getCleanedSelectedContent(input)).to.equal('MM'); - userEvent.keyPress(input, { key: 'ArrowLeft' }); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + v7Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + const input = getTextbox(); + v6Response.selectSection('month'); + expect(getCleanedSelectedContent()).to.equal('MM'); + fireEvent.keyDown(input, { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); }); it('should select the first section when all the sections are selected', () => { - const { input, selectSection } = renderWithProps({}); - selectSection('month'); + // Test with v7 input + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); + v7Response.selectSection('month'); + + // Select all sections + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'a', ctrlKey: true }); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY'); + + fireEvent.keyDown(v7Response.getSectionsContainer(), { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps({ enableAccessibleFieldDOMStructure: false }); + const input = getTextbox(); + v6Response.selectSection('month'); // Select all sections - userEvent.keyPress(input, { key: 'a', ctrlKey: true }); - expect(getCleanedSelectedContent(input)).to.equal('MM/DD/YYYY'); + fireEvent.keyDown(input, { key: 'a', ctrlKey: true }); + expect(getCleanedSelectedContent()).to.equal('MM/DD/YYYY'); - userEvent.keyPress(input, { key: 'ArrowLeft' }); - expect(getCleanedSelectedContent(input)).to.equal('MM'); + fireEvent.keyDown(input, { key: 'ArrowLeft' }); + expect(getCleanedSelectedContent()).to.equal('MM'); }); }); }); diff --git a/packages/x-date-pickers/src/DateField/useDateField.ts b/packages/x-date-pickers/src/DateField/useDateField.ts index 2d5b495c652d2..a6ea87980fdd4 100644 --- a/packages/x-date-pickers/src/DateField/useDateField.ts +++ b/packages/x-date-pickers/src/DateField/useDateField.ts @@ -3,44 +3,38 @@ import { singleItemValueManager, } from '../internals/utils/valueManagers'; import { useField } from '../internals/hooks/useField'; -import { - UseDateFieldProps, - UseDateFieldDefaultizedProps, - UseDateFieldComponentProps, -} from './DateField.types'; +import { UseDateFieldProps } from './DateField.types'; import { validateDate } from '../internals/utils/validation/validateDate'; -import { applyDefaultDate } from '../internals/utils/date-utils'; -import { useUtils, useDefaultDates } from '../internals/hooks/useUtils'; import { splitFieldInternalAndForwardedProps } from '../internals/utils/fields'; -import { PickerValidDate } from '../models'; - -const useDefaultizedDateField = ( - props: UseDateFieldProps, -): AdditionalProps & UseDateFieldDefaultizedProps => { - const utils = useUtils(); - const defaultDates = useDefaultDates(); - - return { - ...props, - disablePast: props.disablePast ?? false, - disableFuture: props.disableFuture ?? false, - format: props.format ?? utils.formats.keyboardDate, - minDate: applyDefaultDate(utils, props.minDate, defaultDates.minDate), - maxDate: applyDefaultDate(utils, props.maxDate, defaultDates.maxDate), - } as any; -}; +import { FieldSection, PickerValidDate } from '../models'; +import { useDefaultizedDateField } from '../internals/hooks/defaultizedFieldProps'; -export const useDateField = ( - inProps: UseDateFieldComponentProps, +export const useDateField = < + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, + TAllProps extends UseDateFieldProps, +>( + inProps: TAllProps, ) => { - const props = useDefaultizedDateField(inProps); + const props = useDefaultizedDateField< + TDate, + UseDateFieldProps, + TAllProps + >(inProps); const { forwardedProps, internalProps } = splitFieldInternalAndForwardedProps< typeof props, - keyof UseDateFieldProps + keyof UseDateFieldProps >(props, 'date'); - return useField({ + return useField< + TDate | null, + TDate, + FieldSection, + TEnableAccessibleFieldDOMStructure, + typeof forwardedProps, + typeof internalProps + >({ forwardedProps, internalProps, valueManager: singleItemValueManager, diff --git a/packages/x-date-pickers/src/DatePicker/DatePicker.tsx b/packages/x-date-pickers/src/DatePicker/DatePicker.tsx index 1cba6df3332fe..e84f5887a9c48 100644 --- a/packages/x-date-pickers/src/DatePicker/DatePicker.tsx +++ b/packages/x-date-pickers/src/DatePicker/DatePicker.tsx @@ -9,8 +9,12 @@ import { DatePickerProps } from './DatePicker.types'; import { DEFAULT_DESKTOP_MODE_MEDIA_QUERY } from '../internals/utils/utils'; import { PickerValidDate } from '../models'; -type DatePickerComponent = (( - props: DatePickerProps & React.RefAttributes, +type DatePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DatePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -23,8 +27,11 @@ type DatePickerComponent = (( * * - [DatePicker API](https://mui.com/x/api/date-pickers/date-picker/) */ -const DatePicker = React.forwardRef(function DatePicker( - inProps: DatePickerProps, +const DatePicker = React.forwardRef(function DatePicker< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DatePickerProps, ref: React.Ref, ) { const props = useThemeProps({ props: inProps, name: 'MuiDatePicker' }); @@ -106,6 +113,10 @@ DatePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -249,11 +260,11 @@ DatePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -270,10 +281,6 @@ DatePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers/src/DatePicker/DatePicker.types.ts b/packages/x-date-pickers/src/DatePicker/DatePicker.types.ts index a287263829adc..1336df730d27b 100644 --- a/packages/x-date-pickers/src/DatePicker/DatePicker.types.ts +++ b/packages/x-date-pickers/src/DatePicker/DatePicker.types.ts @@ -14,13 +14,17 @@ export interface DatePickerSlots extends DesktopDatePickerSlots, MobileDatePickerSlots {} -export interface DatePickerSlotProps - extends DesktopDatePickerSlotProps, - MobileDatePickerSlotProps {} +export interface DatePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends DesktopDatePickerSlotProps, + MobileDatePickerSlotProps {} -export interface DatePickerProps - extends DesktopDatePickerProps, - MobileDatePickerProps { +export interface DatePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends DesktopDatePickerProps, + MobileDatePickerProps { /** * CSS media query when `Mobile` mode will be changed to `Desktop`. * @default '@media (pointer: fine)' @@ -41,5 +45,5 @@ export interface DatePickerProps * The props used for each component slot. * @default {} */ - slotProps?: DatePickerSlotProps; + slotProps?: DatePickerSlotProps; } diff --git a/packages/x-date-pickers/src/DatePicker/tests/DatePicker.test.tsx b/packages/x-date-pickers/src/DatePicker/tests/DatePicker.test.tsx index 01cc75d867352..38bf67e127953 100644 --- a/packages/x-date-pickers/src/DatePicker/tests/DatePicker.test.tsx +++ b/packages/x-date-pickers/src/DatePicker/tests/DatePicker.test.tsx @@ -3,6 +3,7 @@ import { expect } from 'chai'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { screen } from '@mui-internal/test-utils/createRenderer'; import { createPickerRenderer, stubMatchMedia } from 'test/utils/pickers'; +import { pickersInputBaseClasses } from '@mui/x-date-pickers/PickersTextField'; describe('', () => { const { render } = createPickerRenderer(); @@ -11,9 +12,9 @@ describe('', () => { const originalMatchMedia = window.matchMedia; window.matchMedia = stubMatchMedia(false); - render(); + render(); - expect(screen.getByLabelText(/Choose date/)).to.have.tagName('input'); + expect(screen.getByLabelText(/Choose date/)).to.have.class(pickersInputBaseClasses.input); window.matchMedia = originalMatchMedia; }); diff --git a/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx b/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx index 118da3db5daf4..ef7fe6f7229c3 100644 --- a/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx +++ b/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx @@ -4,14 +4,19 @@ import MuiTextField from '@mui/material/TextField'; import { useThemeProps } from '@mui/material/styles'; import { useSlotProps } from '@mui/base/utils'; import { refType } from '@mui/utils'; -import { DateTimeFieldProps, DateTimeFieldSlotProps } from './DateTimeField.types'; +import { DateTimeFieldProps } from './DateTimeField.types'; import { useDateTimeField } from './useDateTimeField'; import { useClearableField } from '../hooks'; +import { PickersTextField } from '../PickersTextField'; import { convertFieldResponseIntoMuiTextFieldProps } from '../internals/utils/convertFieldResponseIntoMuiTextFieldProps'; import { PickerValidDate } from '../models'; -type DateTimeFieldComponent = (( - props: DateTimeFieldProps & React.RefAttributes, +type DateTimeFieldComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DateTimeFieldProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -24,8 +29,11 @@ type DateTimeFieldComponent = (( * * - [DateTimeField API](https://mui.com/x/api/date-pickers/date-time-field/) */ -const DateTimeField = React.forwardRef(function DateTimeField( - inProps: DateTimeFieldProps, +const DateTimeField = React.forwardRef(function DateTimeField< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DateTimeFieldProps, inRef: React.Ref, ) { const themeProps = useThemeProps({ @@ -37,13 +45,10 @@ const DateTimeField = React.forwardRef(function DateTimeField = useSlotProps< - typeof TextField, - DateTimeFieldSlotProps['textField'], - DateTimeFieldProps, - DateTimeFieldProps - >({ + const TextField = + slots?.textField ?? + (inProps.enableAccessibleFieldDOMStructure ? PickersTextField : MuiTextField); + const textFieldProps = useSlotProps({ elementType: TextField, externalSlotProps: slotProps?.textField, externalForwardedProps: other, @@ -51,13 +56,17 @@ const DateTimeField = React.forwardRef(function DateTimeField; // TODO: Remove when mui/material-ui#35088 will be merged textFieldProps.inputProps = { ...inputProps, ...textFieldProps.inputProps }; textFieldProps.InputProps = { ...InputProps, ...textFieldProps.InputProps }; - const fieldResponse = useDateTimeField(textFieldProps); + const fieldResponse = useDateTimeField< + TDate, + TEnableAccessibleFieldDOMStructure, + typeof textFieldProps + >(textFieldProps); const convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); const processedFieldProps = useClearableField({ @@ -122,6 +131,10 @@ DateTimeField.propTypes = { * @default false */ disablePast: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.bool, /** * If `true`, the component is displayed in focused state. */ @@ -271,11 +284,11 @@ DateTimeField.propTypes = { required: PropTypes.bool, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -292,10 +305,6 @@ DateTimeField.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts index 984e6cf62b910..771dc16d121bc 100644 --- a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts +++ b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts @@ -1,9 +1,14 @@ import * as React from 'react'; import { SlotComponentProps } from '@mui/base/utils'; import TextField from '@mui/material/TextField'; -import { DateTimeValidationError, FieldSection, PickerValidDate } from '../models'; +import { + DateTimeValidationError, + FieldSection, + PickerValidDate, + BuiltInFieldTextFieldProps, +} from '../models'; import { UseFieldInternalProps } from '../internals/hooks/useField'; -import { DefaultizedProps, MakeOptional } from '../internals/models/helpers'; +import { MakeOptional } from '../internals/models/helpers'; import { BaseDateValidationProps, BaseTimeValidationProps, @@ -13,12 +18,23 @@ import { TimeValidationProps, YearValidationProps, } from '../internals/models/validation'; -import { FieldsTextFieldProps } from '../internals/models/fields'; -import { UseClearableFieldSlots, UseClearableFieldSlotProps } from '../hooks/useClearableField'; +import { + ExportedUseClearableFieldProps, + UseClearableFieldSlots, + UseClearableFieldSlotProps, +} from '../hooks/useClearableField'; -export interface UseDateTimeFieldProps - extends MakeOptional< - UseFieldInternalProps, +export interface UseDateTimeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends MakeOptional< + UseFieldInternalProps< + TDate | null, + TDate, + FieldSection, + TEnableAccessibleFieldDOMStructure, + DateTimeValidationError + >, 'format' >, DayValidationProps, @@ -27,7 +43,8 @@ export interface UseDateTimeFieldProps BaseDateValidationProps, TimeValidationProps, BaseTimeValidationProps, - DateTimeValidationProps { + DateTimeValidationProps, + ExportedUseClearableFieldProps { /** * 12h/24h view for hour selection clock. * @default `utils.is12HourCycleInCurrentLocale()` @@ -35,18 +52,21 @@ export interface UseDateTimeFieldProps ampm?: boolean; } -export type UseDateTimeFieldDefaultizedProps = DefaultizedProps< - UseDateTimeFieldProps, - keyof BaseDateValidationProps | keyof BaseTimeValidationProps | 'format' ->; - export type UseDateTimeFieldComponentProps< TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, TChildProps extends {}, -> = Omit> & UseDateTimeFieldProps; +> = Omit> & + UseDateTimeFieldProps; -export interface DateTimeFieldProps - extends UseDateTimeFieldComponentProps { +export type DateTimeFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> = UseDateTimeFieldComponentProps< + TDate, + TEnableAccessibleFieldDOMStructure, + BuiltInFieldTextFieldProps +> & { /** * Overridable component slots. * @default {} @@ -56,21 +76,29 @@ export interface DateTimeFieldProps * The props used for each component slot. * @default {} */ - slotProps?: DateTimeFieldSlotProps; -} + slotProps?: DateTimeFieldSlotProps; +}; -export type DateTimeFieldOwnerState = DateTimeFieldProps; +export type DateTimeFieldOwnerState< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> = DateTimeFieldProps; export interface DateTimeFieldSlots extends UseClearableFieldSlots { /** * Form control with an input to render the value. - * Receives the same props as `@mui/material/TextField`. - * @default TextField from '@mui/material' + * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ textField?: React.ElementType; } -export interface DateTimeFieldSlotProps - extends UseClearableFieldSlotProps { - textField?: SlotComponentProps>; +export interface DateTimeFieldSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends UseClearableFieldSlotProps { + textField?: SlotComponentProps< + typeof TextField, + {}, + DateTimeFieldOwnerState + >; } diff --git a/packages/x-date-pickers/src/DateTimeField/index.ts b/packages/x-date-pickers/src/DateTimeField/index.ts index 7dd431c42228e..95952dde94748 100644 --- a/packages/x-date-pickers/src/DateTimeField/index.ts +++ b/packages/x-date-pickers/src/DateTimeField/index.ts @@ -4,5 +4,4 @@ export type { UseDateTimeFieldProps, UseDateTimeFieldComponentProps, DateTimeFieldProps, - UseDateTimeFieldDefaultizedProps, } from './DateTimeField.types'; diff --git a/packages/x-date-pickers/src/DateTimeField/tests/describes.DateTimeField.test.tsx b/packages/x-date-pickers/src/DateTimeField/tests/describes.DateTimeField.test.tsx index 28a69aac0016e..84cf64b862774 100644 --- a/packages/x-date-pickers/src/DateTimeField/tests/describes.DateTimeField.test.tsx +++ b/packages/x-date-pickers/src/DateTimeField/tests/describes.DateTimeField.test.tsx @@ -1,16 +1,14 @@ import * as React from 'react'; -import TextField from '@mui/material/TextField'; -import { userEvent } from '@mui-internal/test-utils'; +import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; import { DateTimeField } from '@mui/x-date-pickers/DateTimeField'; import { adapterToUse, createPickerRenderer, wrapPickerMount, - expectInputValue, - expectInputPlaceholder, - getTextbox, + expectFieldValueV7, describeValidation, describeValue, + getFieldInputRoot, } from 'test/utils/pickers'; import { describeConformance } from 'test/utils/describeConformance'; @@ -24,9 +22,9 @@ describe(' - Describes', () => { componentFamily: 'field', })); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, - inheritComponent: TextField, + inheritComponent: PickersTextField, render, muiName: 'MuiDateTimeField', wrapMount: wrapPickerMount, @@ -50,24 +48,25 @@ describe(' - Describes', () => { clock, assertRenderedValue: (expectedValue: any) => { const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); - const input = getTextbox(); - if (!expectedValue) { - expectInputPlaceholder(input, hasMeridiem ? 'MM/DD/YYYY hh:mm aa' : 'MM/DD/YYYY hh:mm'); + const fieldRoot = getFieldInputRoot(); + + let expectedValueStr: string; + if (expectedValue) { + expectedValueStr = adapterToUse.format( + expectedValue, + hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', + ); + } else { + expectedValueStr = hasMeridiem ? 'MM/DD/YYYY hh:mm aa' : 'MM/DD/YYYY hh:mm'; } - const expectedValueStr = expectedValue - ? adapterToUse.format( - expectedValue, - hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', - ) - : ''; - expectInputValue(input, expectedValueStr); + expectFieldValueV7(fieldRoot, expectedValueStr); }, - setNewValue: (value, { selectSection }) => { + setNewValue: (value, { selectSection, pressKey }) => { const newValue = adapterToUse.addDays(value, 1); selectSection('day'); - const input = getTextbox(); - userEvent.keyPress(input, { key: 'ArrowUp' }); + pressKey(undefined, 'ArrowUp'); + return newValue; }, })); diff --git a/packages/x-date-pickers/src/DateTimeField/tests/editing.DateTimeField.test.tsx b/packages/x-date-pickers/src/DateTimeField/tests/editing.DateTimeField.test.tsx index e0f4f9acc41b1..01307069f339a 100644 --- a/packages/x-date-pickers/src/DateTimeField/tests/editing.DateTimeField.test.tsx +++ b/packages/x-date-pickers/src/DateTimeField/tests/editing.DateTimeField.test.tsx @@ -1,9 +1,13 @@ -import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; -import { userEvent, screen } from '@mui-internal/test-utils'; +import { fireEvent } from '@mui-internal/test-utils'; import { DateTimeField } from '@mui/x-date-pickers/DateTimeField'; -import { adapterToUse, buildFieldInteractions, createPickerRenderer } from 'test/utils/pickers'; +import { + adapterToUse, + buildFieldInteractions, + createPickerRenderer, + expectFieldValueV7, +} from 'test/utils/pickers'; describe(' - Editing', () => { const { render, clock } = createPickerRenderer({ @@ -22,14 +26,15 @@ describe(' - Editing', () => { const onChange = spy(); const referenceDate = adapterToUse.date('2012-05-03T14:30:00'); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, referenceDate, format: adapterToUse.formats.month, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'ArrowUp' }); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); // All sections not present should equal the one from the referenceDate, and the month should equal January (because it's an ArrowUp on an empty month). expect(onChange.lastCall.firstArg).toEqualDateTime(adapterToUse.setMonth(referenceDate, 0)); @@ -40,15 +45,16 @@ describe(' - Editing', () => { const value = adapterToUse.date('2018-11-03T22:15:00'); const referenceDate = adapterToUse.date('2012-05-03T14:30:00'); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, referenceDate, value, format: adapterToUse.formats.month, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'ArrowUp' }); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); // Should equal the initial `value` prop with one less month. expect(onChange.lastCall.firstArg).toEqualDateTime(adapterToUse.setMonth(value, 11)); @@ -59,15 +65,16 @@ describe(' - Editing', () => { const defaultValue = adapterToUse.date('2018-11-03T22:15:00'); const referenceDate = adapterToUse.date('2012-05-03T14:30:00'); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, referenceDate, defaultValue, format: adapterToUse.formats.month, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'ArrowUp' }); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); // Should equal the initial `defaultValue` prop with one less month. expect(onChange.lastCall.firstArg).toEqualDateTime(adapterToUse.setMonth(defaultValue, 11)); @@ -77,13 +84,14 @@ describe(' - Editing', () => { it('should only keep year when granularity = month', () => { const onChange = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, format: adapterToUse.formats.month, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'ArrowUp' }); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); expect(onChange.lastCall.firstArg).toEqualDateTime('2012-01-01'); }); @@ -91,13 +99,14 @@ describe(' - Editing', () => { it('should only keep year and month when granularity = day', () => { const onChange = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, format: adapterToUse.formats.dayOfMonth, }); - selectSection('day'); - userEvent.keyPress(input, { key: 'ArrowUp' }); + v7Response.selectSection('day'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); expect(onChange.lastCall.firstArg).toEqualDateTime('2012-05-01'); }); @@ -105,19 +114,20 @@ describe(' - Editing', () => { it('should only keep up to the hours when granularity = minutes', () => { const onChange = spy(); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, format: adapterToUse.formats.fullTime24h, }); - selectSection('hours'); + v7Response.selectSection('hours'); // Set hours - userEvent.keyPress(input, { key: 'ArrowUp' }); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); // Set minutes - userEvent.keyPress(input, { key: 'ArrowRight' }); - userEvent.keyPress(input, { key: 'ArrowUp' }); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'ArrowUp' }); expect(onChange.lastCall.firstArg).toEqualDateTime('2012-05-03T00:00:00.000Z'); }); @@ -128,14 +138,15 @@ describe(' - Editing', () => { const onChange = spy(); const minDate = adapterToUse.date('2030-05-05T18:30:00'); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, minDate, format: adapterToUse.formats.month, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'ArrowUp' }); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); // Respect the granularity and the minDate expect(onChange.lastCall.firstArg).toEqualDateTime('2030-01-01T00:00'); @@ -145,14 +156,15 @@ describe(' - Editing', () => { const onChange = spy(); const minDate = adapterToUse.date('2007-05-05T18:30:00'); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, minDate, format: adapterToUse.formats.month, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'ArrowUp' }); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); // Respect the granularity but not the minDate expect(onChange.lastCall.firstArg).toEqualDateTime('2012-01-01T00:00'); @@ -162,14 +174,15 @@ describe(' - Editing', () => { const onChange = spy(); const maxDate = adapterToUse.date('2007-05-05T18:30:00'); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, maxDate, format: adapterToUse.formats.month, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'ArrowUp' }); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); // Respect the granularity and the minDate expect(onChange.lastCall.firstArg).toEqualDateTime('2007-01-01T00:00'); @@ -179,14 +192,15 @@ describe(' - Editing', () => { const onChange = spy(); const maxDate = adapterToUse.date('2030-05-05T18:30:00'); - const { input, selectSection } = renderWithProps({ + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, onChange, maxDate, format: adapterToUse.formats.month, }); - selectSection('month'); - userEvent.keyPress(input, { key: 'ArrowUp' }); + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowUp' }); // Respect the granularity but not the maxDate expect(onChange.lastCall.firstArg).toEqualDateTime('2012-01-01T00:00'); @@ -195,13 +209,17 @@ describe(' - Editing', () => { }); it('should correctly update `value` when both `format` and `value` are changed', () => { - const { setProps } = render(); - expect(screen.getByRole('textbox').value).to.equal(''); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: null, + format: 'P', + }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY'); - setProps({ + v7Response.setProps({ format: 'Pp', value: adapterToUse.date('2012-05-03T14:30:00'), }); - expect(screen.getByRole('textbox').value).to.equal('05/03/2012, 02:30 PM'); + expectFieldValueV7(v7Response.getSectionsContainer(), '05/03/2012, 02:30 PM'); }); }); diff --git a/packages/x-date-pickers/src/DateTimeField/tests/timezone.DateTimeField.test.tsx b/packages/x-date-pickers/src/DateTimeField/tests/timezone.DateTimeField.test.tsx index 1779055d06c05..afad44514e142 100644 --- a/packages/x-date-pickers/src/DateTimeField/tests/timezone.DateTimeField.test.tsx +++ b/packages/x-date-pickers/src/DateTimeField/tests/timezone.DateTimeField.test.tsx @@ -1,43 +1,42 @@ -import * as React from 'react'; import { spy } from 'sinon'; import { expect } from 'chai'; -import { userEvent } from '@mui-internal/test-utils'; +import { fireEvent } from '@mui-internal/test-utils'; import { DateTimeField } from '@mui/x-date-pickers/DateTimeField'; import { createPickerRenderer, - expectInputValue, - getTextbox, + expectFieldValueV7, describeAdapters, + buildFieldInteractions, } from 'test/utils/pickers'; const TIMEZONE_TO_TEST = ['UTC', 'system', 'America/New_York']; describe(' - Timezone', () => { - describeAdapters('Timezone prop', DateTimeField, ({ adapter, render, clickOnInput }) => { + describeAdapters('Timezone prop', DateTimeField, ({ adapter, renderWithProps }) => { if (!adapter.isTimezoneCompatible) { return; } const format = `${adapter.formats.keyboardDate} ${adapter.formats.hours24h}`; - const fillEmptyValue = (input: HTMLInputElement, timezone: string) => { - clickOnInput(input, 0); + const fillEmptyValue = (v7Response: ReturnType, timezone: string) => { + v7Response.selectSection('month'); // Set month - userEvent.keyPress(input, { key: 'ArrowDown' }); - userEvent.keyPress(input, { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowDown' }); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowRight' }); // Set day - userEvent.keyPress(input, { key: 'ArrowDown' }); - userEvent.keyPress(input, { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'ArrowDown' }); + fireEvent.keyDown(v7Response.getActiveSection(1), { key: 'ArrowRight' }); // Set year - userEvent.keyPress(input, { key: 'ArrowDown' }); - userEvent.keyPress(input, { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(2), { key: 'ArrowDown' }); + fireEvent.keyDown(v7Response.getActiveSection(2), { key: 'ArrowRight' }); // Set hours - userEvent.keyPress(input, { key: 'ArrowDown' }); - userEvent.keyPress(input, { key: 'ArrowRight' }); + fireEvent.keyDown(v7Response.getActiveSection(3), { key: 'ArrowDown' }); + fireEvent.keyDown(v7Response.getActiveSection(3), { key: 'ArrowRight' }); return adapter.setHours( adapter.setDate(adapter.setMonth(adapter.date(undefined, timezone), 11), 31), @@ -46,18 +45,17 @@ describe(' - Timezone', () => { }; it('should use default timezone for rendering and onChange when no value and no timezone prop are provided', () => { - if (adapter.lib !== 'dayjs') { - return; - } - const onChange = spy(); - render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + onChange, + format, + }); - const input = getTextbox(); - const expectedDate = fillEmptyValue(input, 'default'); + const expectedDate = fillEmptyValue(v7Response, 'default'); // Check the rendered value (uses default timezone, e.g: UTC, see TZ env variable) - expectInputValue(input, '12/31/2022 23'); + expectFieldValueV7(v7Response.getSectionsContainer(), '12/31/2022 23'); // Check the `onChange` value (uses default timezone, e.g: UTC, see TZ env variable) const actualDate = onChange.lastCall.firstArg; @@ -72,12 +70,16 @@ describe(' - Timezone', () => { describe(`Timezone: ${timezone}`, () => { it('should use timezone prop for onChange and rendering when no value is provided', () => { const onChange = spy(); - render(); - const input = getTextbox(); - const expectedDate = fillEmptyValue(input, timezone); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + onChange, + format, + timezone, + }); + const expectedDate = fillEmptyValue(v7Response, timezone); // Check the rendered value (uses timezone prop) - expectInputValue(input, '12/31/2022 23'); + expectFieldValueV7(v7Response.getSectionsContainer(), '12/31/2022 23'); // Check the `onChange` value (uses timezone prop) const actualDate = onChange.lastCall.firstArg; @@ -87,20 +89,19 @@ describe(' - Timezone', () => { it('should use timezone prop for rendering and value timezone for onChange when a value is provided', () => { const onChange = spy(); - render( - , - ); - const input = getTextbox(); - clickOnInput(input, 0); - userEvent.keyPress(input, { key: 'ArrowDown' }); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + value: adapter.date(undefined, timezone), + onChange, + format, + timezone: 'America/Chicago', + }); + + v7Response.selectSection('month'); + fireEvent.keyDown(v7Response.getActiveSection(0), { key: 'ArrowDown' }); // Check the rendered value (uses America/Chicago timezone) - expectInputValue(input, '05/14/2022 19'); + expectFieldValueV7(v7Response.getSectionsContainer(), '05/14/2022 19'); // Check the `onChange` value (uses timezone prop) const expectedDate = adapter.addMonths(adapter.date(undefined, timezone), -1); @@ -113,20 +114,27 @@ describe(' - Timezone', () => { }); describe('Value timezone modification - Luxon', () => { - const { render, adapter } = createPickerRenderer({ clock: 'fake', adapterName: 'luxon' }); + const { render, adapter, clock } = createPickerRenderer({ + clock: 'fake', + adapterName: 'luxon', + }); + const { renderWithProps } = buildFieldInteractions({ + clock, + render, + Component: DateTimeField, + }); it('should update the field when time zone changes (timestamp remains the same)', () => { - const { setProps } = render(); - const input = getTextbox(); + const v7Response = renderWithProps({ enableAccessibleFieldDOMStructure: true }); const date = adapter.date('2020-06-18T14:30:10.000Z').setZone('UTC'); - setProps({ value: date }); + v7Response.setProps({ value: date }); - expectInputValue(input, '06/18/2020 02:30 PM'); + expectFieldValueV7(v7Response.getSectionsContainer(), '06/18/2020 02:30 PM'); - setProps({ value: date.setZone('America/Los_Angeles') }); + v7Response.setProps({ value: date.setZone('America/Los_Angeles') }); - expectInputValue(input, '06/18/2020 07:30 AM'); + expectFieldValueV7(v7Response.getSectionsContainer(), '06/18/2020 07:30 AM'); }); }); }); diff --git a/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts b/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts index 021ea1cc3baa8..b92124fe93731 100644 --- a/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts +++ b/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts @@ -3,52 +3,38 @@ import { singleItemValueManager, } from '../internals/utils/valueManagers'; import { useField } from '../internals/hooks/useField'; -import { - UseDateTimeFieldProps, - UseDateTimeFieldDefaultizedProps, - UseDateTimeFieldComponentProps, -} from './DateTimeField.types'; +import { UseDateTimeFieldProps } from './DateTimeField.types'; import { validateDateTime } from '../internals/utils/validation/validateDateTime'; -import { applyDefaultDate } from '../internals/utils/date-utils'; -import { useUtils, useDefaultDates } from '../internals/hooks/useUtils'; import { splitFieldInternalAndForwardedProps } from '../internals/utils/fields'; -import { PickerValidDate } from '../models'; - -const useDefaultizedDateTimeField = ( - props: UseDateTimeFieldProps, -): AdditionalProps & UseDateTimeFieldDefaultizedProps => { - const utils = useUtils(); - const defaultDates = useDefaultDates(); - - const ampm = props.ampm ?? utils.is12HourCycleInCurrentLocale(); - const defaultFormat = ampm - ? utils.formats.keyboardDateTime12h - : utils.formats.keyboardDateTime24h; - - return { - ...props, - disablePast: props.disablePast ?? false, - disableFuture: props.disableFuture ?? false, - format: props.format ?? defaultFormat, - disableIgnoringDatePartForTimeValidation: Boolean(props.minDateTime || props.maxDateTime), - minDate: applyDefaultDate(utils, props.minDateTime ?? props.minDate, defaultDates.minDate), - maxDate: applyDefaultDate(utils, props.maxDateTime ?? props.maxDate, defaultDates.maxDate), - minTime: props.minDateTime ?? props.minTime, - maxTime: props.maxDateTime ?? props.maxTime, - } as any; -}; +import { FieldSection, PickerValidDate } from '../models'; +import { useDefaultizedDateTimeField } from '../internals/hooks/defaultizedFieldProps'; -export const useDateTimeField = ( - inProps: UseDateTimeFieldComponentProps, +export const useDateTimeField = < + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, + TAllProps extends UseDateTimeFieldProps, +>( + inProps: TAllProps, ) => { - const props = useDefaultizedDateTimeField(inProps); + const props = useDefaultizedDateTimeField< + TDate, + UseDateTimeFieldProps, + TAllProps + >(inProps); const { forwardedProps, internalProps } = splitFieldInternalAndForwardedProps< typeof props, - keyof UseDateTimeFieldProps + keyof UseDateTimeFieldProps >(props, 'date-time'); - return useField({ + return useField< + TDate | null, + TDate, + FieldSection, + TEnableAccessibleFieldDOMStructure, + typeof forwardedProps, + typeof internalProps + >({ forwardedProps, internalProps, valueManager: singleItemValueManager, diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx index 9b6b5853899fd..0056962d249ad 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx @@ -9,8 +9,12 @@ import { DateTimePickerProps } from './DateTimePicker.types'; import { DEFAULT_DESKTOP_MODE_MEDIA_QUERY } from '../internals/utils/utils'; import { PickerValidDate } from '../models'; -type DateTimePickerComponent = (( - props: DateTimePickerProps & React.RefAttributes, +type DateTimePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DateTimePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -23,8 +27,11 @@ type DateTimePickerComponent = (( * * - [DateTimePicker API](https://mui.com/x/api/date-pickers/date-time-picker/) */ -const DateTimePicker = React.forwardRef(function DateTimePicker( - inProps: DateTimePickerProps, +const DateTimePicker = React.forwardRef(function DateTimePicker< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DateTimePickerProps, ref: React.Ref, ) { const props = useThemeProps({ props: inProps, name: 'MuiDateTimePicker' }); @@ -121,6 +128,10 @@ DateTimePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -287,11 +298,11 @@ DateTimePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -308,10 +319,6 @@ DateTimePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts index 261c0907f258e..1c7b8b751903a 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts @@ -15,13 +15,28 @@ export interface DateTimePickerSlots extends DesktopDateTimePickerSlots, MobileDateTimePickerSlots {} -export interface DateTimePickerSlotProps - extends DesktopDateTimePickerSlotProps, - MobileDateTimePickerSlotProps {} +export interface DateTimePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends DesktopDateTimePickerSlotProps, + MobileDateTimePickerSlotProps< + TDate, + DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure + > {} -export interface DateTimePickerProps - extends DesktopDateTimePickerProps, - Omit, 'views'> { +export interface DateTimePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends DesktopDateTimePickerProps, + Omit< + MobileDateTimePickerProps< + TDate, + DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure + >, + 'views' + > { /** * CSS media query when `Mobile` mode will be changed to `Desktop`. * @default '@media (pointer: fine)' @@ -42,5 +57,5 @@ export interface DateTimePickerProps * The props used for each component slot. * @default {} */ - slotProps?: DateTimePickerSlotProps; + slotProps?: DateTimePickerSlotProps; } diff --git a/packages/x-date-pickers/src/DateTimePicker/tests/DateTimePicker.test.tsx b/packages/x-date-pickers/src/DateTimePicker/tests/DateTimePicker.test.tsx index cac1fdb4074df..6a6b494c9fdda 100644 --- a/packages/x-date-pickers/src/DateTimePicker/tests/DateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/tests/DateTimePicker.test.tsx @@ -3,6 +3,7 @@ import { expect } from 'chai'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { screen } from '@mui-internal/test-utils/createRenderer'; import { createPickerRenderer, stubMatchMedia } from 'test/utils/pickers'; +import { pickersInputBaseClasses } from '@mui/x-date-pickers/PickersTextField'; describe('', () => { const { render } = createPickerRenderer(); @@ -11,9 +12,9 @@ describe('', () => { const originalMatchMedia = window.matchMedia; window.matchMedia = stubMatchMedia(false); - render(); + render(); - expect(screen.getByLabelText(/Choose date/)).to.have.tagName('input'); + expect(screen.getByLabelText(/Choose date/)).to.have.class(pickersInputBaseClasses.input); window.matchMedia = originalMatchMedia; }); diff --git a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx index 6016ff812aef4..922a22b3f23ca 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx @@ -16,8 +16,12 @@ import { renderDateViewCalendar } from '../dateViewRenderers'; import { PickerViewRendererLookup } from '../internals/hooks/usePicker/usePickerViews'; import { resolveDateFormat } from '../internals/utils/date-utils'; -type DesktopDatePickerComponent = (( - props: DesktopDatePickerProps & React.RefAttributes, +type DesktopDatePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DesktopDatePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -32,15 +36,19 @@ type DesktopDatePickerComponent = (( */ const DesktopDatePicker = React.forwardRef(function DesktopDatePicker< TDate extends PickerValidDate, ->(inProps: DesktopDatePickerProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DesktopDatePickerProps, + ref: React.Ref, +) { const localeText = useLocaleText(); const utils = useUtils(); // Props with the default values common to all date pickers - const defaultizedProps = useDatePickerDefaultizedProps>( - inProps, - 'MuiDesktopDatePicker', - ); + const defaultizedProps = useDatePickerDefaultizedProps< + TDate, + DesktopDatePickerProps + >(inProps, 'MuiDesktopDatePicker'); const viewRenderers: PickerViewRendererLookup = { day: renderDateViewCalendar, @@ -74,7 +82,12 @@ const DesktopDatePicker = React.forwardRef(function DesktopDatePicker< }, }; - const { renderPicker } = useDesktopPicker({ + const { renderPicker } = useDesktopPicker< + TDate, + DateView, + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: singleItemValueManager, valueType: 'date', @@ -145,6 +158,10 @@ DesktopDatePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -288,11 +305,11 @@ DesktopDatePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -309,10 +326,6 @@ DesktopDatePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.types.ts b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.types.ts index ef33cb389df47..613158d9145b3 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.types.ts +++ b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.types.ts @@ -15,13 +15,17 @@ export interface DesktopDatePickerSlots extends BaseDatePickerSlots, MakeOptional, 'field' | 'openPickerIcon'> {} -export interface DesktopDatePickerSlotProps - extends BaseDatePickerSlotProps, - ExportedUseDesktopPickerSlotProps {} +export interface DesktopDatePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends BaseDatePickerSlotProps, + ExportedUseDesktopPickerSlotProps {} -export interface DesktopDatePickerProps - extends BaseDatePickerProps, - DesktopOnlyPickerProps { +export interface DesktopDatePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends BaseDatePickerProps, + DesktopOnlyPickerProps { /** * Years rendered per row. * @default 4 @@ -36,5 +40,5 @@ export interface DesktopDatePickerProps * The props used for each component slot. * @default {} */ - slotProps?: DesktopDatePickerSlotProps; + slotProps?: DesktopDatePickerSlotProps; } diff --git a/packages/x-date-pickers/src/DesktopDatePicker/tests/DesktopDatePicker.test.tsx b/packages/x-date-pickers/src/DesktopDatePicker/tests/DesktopDatePicker.test.tsx index 7c64c8832c42a..d0dce8a42a2fb 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/tests/DesktopDatePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/tests/DesktopDatePicker.test.tsx @@ -5,35 +5,13 @@ import { TransitionProps } from '@mui/material/transitions'; import { inputBaseClasses } from '@mui/material/InputBase'; import { fireEvent, screen, userEvent } from '@mui-internal/test-utils'; import { DesktopDatePicker } from '@mui/x-date-pickers/DesktopDatePicker'; -import { - createPickerRenderer, - adapterToUse, - openPicker, - expectInputValue, - getTextbox, -} from 'test/utils/pickers'; +import { createPickerRenderer, adapterToUse, openPicker } from 'test/utils/pickers'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); describe('', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); - it('allows to change selected date from the field according to `format`', () => { - const handleChange = spy(); - - render(); - const input = getTextbox(); - - fireEvent.change(input, { - target: { - value: '10/11/2018', - }, - }); - - expectInputValue(input, '10/11/2018'); - expect(handleChange.callCount).to.equal(1); - }); - describe('Views', () => { it('should switch between views uncontrolled', () => { const handleViewChange = spy(); diff --git a/packages/x-date-pickers/src/DesktopDatePicker/tests/describes.DesktopDatePicker.test.tsx b/packages/x-date-pickers/src/DesktopDatePicker/tests/describes.DesktopDatePicker.test.tsx index 19cd7201426af..c5cf80884fa40 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/tests/describes.DesktopDatePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/tests/describes.DesktopDatePicker.test.tsx @@ -2,12 +2,11 @@ import { screen, userEvent } from '@mui-internal/test-utils'; import { createPickerRenderer, adapterToUse, - expectInputValue, - expectInputPlaceholder, - getTextbox, + expectFieldValueV7, describeValidation, describeValue, describePicker, + getFieldInputRoot, } from 'test/utils/pickers'; import { DesktopDatePicker } from '@mui/x-date-pickers/DesktopDatePicker'; @@ -33,16 +32,15 @@ describe(' - Describes', () => { emptyValue: null, clock, assertRenderedValue: (expectedValue: any) => { - const input = getTextbox(); - if (!expectedValue) { - expectInputPlaceholder(input, 'MM/DD/YYYY'); - } - expectInputValue( - input, - expectedValue ? adapterToUse.format(expectedValue, 'keyboardDate') : '', - ); + const fieldRoot = getFieldInputRoot(); + + const expectedValueStr = expectedValue + ? adapterToUse.format(expectedValue, 'keyboardDate') + : 'MM/DD/YYYY'; + + expectFieldValueV7(fieldRoot, expectedValueStr); }, - setNewValue: (value, { isOpened, applySameValue, selectSection }) => { + setNewValue: (value, { isOpened, applySameValue, selectSection, pressKey }) => { const newValue = applySameValue ? value : adapterToUse.addDays(value, 1); if (isOpened) { @@ -51,8 +49,7 @@ describe(' - Describes', () => { ); } else { selectSection('day'); - const input = getTextbox(); - userEvent.keyPress(input, { key: 'ArrowUp' }); + pressKey(undefined, 'ArrowUp'); } return newValue; diff --git a/packages/x-date-pickers/src/DesktopDatePicker/tests/field.DesktopDatePicker.test.tsx b/packages/x-date-pickers/src/DesktopDatePicker/tests/field.DesktopDatePicker.test.tsx index 81dd41756569f..44097f0832b19 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/tests/field.DesktopDatePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/tests/field.DesktopDatePicker.test.tsx @@ -1,12 +1,12 @@ -import * as React from 'react'; import { fireEvent } from '@mui-internal/test-utils'; import { DesktopDatePicker, DesktopDatePickerProps } from '@mui/x-date-pickers/DesktopDatePicker'; import { createPickerRenderer, buildFieldInteractions, getTextbox, - expectInputValue, - expectInputPlaceholder, + expectFieldValueV7, + expectFieldValueV6, + expectFieldPlaceholderV6, adapterToUse, describeAdapters, } from 'test/utils/pickers'; @@ -17,39 +17,77 @@ describe(' - Field', () => { clock: 'fake', clockConfig: new Date('2018-01-01T10:05:05.000'), }); - const { clickOnInput } = buildFieldInteractions({ + const { renderWithProps } = buildFieldInteractions({ clock, render, Component: DesktopDatePicker, }); it('should be able to reset a single section', () => { - render( - , + // Test with v7 input + const v7Response = renderWithProps( + { + enableAccessibleFieldDOMStructure: true as const, + format: `${adapterToUse.formats.month} ${adapterToUse.formats.dayOfMonth}`, + }, + { componentFamily: 'picker' }, + ); + + v7Response.selectSection('month'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM DD'); + + v7Response.pressKey(0, 'N'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'November DD'); + + v7Response.pressKey(1, '4'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'November 04'); + + v7Response.pressKey(1, ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'November DD'); + + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps( + { + enableAccessibleFieldDOMStructure: false as const, + format: `${adapterToUse.formats.month} ${adapterToUse.formats.dayOfMonth}`, + }, + { componentFamily: 'picker' }, ); const input = getTextbox(); - expectInputPlaceholder(input, 'MMMM DD'); - clickOnInput(input, 1); + v6Response.selectSection('month'); + expectFieldPlaceholderV6(input, 'MMMM DD'); - fireEvent.change(input, { target: { value: 'N DD' } }); // Press "1" - expectInputValue(input, 'November DD'); + fireEvent.change(input, { target: { value: 'N DD' } }); // Press "N" + expectFieldValueV6(input, 'November DD'); - fireEvent.change(input, { target: { value: 'November 4' } }); // Press "1" - expectInputValue(input, 'November 04'); + fireEvent.change(input, { target: { value: 'November 4' } }); // Press "4" + expectFieldValueV6(input, 'November 04'); fireEvent.change(input, { target: { value: 'November ' } }); - expectInputValue(input, 'November DD'); + expectFieldValueV6(input, 'November DD'); }); it('should adapt the default field format based on the props of the picker', () => { - const testFormat = (props: DesktopDatePickerProps, expectedFormat: string) => { - const { unmount } = render(); + const testFormat = (props: DesktopDatePickerProps, expectedFormat: string) => { + // Test with v7 input + const v7Response = renderWithProps( + { ...props, enableAccessibleFieldDOMStructure: true as const }, + { componentFamily: 'picker' }, + ); + expectFieldValueV7(v7Response.getSectionsContainer(), expectedFormat); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps( + { ...props, enableAccessibleFieldDOMStructure: false as const }, + { componentFamily: 'picker' }, + ); const input = getTextbox(); - expectInputPlaceholder(input, expectedFormat); - unmount(); + expectFieldPlaceholderV6(input, expectedFormat); + v6Response.unmount(); }; testFormat({ views: ['year'] }, 'YYYY'); @@ -62,27 +100,23 @@ describe(' - Field', () => { }); }); - describeAdapters('Timezone', DesktopDatePicker, ({ adapter, render, clickOnInput }) => { + describeAdapters('Timezone', DesktopDatePicker, ({ adapter, renderWithProps }) => { it('should clear the selected section when all sections are completed when using timezones', () => { - function WrappedDesktopDatePicker() { - const [value, setValue] = React.useState(adapter.date()!); - return ( - - ); - } - render(); + const v7Response = renderWithProps( + { + enableAccessibleFieldDOMStructure: true as const, + value: adapter.date()!, + format: `${adapter.formats.month} ${adapter.formats.year}`, + timezone: 'America/Chicago', + }, + { componentFamily: 'picker' }, + ); - const input = getTextbox(); - expectInputValue(input, 'June 2022'); - clickOnInput(input, 0); + expectFieldValueV7(v7Response.getSectionsContainer(), 'June 2022'); + v7Response.selectSection('month'); - fireEvent.change(input, { target: { value: ' 2022' } }); - expectInputValue(input, 'MMMM 2022'); + v7Response.pressKey(0, ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MMMM 2022'); }); }); }); diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx index e10287304e0fb..65ab6975bd82d 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx @@ -22,8 +22,12 @@ import { import { PickersActionBarAction } from '../PickersActionBar'; import { PickerValidDate } from '../models'; -type DesktopDateTimePickerComponent = (( - props: DesktopDateTimePickerProps & React.RefAttributes, +type DesktopDateTimePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DesktopDateTimePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -38,7 +42,11 @@ type DesktopDateTimePickerComponent = (( */ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker< TDate extends PickerValidDate, ->(inProps: DesktopDateTimePickerProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean, +>( + inProps: DesktopDateTimePickerProps, + ref: React.Ref, +) { const localeText = useLocaleText(); const utils = useUtils(); @@ -46,7 +54,7 @@ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker< const defaultizedProps = useDateTimePickerDefaultizedProps< TDate, DateOrTimeViewWithMeridiem, - DesktopDateTimePickerProps + DesktopDateTimePickerProps >(inProps, 'MuiDesktopDateTimePicker'); const { @@ -124,7 +132,12 @@ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker< }, }; - const { renderPicker } = useDesktopPicker({ + const { renderPicker } = useDesktopPicker< + TDate, + DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: singleItemValueManager, valueType: 'date-time', @@ -210,6 +223,10 @@ DesktopDateTimePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -376,11 +393,11 @@ DesktopDateTimePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -397,10 +414,6 @@ DesktopDateTimePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts index 4848a1477317f..1351c1439e33e 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts @@ -27,15 +27,23 @@ export interface DesktopDateTimePickerSlots DigitalClockSlots, MultiSectionDigitalClockSlots {} -export interface DesktopDateTimePickerSlotProps - extends BaseDateTimePickerSlotProps, - ExportedUseDesktopPickerSlotProps, +export interface DesktopDateTimePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends BaseDateTimePickerSlotProps, + ExportedUseDesktopPickerSlotProps< + TDate, + DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure + >, DigitalClockSlotProps, MultiSectionDigitalClockSlotProps {} -export interface DesktopDateTimePickerProps - extends BaseDateTimePickerProps, - DesktopOnlyPickerProps, +export interface DesktopDateTimePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends BaseDateTimePickerProps, + DesktopOnlyPickerProps, DesktopOnlyTimePickerProps { /** * Available views. @@ -55,5 +63,5 @@ export interface DesktopDateTimePickerProps * The props used for each component slot. * @default {} */ - slotProps?: DesktopDateTimePickerSlotProps; + slotProps?: DesktopDateTimePickerSlotProps; } diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/describes.DesktopDateTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/describes.DesktopDateTimePicker.test.tsx index 596b128c5dede..60145806da007 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/describes.DesktopDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/describes.DesktopDateTimePicker.test.tsx @@ -2,18 +2,31 @@ import { screen, userEvent } from '@mui-internal/test-utils'; import { createPickerRenderer, adapterToUse, - expectInputValue, - expectInputPlaceholder, - getTextbox, + expectFieldValueV7, describeValidation, describeValue, describePicker, + getFieldInputRoot, } from 'test/utils/pickers'; import { DesktopDateTimePicker } from '@mui/x-date-pickers/DesktopDateTimePicker'; +import { expect } from 'chai'; +import * as React from 'react'; describe(' - Describes', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); + it('should respect the `localeText` prop', function test() { + render( + , + ); + + expect(screen.queryByText('Custom cancel')).not.to.equal(null); + }); + describePicker(DesktopDateTimePicker, { render, fieldType: 'single-input', variant: 'desktop' }); describeValidation(DesktopDateTimePicker, () => ({ @@ -34,20 +47,21 @@ describe(' - Describes', () => { clock, assertRenderedValue: (expectedValue: any) => { const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); - const input = getTextbox(); - if (!expectedValue) { - expectInputPlaceholder(input, hasMeridiem ? 'MM/DD/YYYY hh:mm aa' : 'MM/DD/YYYY hh:mm'); + const fieldRoot = getFieldInputRoot(); + + let expectedValueStr: string; + if (expectedValue) { + expectedValueStr = adapterToUse.format( + expectedValue, + hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', + ); + } else { + expectedValueStr = hasMeridiem ? 'MM/DD/YYYY hh:mm aa' : 'MM/DD/YYYY hh:mm'; } - const expectedValueStr = expectedValue - ? adapterToUse.format( - expectedValue, - hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', - ) - : ''; - expectInputValue(input, expectedValueStr); + expectFieldValueV7(fieldRoot, expectedValueStr); }, - setNewValue: (value, { isOpened, applySameValue, selectSection }) => { + setNewValue: (value, { isOpened, applySameValue, selectSection, pressKey }) => { const newValue = applySameValue ? value : adapterToUse.addMinutes(adapterToUse.addHours(adapterToUse.addDays(value, 1), 1), 5); @@ -72,25 +86,22 @@ describe(' - Describes', () => { } } else { selectSection('day'); - const input = getTextbox(); - userEvent.keyPress(input, { key: 'ArrowUp' }); - // move to the hours section - userEvent.keyPress(input, { key: 'ArrowRight' }); - userEvent.keyPress(input, { key: 'ArrowRight' }); - userEvent.keyPress(input, { key: 'ArrowUp' }); - // move to the minutes section - userEvent.keyPress(input, { key: 'ArrowRight' }); - // increment by 5 minutes - userEvent.keyPress(input, { key: 'PageUp' }); + pressKey(undefined, 'ArrowUp'); + + selectSection('hours'); + pressKey(undefined, 'ArrowUp'); + + selectSection('minutes'); + pressKey(undefined, 'PageUp'); // increment by 5 minutes + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); if (hasMeridiem) { - // move to the meridiem section - userEvent.keyPress(input, { key: 'ArrowRight' }); + selectSection('meridiem'); const previousHours = adapterToUse.getHours(value); const newHours = adapterToUse.getHours(newValue); // update meridiem section if it changed if ((previousHours < 12 && newHours >= 12) || (previousHours >= 12 && newHours < 12)) { - userEvent.keyPress(input, { key: 'ArrowUp' }); + pressKey(undefined, 'ArrowUp'); } } } diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/field.DesktopDateTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/field.DesktopDateTimePicker.test.tsx index 32f773f26787d..3a951470e5f32 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/field.DesktopDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/field.DesktopDateTimePicker.test.tsx @@ -1,29 +1,53 @@ -import * as React from 'react'; -import { createPickerRenderer, getTextbox, expectInputPlaceholder } from 'test/utils/pickers'; +import { + createPickerRenderer, + getTextbox, + expectFieldPlaceholderV6, + expectFieldValueV7, + buildFieldInteractions, +} from 'test/utils/pickers'; import { DesktopDateTimePicker, DesktopDateTimePickerProps, } from '@mui/x-date-pickers/DesktopDateTimePicker'; describe(' - Field', () => { - const { render } = createPickerRenderer(); + const { render, clock } = createPickerRenderer(); + const { renderWithProps } = buildFieldInteractions({ + clock, + render, + Component: DesktopDateTimePicker, + }); it('should pass the ampm prop to the field', () => { - const { setProps } = render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true as const, + ampm: true, + }); - const input = getTextbox(); - expectInputPlaceholder(input, 'MM/DD/YYYY hh:mm aa'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY hh:mm aa'); - setProps({ ampm: false }); - expectInputPlaceholder(input, 'MM/DD/YYYY hh:mm'); + v7Response.setProps({ ampm: false }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY hh:mm'); }); it('should adapt the default field format based on the props of the picker', () => { - const testFormat = (props: DesktopDateTimePickerProps, expectedFormat: string) => { - const { unmount } = render(); + const testFormat = (props: DesktopDateTimePickerProps, expectedFormat: string) => { + // Test with v7 input + const v7Response = renderWithProps( + { ...props, enableAccessibleFieldDOMStructure: true as const }, + { componentFamily: 'picker' }, + ); + expectFieldValueV7(v7Response.getSectionsContainer(), expectedFormat); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps( + { ...props, enableAccessibleFieldDOMStructure: false as const }, + { componentFamily: 'picker' }, + ); const input = getTextbox(); - expectInputPlaceholder(input, expectedFormat); - unmount(); + expectFieldPlaceholderV6(input, expectedFormat); + v6Response.unmount(); }; testFormat({ views: ['day', 'hours', 'minutes'], ampm: false }, 'DD hh:mm'); diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx index 659c364afb4e9..74c3edefa7383 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx @@ -22,8 +22,12 @@ import { resolveTimeFormat } from '../internals/utils/time-utils'; import { resolveTimeViewsResponse } from '../internals/utils/date-time-utils'; import { TimeView, PickerValidDate } from '../models'; -type DesktopTimePickerComponent = (( - props: DesktopTimePickerProps & React.RefAttributes, +type DesktopTimePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: DesktopTimePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -38,7 +42,11 @@ type DesktopTimePickerComponent = (( */ const DesktopTimePicker = React.forwardRef(function DesktopTimePicker< TDate extends PickerValidDate, ->(inProps: DesktopTimePickerProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: DesktopTimePickerProps, + ref: React.Ref, +) { const localeText = useLocaleText(); const utils = useUtils(); @@ -46,7 +54,7 @@ const DesktopTimePicker = React.forwardRef(function DesktopTimePicker< const defaultizedProps = useTimePickerDefaultizedProps< TDate, TimeViewWithMeridiem, - DesktopTimePickerProps + DesktopTimePickerProps >(inProps, 'MuiDesktopTimePicker'); const { @@ -112,7 +120,12 @@ const DesktopTimePicker = React.forwardRef(function DesktopTimePicker< }, }; - const { renderPicker } = useDesktopPicker({ + const { renderPicker } = useDesktopPicker< + TDate, + TimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: singleItemValueManager, valueType: 'time', @@ -182,6 +195,10 @@ DesktopTimePicker.propTypes = { * @default false */ disablePast: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * Format of the date when rendered in the input(s). * Defaults to localized format based on the used `views`. @@ -298,11 +315,11 @@ DesktopTimePicker.propTypes = { referenceDate: PropTypes.object, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -319,10 +336,6 @@ DesktopTimePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific time. diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts index ec9a50c0c2c48..77faf64730cf7 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts @@ -24,15 +24,23 @@ export interface DesktopTimePickerSlots DigitalClockSlots, MultiSectionDigitalClockSlots {} -export interface DesktopTimePickerSlotProps - extends BaseTimePickerSlotProps, - ExportedUseDesktopPickerSlotProps, +export interface DesktopTimePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends BaseTimePickerSlotProps, + ExportedUseDesktopPickerSlotProps< + TDate, + TimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure + >, DigitalClockSlotProps, MultiSectionDigitalClockSlotProps {} -export interface DesktopTimePickerProps - extends BaseTimePickerProps, - DesktopOnlyPickerProps, +export interface DesktopTimePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends BaseTimePickerProps, + DesktopOnlyPickerProps, DesktopOnlyTimePickerProps { /** * Available views. @@ -47,5 +55,5 @@ export interface DesktopTimePickerProps * The props used for each component slot. * @default {} */ - slotProps?: DesktopTimePickerSlotProps; + slotProps?: DesktopTimePickerSlotProps; } diff --git a/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx index 5721929175aeb..8509cf5248394 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx @@ -4,13 +4,12 @@ import { createPickerRenderer, wrapPickerMount, adapterToUse, - expectInputValue, - expectInputPlaceholder, - getTextbox, + expectFieldValueV7, describeValidation, describeValue, describePicker, formatFullTimeValue, + getFieldInputRoot, } from 'test/utils/pickers'; import { DesktopTimePicker } from '@mui/x-date-pickers/DesktopTimePicker'; import { describeConformance } from 'test/utils/describeConformance'; @@ -32,7 +31,7 @@ describe(' - Describes', () => { variant: 'desktop', })); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, render, muiName: 'MuiDesktopTimePicker', @@ -61,16 +60,18 @@ describe(' - Describes', () => { clock, assertRenderedValue: (expectedValue: any) => { const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); - const input = getTextbox(); - if (!expectedValue) { - expectInputPlaceholder(input, hasMeridiem ? 'hh:mm aa' : 'hh:mm'); + const fieldRoot = getFieldInputRoot(); + + let expectedValueStr: string; + if (expectedValue) { + expectedValueStr = formatFullTimeValue(adapterToUse, expectedValue); + } else { + expectedValueStr = hasMeridiem ? 'hh:mm aa' : 'hh:mm'; } - expectInputValue( - input, - expectedValue ? formatFullTimeValue(adapterToUse, expectedValue) : '', - ); + + expectFieldValueV7(fieldRoot, expectedValueStr); }, - setNewValue: (value, { isOpened, applySameValue, selectSection }) => { + setNewValue: (value, { isOpened, applySameValue, selectSection, pressKey }) => { const newValue = applySameValue ? value : adapterToUse.addMinutes(adapterToUse.addHours(value, 1), 5); @@ -92,21 +93,19 @@ describe(' - Describes', () => { } } else { selectSection('hours'); - const input = getTextbox(); - userEvent.keyPress(input, { key: 'ArrowUp' }); - // move to the minutes section - userEvent.keyPress(input, { key: 'ArrowRight' }); - // increment by 5 minutes - userEvent.keyPress(input, { key: 'PageUp' }); + pressKey(undefined, 'ArrowUp'); + + selectSection('minutes'); + pressKey(undefined, 'PageUp'); // increment by 5 minutes + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); if (hasMeridiem) { - // move to the meridiem section - userEvent.keyPress(input, { key: 'ArrowRight' }); + selectSection('meridiem'); const previousHours = adapterToUse.getHours(value); const newHours = adapterToUse.getHours(newValue); // update meridiem section if it changed if ((previousHours < 12 && newHours >= 12) || (previousHours >= 12 && newHours < 12)) { - userEvent.keyPress(input, { key: 'ArrowUp' }); + pressKey(undefined, 'ArrowUp'); } } } diff --git a/packages/x-date-pickers/src/DesktopTimePicker/tests/field.DesktopTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopTimePicker/tests/field.DesktopTimePicker.test.tsx index 6f8b09033cd66..10d1848a5fe3c 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/tests/field.DesktopTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/tests/field.DesktopTimePicker.test.tsx @@ -1,26 +1,50 @@ -import * as React from 'react'; -import { createPickerRenderer, getTextbox, expectInputPlaceholder } from 'test/utils/pickers'; +import { + createPickerRenderer, + getTextbox, + expectFieldPlaceholderV6, + expectFieldValueV7, + buildFieldInteractions, +} from 'test/utils/pickers'; import { DesktopTimePicker, DesktopTimePickerProps } from '@mui/x-date-pickers/DesktopTimePicker'; describe(' - Field', () => { - const { render } = createPickerRenderer(); + const { render, clock } = createPickerRenderer(); + const { renderWithProps } = buildFieldInteractions({ + clock, + render, + Component: DesktopTimePicker, + }); it('should pass the ampm prop to the field', () => { - const { setProps } = render(); + const v7Response = renderWithProps( + { enableAccessibleFieldDOMStructure: true as const, ampm: true }, + { componentFamily: 'picker' }, + ); - const input = getTextbox(); - expectInputPlaceholder(input, 'hh:mm aa'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'hh:mm aa'); - setProps({ ampm: false }); - expectInputPlaceholder(input, 'hh:mm'); + v7Response.setProps({ ampm: false }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'hh:mm'); }); it('should adapt the default field format based on the props of the picker', () => { - const testFormat = (props: DesktopTimePickerProps, expectedFormat: string) => { - const { unmount } = render(); + const testFormat = (props: DesktopTimePickerProps, expectedFormat: string) => { + // Test with v7 input + const v7Response = renderWithProps( + { ...props, enableAccessibleFieldDOMStructure: true as const }, + { componentFamily: 'picker' }, + ); + expectFieldValueV7(v7Response.getSectionsContainer(), expectedFormat); + v7Response.unmount(); + + // Test with v6 input + const v6Response = renderWithProps( + { ...props, enableAccessibleFieldDOMStructure: false as const }, + { componentFamily: 'picker' }, + ); const input = getTextbox(); - expectInputPlaceholder(input, expectedFormat); - unmount(); + expectFieldPlaceholderV6(input, expectedFormat); + v6Response.unmount(); }; testFormat({ views: ['hours'], ampm: false }, 'hh'); diff --git a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx index 7ae4e5a566365..33c402bfe9c33 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx +++ b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx @@ -15,8 +15,12 @@ import { singleItemValueManager } from '../internals/utils/valueManagers'; import { renderDateViewCalendar } from '../dateViewRenderers'; import { resolveDateFormat } from '../internals/utils/date-utils'; -type MobileDatePickerComponent = (( - props: MobileDatePickerProps & React.RefAttributes, +type MobileDatePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: MobileDatePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -29,18 +33,21 @@ type MobileDatePickerComponent = (( * * - [MobileDatePicker API](https://mui.com/x/api/date-pickers/mobile-date-picker/) */ -const MobileDatePicker = React.forwardRef(function MobileDatePicker( - inProps: MobileDatePickerProps, +const MobileDatePicker = React.forwardRef(function MobileDatePicker< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: MobileDatePickerProps, ref: React.Ref, ) { const localeText = useLocaleText(); const utils = useUtils(); // Props with the default values common to all date pickers - const defaultizedProps = useDatePickerDefaultizedProps>( - inProps, - 'MuiMobileDatePicker', - ); + const defaultizedProps = useDatePickerDefaultizedProps< + TDate, + MobileDatePickerProps + >(inProps, 'MuiMobileDatePicker'); const viewRenderers: PickerViewRendererLookup = { day: renderDateViewCalendar, @@ -72,7 +79,12 @@ const MobileDatePicker = React.forwardRef(function MobileDatePicker({ + const { renderPicker } = useMobilePicker< + TDate, + DateView, + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: singleItemValueManager, valueType: 'date', @@ -143,6 +155,10 @@ MobileDatePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -286,11 +302,11 @@ MobileDatePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -307,10 +323,6 @@ MobileDatePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.types.ts b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.types.ts index d6371b06907e5..b7b12ed698a6f 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.types.ts +++ b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.types.ts @@ -15,13 +15,17 @@ export interface MobileDatePickerSlots extends BaseDatePickerSlots, MakeOptional, 'field'> {} -export interface MobileDatePickerSlotProps - extends BaseDatePickerSlotProps, - ExportedUseMobilePickerSlotProps {} +export interface MobileDatePickerSlotProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> extends BaseDatePickerSlotProps, + ExportedUseMobilePickerSlotProps {} -export interface MobileDatePickerProps - extends BaseDatePickerProps, - MobileOnlyPickerProps { +export interface MobileDatePickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +> extends BaseDatePickerProps, + MobileOnlyPickerProps { /** * Overridable component slots. * @default {} @@ -31,5 +35,5 @@ export interface MobileDatePickerProps * The props used for each component slot. * @default {} */ - slotProps?: MobileDatePickerSlotProps; + slotProps?: MobileDatePickerSlotProps; } diff --git a/packages/x-date-pickers/src/MobileDatePicker/tests/MobileDatePicker.test.tsx b/packages/x-date-pickers/src/MobileDatePicker/tests/MobileDatePicker.test.tsx index b5c1cfaaf86fe..2cb49768d4426 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/tests/MobileDatePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileDatePicker/tests/MobileDatePicker.test.tsx @@ -8,17 +8,29 @@ import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker'; import { createPickerRenderer, adapterToUse, - getTextbox, - expectInputValue, + expectFieldValueV7, + buildFieldInteractions, + openPicker, + getFieldSectionsContainer, } from 'test/utils/pickers'; describe('', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: MobileDatePicker, + }); it('allows to change only year', () => { const onChangeMock = spy(); render( - , + , ); fireEvent.click(screen.getByLabelText(/switch to year view/i)); @@ -31,6 +43,7 @@ describe('', () => { it('allows to select edge years from list', () => { render( ', () => { it('prop `onMonthChange` – dispatches callback when months switching', () => { const onMonthChangeMock = spy(); - render(); + render( + , + ); fireEvent.click(screen.getByLabelText('Next month')); expect(onMonthChangeMock.callCount).to.equal(1); }); it('prop `loading` – displays default loading indicator', () => { - render(); + render(); expect(screen.queryAllByMuiTest('day')).to.have.length(0); expect(screen.getByMuiTest('loading-progress')).toBeVisible(); @@ -61,6 +76,7 @@ describe('', () => { it('prop `renderLoading` – displays custom loading indicator', () => { render( } open @@ -75,6 +91,7 @@ describe('', () => { it('should render custom toolbar component', () => { render(
, @@ -88,6 +105,7 @@ describe('', () => { it('should format toolbar according to `toolbarFormat` prop', () => { render( ', () => { }); it('should render the toolbar when `hidden` is `false`', () => { - render(); + render( + , + ); expect(screen.getByMuiTest('picker-toolbar')).toBeVisible(); }); @@ -112,6 +136,7 @@ describe('', () => { it('should render custom day', () => { render( ', () => { }); describe('picker state', () => { - it('should open when clicking "Choose date"', () => { + it('should open when clicking the input', () => { const onOpen = spy(); - render(); + render(); - userEvent.mousePress(screen.getByRole('textbox')); + userEvent.mousePress(getFieldSectionsContainer()); expect(onOpen.callCount).to.equal(1); expect(screen.queryByRole('dialog')).toBeVisible(); @@ -142,12 +167,19 @@ describe('', () => { function ControlledMobileDatePicker(props) { const [value, setValue] = React.useState(null); - return ; + return ( + + ); } render(); - userEvent.mousePress(screen.getByRole('textbox')); + openPicker({ type: 'date', variant: 'mobile' }); fireEvent.click(screen.getByText('15', { selector: 'button' })); fireEvent.click(screen.getByText('OK', { selector: 'button' })); @@ -156,24 +188,26 @@ describe('', () => { }); it('should update internal state when controlled value is updated', () => { - const value = adapterToUse.date('2019-01-01'); - - const { setProps } = render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true as const, + value: adapterToUse.date('2019-01-01'), + }); // Set a date - expectInputValue(getTextbox(), '01/01/2019'); + expectFieldValueV7(v7Response.getSectionsContainer(), '01/01/2019'); // Clean value using external control - setProps({ value: null }); - expectInputValue(getTextbox(), ''); + v7Response.setProps({ value: null }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY'); // Open and Dismiss the picker - userEvent.mousePress(screen.getByRole('textbox')); - userEvent.keyPress(document.activeElement!, { key: 'Escape' }); + openPicker({ type: 'date', variant: 'mobile' }); + // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target + fireEvent.keyDown(document.activeElement!, { key: 'Escape' }); clock.runToLast(); // Verify it's still a clean value - expectInputValue(getTextbox(), ''); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY'); }); }); }); diff --git a/packages/x-date-pickers/src/MobileDatePicker/tests/describes.MobileDatePicker.test.tsx b/packages/x-date-pickers/src/MobileDatePicker/tests/describes.MobileDatePicker.test.tsx index 7160fc9f73c96..aec1d5224624f 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/tests/describes.MobileDatePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileDatePicker/tests/describes.MobileDatePicker.test.tsx @@ -1,14 +1,13 @@ -import { screen, userEvent } from '@mui-internal/test-utils'; +import { screen, userEvent, fireEvent } from '@mui-internal/test-utils'; import { createPickerRenderer, adapterToUse, - expectInputValue, - expectInputPlaceholder, + expectFieldValueV7, openPicker, - getTextbox, describeValidation, describeValue, describePicker, + getFieldInputRoot, } from 'test/utils/pickers'; import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker'; @@ -33,14 +32,13 @@ describe(' - Describes', () => { emptyValue: null, clock, assertRenderedValue: (expectedValue: any) => { - const input = getTextbox(); - if (!expectedValue) { - expectInputPlaceholder(input, 'MM/DD/YYYY'); - } - expectInputValue( - input, - expectedValue ? adapterToUse.format(expectedValue, 'keyboardDate') : '', - ); + const fieldRoot = getFieldInputRoot(); + + const expectedValueStr = expectedValue + ? adapterToUse.format(expectedValue, 'keyboardDate') + : 'MM/DD/YYYY'; + + expectFieldValueV7(fieldRoot, expectedValueStr); }, setNewValue: (value, { isOpened, applySameValue }) => { if (!isOpened) { @@ -54,7 +52,8 @@ describe(' - Describes', () => { // Close the picker to return to the initial state if (!isOpened) { - userEvent.keyPress(document.activeElement!, { key: 'Escape' }); + // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target + fireEvent.keyDown(document.activeElement!, { key: 'Escape' }); clock.runToLast(); } diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx index dfb26f269d96a..0573711c48938 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx @@ -16,8 +16,12 @@ import { renderDateViewCalendar } from '../dateViewRenderers'; import { renderTimeViewClock } from '../timeViewRenderers'; import { resolveDateTimeFormat } from '../internals/utils/date-time-utils'; -type MobileDateTimePickerComponent = (( - props: MobileDateTimePickerProps & React.RefAttributes, +type MobileDateTimePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: MobileDateTimePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -32,7 +36,11 @@ type MobileDateTimePickerComponent = (( */ const MobileDateTimePicker = React.forwardRef(function MobileDateTimePicker< TDate extends PickerValidDate, ->(inProps: MobileDateTimePickerProps, ref: React.Ref) { + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: MobileDateTimePickerProps, + ref: React.Ref, +) { const localeText = useLocaleText(); const utils = useUtils(); @@ -40,7 +48,7 @@ const MobileDateTimePicker = React.forwardRef(function MobileDateTimePicker< const defaultizedProps = useDateTimePickerDefaultizedProps< TDate, DateOrTimeView, - MobileDateTimePickerProps + MobileDateTimePickerProps >(inProps, 'MuiMobileDateTimePicker'); const viewRenderers: PickerViewRendererLookup = { @@ -83,7 +91,12 @@ const MobileDateTimePicker = React.forwardRef(function MobileDateTimePicker< }, }; - const { renderPicker } = useMobilePicker({ + const { renderPicker } = useMobilePicker< + TDate, + DateOrTimeView, + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: singleItemValueManager, valueType: 'date-time', @@ -169,6 +182,10 @@ MobileDateTimePicker.propTypes = { * If `true`, the week number will be display in the calendar. */ displayWeekNumber: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -335,11 +352,11 @@ MobileDateTimePicker.propTypes = { renderLoading: PropTypes.func, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -356,10 +373,6 @@ MobileDateTimePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific date. diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts index d8b6a4c1d9621..ff38d040253e2 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts @@ -14,21 +14,23 @@ import { DateOrTimeViewWithMeridiem } from '../internals/models'; export interface MobileDateTimePickerSlots< TDate extends PickerValidDate, - TView extends DateOrTimeViewWithMeridiem = DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, > extends BaseDateTimePickerSlots, MakeOptional, 'field'> {} export interface MobileDateTimePickerSlotProps< TDate extends PickerValidDate, - TView extends DateOrTimeViewWithMeridiem = DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure extends boolean, > extends BaseDateTimePickerSlotProps, - ExportedUseMobilePickerSlotProps {} + ExportedUseMobilePickerSlotProps {} export interface MobileDateTimePickerProps< TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem = DateOrTimeView, + TEnableAccessibleFieldDOMStructure extends boolean = false, > extends BaseDateTimePickerProps, - MobileOnlyPickerProps { + MobileOnlyPickerProps { /** * Overridable component slots. * @default {} @@ -38,5 +40,5 @@ export interface MobileDateTimePickerProps< * The props used for each component slot. * @default {} */ - slotProps?: MobileDateTimePickerSlotProps; + slotProps?: MobileDateTimePickerSlotProps; } diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/tests/MobileDateTimePicker.test.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/tests/MobileDateTimePicker.test.tsx index e796cd42c98a5..bea491298d7ef 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/tests/MobileDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/tests/MobileDateTimePicker.test.tsx @@ -8,6 +8,7 @@ import { createPickerRenderer, openPicker, getClockTouchEvent, + getFieldSectionsContainer, } from 'test/utils/pickers'; describe('', () => { @@ -16,6 +17,7 @@ describe('', () => { it('should render date and time by default', () => { render( ', () => { }); it('should render toolbar and tabs by default', () => { - render(); + render( + , + ); expect(screen.queryByMuiTest('picker-toolbar-title')).not.to.equal(null); expect(screen.getByRole('tab', { name: 'pick date' })).not.to.equal(null); @@ -39,6 +47,7 @@ describe('', () => { it('can render seconds on view', () => { render( ', () => { it('should not render tabs when `hidden` is `true`', () => { render( ', () => { it('should not render only toolbar when `hidden` is `true`', () => { render( ', () => { }); describe('picker state', () => { - it('should open when clicking "Choose date"', () => { + it('should open when clicking the input', () => { const onOpen = spy(); - render(); + render(); - userEvent.mousePress(screen.getByRole('textbox')); + userEvent.mousePress(getFieldSectionsContainer()); expect(onOpen.callCount).to.equal(1); expect(screen.queryByRole('dialog')).toBeVisible(); @@ -105,6 +116,7 @@ describe('', () => { render( - Describes', () => { emptyValue: null, assertRenderedValue: (expectedValue: any) => { const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); - const input = getTextbox(); - if (!expectedValue) { - expectInputPlaceholder(input, hasMeridiem ? 'MM/DD/YYYY hh:mm aa' : 'MM/DD/YYYY hh:mm'); + const fieldRoot = getFieldInputRoot(); + + let expectedValueStr: string; + if (expectedValue) { + expectedValueStr = adapterToUse.format( + expectedValue, + hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', + ); + } else { + expectedValueStr = hasMeridiem ? 'MM/DD/YYYY hh:mm aa' : 'MM/DD/YYYY hh:mm'; } - const expectedValueStr = expectedValue - ? adapterToUse.format( - expectedValue, - hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', - ) - : ''; - expectInputValue(input, expectedValueStr); + expectFieldValueV7(fieldRoot, expectedValueStr); }, setNewValue: (value, { isOpened, applySameValue }) => { if (!isOpened) { @@ -84,7 +84,8 @@ describe(' - Describes', () => { // Close the picker if (!isOpened) { - userEvent.keyPress(document.activeElement!, { key: 'Escape' }); + // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target + fireEvent.keyDown(document.activeElement!, { key: 'Escape' }); clock.runToLast(); } else { // return to the date view in case we'd like to repeat the selection process diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/tests/field.MobileDateTimePicker.test.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/tests/field.MobileDateTimePicker.test.tsx index 7debc4602f3c0..2a8aae0ea83ce 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/tests/field.MobileDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/tests/field.MobileDateTimePicker.test.tsx @@ -1,17 +1,27 @@ -import * as React from 'react'; import { MobileDateTimePicker } from '@mui/x-date-pickers/MobileDateTimePicker'; -import { createPickerRenderer, getTextbox, expectInputPlaceholder } from 'test/utils/pickers'; +import { + createPickerRenderer, + expectFieldValueV7, + buildFieldInteractions, +} from 'test/utils/pickers'; describe(' - Field', () => { - const { render } = createPickerRenderer(); + const { render, clock } = createPickerRenderer(); + const { renderWithProps } = buildFieldInteractions({ + clock, + render, + Component: MobileDateTimePicker, + }); it('should pass the ampm prop to the field', () => { - const { setProps } = render(); + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true as const, + ampm: true, + }); - const input = getTextbox(); - expectInputPlaceholder(input, 'MM/DD/YYYY hh:mm aa'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY hh:mm aa'); - setProps({ ampm: false }); - expectInputPlaceholder(input, 'MM/DD/YYYY hh:mm'); + v7Response.setProps({ ampm: false }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'MM/DD/YYYY hh:mm'); }); }); diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx index fc8d5747ef0ea..7ade9bc78f68a 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx @@ -15,8 +15,12 @@ import { extractValidationProps } from '../internals/utils/validation/extractVal import { renderTimeViewClock } from '../timeViewRenderers'; import { resolveTimeFormat } from '../internals/utils/time-utils'; -type MobileTimePickerComponent = (( - props: MobileTimePickerProps & React.RefAttributes, +type MobileTimePickerComponent = (< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + props: MobileTimePickerProps & + React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; /** @@ -29,8 +33,11 @@ type MobileTimePickerComponent = (( * * - [MobileTimePicker API](https://mui.com/x/api/date-pickers/mobile-time-picker/) */ -const MobileTimePicker = React.forwardRef(function MobileTimePicker( - inProps: MobileTimePickerProps, +const MobileTimePicker = React.forwardRef(function MobileTimePicker< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = false, +>( + inProps: MobileTimePickerProps, ref: React.Ref, ) { const localeText = useLocaleText(); @@ -40,7 +47,7 @@ const MobileTimePicker = React.forwardRef(function MobileTimePicker + MobileTimePickerProps >(inProps, 'MuiMobileTimePicker'); const viewRenderers: PickerViewRendererLookup = { @@ -76,7 +83,12 @@ const MobileTimePicker = React.forwardRef(function MobileTimePicker({ + const { renderPicker } = useMobilePicker< + TDate, + TimeView, + TEnableAccessibleFieldDOMStructure, + typeof props + >({ props, valueManager: singleItemValueManager, valueType: 'time', @@ -146,6 +158,10 @@ MobileTimePicker.propTypes = { * @default false */ disablePast: PropTypes.bool, + /** + * @default false + */ + enableAccessibleFieldDOMStructure: PropTypes.any, /** * Format of the date when rendered in the input(s). * Defaults to localized format based on the used `views`. @@ -262,11 +278,11 @@ MobileTimePicker.propTypes = { referenceDate: PropTypes.object, /** * The currently selected sections. - * This prop accept four formats: + * This prop accepts four formats: * 1. If a number is provided, the section at this index will be selected. - * 2. If an object with a `startIndex` and `endIndex` properties are provided, the sections between those two indexes will be selected. - * 3. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. - * 4. If `null` is provided, no section will be selected + * 2. If a string of type `FieldSectionType` is provided, the first section with that name will be selected. + * 3. If `"all"` is provided, all the sections will be selected. + * 4. If `null` is provided, no section will be selected. * If not provided, the selected sections will be handled internally. */ selectedSections: PropTypes.oneOfType([ @@ -283,10 +299,6 @@ MobileTimePicker.propTypes = { 'year', ]), PropTypes.number, - PropTypes.shape({ - endIndex: PropTypes.number.isRequired, - startIndex: PropTypes.number.isRequired, - }), ]), /** * Disable specific time. diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts index 152f1f2402ddf..a86ee69bf2bd7 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts @@ -14,21 +14,23 @@ import { TimeViewWithMeridiem } from '../internals/models'; export interface MobileTimePickerSlots< TDate extends PickerValidDate, - TView extends TimeViewWithMeridiem = TimeView, + TView extends TimeViewWithMeridiem, > extends BaseTimePickerSlots, MakeOptional, 'field'> {} export interface MobileTimePickerSlotProps< TDate extends PickerValidDate, - TView extends TimeViewWithMeridiem = TimeView, + TView extends TimeViewWithMeridiem, + TEnableAccessibleFieldDOMStructure extends boolean, > extends BaseTimePickerSlotProps, - ExportedUseMobilePickerSlotProps {} + ExportedUseMobilePickerSlotProps {} export interface MobileTimePickerProps< TDate extends PickerValidDate, TView extends TimeViewWithMeridiem = TimeView, + TEnableAccessibleFieldDOMStructure extends boolean = false, > extends BaseTimePickerProps, - MobileOnlyPickerProps { + MobileOnlyPickerProps { /** * Overridable component slots. * @default {} @@ -38,5 +40,5 @@ export interface MobileTimePickerProps< * The props used for each component slot. * @default {} */ - slotProps?: MobileTimePickerSlotProps; + slotProps?: MobileTimePickerSlotProps; } diff --git a/packages/x-date-pickers/src/MobileTimePicker/tests/MobileTimePicker.test.tsx b/packages/x-date-pickers/src/MobileTimePicker/tests/MobileTimePicker.test.tsx index a35ebc95f0f9a..aac6813953bbe 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/tests/MobileTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/tests/MobileTimePicker.test.tsx @@ -8,18 +8,19 @@ import { adapterToUse, openPicker, getClockTouchEvent, + getFieldSectionsContainer, } from 'test/utils/pickers'; describe('', () => { const { render } = createPickerRenderer({ clock: 'fake' }); describe('picker state', () => { - it('should open when clicking the textbox', () => { + it('should open when clicking the input', () => { const onOpen = spy(); - render(); + render(); - userEvent.mousePress(screen.getByRole('textbox')); + userEvent.mousePress(getFieldSectionsContainer()); expect(onOpen.callCount).to.equal(1); expect(screen.queryByRole('dialog')).toBeVisible(); @@ -29,6 +30,7 @@ describe('', () => { const handleChange = spy(); render( ', () => { render( - Describes', () => { variant: 'mobile', })); - describeConformance(, () => ({ + describeConformance(, () => ({ classes: {} as any, render, muiName: 'MuiMobileTimePicker', @@ -62,15 +61,16 @@ describe(' - Describes', () => { clock, assertRenderedValue: (expectedValue: any) => { const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); - const input = getTextbox(); - if (!expectedValue) { - expectInputPlaceholder(input, hasMeridiem ? 'hh:mm aa' : 'hh:mm'); + const fieldRoot = getFieldInputRoot(); + + let expectedValueStr: string; + if (expectedValue) { + expectedValueStr = formatFullTimeValue(adapterToUse, expectedValue); + } else { + expectedValueStr = hasMeridiem ? 'hh:mm aa' : 'hh:mm'; } - const expectedValueStr = expectedValue - ? formatFullTimeValue(adapterToUse, expectedValue) - : ''; - expectInputValue(input, expectedValueStr); + expectFieldValueV7(fieldRoot, expectedValueStr); }, setNewValue: (value, { isOpened, applySameValue }) => { if (!isOpened) { @@ -101,7 +101,8 @@ describe(' - Describes', () => { // Close the picker if (!isOpened) { - userEvent.keyPress(document.activeElement!, { key: 'Escape' }); + // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target + fireEvent.keyDown(document.activeElement!, { key: 'Escape' }); clock.runToLast(); } else { // return to the hours view in case we'd like to repeat the selection process diff --git a/packages/x-date-pickers/src/MobileTimePicker/tests/field.MobileTimePicker.test.tsx b/packages/x-date-pickers/src/MobileTimePicker/tests/field.MobileTimePicker.test.tsx index bd582c593fe0f..206189acc74a2 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/tests/field.MobileTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/tests/field.MobileTimePicker.test.tsx @@ -1,17 +1,27 @@ -import * as React from 'react'; -import { createPickerRenderer, getTextbox, expectInputPlaceholder } from 'test/utils/pickers'; +import { + createPickerRenderer, + buildFieldInteractions, + expectFieldValueV7, +} from 'test/utils/pickers'; import { MobileTimePicker } from '@mui/x-date-pickers/MobileTimePicker'; describe(' - Field', () => { - const { render } = createPickerRenderer(); + const { render, clock } = createPickerRenderer(); + const { renderWithProps } = buildFieldInteractions({ + render, + clock, + Component: MobileTimePicker, + }); it('should pass the ampm prop to the field', () => { - const { setProps } = render(); + const v7Response = renderWithProps( + { enableAccessibleFieldDOMStructure: true as const, ampm: true }, + { componentFamily: 'picker' }, + ); - const input = getTextbox(); - expectInputPlaceholder(input, 'hh:mm aa'); + expectFieldValueV7(v7Response.getSectionsContainer(), 'hh:mm aa'); - setProps({ ampm: false }); - expectInputPlaceholder(input, 'hh:mm'); + v7Response.setProps({ ampm: false }); + expectFieldValueV7(v7Response.getSectionsContainer(), 'hh:mm'); }); }); diff --git a/packages/x-date-pickers/src/PickersSectionList/PickersSectionList.tsx b/packages/x-date-pickers/src/PickersSectionList/PickersSectionList.tsx index ae7ed9388cc60..64f3ddf16c2f7 100644 --- a/packages/x-date-pickers/src/PickersSectionList/PickersSectionList.tsx +++ b/packages/x-date-pickers/src/PickersSectionList/PickersSectionList.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; -import styled from '@mui/system/styled'; import PropTypes from 'prop-types'; import { useSlotProps } from '@mui/base/utils'; import composeClasses from '@mui/utils/composeClasses'; import useForkRef from '@mui/utils/useForkRef'; -import { useThemeProps } from '@mui/material/styles'; +import { styled, useThemeProps } from '@mui/material/styles'; import { getPickersSectionListUtilityClass, pickersSectionListClasses, diff --git a/packages/x-date-pickers/src/PickersTextField/PickersFilledInput/PickersFilledInput.tsx b/packages/x-date-pickers/src/PickersTextField/PickersFilledInput/PickersFilledInput.tsx index b483ad3168427..95d9343c5893f 100644 --- a/packages/x-date-pickers/src/PickersTextField/PickersFilledInput/PickersFilledInput.tsx +++ b/packages/x-date-pickers/src/PickersTextField/PickersFilledInput/PickersFilledInput.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { FormControlState, useFormControl } from '@mui/material/FormControl'; import { styled, useThemeProps } from '@mui/material/styles'; +import { refType } from '@mui/utils'; import composeClasses from '@mui/utils/composeClasses'; import { pickersFilledInputClasses, @@ -237,14 +238,10 @@ PickersFilledInput.propTypes = { hiddenLabel: PropTypes.bool, id: PropTypes.string, inputProps: PropTypes.object, - inputRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.object, - }), - ]), + inputRef: refType, label: PropTypes.node, margin: PropTypes.oneOf(['dense', 'none', 'normal']), + name: PropTypes.string, onChange: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, onInput: PropTypes.func.isRequired, diff --git a/packages/x-date-pickers/src/PickersTextField/PickersInput/PickersInput.tsx b/packages/x-date-pickers/src/PickersTextField/PickersInput/PickersInput.tsx index c5b1b05670c90..3670ea5cbab9d 100644 --- a/packages/x-date-pickers/src/PickersTextField/PickersInput/PickersInput.tsx +++ b/packages/x-date-pickers/src/PickersTextField/PickersInput/PickersInput.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { FormControlState, useFormControl } from '@mui/material/FormControl'; import { styled, useThemeProps } from '@mui/material/styles'; +import { refType } from '@mui/utils'; import composeClasses from '@mui/utils/composeClasses'; import { pickersInputClasses, getPickersInputUtilityClass } from './pickersInputClasses'; import { PickersInputBase, PickersInputBaseProps } from '../PickersInputBase'; @@ -174,14 +175,10 @@ PickersInput.propTypes = { fullWidth: PropTypes.bool, id: PropTypes.string, inputProps: PropTypes.object, - inputRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.object, - }), - ]), + inputRef: refType, label: PropTypes.node, margin: PropTypes.oneOf(['dense', 'none', 'normal']), + name: PropTypes.string, onChange: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, onInput: PropTypes.func.isRequired, diff --git a/packages/x-date-pickers/src/PickersTextField/PickersInputBase/PickersInputBase.tsx b/packages/x-date-pickers/src/PickersTextField/PickersInputBase/PickersInputBase.tsx index 4c30892dc2759..6d37ff7186417 100644 --- a/packages/x-date-pickers/src/PickersTextField/PickersInputBase/PickersInputBase.tsx +++ b/packages/x-date-pickers/src/PickersTextField/PickersInputBase/PickersInputBase.tsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { FormControlState, useFormControl } from '@mui/material/FormControl'; import { styled, useThemeProps } from '@mui/material/styles'; import useForkRef from '@mui/utils/useForkRef'; +import { refType } from '@mui/utils'; import composeClasses from '@mui/utils/composeClasses'; import capitalize from '@mui/utils/capitalize'; import visuallyHidden from '@mui/utils/visuallyHidden'; @@ -190,6 +191,8 @@ const PickersInputBase = React.forwardRef(function PickersInputBase( onPaste, onKeyDown, fullWidth, + name, + readOnly, inputProps, inputRef, sectionListRef, @@ -292,12 +295,16 @@ const PickersInputBase = React.forwardRef(function PickersInputBase( }) : null}