[scheduler] Responsive header#22954
Conversation
Deploy previewBundle size
Check out the code infra dashboard for more information about this PR. |
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
flaviendelangle
left a comment
There was a problem hiding this comment.
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
|
I tested this on my phone and it feels really natural, I love this approach 🙌 Bugs (1)1. 🟡 The Drawer root
|
|
Thanks for the quick follow-up! The slot rename, the ARIA restructure, the dead Bugs (2)1. 🔴 The new preference-toggle test is what's failing
|
rita-codes
left a comment
There was a problem hiding this comment.
Thanks for this!! This is a really good improvement and I'm sure it would be super useful soon 👏
Good job 🥇
This PR implements a responsive header relying on container queries. On small screens