Skip to content

[scheduler] Responsive header#22954

Merged
noraleonte merged 8 commits into
mui:masterfrom
noraleonte:responsive-header
Jul 21, 2026
Merged

[scheduler] Responsive header#22954
noraleonte merged 8 commits into
mui:masterfrom
noraleonte:responsive-header

Conversation

@noraleonte

@noraleonte noraleonte commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

This PR implements a responsive header relying on container queries. On small screens

  • the header scales down and contains the menu button, view navigation icon buttons and a today icon button
  • The side panel is replaced by a drawer
  • the view switcher and preferences moved inside the drawer
  • no mini calendar on small screens

@code-infra-dashboard

code-infra-dashboard Bot commented Jun 25, 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-scheduler 🔺+5.51KB(+1.55%) 🔺+1.66KB(+1.74%)
@mui/x-scheduler-premium 🔺+5.53KB(+1.15%) 🔺+1.65KB(+1.25%)
@mui/x-chat 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.

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 1, 2026
@noraleonte noraleonte changed the title [DRAFT][scheduler] Responsive header [scheduler] Responsive header Jul 1, 2026
@noraleonte
noraleonte marked this pull request as ready for review July 1, 2026 13:18
@noraleonte noraleonte added type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. design This is about UI or UX design, please involve a designer. scope: scheduler Changes related to the scheduler. labels Jul 1, 2026
@noraleonte noraleonte self-assigned this Jul 1, 2026

@flaviendelangle flaviendelangle 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.

PR review

mui/mui-x#22954 (x-scheduler responsive header) is a well-structured refactor: it extracts the preferences menu into a reusable usePreferencesMenuItems hook and adds a mobile SidePanelDrawer toggled by a root CSS container query. Nothing is strictly merge-blocking, but there's one real responsive-state bug (an open mobile drawer isn't dismissed when the container grows past the desktop breakpoint), one invalid-ARIA structure, a dead translation key, and a notable test gap: the entire responsive show/hide behavior is unverifiable in jsdom and has no Chromium test. I checked out the branch and read the source plus the MUI Drawer/MenuList internals to substantiate each finding.

Bugs (2)

1. 🟡 An open mobile drawer stays visible on the desktop layout after the container crosses the breakpoint

Location: packages/x-scheduler/src/event-calendar/EventCalendarRoot.tsx:213

<SidePanelDrawer
  open={isMobileDrawerOpen}
  onClose={() => setIsMobileDrawerOpen(false)}
  container={rootRef}
/>

The desktop/mobile switch is CSS-only: the root container query hides [data-mobile-only] on desktop and [data-desktop-only] on mobile. But SidePanelDrawer is rendered without data-mobile-only, and SidePanelDrawerRoot has no desktop container query to hide itself. Its open/closed state (isMobileDrawerOpen) is plain React state that is never reset when the layout crosses 550px. Container queries are live and re-evaluate on any container resize.

Failure scenario: On a tablet, open the drawer in portrait (container < 550px), then rotate to landscape (container ≥ 550px). The container query now switches the calendar to the desktop layout — the inline side panel appears — but the modal drawer + backdrop remain stuck on top, covering the desktop calendar until the user manually dismisses it. The same happens on any window/parent resize while the drawer is open.

Fix: Tag the drawer data-mobile-only (so the existing root rule hides it on desktop) and/or reset isMobileDrawerOpen to false when the root query switches to desktop, so the mobile drawer can never linger over the desktop layout.

2. 🟡 Invalid ARIA: a role="menu" list wraps a single role="button"

Location: packages/x-scheduler/src/event-calendar/side-panel-drawer/SidePanelDrawer.tsx:200-218

<MenuList className={classes.sidePanelDrawerPreferences} aria-label={localeText.preferencesMenu}>
  <MenuItem
    className={classes.sidePanelDrawerPreferencesButton}
    role="button"
    aria-expanded={preferencesOpen}
    onClick={() => setPreferencesOpen((prev) => !prev)}
  >

MUI MenuList renders role="menu", whose only valid children are menuitem/menuitemradio/menuitemcheckbox — not button. Here the sole child is a disclosure control with role="button", so the parent/child ARIA structure is invalid.

Failure scenario: With any preference enabled, a screen-reader user (NVDA/VoiceOver) opening the drawer on mobile hears a "menu" containing one "button" and gets menu-navigation semantics applied to what is really an expand/collapse toggle — a mis-described control.

Fix: Render the toggle in a plain <div>/List instead of a MenuList, or drop the outer MenuList wrapper so no role="menu" is emitted around the button.

Tests (4)

1. 🟡 The responsive show/hide behavior — the point of the PR — has no test

Location: packages/x-scheduler/src/event-calendar/side-panel-drawer/SidePanelDrawer.test.tsx (and no Chromium test anywhere)

