Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pickers] Fix aria-labelledby assignment to dialog #7608

Merged
merged 24 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c27a3ed
Fix `aria-labelledby` on mobile picker dialog paper
LukasTy Jan 18, 2023
c4a695a
Handle case when toolbar is hidden and label is empty
LukasTy Jan 18, 2023
27fd8b0
Handle mobile date range picker
LukasTy Jan 18, 2023
b84a5fc
fix lint issues
LukasTy Jan 18, 2023
d166f5e
Handle desktop pickers
LukasTy Jan 18, 2023
86d1d2f
proptypes
LukasTy Jan 18, 2023
b5eb9b8
Actually handle `titleId` privately
LukasTy Jan 18, 2023
8771a10
fix
LukasTy Jan 18, 2023
074460f
Fix to not post warning for DesktopTimePicker without views
LukasTy Jan 18, 2023
2606211
Update tests to include label and avoid warning being thrown
LukasTy Jan 18, 2023
6a1e69a
Add test cases asserting pickers `aria-labelledby` relationship
LukasTy Jan 18, 2023
75ac81e
Code review: Flavien
LukasTy Jan 19, 2023
fc0348b
Code review: Flavien 2
LukasTy Jan 19, 2023
fabf4e0
Merge remote-tracking branch 'origin/next' into fix-pickers-aria-labe…
LukasTy Jan 19, 2023
026af96
Remove redundant omit
LukasTy Jan 19, 2023
10484bb
Merge remote-tracking branch 'origin/next' into fix-pickers-aria-labe…
LukasTy Jan 21, 2023
3e20a2d
Update refactored test file
LukasTy Jan 21, 2023
7a47004
Merge remote-tracking branch 'origin/next' into fix-pickers-aria-labe…
LukasTy Jan 25, 2023
fcedecf
Fix types
LukasTy Jan 25, 2023
c8c4a75
Code review: Alex
LukasTy Jan 26, 2023
b8f10dd
Remove warnings
LukasTy Jan 26, 2023
b34a2aa
Revert no longer relevant test file changes
LukasTy Jan 26, 2023
9f6532e
Add documentation section regarding `aria-labelledby` behavior
LukasTy Jan 26, 2023
a7de910
Apply suggestions from code review
LukasTy Jan 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/data/date-pickers/base-concepts/base-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
PickersModalDialog,
InferError,
ExportedBaseToolbarProps,
useLocaleText,
} from '@mui/x-date-pickers/internals';
import useId from '@mui/utils/useId';
import {
MobileRangePickerAdditionalViewProps,
UseMobileRangePickerParams,
Expand All @@ -39,7 +41,7 @@ export const useMobileRangePicker = <

const {
slots,
slotProps,
slotProps: innerSlotProps,
className,
sx,
format,
Expand All @@ -50,6 +52,8 @@ export const useMobileRangePicker = <
} = props;

const [rangePosition, setRangePosition] = React.useState<RangePosition>('start');
const labelId = useId();
const contextLocaleText = useLocaleText();

const {
open,
Expand Down Expand Up @@ -93,7 +97,7 @@ export const useMobileRangePicker = <
InferError<TExternalProps>
> = useSlotProps({
elementType: Field,
externalSlotProps: slotProps?.field,
externalSlotProps: innerSlotProps?.field,
additionalProps: {
...pickerFieldProps,
readOnly: readOnly ?? true,
Expand All @@ -110,10 +114,12 @@ export const useMobileRangePicker = <
...fieldProps.slots,
};

const isToolbarHidden = innerSlotProps?.toolbar?.hidden ?? false;

const slotPropsForField: BaseMultiInputFieldProps<DateRange<TDate>, 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,
Expand All @@ -122,6 +128,7 @@ export const useMobileRangePicker = <
ownerState.position === 'start' ? fieldSlotProps.startInput : fieldSlotProps.endInput;

return {
...(isToolbarHidden && { id: `${labelId}-${ownerState.position}` }),
...externalInputProps,
...inputPropsPassedByField,
...inputPropsPassedByPicker,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -155,16 +165,40 @@ export const useMobileRangePicker = <
};

const slotPropsForLayout: PickersLayoutSlotsComponentsProps<DateRange<TDate>, TDate, TView> = {
...slotProps,
...innerSlotProps,
toolbar: {
...slotProps?.toolbar,
...innerSlotProps?.toolbar,
titleId: labelId,
rangePosition,
onRangePositionChange: setRangePosition,
} as ExportedBaseToolbarProps,
};

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 = () => (
<LocalizationProvider localeText={localeText}>
<WrapperVariantContext.Provider value="mobile">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ DatePickerToolbar.propTypes = {
PropTypes.func,
PropTypes.object,
]),
titleId: PropTypes.string,
/**
* Toolbar date format.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ DateTimePickerToolbar.propTypes = {
*/
onViewChange: PropTypes.func.isRequired,
readOnly: PropTypes.bool,
titleId: PropTypes.string,
/**
* Toolbar date format.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
UseDesktopPickerSlotsComponent,
UseDesktopPickerSlotsComponentsProps,
ExportedUseDesktopPickerSlotsComponentsProps,
DesktopOnlyPickerProps,
} from '../internals/hooks/useDesktopPicker';
import {
Expand All @@ -18,7 +18,7 @@ export interface DesktopDatePickerSlotsComponent<TDate>

export interface DesktopDatePickerSlotsComponentsProps<TDate>
extends BaseDatePickerSlotsComponentsProps<TDate>,
UseDesktopPickerSlotsComponentsProps<TDate, DateView> {}
ExportedUseDesktopPickerSlotsComponentsProps<TDate, DateView> {}

export interface DesktopDatePickerProps<TDate>
extends BaseDatePickerProps<TDate>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
UseDesktopPickerSlotsComponent,
UseDesktopPickerSlotsComponentsProps,
ExportedUseDesktopPickerSlotsComponentsProps,
DesktopOnlyPickerProps,
} from '../internals/hooks/useDesktopPicker';
import {
Expand All @@ -21,7 +21,7 @@ export interface DesktopDateTimePickerSlotsComponent<TDate>

export interface DesktopDateTimePickerSlotsComponentsProps<TDate>
extends BaseDateTimePickerSlotsComponentsProps<TDate>,
UseDesktopPickerSlotsComponentsProps<TDate, DateOrTimeView> {}
ExportedUseDesktopPickerSlotsComponentsProps<TDate, DateOrTimeView> {}

export interface DesktopDateTimePickerProps<TDate>
extends BaseDateTimePickerProps<TDate>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
UseDesktopPickerSlotsComponent,
UseDesktopPickerSlotsComponentsProps,
ExportedUseDesktopPickerSlotsComponentsProps,
DesktopOnlyPickerProps,
} from '../internals/hooks/useDesktopPicker';
import {
Expand All @@ -18,7 +18,7 @@ export interface DesktopTimePickerSlotsComponent<TDate>

export interface DesktopTimePickerSlotsComponentsProps<TDate>
extends BaseTimePickerSlotsComponentsProps,
UseDesktopPickerSlotsComponentsProps<TDate, TimeView> {}
ExportedUseDesktopPickerSlotsComponentsProps<TDate, TimeView> {}

export interface DesktopTimePickerProps<TDate>
extends BaseTimePickerProps<TDate>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
UseMobilePickerSlotsComponent,
UseMobilePickerSlotsComponentsProps,
MobileOnlyPickerProps,
ExportedUseMobilePickerSlotsComponentsProps,
} from '../internals/hooks/useMobilePicker';
import {
BaseDatePickerProps,
Expand All @@ -18,7 +18,7 @@ export interface MobileDatePickerSlotsComponent<TDate>

export interface MobileDatePickerSlotsComponentsProps<TDate>
extends BaseDatePickerSlotsComponentsProps<TDate>,
UseMobilePickerSlotsComponentsProps<TDate, DateView> {}
ExportedUseMobilePickerSlotsComponentsProps<TDate, DateView> {}

export interface MobileDatePickerProps<TDate>
extends BaseDatePickerProps<TDate>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
UseMobilePickerSlotsComponent,
UseMobilePickerSlotsComponentsProps,
ExportedUseMobilePickerSlotsComponentsProps,
MobileOnlyPickerProps,
} from '../internals/hooks/useMobilePicker';
import {
Expand All @@ -18,7 +18,7 @@ export interface MobileDateTimePickerSlotsComponent<TDate>

export interface MobileDateTimePickerSlotsComponentsProps<TDate>
extends BaseDateTimePickerSlotsComponentsProps<TDate>,
UseMobilePickerSlotsComponentsProps<TDate, DateOrTimeView> {}
ExportedUseMobilePickerSlotsComponentsProps<TDate, DateOrTimeView> {}

export interface MobileDateTimePickerProps<TDate>
extends BaseDateTimePickerProps<TDate>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
UseMobilePickerSlotsComponent,
UseMobilePickerSlotsComponentsProps,
ExportedUseMobilePickerSlotsComponentsProps,
MobileOnlyPickerProps,
} from '../internals/hooks/useMobilePicker';
import {
Expand All @@ -18,7 +18,7 @@ export interface MobileTimePickerSlotsComponent<TDate>

export interface MobileTimePickerSlotsComponentsProps<TDate>
extends BaseTimePickerSlotsComponentsProps,
UseMobilePickerSlotsComponentsProps<TDate, TimeView> {}
ExportedUseMobilePickerSlotsComponentsProps<TDate, TimeView> {}

export interface MobileTimePickerProps<TDate>
extends BaseTimePickerProps<TDate>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ TimePickerToolbar.propTypes = {
*/
onViewChange: PropTypes.func.isRequired,
readOnly: PropTypes.bool,
titleId: PropTypes.string,
/**
* Toolbar date format.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getPickersToolbarUtilityClass, PickersToolbarClasses } from './pickersT
import { DateOrTimeView } from '../models/views';

export interface PickersToolbarProps<TValue, TView extends DateOrTimeView>
extends Pick<BaseToolbarProps<TValue, TView>, 'isLandscape' | 'hidden'> {
extends Pick<BaseToolbarProps<TValue, TView>, 'isLandscape' | 'hidden' | 'titleId'> {
className?: string;
landscapeDirection?: 'row' | 'column';
toolbarTitle: React.ReactNode;
Expand Down Expand Up @@ -81,6 +81,7 @@ export const PickersToolbar = React.forwardRef(function PickersToolbar<
landscapeDirection = 'column',
toolbarTitle,
hidden,
titleId,
} = props;

const ownerState = props;
Expand All @@ -97,7 +98,12 @@ export const PickersToolbar = React.forwardRef(function PickersToolbar<
className={clsx(classes.root, className)}
ownerState={ownerState}
>
<Typography data-mui-test="picker-toolbar-title" color="text.secondary" variant="overline">
<Typography
data-mui-test="picker-toolbar-title"
color="text.secondary"
variant="overline"
id={titleId}
>
{toolbarTitle}
</Typography>
<PickersToolbarContent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export { useDesktopPicker } from './useDesktopPicker';
export type {
UseDesktopPickerSlotsComponent,
UseDesktopPickerSlotsComponentsProps,
ExportedUseDesktopPickerSlotsComponentsProps,
DesktopOnlyPickerProps,
} from './useDesktopPicker.types';
Loading