Skip to content

[scheduler] Create accessibility documentation section#22557

Open
mustafajw07 wants to merge 15 commits into
mui:masterfrom
mustafajw07:docs/21463-scheduler-accessibility-section
Open

[scheduler] Create accessibility documentation section#22557
mustafajw07 wants to merge 15 commits into
mui:masterfrom
mustafajw07:docs/21463-scheduler-accessibility-section

Conversation

@mustafajw07

@mustafajw07 mustafajw07 commented May 22, 2026

Copy link
Copy Markdown
Contributor

Closes #21463

Summary

  • Add a dedicated accessibility documentation section for Scheduler
  • Document current keyboard navigation behavior and supported interactions
  • Document existing ARIA attributes and screen reader support
  • Add known accessibility limitations and unsupported behaviors
  • Audit the current Scheduler a11y implementation to ensure docs reflect shipped behavior

Changelog

@code-infra-dashboard

code-infra-dashboard Bot commented May 22, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@rita-codes

rita-codes commented May 22, 2026

Copy link
Copy Markdown
Member

Thanks for opening this — really appreciate you tackling the a11y docs! 🙌

I went through the doc against the current code and against the other MUI X a11y pages (Tree View, Data Grid) — sharing a first batch of things to revisit. Before the next push, could you do an audit pass against the shipped behavior? A few claims in the doc don't quite match what the code does today.

A couple of things that aren't quite accurate

1. Arrow key navigation actually works in Week, Day, and Month views.

All three implement arrow nav between grid cells via the shared getNavigationTarget util in @mui/x-scheduler-internals/calendar-grid. Handlers are on:

  • CalendarGridHeaderCell — between header cells, plus cross-row into all-day / time-grid
  • CalendarGridDayCell — all-day cells (Week/Day) and day cells (Month)
  • CalendarGridTimeColumn — between columns, plus cross-row

There's a full test suite at packages/x-scheduler-internals/src/calendar-grid/tests/keyboard-navigation.CalendarGrid.test.tsx if it helps as a reference. So the only spot where arrow nav is genuinely missing is the mini calendar — which means most of the "Known limitations" section can probably go away.

2. The waiAria frontmatter points to the treeview pattern, but the Scheduler's main structure is a grid — only the Resources sidebar is a tree. The Data Grid a11y page is a good reference for the right link.

Sections that could use more love

3. Keyboard interactions doesn't cover the calendar grid yet — which is a big part of what users will look for here. Worth adding a section for it that covers:

  • Arrow keys across header / all-day / time-grid rows in Week/Day, and between day cells in Month
  • The roving-tabIndex pattern for events (Tab from a focused cell enters the events inside it, then moves on to the next cell)
  • Enter on a grid cell to start event creation, and Enter on a header cell to activate the date button

4. The Resources sidebar table is a bit thin — since it's a RichTreeView under the hood, it inherits everything from the Tree View (Home, End, *, jump-to-character, Enter, selection keys, etc.). Probably nicer to just link to the Tree View accessibility page rather than maintain a duplicate (and partial) list here.

Small consistency tweaks with the other a11y pages

  • A short "WAI-ARIA Authoring Practices provide valuable information…" pointer right after Guidelines (TreeView and Data Grid both have this)
  • The "Some devices may lack certain keys…" admonition for Home / End / Fn substitutions, alongside the existing Ctrl/⌘ one
  • An "API" section at the bottom linking to the relevant Scheduler pages, like Data Grid does

Happy to chat through any of these if something's unclear — and thanks again for picking this up! 💛

@rita-codes rita-codes added accessibility a11y type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. scope: scheduler Changes related to the scheduler. labels May 22, 2026
@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks for opening this — really appreciate you tackling the a11y docs! 🙌

I went through the doc against the current code and against the other MUI X a11y pages (Tree View, Data Grid) — sharing a first batch of things to revisit. Before the next push, could you do an audit pass against the shipped behavior? A few claims in the doc don't quite match what the code does today.

A couple of things that aren't quite accurate

1. Arrow key navigation actually works in Week, Day, and Month views.

All three implement arrow nav between grid cells via the shared getNavigationTarget util in @mui/x-scheduler-internals/calendar-grid. Handlers are on:

  • CalendarGridHeaderCell — between header cells, plus cross-row into all-day / time-grid
  • CalendarGridDayCell — all-day cells (Week/Day) and day cells (Month)
  • CalendarGridTimeColumn — between columns, plus cross-row

