diff --git a/docs/data/date-pickers/base-concepts/base-concepts.md b/docs/data/date-pickers/base-concepts/base-concepts.md index e6953b53c0b1..a6d3a6426c29 100644 --- a/docs/data/date-pickers/base-concepts/base-concepts.md +++ b/docs/data/date-pickers/base-concepts/base-concepts.md @@ -83,6 +83,19 @@ There are many components available, each fitting specific use cases. Use the fo {{"demo": "ComponentExplorerNoSnap.js", "hideToolbar": true}} +## Accessibility + +Both `Desktop` and `Mobile` Date and Time Pickers are using `role="dialog"` to display their interactive view parts and thus they should follow [Modal accessibility guidelines](/material-ui/react-modal/#accessibility). +This behavior is automated as much as possible, ensuring that the Date and Time Pickers are accessible in most cases. +A correct `aria-labelledby` value is assigned to the dialog component based on the following rules: + +- Use `toolbar` id if the toolbar is visible; +- Use the id of the input label if the toolbar is hidden; + +:::info +Make sure to provide an `aria-labelledby` prop to `popper` and/or `mobilePaper` `slotProps` in case you are using Date and Time Pickers component with **hidden toolbar** and **without a label**. +::: + ## TypeScript In order to benefit from the [CSS overrides](/material-ui/customization/theme-components/#global-style-overrides) and [default prop customization](/material-ui/customization/theme-components/#default-props) with the theme, TypeScript users need to import the following types. diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerToolbar.tsx b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerToolbar.tsx index 5eaf7fe26657..29b1a376046b 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerToolbar.tsx +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerToolbar.tsx @@ -136,6 +136,7 @@ DateRangePickerToolbar.propTypes = { onRangePositionChange: PropTypes.func.isRequired, rangePosition: PropTypes.oneOf(['end', 'start']).isRequired, readOnly: PropTypes.bool, + titleId: PropTypes.string, /** * Toolbar date format. */ diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx b/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx index 164ed8e8f0a1..fa9ba5016be2 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx @@ -13,7 +13,9 @@ import { PickersModalDialog, InferError, ExportedBaseToolbarProps, + useLocaleText, } from '@mui/x-date-pickers/internals'; +import useId from '@mui/utils/useId'; import { MobileRangePickerAdditionalViewProps, UseMobileRangePickerParams, @@ -39,7 +41,7 @@ export const useMobileRangePicker = < const { slots, - slotProps, + slotProps: innerSlotProps, className, sx, format, @@ -50,6 +52,8 @@ export const useMobileRangePicker = < } = props; const [rangePosition, setRangePosition] = React.useState('start'); + const labelId = useId(); + const contextLocaleText = useLocaleText(); const { open, @@ -93,7 +97,7 @@ export const useMobileRangePicker = < InferError > = useSlotProps({ elementType: Field, - externalSlotProps: slotProps?.field, + externalSlotProps: innerSlotProps?.field, additionalProps: { ...pickerFieldProps, readOnly: readOnly ?? true, @@ -110,10 +114,12 @@ export const useMobileRangePicker = < ...fieldProps.slots, }; + const isToolbarHidden = innerSlotProps?.toolbar?.hidden ?? false; + const slotPropsForField: BaseMultiInputFieldProps, unknown>['slotProps'] = { ...fieldProps.slotProps, textField: (ownerState) => { - const externalInputProps = resolveComponentProps(slotProps?.textField, ownerState); + const externalInputProps = resolveComponentProps(innerSlotProps?.textField, ownerState); const inputPropsPassedByField = resolveComponentProps( fieldProps.slotProps?.textField, ownerState, @@ -122,6 +128,7 @@ export const useMobileRangePicker = < ownerState.position === 'start' ? fieldSlotProps.startInput : fieldSlotProps.endInput; return { + ...(isToolbarHidden && { id: `${labelId}-${ownerState.position}` }), ...externalInputProps, ...inputPropsPassedByField, ...inputPropsPassedByPicker, @@ -132,7 +139,7 @@ export const useMobileRangePicker = < }; }, root: (ownerState) => { - const externalRootProps = resolveComponentProps(slotProps?.fieldRoot, ownerState); + const externalRootProps = resolveComponentProps(innerSlotProps?.fieldRoot, ownerState); const rootPropsPassedByField = resolveComponentProps(fieldProps.slotProps?.root, ownerState); return { ...externalRootProps, @@ -141,7 +148,10 @@ export const useMobileRangePicker = < }; }, separator: (ownerState) => { - const externalSeparatorProps = resolveComponentProps(slotProps?.fieldSeparator, ownerState); + const externalSeparatorProps = resolveComponentProps( + innerSlotProps?.fieldSeparator, + ownerState, + ); const separatorPropsPassedByField = resolveComponentProps( fieldProps.slotProps?.separator, ownerState, @@ -155,9 +165,10 @@ export const useMobileRangePicker = < }; const slotPropsForLayout: PickersLayoutSlotsComponentsProps, TDate, TView> = { - ...slotProps, + ...innerSlotProps, toolbar: { - ...slotProps?.toolbar, + ...innerSlotProps?.toolbar, + titleId: labelId, rangePosition, onRangePositionChange: setRangePosition, } as ExportedBaseToolbarProps, @@ -165,6 +176,29 @@ export const useMobileRangePicker = < const Layout = slots?.layout ?? PickersLayout; + const finalLocaleText = { + ...contextLocaleText, + ...localeText, + }; + let labelledById = labelId; + if (isToolbarHidden) { + const labels: string[] = []; + if (finalLocaleText.start) { + labels.push(`${labelId}-start-label`); + } + if (finalLocaleText.end) { + labels.push(`${labelId}-end-label`); + } + labelledById = labels.length > 0 ? labels.join(' ') : undefined; + } + const slotProps = { + ...innerSlotProps, + mobilePaper: { + 'aria-labelledby': labelledById, + ...innerSlotProps?.mobilePaper, + }, + }; + const renderPicker = () => ( diff --git a/packages/x-date-pickers/src/DatePicker/DatePickerToolbar.tsx b/packages/x-date-pickers/src/DatePicker/DatePickerToolbar.tsx index e83e247776b4..1124ece12ee3 100644 --- a/packages/x-date-pickers/src/DatePicker/DatePickerToolbar.tsx +++ b/packages/x-date-pickers/src/DatePicker/DatePickerToolbar.tsx @@ -149,6 +149,7 @@ DatePickerToolbar.propTypes = { PropTypes.func, PropTypes.object, ]), + titleId: PropTypes.string, /** * Toolbar date format. */ diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx b/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx index f474ac495e40..9ff7a98920d2 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx @@ -269,6 +269,7 @@ DateTimePickerToolbar.propTypes = { */ onViewChange: PropTypes.func.isRequired, readOnly: PropTypes.bool, + titleId: PropTypes.string, /** * Toolbar date format. */ diff --git a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.types.ts b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.types.ts index fcc514c260d2..4053b4a534c7 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.types.ts +++ b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.types.ts @@ -1,6 +1,6 @@ import { UseDesktopPickerSlotsComponent, - UseDesktopPickerSlotsComponentsProps, + ExportedUseDesktopPickerSlotsComponentsProps, DesktopOnlyPickerProps, } from '../internals/hooks/useDesktopPicker'; import { @@ -18,7 +18,7 @@ export interface DesktopDatePickerSlotsComponent export interface DesktopDatePickerSlotsComponentsProps extends BaseDatePickerSlotsComponentsProps, - UseDesktopPickerSlotsComponentsProps {} + ExportedUseDesktopPickerSlotsComponentsProps {} export interface DesktopDatePickerProps extends BaseDatePickerProps, diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts index 96f02ccfa87d..1cffb060f123 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts @@ -1,6 +1,6 @@ import { UseDesktopPickerSlotsComponent, - UseDesktopPickerSlotsComponentsProps, + ExportedUseDesktopPickerSlotsComponentsProps, DesktopOnlyPickerProps, } from '../internals/hooks/useDesktopPicker'; import { @@ -21,7 +21,7 @@ export interface DesktopDateTimePickerSlotsComponent export interface DesktopDateTimePickerSlotsComponentsProps extends BaseDateTimePickerSlotsComponentsProps, - UseDesktopPickerSlotsComponentsProps {} + ExportedUseDesktopPickerSlotsComponentsProps {} export interface DesktopDateTimePickerProps extends BaseDateTimePickerProps, diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts index 0993ecd3024e..380a670d6258 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts @@ -1,6 +1,6 @@ import { UseDesktopPickerSlotsComponent, - UseDesktopPickerSlotsComponentsProps, + ExportedUseDesktopPickerSlotsComponentsProps, DesktopOnlyPickerProps, } from '../internals/hooks/useDesktopPicker'; import { @@ -18,7 +18,7 @@ export interface DesktopTimePickerSlotsComponent export interface DesktopTimePickerSlotsComponentsProps extends BaseTimePickerSlotsComponentsProps, - UseDesktopPickerSlotsComponentsProps {} + ExportedUseDesktopPickerSlotsComponentsProps {} export interface DesktopTimePickerProps extends BaseTimePickerProps, diff --git a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.types.ts b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.types.ts index fb23cee98951..bbc3553b2cee 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.types.ts +++ b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.types.ts @@ -1,7 +1,7 @@ import { UseMobilePickerSlotsComponent, - UseMobilePickerSlotsComponentsProps, MobileOnlyPickerProps, + ExportedUseMobilePickerSlotsComponentsProps, } from '../internals/hooks/useMobilePicker'; import { BaseDatePickerProps, @@ -18,7 +18,7 @@ export interface MobileDatePickerSlotsComponent export interface MobileDatePickerSlotsComponentsProps extends BaseDatePickerSlotsComponentsProps, - UseMobilePickerSlotsComponentsProps {} + ExportedUseMobilePickerSlotsComponentsProps {} export interface MobileDatePickerProps extends BaseDatePickerProps, diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts index 76308ee00727..0fdb8d949118 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts @@ -1,6 +1,6 @@ import { UseMobilePickerSlotsComponent, - UseMobilePickerSlotsComponentsProps, + ExportedUseMobilePickerSlotsComponentsProps, MobileOnlyPickerProps, } from '../internals/hooks/useMobilePicker'; import { @@ -18,7 +18,7 @@ export interface MobileDateTimePickerSlotsComponent export interface MobileDateTimePickerSlotsComponentsProps extends BaseDateTimePickerSlotsComponentsProps, - UseMobilePickerSlotsComponentsProps {} + ExportedUseMobilePickerSlotsComponentsProps {} export interface MobileDateTimePickerProps extends BaseDateTimePickerProps, diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts index a36c8d0f1640..d1d0f1e69d25 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts @@ -1,6 +1,6 @@ import { UseMobilePickerSlotsComponent, - UseMobilePickerSlotsComponentsProps, + ExportedUseMobilePickerSlotsComponentsProps, MobileOnlyPickerProps, } from '../internals/hooks/useMobilePicker'; import { @@ -18,7 +18,7 @@ export interface MobileTimePickerSlotsComponent export interface MobileTimePickerSlotsComponentsProps extends BaseTimePickerSlotsComponentsProps, - UseMobilePickerSlotsComponentsProps {} + ExportedUseMobilePickerSlotsComponentsProps {} export interface MobileTimePickerProps extends BaseTimePickerProps, diff --git a/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx b/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx index 5ee3bee58c0d..cc0bd553ec7b 100644 --- a/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx +++ b/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx @@ -283,6 +283,7 @@ TimePickerToolbar.propTypes = { */ onViewChange: PropTypes.func.isRequired, readOnly: PropTypes.bool, + titleId: PropTypes.string, /** * Toolbar date format. */ diff --git a/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx b/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx index c74140801808..16120d392215 100644 --- a/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx +++ b/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx @@ -9,7 +9,7 @@ import { getPickersToolbarUtilityClass, PickersToolbarClasses } from './pickersT import { DateOrTimeView } from '../models/views'; export interface PickersToolbarProps - extends Pick, 'isLandscape' | 'hidden'> { + extends Pick, 'isLandscape' | 'hidden' | 'titleId'> { className?: string; landscapeDirection?: 'row' | 'column'; toolbarTitle: React.ReactNode; @@ -81,6 +81,7 @@ export const PickersToolbar = React.forwardRef(function PickersToolbar< landscapeDirection = 'column', toolbarTitle, hidden, + titleId, } = props; const ownerState = props; @@ -97,7 +98,12 @@ export const PickersToolbar = React.forwardRef(function PickersToolbar< className={clsx(classes.root, className)} ownerState={ownerState} > - + {toolbarTitle} ) => { const { slots, - slotProps, + slotProps: innerSlotProps, className, sx, format, @@ -46,6 +47,7 @@ export const useDesktopPicker = < const utils = useUtils(); const internalInputRef = React.useRef(null); + const labelId = useId(); const { open, @@ -68,7 +70,7 @@ export const useDesktopPicker = < const Field = slots.field; const fieldProps: BaseFieldProps> = useSlotProps({ elementType: Field, - externalSlotProps: slotProps?.field, + externalSlotProps: innerSlotProps?.field, additionalProps: { ...pickerFieldProps, readOnly, @@ -85,7 +87,7 @@ export const useDesktopPicker = < const InputAdornment = slots.inputAdornment ?? MuiInputAdornment; const inputAdornmentProps = useSlotProps({ elementType: InputAdornment, - externalSlotProps: slotProps?.inputAdornment, + externalSlotProps: innerSlotProps?.inputAdornment, additionalProps: { position: 'end' as const, }, @@ -95,7 +97,7 @@ export const useDesktopPicker = < const OpenPickerButton = slots.openPickerButton ?? IconButton; const { ownerState: openPickerButtonOwnerState, ...openPickerButtonProps } = useSlotProps({ elementType: OpenPickerButton, - externalSlotProps: slotProps?.openPickerButton, + externalSlotProps: innerSlotProps?.openPickerButton, additionalProps: { disabled: disabled || readOnly, onClick: actions.onOpen, @@ -112,23 +114,26 @@ export const useDesktopPicker = < ...fieldProps.slots, }; + const isToolbarHidden = innerSlotProps?.toolbar?.hidden ?? false; + const slotPropsForField: BaseFieldProps['slotProps'] = { ...fieldProps.slotProps, textField: (ownerState) => { - const externalInputProps = resolveComponentProps(slotProps?.textField, ownerState); + const externalInputProps = resolveComponentProps(innerSlotProps?.textField, ownerState); const inputPropsPassedByField = resolveComponentProps( fieldProps.slotProps?.textField, ownerState, ); return { + ...(isToolbarHidden && { id: labelId }), ...inputPropsPassedByField, ...externalInputProps, InputProps: { [`${inputAdornmentProps.position}Adornment`]: hasUIView ? ( - + ) : undefined, @@ -143,6 +148,26 @@ export const useDesktopPicker = < const handleInputRef = useForkRef(internalInputRef, fieldProps.inputRef, inputRef); + let labelledById = labelId; + if (isToolbarHidden) { + if (label) { + labelledById = `${labelId}-label`; + } else { + labelledById = undefined; + } + } + const slotProps = { + ...innerSlotProps, + toolbar: { + ...innerSlotProps?.toolbar, + titleId: labelId, + }, + popper: { + 'aria-labelledby': labelledById, + ...innerSlotProps?.popper, + }, + }; + const renderPicker = () => ( 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 2ff419b22a7b..21bdc52ee565 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 @@ -18,6 +18,7 @@ import { BaseFieldProps } from '../../models/fields'; import { ExportedPickersLayoutSlotsComponent, ExportedPickersLayoutSlotsComponentsProps, + PickersLayoutSlotsComponentsProps, } from '../../../PickersLayout/PickersLayout.types'; import { UsePickerValueNonStaticProps } from '../usePicker/usePickerValue'; import { UsePickerViewsNonStaticProps, UsePickerViewsProps } from '../usePicker/usePickerViews'; @@ -56,6 +57,10 @@ export interface UseDesktopPickerSlotsComponent + extends ExportedUseDesktopPickerSlotsComponentsProps, + Pick, 'toolbar'> {} + +export interface ExportedUseDesktopPickerSlotsComponentsProps extends PickersPopperSlotsComponentsProps, ExportedPickersLayoutSlotsComponentsProps { field?: SlotComponentProps< diff --git a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/index.ts b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/index.ts index f38c04003728..761b84a6dfd6 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/index.ts +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/index.ts @@ -2,5 +2,6 @@ export { useMobilePicker } from './useMobilePicker'; export type { UseMobilePickerSlotsComponent, UseMobilePickerSlotsComponentsProps, + ExportedUseMobilePickerSlotsComponentsProps, MobileOnlyPickerProps, } from './useMobilePicker.types'; 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 ef46515c8893..b18fbfdbc533 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { resolveComponentProps, useSlotProps } from '@mui/base/utils'; import useForkRef from '@mui/utils/useForkRef'; +import useId from '@mui/utils/useId'; import { PickersModalDialog } from '../../components/PickersModalDialog'; import { DateOrTimeView } from '../../models'; import { UseMobilePickerParams, UseMobilePickerProps } from './useMobilePicker.types'; @@ -31,7 +32,7 @@ export const useMobilePicker = < }: UseMobilePickerParams) => { const { slots, - slotProps, + slotProps: innerSlotProps, className, sx, format, @@ -44,6 +45,7 @@ export const useMobilePicker = < const utils = useUtils(); const internalInputRef = React.useRef(null); + const labelId = useId(); const { open, @@ -64,7 +66,7 @@ export const useMobilePicker = < const Field = slots.field; const fieldProps: BaseFieldProps> = useSlotProps({ elementType: Field, - externalSlotProps: slotProps?.field, + externalSlotProps: innerSlotProps?.field, additionalProps: { ...pickerFieldProps, readOnly: readOnly ?? true, @@ -82,16 +84,19 @@ export const useMobilePicker = < ...fieldProps.slots, }; + const isToolbarHidden = innerSlotProps?.toolbar?.hidden ?? false; + const slotPropsForField: BaseFieldProps['slotProps'] = { ...fieldProps.slotProps, textField: (ownerState) => { - const externalInputProps = resolveComponentProps(slotProps?.textField, ownerState); + const externalInputProps = resolveComponentProps(innerSlotProps?.textField, ownerState); const inputPropsPassedByField = resolveComponentProps( fieldProps.slotProps?.textField, ownerState, ); return { + ...(isToolbarHidden && { id: labelId }), ...inputPropsPassedByField, ...externalInputProps, disabled, @@ -112,6 +117,26 @@ export const useMobilePicker = < const handleInputRef = useForkRef(internalInputRef, fieldProps.inputRef, inputRef); + let labelledById = labelId; + if (isToolbarHidden) { + if (label) { + labelledById = `${labelId}-label`; + } else { + labelledById = undefined; + } + } + const slotProps = { + ...innerSlotProps, + toolbar: { + ...innerSlotProps?.toolbar, + titleId: labelId, + }, + mobilePaper: { + 'aria-labelledby': labelledById, + ...innerSlotProps?.mobilePaper, + }, + }; + const renderPicker = () => ( 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 4b7b9e476f95..d27a2b0cc276 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 @@ -16,6 +16,7 @@ import { BaseFieldProps } from '../../models/fields'; import { ExportedPickersLayoutSlotsComponent, ExportedPickersLayoutSlotsComponentsProps, + PickersLayoutSlotsComponentsProps, } from '../../../PickersLayout/PickersLayout.types'; import { UsePickerValueNonStaticProps } from '../usePicker/usePickerValue'; import { UsePickerViewsNonStaticProps, UsePickerViewsProps } from '../usePicker/usePickerViews'; @@ -36,7 +37,7 @@ export interface UseMobilePickerSlotsComponent; } -export interface UseMobilePickerSlotsComponentsProps +export interface ExportedUseMobilePickerSlotsComponentsProps extends PickersModalDialogSlotsComponentsProps, ExportedPickersLayoutSlotsComponentsProps { field?: SlotComponentProps< @@ -47,6 +48,10 @@ export interface UseMobilePickerSlotsComponentsProps>; } +export interface UseMobilePickerSlotsComponentsProps + extends ExportedUseMobilePickerSlotsComponentsProps, + Pick, 'toolbar'> {} + export interface MobileOnlyPickerProps extends BaseNonStaticPickerProps, BaseSingleInputNonStaticPickerProps, diff --git a/packages/x-date-pickers/src/internals/models/props/toolbar.ts b/packages/x-date-pickers/src/internals/models/props/toolbar.ts index 4fc9228798a9..9fdbb976940e 100644 --- a/packages/x-date-pickers/src/internals/models/props/toolbar.ts +++ b/packages/x-date-pickers/src/internals/models/props/toolbar.ts @@ -19,6 +19,7 @@ export interface BaseToolbarProps views: readonly DateOrTimeView[]; disabled?: boolean; readOnly?: boolean; + titleId?: string; } export interface ExportedBaseToolbarProps { diff --git a/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx b/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx index 82fa2827e8c5..5c963328df22 100644 --- a/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx +++ b/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx @@ -114,5 +114,85 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( clock.runToLast(); expect(handleChange.callCount).to.equal(0); }); + + it('should have correct labelledby relationship when toolbar is shown', () => { + const params = pickerParams as DescribeValueOptions<'picker', any>; + if ( + componentFamily !== 'picker' || + (params.variant === 'desktop' && (params.type === 'time' || params.type === 'date-range')) + ) { + return; + } + + render( + , + ); + expect(screen.getByLabelText('Test toolbar')).to.have.attribute('role', 'dialog'); + }); + + it('should have correct labelledby relationship with provided label when toolbar is hidden', () => { + const params = pickerParams as DescribeValueOptions<'picker', any>; + if ( + componentFamily !== 'picker' || + (params.variant === 'desktop' && (params.type === 'time' || params.type === 'date-range')) + ) { + return; + } + + render( + , + ); + expect(screen.getByLabelText('test relationship', { selector: 'div' })).to.have.attribute( + 'role', + 'dialog', + ); + }); + + it('should have correct labelledby relationship without label and hidden toolbar but external props', () => { + const params = pickerParams as DescribeValueOptions<'picker', any>; + if ( + componentFamily !== 'picker' || + (params.variant === 'desktop' && (params.type === 'time' || params.type === 'date-range')) + ) { + return; + } + + render( +
+
external label
+ +
, + ); + expect(screen.getByLabelText('external label')).to.have.attribute('role', 'dialog'); + }); }); };