The data-desktop-only/data-mobile-only toggling and the drawer-vs-inline-panel swap are driven entirely by CSS container queries, which jsdom does not evaluate. Every drawer test runs in jsdom and passes only because jsdom ignores the container-query display:none (so the mobile menu button is always "present"). There is no describe.skipIf(isJSDOM) Chromium test for the responsive header, and the limitation isn't noted in a comment.

Failure scenario: The responsive CSS could regress (wrong breakpoint, a mistagged element, the drawer visible on desktop as in Bug #1) and the whole suite would stay green.

Fix: Add a Chromium-gated test that sets the container width on each side of 550px and asserts which controls/panels are visible; at minimum, comment that the jsdom tests cannot cover the responsive toggling.

2. 🟡 Drawer close via backdrop and Escape is untested

Location: SidePanelDrawer.test.tsx:34-42

Only the close-button path (onClick={onClose}) is exercised. The backdrop-click and Escape paths flow solely through the Drawer's own onClose and are unverified — even though the sibling EventCalendar.test.tsx already closes the preferences menu via Escape, showing the pattern is available and expected here.

3. 🟡 Preference toggling through the drawer asserts only that the list appears, not that it changes anything

Location: SidePanelDrawer.test.tsx:64-86

The expand/collapse test checks that sidePanelDrawerPreferencesList mounts/unmounts but never clicks an option, so the drawer's wiring of usePreferencesMenuItems()store.setPreferences(...) is never confirmed to actually toggle a preference. The desktop path is covered by EventCalendar.test.tsx, but the drawer reuse is not.

Fix: Click a menuitemcheckbox/menuitemradio inside the expanded drawer list and assert the corresponding store preference / rendered effect changed.

4. ℹ️ Negative branches for the two conditional drawer sections are untested

Location: SidePanelDrawer.tsx:127 (showViewSwitcher = views.length > 1) and :198 (hasAnyOption)

No test renders a single-view calendar (view listbox should be absent) or preferencesMenuConfig={false} (preferences section should be absent). Both are the negative edges of conditionally-rendered sections.

Simplifications (1)

1. 🟡 Dead back translation key added to the public localization API

Location: packages/x-scheduler/src/models/translations.ts:98 (+ enUS.ts:92 and all 41 locale files)

// SidePanelDrawer (small screens)
back: string;
openMenu: string;

openMenu is used (HeaderToolbar.tsx:130), but back is not referenced anywhere in the codebase. It's been added to the public EventCalendarLocaleText interface, enUS, all 41 locale files, and the generated docs/localization data — surface that must now be maintained for a key that does nothing.

Fix: Remove back from the interface and locale files (re-run the localization/docs generation), or wire it up if a "back" affordance was intended in the drawer.

Docs (0)

No findings.

Verdict

Approve after nits — no merge-blocker, but the drawer-stuck-on-desktop responsive bug (#1), the invalid ARIA structure, the dead back key, and the absent responsive/close-path test coverage are all worth addressing before merge.


Notes I investigated and cleared (no action needed): data-desktop-only is correctly forwarded to the DOM by both ViewSwitcher and PreferencesMenu (they spread ...props); focus trap/restore, container/disableScrollLock via slotProps.root, and Rules-of-Hooks in the extracted preference hooks are all correct; the responsiveTypography container-query refactor is behavior-preserving; no duplicate DOM ids across the two mounted resource trees; SSR/hydration is sound. The view-list role="listbox" with roving-tabindex (rather than aria-activedescendant) and the drawer not closing on view/preference selection both match existing conventions — flagged only as design call-outs.


🤖 Review generated with Claude Code

@rita-codes

rita-codes commented Jul 1, 2026

Copy link
Copy Markdown
Member

I tested this on my phone and it feels really natural, I love this approach 🙌
Just a couple of small things on top of Flavien's review that I didn't see covered there:

Bugs (1)

1. 🟡 The Drawer root slot collides with the paper's class key

Location: packages/x-scheduler/src/event-calendar/side-panel-drawer/SidePanelDrawer.tsx:30-32, :148, :155

const SidePanelDrawerRoot = styled(Drawer, {
  name: 'MuiEventCalendar',
  slot: 'SidePanelDrawer',   // → override key `sidePanelDrawer`, applied to the MODAL ROOT
})(...);
// ...
    className={classes.sidePanelDrawerViewport}         // the root/modal carries the "viewport" class
// ...
      className: clsx(className, classes.sidePanelDrawer), // the PAPER carries the "sidePanelDrawer" class

@mui/material's styled({ name, slot }) derives the styleOverrides key from the slot, so theme.components.MuiEventCalendar.styleOverrides.sidePanelDrawer targets the modal root. But the class docs (and the runtime className) put sidePanelDrawer on the paper — the root/modal is sidePanelDrawerViewport. Anyone who reads "Styles applied to the side panel drawer paper (popup) element" and overrides sidePanelDrawer will style the wrong element.

Failure scenario: a consumer sets styleOverrides: { sidePanelDrawer: { padding: 16 } } expecting to pad the drawer panel; the padding lands on the modal root and the panel is unchanged.

Fix: name the root slot 'SidePanelDrawerViewport' (matching the class it actually carries), freeing sidePanelDrawer for the paper.

Tests (1)

1. 🟡 New test descriptions don't start with "should"

Location: packages/x-scheduler/src/event-calendar/side-panel-drawer/SidePanelDrawer.test.tsx:24, :34, :44, :63

it('is closed by default and opens via the mobile menu button', ...)
it('closes via the close button', ...)
it('lists the views and reflects the selection when one is picked', ...)
it('expands and collapses the preferences options inline', ...)

The package uses "should …" almost universally (128 cases vs. a handful of legacy exceptions). These four break the convention.

Fix: reword to "should be closed by default…", "should close via the close button", etc.

Verdict

Approve after nits — on top of Flavien's review; nothing blocking, but the mislabeled slot (a theming footgun) and the test-description convention are worth fixing before merge.


🤖 Review generated with Claude Code

@rita-codes

Copy link
Copy Markdown
Member

Thanks for the quick follow-up! The slot rename, the ARIA restructure, the dead back key removal, and the new negative-branch tests all look good 👍. A few things on the new round:

Bugs (2)

1. 🔴 The new preference-toggle test is what's failing test_browser / test_browser_react_18

Location: packages/x-scheduler/src/event-calendar/side-panel-drawer/SidePanelDrawer.test.tsx:104

In browser mode the test viewport is wider than 550px, so the drawer root (now tagged data-mobile-only) is display: none and its whole subtree drops out of the accessibility tree — getByRole('menuitemcheckbox', ...) without hidden: true finds nothing. The sibling view-list test already works around this with { hidden: true } (lines 55/63); this one misses it. It passes in jsdom only because jsdom doesn't evaluate container queries. Worth considering sizing the rendered container below the breakpoint in browser mode instead, so these tests exercise the real compact layout rather than asserting on hidden nodes.

Failure scenario: both browser CI jobs stay red and the PR can't merge.

2. 🟡 CSS-hiding the open drawer leaves its modal behavior active on the desktop layout

Location: packages/x-scheduler/src/event-calendar/side-panel-drawer/SidePanelDrawer.tsx:152

data-mobile-only hides the drawer visually, but the Modal underneath stays open: ModalManager keeps aria-hidden="true" on all of the modal's siblings inside the portal container (i.e. the toolbar and the whole main panel), the backdrop can't be clicked once it's display: none, and focus has left the subtree so Escape no longer reaches the modal. In the rotate-to-landscape scenario from the previous review, a screen-reader user now gets a fully aria-hidden calendar with no way to dismiss the invisible drawer. I think the state-reset half of the original suggestion is still needed, so the modal actually closes instead of just disappearing.

Failure scenario: open the drawer in portrait, rotate to landscape — the desktop calendar renders but is invisible to assistive tech until the container shrinks back below 550px.

Tests (2)

1. 🟡 Backdrop/Escape close paths are still untested

Location: packages/x-scheduler/src/event-calendar/side-panel-drawer/SidePanelDrawer.test.tsx:40-48

Point 2 of the previous review seems to have slipped through — only the close-button path is exercised; the backdrop-click and Escape paths flow through the Drawer's own onClose wiring and remain unverified.

2. ℹ️ The new tests use fireEvent instead of the renderer's user

Location: packages/x-scheduler/src/event-calendar/side-panel-drawer/SidePanelDrawer.test.tsx:19, :25, :45, etc.

The package convention is await user.click(...) from createSchedulerRenderer (e.g. PreferencesMenu.test.tsx:240, and the pattern documented in test/utils/scheduler/dom-queries.ts). user.click runs the full pointer sequence (pointerdownmousedown → focus → click), which is more faithful — and particularly relevant for a Drawer, where backdrop dismissal depends on that event composition.

API (1)

1. ℹ️ The "mobile" vocabulary describes container-driven behavior

The layout switch triggers on the calendar's container width (550px), not on the device — a narrow calendar embedded in a desktop page also gets the drawer layout. Given that, the "mobile" naming that ships publicly (headerToolbarMobileMenuButton / headerToolbarMobileTodayButton class keys, the "(small screens)" class descriptions, data-mobile-only / data-desktop-only) is a bit misleading about when it applies. Class keys are hard to rename after release, so it's worth deciding now whether "mobile" is the vocabulary we want here or something layout-based (compact/expanded-style) fits better.

Verdict

Request changes — the browser CI failure (Bug 1) is merge-blocking; the rest can ride along in the same push.


🤖 Review generated with Claude Code

@rita-codes rita-codes 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.

Thanks for this!! This is a really good improvement and I'm sure it would be super useful soon 👏
Good job 🥇

@noraleonte
noraleonte merged commit 5050dce into mui:master Jul 21, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

design This is about UI or UX design, please involve a designer. 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.

3 participants