From 7c505ea54f2d1848d393a42d8a532295fdde0859 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Tue, 14 Jan 2025 07:35:12 +0100 Subject: [PATCH] [pickers] Let the field components handle their opening UI, and allow field editing on mobile pickers (#15671) Signed-off-by: Flavien DELANGLE Co-authored-by: Lukas Tyla --- .../base-concepts/base-concepts.md | 4 +- .../calendar-systems/AdapterHijri.js | 13 +- .../calendar-systems/AdapterHijri.tsx | 13 +- .../custom-field/BrowserV7Field.js | 18 +- .../custom-field/BrowserV7Field.tsx | 20 +- .../BrowserV7MultiInputRangeField.js | 23 +- .../BrowserV7MultiInputRangeField.tsx | 29 +- .../BrowserV7SingleInputRangeField.js | 5 +- .../BrowserV7SingleInputRangeField.tsx | 6 +- .../date-pickers/custom-field/JoyV6Field.js | 37 +- .../date-pickers/custom-field/JoyV6Field.tsx | 40 +- .../custom-field/JoyV6MultiInputRangeField.js | 23 +- .../JoyV6MultiInputRangeField.tsx | 29 +- .../JoyV6SingleInputRangeField.js | 11 +- .../JoyV6SingleInputRangeField.tsx | 10 +- .../MaterialDatePicker.js | 75 ++- .../MaterialDatePicker.tsx | 76 ++- .../behavior-button/MaterialDatePicker.js | 13 +- .../behavior-button/MaterialDatePicker.tsx | 13 +- .../MaterialDateRangePicker.js | 13 +- .../MaterialDateRangePicker.tsx | 13 +- .../MaskedMaterialTextField.js | 20 +- .../MaskedMaterialTextField.tsx | 20 +- .../MaterialDatePicker.js | 74 +++ .../MaterialDatePicker.tsx | 78 +++ .../MaterialDatePicker.tsx.preview | 1 + .../MaterialDatePicker.js | 5 +- .../MaterialDatePicker.tsx | 5 +- .../behavior-tutorial/MaterialDatePicker.js | 4 +- .../behavior-tutorial/MaterialDatePicker.tsx | 4 +- .../date-pickers/custom-field/custom-field.md | 12 +- .../CustomPropsOpeningButton.js | 2 +- .../CustomPropsOpeningButton.tsx | 2 +- .../CustomPropsOpeningButton.tsx.preview | 2 +- .../StartEdgeOpeningButton.js | 19 + .../StartEdgeOpeningButton.tsx | 19 + .../StartEdgeOpeningButton.tsx.preview | 5 + .../custom-opening-button.md | 6 + .../date-pickers/date-picker/date-picker.md | 10 +- .../date-range-picker/date-range-picker.md | 4 +- .../date-time-picker/date-time-picker.md | 4 +- .../date-time-range-picker.md | 4 +- .../date-pickers/time-picker/time-picker.md | 4 +- .../migration-pickers-v7.md | 105 ++++ docs/pages/x/api/date-pickers/date-field.json | 8 + .../x/api/date-pickers/date-time-field.json | 8 + .../api/date-pickers/mobile-date-picker.json | 33 +- .../date-pickers/mobile-date-time-picker.json | 33 +- .../api/date-pickers/mobile-time-picker.json | 33 +- .../single-input-date-range-field.json | 4 + .../single-input-date-time-range-field.json | 4 + .../single-input-time-range-field.json | 4 + docs/pages/x/api/date-pickers/time-field.json | 8 + .../overview/mainDemo/PickerButton.tsx | 5 +- .../date-pickers/date-field/date-field.json | 6 + .../date-time-field/date-time-field.json | 6 + .../mobile-date-picker.json | 7 +- .../mobile-date-time-picker.json | 7 +- .../mobile-time-picker.json | 7 +- .../single-input-date-range-field.json | 3 + .../single-input-date-time-range-field.json | 3 + .../single-input-time-range-field.json | 3 + .../date-pickers/time-field/time-field.json | 6 + .../tests/DesktopDateRangePicker.test.tsx | 48 +- .../describes.DesktopDateRangePicker.test.tsx | 1 + .../tests/DesktopDateTimeRangePicker.test.tsx | 4 +- .../tests/MobileDateRangePicker.test.tsx | 18 +- .../describes.MobileDateRangePicker.test.tsx | 2 +- ...scribes.MobileDateTimeRangePicker.test.tsx | 2 +- .../MultiInputDateRangeField.tsx | 9 +- .../MultiInputDateTimeRangeField.tsx | 9 +- .../MultiInputTimeRangeField.tsx | 9 +- .../SingleInputDateRangeField.tsx | 58 +- .../SingleInputDateRangeField.types.ts | 38 +- .../useSingleInputDateRangeField.ts | 1 + .../SingleInputDateTimeRangeField.tsx | 58 +- .../SingleInputDateTimeRangeField.types.ts | 38 +- .../useSingleInputDateTimeRangeField.ts | 1 + .../SingleInputTimeRangeField.tsx | 58 +- .../SingleInputTimeRangeField.types.ts | 38 +- .../useSingleInputTimeRangeField.ts | 1 + .../useDesktopRangePicker.tsx | 45 +- .../hooks/useEnrichedRangePickerField.ts | 25 +- .../useMobileRangePicker.tsx | 23 +- .../hooks/useMultiInputRangeField/shared.ts | 12 - .../useMultiInputDateRangeField.ts | 6 +- .../useMultiInputDateTimeRangeField.ts | 6 +- .../useMultiInputTimeRangeField.ts | 6 +- .../src/managers/useDateRangeManager.ts | 2 + .../src/managers/useDateTimeRangeManager.ts | 2 + .../src/managers/useTimeRangeManager.ts | 2 + .../x-date-pickers-pro/src/models/fields.ts | 12 +- .../src/DateField/DateField.tsx | 60 +-- .../src/DateField/DateField.types.ts | 35 +- .../src/DateField/useDateField.ts | 1 + .../src/DatePicker/tests/DatePicker.test.tsx | 8 +- .../src/DateTimeField/DateTimeField.tsx | 62 +-- .../src/DateTimeField/DateTimeField.types.ts | 35 +- .../src/DateTimeField/useDateTimeField.ts | 1 + .../tests/DateTimePicker.test.tsx | 8 +- .../DesktopDatePicker/DesktopDatePicker.tsx | 11 - .../tests/DesktopDatePicker.test.tsx | 30 +- .../describes.DesktopDatePicker.test.tsx | 1 - .../DesktopDateTimePicker.tsx | 11 - .../tests/DesktopDateTimePicker.test.tsx | 4 +- .../describes.DesktopDateTimePicker.test.tsx | 1 - .../DesktopTimePicker/DesktopTimePicker.tsx | 11 - .../tests/DesktopTimePicker.test.tsx | 8 +- .../describes.DesktopTimePicker.test.tsx | 1 - .../tests/describes.DigitalClock.test.tsx | 2 - .../src/MobileDatePicker/MobileDatePicker.tsx | 9 - .../tests/MobileDatePicker.test.tsx | 16 +- .../tests/describes.MobileDatePicker.test.tsx | 2 +- .../MobileDateTimePicker.tsx | 9 - .../tests/MobileDateTimePicker.test.tsx | 14 +- .../describes.MobileDateTimePicker.test.tsx | 3 +- .../src/MobileTimePicker/MobileTimePicker.tsx | 9 - .../tests/MobileTimePicker.test.tsx | 14 +- .../tests/describes.MobileTimePicker.test.tsx | 3 +- ...escribes.MultiSectionDigitalClock.test.tsx | 2 - .../src/TimeField/TimeField.tsx | 58 +- .../src/TimeField/TimeField.types.ts | 35 +- .../src/TimeField/useTimeField.ts | 1 + .../src/TimePicker/tests/TimePicker.test.tsx | 10 +- .../internals/components/PickerFieldUI.tsx | 500 ++++++++++++++++++ .../internals/components/PickerProvider.tsx | 12 + .../useDesktopPicker/useDesktopPicker.tsx | 101 +--- .../useDesktopPicker.types.ts | 52 +- .../src/internals/hooks/useField/useField.ts | 10 +- .../hooks/useField/useField.types.ts | 16 +- .../src/internals/hooks/useFieldOwnerState.ts | 2 +- .../hooks/useMobilePicker/useMobilePicker.tsx | 47 +- .../useMobilePicker/useMobilePicker.types.ts | 30 +- .../internals/hooks/usePicker/usePicker.ts | 1 - .../hooks/usePicker/usePicker.types.ts | 2 - .../hooks/usePicker/usePickerProvider.ts | 17 + .../src/internals/hooks/useUtils.ts | 9 +- .../x-date-pickers/src/internals/index.ts | 12 +- .../src/internals/models/fields.ts | 21 +- ...nvertFieldResponseIntoMuiTextFieldProps.ts | 27 - .../locales/utils/getPickersLocalization.ts | 16 - .../src/managers/useDateManager.ts | 4 + .../src/managers/useDateTimeManager.ts | 4 + .../src/managers/useTimeManager.ts | 4 + packages/x-date-pickers/src/models/fields.ts | 20 +- packages/x-date-pickers/src/models/manager.ts | 19 +- test/e2e/index.test.ts | 17 +- .../pickers/describePicker/describePicker.tsx | 6 +- .../describeValue/describeValue.types.ts | 1 + .../testControlledUnControlled.tsx | 6 +- test/utils/pickers/misc.ts | 2 +- test/utils/pickers/openPicker.ts | 8 - 152 files changed, 1859 insertions(+), 1197 deletions(-) create mode 100644 docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.js create mode 100644 docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx create mode 100644 docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx.preview create mode 100644 docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.js create mode 100644 docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx create mode 100644 docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx.preview delete mode 100644 packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/shared.ts create mode 100644 packages/x-date-pickers/src/internals/components/PickerFieldUI.tsx delete mode 100644 packages/x-date-pickers/src/internals/utils/convertFieldResponseIntoMuiTextFieldProps.ts diff --git a/docs/data/date-pickers/base-concepts/base-concepts.md b/docs/data/date-pickers/base-concepts/base-concepts.md index ca61ae0cbe0f0..37ee4a4c2c584 100644 --- a/docs/data/date-pickers/base-concepts/base-concepts.md +++ b/docs/data/date-pickers/base-concepts/base-concepts.md @@ -113,10 +113,10 @@ Each _Picker_ is available in a responsive, desktop and mobile variant: - The responsive component (for example `DatePicker`) which renders the desktop component or the mobile one depending on the device it runs on. - The desktop component (for example `DesktopDatePicker`) which works best for mouse devices and large screens. - It renders the views inside a popover and allows editing values directly inside the field. + It renders the views inside a popover and a field for keyboard editing. - The mobile component (for example `MobileDatePicker`) which works best for touch devices and small screens. - It renders the view inside a modal and does not allow editing values directly inside the field. + It renders the view inside a modal and a field for keyboard editing. {{"demo": "ResponsivePickers.js"}} diff --git a/docs/data/date-pickers/calendar-systems/AdapterHijri.js b/docs/data/date-pickers/calendar-systems/AdapterHijri.js index 3260e904fba9b..fb8974424621a 100644 --- a/docs/data/date-pickers/calendar-systems/AdapterHijri.js +++ b/docs/data/date-pickers/calendar-systems/AdapterHijri.js @@ -24,16 +24,7 @@ const cacheRtl = createCache({ function ButtonDateTimeField(props) { const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); - const { - InputProps, - slotProps, - slots, - ownerState, - label, - focused, - name, - ...other - } = forwardedProps; + const { ownerState, label, focused, name, ...other } = forwardedProps; const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -54,7 +45,7 @@ function ButtonDateTimeField(props) { {...other} variant="outlined" color={hasValidationError ? 'error' : 'primary'} - ref={InputProps?.ref} + ref={pickerContext.triggerRef} onClick={() => pickerContext.setOpen((prev) => !prev)} > {label ? `${label}: ${valueStr}` : valueStr} diff --git a/docs/data/date-pickers/calendar-systems/AdapterHijri.tsx b/docs/data/date-pickers/calendar-systems/AdapterHijri.tsx index 39689b23af0fc..fc6da7e2e11bb 100644 --- a/docs/data/date-pickers/calendar-systems/AdapterHijri.tsx +++ b/docs/data/date-pickers/calendar-systems/AdapterHijri.tsx @@ -28,16 +28,7 @@ const cacheRtl = createCache({ function ButtonDateTimeField(props: DateTimePickerFieldProps) { const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); - const { - InputProps, - slotProps, - slots, - ownerState, - label, - focused, - name, - ...other - } = forwardedProps; + const { ownerState, label, focused, name, ...other } = forwardedProps; const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -58,7 +49,7 @@ function ButtonDateTimeField(props: DateTimePickerFieldProps) { {...other} variant="outlined" color={hasValidationError ? 'error' : 'primary'} - ref={InputProps?.ref} + ref={pickerContext.triggerRef} onClick={() => pickerContext.setOpen((prev) => !prev)} > {label ? `${label}: ${valueStr}` : valueStr} diff --git a/docs/data/date-pickers/custom-field/BrowserV7Field.js b/docs/data/date-pickers/custom-field/BrowserV7Field.js index 11f5b168ae3fc..f4a7c867b747a 100644 --- a/docs/data/date-pickers/custom-field/BrowserV7Field.js +++ b/docs/data/date-pickers/custom-field/BrowserV7Field.js @@ -1,11 +1,14 @@ import * as React from 'react'; import useForkRef from '@mui/utils/useForkRef'; import { styled } from '@mui/material/styles'; +import IconButton from '@mui/material/IconButton'; +import { CalendarIcon } from '@mui/x-date-pickers/icons'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { unstable_useDateField as useDateField } from '@mui/x-date-pickers/DateField'; import { Unstable_PickersSectionList as PickersSectionList } from '@mui/x-date-pickers/PickersSectionList'; +import { usePickerContext } from '@mui/x-date-pickers/hooks'; const BrowserFieldRoot = styled('div', { name: 'BrowserField', slot: 'Root' })({ display: 'flex', @@ -41,6 +44,8 @@ const BrowserDateField = React.forwardRef((props, ref) => { onInput, onPaste, onKeyDown, + // Should be passed to the button that opens the picker + openPickerAriaLabel, // Can be passed to a hidden element onChange, value, @@ -55,16 +60,15 @@ const BrowserDateField = React.forwardRef((props, ref) => { readOnly, focused, error, - InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, // The rest can be passed to the root element ...other } = fieldResponse; - const handleRef = useForkRef(InputPropsRef, ref); + const pickerContext = usePickerContext(); + const handleRef = useForkRef(pickerContext.triggerRef, ref); return ( - {startAdornment} { onKeyDown={onKeyDown} /> - {endAdornment} + pickerContext.setOpen((prev) => !prev)} + sx={{ marginLeft: 1.5 }} + aria-label={openPickerAriaLabel} + > + + ); }); diff --git a/docs/data/date-pickers/custom-field/BrowserV7Field.tsx b/docs/data/date-pickers/custom-field/BrowserV7Field.tsx index a218b603030fb..75545b14ce8cc 100644 --- a/docs/data/date-pickers/custom-field/BrowserV7Field.tsx +++ b/docs/data/date-pickers/custom-field/BrowserV7Field.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import useForkRef from '@mui/utils/useForkRef'; import { styled } from '@mui/material/styles'; +import IconButton from '@mui/material/IconButton'; +import { CalendarIcon } from '@mui/x-date-pickers/icons'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { @@ -10,6 +12,7 @@ import { } from '@mui/x-date-pickers/DatePicker'; import { unstable_useDateField as useDateField } from '@mui/x-date-pickers/DateField'; import { Unstable_PickersSectionList as PickersSectionList } from '@mui/x-date-pickers/PickersSectionList'; +import { usePickerContext } from '@mui/x-date-pickers/hooks'; const BrowserFieldRoot = styled('div', { name: 'BrowserField', slot: 'Root' })({ display: 'flex', @@ -48,6 +51,9 @@ const BrowserDateField = React.forwardRef( onPaste, onKeyDown, + // Should be passed to the button that opens the picker + openPickerAriaLabel, + // Can be passed to a hidden element onChange, value, @@ -66,17 +72,15 @@ const BrowserDateField = React.forwardRef( focused, error, - InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, - // The rest can be passed to the root element ...other } = fieldResponse; - const handleRef = useForkRef(InputPropsRef, ref); + const pickerContext = usePickerContext(); + const handleRef = useForkRef(pickerContext.triggerRef, ref); return ( - {startAdornment} - {endAdornment} + pickerContext.setOpen((prev) => !prev)} + sx={{ marginLeft: 1.5 }} + aria-label={openPickerAriaLabel} + > + + ); }, diff --git a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js index adca0c631ed59..7743be94934f5 100644 --- a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js +++ b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js @@ -94,13 +94,13 @@ const BrowserMultiInputDateRangeField = React.forwardRef((props, ref) => { const startTextFieldProps = useSlotProps({ elementType: 'input', externalSlotProps: slotProps?.textField, - ownerState: { ...props, position: 'start' }, + ownerState: { position: 'start' }, }); const endTextFieldProps = useSlotProps({ elementType: 'input', externalSlotProps: slotProps?.textField, - ownerState: { ...props, position: 'end' }, + ownerState: { position: 'end' }, }); const fieldResponse = useMultiInputDateRangeField({ @@ -119,6 +119,21 @@ const BrowserMultiInputDateRangeField = React.forwardRef((props, ref) => { unstableEndFieldRef, }); + const { + // The multi input range field do not support clearable and onClear + onClear: onClearStartDate, + clearable: isStartDateClearable, + openPickerAriaLabel: openPickerStartDateAriaLabel, + ...startDateProps + } = fieldResponse.startDate; + const { + // The multi input range field do not support clearable and onClear + onClear: onClearEndDate, + clearable: isEndDateClearable, + openPickerAriaLabel: openPickerEndDateAriaLabel, + ...endDateProps + } = fieldResponse.endDate; + return ( { overflow="auto" className={className} > - + - + ); }); diff --git a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx index 7477ed13aaa04..b5c605dc0ac8f 100644 --- a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx +++ b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx @@ -106,7 +106,11 @@ interface BrowserMultiInputDateRangeFieldProps DateRangePickerFieldProps, 'unstableFieldRef' | 'clearable' | 'onClear' >, - MultiInputFieldRefs {} + MultiInputFieldRefs { + slotProps: { + textField: any; + }; +} type BrowserMultiInputDateRangeFieldComponent = (( props: BrowserMultiInputDateRangeFieldProps & React.RefAttributes, @@ -130,13 +134,13 @@ const BrowserMultiInputDateRangeField = React.forwardRef( const startTextFieldProps = useSlotProps({ elementType: 'input', externalSlotProps: slotProps?.textField, - ownerState: { ...props, position: 'start' }, + ownerState: { position: 'start' } as any, }) as MultiInputFieldSlotTextFieldProps; const endTextFieldProps = useSlotProps({ elementType: 'input', externalSlotProps: slotProps?.textField, - ownerState: { ...props, position: 'end' }, + ownerState: { position: 'end' } as any, }) as MultiInputFieldSlotTextFieldProps; const fieldResponse = useMultiInputDateRangeField< @@ -158,6 +162,21 @@ const BrowserMultiInputDateRangeField = React.forwardRef( unstableEndFieldRef, }); + const { + // The multi input range field do not support clearable and onClear + onClear: onClearStartDate, + clearable: isStartDateClearable, + openPickerAriaLabel: openPickerStartDateAriaLabel, + ...startDateProps + } = fieldResponse.startDate; + const { + // The multi input range field do not support clearable and onClear + onClear: onClearEndDate, + clearable: isEndDateClearable, + openPickerAriaLabel: openPickerEndDateAriaLabel, + ...endDateProps + } = fieldResponse.endDate; + return ( - + - + ); }, diff --git a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js index ae55770f60785..5c2fdcc30f448 100644 --- a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js +++ b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js @@ -59,13 +59,12 @@ const BrowserSingleInputDateRangeField = React.forwardRef((props, ref) => { readOnly, focused, error, - InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, // The rest can be passed to the root element ...other } = fieldResponse; const pickerContext = usePickerContext(); - const handleRef = useForkRef(InputPropsRef, ref); + const handleRef = useForkRef(pickerContext.triggerRef, ref); return ( { minWidth: 300, }} > - {startAdornment} { onKeyDown={onKeyDown} /> - {endAdornment} pickerContext.setOpen((prev) => !prev)}> diff --git a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx index 68f5f4c21737b..bcb591ba663af 100644 --- a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx +++ b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx @@ -77,14 +77,12 @@ const BrowserSingleInputDateRangeField = React.forwardRef( focused, error, - InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {}, - // The rest can be passed to the root element ...other } = fieldResponse; const pickerContext = usePickerContext(); - const handleRef = useForkRef(InputPropsRef, ref); + const handleRef = useForkRef(pickerContext.triggerRef, ref); return ( - {startAdornment} - {endAdornment} pickerContext.setOpen((prev) => !prev)}> diff --git a/docs/data/date-pickers/custom-field/JoyV6Field.js b/docs/data/date-pickers/custom-field/JoyV6Field.js index e445dc0c0fb50..6efa50f33ba40 100644 --- a/docs/data/date-pickers/custom-field/JoyV6Field.js +++ b/docs/data/date-pickers/custom-field/JoyV6Field.js @@ -11,12 +11,20 @@ import { THEME_ID, } from '@mui/joy/styles'; import Input from '@mui/joy/Input'; +import IconButton from '@mui/joy/IconButton'; import FormControl from '@mui/joy/FormControl'; import FormLabel from '@mui/joy/FormLabel'; +import { createSvgIcon } from '@mui/joy/utils'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { unstable_useDateField as useDateField } from '@mui/x-date-pickers/DateField'; +import { usePickerContext } from '@mui/x-date-pickers/hooks'; + +const CalendarIcon = createSvgIcon( + , + 'Calendar', +); const joyTheme = extendJoyTheme(); @@ -26,27 +34,40 @@ const JoyDateField = React.forwardRef((props, ref) => { const { // Should be ignored enableAccessibleFieldDOMStructure, + // Should be passed to the button that opens the picker + openPickerAriaLabel, // Can be passed to the button that clears the value onClear, clearable, - disabled, - id, + // Can be used to render a custom label label, - InputProps: { ref: containerRef, startAdornment, endAdornment } = {}, + // Can be used to style the component + disabled, + readOnly, + focused, + error, inputRef, - slots, - slotProps, + // The rest can be passed to the root element + id, ...other } = fieldResponse; + const pickerContext = usePickerContext(); + return ( {label} pickerContext.setOpen((prev) => !prev)} + aria-label={openPickerAriaLabel} + > + + + } slotProps={{ input: { ref: inputRef }, }} diff --git a/docs/data/date-pickers/custom-field/JoyV6Field.tsx b/docs/data/date-pickers/custom-field/JoyV6Field.tsx index 8cddb46983906..97d8c85b4be5d 100644 --- a/docs/data/date-pickers/custom-field/JoyV6Field.tsx +++ b/docs/data/date-pickers/custom-field/JoyV6Field.tsx @@ -11,8 +11,10 @@ import { THEME_ID, } from '@mui/joy/styles'; import Input from '@mui/joy/Input'; +import IconButton from '@mui/joy/IconButton'; import FormControl from '@mui/joy/FormControl'; import FormLabel from '@mui/joy/FormLabel'; +import { createSvgIcon } from '@mui/joy/utils'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { @@ -21,6 +23,12 @@ import { DatePickerProps, } from '@mui/x-date-pickers/DatePicker'; import { unstable_useDateField as useDateField } from '@mui/x-date-pickers/DateField'; +import { usePickerContext } from '@mui/x-date-pickers/hooks'; + +const CalendarIcon = createSvgIcon( + , + 'Calendar', +); const joyTheme = extendJoyTheme(); @@ -32,28 +40,44 @@ const JoyDateField = React.forwardRef( // Should be ignored enableAccessibleFieldDOMStructure, + // Should be passed to the button that opens the picker + openPickerAriaLabel, + // Can be passed to the button that clears the value onClear, clearable, - disabled, - id, + // Can be used to render a custom label label, - InputProps: { ref: containerRef, startAdornment, endAdornment } = {}, + + // Can be used to style the component + disabled, + readOnly, + focused, + error, inputRef, - slots, - slotProps, + + // The rest can be passed to the root element + id, ...other } = fieldResponse; + const pickerContext = usePickerContext(); + return ( {label} pickerContext.setOpen((prev) => !prev)} + aria-label={openPickerAriaLabel} + > + + + } slotProps={{ input: { ref: inputRef }, }} diff --git a/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js index af4a282bd9b6f..6adab583bfa13 100644 --- a/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js +++ b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js @@ -118,13 +118,13 @@ const JoyMultiInputDateRangeField = React.forwardRef((props, ref) => { const startTextFieldProps = useSlotProps({ elementType: FormControl, externalSlotProps: slotProps?.textField, - ownerState: { ...props, position: 'start' }, + ownerState: { position: 'start' }, }); const endTextFieldProps = useSlotProps({ elementType: FormControl, externalSlotProps: slotProps?.textField, - ownerState: { ...props, position: 'end' }, + ownerState: { position: 'end' }, }); const fieldResponse = useMultiInputDateRangeField({ @@ -143,11 +143,26 @@ const JoyMultiInputDateRangeField = React.forwardRef((props, ref) => { unstableEndFieldRef, }); + const { + // The multi input range field do not support clearable, onClear and openPickerAriaLabel + onClear: onClearStartDate, + clearable: isStartDateClearable, + openPickerAriaLabel: openPickerStartDateAriaLabel, + ...startDateProps + } = fieldResponse.startDate; + const { + // The multi input range field do not support clearable, onClear and openPickerAriaLabel + onClear: onClearEndDate, + clearable: isEndDateClearable, + openPickerAriaLabel: openPickerEndDateAriaLabel, + ...endDateProps + } = fieldResponse.endDate; + return ( - + - + ); }); diff --git a/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx index f7d9fa0bd3e29..9f5cbc60eff72 100644 --- a/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx +++ b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx @@ -132,7 +132,11 @@ interface JoyMultiInputDateRangeFieldProps DateRangePickerFieldProps, 'unstableFieldRef' | 'clearable' | 'onClear' >, - MultiInputFieldRefs {} + MultiInputFieldRefs { + slotProps: { + textField: any; + }; +} type JoyMultiInputDateRangeFieldComponent = (( props: JoyMultiInputDateRangeFieldProps & React.RefAttributes, @@ -156,13 +160,13 @@ const JoyMultiInputDateRangeField = React.forwardRef( const startTextFieldProps = useSlotProps({ elementType: FormControl, externalSlotProps: slotProps?.textField, - ownerState: { ...props, position: 'start' }, + ownerState: { position: 'start' } as any, }) as MultiInputFieldSlotTextFieldProps; const endTextFieldProps = useSlotProps({ elementType: FormControl, externalSlotProps: slotProps?.textField, - ownerState: { ...props, position: 'end' }, + ownerState: { position: 'end' } as any, }) as MultiInputFieldSlotTextFieldProps; const fieldResponse = useMultiInputDateRangeField< @@ -184,11 +188,26 @@ const JoyMultiInputDateRangeField = React.forwardRef( unstableEndFieldRef, }); + const { + // The multi input range field do not support clearable, onClear and openPickerAriaLabel + onClear: onClearStartDate, + clearable: isStartDateClearable, + openPickerAriaLabel: openPickerStartDateAriaLabel, + ...startDateProps + } = fieldResponse.startDate; + const { + // The multi input range field do not support clearable, onClear and openPickerAriaLabel + onClear: onClearEndDate, + clearable: isEndDateClearable, + openPickerAriaLabel: openPickerEndDateAriaLabel, + ...endDateProps + } = fieldResponse.endDate; + return ( - + - + ); }, diff --git a/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js index 7e0c7f6ed10a7..eadea36d43b24 100644 --- a/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js +++ b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js @@ -19,7 +19,9 @@ 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'; -export const DateRangeIcon = createSvgIcon( +import { usePickerContext } from '@mui/x-date-pickers/hooks'; + +const DateRangeIcon = createSvgIcon( , 'DateRange', ); @@ -38,13 +40,12 @@ const JoySingleInputDateRangeField = React.forwardRef((props, ref) => { disabled, id, label, - InputProps: { ref: containerRef } = {}, inputRef, - slots, - slotProps, ...other } = fieldResponse; + const pickerContext = usePickerContext(); + return ( { > {label} } slotProps={{ diff --git a/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx index 07b8927cc1820..31da6b6756efe 100644 --- a/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx +++ b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx @@ -23,8 +23,9 @@ import { } from '@mui/x-date-pickers-pro/DateRangePicker'; import { unstable_useSingleInputDateRangeField as useSingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; import { FieldType } from '@mui/x-date-pickers-pro/models'; +import { usePickerContext } from '@mui/x-date-pickers/hooks'; -export const DateRangeIcon = createSvgIcon( +const DateRangeIcon = createSvgIcon( , 'DateRange', ); @@ -50,13 +51,12 @@ const JoySingleInputDateRangeField = React.forwardRef( disabled, id, label, - InputProps: { ref: containerRef } = {}, inputRef, - slots, - slotProps, ...other } = fieldResponse; + const pickerContext = usePickerContext(); + return ( {label} } slotProps={{ diff --git a/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.js index 32bc8082bad18..324a11699329c 100644 --- a/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.js +++ b/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.js @@ -1,8 +1,9 @@ import * as React from 'react'; import dayjs from 'dayjs'; import Autocomplete from '@mui/material/Autocomplete'; +import IconButton from '@mui/material/IconButton'; +import { CalendarIcon } from '@mui/x-date-pickers/icons'; import TextField from '@mui/material/TextField'; -import Stack from '@mui/material/Stack'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; @@ -13,18 +14,16 @@ function AutocompleteField(props) { const { forwardedProps, internalProps } = useSplitFieldProps(props, 'date'); const { timezone, value, setValue } = usePickerContext(); const { - InputProps, - slotProps, - slots, ownerState, label, focused, name, options = [], - inputProps, ...other } = forwardedProps; + const pickerContext = usePickerContext(); + const { hasValidationError, getValidationErrorForNewValue } = useValidation({ validator: validateDate, value, @@ -32,50 +31,38 @@ function AutocompleteField(props) { props: internalProps, }); - const mergeAdornments = (...adornments) => { - const nonNullAdornments = adornments.filter((el) => el != null); - if (nonNullAdornments.length === 0) { - return null; - } - - if (nonNullAdornments.length === 1) { - return nonNullAdornments[0]; - } - - return ( - - {nonNullAdornments.map((adornment, index) => ( - {adornment} - ))} - - ); - }; - return ( ( - - )} + renderInput={(params) => { + const endAdornment = params.InputProps.endAdornment; + return ( + + pickerContext.setOpen((prev) => !prev)} + size="small" + > + + + {endAdornment.props.children} + + ), + }), + }} + /> + ); + }} getOptionLabel={(option) => { if (!dayjs.isDayjs(option)) { return ''; diff --git a/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.tsx index 43870edeb6e9a..e4884ca5b144d 100644 --- a/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.tsx +++ b/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import dayjs, { Dayjs } from 'dayjs'; import Autocomplete from '@mui/material/Autocomplete'; +import IconButton from '@mui/material/IconButton'; +import { CalendarIcon } from '@mui/x-date-pickers/icons'; import TextField from '@mui/material/TextField'; -import Stack from '@mui/material/Stack'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { @@ -24,18 +25,16 @@ function AutocompleteField(props: AutocompleteFieldProps) { const { forwardedProps, internalProps } = useSplitFieldProps(props, 'date'); const { timezone, value, setValue } = usePickerContext(); const { - InputProps, - slotProps, - slots, ownerState, label, focused, name, options = [], - inputProps, ...other } = forwardedProps; + const pickerContext = usePickerContext(); + const { hasValidationError, getValidationErrorForNewValue } = useValidation({ validator: validateDate, value, @@ -43,50 +42,39 @@ function AutocompleteField(props: AutocompleteFieldProps) { props: internalProps, }); - const mergeAdornments = (...adornments: React.ReactNode[]) => { - const nonNullAdornments = adornments.filter((el) => el != null); - if (nonNullAdornments.length === 0) { - return null; - } - - if (nonNullAdornments.length === 1) { - return nonNullAdornments[0]; - } - - return ( - - {nonNullAdornments.map((adornment, index) => ( - {adornment} - ))} - - ); - }; - return ( ( - - )} + renderInput={(params) => { + const endAdornment = params.InputProps + .endAdornment as React.ReactElement; + return ( + + pickerContext.setOpen((prev) => !prev)} + size="small" + > + + + {endAdornment.props.children} + + ), + }), + }} + /> + ); + }} getOptionLabel={(option) => { if (!dayjs.isDayjs(option)) { return ''; diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js index e9ae897f8f41a..d6690a9fee09a 100644 --- a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js @@ -12,16 +12,7 @@ import { function ButtonDateField(props) { const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); - const { - InputProps, - slotProps, - slots, - ownerState, - label, - focused, - name, - ...other - } = forwardedProps; + const { ownerState, label, focused, name, ...other } = forwardedProps; const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -42,7 +33,7 @@ function ButtonDateField(props) { {...other} variant="outlined" color={hasValidationError ? 'error' : 'primary'} - ref={InputProps?.ref} + ref={pickerContext.triggerRef} onClick={() => pickerContext.setOpen((prev) => !prev)} > {label ? `${label}: ${valueStr}` : valueStr} diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx index 28793f7cad3c7..56af8ea31b4dc 100644 --- a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx @@ -16,16 +16,7 @@ import { function ButtonDateField(props: DatePickerFieldProps) { const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); - const { - InputProps, - slotProps, - slots, - ownerState, - label, - focused, - name, - ...other - } = forwardedProps; + const { ownerState, label, focused, name, ...other } = forwardedProps; const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -46,7 +37,7 @@ function ButtonDateField(props: DatePickerFieldProps) { {...other} variant="outlined" color={hasValidationError ? 'error' : 'primary'} - ref={InputProps?.ref} + ref={pickerContext.triggerRef} onClick={() => pickerContext.setOpen((prev) => !prev)} > {label ? `${label}: ${valueStr}` : valueStr} diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js index 201f32fa8344f..84c9ce3f03eb0 100644 --- a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js @@ -14,16 +14,7 @@ import { function ButtonDateRangeField(props) { const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); - const { - InputProps, - slotProps, - slots, - ownerState, - label, - focused, - name, - ...other - } = forwardedProps; + const { ownerState, label, focused, name, ...other } = forwardedProps; const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -45,7 +36,7 @@ function ButtonDateRangeField(props) { {...other} variant="outlined" color={hasValidationError ? 'error' : 'primary'} - ref={InputProps?.ref} + ref={pickerContext.triggerRef} onClick={() => pickerContext.setOpen((prev) => !prev)} > {label ? `${label}: ${formattedValue}` : formattedValue} diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx index cb66e4a2935a5..135ddbd46ec80 100644 --- a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx @@ -18,16 +18,7 @@ import { function ButtonDateRangeField(props: DateRangePickerFieldProps) { const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); - const { - InputProps, - slotProps, - slots, - ownerState, - label, - focused, - name, - ...other - } = forwardedProps; + const { ownerState, label, focused, name, ...other } = forwardedProps; const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -49,7 +40,7 @@ function ButtonDateRangeField(props: DateRangePickerFieldProps) { {...other} variant="outlined" color={hasValidationError ? 'error' : 'primary'} - ref={InputProps?.ref} + ref={pickerContext.triggerRef} onClick={() => pickerContext.setOpen((prev) => !prev)} > {label ? `${label}: ${formattedValue}` : formattedValue} diff --git a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js index bcae377944961..4ee50736a08b2 100644 --- a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js +++ b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js @@ -2,6 +2,8 @@ import * as React from 'react'; import dayjs from 'dayjs'; import { useRifm } from 'rifm'; import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import IconButton from '@mui/material/IconButton'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; @@ -11,6 +13,7 @@ import { usePickerContext, } from '@mui/x-date-pickers/hooks'; import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; +import { CalendarIcon } from '@mui/x-date-pickers/icons'; const MASK_USER_INPUT_SYMBOL = '_'; const ACCEPT_REGEX = /[\d]/gi; @@ -27,9 +30,7 @@ function getInputValueFromValue(value, format) { } function MaskedDateField(props) { - const { slots, slotProps, ...other } = props; - - const { forwardedProps, internalProps } = useSplitFieldProps(other, 'date'); + const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -134,6 +135,19 @@ function MaskedDateField(props) { error={hasValidationError} {...rifmProps} {...forwardedProps} + InputProps={{ + ref: pickerContext.triggerRef, + endAdornment: ( + + pickerContext.setOpen((prev) => !prev)} + edge="end" + > + + + + ), + }} /> ); } diff --git a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx index 30d42fcf110d3..7e61770e25ad6 100644 --- a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx +++ b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import dayjs, { Dayjs } from 'dayjs'; import { useRifm } from 'rifm'; import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import IconButton from '@mui/material/IconButton'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { @@ -15,6 +17,7 @@ import { usePickerContext, } from '@mui/x-date-pickers/hooks'; import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; +import { CalendarIcon } from '@mui/x-date-pickers/icons'; const MASK_USER_INPUT_SYMBOL = '_'; const ACCEPT_REGEX = /[\d]/gi; @@ -31,9 +34,7 @@ function getInputValueFromValue(value: Dayjs | null, format: string) { } function MaskedDateField(props: DatePickerFieldProps) { - const { slots, slotProps, ...other } = props; - - const { forwardedProps, internalProps } = useSplitFieldProps(other, 'date'); + const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -138,6 +139,19 @@ function MaskedDateField(props: DatePickerFieldProps) { error={hasValidationError} {...rifmProps} {...forwardedProps} + InputProps={{ + ref: pickerContext.triggerRef, + endAdornment: ( + + pickerContext.setOpen((prev) => !prev)} + edge="end" + > + + + + ), + }} /> ); } diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.js new file mode 100644 index 0000000000000..a386d83c03392 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.js @@ -0,0 +1,74 @@ +import * as React from 'react'; +import TextField from '@mui/material/TextField'; +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 { useValidation, validateDate } from '@mui/x-date-pickers/validation'; +import { + useSplitFieldProps, + useParsedFormat, + usePickerContext, +} from '@mui/x-date-pickers/hooks'; +import { CalendarIcon } from '@mui/x-date-pickers/icons'; +import { DateField } from '@mui/x-date-pickers/DateField'; + +function ReadOnlyDateField(props) { + const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); + + const pickerContext = usePickerContext(); + + const parsedFormat = useParsedFormat(); + const { hasValidationError } = useValidation({ + validator: validateDate, + value: pickerContext.value, + timezone: pickerContext.timezone, + props: internalProps, + }); + + return ( + , + sx: { cursor: 'pointer', '& *': { cursor: 'inherit' } }, + }} + error={hasValidationError} + onClick={() => pickerContext.setOpen((prev) => !prev)} + /> + ); +} + +function ReadOnlyOnMobileDateField(props) { + const pickerContext = usePickerContext(); + + if (pickerContext.variant === 'mobile') { + return ; + } + + return ; +} + +function ReadOnlyFieldDatePicker(props) { + return ( + + ); +} + +export default function MaterialDatePicker() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx new file mode 100644 index 0000000000000..3b3e0e22bdc96 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import TextField from '@mui/material/TextField'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { + DatePicker, + DatePickerProps, + DatePickerFieldProps, +} from '@mui/x-date-pickers/DatePicker'; +import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; +import { + useSplitFieldProps, + useParsedFormat, + usePickerContext, +} from '@mui/x-date-pickers/hooks'; +import { CalendarIcon } from '@mui/x-date-pickers/icons'; +import { DateField } from '@mui/x-date-pickers/DateField'; + +function ReadOnlyDateField(props: DatePickerFieldProps) { + const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); + + const pickerContext = usePickerContext(); + + const parsedFormat = useParsedFormat(); + const { hasValidationError } = useValidation({ + validator: validateDate, + value: pickerContext.value, + timezone: pickerContext.timezone, + props: internalProps, + }); + + return ( + , + sx: { cursor: 'pointer', '& *': { cursor: 'inherit' } }, + }} + error={hasValidationError} + onClick={() => pickerContext.setOpen((prev) => !prev)} + /> + ); +} + +function ReadOnlyOnMobileDateField(props: DatePickerFieldProps) { + const pickerContext = usePickerContext(); + + if (pickerContext.variant === 'mobile') { + return ; + } + + return ; +} + +function ReadOnlyFieldDatePicker(props: DatePickerProps) { + return ( + + ); +} + +export default function MaterialDatePicker() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx.preview b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx.preview new file mode 100644 index 0000000000000..e3842a12cb5d3 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js index 213c1df27df35..c1dbf31a52867 100644 --- a/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js +++ b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js @@ -13,7 +13,6 @@ import { CalendarIcon } from '@mui/x-date-pickers/icons'; function ReadOnlyDateField(props) { const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); - const { InputProps, slotProps, slots, ...other } = forwardedProps; const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -26,7 +25,7 @@ function ReadOnlyDateField(props) { return ( , sx: { cursor: 'pointer', '& *': { cursor: 'inherit' } }, diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx index 16f52863cc24a..ab5c6c38f0cc7 100644 --- a/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx +++ b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx @@ -17,7 +17,6 @@ import { CalendarIcon } from '@mui/x-date-pickers/icons'; function ReadOnlyDateField(props: DatePickerFieldProps) { const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); - const { InputProps, slotProps, slots, ...other } = forwardedProps; const pickerContext = usePickerContext(); const parsedFormat = useParsedFormat(); @@ -30,7 +29,7 @@ function ReadOnlyDateField(props: DatePickerFieldProps) { return ( , sx: { cursor: 'pointer', '& *': { cursor: 'inherit' } }, diff --git a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js index 3c6631b19ee00..24f16cc407460 100644 --- a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js +++ b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js @@ -12,9 +12,7 @@ import { import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; function CustomDateField(props) { - // TextField does not support slots and slotProps before `@mui/material` v6.0 - const { slots, slotProps, ...other } = props; - const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date'); + const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); const pickerContext = usePickerContext(); const placeholder = useParsedFormat(); diff --git a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx index 9d91655805fc5..9d5f1711c88c6 100644 --- a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx +++ b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx @@ -16,9 +16,7 @@ import { import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; function CustomDateField(props: DatePickerFieldProps) { - // TextField does not support slots and slotProps before `@mui/material` v6.0 - const { slots, slotProps, ...other } = props; - const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date'); + const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); const pickerContext = usePickerContext(); const placeholder = useParsedFormat(); diff --git a/docs/data/date-pickers/custom-field/custom-field.md b/docs/data/date-pickers/custom-field/custom-field.md index 44d13054ceb40..197857b20fc4f 100644 --- a/docs/data/date-pickers/custom-field/custom-field.md +++ b/docs/data/date-pickers/custom-field/custom-field.md @@ -144,6 +144,12 @@ but you still want the UI to look like a Text Field, you can replace the field w {{"demo": "behavior-read-only-text-field/MaterialDatePicker.js", "defaultCodeOpen": false}} +### Using a read-only Text Field on mobile + +If you want to keep the default behavior on desktop but have a read-only TextField on mobile, you can conditionally render the custom field presented in the previous section: + +{{"demo": "behavior-read-only-mobile-text-field/MaterialDatePicker.js", "defaultCodeOpen": false}} + ### Using a Button If you want users to select a value exclusively through the views @@ -269,11 +275,11 @@ return ( ``` :::success -The `forwardedProps` contain props like `slots`, `slotProps` and `sx` that are specific to MUI. -You can omit them if the component your are forwarding the props to does not support those concepts: +The `forwardedProps` contains the `sx` which is specific to MUI. +You can omit it if the component your are forwarding the props to does not support this concept: ```jsx -const { slots, slotProps, sx, ...other } = props; +const { sx, ...other } = props; const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date'); return ( diff --git a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.js b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.js index 5ecabf3b7b204..5331d61230248 100644 --- a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.js +++ b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.js @@ -16,7 +16,7 @@ export default function CustomPropsOpeningButton() { }, // Targets the `InputAdornment` component. inputAdornment: { - position: 'start', + component: 'span', }, }} /> diff --git a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx index 5ecabf3b7b204..5331d61230248 100644 --- a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx +++ b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx @@ -16,7 +16,7 @@ export default function CustomPropsOpeningButton() { }, // Targets the `InputAdornment` component. inputAdornment: { - position: 'start', + component: 'span', }, }} /> diff --git a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx.preview b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx.preview index 685e218097c64..1487b57d2a908 100644 --- a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx.preview +++ b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx.preview @@ -6,7 +6,7 @@ }, // Targets the `InputAdornment` component. inputAdornment: { - position: 'start', + component: 'span', }, }} /> \ No newline at end of file diff --git a/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.js b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.js new file mode 100644 index 0000000000000..04a1a2803acd9 --- /dev/null +++ b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.js @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; + +export default function StartEdgeOpeningButton() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx new file mode 100644 index 0000000000000..04a1a2803acd9 --- /dev/null +++ b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; + +export default function StartEdgeOpeningButton() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx.preview b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx.preview new file mode 100644 index 0000000000000..0a9731df02dd7 --- /dev/null +++ b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx.preview @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-opening-button/custom-opening-button.md b/docs/data/date-pickers/custom-opening-button/custom-opening-button.md index c0ef70d69eb9f..eb5061b66a5c7 100644 --- a/docs/data/date-pickers/custom-opening-button/custom-opening-button.md +++ b/docs/data/date-pickers/custom-opening-button/custom-opening-button.md @@ -39,6 +39,12 @@ If you want to track the opening of the picker, you should use the `onOpen` / `o ::: +## Render the opening button at the start of the input + +You can use the `openPickerButtonPosition` on the `field` slot to position the opening button at the start or the end of the input: + +{{"demo": "StartEdgeOpeningButton.js"}} + ## Add an icon next to the opening button If you want to add an icon next to the opening button, you can use the `inputAdornment` slot. diff --git a/docs/data/date-pickers/date-picker/date-picker.md b/docs/data/date-pickers/date-picker/date-picker.md index 8467168363613..f702279ebb37d 100644 --- a/docs/data/date-pickers/date-picker/date-picker.md +++ b/docs/data/date-pickers/date-picker/date-picker.md @@ -46,10 +46,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen The component is available in four variants: - The `DesktopDatePicker` component which works best for mouse devices and large screens. - It renders the views inside a popover and allows editing values directly inside the field. + It renders the views inside a popover and a field for keyboard editing. - The `MobileDatePicker` component which works best for touch devices and small screens. - It renders the view inside a modal and does not allow editing values directly inside the field. + It renders the view inside a modal and a field for keyboard editing. - The `DatePicker` component which renders `DesktopDatePicker` or `MobileDatePicker` depending on the device it runs on. @@ -125,12 +125,6 @@ You can enable the clearable behavior: See [Field components—Clearable behavior](/x/react-date-pickers/fields/#clearable-behavior) for more details. ::: -:::warning -The clearable prop is not supported yet by the mobile Picker variants. - -See discussion [in this GitHub issue](/~https://github.com/mui/mui-x/issues/10842#issuecomment-1951887408) for more information. -::: - ## Localization See the [Date format and localization](/x/react-date-pickers/adapters-locale/) and [Translated components](/x/react-date-pickers/localization/) documentation pages for more details. diff --git a/docs/data/date-pickers/date-range-picker/date-range-picker.md b/docs/data/date-pickers/date-range-picker/date-range-picker.md index 43e680df8d33a..227d9b971b183 100644 --- a/docs/data/date-pickers/date-range-picker/date-range-picker.md +++ b/docs/data/date-pickers/date-range-picker/date-range-picker.md @@ -46,10 +46,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen The component is available in four variants: - The `DesktopDateRangePicker` component which works best for mouse devices and large screens. - It renders the views inside a popover and allows editing values directly inside the field. + It renders the views inside a popover and a field for keyboard editing. - The `MobileDateRangePicker` component which works best for touch devices and small screens. - It renders the view inside a modal and does not allow editing values directly inside the field. + It renders the view inside a modal and does not allow editing values with the keyboard in the field. - The `DateRangePicker` component which renders `DesktopDateRangePicker` or `MobileDateRangePicker` depending on the device it runs on. diff --git a/docs/data/date-pickers/date-time-picker/date-time-picker.md b/docs/data/date-pickers/date-time-picker/date-time-picker.md index 075ec25dd1567..dcead399e2fc0 100644 --- a/docs/data/date-pickers/date-time-picker/date-time-picker.md +++ b/docs/data/date-pickers/date-time-picker/date-time-picker.md @@ -48,10 +48,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen The component is available in four variants: - The `DesktopDateTimePicker` component which works best for mouse devices and large screens. - It renders the views inside a popover and allows editing values directly inside the field. + It renders the views inside a popover and a field for keyboard editing. - The `MobileDateTimePicker` component which works best for touch devices and small screens. - It renders the view inside a modal and does not allow editing values directly inside the field. + It renders the view inside a modal and a field for keyboard editing. - The `DateTimePicker` component which renders `DesktopDateTimePicker` or `MobileDateTimePicker` depending on the device it runs on. diff --git a/docs/data/date-pickers/date-time-range-picker/date-time-range-picker.md b/docs/data/date-pickers/date-time-range-picker/date-time-range-picker.md index 92137b72ba818..3b94e397925fb 100644 --- a/docs/data/date-pickers/date-time-range-picker/date-time-range-picker.md +++ b/docs/data/date-pickers/date-time-range-picker/date-time-range-picker.md @@ -47,10 +47,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen The component is available in three variants: - The `DesktopDateTimeRangePicker` component which works best for mouse devices and large screens. - It renders the views inside a popover and allows editing values directly inside the field. + It renders the views inside a popover and a field for keyboard editing. - The `MobileDateTimeRangePicker` component which works best for touch devices and small screens. - It renders the view inside a modal and does not allow editing values directly inside the field. + It renders the view inside a modal and does not allow editing values with the keyboard in the field. - The `DateTimeRangePicker` component which renders `DesktopDateTimeRangePicker` or `MobileDateTimeRangePicker` depending on the device it runs on. diff --git a/docs/data/date-pickers/time-picker/time-picker.md b/docs/data/date-pickers/time-picker/time-picker.md index d6db6b9d53c99..3332371393684 100644 --- a/docs/data/date-pickers/time-picker/time-picker.md +++ b/docs/data/date-pickers/time-picker/time-picker.md @@ -47,10 +47,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen The component is available in four variants: - The `DesktopTimePicker` component which works best for mouse devices and large screens. - It renders the views inside a popover and allows editing values directly inside the field. + It renders the views inside a popover and a field for keyboard editing. - The `MobileTimePicker` component which works best for touch devices and small screens. - It renders the view inside a modal and does not allow editing values directly inside the field. + It renders the view inside a modal and a field for keyboard editing. - The `TimePicker` component which renders `DesktopTimePicker` or `MobileTimePicker` depending on the device it runs on. diff --git a/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md b/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md index e581a783ba8f8..d8b17b13012ec 100644 --- a/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md +++ b/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md @@ -262,6 +262,20 @@ const theme = createTheme({ }); ``` +### Field editing on mobile Pickers + +The field is now editable if rendered inside a mobile Picker. +Before v8, if rendered inside a mobile Picker, the field was read-only, and clicking anywhere on it would open the Picker. +The mobile and desktop Pickers now behave similarly: + +- clicking on the field allows editing the value with the keyboard +- clicking on the input adornment opens the Picker + +:::success +If you prefer the old behavior, you can create a custom field that renders a read-only Text Field on mobile. +See [Custom field—Using a read-only Text Field on mobile](/x/react-date-pickers/custom-field/#using-a-read-only-text-field-on-mobile) to learn more. +::: + ### Month Calendar To simplify the theme and class structure, the `` component has been moved inside the Month Calendar component. @@ -357,6 +371,82 @@ If the updated values do not fit your use case, you can [override them](/x/react ### Slot: `field` +- The component passed to the `field` slot no longer receives `InputProps` and `inputProps` props. + You now need to manually add the UI to open the picker using the `usePickerContext` hook: + + ```diff + import { unstable_useDateField } from '@mui/x-date-pickers/DateField'; + +import { usePickerContext } from '@mui/x-date-pickers/hooks'; + + function CustomField(props) { + + const pickerContext = usePickerContext(); + + return ( + + + pickerContext.setOpen((prev) => !prev)} + + edge="end" + + aria-label={fieldResponse.openPickerAriaLabel} + + > + + + + + + + + ), + + }} + /> + ); + } + ``` + + If you are extracting the `ref` from `InputProps` to pass it to another trigger component, you can replace it with `pickerContext.triggerRef`: + + ```diff + +import { usePickerContext } from '@mui/x-date-pickers/hooks'; + + function CustomField(props) { + + const pickerContext = usePickerContext(); + + return ( + + ); + } + ``` + + If you are using a custom editing behavior, instead of using the `openPickerAriaLabel` property returned by the `useXXXField` hooks, you can generate it manually: + + ```diff + +import { usePickerTranslations } from '@mui/x-date-pickers/hooks'; + + function CustomField(props) { + + const translations = usePickerTranslations(); + + const formattedValue = props.value?.isValid() ? value.format('ll') : null; + + const ariaLabel = translations.openDatePickerDialogue(formattedValue); + + return ( + + ); + } + ``` + - The component passed to the `field` slot no longer receives the `value`, `onChange`, `timezone`, `format` and `disabled` props. You can use the `usePickerContext` hook instead: @@ -418,6 +508,21 @@ If the updated values do not fit your use case, you can [override them](/x/react If you are using a hook like `useDateField`, you don't have to do anything, the value from the context are automatically applied. ::: +### Slot: `inputAdornment` + +- The `position` props passed to the `inputAdornment` slot props no longer sets the position of the opening button. + This allows defining the position of the opening and clear buttons independently. + You can use the `openPickerButtonPosition` prop instead: + + ```diff + + ``` + ### Slot: `layout` - The `` and `` components must now receive the `ownerState` returned by `usePickerLayout` instead of their props: diff --git a/docs/pages/x/api/date-pickers/date-field.json b/docs/pages/x/api/date-pickers/date-field.json index 6a53307f3dc31..90efc47fd6603 100644 --- a/docs/pages/x/api/date-pickers/date-field.json +++ b/docs/pages/x/api/date-pickers/date-field.json @@ -2,6 +2,10 @@ "props": { "autoFocus": { "type": { "name": "bool" }, "default": "false" }, "clearable": { "type": { "name": "bool" }, "default": "false" }, + "clearButtonPosition": { + "type": { "name": "enum", "description": "'end'
| 'start'" }, + "default": "'end'" + }, "color": { "type": { "name": "enum", @@ -61,6 +65,10 @@ "describedArgs": ["newValue"] } }, + "openPickerButtonPosition": { + "type": { "name": "enum", "description": "'end'
| 'start'" }, + "default": "'end'" + }, "readOnly": { "type": { "name": "bool" }, "default": "false" }, "referenceDate": { "type": { "name": "object" }, 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 8ddb6b01c5cc3..cadb7130bc609 100644 --- a/docs/pages/x/api/date-pickers/date-time-field.json +++ b/docs/pages/x/api/date-pickers/date-time-field.json @@ -3,6 +3,10 @@ "ampm": { "type": { "name": "bool" }, "default": "utils.is12HourCycleInCurrentLocale()" }, "autoFocus": { "type": { "name": "bool" }, "default": "false" }, "clearable": { "type": { "name": "bool" }, "default": "false" }, + "clearButtonPosition": { + "type": { "name": "enum", "description": "'end'
| 'start'" }, + "default": "'end'" + }, "color": { "type": { "name": "enum", @@ -68,6 +72,10 @@ "describedArgs": ["newValue"] } }, + "openPickerButtonPosition": { + "type": { "name": "enum", "description": "'end'
| 'start'" }, + "default": "'end'" + }, "readOnly": { "type": { "name": "bool" }, "default": "false" }, "referenceDate": { "type": { "name": "object" }, 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 840d432dd53a4..08b336b439cb0 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-picker.json @@ -204,6 +204,18 @@ "default": "PickersCalendarHeader", "class": null }, + { + "name": "clearButton", + "description": "Button to clear the value.", + "default": "IconButton", + "class": null + }, + { + "name": "clearIcon", + "description": "Icon to display inside the clear button.", + "default": "ClearIcon", + "class": null + }, { "name": "day", "description": "Custom component for day.\nCheck the [PickersDay](https://mui.com/x/api/date-pickers/pickers-day/) component.", @@ -221,6 +233,12 @@ "description": "Component used to enter the date with the keyboard.", "class": null }, + { + "name": "inputAdornment", + "description": "Component displayed on the start or end input adornment used to open the picker on desktop.", + "default": "InputAdornment", + "class": null + }, { "name": "layout", "description": "Custom component for wrapping the layout.\nIt wraps the toolbar, views, action bar, and shortcuts.", @@ -256,6 +274,17 @@ "default": "IconButton", "class": null }, + { + "name": "openPickerButton", + "description": "Button to open the picker on desktop.", + "default": "IconButton", + "class": null + }, + { + "name": "openPickerIcon", + "description": "Icon displayed in the open picker button on desktop.", + "class": null + }, { "name": "previousIconButton", "description": "Button allowing to switch to the left view.", @@ -288,8 +317,8 @@ }, { "name": "textField", - "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`.", + "description": "Form control with an input to render the value.", + "default": ", or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`.", "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 bfc1d0d67b717..5547ade99e31d 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 @@ -223,6 +223,18 @@ "default": "PickersCalendarHeader", "class": null }, + { + "name": "clearButton", + "description": "Button to clear the value.", + "default": "IconButton", + "class": null + }, + { + "name": "clearIcon", + "description": "Icon to display inside the clear button.", + "default": "ClearIcon", + "class": null + }, { "name": "day", "description": "Custom component for day.\nCheck the [PickersDay](https://mui.com/x/api/date-pickers/pickers-day/) component.", @@ -240,6 +252,12 @@ "description": "Component used to enter the date with the keyboard.", "class": null }, + { + "name": "inputAdornment", + "description": "Component displayed on the start or end input adornment used to open the picker on desktop.", + "default": "InputAdornment", + "class": null + }, { "name": "layout", "description": "Custom component for wrapping the layout.\nIt wraps the toolbar, views, action bar, and shortcuts.", @@ -275,6 +293,17 @@ "default": "IconButton", "class": null }, + { + "name": "openPickerButton", + "description": "Button to open the picker on desktop.", + "default": "IconButton", + "class": null + }, + { + "name": "openPickerIcon", + "description": "Icon displayed in the open picker button on desktop.", + "class": null + }, { "name": "previousIconButton", "description": "Button allowing to switch to the left view.", @@ -313,8 +342,8 @@ }, { "name": "textField", - "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`.", + "description": "Form control with an input to render the value.", + "default": ", or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`.", "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 42e778868e5eb..30f368b62370c 100644 --- a/docs/pages/x/api/date-pickers/mobile-time-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-time-picker.json @@ -140,6 +140,18 @@ "default": "PickersActionBar", "class": null }, + { + "name": "clearButton", + "description": "Button to clear the value.", + "default": "IconButton", + "class": null + }, + { + "name": "clearIcon", + "description": "Icon to display inside the clear button.", + "default": "ClearIcon", + "class": null + }, { "name": "dialog", "description": "Custom component for the dialog inside which the views are rendered on mobile.", @@ -151,6 +163,12 @@ "description": "Component used to enter the date with the keyboard.", "class": null }, + { + "name": "inputAdornment", + "description": "Component displayed on the start or end input adornment used to open the picker on desktop.", + "default": "InputAdornment", + "class": null + }, { "name": "layout", "description": "Custom component for wrapping the layout.\nIt wraps the toolbar, views, action bar, and shortcuts.", @@ -180,6 +198,17 @@ "default": "IconButton", "class": null }, + { + "name": "openPickerButton", + "description": "Button to open the picker on desktop.", + "default": "IconButton", + "class": null + }, + { + "name": "openPickerIcon", + "description": "Icon displayed in the open picker button on desktop.", + "class": null + }, { "name": "previousIconButton", "description": "Button allowing to switch to the left view.", @@ -200,8 +229,8 @@ }, { "name": "textField", - "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`.", + "description": "Form control with an input to render the value.", + "default": ", or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`.", "class": null }, { 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 179b58765222e..b12c8e6071cd9 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 @@ -2,6 +2,10 @@ "props": { "autoFocus": { "type": { "name": "bool" }, "default": "false" }, "clearable": { "type": { "name": "bool" }, "default": "false" }, + "clearButtonPosition": { + "type": { "name": "enum", "description": "'end'
| 'start'" }, + "default": "'end'" + }, "color": { "type": { "name": "enum", 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 483f9c0016493..119936cbddf19 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 @@ -3,6 +3,10 @@ "ampm": { "type": { "name": "bool" }, "default": "utils.is12HourCycleInCurrentLocale()" }, "autoFocus": { "type": { "name": "bool" }, "default": "false" }, "clearable": { "type": { "name": "bool" }, "default": "false" }, + "clearButtonPosition": { + "type": { "name": "enum", "description": "'end'
| 'start'" }, + "default": "'end'" + }, "color": { "type": { "name": "enum", 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 fbfcdcd2781ec..ceeded9aa6802 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 @@ -3,6 +3,10 @@ "ampm": { "type": { "name": "bool" }, "default": "utils.is12HourCycleInCurrentLocale()" }, "autoFocus": { "type": { "name": "bool" }, "default": "false" }, "clearable": { "type": { "name": "bool" }, "default": "false" }, + "clearButtonPosition": { + "type": { "name": "enum", "description": "'end'
| 'start'" }, + "default": "'end'" + }, "color": { "type": { "name": "enum", diff --git a/docs/pages/x/api/date-pickers/time-field.json b/docs/pages/x/api/date-pickers/time-field.json index fb99ace144ee2..3bb471d4c5f04 100644 --- a/docs/pages/x/api/date-pickers/time-field.json +++ b/docs/pages/x/api/date-pickers/time-field.json @@ -3,6 +3,10 @@ "ampm": { "type": { "name": "bool" }, "default": "utils.is12HourCycleInCurrentLocale()" }, "autoFocus": { "type": { "name": "bool" }, "default": "false" }, "clearable": { "type": { "name": "bool" }, "default": "false" }, + "clearButtonPosition": { + "type": { "name": "enum", "description": "'end'
| 'start'" }, + "default": "'end'" + }, "color": { "type": { "name": "enum", @@ -64,6 +68,10 @@ "describedArgs": ["newValue"] } }, + "openPickerButtonPosition": { + "type": { "name": "enum", "description": "'end'
| 'start'" }, + "default": "'end'" + }, "readOnly": { "type": { "name": "bool" }, "default": "false" }, "referenceDate": { "type": { "name": "object" }, diff --git a/docs/src/modules/components/overview/mainDemo/PickerButton.tsx b/docs/src/modules/components/overview/mainDemo/PickerButton.tsx index 3a823c2c492c1..e90418b6c09ee 100644 --- a/docs/src/modules/components/overview/mainDemo/PickerButton.tsx +++ b/docs/src/modules/components/overview/mainDemo/PickerButton.tsx @@ -9,8 +9,7 @@ import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; function ButtonDateField(props: DatePickerFieldProps) { const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); - const { InputProps, slotProps, slots, ownerState, label, focused, name, ...other } = - forwardedProps; + const { ownerState, label, focused, name, ...other } = forwardedProps; const pickerContext = usePickerContext(); @@ -36,7 +35,7 @@ function ButtonDateField(props: DatePickerFieldProps) { sx={{ minWidth: 'fit-content' }} fullWidth color={hasValidationError ? 'error' : 'primary'} - ref={InputProps?.ref} + ref={pickerContext.triggerRef} onClick={() => pickerContext.setOpen((prev) => !prev)} > {label ? `${label}: ${valueStr}` : valueStr} 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 08b247cab6671..6022893a1b8c9 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 @@ -7,6 +7,9 @@ "clearable": { "description": "If true, a clear button will be shown in the field allowing value clearing." }, + "clearButtonPosition": { + "description": "The position at which the clear button is placed. If the field is not clearable, the button is not rendered." + }, "color": { "description": "The color of the component. It supports both default and custom theme colors, which can be added as shown in the palette customization guide." }, @@ -78,6 +81,9 @@ "description": "Callback fired when the selected sections change.", "typeDescriptions": { "newValue": "The new selected sections." } }, + "openPickerButtonPosition": { + "description": "The position at which the opening button is placed. If there is no picker to open, the button is not rendered" + }, "readOnly": { "description": "If true, the component is read-only. When read-only, the value cannot be changed but the user can interact with the interface." }, 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 30b020a97b080..3a6bc00dd54fb 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 @@ -8,6 +8,9 @@ "clearable": { "description": "If true, a clear button will be shown in the field allowing value clearing." }, + "clearButtonPosition": { + "description": "The position at which the clear button is placed. If the field is not clearable, the button is not rendered." + }, "color": { "description": "The color of the component. It supports both default and custom theme colors, which can be added as shown in the palette customization guide." }, @@ -95,6 +98,9 @@ "description": "Callback fired when the selected sections change.", "typeDescriptions": { "newValue": "The new selected sections." } }, + "openPickerButtonPosition": { + "description": "The position at which the opening button is placed. If there is no picker to open, the button is not rendered" + }, "readOnly": { "description": "If true, the component is read-only. When read-only, the value cannot be changed but the user can interact with the interface." }, 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 d3bf52f7d9609..698da88d81492 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 @@ -170,21 +170,26 @@ "slotDescriptions": { "actionBar": "Custom component for the action bar, it is placed below the picker views.", "calendarHeader": "Custom component for calendar header. Check the PickersCalendarHeader component.", + "clearButton": "Button to clear the value.", + "clearIcon": "Icon to display inside the clear button.", "day": "Custom component for day. Check the PickersDay component.", "dialog": "Custom component for the dialog inside which the views are rendered on mobile.", "field": "Component used to enter the date with the keyboard.", + "inputAdornment": "Component displayed on the start or end input adornment used to open the picker on desktop.", "layout": "Custom component for wrapping the layout. It wraps the toolbar, views, action bar, and shortcuts.", "leftArrowIcon": "Icon displayed in the left view switch button.", "mobilePaper": "Custom component for the paper rendered inside the mobile picker's Dialog.", "mobileTransition": "Custom component for the mobile dialog Transition.", "monthButton": "Button displayed to render a single month in the month view.", "nextIconButton": "Button allowing to switch to the right view.", + "openPickerButton": "Button to open the picker on desktop.", + "openPickerIcon": "Icon displayed in the open picker button on desktop.", "previousIconButton": "Button allowing to switch to the left view.", "rightArrowIcon": "Icon displayed in the right view switch button.", "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.", + "textField": "Form control with an input to render the value.", "toolbar": "Custom component for the toolbar rendered above the views.", "yearButton": "Button displayed to render a single year in the year view." } 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 e08489562eeb6..661fad9c125c6 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 @@ -198,22 +198,27 @@ "slotDescriptions": { "actionBar": "Custom component for the action bar, it is placed below the picker views.", "calendarHeader": "Custom component for calendar header. Check the PickersCalendarHeader component.", + "clearButton": "Button to clear the value.", + "clearIcon": "Icon to display inside the clear button.", "day": "Custom component for day. Check the PickersDay component.", "dialog": "Custom component for the dialog inside which the views are rendered on mobile.", "field": "Component used to enter the date with the keyboard.", + "inputAdornment": "Component displayed on the start or end input adornment used to open the picker on desktop.", "layout": "Custom component for wrapping the layout. It wraps the toolbar, views, action bar, and shortcuts.", "leftArrowIcon": "Icon displayed in the left view switch button.", "mobilePaper": "Custom component for the paper rendered inside the mobile picker's Dialog.", "mobileTransition": "Custom component for the mobile dialog Transition.", "monthButton": "Button displayed to render a single month in the month view.", "nextIconButton": "Button allowing to switch to the right view.", + "openPickerButton": "Button to open the picker on desktop.", + "openPickerIcon": "Icon displayed in the open picker button on desktop.", "previousIconButton": "Button allowing to switch to the left view.", "rightArrowIcon": "Icon displayed in the right view switch button.", "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.", "tabs": "Tabs enabling toggling between date and time pickers.", - "textField": "Form control with an input to render the value inside the default field.", + "textField": "Form control with an input to render the value.", "toolbar": "Custom component for the toolbar rendered above the views.", "yearButton": "Button displayed to render a single year in the year view." } 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 8d76eacc2d628..10d1272f67d3d 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 @@ -129,17 +129,22 @@ "classDescriptions": {}, "slotDescriptions": { "actionBar": "Custom component for the action bar, it is placed below the picker views.", + "clearButton": "Button to clear the value.", + "clearIcon": "Icon to display inside the clear button.", "dialog": "Custom component for the dialog inside which the views are rendered on mobile.", "field": "Component used to enter the date with the keyboard.", + "inputAdornment": "Component displayed on the start or end input adornment used to open the picker on desktop.", "layout": "Custom component for wrapping the layout. It wraps the toolbar, views, action bar, and shortcuts.", "leftArrowIcon": "Icon displayed in the left view switch button.", "mobilePaper": "Custom component for the paper rendered inside the mobile picker's Dialog.", "mobileTransition": "Custom component for the mobile dialog Transition.", "nextIconButton": "Button allowing to switch to the right view.", + "openPickerButton": "Button to open the picker on desktop.", + "openPickerIcon": "Icon displayed in the open picker button on desktop.", "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.", + "textField": "Form control with an input to render the value.", "toolbar": "Custom component for the toolbar rendered above the views." } } 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 72e27d93359fe..03f4bf83d3a7a 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 @@ -7,6 +7,9 @@ "clearable": { "description": "If true, a clear button will be shown in the field allowing value clearing." }, + "clearButtonPosition": { + "description": "The position at which the clear button is placed. If the field is not clearable, the button is not rendered." + }, "color": { "description": "The color of the component. It supports both default and custom theme colors, which can be added as shown in the palette customization guide." }, 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 871afaaa25741..08f57b1fb609a 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 @@ -8,6 +8,9 @@ "clearable": { "description": "If true, a clear button will be shown in the field allowing value clearing." }, + "clearButtonPosition": { + "description": "The position at which the clear button is placed. If the field is not clearable, the button is not rendered." + }, "color": { "description": "The color of the component. It supports both default and custom theme colors, which can be added as shown in the palette customization guide." }, 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 11ab48673c0fa..32b0d8c393e7b 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 @@ -8,6 +8,9 @@ "clearable": { "description": "If true, a clear button will be shown in the field allowing value clearing." }, + "clearButtonPosition": { + "description": "The position at which the clear button is placed. If the field is not clearable, the button is not rendered." + }, "color": { "description": "The color of the component. It supports both default and custom theme colors, which can be added as shown in the palette customization guide." }, 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 376ab6e3e4225..9a6ba3f53aab4 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 @@ -8,6 +8,9 @@ "clearable": { "description": "If true, a clear button will be shown in the field allowing value clearing." }, + "clearButtonPosition": { + "description": "The position at which the clear button is placed. If the field is not clearable, the button is not rendered." + }, "color": { "description": "The color of the component. It supports both default and custom theme colors, which can be added as shown in the palette customization guide." }, @@ -87,6 +90,9 @@ "description": "Callback fired when the selected sections change.", "typeDescriptions": { "newValue": "The new selected sections." } }, + "openPickerButtonPosition": { + "description": "The position at which the opening button is placed. If there is no picker to open, the button is not rendered" + }, "readOnly": { "description": "If true, the component is read-only. When read-only, the value cannot be changed but the user can interact with the interface." }, 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 a79441429611e..2ad4f2155adf4 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 @@ -35,14 +35,14 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(screen.getByText('May 2019')).toBeVisible(); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'end' }); + openPicker({ type: 'date-range', initialFocus: 'end' }); expect(screen.getByText('October 2019')).toBeVisible(); // scroll back - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(screen.getByText('May 2019')).toBeVisible(); }); @@ -51,7 +51,7 @@ describe('', () => { , ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(screen.getByRole('tooltip')).toBeVisible(); }); @@ -167,7 +167,7 @@ describe('', () => { render(); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(onOpen.callCount).to.equal(1); expect(screen.getByRole('tooltip')).toBeVisible(); @@ -178,7 +178,7 @@ describe('', () => { render(); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'end' }); + openPicker({ type: 'date-range', initialFocus: 'end' }); expect(onOpen.callCount).to.equal(1); expect(screen.getByRole('tooltip')).toBeVisible(); @@ -235,7 +235,7 @@ describe('', () => { ); // Open the picker - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(onChange.callCount).to.equal(0); expect(onAccept.callCount).to.equal(0); expect(onClose.callCount).to.equal(0); @@ -277,7 +277,7 @@ describe('', () => { ); // Open the picker - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'end' }); + openPicker({ type: 'date-range', initialFocus: 'end' }); expect(onChange.callCount).to.equal(0); expect(onAccept.callCount).to.equal(0); expect(onClose.callCount).to.equal(0); @@ -310,7 +310,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'end' }); + openPicker({ type: 'date-range', initialFocus: 'end' }); // Change the end date fireEvent.click(getPickerDay('3')); @@ -337,7 +337,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Change the start date (already tested) fireEvent.click(getPickerDay('3')); @@ -364,7 +364,7 @@ describe('', () => { , ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Dismiss the picker const input = document.getElementById('test-id')!; @@ -402,7 +402,7 @@ describe('', () => { , ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Change the start date (already tested) fireEvent.click(getPickerDay('3')); @@ -458,7 +458,7 @@ describe('', () => { , ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(screen.getByRole('tooltip')).toBeVisible(); document.querySelector('#test')!.focus(); @@ -491,7 +491,7 @@ describe('', () => { , ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(screen.getByRole('tooltip')).toBeVisible(); // Change the start date (already tested) @@ -529,7 +529,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Clear the date fireEvent.click(screen.getByText(/clear/i)); @@ -555,7 +555,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Clear the date fireEvent.click(screen.getByText(/clear/i)); @@ -582,10 +582,10 @@ describe('', () => { ); // Open the picker (already tested) - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Switch to end date - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'end' }); + openPicker({ type: 'date-range', initialFocus: 'end' }); expect(onChange.callCount).to.equal(0); expect(onAccept.callCount).to.equal(0); expect(onClose.callCount).to.equal(0); @@ -606,10 +606,10 @@ describe('', () => { ); // Open the picker (already tested) - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'end' }); + openPicker({ type: 'date-range', initialFocus: 'end' }); // Switch to start date - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(onChange.callCount).to.equal(0); expect(onAccept.callCount).to.equal(0); expect(onClose.callCount).to.equal(0); @@ -620,7 +620,7 @@ describe('', () => { it('should respect the disablePast prop', () => { render(); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(getPickerDay('8')).to.have.attribute('disabled'); expect(getPickerDay('9')).to.have.attribute('disabled'); @@ -632,7 +632,7 @@ describe('', () => { it('should respect the disableFuture prop', () => { render(); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(getPickerDay('8')).not.to.have.attribute('disabled'); expect(getPickerDay('9')).not.to.have.attribute('disabled'); @@ -644,7 +644,7 @@ describe('', () => { it('should respect the minDate prop', () => { render(); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(getPickerDay('13')).to.have.attribute('disabled'); expect(getPickerDay('14')).to.have.attribute('disabled'); @@ -656,7 +656,7 @@ describe('', () => { it('should respect the maxDate prop', () => { render(); - openPicker({ type: 'date-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(getPickerDay('13')).not.to.have.attribute('disabled'); expect(getPickerDay('14')).not.to.have.attribute('disabled'); 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 f9255bdd65af6..0ece845e39397 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 @@ -28,6 +28,7 @@ describe(' - Describes', () => { clock, componentFamily: 'picker', views: ['day'], + variant: 'desktop', })); describeConformance(, () => ({ diff --git a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx index e8537c5cfc1d3..4dea6eb3e1865 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx @@ -20,7 +20,7 @@ describe('', () => { it('should allow to select range within the same day', () => { render(); - openPicker({ type: 'date-time-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-time-range', initialFocus: 'start' }); // select start date range fireEvent.click(screen.getByRole('gridcell', { name: '11' })); @@ -45,7 +45,7 @@ describe('', () => { , ); - openPicker({ type: 'date-time-range', variant: 'desktop', initialFocus: 'start' }); + openPicker({ type: 'date-time-range', initialFocus: 'start' }); fireEvent.click(screen.getByRole('gridcell', { name: '11' })); 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 83fee1c9814b9..83e2457bd23bd 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 @@ -44,7 +44,7 @@ describe('', () => { render(); - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(onOpen.callCount).to.equal(1); expect(screen.queryByRole('dialog')).toBeVisible(); @@ -55,7 +55,7 @@ describe('', () => { render(); - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'end' }); + openPicker({ type: 'date-range', initialFocus: 'end' }); expect(onOpen.callCount).to.equal(1); expect(screen.queryByRole('dialog')).toBeVisible(); @@ -80,7 +80,7 @@ describe('', () => { ); // Open the picker - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); expect(onChange.callCount).to.equal(0); expect(onAccept.callCount).to.equal(0); expect(onClose.callCount).to.equal(0); @@ -120,7 +120,7 @@ describe('', () => { ); // Open the picker - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'end' }); + openPicker({ type: 'date-range', initialFocus: 'end' }); expect(onChange.callCount).to.equal(0); expect(onAccept.callCount).to.equal(0); expect(onClose.callCount).to.equal(0); @@ -151,7 +151,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'end' }); + openPicker({ type: 'date-range', initialFocus: 'end' }); // Change the end date fireEvent.click(screen.getByRole('gridcell', { name: '3' })); @@ -181,7 +181,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Change the start date (already tested) fireEvent.click(screen.getByRole('gridcell', { name: '3' })); @@ -213,7 +213,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Change the start date (already tested) fireEvent.click(screen.getByRole('gridcell', { name: '3' })); @@ -246,7 +246,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Clear the date fireEvent.click(screen.getByText(/clear/i)); @@ -272,7 +272,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); // Clear the date fireEvent.click(screen.getByText(/clear/i)); 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 8304680152111..1b8a91aec129f 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 @@ -84,7 +84,7 @@ describe(' - Describes', () => { } if (!isOpened) { - openPicker({ type: 'date-range', variant: 'mobile', initialFocus: 'start' }); + openPicker({ type: 'date-range', initialFocus: 'start' }); } fireEvent.click( diff --git a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/tests/describes.MobileDateTimeRangePicker.test.tsx b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/tests/describes.MobileDateTimeRangePicker.test.tsx index 90a5dc4057ec1..fe4f71d11525d 100644 --- a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/tests/describes.MobileDateTimeRangePicker.test.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/tests/describes.MobileDateTimeRangePicker.test.tsx @@ -88,7 +88,7 @@ describe(' - Describes', () => { if (!isOpened) { openPicker({ type: 'date-time-range', - variant: 'mobile', + initialFocus: setEndDate ? 'end' : 'start', }); } diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx index cb1441aca093c..6d58b8a4bb59b 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx @@ -12,10 +12,7 @@ import { unstable_generateUtilityClass as generateUtilityClass, unstable_generateUtilityClasses as generateUtilityClasses, } from '@mui/utils'; -import { - convertFieldResponseIntoMuiTextFieldProps, - useFieldOwnerState, -} from '@mui/x-date-pickers/internals'; +import { cleanFieldResponse, useFieldOwnerState } from '@mui/x-date-pickers/internals'; import { useSplitFieldProps } from '@mui/x-date-pickers/hooks'; import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; import { @@ -163,8 +160,8 @@ const MultiInputDateRangeField = React.forwardRef(function MultiInputDateRangeFi unstableEndFieldRef, }); - const startDateProps = convertFieldResponseIntoMuiTextFieldProps(fieldResponse.startDate); - const endDateProps = convertFieldResponseIntoMuiTextFieldProps(fieldResponse.endDate); + const { textFieldProps: startDateProps } = cleanFieldResponse(fieldResponse.startDate); + const { textFieldProps: endDateProps } = cleanFieldResponse(fieldResponse.endDate); const TextField = slots?.textField ?? diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx index 26557e1ed3fb5..d74e569613ef4 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx @@ -12,10 +12,7 @@ import { unstable_generateUtilityClass as generateUtilityClass, unstable_generateUtilityClasses as generateUtilityClasses, } from '@mui/utils'; -import { - convertFieldResponseIntoMuiTextFieldProps, - useFieldOwnerState, -} from '@mui/x-date-pickers/internals'; +import { cleanFieldResponse, useFieldOwnerState } from '@mui/x-date-pickers/internals'; import { useSplitFieldProps } from '@mui/x-date-pickers/hooks'; import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; import { @@ -161,8 +158,8 @@ const MultiInputDateTimeRangeField = React.forwardRef(function MultiInputDateTim unstableEndFieldRef, }); - const startDateProps = convertFieldResponseIntoMuiTextFieldProps(fieldResponse.startDate); - const endDateProps = convertFieldResponseIntoMuiTextFieldProps(fieldResponse.endDate); + const { textFieldProps: startDateProps } = cleanFieldResponse(fieldResponse.startDate); + const { textFieldProps: endDateProps } = cleanFieldResponse(fieldResponse.endDate); const TextField = slots?.textField ?? diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx index d2d9bb8e6bf92..f71baf65aac82 100644 --- a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx @@ -12,10 +12,7 @@ import { unstable_generateUtilityClass as generateUtilityClass, unstable_generateUtilityClasses as generateUtilityClasses, } from '@mui/utils'; -import { - convertFieldResponseIntoMuiTextFieldProps, - useFieldOwnerState, -} from '@mui/x-date-pickers/internals'; +import { cleanFieldResponse, useFieldOwnerState } from '@mui/x-date-pickers/internals'; import { useSplitFieldProps } from '@mui/x-date-pickers/hooks'; import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; import { @@ -164,8 +161,8 @@ const MultiInputTimeRangeField = React.forwardRef(function MultiInputTimeRangeFi unstableEndFieldRef, }); - const startDateProps = convertFieldResponseIntoMuiTextFieldProps(fieldResponse.startDate); - const endDateProps = convertFieldResponseIntoMuiTextFieldProps(fieldResponse.endDate); + const { textFieldProps: startDateProps } = cleanFieldResponse(fieldResponse.startDate); + const { textFieldProps: endDateProps } = cleanFieldResponse(fieldResponse.endDate); const TextField = slots?.textField ?? diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx index 26edd8cc6ad0c..5ff8e081f95a2 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx @@ -1,16 +1,10 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import MuiTextField from '@mui/material/TextField'; import { useThemeProps } from '@mui/material/styles'; -import useSlotProps from '@mui/utils/useSlotProps'; import { refType } from '@mui/utils'; -import { useClearableField } from '@mui/x-date-pickers/hooks'; -import { - convertFieldResponseIntoMuiTextFieldProps, - useFieldOwnerState, -} from '@mui/x-date-pickers/internals'; -import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; +import { DateRangeIcon } from '@mui/x-date-pickers/icons'; +import { PickerFieldUI, useFieldTextFieldProps } from '@mui/x-date-pickers/internals'; import { SingleInputDateRangeFieldProps } from './SingleInputDateRangeField.types'; import { useSingleInputDateRangeField } from './useSingleInputDateRangeField'; import { FieldType } from '../models'; @@ -41,41 +35,29 @@ const SingleInputDateRangeField = React.forwardRef(function SingleInputDateRange name: 'MuiSingleInputDateRangeField', }); - const { slots, slotProps, InputProps, inputProps, ...other } = themeProps; + const { slots, slotProps, ...other } = themeProps; - const ownerState = useFieldOwnerState(themeProps); - - const textFieldProps = useSlotProps({ - elementType: PickersTextField, - externalSlotProps: slotProps?.textField, + const textFieldProps = useFieldTextFieldProps< + SingleInputDateRangeFieldProps + >({ + slotProps, + ref: inRef, externalForwardedProps: other, - ownerState, - 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< TEnableAccessibleFieldDOMStructure, typeof textFieldProps >(textFieldProps); - const convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); - - const processedFieldProps = useClearableField({ - ...convertedFieldResponse, - slots, - slotProps, - }); - - const TextField = - slots?.textField ?? - (fieldResponse.enableAccessibleFieldDOMStructure === false ? MuiTextField : PickersTextField); - return ; + return ( + + ); }) as DateRangeFieldComponent; SingleInputDateRangeField.fieldType = 'single-input'; @@ -96,6 +78,12 @@ SingleInputDateRangeField.propTypes = { * @default false */ clearable: PropTypes.bool, + /** + * The position at which the clear button is placed. + * If the field is not clearable, the button is not rendered. + * @default 'end' + */ + clearButtonPosition: PropTypes.oneOf(['end', 'start']), /** * The color of the component. * It supports both default and custom theme colors, which can be added as shown in the 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 027ffbfbdb1de..7828b1643c3c0 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts @@ -1,20 +1,16 @@ -import * as React from 'react'; -import { TextFieldProps } from '@mui/material/TextField'; -import { SlotComponentPropsFromProps } from '@mui/x-internals/types'; -import { PickerRangeValue, UseFieldInternalProps } from '@mui/x-date-pickers/internals'; -import { BuiltInFieldTextFieldProps, FieldOwnerState } from '@mui/x-date-pickers/models'; import { - ExportedUseClearableFieldProps, - UseClearableFieldSlots, - UseClearableFieldSlotProps, -} from '@mui/x-date-pickers/hooks'; -import { PickersTextFieldProps } from '@mui/x-date-pickers/PickersTextField'; + PickerRangeValue, + UseFieldInternalProps, + ExportedPickerFieldUIProps, + PickerFieldUISlots, + PickerFieldUISlotProps, +} from '@mui/x-date-pickers/internals'; +import { BuiltInFieldTextFieldProps } from '@mui/x-date-pickers/models'; import type { DateRangeValidationError, UseDateRangeFieldProps } from '../models'; export interface UseSingleInputDateRangeFieldProps< TEnableAccessibleFieldDOMStructure extends boolean, > extends UseDateRangeFieldProps, - ExportedUseClearableFieldProps, Pick< UseFieldInternalProps< PickerRangeValue, @@ -22,7 +18,9 @@ export interface UseSingleInputDateRangeFieldProps< DateRangeValidationError >, 'unstableFieldRef' - > {} + >, + // TODO v8: Remove once the range fields open with a button. + Omit {} export type SingleInputDateRangeFieldProps< TEnableAccessibleFieldDOMStructure extends boolean = true, @@ -43,18 +41,6 @@ export type SingleInputDateRangeFieldProps< slotProps?: SingleInputDateRangeFieldSlotProps; }; -export interface SingleInputDateRangeFieldSlots extends UseClearableFieldSlots { - /** - * Form control with an input to render the value. - * @default , or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`. - */ - textField?: React.ElementType; -} +export interface SingleInputDateRangeFieldSlots extends PickerFieldUISlots {} -export interface SingleInputDateRangeFieldSlotProps extends UseClearableFieldSlotProps { - textField?: SlotComponentPropsFromProps< - PickersTextFieldProps | TextFieldProps, - {}, - FieldOwnerState - >; -} +export interface SingleInputDateRangeFieldSlotProps extends PickerFieldUISlotProps {} diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts index 3148b6e40b37b..af4f32912bb52 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts @@ -33,5 +33,6 @@ export const useSingleInputDateRangeField = < fieldValueManager: manager.internal_fieldValueManager, validator: manager.validator, valueType: manager.valueType, + getOpenPickerButtonAriaLabel: manager.internal_getOpenPickerButtonAriaLabel, }); }; diff --git a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx index 74a4ea87c9550..f004f28093d36 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx @@ -1,16 +1,10 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import MuiTextField from '@mui/material/TextField'; -import { - convertFieldResponseIntoMuiTextFieldProps, - useFieldOwnerState, -} from '@mui/x-date-pickers/internals'; -import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; +import { DateRangeIcon } from '@mui/x-date-pickers/icons'; +import { PickerFieldUI, useFieldTextFieldProps } from '@mui/x-date-pickers/internals'; import { useThemeProps } from '@mui/material/styles'; import { refType } from '@mui/utils'; -import useSlotProps from '@mui/utils/useSlotProps'; -import { useClearableField } from '@mui/x-date-pickers/hooks'; import { SingleInputDateTimeRangeFieldProps } from './SingleInputDateTimeRangeField.types'; import { useSingleInputDateTimeRangeField } from './useSingleInputDateTimeRangeField'; import { FieldType } from '../models'; @@ -41,41 +35,29 @@ const SingleInputDateTimeRangeField = React.forwardRef(function SingleInputDateT name: 'MuiSingleInputDateTimeRangeField', }); - const { slots, slotProps, InputProps, inputProps, ...other } = themeProps; + const { slots, slotProps, ...other } = themeProps; - const ownerState = useFieldOwnerState(themeProps); - - const textFieldProps = useSlotProps({ - elementType: PickersTextField, - externalSlotProps: slotProps?.textField, + const textFieldProps = useFieldTextFieldProps< + SingleInputDateTimeRangeFieldProps + >({ + slotProps, + ref: inRef, externalForwardedProps: other, - ownerState, - 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< TEnableAccessibleFieldDOMStructure, typeof textFieldProps >(textFieldProps); - const convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); - - const processedFieldProps = useClearableField({ - ...convertedFieldResponse, - slots, - slotProps, - }); - - const TextField = - slots?.textField ?? - (fieldResponse.enableAccessibleFieldDOMStructure === false ? MuiTextField : PickersTextField); - return ; + return ( + + ); }) as DateRangeFieldComponent; SingleInputDateTimeRangeField.fieldType = 'single-input'; @@ -101,6 +83,12 @@ SingleInputDateTimeRangeField.propTypes = { * @default false */ clearable: PropTypes.bool, + /** + * The position at which the clear button is placed. + * If the field is not clearable, the button is not rendered. + * @default 'end' + */ + clearButtonPosition: PropTypes.oneOf(['end', 'start']), /** * The color of the component. * It supports both default and custom theme colors, which can be added as shown in the 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 9624ffa1c73e0..d7f51a40420b5 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.types.ts @@ -1,21 +1,17 @@ -import * as React from 'react'; -import { TextFieldProps } from '@mui/material/TextField'; -import { SlotComponentPropsFromProps } from '@mui/x-internals/types'; -import { PickersTextFieldProps } from '@mui/x-date-pickers/PickersTextField'; -import { PickerRangeValue, UseFieldInternalProps } from '@mui/x-date-pickers/internals'; -import { BuiltInFieldTextFieldProps, FieldOwnerState } from '@mui/x-date-pickers/models'; import { - ExportedUseClearableFieldProps, - UseClearableFieldSlots, - UseClearableFieldSlotProps, -} from '@mui/x-date-pickers/hooks'; + ExportedPickerFieldUIProps, + PickerFieldUISlots, + PickerFieldUISlotProps, + PickerRangeValue, + UseFieldInternalProps, +} from '@mui/x-date-pickers/internals'; +import { BuiltInFieldTextFieldProps } from '@mui/x-date-pickers/models'; import { UseDateTimeRangeFieldProps } from '../internals/models'; import { DateTimeRangeValidationError } from '../models'; export interface UseSingleInputDateTimeRangeFieldProps< TEnableAccessibleFieldDOMStructure extends boolean, > extends UseDateTimeRangeFieldProps, - ExportedUseClearableFieldProps, Pick< UseFieldInternalProps< PickerRangeValue, @@ -23,7 +19,9 @@ export interface UseSingleInputDateTimeRangeFieldProps< DateTimeRangeValidationError >, 'unstableFieldRef' - > {} + >, + // TODO v8: Remove once the range fields open with a button. + Omit {} export type SingleInputDateTimeRangeFieldProps< TEnableAccessibleFieldDOMStructure extends boolean = true, @@ -44,18 +42,6 @@ export type SingleInputDateTimeRangeFieldProps< slotProps?: SingleInputDateTimeRangeFieldSlotProps; }; -export interface SingleInputDateTimeRangeFieldSlots extends UseClearableFieldSlots { - /** - * Form control with an input to render the value. - * @default , or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`. - */ - textField?: React.ElementType; -} +export interface SingleInputDateTimeRangeFieldSlots extends PickerFieldUISlots {} -export interface SingleInputDateTimeRangeFieldSlotProps extends UseClearableFieldSlotProps { - textField?: SlotComponentPropsFromProps< - PickersTextFieldProps | TextFieldProps, - {}, - FieldOwnerState - >; -} +export interface SingleInputDateTimeRangeFieldSlotProps extends PickerFieldUISlotProps {} diff --git a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts index 12835555e9a57..c65e16c92ab8f 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts @@ -33,5 +33,6 @@ export const useSingleInputDateTimeRangeField = < fieldValueManager: manager.internal_fieldValueManager, validator: manager.validator, valueType: manager.valueType, + getOpenPickerButtonAriaLabel: manager.internal_getOpenPickerButtonAriaLabel, }); }; diff --git a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx index 509cf32d2daf6..a2c1fbe79cf76 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx @@ -1,15 +1,9 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import MuiTextField from '@mui/material/TextField'; -import { useClearableField } from '@mui/x-date-pickers/hooks'; -import { - convertFieldResponseIntoMuiTextFieldProps, - useFieldOwnerState, -} from '@mui/x-date-pickers/internals'; -import { PickersTextField } from '@mui/x-date-pickers/PickersTextField'; +import { ClockIcon } from '@mui/x-date-pickers/icons'; +import { PickerFieldUI, useFieldTextFieldProps } from '@mui/x-date-pickers/internals'; import { useThemeProps } from '@mui/material/styles'; -import useSlotProps from '@mui/utils/useSlotProps'; import { refType } from '@mui/utils'; import { SingleInputTimeRangeFieldProps } from './SingleInputTimeRangeField.types'; import { useSingleInputTimeRangeField } from './useSingleInputTimeRangeField'; @@ -41,41 +35,29 @@ const SingleInputTimeRangeField = React.forwardRef(function SingleInputTimeRange name: 'MuiSingleInputTimeRangeField', }); - const { slots, slotProps, InputProps, inputProps, ...other } = themeProps; + const { slots, slotProps, ...other } = themeProps; - const ownerState = useFieldOwnerState(themeProps); - - const textFieldProps = useSlotProps({ - elementType: PickersTextField, - externalSlotProps: slotProps?.textField, + const textFieldProps = useFieldTextFieldProps< + SingleInputTimeRangeFieldProps + >({ + slotProps, + ref: inRef, externalForwardedProps: other, - ownerState, - 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< TEnableAccessibleFieldDOMStructure, typeof textFieldProps >(textFieldProps); - const convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); - - const processedFieldProps = useClearableField({ - ...convertedFieldResponse, - slots, - slotProps, - }); - - const TextField = - slots?.textField ?? - (fieldResponse.enableAccessibleFieldDOMStructure === false ? MuiTextField : PickersTextField); - return ; + return ( + + ); }) as DateRangeFieldComponent; SingleInputTimeRangeField.fieldType = 'single-input'; @@ -101,6 +83,12 @@ SingleInputTimeRangeField.propTypes = { * @default false */ clearable: PropTypes.bool, + /** + * The position at which the clear button is placed. + * If the field is not clearable, the button is not rendered. + * @default 'end' + */ + clearButtonPosition: PropTypes.oneOf(['end', 'start']), /** * The color of the component. * It supports both default and custom theme colors, which can be added as shown in the 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 46b019e84e955..56fcb66f9b5f9 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.types.ts @@ -1,21 +1,17 @@ -import * as React from 'react'; -import type { TextFieldProps } from '@mui/material/TextField'; -import { PickerRangeValue, UseFieldInternalProps } from '@mui/x-date-pickers/internals'; -import { BuiltInFieldTextFieldProps, FieldOwnerState } from '@mui/x-date-pickers/models'; -import { SlotComponentPropsFromProps } from '@mui/x-internals/types'; -import { PickersTextFieldProps } from '@mui/x-date-pickers/PickersTextField'; import { - ExportedUseClearableFieldProps, - UseClearableFieldSlots, - UseClearableFieldSlotProps, -} from '@mui/x-date-pickers/hooks'; + ExportedPickerFieldUIProps, + PickerFieldUISlots, + PickerFieldUISlotProps, + PickerRangeValue, + UseFieldInternalProps, +} from '@mui/x-date-pickers/internals'; +import { BuiltInFieldTextFieldProps } from '@mui/x-date-pickers/models'; import { UseTimeRangeFieldProps } from '../internals/models'; import { TimeRangeValidationError } from '../models'; export interface UseSingleInputTimeRangeFieldProps< TEnableAccessibleFieldDOMStructure extends boolean, > extends UseTimeRangeFieldProps, - ExportedUseClearableFieldProps, Pick< UseFieldInternalProps< PickerRangeValue, @@ -23,7 +19,9 @@ export interface UseSingleInputTimeRangeFieldProps< TimeRangeValidationError >, 'unstableFieldRef' - > {} + >, + // TODO v8: Remove once the range fields open with a button. + Omit {} export type SingleInputTimeRangeFieldProps< TEnableAccessibleFieldDOMStructure extends boolean = true, @@ -44,18 +42,6 @@ export type SingleInputTimeRangeFieldProps< slotProps?: SingleInputTimeRangeFieldSlotProps; }; -export interface SingleInputTimeRangeFieldSlots extends UseClearableFieldSlots { - /** - * Form control with an input to render the value. - * @default , or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`. - */ - textField?: React.ElementType; -} +export interface SingleInputTimeRangeFieldSlots extends PickerFieldUISlots {} -export interface SingleInputTimeRangeFieldSlotProps extends UseClearableFieldSlotProps { - textField?: SlotComponentPropsFromProps< - PickersTextFieldProps | TextFieldProps, - {}, - FieldOwnerState - >; -} +export interface SingleInputTimeRangeFieldSlotProps extends PickerFieldUISlotProps {} diff --git a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts index bfa482d68eedf..2416c57b1bd69 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts @@ -33,5 +33,6 @@ export const useSingleInputTimeRangeField = < fieldValueManager: manager.internal_fieldValueManager, validator: manager.validator, valueType: manager.valueType, + getOpenPickerButtonAriaLabel: manager.internal_getOpenPickerButtonAriaLabel, }); }; 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 d0b5285994826..1b6a0c70a0802 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 @@ -11,6 +11,7 @@ import { PickerProvider, PickerValue, PickerRangeValue, + PickerFieldUIContextProvider, } from '@mui/x-date-pickers/internals'; import { FieldRef, InferError } from '@mui/x-date-pickers/models'; import { @@ -58,7 +59,6 @@ export const useDesktopRangePicker = < } = props; 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); @@ -93,6 +93,9 @@ export const useDesktopRangePicker = < localeText, }); + // Temporary hack to hide the opening button on the range pickers until we have migrate them to the new opening logic. + providerProps.contextValue.triggerStatus = 'hidden'; + React.useEffect(() => { if (providerProps.contextValue.view) { initialView.current = providerProps.contextValue.view; @@ -156,7 +159,7 @@ export const useDesktopRangePicker = < pickerSlotProps: slotProps, pickerSlots: slots, fieldProps, - anchorRef, + anchorRef: providerProps.contextValue.triggerRef, startFieldRef, endFieldRef, singleInputFieldRef, @@ -179,24 +182,26 @@ export const useDesktopRangePicker = < ...enrichedFieldResponse.fieldPrivateContextValue, }} > - - - - - {renderCurrentView()} - - - + + + + + + {renderCurrentView()} + + + + ); diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerField.ts index 3c48c357e8bd0..196673da40d24 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerField.ts @@ -110,7 +110,7 @@ export interface UseEnrichedRangePickerFieldPropsParams< TEnableAccessibleFieldDOMStructure, TError >; - anchorRef?: React.Ref; + anchorRef?: React.Ref; currentView?: TView | null; initialView?: TView; startFieldRef: React.RefObject | null>; @@ -310,10 +310,7 @@ const useSingleInputFieldSlotProps = < rangePosition, onRangePositionChange, singleInputFieldRef, - pickerSlots, - pickerSlotProps, fieldProps, - anchorRef, currentView, }: UseEnrichedRangePickerFieldPropsParams< true, @@ -380,32 +377,12 @@ const useSingleInputFieldSlotProps = < } }; - const slots = { - ...fieldProps.slots, - textField: pickerSlots?.textField, - clearButton: pickerSlots?.clearButton, - clearIcon: pickerSlots?.clearIcon, - }; - - const slotProps = { - ...fieldProps.slotProps, - textField: pickerSlotProps?.textField, - clearButton: pickerSlotProps?.clearButton, - clearIcon: pickerSlotProps?.clearIcon, - }; - const enrichedFieldProps: ReturnType = { ...fieldProps, - slots, - slotProps, label, unstableFieldRef: handleFieldRef, onKeyDown: onSpaceOrEnter(openPicker, fieldProps.onKeyDown), onBlur, - InputProps: { - ref: anchorRef, - ...fieldProps?.InputProps, - }, focused: contextValue.open ? true : undefined, ...(labelId != null && { id: labelId }), ...(variant === 'mobile' && { readOnly: true }), 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 d6d2382e90148..459323308e98d 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,6 +10,7 @@ import { PickerProvider, PickerRangeValue, PickerValue, + PickerFieldUIContextProvider, } from '@mui/x-date-pickers/internals'; import { usePickerTranslations } from '@mui/x-date-pickers/hooks'; import { FieldRef, InferError } from '@mui/x-date-pickers/models'; @@ -90,8 +91,10 @@ export const useMobileRangePicker = < localeText, }); - const Field = slots.field; + // Temporary hack to hide the opening button on the range pickers until we have migrate them to the new opening logic. + providerProps.contextValue.triggerStatus = 'hidden'; + const Field = slots.field; const fieldProps: RangePickerPropsForFieldSlot< boolean, TEnableAccessibleFieldDOMStructure, @@ -184,14 +187,16 @@ export const useMobileRangePicker = < ...enrichedFieldResponse.fieldPrivateContextValue, }} > - - - - - {renderCurrentView()} - - - + + + + + + {renderCurrentView()} + + + + ); diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/shared.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/shared.ts deleted file mode 100644 index f3568f99ad13d..0000000000000 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/shared.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* TODO: remove this when a clearable behavior for multiple input range fields is implemented */ -export const excludeProps = ( - props: TProps, - excludedProps: Array, -): TProps => { - return (Object.keys(props) as Array).reduce((acc, key) => { - if (!excludedProps.includes(key)) { - acc[key] = props[key]; - } - return acc; - }, {} as TProps); -}; 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 d255c2e719c75..e2b0b8483f184 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 @@ -16,7 +16,6 @@ import { validateDateRange } from '../../../validation'; import { rangeValueManager } from '../../utils/valueManagers'; import type { UseMultiInputRangeFieldResponse } from './useMultiInputRangeField.types'; import { DateRangeValidationError } from '../../../models'; -import { excludeProps } from './shared'; import { useMultiInputFieldSelectedSections } from '../useMultiInputFieldSelectedSections'; import { useDateRangeManager } from '../../../managers'; @@ -143,9 +142,8 @@ export const useMultiInputDateRangeField = < endFieldProps, ) as UseFieldResponse; - /* TODO: Undo this change when a clearable behavior for multiple input range fields is implemented */ return { - startDate: excludeProps(startDateResponse, ['clearable', 'onClear']), - endDate: excludeProps(endDateResponse, ['clearable', 'onClear']), + startDate: startDateResponse, + endDate: endDateResponse, }; }; 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 f4a13c557de6d..840ef757f2e9e 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 @@ -16,7 +16,6 @@ import { validateDateTimeRange } from '../../../validation'; import { rangeValueManager } from '../../utils/valueManagers'; import type { UseMultiInputRangeFieldResponse } from './useMultiInputRangeField.types'; import { DateTimeRangeValidationError } from '../../../models'; -import { excludeProps } from './shared'; import { useMultiInputFieldSelectedSections } from '../useMultiInputFieldSelectedSections'; import { useDateTimeRangeManager } from '../../../managers'; @@ -144,9 +143,8 @@ export const useMultiInputDateTimeRangeField = < typeof endFieldProps >(endFieldProps) as UseFieldResponse; - /* TODO: Undo this change when a clearable behavior for multiple input range fields is implemented */ return { - startDate: excludeProps(startDateResponse, ['clearable', 'onClear']), - endDate: excludeProps(endDateResponse, ['clearable', 'onClear']), + startDate: startDateResponse, + endDate: endDateResponse, }; }; 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 ea881fb4349d3..33c54bae45ae1 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 @@ -16,7 +16,6 @@ import { validateTimeRange } from '../../../validation'; import { rangeValueManager } from '../../utils/valueManagers'; import type { UseMultiInputRangeFieldResponse } from './useMultiInputRangeField.types'; import { TimeRangeValidationError } from '../../../models'; -import { excludeProps } from './shared'; import { useMultiInputFieldSelectedSections } from '../useMultiInputFieldSelectedSections'; import { useTimeRangeManager } from '../../../managers'; @@ -143,9 +142,8 @@ export const useMultiInputTimeRangeField = < endFieldProps, ) as UseFieldResponse; - /* TODO: Undo this change when a clearable behavior for multiple input range fields is implemented */ return { - startDate: excludeProps(startDateResponse, ['clearable', 'onClear']), - endDate: excludeProps(endDateResponse, ['clearable', 'onClear']), + startDate: startDateResponse, + endDate: endDateResponse, }; }; diff --git a/packages/x-date-pickers-pro/src/managers/useDateRangeManager.ts b/packages/x-date-pickers-pro/src/managers/useDateRangeManager.ts index ed4785f5e9ead..4e08aeaee8301 100644 --- a/packages/x-date-pickers-pro/src/managers/useDateRangeManager.ts +++ b/packages/x-date-pickers-pro/src/managers/useDateRangeManager.ts @@ -34,6 +34,8 @@ export function useDateRangeManager '', }), [enableAccessibleFieldDOMStructure, dateSeparator], ); diff --git a/packages/x-date-pickers-pro/src/managers/useDateTimeRangeManager.ts b/packages/x-date-pickers-pro/src/managers/useDateTimeRangeManager.ts index 8e779c3230b8b..dd99267e4f869 100644 --- a/packages/x-date-pickers-pro/src/managers/useDateTimeRangeManager.ts +++ b/packages/x-date-pickers-pro/src/managers/useDateTimeRangeManager.ts @@ -35,6 +35,8 @@ export function useDateTimeRangeManager '', }), [enableAccessibleFieldDOMStructure, dateSeparator], ); diff --git a/packages/x-date-pickers-pro/src/managers/useTimeRangeManager.ts b/packages/x-date-pickers-pro/src/managers/useTimeRangeManager.ts index a17555bb6b5e9..4563793170fec 100644 --- a/packages/x-date-pickers-pro/src/managers/useTimeRangeManager.ts +++ b/packages/x-date-pickers-pro/src/managers/useTimeRangeManager.ts @@ -35,6 +35,8 @@ export function useTimeRangeManager '', }), [enableAccessibleFieldDOMStructure, dateSeparator], ); diff --git a/packages/x-date-pickers-pro/src/models/fields.ts b/packages/x-date-pickers-pro/src/models/fields.ts index 648dc4c4f277a..0fe3c8c4415ad 100644 --- a/packages/x-date-pickers-pro/src/models/fields.ts +++ b/packages/x-date-pickers-pro/src/models/fields.ts @@ -6,7 +6,6 @@ import { PickerRangeValue, } from '@mui/x-date-pickers/internals'; import { FieldRef, PickerFieldSlotProps } from '@mui/x-date-pickers/models'; -import { UseClearableFieldResponse } from '@mui/x-date-pickers/hooks'; export type { FieldRangeSection } from '@mui/x-date-pickers/internals'; @@ -62,6 +61,13 @@ export type PickerRangeFieldSlotProps = UseClearableFieldResponse< - UseFieldResponse +> = Omit< + UseFieldResponse, + | 'slots' + | 'slotProps' + | 'clearable' + | 'onClear' + | 'openPickerButtonPosition' + | 'clearButtonPosition' + | 'openPickerAriaLabel' >; diff --git a/packages/x-date-pickers/src/DateField/DateField.tsx b/packages/x-date-pickers/src/DateField/DateField.tsx index a7a085a71570e..fca2497ad6219 100644 --- a/packages/x-date-pickers/src/DateField/DateField.tsx +++ b/packages/x-date-pickers/src/DateField/DateField.tsx @@ -1,16 +1,12 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import MuiTextField from '@mui/material/TextField'; import { useThemeProps } from '@mui/material/styles'; -import useSlotProps from '@mui/utils/useSlotProps'; import { refType } from '@mui/utils'; import { DateFieldProps } from './DateField.types'; import { useDateField } from './useDateField'; -import { useClearableField } from '../hooks'; -import { PickersTextField } from '../PickersTextField'; -import { convertFieldResponseIntoMuiTextFieldProps } from '../internals/utils/convertFieldResponseIntoMuiTextFieldProps'; -import { useFieldOwnerState } from '../internals/hooks/useFieldOwnerState'; +import { PickerFieldUI, useFieldTextFieldProps } from '../internals/components/PickerFieldUI'; +import { CalendarIcon } from '../icons'; type DateFieldComponent = (( props: DateFieldProps & React.RefAttributes, @@ -34,40 +30,28 @@ const DateField = React.forwardRef(function DateField< name: 'MuiDateField', }); - const { slots, slotProps, InputProps, inputProps, ...other } = themeProps; + const { slots, slotProps, ...other } = themeProps; - const ownerState = useFieldOwnerState(themeProps); - - const textFieldProps = useSlotProps({ - elementType: PickersTextField, - externalSlotProps: slotProps?.textField, - externalForwardedProps: other, - additionalProps: { + const textFieldProps = useFieldTextFieldProps>( + { + slotProps, ref: inRef, + externalForwardedProps: other, }, - ownerState, - }) as DateFieldProps; - - // 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 convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); - - const processedFieldProps = useClearableField({ - ...convertedFieldResponse, - slots, - slotProps, - }); - - const TextField = - slots?.textField ?? - (fieldResponse.enableAccessibleFieldDOMStructure === false ? MuiTextField : PickersTextField); - return ; + return ( + + ); }) as DateFieldComponent; DateField.propTypes = { @@ -86,6 +70,12 @@ DateField.propTypes = { * @default false */ clearable: PropTypes.bool, + /** + * The position at which the clear button is placed. + * If the field is not clearable, the button is not rendered. + * @default 'end' + */ + clearButtonPosition: PropTypes.oneOf(['end', 'start']), /** * The color of the component. * It supports both default and custom theme colors, which can be added as shown in the @@ -229,6 +219,12 @@ DateField.propTypes = { * @param {FieldSelectedSections} newValue The new selected sections. */ onSelectedSectionsChange: PropTypes.func, + /** + * The position at which the opening button is placed. + * If there is no picker to open, the button is not rendered + * @default 'end' + */ + openPickerButtonPosition: PropTypes.oneOf(['end', 'start']), /** * If `true`, the component is read-only. * When read-only, the value cannot be changed but the user can interact with the interface. diff --git a/packages/x-date-pickers/src/DateField/DateField.types.ts b/packages/x-date-pickers/src/DateField/DateField.types.ts index 7226c4ac65791..8a4d1c048b14b 100644 --- a/packages/x-date-pickers/src/DateField/DateField.types.ts +++ b/packages/x-date-pickers/src/DateField/DateField.types.ts @@ -1,16 +1,13 @@ -import * as React from 'react'; -import type { TextFieldProps } from '@mui/material/TextField'; -import { MakeOptional, SlotComponentPropsFromProps } from '@mui/x-internals/types'; -import { - ExportedUseClearableFieldProps, - UseClearableFieldSlots, - UseClearableFieldSlotProps, -} from '../hooks/useClearableField'; -import { DateValidationError, BuiltInFieldTextFieldProps, FieldOwnerState } from '../models'; +import { MakeOptional } from '@mui/x-internals/types'; +import { DateValidationError, BuiltInFieldTextFieldProps } from '../models'; import { UseFieldInternalProps } from '../internals/hooks/useField'; import { ExportedValidateDateProps } from '../validation/validateDate'; -import { PickersTextFieldProps } from '../PickersTextField'; import { PickerValue } from '../internals/models'; +import { + ExportedPickerFieldUIProps, + PickerFieldUISlotProps, + PickerFieldUISlots, +} from '../internals/components/PickerFieldUI'; export interface UseDateFieldProps extends MakeOptional< @@ -18,7 +15,7 @@ export interface UseDateFieldProps, ExportedValidateDateProps, - ExportedUseClearableFieldProps {} + ExportedPickerFieldUIProps {} export type DateFieldProps = // The hook props @@ -43,18 +40,6 @@ export type DateFieldProps = DateFieldProps; -export interface DateFieldSlots extends UseClearableFieldSlots { - /** - * Form control with an input to render the value. - * @default , or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`. - */ - textField?: React.ElementType; -} +export interface DateFieldSlots extends PickerFieldUISlots {} -export interface DateFieldSlotProps extends UseClearableFieldSlotProps { - textField?: SlotComponentPropsFromProps< - PickersTextFieldProps | TextFieldProps, - {}, - FieldOwnerState - >; -} +export interface DateFieldSlotProps extends PickerFieldUISlotProps {} diff --git a/packages/x-date-pickers/src/DateField/useDateField.ts b/packages/x-date-pickers/src/DateField/useDateField.ts index 49b1e6d8b57df..fcbd4f3c4261d 100644 --- a/packages/x-date-pickers/src/DateField/useDateField.ts +++ b/packages/x-date-pickers/src/DateField/useDateField.ts @@ -30,5 +30,6 @@ export const useDateField = < fieldValueManager: manager.internal_fieldValueManager, validator: manager.validator, valueType: manager.valueType, + getOpenPickerButtonAriaLabel: manager.internal_getOpenPickerButtonAriaLabel, }); }; 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 3ad845d85883b..902c7f656cfbb 100644 --- a/packages/x-date-pickers/src/DatePicker/tests/DatePicker.test.tsx +++ b/packages/x-date-pickers/src/DatePicker/tests/DatePicker.test.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; import { expect } from 'chai'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { screen } from '@mui/internal-test-utils/createRenderer'; +import { fireEvent, 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(); + const { render } = createPickerRenderer({ clock: 'fake' }); it('should render in mobile mode when `useMediaQuery` returns `false`', () => { const originalMatchMedia = window.matchMedia; @@ -14,7 +13,8 @@ describe('', () => { render(); - expect(screen.getByLabelText(/Choose date/)).to.have.class(pickersInputBaseClasses.input); + fireEvent.click(screen.getByLabelText(/Choose date/)); + expect(screen.queryByRole('dialog')).to.not.equal(null); window.matchMedia = originalMatchMedia; }); diff --git a/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx b/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx index 187f0ffe62568..1ee01deb4b4c7 100644 --- a/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx +++ b/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx @@ -1,16 +1,12 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import MuiTextField from '@mui/material/TextField'; import { useThemeProps } from '@mui/material/styles'; -import useSlotProps from '@mui/utils/useSlotProps'; import { refType } from '@mui/utils'; import { DateTimeFieldProps } from './DateTimeField.types'; import { useDateTimeField } from './useDateTimeField'; -import { useClearableField } from '../hooks'; -import { PickersTextField } from '../PickersTextField'; -import { convertFieldResponseIntoMuiTextFieldProps } from '../internals/utils/convertFieldResponseIntoMuiTextFieldProps'; -import { useFieldOwnerState } from '../internals/hooks/useFieldOwnerState'; +import { PickerFieldUI, useFieldTextFieldProps } from '../internals/components/PickerFieldUI'; +import { CalendarIcon } from '../icons'; type DateTimeFieldComponent = (( props: DateTimeFieldProps & @@ -38,40 +34,28 @@ const DateTimeField = React.forwardRef(function DateTimeField< name: 'MuiDateTimeField', }); - const { slots, slotProps, InputProps, inputProps, ...other } = themeProps; + const { slots, slotProps, ...other } = themeProps; - const ownerState = useFieldOwnerState(themeProps); - - const textFieldProps = useSlotProps({ - elementType: PickersTextField, - externalSlotProps: slotProps?.textField, + const textFieldProps = useFieldTextFieldProps< + DateTimeFieldProps + >({ + slotProps, + ref: inRef, externalForwardedProps: other, - ownerState, - additionalProps: { - ref: inRef, - }, - }) as DateTimeFieldProps; - - // 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 convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); - - const processedFieldProps = useClearableField({ - ...convertedFieldResponse, - slots, - slotProps, - }); - const TextField = - slots?.textField ?? - (fieldResponse.enableAccessibleFieldDOMStructure === false ? MuiTextField : PickersTextField); - - return ; + return ( + + ); }) as DateTimeFieldComponent; DateTimeField.propTypes = { @@ -95,6 +79,12 @@ DateTimeField.propTypes = { * @default false */ clearable: PropTypes.bool, + /** + * The position at which the clear button is placed. + * If the field is not clearable, the button is not rendered. + * @default 'end' + */ + clearButtonPosition: PropTypes.oneOf(['end', 'start']), /** * The color of the component. * It supports both default and custom theme colors, which can be added as shown in the @@ -266,6 +256,12 @@ DateTimeField.propTypes = { * @param {FieldSelectedSections} newValue The new selected sections. */ onSelectedSectionsChange: PropTypes.func, + /** + * The position at which the opening button is placed. + * If there is no picker to open, the button is not rendered + * @default 'end' + */ + openPickerButtonPosition: PropTypes.oneOf(['end', 'start']), /** * If `true`, the component is read-only. * When read-only, the value cannot be changed but the user can interact with the interface. diff --git a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts index 2c3964472c396..ec523e87383ab 100644 --- a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts +++ b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts @@ -1,17 +1,14 @@ -import * as React from 'react'; -import { MakeOptional, SlotComponentPropsFromProps } from '@mui/x-internals/types'; -import { TextFieldProps } from '@mui/material/TextField'; -import { DateTimeValidationError, BuiltInFieldTextFieldProps, FieldOwnerState } from '../models'; +import { MakeOptional } from '@mui/x-internals/types'; +import { DateTimeValidationError, BuiltInFieldTextFieldProps } from '../models'; import { UseFieldInternalProps } from '../internals/hooks/useField'; -import { - ExportedUseClearableFieldProps, - UseClearableFieldSlots, - UseClearableFieldSlotProps, -} from '../hooks/useClearableField'; import { ExportedValidateDateTimeProps } from '../validation/validateDateTime'; import { AmPmProps } from '../internals/models/props/time'; import { PickerValue } from '../internals/models'; -import { PickersTextFieldProps } from '../PickersTextField'; +import { + ExportedPickerFieldUIProps, + PickerFieldUISlotProps, + PickerFieldUISlots, +} from '../internals/components/PickerFieldUI'; export interface UseDateTimeFieldProps extends MakeOptional< @@ -23,7 +20,7 @@ export interface UseDateTimeFieldProps, ExportedValidateDateTimeProps, - ExportedUseClearableFieldProps, + ExportedPickerFieldUIProps, AmPmProps {} export type DateTimeFieldProps = @@ -46,18 +43,6 @@ export type DateTimeFieldProps, or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`. - */ - textField?: React.ElementType; -} +export interface DateTimeFieldSlots extends PickerFieldUISlots {} -export interface DateTimeFieldSlotProps extends UseClearableFieldSlotProps { - textField?: SlotComponentPropsFromProps< - PickersTextFieldProps | TextFieldProps, - {}, - FieldOwnerState - >; -} +export interface DateTimeFieldSlotProps extends PickerFieldUISlotProps {} diff --git a/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts b/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts index 5943673b20c9b..7884f56705e76 100644 --- a/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts +++ b/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts @@ -30,5 +30,6 @@ export const useDateTimeField = < fieldValueManager: manager.internal_fieldValueManager, validator: manager.validator, valueType: manager.valueType, + getOpenPickerButtonAriaLabel: manager.internal_getOpenPickerButtonAriaLabel, }); }; 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 933f61c48f166..bd47d4b3f04db 100644 --- a/packages/x-date-pickers/src/DateTimePicker/tests/DateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/tests/DateTimePicker.test.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; import { expect } from 'chai'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; -import { screen } from '@mui/internal-test-utils/createRenderer'; +import { fireEvent, 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(); + const { render } = createPickerRenderer({ clock: 'fake' }); it('should render in mobile mode when `useMediaQuery` returns `false`', () => { const originalMatchMedia = window.matchMedia; @@ -14,7 +13,8 @@ describe('', () => { render(); - expect(screen.getByLabelText(/Choose date/)).to.have.class(pickersInputBaseClasses.input); + fireEvent.click(screen.getByLabelText(/Choose date/)); + expect(screen.queryByRole('dialog')).to.not.equal(null); window.matchMedia = originalMatchMedia; }); diff --git a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx index 874017a90b7b8..4ee42b330bc98 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx @@ -6,16 +6,13 @@ import { refType } from '@mui/utils'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { DesktopDatePickerProps } from './DesktopDatePicker.types'; import { DatePickerViewRenderers, useDatePickerDefaultizedProps } from '../DatePicker/shared'; -import { usePickerTranslations } from '../hooks/usePickerTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { validateDate, extractValidationProps } from '../validation'; import { DateView, PickerOwnerState } from '../models'; import { useDesktopPicker } from '../internals/hooks/useDesktopPicker'; -import { CalendarIcon } from '../icons'; import { DateField } from '../DateField'; import { renderDateViewCalendar } from '../dateViewRenderers'; import { resolveDateFormat } from '../internals/utils/date-utils'; -import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; import { PickerLayoutOwnerState } from '../PickersLayout'; import { PickersActionBarAction } from '../PickersActionBar'; @@ -42,7 +39,6 @@ const DesktopDatePicker = React.forwardRef(function DesktopDatePicker< inProps: DesktopDatePickerProps, ref: React.Ref, ) { - const translations = usePickerTranslations(); const utils = useUtils(); // Props with the default values common to all date pickers @@ -65,7 +61,6 @@ const DesktopDatePicker = React.forwardRef(function DesktopDatePicker< format: resolveDateFormat(utils, defaultizedProps, false), yearsPerRow: defaultizedProps.yearsPerRow ?? 4, slots: { - openPickerIcon: CalendarIcon, field: DateField, ...defaultizedProps.slots, }, @@ -95,12 +90,6 @@ const DesktopDatePicker = React.forwardRef(function DesktopDatePicker< props, valueManager: singleItemValueManager, valueType: 'date', - getOpenDialogAriaText: buildGetOpenDialogAriaText({ - utils, - formatKey: 'fullDate', - contextTranslation: translations.openDatePickerDialogue, - propsTranslation: props.localeText?.openDatePickerDialogue, - }), validator: validateDate, }); 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 1c55b5a4bbf50..9d63482b4442d 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/tests/DesktopDatePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/tests/DesktopDatePicker.test.tsx @@ -40,7 +40,7 @@ describe('', () => { />, ); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); fireEvent.click(screen.getByLabelText(/switch to year view/i)); expect(handleViewChange.callCount).to.equal(1); @@ -49,7 +49,7 @@ describe('', () => { // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target fireEvent.keyDown(document.activeElement!, { key: 'Escape' }); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); expect(handleViewChange.callCount).to.equal(2); expect(handleViewChange.lastCall.firstArg).to.equal('day'); }); @@ -66,7 +66,7 @@ describe('', () => { />, ); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); fireEvent.click(screen.getByLabelText(/switch to year view/i)); expect(handleViewChange.callCount).to.equal(1); @@ -75,7 +75,7 @@ describe('', () => { // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target fireEvent.keyDown(document.activeElement!, { key: 'Escape' }); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); expect(handleViewChange.callCount).to.equal(2); expect(handleViewChange.lastCall.firstArg).to.equal('month'); }); @@ -85,7 +85,7 @@ describe('', () => { , ); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); expect(screen.getByRole('radio', { checked: true, name: '2018' })).not.to.equal(null); @@ -93,7 +93,7 @@ describe('', () => { // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target fireEvent.keyDown(document.activeElement!, { key: 'Escape' }); setProps({ views: ['month', 'year'] }); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); // wait for all pending changes to be flushed clock.runToLast(); @@ -104,7 +104,7 @@ describe('', () => { testSkipIf(isJSDOM)('should move the focus to the newly opened views', () => { render(); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); expect(document.activeElement).to.have.text('2019'); fireEvent.click(screen.getByText('2020')); @@ -120,7 +120,7 @@ describe('', () => { />, ); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); expect(screen.getByRole('radio', { checked: true, name: 'January' })).not.to.equal(null); @@ -128,7 +128,7 @@ describe('', () => { // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target fireEvent.keyDown(document.activeElement!, { key: 'Escape' }); setProps({ view: 'year' }); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); // wait for all pending changes to be flushed clock.runToLast(); @@ -236,7 +236,7 @@ describe('', () => { />, ); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); // Select year fireEvent.click(screen.getByRole('radio', { name: '2025' })); @@ -263,7 +263,7 @@ describe('', () => { />, ); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); expect(screen.getByLabelText('Previous month')).to.have.attribute('disabled'); }); @@ -276,7 +276,7 @@ describe('', () => { />, ); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); expect(screen.getByLabelText('Previous month')).not.to.have.attribute('disabled'); }); @@ -289,7 +289,7 @@ describe('', () => { />, ); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); expect(screen.getByLabelText('Next month')).to.have.attribute('disabled'); }); @@ -302,7 +302,7 @@ describe('', () => { />, ); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); expect(screen.getByLabelText('Next month')).not.to.have.attribute('disabled'); }); @@ -347,7 +347,7 @@ describe('', () => { expect(() => { render(); - openPicker({ type: 'date', variant: 'desktop' }); + openPicker({ type: 'date' }); }).toWarnDev('MUI X: `openTo="month"` is not a valid prop.'); }); 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 1c79140c48074..1bb38f834e57b 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 @@ -23,7 +23,6 @@ describe(' - Describes', () => { clock, views: ['year', 'month', 'day'], componentFamily: 'picker', - variant: 'desktop', })); describeConformance(, () => ({ diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx index fef966c911ec2..d4bf5da32c03c 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx @@ -9,11 +9,9 @@ import { DateTimeField } from '../DateTimeField'; import { DesktopDateTimePickerProps } from './DesktopDateTimePicker.types'; import { useDateTimePickerDefaultizedProps } from '../DateTimePicker/shared'; import { renderDateViewCalendar } from '../dateViewRenderers/dateViewRenderers'; -import { usePickerTranslations } from '../hooks/usePickerTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { validateDateTime, extractValidationProps } from '../validation'; import { DateOrTimeViewWithMeridiem, PickerValue } from '../internals/models'; -import { CalendarIcon } from '../icons'; import { useDesktopPicker } from '../internals/hooks/useDesktopPicker'; import { resolveDateTimeFormat, @@ -38,7 +36,6 @@ import { } from '../internals/hooks/usePicker/usePickerViews'; import { isInternalTimeView } from '../internals/utils/time-utils'; import { isDatePickerView } from '../internals/utils/date-utils'; -import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; const rendererInterceptor = function RendererInterceptor( props: PickerRendererInterceptorProps, @@ -112,7 +109,6 @@ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker< inProps: DesktopDateTimePickerProps, ref: React.Ref, ) { - const translations = usePickerTranslations(); const utils = useUtils(); // Props with the default values common to all date time pickers @@ -164,7 +160,6 @@ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker< slots: { field: DateTimeField, layout: DesktopDateTimePickerLayout, - openPickerIcon: CalendarIcon, ...defaultizedProps.slots, }, slotProps: { @@ -194,12 +189,6 @@ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker< props, valueManager: singleItemValueManager, valueType: 'date-time', - getOpenDialogAriaText: buildGetOpenDialogAriaText({ - utils, - formatKey: 'fullDate', - contextTranslation: translations.openDatePickerDialogue, - propsTranslation: props.localeText?.openDatePickerDialogue, - }), validator: validateDateTime, rendererInterceptor, }); diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/DesktopDateTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/DesktopDateTimePicker.test.tsx index fda2d1f68cf0f..f3b787bfe1473 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/DesktopDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/DesktopDateTimePicker.test.tsx @@ -35,7 +35,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-time', variant: 'desktop' }); + openPicker({ type: 'date-time' }); // Select year fireEvent.click(screen.getByRole('radio', { name: '2025' })); @@ -81,7 +81,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-time', variant: 'desktop' }); + openPicker({ type: 'date-time' }); // Change the date multiple times to check that picker doesn't close after cycling through all views internally fireEvent.click(screen.getByRole('gridcell', { name: '2' })); 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 1370af67311b2..551a73fee6e39 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 @@ -36,7 +36,6 @@ describe(' - Describes', () => { clock, views: ['year', 'month', 'day', 'hours', 'minutes'], componentFamily: 'picker', - variant: 'desktop', })); describeConformance(, () => ({ diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx index 01bcb39d8eccc..777a2806aeea7 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx @@ -7,10 +7,8 @@ import { singleItemValueManager } from '../internals/utils/valueManagers'; import { TimeField } from '../TimeField'; import { DesktopTimePickerProps } from './DesktopTimePicker.types'; import { TimePickerViewRenderers, useTimePickerDefaultizedProps } from '../TimePicker/shared'; -import { usePickerTranslations } from '../hooks/usePickerTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { extractValidationProps, validateTime } from '../validation'; -import { ClockIcon } from '../icons'; import { useDesktopPicker } from '../internals/hooks/useDesktopPicker'; import { renderDigitalClockTimeView, @@ -20,7 +18,6 @@ import { TimeViewWithMeridiem } from '../internals/models'; import { resolveTimeFormat } from '../internals/utils/time-utils'; import { resolveTimeViewsResponse } from '../internals/utils/date-time-utils'; import { TimeView, PickerOwnerState } from '../models'; -import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; type DesktopTimePickerComponent = (( props: DesktopTimePickerProps & @@ -43,7 +40,6 @@ const DesktopTimePicker = React.forwardRef(function DesktopTimePicker< inProps: DesktopTimePickerProps, ref: React.Ref, ) { - const translations = usePickerTranslations(); const utils = useUtils(); // Props with the default values common to all time pickers @@ -90,7 +86,6 @@ const DesktopTimePicker = React.forwardRef(function DesktopTimePicker< views: shouldRenderTimeInASingleColumn ? ['hours' as TimeViewWithMeridiem] : views, slots: { field: TimeField, - openPickerIcon: ClockIcon, ...defaultizedProps.slots, }, slotProps: { @@ -116,12 +111,6 @@ const DesktopTimePicker = React.forwardRef(function DesktopTimePicker< props, valueManager: singleItemValueManager, valueType: 'time', - getOpenDialogAriaText: buildGetOpenDialogAriaText({ - utils, - formatKey: 'fullTime', - contextTranslation: translations.openTimePickerDialogue, - propsTranslation: props.localeText?.openTimePickerDialogue, - }), validator: validateTime, }); diff --git a/packages/x-date-pickers/src/DesktopTimePicker/tests/DesktopTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopTimePicker/tests/DesktopTimePicker.test.tsx index 096830e2565b8..e1fca7f54c978 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/tests/DesktopTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/tests/DesktopTimePicker.test.tsx @@ -75,7 +75,7 @@ describe('', () => { />, ); - openPicker({ type: 'time', variant: 'desktop' }); + openPicker({ type: 'time' }); fireEvent.click(screen.getByRole('option', { name: '09:00 AM' })); expect(onChange.callCount).to.equal(1); @@ -104,7 +104,7 @@ describe('', () => { />, ); - openPicker({ type: 'time', variant: 'desktop' }); + openPicker({ type: 'time' }); fireEvent.click(screen.getByRole('option', { name: '2 hours' })); expect(onChange.callCount).to.equal(1); @@ -142,7 +142,7 @@ describe('', () => { />, ); - openPicker({ type: 'time', variant: 'desktop' }); + openPicker({ type: 'time' }); fireEvent.click(screen.getByRole('option', { name: '15 minutes' })); expect(onChange.callCount).to.equal(1); @@ -185,7 +185,7 @@ describe('', () => { />, ); - openPicker({ type: 'time', variant: 'desktop' }); + openPicker({ type: 'time' }); fireEvent.click(screen.getByRole('option', { name: 'PM' })); expect(onChange.callCount).to.equal(1); 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 700a92a4156ff..77caadcfb15c8 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 @@ -28,7 +28,6 @@ describe(' - Describes', () => { clock, views: ['hours', 'minutes'], componentFamily: 'picker', - variant: 'desktop', })); describeConformance(, () => ({ diff --git a/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx b/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx index c4f5013abc991..4ec445f96f690 100644 --- a/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx +++ b/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx @@ -21,7 +21,6 @@ describe(' - Describes', () => { clock, views: ['hours'], componentFamily: 'digital-clock', - variant: 'desktop', })); describeConformance(, () => ({ @@ -37,7 +36,6 @@ describe(' - Describes', () => { render, componentFamily: 'digital-clock', type: 'time', - variant: 'desktop', defaultProps: { views: ['hours'], }, diff --git a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx index 3f40a6ad1f1f4..a9f01953e7dba 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx +++ b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx @@ -6,7 +6,6 @@ import { refType } from '@mui/utils'; import { useMobilePicker } from '../internals/hooks/useMobilePicker'; import { MobileDatePickerProps } from './MobileDatePicker.types'; import { DatePickerViewRenderers, useDatePickerDefaultizedProps } from '../DatePicker/shared'; -import { usePickerTranslations } from '../hooks/usePickerTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { extractValidationProps, validateDate } from '../validation'; import { DateView, PickerOwnerState } from '../models'; @@ -14,7 +13,6 @@ import { DateField } from '../DateField'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { renderDateViewCalendar } from '../dateViewRenderers'; import { resolveDateFormat } from '../internals/utils/date-utils'; -import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; type MobileDatePickerComponent = (( props: MobileDatePickerProps & @@ -37,7 +35,6 @@ const MobileDatePicker = React.forwardRef(function MobileDatePicker< inProps: MobileDatePickerProps, ref: React.Ref, ) { - const translations = usePickerTranslations(); const utils = useUtils(); // Props with the default values common to all date pickers @@ -83,12 +80,6 @@ const MobileDatePicker = React.forwardRef(function MobileDatePicker< props, valueManager: singleItemValueManager, valueType: 'date', - getOpenDialogAriaText: buildGetOpenDialogAriaText({ - utils, - formatKey: 'fullDate', - contextTranslation: translations.openDatePickerDialogue, - propsTranslation: props.localeText?.openDatePickerDialogue, - }), validator: validateDate, }); 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 586d963955a2d..1c6255189b9e3 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/tests/MobileDatePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileDatePicker/tests/MobileDatePicker.test.tsx @@ -11,7 +11,6 @@ import { expectFieldValueV7, buildFieldInteractions, openPicker, - getFieldSectionsContainer, } from 'test/utils/pickers'; describe('', () => { @@ -132,17 +131,6 @@ describe('', () => { }); describe('picker state', () => { - it('should open when clicking the input', () => { - const onOpen = spy(); - - render(); - - fireEvent.click(getFieldSectionsContainer()); - - expect(onOpen.callCount).to.equal(1); - expect(screen.queryByRole('dialog')).toBeVisible(); - }); - it('should call `onAccept` even if controlled', () => { const onAccept = spy(); @@ -154,7 +142,7 @@ describe('', () => { render(); - openPicker({ type: 'date', variant: 'mobile' }); + openPicker({ type: 'date' }); fireEvent.click(screen.getByText('15', { selector: 'button' })); fireEvent.click(screen.getByText('OK', { selector: 'button' })); @@ -176,7 +164,7 @@ describe('', () => { expectFieldValueV7(view.getSectionsContainer(), 'MM/DD/YYYY'); // Open and Dismiss the picker - openPicker({ type: 'date', variant: 'mobile' }); + openPicker({ type: 'date' }); // 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/MobileDatePicker/tests/describes.MobileDatePicker.test.tsx b/packages/x-date-pickers/src/MobileDatePicker/tests/describes.MobileDatePicker.test.tsx index 2a75c950b93df..ed5371703071e 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 @@ -61,7 +61,7 @@ describe(' - Describes', () => { }, setNewValue: (value, { isOpened, applySameValue }) => { if (!isOpened) { - openPicker({ type: 'date', variant: 'mobile' }); + openPicker({ type: 'date' }); } const newValue = applySameValue ? value! : adapterToUse.addDays(value!, 1); diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx index c0410f5d2499a..93cc5ca8ed812 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx @@ -7,7 +7,6 @@ import { singleItemValueManager } from '../internals/utils/valueManagers'; import { DateTimeField } from '../DateTimeField'; import { MobileDateTimePickerProps } from './MobileDateTimePicker.types'; import { useDateTimePickerDefaultizedProps } from '../DateTimePicker/shared'; -import { usePickerTranslations } from '../hooks/usePickerTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { extractValidationProps, validateDateTime } from '../validation'; import { DateOrTimeView, PickerOwnerState } from '../models'; @@ -15,7 +14,6 @@ import { useMobilePicker } from '../internals/hooks/useMobilePicker'; import { renderDateViewCalendar } from '../dateViewRenderers'; import { renderTimeViewClock } from '../timeViewRenderers'; import { resolveDateTimeFormat } from '../internals/utils/date-time-utils'; -import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; import { PickerViewRendererLookup } from '../internals/hooks/usePicker/usePickerViews'; import { PickerValue } from '../internals/models'; @@ -40,7 +38,6 @@ const MobileDateTimePicker = React.forwardRef(function MobileDateTimePicker< inProps: MobileDateTimePickerProps, ref: React.Ref, ) { - const translations = usePickerTranslations(); const utils = useUtils(); // Props with the default values common to all date time pickers @@ -97,12 +94,6 @@ const MobileDateTimePicker = React.forwardRef(function MobileDateTimePicker< props, valueManager: singleItemValueManager, valueType: 'date-time', - getOpenDialogAriaText: buildGetOpenDialogAriaText({ - utils, - formatKey: 'fullDate', - contextTranslation: translations.openDatePickerDialogue, - propsTranslation: props.localeText?.openDatePickerDialogue, - }), validator: validateDateTime, }); 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 16a032c454a06..34f64d19cfe3e 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/tests/MobileDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/tests/MobileDateTimePicker.test.tsx @@ -8,7 +8,6 @@ import { createPickerRenderer, openPicker, getClockTouchEvent, - getFieldSectionsContainer, } from 'test/utils/pickers'; import { hasTouchSupport, testSkipIf } from 'test/utils/skipIf'; @@ -84,17 +83,6 @@ describe('', () => { }); describe('picker state', () => { - it('should open when clicking the input', () => { - const onOpen = spy(); - - render(); - - fireEvent.click(getFieldSectionsContainer()); - - expect(onOpen.callCount).to.equal(1); - expect(screen.queryByRole('dialog')).toBeVisible(); - }); - testSkipIf(!hasTouchSupport)('should call onChange when selecting each view', () => { const onChange = spy(); const onAccept = spy(); @@ -111,7 +99,7 @@ describe('', () => { />, ); - openPicker({ type: 'date-time', variant: 'mobile' }); + openPicker({ type: 'date-time' }); expect(onChange.callCount).to.equal(0); expect(onAccept.callCount).to.equal(0); expect(onClose.callCount).to.equal(0); diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/tests/describes.MobileDateTimePicker.test.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/tests/describes.MobileDateTimePicker.test.tsx index 30ae63e192e42..88975bfea2bd1 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/tests/describes.MobileDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/tests/describes.MobileDateTimePicker.test.tsx @@ -28,7 +28,6 @@ describe(' - Describes', () => { clock, views: ['year', 'day', 'hours', 'minutes'], componentFamily: 'picker', - variant: 'mobile', })); describeConformance(, () => ({ @@ -73,7 +72,7 @@ describe(' - Describes', () => { }, setNewValue: (value, { isOpened, applySameValue }) => { if (!isOpened) { - openPicker({ type: 'date-time', variant: 'mobile' }); + openPicker({ type: 'date-time' }); } const newValue = applySameValue diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx index e36e6cf1cd416..f73e78b7562fe 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx @@ -7,14 +7,12 @@ import { singleItemValueManager } from '../internals/utils/valueManagers'; import { TimeField } from '../TimeField'; import { MobileTimePickerProps } from './MobileTimePicker.types'; import { TimePickerViewRenderers, useTimePickerDefaultizedProps } from '../TimePicker/shared'; -import { usePickerTranslations } from '../hooks/usePickerTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { extractValidationProps, validateTime } from '../validation'; import { PickerOwnerState, TimeView } from '../models'; import { useMobilePicker } from '../internals/hooks/useMobilePicker'; import { renderTimeViewClock } from '../timeViewRenderers'; import { resolveTimeFormat } from '../internals/utils/time-utils'; -import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; type MobileTimePickerComponent = (( props: MobileTimePickerProps & @@ -37,7 +35,6 @@ const MobileTimePicker = React.forwardRef(function MobileTimePicker< inProps: MobileTimePickerProps, ref: React.Ref, ) { - const translations = usePickerTranslations(); const utils = useUtils(); // Props with the default values common to all time pickers @@ -87,12 +84,6 @@ const MobileTimePicker = React.forwardRef(function MobileTimePicker< props, valueManager: singleItemValueManager, valueType: 'time', - getOpenDialogAriaText: buildGetOpenDialogAriaText({ - utils, - formatKey: 'fullTime', - contextTranslation: translations.openTimePickerDialogue, - propsTranslation: props.localeText?.openTimePickerDialogue, - }), validator: validateTime, }); 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 b902e6808b17b..a673288bd284e 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/tests/MobileTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/tests/MobileTimePicker.test.tsx @@ -8,7 +8,6 @@ import { adapterToUse, openPicker, getClockTouchEvent, - getFieldSectionsContainer, } from 'test/utils/pickers'; import { testSkipIf, hasTouchSupport } from 'test/utils/skipIf'; @@ -16,17 +15,6 @@ describe('', () => { const { render } = createPickerRenderer({ clock: 'fake' }); describe('picker state', () => { - it('should open when clicking the input', () => { - const onOpen = spy(); - - render(); - - fireEvent.click(getFieldSectionsContainer()); - - expect(onOpen.callCount).to.equal(1); - expect(screen.queryByRole('dialog')).toBeVisible(); - }); - it('should fire a change event when meridiem changes', () => { const handleChange = spy(); render( @@ -61,7 +49,7 @@ describe('', () => { />, ); - openPicker({ type: 'time', variant: 'mobile' }); + openPicker({ type: 'time' }); // Change the hours const hourClockEvent = getClockTouchEvent(11, '12hours'); diff --git a/packages/x-date-pickers/src/MobileTimePicker/tests/describes.MobileTimePicker.test.tsx b/packages/x-date-pickers/src/MobileTimePicker/tests/describes.MobileTimePicker.test.tsx index 5cff45ba0c9ce..cb211de30b1dc 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/tests/describes.MobileTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/tests/describes.MobileTimePicker.test.tsx @@ -29,7 +29,6 @@ describe(' - Describes', () => { clock, views: ['hours', 'minutes'], componentFamily: 'picker', - variant: 'mobile', })); describeConformance(, () => ({ @@ -71,7 +70,7 @@ describe(' - Describes', () => { }, setNewValue: (value, { isOpened, applySameValue }) => { if (!isOpened) { - openPicker({ type: 'time', variant: 'mobile' }); + openPicker({ type: 'time' }); } const newValue = applySameValue diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx index 3ab127d6bafa6..992c33dc6decd 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx @@ -23,7 +23,6 @@ describe(' - Describes', () => { clock, views: ['hours', 'minutes'], componentFamily: 'multi-section-digital-clock', - variant: 'desktop', })); describeConformance(, () => ({ @@ -39,7 +38,6 @@ describe(' - Describes', () => { render, componentFamily: 'multi-section-digital-clock', type: 'time', - variant: 'desktop', values: [adapterToUse.date('2018-01-01T11:30:00'), adapterToUse.date('2018-01-01T12:35:00')], emptyValue: null, clock, diff --git a/packages/x-date-pickers/src/TimeField/TimeField.tsx b/packages/x-date-pickers/src/TimeField/TimeField.tsx index e99c0e4166965..c92a4ac60e896 100644 --- a/packages/x-date-pickers/src/TimeField/TimeField.tsx +++ b/packages/x-date-pickers/src/TimeField/TimeField.tsx @@ -1,16 +1,12 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import MuiTextField from '@mui/material/TextField'; import { useThemeProps } from '@mui/material/styles'; -import useSlotProps from '@mui/utils/useSlotProps'; import { refType } from '@mui/utils'; import { TimeFieldProps } from './TimeField.types'; import { useTimeField } from './useTimeField'; -import { useClearableField } from '../hooks'; -import { PickersTextField } from '../PickersTextField'; -import { convertFieldResponseIntoMuiTextFieldProps } from '../internals/utils/convertFieldResponseIntoMuiTextFieldProps'; -import { useFieldOwnerState } from '../internals/hooks/useFieldOwnerState'; +import { PickerFieldUI, useFieldTextFieldProps } from '../internals/components/PickerFieldUI'; +import { ClockIcon } from '../icons'; type TimeFieldComponent = (( props: TimeFieldProps & React.RefAttributes, @@ -36,38 +32,26 @@ const TimeField = React.forwardRef(function TimeField< const { slots, slotProps, InputProps, inputProps, ...other } = themeProps; - const ownerState = useFieldOwnerState(themeProps); - - const textFieldProps = useSlotProps({ - elementType: PickersTextField, - externalSlotProps: slotProps?.textField, - externalForwardedProps: other, - ownerState, - additionalProps: { + const textFieldProps = useFieldTextFieldProps>( + { + slotProps, ref: inRef, + externalForwardedProps: other, }, - }) as TimeFieldProps; - - // TODO: Remove when mui/material-ui#35088 will be merged - textFieldProps.inputProps = { ...inputProps, ...textFieldProps.inputProps }; - textFieldProps.InputProps = { ...InputProps, ...textFieldProps.InputProps }; + ); const fieldResponse = useTimeField( textFieldProps, ); - const convertedFieldResponse = convertFieldResponseIntoMuiTextFieldProps(fieldResponse); - - const processedFieldProps = useClearableField({ - ...convertedFieldResponse, - slots, - slotProps, - }); - - const TextField = - slots?.textField ?? - (fieldResponse.enableAccessibleFieldDOMStructure === false ? MuiTextField : PickersTextField); - return ; + return ( + + ); }) as TimeFieldComponent; TimeField.propTypes = { @@ -91,6 +75,12 @@ TimeField.propTypes = { * @default false */ clearable: PropTypes.bool, + /** + * The position at which the clear button is placed. + * If the field is not clearable, the button is not rendered. + * @default 'end' + */ + clearButtonPosition: PropTypes.oneOf(['end', 'start']), /** * The color of the component. * It supports both default and custom theme colors, which can be added as shown in the @@ -244,6 +234,12 @@ TimeField.propTypes = { * @param {FieldSelectedSections} newValue The new selected sections. */ onSelectedSectionsChange: PropTypes.func, + /** + * The position at which the opening button is placed. + * If there is no picker to open, the button is not rendered + * @default 'end' + */ + openPickerButtonPosition: PropTypes.oneOf(['end', 'start']), /** * If `true`, the component is read-only. * When read-only, the value cannot be changed but the user can interact with the interface. diff --git a/packages/x-date-pickers/src/TimeField/TimeField.types.ts b/packages/x-date-pickers/src/TimeField/TimeField.types.ts index 819535baa8fb5..ce58f46cd7e27 100644 --- a/packages/x-date-pickers/src/TimeField/TimeField.types.ts +++ b/packages/x-date-pickers/src/TimeField/TimeField.types.ts @@ -1,17 +1,14 @@ -import * as React from 'react'; -import type { TextFieldProps } from '@mui/material/TextField'; -import { MakeOptional, SlotComponentPropsFromProps } from '@mui/x-internals/types'; +import { MakeOptional } from '@mui/x-internals/types'; import { UseFieldInternalProps } from '../internals/hooks/useField'; -import { TimeValidationError, BuiltInFieldTextFieldProps, FieldOwnerState } from '../models'; -import { - ExportedUseClearableFieldProps, - UseClearableFieldSlots, - UseClearableFieldSlotProps, -} from '../hooks/useClearableField'; +import { TimeValidationError, BuiltInFieldTextFieldProps } from '../models'; import { ExportedValidateTimeProps } from '../validation/validateTime'; import { AmPmProps } from '../internals/models/props/time'; import { PickerValue } from '../internals/models'; -import { PickersTextFieldProps } from '../PickersTextField'; +import { + ExportedPickerFieldUIProps, + PickerFieldUISlotProps, + PickerFieldUISlots, +} from '../internals/components/PickerFieldUI'; export interface UseTimeFieldProps extends MakeOptional< @@ -19,7 +16,7 @@ export interface UseTimeFieldProps, ExportedValidateTimeProps, - ExportedUseClearableFieldProps, + ExportedPickerFieldUIProps, AmPmProps {} export type TimeFieldProps = @@ -42,18 +39,6 @@ export type TimeFieldProps, or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`. - */ - textField?: React.ElementType; -} +export interface TimeFieldSlots extends PickerFieldUISlots {} -export interface TimeFieldSlotProps extends UseClearableFieldSlotProps { - textField?: SlotComponentPropsFromProps< - PickersTextFieldProps | TextFieldProps, - {}, - FieldOwnerState - >; -} +export interface TimeFieldSlotProps extends PickerFieldUISlotProps {} diff --git a/packages/x-date-pickers/src/TimeField/useTimeField.ts b/packages/x-date-pickers/src/TimeField/useTimeField.ts index 5b6e155433d54..78630fb8e8e79 100644 --- a/packages/x-date-pickers/src/TimeField/useTimeField.ts +++ b/packages/x-date-pickers/src/TimeField/useTimeField.ts @@ -30,5 +30,6 @@ export const useTimeField = < fieldValueManager: manager.internal_fieldValueManager, validator: manager.validator, valueType: manager.valueType, + getOpenPickerButtonAriaLabel: manager.internal_getOpenPickerButtonAriaLabel, }); }; diff --git a/packages/x-date-pickers/src/TimePicker/tests/TimePicker.test.tsx b/packages/x-date-pickers/src/TimePicker/tests/TimePicker.test.tsx index aaa91fd391fa5..d8c17b9ef0bc3 100644 --- a/packages/x-date-pickers/src/TimePicker/tests/TimePicker.test.tsx +++ b/packages/x-date-pickers/src/TimePicker/tests/TimePicker.test.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; -import { TimePicker } from '@mui/x-date-pickers/TimePicker'; -import { screen } from '@mui/internal-test-utils/createRenderer'; import { expect } from 'chai'; +import { TimePicker } from '@mui/x-date-pickers/TimePicker'; +import { fireEvent, 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(); + const { render } = createPickerRenderer({ clock: 'fake' }); it('should render in mobile mode when `useMediaQuery` returns `false`', () => { const originalMatchMedia = window.matchMedia; @@ -14,7 +13,8 @@ describe('', () => { render(); - expect(screen.getByLabelText(/Choose time/)).to.have.class(pickersInputBaseClasses.input); + fireEvent.click(screen.getByLabelText(/Choose time/)); + expect(screen.queryByRole('dialog')).to.not.equal(null); window.matchMedia = originalMatchMedia; }); diff --git a/packages/x-date-pickers/src/internals/components/PickerFieldUI.tsx b/packages/x-date-pickers/src/internals/components/PickerFieldUI.tsx new file mode 100644 index 0000000000000..3f78963434412 --- /dev/null +++ b/packages/x-date-pickers/src/internals/components/PickerFieldUI.tsx @@ -0,0 +1,500 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import resolveComponentProps from '@mui/utils/resolveComponentProps'; +import MuiTextField, { TextFieldProps } from '@mui/material/TextField'; +import MuiIconButton, { IconButtonProps } from '@mui/material/IconButton'; +import MuiInputAdornment, { InputAdornmentProps } from '@mui/material/InputAdornment'; +import { SvgIconProps } from '@mui/material/SvgIcon'; +import useSlotProps from '@mui/utils/useSlotProps'; +import { SlotComponentPropsFromProps } from '@mui/x-internals/types'; +import { FieldOwnerState } from '../../models'; +import { useFieldOwnerState, UseFieldOwnerStateParameters } from '../hooks/useFieldOwnerState'; +import { usePickerTranslations } from '../../hooks'; +import { ClearIcon as MuiClearIcon } from '../../icons'; +import { useNullablePickerContext } from '../hooks/useNullablePickerContext'; +import type { UseFieldResponse } from '../hooks/useField'; +import { PickersTextField, PickersTextFieldProps } from '../../PickersTextField'; + +export const cleanFieldResponse = < + TFieldResponse extends UseFieldResponse, +>({ + enableAccessibleFieldDOMStructure, + ...fieldResponse +}: TFieldResponse): ExportedPickerFieldUIProps & { + openPickerAriaLabel: string; + textFieldProps: TextFieldProps | PickersTextFieldProps; +} => { + if (enableAccessibleFieldDOMStructure) { + const { + InputProps, + readOnly, + onClear, + clearable, + clearButtonPosition, + openPickerButtonPosition, + openPickerAriaLabel, + ...other + } = fieldResponse; + + return { + clearable, + onClear, + clearButtonPosition, + openPickerButtonPosition, + openPickerAriaLabel, + textFieldProps: { + ...other, + InputProps: { ...(InputProps ?? {}), readOnly }, + }, + }; + } + + const { + onPaste, + onKeyDown, + inputMode, + readOnly, + InputProps, + inputProps, + inputRef, + onClear, + clearable, + clearButtonPosition, + openPickerButtonPosition, + openPickerAriaLabel, + ...other + } = fieldResponse; + + return { + clearable, + onClear, + clearButtonPosition, + openPickerButtonPosition, + openPickerAriaLabel, + textFieldProps: { + ...other, + InputProps: { ...(InputProps ?? {}), readOnly }, + inputProps: { ...(inputProps ?? {}), inputMode, onPaste, onKeyDown, ref: inputRef }, + }, + }; +}; + +const PickerFieldUIContext = React.createContext({ + slots: {}, + slotProps: {}, +}); + +/** + * Adds the button to open the picker and the button to clear the value of the field. + * @ignore - internal component. + */ +export function PickerFieldUI(props: PickerFieldUIProps) { + const { slots, slotProps, fieldResponse, defaultOpenPickerIcon } = props; + + const translations = usePickerTranslations(); + const pickerContext = useNullablePickerContext(); + const pickerFieldUIContext = React.useContext(PickerFieldUIContext); + const { + textFieldProps, + onClear, + clearable, + openPickerAriaLabel, + clearButtonPosition: clearButtonPositionProp = 'end', + openPickerButtonPosition: openPickerButtonPositionProp = 'end', + } = cleanFieldResponse(fieldResponse); + const ownerState = useFieldOwnerState(textFieldProps); + + const handleClickOpeningButton = useEventCallback((event: React.MouseEvent) => { + event.preventDefault(); + pickerContext?.setOpen((prev) => !prev); + }); + + const triggerStatus = pickerContext ? pickerContext.triggerStatus : 'hidden'; + const clearButtonPosition = clearable ? clearButtonPositionProp : null; + const openPickerButtonPosition = triggerStatus !== 'hidden' ? openPickerButtonPositionProp : null; + + const TextField = + slots?.textField ?? + pickerFieldUIContext.slots.textField ?? + (fieldResponse.enableAccessibleFieldDOMStructure === false ? MuiTextField : PickersTextField); + + const InputAdornment = + slots?.inputAdornment ?? pickerFieldUIContext.slots.inputAdornment ?? MuiInputAdornment; + const { ownerState: startInputAdornmentOwnerState, ...startInputAdornmentProps } = useSlotProps({ + elementType: InputAdornment, + externalSlotProps: mergeSlotProps( + pickerFieldUIContext.slotProps.inputAdornment, + slotProps?.inputAdornment, + ), + additionalProps: { + position: 'start' as const, + }, + ownerState: { ...ownerState, position: 'start' }, + }); + const { ownerState: endInputAdornmentOwnerState, ...endInputAdornmentProps } = useSlotProps({ + elementType: InputAdornment, + externalSlotProps: slotProps?.inputAdornment, + additionalProps: { + position: 'end' as const, + }, + ownerState: { ...ownerState, position: 'end' }, + }); + + const OpenPickerButton = pickerFieldUIContext.slots.openPickerButton ?? MuiIconButton; + // We don't want to forward the `ownerState` to the `` component, see mui/material-ui#34056 + const { + ownerState: openPickerButtonOwnerState, + ...openPickerButtonProps + }: IconButtonProps & { ownerState: any } = useSlotProps({ + elementType: OpenPickerButton, + externalSlotProps: pickerFieldUIContext.slotProps.openPickerButton, + additionalProps: { + disabled: triggerStatus === 'disabled', + onClick: handleClickOpeningButton, + 'aria-label': openPickerAriaLabel, + edge: + clearButtonPosition === 'start' && openPickerButtonPosition === 'start' + ? undefined + : openPickerButtonPosition, + }, + ownerState, + }); + + const OpenPickerIcon = pickerFieldUIContext.slots.openPickerIcon ?? defaultOpenPickerIcon; + const openPickerIconProps = useSlotProps({ + elementType: OpenPickerIcon, + externalSlotProps: pickerFieldUIContext.slotProps.openPickerIcon, + ownerState, + }); + + const ClearButton = slots?.clearButton ?? pickerFieldUIContext.slots.clearButton ?? MuiIconButton; + // We don't want to forward the `ownerState` to the `` component, see mui/material-ui#34056 + const { ownerState: clearButtonOwnerState, ...clearButtonProps } = useSlotProps({ + elementType: ClearButton, + externalSlotProps: mergeSlotProps( + pickerFieldUIContext.slotProps.clearButton, + slotProps?.clearButton, + ), + className: 'clearButton', + additionalProps: { + title: translations.fieldClearLabel, + tabIndex: -1, + onClick: onClear, + disabled: fieldResponse.disabled || fieldResponse.readOnly, + edge: + clearButtonPosition === 'end' && openPickerButtonPosition === 'end' + ? undefined + : clearButtonPosition, + }, + ownerState, + }); + + const ClearIcon = slots?.clearIcon ?? pickerFieldUIContext.slots.clearIcon ?? MuiClearIcon; + const clearIconProps = useSlotProps({ + elementType: ClearIcon, + externalSlotProps: mergeSlotProps( + pickerFieldUIContext.slotProps.clearIcon, + slotProps?.clearIcon, + ), + additionalProps: { + fontSize: 'small', + }, + ownerState, + }); + + if (!textFieldProps.InputProps) { + textFieldProps.InputProps = {}; + } + + if (pickerContext) { + textFieldProps.InputProps.ref = pickerContext.triggerRef; + } + + if ( + !textFieldProps.InputProps?.startAdornment && + (clearButtonPosition === 'start' || openPickerButtonPosition === 'start') + ) { + textFieldProps.InputProps.startAdornment = ( + + {openPickerButtonPosition === 'start' && ( + + + + )} + {clearButtonPosition === 'start' && ( + + + + )} + + ); + } + + if ( + !textFieldProps.InputProps?.endAdornment && + (clearButtonPosition === 'end' || openPickerButtonPosition === 'end') + ) { + textFieldProps.InputProps.endAdornment = ( + + {clearButtonPosition === 'end' && ( + + + + )} + {openPickerButtonPosition === 'end' && ( + + + + )} + + ); + } + + if (clearButtonPosition != null) { + textFieldProps.sx = [ + { + '& .clearButton': { + opacity: 1, + }, + '@media (pointer: fine)': { + '& .clearButton': { + opacity: 0, + }, + '&:hover, &:focus-within': { + '.clearButton': { + opacity: 1, + }, + }, + }, + }, + ...(Array.isArray(textFieldProps.sx) ? textFieldProps.sx : [textFieldProps.sx]), + ]; + } + + return ; +} + +export interface ExportedPickerFieldUIProps { + /** + * If `true`, a clear button will be shown in the field allowing value clearing. + * @default false + */ + clearable?: boolean; + /** + * Callback fired when the clear button is clicked. + */ + onClear?: React.MouseEventHandler; + /** + * The position at which the clear button is placed. + * If the field is not clearable, the button is not rendered. + * @default 'end' + */ + clearButtonPosition?: 'start' | 'end'; + /** + * The position at which the opening button is placed. + * If there is no picker to open, the button is not rendered + * @default 'end' + */ + openPickerButtonPosition?: 'start' | 'end'; +} + +export interface PickerFieldUIProps { + /** + * Overridable component slots. + * @default {} + */ + slots?: PickerFieldUISlots; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: PickerFieldUISlotProps; + /** + * Object returned by the `useField` hook or one of its wrapper (for example `useDateField`). + */ + fieldResponse: UseFieldResponse; + /** + * The component to use to render the picker opening icon if none is provided in the picker's slots. + */ + defaultOpenPickerIcon: React.ElementType; +} + +export interface PickerFieldUISlots { + /** + * Form control with an input to render the value. + * @default , or from '@mui/material' if `enableAccessibleFieldDOMStructure` is `false`. + */ + textField?: React.ElementType; + /** + * Component displayed on the start or end input adornment used to open the picker on desktop. + * @default InputAdornment + */ + inputAdornment?: React.ElementType; + /** + * Icon to display inside the clear button. + * @default ClearIcon + */ + clearIcon?: React.ElementType; + /** + * Button to clear the value. + * @default IconButton + */ + clearButton?: React.ElementType; +} + +export interface PickerFieldUISlotsFromContext extends PickerFieldUISlots { + /** + * Button to open the picker on desktop. + * @default IconButton + */ + openPickerButton?: React.ElementType; + /** + * Icon displayed in the open picker button on desktop. + */ + openPickerIcon?: React.ElementType; +} + +export interface PickerFieldUISlotProps { + textField?: SlotComponentPropsFromProps< + PickersTextFieldProps | TextFieldProps, + {}, + FieldOwnerState + >; + inputAdornment?: SlotComponentPropsFromProps< + InputAdornmentProps, + {}, + FieldInputAdornmentOwnerState + >; + clearIcon?: SlotComponentPropsFromProps; + clearButton?: SlotComponentPropsFromProps; +} + +export interface PickerFieldUISlotPropsFromContext extends PickerFieldUISlotProps { + openPickerButton?: SlotComponentPropsFromProps; + openPickerIcon?: SlotComponentPropsFromProps; +} + +interface FieldInputAdornmentOwnerState extends FieldOwnerState { + position: 'start' | 'end'; +} + +interface PickerFieldUIContextValue { + slots: PickerFieldUISlotsFromContext; + slotProps: PickerFieldUISlotPropsFromContext; +} + +function mergeSlotProps( + slotPropsA: SlotComponentPropsFromProps | undefined, + slotPropsB: SlotComponentPropsFromProps | undefined, +) { + if (!slotPropsA) { + return slotPropsB; + } + + if (!slotPropsB) { + return slotPropsA; + } + + return (ownerState: TOwnerState) => { + return { + ...resolveComponentProps(slotPropsB, ownerState), + ...resolveComponentProps(slotPropsA, ownerState), + }; + }; +} + +/** + * The `textField` slot props cannot be handled inside `PickerFieldUI` because it would be a breaking change to not pass the enriched props to `useField`. + * Once the non-accessible DOM structure will be removed, we will be able to remove the `textField` slot and clean this logic. + */ +export function useFieldTextFieldProps< + TProps extends UseFieldOwnerStateParameters & { inputProps?: {}; InputProps?: {} }, +>(parameters: UseFieldTextFieldPropsParameters) { + const { ref, externalForwardedProps, slotProps } = parameters; + const pickerFieldUIContext = React.useContext(PickerFieldUIContext); + const ownerState = useFieldOwnerState(externalForwardedProps); + + const { InputProps, inputProps, ...otherExternalForwardedProps } = externalForwardedProps; + + const textFieldProps = useSlotProps({ + elementType: PickersTextField, + externalSlotProps: mergeSlotProps( + pickerFieldUIContext.slotProps.textField as any, + slotProps?.textField as any, + ), + externalForwardedProps: otherExternalForwardedProps, + additionalProps: { + ref, + }, + ownerState, + }) as any as TProps; + + // TODO: Remove when mui/material-ui#35088 will be merged + textFieldProps.inputProps = { ...inputProps, ...textFieldProps.inputProps }; + textFieldProps.InputProps = { ...InputProps, ...textFieldProps.InputProps }; + + return textFieldProps; +} + +interface UseFieldTextFieldPropsParameters { + slotProps: + | { + textField?: SlotComponentPropsFromProps< + PickersTextFieldProps | TextFieldProps, + {}, + FieldOwnerState + >; + } + | undefined; + ref: React.Ref; + externalForwardedProps: any; +} + +export function PickerFieldUIContextProvider(props: PickerFieldUIContextProviderProps) { + const { slots = {}, slotProps = {}, children } = props; + + const contextValue = React.useMemo( + () => ({ + slots: { + openPickerButton: slots.openPickerButton, + openPickerIcon: slots.openPickerIcon, + textField: slots.textField, + inputAdornment: slots.inputAdornment, + clearIcon: slots.clearIcon, + clearButton: slots.clearButton, + }, + slotProps: { + openPickerButton: slotProps.openPickerButton, + openPickerIcon: slotProps.openPickerIcon, + textField: slotProps.textField, + inputAdornment: slotProps.inputAdornment, + clearIcon: slotProps.clearIcon, + clearButton: slotProps.clearButton, + }, + }), + [ + slots.openPickerButton, + slots.openPickerIcon, + slots.textField, + slots.inputAdornment, + slots.clearIcon, + slots.clearButton, + slotProps.openPickerButton, + slotProps.openPickerIcon, + slotProps.textField, + slotProps.inputAdornment, + slotProps.clearIcon, + slotProps.clearButton, + ], + ); + + return ( + {children} + ); +} + +interface PickerFieldUIContextProviderProps { + children: React.ReactNode; + slots: PickerFieldUISlotsFromContext | undefined; + slotProps: PickerFieldUISlotPropsFromContext | undefined; +} diff --git a/packages/x-date-pickers/src/internals/components/PickerProvider.tsx b/packages/x-date-pickers/src/internals/components/PickerProvider.tsx index 16997021db476..30d4aa9be6735 100644 --- a/packages/x-date-pickers/src/internals/components/PickerProvider.tsx +++ b/packages/x-date-pickers/src/internals/components/PickerProvider.tsx @@ -118,6 +118,18 @@ export interface PickerContextValue< * Is always equal to "portrait" if the component you are accessing the context from is not wrapped by a picker. */ orientation: PickerOrientation; + /** + * The ref that should be attached to the element that triggers the Picker opening. + * When using a built-in field component, this property is automatically handled. + */ + triggerRef: React.RefObject; + /** + * The status of the element that triggers the Picker opening. + * If it is "hidden", the field should not render the UI to open the Picker. + * If it is "disabled", the field should render a disabled UI to open the Picker. + * If it is "enabled", the field should render an interactive UI to open the Picker. + */ + triggerStatus: 'hidden' | 'disabled' | 'enabled'; /** * Format that should be used to render the value in the field. * Is equal to `props.format` on the picker component if defined. diff --git a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx index eed6bbba05946..8749e675362ee 100644 --- a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx @@ -1,7 +1,5 @@ import * as React from 'react'; import useSlotProps from '@mui/utils/useSlotProps'; -import MuiInputAdornment from '@mui/material/InputAdornment'; -import IconButton from '@mui/material/IconButton'; import useForkRef from '@mui/utils/useForkRef'; import useId from '@mui/utils/useId'; import { PickersPopper } from '../../components/PickersPopper'; @@ -11,6 +9,7 @@ import { PickersLayout } from '../../../PickersLayout'; import { FieldRef, InferError } from '../../../models'; import { DateOrTimeViewWithMeridiem, BaseSingleInputFieldProps, PickerValue } from '../../models'; import { PickerProvider } from '../../components/PickerProvider'; +import { PickerFieldUIContextProvider } from '../../components/PickerFieldUI'; /** * Hook managing all the single-date desktop pickers: @@ -29,7 +28,6 @@ export const useDesktopPicker = < >, >({ props, - getOpenDialogAriaText, ...pickerParams }: UseDesktopPickerParams) => { const { @@ -41,19 +39,17 @@ export const useDesktopPicker = < label, inputRef, readOnly, - disabled, autoFocus, localeText, reduceAnimations, } = props; - const containerRef = React.useRef(null); const fieldRef = React.useRef>(null); const labelId = useId(); const isToolbarHidden = innerSlotProps?.toolbar?.hidden ?? false; - const { hasUIView, providerProps, renderCurrentView, shouldRestoreFocus, ownerState } = usePicker< + const { providerProps, renderCurrentView, shouldRestoreFocus, ownerState } = usePicker< PickerValue, TView, TExternalProps @@ -66,40 +62,6 @@ export const useDesktopPicker = < variant: 'desktop', }); - const InputAdornment = slots.inputAdornment ?? MuiInputAdornment; - const { ownerState: inputAdornmentOwnerState, ...inputAdornmentProps } = useSlotProps({ - elementType: InputAdornment, - externalSlotProps: innerSlotProps?.inputAdornment, - additionalProps: { - position: 'end' as const, - }, - ownerState, - }); - - const OpenPickerButton = slots.openPickerButton ?? IconButton; - const { ownerState: openPickerButtonOwnerState, ...openPickerButtonProps } = useSlotProps({ - elementType: OpenPickerButton, - externalSlotProps: innerSlotProps?.openPickerButton, - additionalProps: { - disabled: disabled || readOnly, - // This direct access to `providerProps` will go away in /~https://github.com/mui/mui-x/pull/15671 - onClick: (event: React.UIEvent) => { - event.preventDefault(); - providerProps.contextValue.setOpen((prevOpen) => !prevOpen); - }, - 'aria-label': getOpenDialogAriaText(providerProps.contextValue.value), - edge: inputAdornmentProps.position, - }, - ownerState, - }); - - const OpenPickerIcon = slots.openPickerIcon; - const openPickerIconProps = useSlotProps({ - elementType: OpenPickerIcon, - externalSlotProps: innerSlotProps?.openPickerIcon, - ownerState, - }); - const Field = slots.field; const fieldProps: BaseSingleInputFieldProps< PickerValue, @@ -125,30 +87,6 @@ export const useDesktopPicker = < ownerState, }); - // TODO: Move to `useSlotProps` when /~https://github.com/mui/material-ui/pull/35088 will be merged - if (hasUIView) { - fieldProps.InputProps = { - ...fieldProps.InputProps, - ref: containerRef, - ...(!props.disableOpenPicker && { - [`${inputAdornmentProps.position}Adornment`]: ( - - - - - - ), - }), - } as typeof fieldProps.InputProps; - } - - const slotsForField = { - textField: slots.textField, - clearIcon: slots.clearIcon, - clearButton: slots.clearButton, - ...fieldProps.slots, - }; - const Layout = slots.layout ?? PickersLayout; let labelledById = labelId; @@ -175,25 +113,22 @@ export const useDesktopPicker = < const renderPicker = () => ( - - - - {renderCurrentView()} - - + + + + + {renderCurrentView()} + + + ); diff --git a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.types.ts b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.types.ts index b8a7f5d0d8e07..58d97811de6cc 100644 --- a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.types.ts @@ -1,7 +1,4 @@ import * as React from 'react'; -import type { IconButtonProps } from '@mui/material/IconButton'; -import type { InputAdornmentProps } from '@mui/material/InputAdornment'; -import type { TextFieldProps } from '@mui/material/TextField'; import { MakeRequired, SlotComponentPropsFromProps } from '@mui/x-internals/types'; import { BasePickerProps, @@ -9,12 +6,7 @@ import { } from '../../models/props/basePickerProps'; import { PickersPopperSlots, PickersPopperSlotProps } from '../../components/PickersPopper'; import { UsePickerParams } from '../usePicker'; -import { - FieldOwnerState, - PickerFieldSlotProps, - PickerOwnerState, - PickerValidDate, -} from '../../../models'; +import { PickerFieldSlotProps, PickerOwnerState } from '../../../models'; import { ExportedPickersLayoutSlots, ExportedPickersLayoutSlotProps, @@ -24,10 +16,9 @@ import { UsePickerValueNonStaticProps } from '../usePicker/usePickerValue.types' import { UsePickerViewsProps } from '../usePicker/usePickerViews'; import { DateOrTimeViewWithMeridiem, PickerValue } from '../../models'; import { - UseClearableFieldSlots, - UseClearableFieldSlotProps, -} from '../../../hooks/useClearableField'; -import { PickersTextFieldProps } from '../../../PickersTextField'; + PickerFieldUISlotsFromContext, + PickerFieldUISlotPropsFromContext, +} from '../../components/PickerFieldUI'; import { UsePickerProviderNonStaticProps } from '../usePicker/usePickerProvider'; export interface UseDesktopPickerSlots @@ -36,7 +27,7 @@ export interface UseDesktopPickerSlots 'desktopPaper' | 'desktopTransition' | 'desktopTrapFocus' | 'popper' >, ExportedPickersLayoutSlots, - UseClearableFieldSlots { + PickerFieldUISlotsFromContext { /** * Component used to enter the date with the keyboard. */ @@ -46,46 +37,24 @@ export interface UseDesktopPickerSlots * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. */ textField?: React.ElementType; - /** - * Component displayed on the start or end input adornment used to open the picker on desktop. - * @default InputAdornment - */ - inputAdornment?: React.ElementType; - /** - * Button to open the picker on desktop. - * @default IconButton - */ - openPickerButton?: React.ElementType; - /** - * Icon displayed in the open picker button on desktop. - */ - openPickerIcon: React.ElementType; } -export interface UseDesktopPickerSlotProps - extends ExportedUseDesktopPickerSlotProps, - Pick, 'toolbar'> {} - export interface ExportedUseDesktopPickerSlotProps< TEnableAccessibleFieldDOMStructure extends boolean, > extends PickersPopperSlotProps, ExportedPickersLayoutSlotProps, - UseClearableFieldSlotProps { + PickerFieldUISlotPropsFromContext { field?: SlotComponentPropsFromProps< PickerFieldSlotProps, {}, PickerOwnerState >; - textField?: SlotComponentPropsFromProps< - PickersTextFieldProps | TextFieldProps, - {}, - FieldOwnerState - >; - inputAdornment?: SlotComponentPropsFromProps; - openPickerButton?: SlotComponentPropsFromProps; - openPickerIcon?: SlotComponentPropsFromProps, {}, PickerOwnerState>; } +export interface UseDesktopPickerSlotProps + extends ExportedUseDesktopPickerSlotProps, + Pick, 'toolbar'> {} + export interface DesktopOnlyPickerProps extends BaseNonRangeNonStaticPickerProps, UsePickerValueNonStaticProps, @@ -130,5 +99,4 @@ export interface UseDesktopPickerParams< 'valueManager' | 'valueType' | 'validator' | 'rendererInterceptor' > { props: TExternalProps; - getOpenDialogAriaText: (date: PickerValidDate | null) => string; } diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts index 8bec0c4e9afa6..3f92bef606d7b 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts @@ -3,7 +3,7 @@ import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import useEventCallback from '@mui/utils/useEventCallback'; import { useRtl } from '@mui/system/RtlProvider'; import { useValidation } from '../../../validation'; -import { useUtils } from '../useUtils'; +import { useLocalizationContext, useUtils } from '../useUtils'; import { UseFieldParams, UseFieldResponse, @@ -52,6 +52,7 @@ export const useField = < fieldValueManager, valueManager, validator, + getOpenPickerButtonAriaLabel: getOpenDialogAriaText, } = params; const isRtl = useRtl(); @@ -279,9 +280,16 @@ export const useField = < clearable: Boolean(clearable && !areAllSectionsEmpty && !readOnly && !disabled), }; + const localizationContext = useLocalizationContext(); + const openPickerAriaLabel = React.useMemo( + () => getOpenDialogAriaText({ ...localizationContext, value: state.value }), + [getOpenDialogAriaText, state.value, localizationContext], + ); + const commonAdditionalProps: UseFieldCommonAdditionalProps = { disabled, readOnly, + openPickerAriaLabel, }; return { diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts index e286389178177..2b9dbd7d3598c 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts @@ -18,8 +18,9 @@ import type { Validator } from '../../../validation'; import type { UseFieldStateResponse } from './useFieldState'; import type { UseFieldCharacterEditingResponse } from './useFieldCharacterEditing'; import { PickersSectionElement, PickersSectionListRef } from '../../../PickersSectionList'; -import { ExportedUseClearableFieldProps } from '../../../hooks/useClearableField'; import { FormProps, InferNonNullablePickerValue, PickerValidValue } from '../../models'; +import type { ExportedPickerFieldUIProps } from '../../components/PickerFieldUI'; +import { UseLocalizationContextReturnValue } from '../useUtils'; export interface UseFieldParams< TValue extends PickerValidValue, @@ -34,6 +35,9 @@ export interface UseFieldParams< fieldValueManager: FieldValueManager; validator: Validator, TInternalProps>; valueType: PickerValueType; + getOpenPickerButtonAriaLabel: ( + parameters: UseLocalizationContextReturnValue & { value: TValue }, + ) => string; } export interface UseFieldInternalProps< @@ -122,9 +126,15 @@ export interface UseFieldInternalProps< } export interface UseFieldCommonAdditionalProps - extends Required, 'disabled' | 'readOnly'>> {} + extends Required, 'disabled' | 'readOnly'>> { + /** + * The aria label to set on the button that opens the picker. + */ + openPickerAriaLabel: string; +} -export interface UseFieldCommonForwardedProps extends ExportedUseClearableFieldProps { +export interface UseFieldCommonForwardedProps + extends Pick { onKeyDown?: React.KeyboardEventHandler; error?: boolean; } diff --git a/packages/x-date-pickers/src/internals/hooks/useFieldOwnerState.ts b/packages/x-date-pickers/src/internals/hooks/useFieldOwnerState.ts index 9ec96ac340d73..16f9c3ce49905 100644 --- a/packages/x-date-pickers/src/internals/hooks/useFieldOwnerState.ts +++ b/packages/x-date-pickers/src/internals/hooks/useFieldOwnerState.ts @@ -20,6 +20,6 @@ export function useFieldOwnerState(parameters: UseFieldOwnerStateParameters) { ); } -interface UseFieldOwnerStateParameters extends FormProps { +export interface UseFieldOwnerStateParameters extends FormProps { required?: boolean; } diff --git a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx index 495276ff0aa50..f484f461a0075 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx @@ -5,11 +5,11 @@ import useId from '@mui/utils/useId'; import { PickersModalDialog } from '../../components/PickersModalDialog'; import { UseMobilePickerParams, UseMobilePickerProps } from './useMobilePicker.types'; import { usePicker } from '../usePicker'; -import { onSpaceOrEnter } from '../../utils/utils'; import { PickersLayout } from '../../../PickersLayout'; import { FieldRef, InferError } from '../../../models'; import { BaseSingleInputFieldProps, DateOrTimeViewWithMeridiem, PickerValue } from '../../models'; import { PickerProvider } from '../../components/PickerProvider'; +import { PickerFieldUIContextProvider } from '../../components/PickerFieldUI'; /** * Hook managing all the single-date mobile pickers: @@ -28,7 +28,6 @@ export const useMobilePicker = < >, >({ props, - getOpenDialogAriaText, ...pickerParams }: UseMobilePickerParams) => { const { @@ -40,7 +39,7 @@ export const useMobilePicker = < label, inputRef, readOnly, - disabled, + autoFocus, localeText, } = props; @@ -72,38 +71,21 @@ export const useMobilePicker = < externalSlotProps: innerSlotProps?.field, additionalProps: { // Internal props - readOnly: readOnly ?? true, + readOnly, + autoFocus: autoFocus && !props.open, // Forwarded props className, sx, label, name, + focused: providerProps.contextValue.open ? true : undefined, ...(isToolbarHidden && { id: labelId }), - ...(!(disabled || readOnly) && { - // These direct access to `providerProps` will go away in /~https://github.com/mui/mui-x/pull/15671 - onClick: (event: React.UIEvent) => { - event.preventDefault(); - providerProps.contextValue.setOpen(true); - }, - onKeyDown: onSpaceOrEnter(() => providerProps.contextValue.setOpen(true)), - }), ...(!!inputRef && { inputRef }), }, ownerState, }); - // TODO: Move to `useSlotProps` when /~https://github.com/mui/material-ui/pull/35088 will be merged - fieldProps.inputProps = { - ...fieldProps.inputProps, - 'aria-label': getOpenDialogAriaText(providerProps.contextValue.value), - } as typeof fieldProps.inputProps; - - const slotsForField = { - textField: slots.textField, - ...fieldProps.slots, - }; - const Layout = slots.layout ?? PickersLayout; let labelledById = labelId; @@ -130,17 +112,14 @@ export const useMobilePicker = < const renderPicker = () => ( - - - - {renderCurrentView()} - - + + + + + {renderCurrentView()} + + + ); diff --git a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.types.ts b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.types.ts index c169f6713f5be..3398e2d1dfdb2 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.types.ts @@ -1,5 +1,4 @@ import * as React from 'react'; -import type { TextFieldProps } from '@mui/material/TextField'; import { MakeRequired, SlotComponentPropsFromProps } from '@mui/x-internals/types'; import { BasePickerProps, @@ -10,12 +9,7 @@ import { PickersModalDialogSlotProps, } from '../../components/PickersModalDialog'; import { UsePickerParams } from '../usePicker'; -import { - FieldOwnerState, - PickerFieldSlotProps, - PickerOwnerState, - PickerValidDate, -} from '../../../models'; +import { PickerFieldSlotProps, PickerOwnerState } from '../../../models'; import { ExportedPickersLayoutSlots, ExportedPickersLayoutSlotProps, @@ -24,37 +18,32 @@ import { import { UsePickerValueNonStaticProps } from '../usePicker/usePickerValue.types'; import { UsePickerViewsProps } from '../usePicker/usePickerViews'; import { DateOrTimeViewWithMeridiem, PickerValue } from '../../models'; -import { PickersTextFieldProps } from '../../../PickersTextField'; import { UsePickerProviderNonStaticProps } from '../usePicker/usePickerProvider'; +import { + PickerFieldUISlotsFromContext, + PickerFieldUISlotPropsFromContext, +} from '../../components/PickerFieldUI'; export interface UseMobilePickerSlots extends PickersModalDialogSlots, - ExportedPickersLayoutSlots { + ExportedPickersLayoutSlots, + PickerFieldUISlotsFromContext { /** * Component used to enter the date with the keyboard. */ field: React.ElementType; - /** - * Form control with an input to render the value inside the default field. - * @default TextField from '@mui/material' or PickersTextField if `enableAccessibleFieldDOMStructure` is `true`. - */ - textField?: React.ElementType; } export interface ExportedUseMobilePickerSlotProps< TEnableAccessibleFieldDOMStructure extends boolean, > extends PickersModalDialogSlotProps, - ExportedPickersLayoutSlotProps { + ExportedPickersLayoutSlotProps, + PickerFieldUISlotPropsFromContext { field?: SlotComponentPropsFromProps< PickerFieldSlotProps, {}, PickerOwnerState >; - textField?: SlotComponentPropsFromProps< - PickersTextFieldProps | TextFieldProps, - {}, - FieldOwnerState - >; } export interface UseMobilePickerSlotProps @@ -99,5 +88,4 @@ export interface UseMobilePickerParams< 'valueManager' | 'valueType' | 'validator' > { props: TExternalProps; - getOpenDialogAriaText: (date: PickerValidDate | null) => string; } diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts index 06f6408ab5c1e..a27c1306f22a2 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts @@ -56,7 +56,6 @@ export const usePicker = < return { // Picker views renderCurrentView: pickerViewsResponse.renderCurrentView, - hasUIView: pickerViewsResponse.provider.hasUIView, shouldRestoreFocus: pickerViewsResponse.shouldRestoreFocus, // Picker provider diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts index 96bf2f4673a65..4bf6424a9de93 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts @@ -63,6 +63,4 @@ export interface UsePickerResponse< > extends Pick, 'shouldRestoreFocus' | 'renderCurrentView'> { ownerState: PickerOwnerState; providerProps: UsePickerProviderReturnValue; - // TODO v8: Remove in /~https://github.com/mui/mui-x/pull/15671 - hasUIView: boolean; } diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts index 97be050ea82f9..efbbae1fb221b 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts @@ -79,6 +79,7 @@ export function usePickerProvider< const utils = useUtils(); const orientation = usePickerOrientation(paramsFromUsePickerViews.views, props.orientation); + const triggerRef = React.useRef(null); const ownerState = React.useMemo( () => ({ @@ -105,6 +106,18 @@ export function usePickerProvider< ], ); + const triggerStatus = React.useMemo(() => { + if (props.disableOpenPicker || !paramsFromUsePickerViews.hasUIView) { + return 'hidden'; + } + + if (props.disabled || props.readOnly) { + return 'disabled'; + } + + return 'enabled'; + }, [props.disableOpenPicker, paramsFromUsePickerViews.hasUIView, props.disabled, props.readOnly]); + const contextValue = React.useMemo>( () => ({ ...paramsFromUsePickerValue.contextValue, @@ -113,6 +126,8 @@ export function usePickerProvider< readOnly: props.readOnly ?? false, variant, orientation, + triggerRef, + triggerStatus, fieldFormat: props.format ?? '', }), [ @@ -122,6 +137,8 @@ export function usePickerProvider< orientation, props.disabled, props.readOnly, + triggerRef, + triggerStatus, props.format, ], ); diff --git a/packages/x-date-pickers/src/internals/hooks/useUtils.ts b/packages/x-date-pickers/src/internals/hooks/useUtils.ts index 814918372fcc6..d7e4aeb94179e 100644 --- a/packages/x-date-pickers/src/internals/hooks/useUtils.ts +++ b/packages/x-date-pickers/src/internals/hooks/useUtils.ts @@ -38,9 +38,7 @@ export const useLocalizationContext = () => { ({ ...localization, localeText, - }) as Omit & { - localeText: PickersLocaleText; - }, + }) as UseLocalizationContextReturnValue, [localization, localeText], ); }; @@ -59,3 +57,8 @@ export const useNow = (timezone: PickersTimezone): PickerValidDate => { return now.current!; }; + +export interface UseLocalizationContextReturnValue + extends Omit { + localeText: PickersLocaleText; +} diff --git a/packages/x-date-pickers/src/internals/index.ts b/packages/x-date-pickers/src/internals/index.ts index 2bbb31ef4487a..85394bf31fdd6 100644 --- a/packages/x-date-pickers/src/internals/index.ts +++ b/packages/x-date-pickers/src/internals/index.ts @@ -4,6 +4,17 @@ export type { PickersArrowSwitcherSlots, PickersArrowSwitcherSlotProps, } from './components/PickersArrowSwitcher'; +export { + PickerFieldUI, + PickerFieldUIContextProvider, + cleanFieldResponse, + useFieldTextFieldProps, +} from './components/PickerFieldUI'; +export type { + ExportedPickerFieldUIProps, + PickerFieldUISlots, + PickerFieldUISlotProps, +} from './components/PickerFieldUI'; export { PickerProvider } from './components/PickerProvider'; export type { PickerContextValue } from './components/PickerProvider'; export { PickersModalDialog } from './components/PickersModalDialog'; @@ -132,7 +143,6 @@ export type { PickerValidValue, } from './models/value'; -export { convertFieldResponseIntoMuiTextFieldProps } from './utils/convertFieldResponseIntoMuiTextFieldProps'; export { applyDefaultDate, replaceInvalidDateByNull, diff --git a/packages/x-date-pickers/src/internals/models/fields.ts b/packages/x-date-pickers/src/internals/models/fields.ts index 016c94b661d5f..44f0baa740eb0 100644 --- a/packages/x-date-pickers/src/internals/models/fields.ts +++ b/packages/x-date-pickers/src/internals/models/fields.ts @@ -1,19 +1,16 @@ import { SxProps } from '@mui/material/styles'; -import type { - ExportedUseClearableFieldProps, - UseClearableFieldSlotProps, - UseClearableFieldSlots, -} from '../../hooks/useClearableField'; import type { FieldSection, PickerOwnerState } from '../../models'; import type { UseFieldInternalProps } from '../hooks/useField'; import { RangePosition } from './pickers'; import { PickerValidValue } from './value'; +import type { ExportedPickerFieldUIProps } from '../components/PickerFieldUI'; export interface FieldRangeSection extends FieldSection { dateName: RangePosition; } -export interface BaseForwardedSingleInputFieldProps extends ExportedUseClearableFieldProps { +export interface BaseForwardedSingleInputFieldProps + extends Pick { className: string | undefined; sx: SxProps | undefined; label: React.ReactNode | undefined; @@ -24,18 +21,6 @@ export interface BaseForwardedSingleInputFieldProps extends ExportedUseClearable onBlur?: React.FocusEventHandler; ref?: React.Ref; inputRef?: React.Ref; - InputProps?: { - ref?: React.Ref; - endAdornment?: React.ReactNode; - startAdornment?: React.ReactNode; - }; - inputProps?: { - 'aria-label'?: string; - }; - slots?: UseClearableFieldSlots; - slotProps?: UseClearableFieldSlotProps & { - textField?: {}; - }; ownerState: PickerOwnerState; } diff --git a/packages/x-date-pickers/src/internals/utils/convertFieldResponseIntoMuiTextFieldProps.ts b/packages/x-date-pickers/src/internals/utils/convertFieldResponseIntoMuiTextFieldProps.ts deleted file mode 100644 index 7124c455941ab..0000000000000 --- a/packages/x-date-pickers/src/internals/utils/convertFieldResponseIntoMuiTextFieldProps.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { TextFieldProps } from '@mui/material/TextField'; -import { UseFieldResponse } from '../hooks/useField'; - -export const convertFieldResponseIntoMuiTextFieldProps = < - TFieldResponse extends UseFieldResponse, ->({ - enableAccessibleFieldDOMStructure, - ...fieldResponse -}: TFieldResponse): TextFieldProps => { - if (enableAccessibleFieldDOMStructure) { - const { InputProps, readOnly, ...other } = fieldResponse; - - return { - ...other, - InputProps: { ...(InputProps ?? {}), readOnly }, - } as any; - } - - const { onPaste, onKeyDown, inputMode, readOnly, InputProps, inputProps, inputRef, ...other } = - fieldResponse; - - return { - ...other, - InputProps: { ...(InputProps ?? {}), readOnly }, - inputProps: { ...(inputProps ?? {}), inputMode, onPaste, onKeyDown, ref: inputRef }, - } as any; -}; diff --git a/packages/x-date-pickers/src/locales/utils/getPickersLocalization.ts b/packages/x-date-pickers/src/locales/utils/getPickersLocalization.ts index 40ab3fe2714ae..a6cf8e68da86c 100644 --- a/packages/x-date-pickers/src/locales/utils/getPickersLocalization.ts +++ b/packages/x-date-pickers/src/locales/utils/getPickersLocalization.ts @@ -1,4 +1,3 @@ -import { AdapterFormats, MuiPickersAdapter, PickerValidDate } from '../../models'; import { PickersLocaleText } from './pickersLocaleTextApi'; export const getPickersLocalization = (pickersTranslations: Partial) => { @@ -12,18 +11,3 @@ export const getPickersLocalization = (pickersTranslations: Partial string; - propsTranslation: ((formattedValue: string | null) => string) | undefined; -}) => { - const { utils, formatKey, contextTranslation, propsTranslation } = params; - - return (value: PickerValidDate | null) => { - const formattedValue = utils.isValid(value) ? utils.format(value, formatKey) : null; - const translation = propsTranslation ?? contextTranslation; - return translation(formattedValue); - }; -}; diff --git a/packages/x-date-pickers/src/managers/useDateManager.ts b/packages/x-date-pickers/src/managers/useDateManager.ts index aec21fd0379bf..b99686fbd4d85 100644 --- a/packages/x-date-pickers/src/managers/useDateManager.ts +++ b/packages/x-date-pickers/src/managers/useDateManager.ts @@ -34,6 +34,10 @@ export function useDateManager { + const formattedValue = utils.isValid(value) ? utils.format(value, 'fullDate') : null; + return localeText.openDatePickerDialogue(formattedValue); + }, }), [enableAccessibleFieldDOMStructure], ); diff --git a/packages/x-date-pickers/src/managers/useDateTimeManager.ts b/packages/x-date-pickers/src/managers/useDateTimeManager.ts index 965333a10b704..dc8b37c307152 100644 --- a/packages/x-date-pickers/src/managers/useDateTimeManager.ts +++ b/packages/x-date-pickers/src/managers/useDateTimeManager.ts @@ -35,6 +35,10 @@ export function useDateTimeManager { + const formattedValue = utils.isValid(value) ? utils.format(value, 'fullDate') : null; + return localeText.openDatePickerDialogue(formattedValue); + }, }), [enableAccessibleFieldDOMStructure], ); diff --git a/packages/x-date-pickers/src/managers/useTimeManager.ts b/packages/x-date-pickers/src/managers/useTimeManager.ts index cfbccbabbfe33..5ce9df999af2c 100644 --- a/packages/x-date-pickers/src/managers/useTimeManager.ts +++ b/packages/x-date-pickers/src/managers/useTimeManager.ts @@ -34,6 +34,10 @@ export function useTimeManager { + const formattedValue = utils.isValid(value) ? utils.format(value, 'fullTime') : null; + return localeText.openTimePickerDialogue(formattedValue); + }, }), [enableAccessibleFieldDOMStructure], ); diff --git a/packages/x-date-pickers/src/models/fields.ts b/packages/x-date-pickers/src/models/fields.ts index eac8aedd90db1..2002619cb4e25 100644 --- a/packages/x-date-pickers/src/models/fields.ts +++ b/packages/x-date-pickers/src/models/fields.ts @@ -1,9 +1,5 @@ import * as React from 'react'; import { TextFieldProps } from '@mui/material/TextField'; -import type { - ExportedUseClearableFieldProps, - UseClearableFieldResponse, -} from '../hooks/useClearableField'; import type { ExportedPickersSectionListProps } from '../PickersSectionList'; import type { UseFieldInternalProps, UseFieldResponse } from '../internals/hooks/useField'; import type { PickersTextFieldProps } from '../PickersTextField'; @@ -14,6 +10,7 @@ import { PickerValidValue, } from '../internals/models'; import { PickerOwnerState } from './pickers'; +import type { ExportedPickerFieldUIProps } from '../internals/components/PickerFieldUI'; // Update PickersComponentAgnosticLocaleText -> viewNames when adding new entries export type FieldSectionType = @@ -160,7 +157,7 @@ export interface FieldOwnerState extends PickerOwnerState { export type PickerFieldSlotProps< TValue extends PickerValidValue, TEnableAccessibleFieldDOMStructure extends boolean, -> = ExportedUseClearableFieldProps & +> = ExportedPickerFieldUIProps & Pick< UseFieldInternalProps, 'shouldRespectLeadingZeros' | 'readOnly' @@ -170,13 +167,20 @@ export type PickerFieldSlotProps< }; /** - * Props the text field receives when used with a single input picker. + * Props the text field receives when used inside a single input picker. * Only contains what the MUI components are passing to the text field, not what users can pass using the `props.slotProps.field` and `props.slotProps.textField`. */ export type BaseSingleInputPickersTextFieldProps< TEnableAccessibleFieldDOMStructure extends boolean, -> = UseClearableFieldResponse< - UseFieldResponse +> = Omit< + UseFieldResponse, + | 'slots' + | 'slotProps' + | 'clearable' + | 'onClear' + | 'openPickerButtonPosition' + | 'clearButtonPosition' + | 'openPickerAriaLabel' >; /** diff --git a/packages/x-date-pickers/src/models/manager.ts b/packages/x-date-pickers/src/models/manager.ts index dd2346f71e953..c7a0c42db75ec 100644 --- a/packages/x-date-pickers/src/models/manager.ts +++ b/packages/x-date-pickers/src/models/manager.ts @@ -1,7 +1,7 @@ import type { FieldValueManager, UseFieldInternalProps } from '../internals/hooks/useField'; import type { PickerValueManager } from '../internals/hooks/usePicker'; +import type { UseLocalizationContextReturnValue } from '../internals/hooks/useUtils'; import type { PickerValidValue } from '../internals/models'; -import type { MuiPickersAdapterContextValue } from '../LocalizationProvider/LocalizationProvider'; import type { Validator } from '../validation'; import type { PickerValueType } from './common'; @@ -78,15 +78,28 @@ export interface PickerManager< * - a default format to display the value in the field * - some default validation props that are needed to validate the value (e.g: minDate, maxDate) * This property is not part of the public API and should not be used directly. - * @param {ApplyDefaultsToFieldInternalPropsParameters} parameters The parameters to apply the defaults. + * @param {ApplyDefaultsToFieldInternalPropsParameters} parameters The parameters to apply the defaults. * @returns {TFieldInternalPropsWithDefaults} The field internal props with the defaults applied. */ internal_applyDefaultsToFieldInternalProps: ( parameters: ApplyDefaultsToFieldInternalPropsParameters, ) => TFieldInternalPropsWithDefaults; + /** + * Returns the aria-label to apply on the button that opens the picker. + * @param {GetOpenPickerButtonAriaLabelParameters} params The parameters to get the aria-label. + * @returns {string} The aria-label to apply on the button that opens the picker. + */ + internal_getOpenPickerButtonAriaLabel: ( + params: GetOpenPickerButtonAriaLabelParameters, + ) => string; } interface ApplyDefaultsToFieldInternalPropsParameters - extends MuiPickersAdapterContextValue { + extends UseLocalizationContextReturnValue { internalProps: TFieldInternalProps; } + +interface GetOpenPickerButtonAriaLabelParameters + extends UseLocalizationContextReturnValue { + value: TValue; +} diff --git a/test/e2e/index.test.ts b/test/e2e/index.test.ts index a60c3ecfe0cb4..1616748cefe1e 100644 --- a/test/e2e/index.test.ts +++ b/test/e2e/index.test.ts @@ -13,7 +13,6 @@ import { WebError, Locator, } from '@playwright/test'; -import { pickersTextFieldClasses } from '@mui/x-date-pickers/PickersTextField'; import { pickersSectionListClasses } from '@mui/x-date-pickers/PickersSectionList'; function sleep(timeoutMS: number): Promise { @@ -799,19 +798,13 @@ async function initializeEnvironment( it('should allow selecting a value', async () => { await renderFixture('DatePicker/BasicMobileDatePicker'); - // Old selector: await page.getByRole('textbox').click({ position: { x: 10, y: 2 } }); - await page - .locator(`.${pickersTextFieldClasses.root}`) - .click({ position: { x: 10, y: 2 } }); + await page.getByRole('button').click(); await page.getByRole('gridcell', { name: '11' }).click(); await page.getByRole('button', { name: 'OK' }).click(); - await waitFor(async () => { - // assert that the dialog has been closed and the focused element is the input - expect(await page.evaluate(() => document.activeElement?.className)).to.contain( - pickersSectionListClasses.sectionContent, - ); - }); + // assert that the dialog closes after selection is complete + // could run into race condition otherwise + await page.waitForSelector('[role="dialog"]', { state: 'detached' }); expect(await page.getByRole('textbox', { includeHidden: true }).inputValue()).to.equal( '04/11/2022', ); @@ -824,7 +817,7 @@ async function initializeEnvironment( const input = page.getByRole('textbox'); - await input.click({ position: { x: 10, y: 2 } }); + await page.getByRole('button').click(); await page.getByRole('button', { name: 'Clear' }).click(); await input.blur(); diff --git a/test/utils/pickers/describePicker/describePicker.tsx b/test/utils/pickers/describePicker/describePicker.tsx index 5362abab6eaa5..53a3b203855bf 100644 --- a/test/utils/pickers/describePicker/describePicker.tsx +++ b/test/utils/pickers/describePicker/describePicker.tsx @@ -56,11 +56,7 @@ function innerDescribePicker(ElementToTest: React.ElementType, options: Describe />, ); - const shouldRenderOpenPickerIcon = !hasNoView && variant !== 'mobile'; - - expect(queryAllByTestId('component-test')).to.have.length( - shouldRenderOpenPickerIcon ? 1 : 0, - ); + expect(queryAllByTestId('component-test')).to.have.length(hasNoView ? 0 : 1); }, ); }); diff --git a/test/utils/pickers/describeValue/describeValue.types.ts b/test/utils/pickers/describeValue/describeValue.types.ts index c823c418143a2..93b8857a59da0 100644 --- a/test/utils/pickers/describeValue/describeValue.types.ts +++ b/test/utils/pickers/describeValue/describeValue.types.ts @@ -29,6 +29,7 @@ export type DescribeValueOptions< > = DescribeValueBaseOptions & (C extends 'picker' ? OpenPickerParams & { + variant: 'desktop' | 'mobile'; setNewValue: ( value: InferNonNullablePickerValue, options: { diff --git a/test/utils/pickers/describeValue/testControlledUnControlled.tsx b/test/utils/pickers/describeValue/testControlledUnControlled.tsx index 272360c609c0c..77be94a9414aa 100644 --- a/test/utils/pickers/describeValue/testControlledUnControlled.tsx +++ b/test/utils/pickers/describeValue/testControlledUnControlled.tsx @@ -157,11 +157,13 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( }); }); - it('should not allow editing with keyboard in mobile pickers', () => { + it('should allow editing in field on non-range mobile pickers', () => { if (componentFamily !== 'picker' || params.variant !== 'mobile') { return; } + const hasMobileFieldEditing = ['time', 'date', 'date-time'].includes(params.type); + const handleChange = spy(); const v7Response = renderWithProps({ @@ -170,7 +172,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( }); v7Response.selectSection(undefined); fireUserEvent.keyPress(v7Response.getActiveSection(0), { key: 'ArrowUp' }); - expect(handleChange.callCount).to.equal(0); + expect(handleChange.callCount).to.equal(hasMobileFieldEditing ? 1 : 0); }); it('should have correct labelledby relationship when toolbar is shown', () => { diff --git a/test/utils/pickers/misc.ts b/test/utils/pickers/misc.ts index 76e9ba1d50689..8fe2f45957bbb 100644 --- a/test/utils/pickers/misc.ts +++ b/test/utils/pickers/misc.ts @@ -24,7 +24,7 @@ const getChangeCountForComponentFamily = (componentFamily: PickerComponentFamily export const getExpectedOnChangeCount = ( componentFamily: PickerComponentFamily, - params: OpenPickerParams, + params: OpenPickerParams & { variant: 'desktop' | 'mobile' }, ) => { if (componentFamily === 'digital-clock') { return getChangeCountForComponentFamily(componentFamily); diff --git a/test/utils/pickers/openPicker.ts b/test/utils/pickers/openPicker.ts index b34715ce5f8b6..1a4285de7b396 100644 --- a/test/utils/pickers/openPicker.ts +++ b/test/utils/pickers/openPicker.ts @@ -5,11 +5,9 @@ import { pickersInputBaseClasses } from '@mui/x-date-pickers/PickersTextField'; export type OpenPickerParams = | { type: 'date' | 'date-time' | 'time'; - variant: 'mobile' | 'desktop'; } | { type: 'date-range' | 'date-time-range'; - variant: 'mobile' | 'desktop'; initialFocus: 'start' | 'end'; /** * @default false @@ -36,12 +34,6 @@ export const openPicker = (params: OpenPickerParams) => { return true; } - if (params.variant === 'mobile') { - fireEvent.click(fieldSectionsContainer); - - return true; - } - const target = params.type === 'time' ? screen.getByLabelText(/choose time/i)