There's a full test suite at packages/x-scheduler-internals/src/calendar-grid/tests/keyboard-navigation.CalendarGrid.test.tsx if it helps as a reference. So the only spot where arrow nav is genuinely missing is the mini calendar — which means most of the "Known limitations" section can probably go away.

2. The waiAria frontmatter points to the treeview pattern, but the Scheduler's main structure is a grid — only the Resources sidebar is a tree. The Data Grid a11y page is a good reference for the right link.

Sections that could use more love

3. Keyboard interactions doesn't cover the calendar grid yet — which is a big part of what users will look for here. Worth adding a section for it that covers:

  • Arrow keys across header / all-day / time-grid rows in Week/Day, and between day cells in Month
  • The roving-tabIndex pattern for events (Tab from a focused cell enters the events inside it, then moves on to the next cell)
  • Enter on a grid cell to start event creation, and Enter on a header cell to activate the date button

4. The Resources sidebar table is a bit thin — since it's a RichTreeView under the hood, it inherits everything from the Tree View (Home, End, *, jump-to-character, Enter, selection keys, etc.). Probably nicer to just link to the Tree View accessibility page rather than maintain a duplicate (and partial) list here.

Small consistency tweaks with the other a11y pages

  • A short "WAI-ARIA Authoring Practices provide valuable information…" pointer right after Guidelines (TreeView and Data Grid both have this)
  • The "Some devices may lack certain keys…" admonition for Home / End / Fn substitutions, alongside the existing Ctrl/⌘ one
  • An "API" section at the bottom linking to the relevant Scheduler pages, like Data Grid does

Happy to chat through any of these if something's unclear — and thanks again for picking this up! 💛

Thanks for the review.
I’ve updated all the requested changes. Please take a look when you get a chance. Thanks again!

@mj12albert mj12albert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed a few inconsistencies between the doc and the implementation (possibly some are bugs?) CC @rita-codes

When a month cell has more events than can be displayed, a **"X more"** button opens a popover listing all events for that day.

- The popover header element carries an `aria-label` with the full formatted date (for example, `"Monday, May 26"`).
- Each event inside the popover uses `aria-labelledby` that composes the popover header ID and the event title element ID, so screen readers announce the day context alongside the event title.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed this doesn't match the implementation:

id={`${schedulerId}-PopoverHeader-${day.key}`}
aria-label={`${formatWeekDayMonthAndDayOfMonth(day.value, adapter)}`}
>
<MoreEventsPopoverTitle className={classes.moreEventsPopoverTitle}>
{formatWeekDayMonthAndDayOfMonth(day.value, adapter)}
</MoreEventsPopoverTitle>
</MoreEventsPopoverHeader>
<MoreEventsPopoverBody className={classes.moreEventsPopoverBody}>
{occurrences.map((occurrence) => (
<EventDialogTrigger occurrence={occurrence} key={occurrence.key}>
<EventItem
variant={isOccurrenceAllDayOrMultipleDay(occurrence, adapter) ? 'filled' : 'compact'}
occurrence={occurrence}
date={day}
ariaLabelledBy={`PopoverHeader-${day.key}`}

(Could be a bug, one of those don't have a schedulerId)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — there was an inconsistency between the generated header ID and the referenced aria-labelledby value. Updated the implementation so both now use the same ID format.


## Localization of ARIA labels

All user-facing accessible labels are provided through the `localeText` prop and can be customized.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some are not yet exposed, e.g.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — updated the documentation wording.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the right thing to do is actually expose all user-facing text labels

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @mj12albert , I noticed that ViewSwitcher.tsx has aria-label="Switch View" hardcoded (line 67). I wanted to flag two things:

  1. It's not localized. The button's visible text {localeText[view]} (for example, "Week", "Month") is already going through localeText, but the aria-label overrides that for screen readers with a hardcoded English string.

  2. Is it even needed? When a button has both visible text and an aria-label, the aria-label takes precedence and suppresses the visible text for screen readers. Since {localeText[view]} already communicates the current view ("Week", "Month", etc.) and the button also gets aria-expanded from the menu pattern, the hardcoded aria-label is actually making things worse — a French user would hear "Switch View" instead of "Mois".

Should we just remove it and let the button's localized visible text do the job? Or if we want to keep a label like "Switch View", it should go through localeText so it can be translated. Happy to do either — just wanted your input before changing it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not specifically calling out "Switch View" here, but it's just one example of a non-exposed aria-label I noticed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe all such cases have already been covered. Could you point out any specific aria-labels that you think are still missing so I can verify them?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This for example:

aria-label={`Select ${colorOption} as event color`}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case is already covered. I added the corresponding label on line 182.

## Live region announcements

The current date range label in the header toolbar is wrapped in an `aria-live="polite"` region.
When a user navigates to the previous or next time span (for example, clicking **Previous week**), the updated date range is announced automatically by screen readers without requiring focus to move.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This region doesn't seem to contain the "updated date range":

<HeaderToolbarLabel aria-live="polite">
{adapter.format(visibleDate, 'monthFullLetter')}{' '}
{adapter.format(visibleDate, 'yearPadded')}
</HeaderToolbarLabel>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the wording to reflect the actual toolbar label behavior.


The Event Dialog is a non-modal dialog (`aria-modal="false"`) that floats next to the event that opened it.

- It is announced by screen readers via `aria-labelledby` pointing to the dialog title.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't account for read-only dialogs which have a different id:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — updated the documentation to account for the separate read-only dialog title ID as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the ids different in the first place? Is there any value to readers by telling them the exact id is "${schedulerId}-draggable-dialog-title"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question on the event dialog title IDs: ReadonlyContent uses draggable-dialog-title and FormContent uses event-dialog-title. The "draggable" prefix feels like an implementation detail leaking into a semantic identifier — assistive tech only cares that aria-labelledby points to the title, not what the ID is called.

Should we normalize both to event-dialog-title for consistency? Since the two dialogs are never rendered at the same time there's no collision risk.

Also, the a11y docs currently expose the exact ID strings. I think that's more internal code detail than useful guidance — I'd suggest replacing it with a simple description like "The dialog is labelled by the event title via aria-labelledby." Does that sound right to you?

Comment on lines +57 to +62
The grid body is wrapped in a `role="rowgroup"` element. Day cells use `role="gridcell"` and carry:

- `aria-label` — the full formatted date string
- `aria-current="date"` — on today's date
- `aria-selected` — on the currently active (selected) date
- A roving `tabIndex` — `0` on the active cell, `-1` on all others

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to imply that Day cells apply all of these to one element, when in fact they are split between a button with a wrapping div:

<MiniCalendarDayCell
key={day.key}
role="gridcell"
aria-colindex={dayIndex + 1}
className={classes.miniCalendarDayCell}
>
<MiniCalendarDayButton
type="button"
className={classes.miniCalendarDayButton}
data-other-month={isOtherMonth || undefined}
data-today={isToday || undefined}
data-active={isActive || undefined}
aria-label={fullDateLabel}
aria-current={isToday ? 'date' : undefined}
aria-selected={isActive}
tabIndex={isActive ? 0 : -1}
onClick={(event) => store.goToDate(day.value, event)}
>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the description to clarify that the grid structure and interactive accessibility attributes are split between the wrapper gridcell and the inner button element.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering did you test how the current markup structure is actually announced? (e.g. by VoiceOver)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes i have tested those out.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it currently announced?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During my testing, the announcement was consistent with the current markup structure and the accessibility attributes exposed by both elements.

@mustafajw07

Copy link
Copy Markdown
Contributor Author

@rita-codes / @mj12albert
Requested changes have been updated. Please review and let me know if any further changes are needed.

Thanks.

@mj12albert mj12albert added the docs Improvements or additions to the documentation. label May 27, 2026
@mustafajw07

Copy link
Copy Markdown
Contributor Author

Hi @mj12albert, just a friendly follow-up on this PR when you have a chance. Please let me know if any changes are needed. Thanks!

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Hey @rita-codes, can I get a review on this pr?

Thanks.

@rita-codes

Copy link
Copy Markdown
Member

Thanks for the patience on this — the doc is in really good shape now. 🙌 I did a fresh audit pass against the shipped code and most of it checks out: the grid roles, aria-rowindex/colindex, time-axis aria-hidden, mini-calendar attributes, live region, preferences menu, multi-day placeholders — all accurate. And the MoreEventsPopover schedulerId fix is correct, header id and aria-labelledby line up now. ✅

A few things are still open:

1. The color-option label still isn't exposed (re: @mj12albert's "expose all user-facing labels" thread).
The doc now lists selectColorAriaLabel in the localeText block, but that key doesn't actually exist in the implementation — the label is still hardcoded:

ResourceAndColorSection.tsx:310 → aria-label={`Select ${colorOption} as event color`}

So a non-English user still hears "Select green as event color". To close @mj12albert's point we'd need to add a real selectColorAriaLabel key to the translations and wire it up here — then the doc entry becomes true. Right now the doc documents a key that isn't real. Could you also double-check the other user-facing labels (e.g. ViewSwitcher's "Switch View") so none are left hardcoded?

2. Read-only Event Dialog aria-labelledby points to a non-existent id.
Following up on the title-id thread — this looks like a real bug, not just a doc nuance. The dialog hardcodes the same aria-labelledby for both modes:

EventDialog.tsx:146      → aria-labelledby={`${schedulerId}-event-dialog-title`}
FormContent.tsx:255      → id={`${schedulerId}-event-dialog-title`}        // editable ✅
ReadonlyContent.tsx:160  → id={`${schedulerId}-draggable-dialog-title`}    // read-only ❌

So in read-only mode the label target doesn't exist and the dialog ends up unlabelled. Your suggestion of normalizing both to event-dialog-title would fix it nicely (no collision since they're never rendered together) — and then we can drop the exact id strings from the doc in favor of a plain "the dialog is labelled by the event title via aria-labelledby".

3. Same bug class as the popover fix, but in the Agenda view.
The Agenda event items reference a header id without the schedulerId prefix:

AgendaView.tsx:238 → id={`${schedulerId}-DayHeaderCell-${date.key}`}
AgendaView.tsx:278 → ariaLabelledBy={`DayHeaderCell-${date.key}`}   // ← missing ${schedulerId}-

So Agenda events don't announce the day context the doc describes. Since you already fixed the exact same thing in MoreEventsPopover, might be worth folding this one-liner in too.

4. The page is "Scheduler accessibility", but it only covers the Event Calendar — the Event Timeline isn't documented at all.
EventTimelinePremium has its own substantial a11y surface that's worth a dedicated section, e.g.:

  • Structure: role="grid" on the timeline root, role="row" on rows, role="cell" on cells, with aria-rowindex (header row = 1, resource rows from 2). The resource title column and its timelineResourceTitleHeader label.
  • Events: labelled via aria-labelledby composing the resource title cell id + the event's own title; creation placeholders, recurring icons, and the current-time indicator are aria-hidden.
  • Keyboard: Arrow Up/Down move between resource rows, Arrow Left/Right between column types, Enter starts event creation, and Tab/Shift+Tab walk through the events within a row (roving tabIndex — events are only tabbable when their row has focus).
  • Known limitations: aria-colindex / aria-rowcount / aria-colcount aren't exposed yet, and timeline header/title cells don't use columnheader/rowheader roles.

Since that's a fair bit of new ground, maybe we keep it out of this PR and do the Timeline section as a follow-up — that way this one stays focused and doesn't grow too big. Up to you!

5. Minor — @mj12albert's VoiceOver question on the mini-calendar markup is still open. Could you drop the actual announcement string you got when testing the split gridcell + inner button? That'd let us close that thread.

None of these are blockers on the writing itself — the doc reads great. Thanks again for sticking with it! 💛

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review.

I've addressed the remaining items and updated the PR:

Added and wired up the localization support for the color selection aria-label and reviewed the remaining user-facing labels.
Fixed the read-only Event Dialog labeling issue by normalizing the title ID used by aria-labelledby.
Fixed the missing schedulerId prefix in the Agenda view ariaLabelledBy.

For the Event Timeline accessibility documentation, I agree it's substantial enough to deserve its own follow-up. I'd prefer to keep this PR focused on the Event Calendar accessibility documentation and handle the Timeline accessibility surface in a separate issue/PR.

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Regarding the VoiceOver question: I retested the Mini Calendar markup with VoiceOver.

When focusing a regular day in the Mini Calendar, VoiceOver announces something similar to:

"Tuesday, July 2, 2025, button, calendar table, 7 columns, 7 rows"

For today's date, it additionally announces:

"Current date"

along with the date information.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accessibility a11y docs Improvements or additions to the documentation. scope: scheduler Changes related to the scheduler. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[scheduler][docs] Create an accessibility section

3 participants