Skip to content

MainMenu accessibility improvements#13878

Merged
mayagbarnes merged 15 commits intofeature/new-app-menufrom
menu-access
Feb 12, 2026
Merged

MainMenu accessibility improvements#13878
mayagbarnes merged 15 commits intofeature/new-app-menufrom
menu-access

Conversation

@mayagbarnes
Copy link
Copy Markdown
Collaborator

@mayagbarnes mayagbarnes commented Feb 10, 2026

Describe your changes

Adds full WAI-ARIA menu pattern support to the MainMenu, including keyboard navigation, proper ARIA semantics, and focus management.

Keyboard navigation

  • Open menu: Enter / Space on the menu button (handles legacy Spacebar / Space key values)
  • Navigate items: ArrowDown / ArrowUp with wrapping at boundaries
  • Jump to ends: Home / End
  • Activate item: Enter / Space on focused menu item
  • Close menu: Escape returns focus to the trigger; Tab / Shift+Tab close the menu and advance focus to the next/previous tabbable element
  • Disabled items: All items are focusable and navigable, including disabled ones (per WAI-ARIA menuitem role guidance). Disabled items use aria-disabled instead of the native disabled attribute; clicks are blocked via the onClick handler.

ARIA attributes

  • Menu button: aria-haspopup="menu", aria-expanded (tracks open/close state), aria-label="Main menu"
  • Menu container: role="menu", aria-label="Main menu"
  • Menu items: role="menuitem", aria-disabled for disabled items (native disabled removed to keep items focusable)
  • Dividers: role="separator", aria-hidden="true"
  • Roving tabindex: focused item gets tabIndex={0}, all others get tabIndex={-1}

Focus management

  • Focus moves to the first menu item when the menu opens
  • Focus returns to the menu button after Escape or item click, via react-focus-lock's returnFocus callback
  • Tab / Shift+Tab close the menu and programmatically advance focus to the next/previous tabbable element using focusNextElement/focusPrevElement from focus-lock
  • WebKit limitation: On Safari, focus restoration after menu close may not work because react-focus-lock invokes the returnFocus callback after BaseWeb's close animation timer, which puts the .focus() call outside the user-activation context

Clear cache dialog (WAI-ARIA best practice)

  • Moves autoFocus from the destructive "Clear caches" button to the non-destructive "Cancel" button, per WAI-ARIA guidance for confirmation dialogs
  • Removes the now-unnecessary defaultAction prop from ClearCacheProps

Theme fix

  • focusRingMuted shadow now uses gray10 on dark backgrounds (was using gray90, which was invisible)

Testing Plan

  • JS Unit Tests: ✅ Added
  • E2E Tests: ✅ Added
  • Manual Testing: ✅

@mayagbarnes mayagbarnes added security-assessment-completed change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Feb 10, 2026
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Feb 10, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 10, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-13878/streamlit-1.54.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-13878.streamlit.app (☁️ Deploy here if not accessible)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 10, 2026

📈 Frontend coverage change detected

The frontend unit test (vitest) coverage has increased by 0.0500%

  • Current PR: 86.8700% (13961 lines, 1833 missed)
  • Latest develop: 86.8200% (13884 lines, 1829 missed)

🎉 Great job on improving test coverage!

📊 View detailed coverage comparison

@mayagbarnes mayagbarnes changed the title [WIP] MainMenu accessibility improvements MainMenu accessibility improvements Feb 11, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the frontend MainMenu to follow the WAI-ARIA menu button pattern more closely, including keyboard navigation, ARIA semantics, and focus behavior. It also adjusts the clear-cache confirmation dialog focus behavior and fixes a theme focus ring color issue on dark backgrounds.

Changes:

  • Add roving-tabindex keyboard navigation + ARIA roles/attributes for MainMenu (and tests).
  • Update clear-cache dialog to autofocus the non-destructive “Cancel” action and remove defaultAction plumbing.
  • Fix focusRingMuted shadow color selection for dark backgrounds (and tests).

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
frontend/lib/src/theme/getShadows.ts Use gray10 on dark backgrounds for focusRingMuted.
frontend/lib/src/theme/getShadows.test.ts Update unit tests to validate the new focusRingMuted behavior.
frontend/lib/src/components/shared/BaseButton/styled-components.ts Extend BaseButton props to support aria-haspopup / aria-expanded (optionally).
frontend/lib/src/components/shared/BaseButton/BaseButton.tsx Forward aria-haspopup / aria-expanded to the rendered button element.
frontend/app/src/components/StreamlitDialog/StreamlitDialog.tsx Move autofocus in Clear Cache dialog from destructive to non-destructive action; remove defaultAction.
frontend/app/src/components/StreamlitDialog/StreamlitDialog.test.tsx Update dialog focus expectation to “Cancel”.
frontend/app/src/components/MainMenu/styled-components.ts Add visible keyboard focus ring styling for menu items.
frontend/app/src/components/MainMenu/MainMenu.tsx Implement ARIA menu semantics, roving tabindex, keyboard navigation, and focus restoration strategy.
frontend/app/src/components/MainMenu/MainMenu.test.tsx Add unit tests covering keyboard navigation, ARIA attributes, roving tabindex, and focus behavior.
frontend/app/src/App.tsx Remove defaultAction usage when opening Clear Cache dialog.
e2e_playwright/main_menu_test.py Add Playwright E2E coverage for MainMenu ARIA attributes/roles and keyboard interactions.

@mayagbarnes mayagbarnes added the ai-review If applied to PR or issue will run AI review workflow label Feb 11, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Feb 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR adds comprehensive WAI-ARIA menu-button pattern support to the MainMenu component, including:

  • Keyboard navigation: ArrowUp/Down, Home/End, Enter/Space to activate, Escape to close (with focus return), Tab/Shift+Tab to close and advance focus.
  • ARIA attributes: role="menu", role="menuitem", role="separator", aria-haspopup="menu", aria-expanded, aria-disabled, aria-hidden on separators.
  • Roving tabindex: Focused item gets tabIndex={0}, all others get tabIndex={-1}.
  • Focus management: Focus moves to first enabled item on open, returns to trigger on Escape/click close, advances naturally on Tab.
  • Clear cache dialog: autoFocus moved from destructive "Clear caches" to non-destructive "Cancel" (WAI-ARIA best practice for destructive dialogs). defaultAction prop removed.
  • Theme fix: focusRingMuted now uses gray10 on dark backgrounds (previously gray90 was invisible).
  • BaseButton: Now supports aria-haspopup and aria-expanded props.

Files changed: 11 (770 additions, 35 deletions).

Code Quality

The code is well-structured and follows existing patterns. Specific observations:

Strengths:

  • Clean implementation of WAI-ARIA menu-button pattern with proper separation of concerns (CloseReason type, roving tabindex logic in MenuContent).
  • Module-level constants (MENU_OPEN_KEYS, SCREENCAST_LABEL) follow the TypeScript best practices for static data.
  • All hooks follow the React rules: useCallback for stable references, useMemo for computed values, useRef for mutable data that doesn't trigger re-renders.
  • The useEffect for focus management (line 463-467 of MainMenu.tsx) is appropriate — it synchronizes with an imperative DOM API (.focus()), which qualifies as an "external system."
  • Menu item activation leverages native <button> click behavior (Enter/Space on focused buttons) rather than duplicating activation logic in the keyDown handler. This is elegant.
  • Comments explain "why" not "what" — especially the BaseWeb workarounds.
  • The RequiredBaseButtonProps type in BaseButton/styled-components.ts is well-designed: ARIA popup attributes stay optional so they only render in the DOM when explicitly provided.

Minor observations (non-blocking):

  1. Timer race in handlePopoverClose (MainMenu.tsx:686): If the popover closes and reopens within the 50ms focus-return timer window, the pending timer could fire while the popover is open and briefly steal focus to the button. Consider clearing the previous timer before setting a new one:
// In handlePopoverClose:
clearTimeout(focusReturnTimerRef.current)
focusReturnTimerRef.current = setTimeout(...)

Similarly, handlePopoverOpen could clear any pending focus-return timer to be defensive:

const handlePopoverOpen = useCallback((): void => {
  clearTimeout(focusReturnTimerRef.current)
  setIsMenuOpen(true)
}, [])

This is extremely unlikely to manifest in practice (requires close-then-reopen within 50ms) but would be more robust.

  1. document.querySelector for focus return (MainMenu.tsx:687-689): The code uses a global DOM query because BaseWeb overwrites refs via cloneElement. This is well-justified by the inline comment. No action needed — just noting it as a known limitation.

  2. Content render prop arrow function (MainMenu.tsx:725-729): The closeMenu callback passed to MenuContent creates a new arrow function on each render. However, the inline comment on MenuContent (line 427-429) already explains why memoization would be futile here (BaseWeb's close is also a new reference each render). This is fine.

Test Coverage

Excellent coverage across all layers:

Frontend unit tests (MainMenu.test.tsx) — ~30 new tests:

  • Keyboard open (Enter, Space) via parameterized it.each
  • Arrow key navigation (up, down, wrapping)
  • Home/End keys
  • Disabled item skipping (single, multiple consecutive, leading disabled items)
  • Item activation via Enter and Space
  • Menu close via Escape, Tab, Shift+Tab
  • Focus return to trigger after Escape and after item click
  • Tab/Shift+Tab correctly does NOT return focus (negative assertion — good!)
  • Roving tabindex verification (tabIndex 0 vs -1)
  • ARIA roles (menu, menuitem, separator)
  • ARIA attributes lifecycle (aria-haspopup, aria-expanded open/close cycle, aria-disabled, aria-hidden)
  • Tests properly use userEvent with advanceTimers for fake timers compatibility
  • Uses act() to flush timer-based state transitions
  • Includes complementary negative assertions throughout (e.g., "Tab should NOT return focus to trigger")

E2E tests (main_menu_test.py) — 6 new tests:

  • ARIA attributes on menu button (before and after open)
  • Roles on menu container and items (with WebKit MediaRecorder exemption)
  • Full keyboard navigation flow (open, navigate, close)
  • Keyboard item activation (Settings dialog opens)
  • Focus return after close (@pytest.mark.skip_browser("webkit") — well-justified)
  • Tab closing behavior (focus does NOT return to trigger)
  • All tests use expect for auto-wait assertions
  • All tests have descriptive docstrings

StreamlitDialog tests: Updated for defaultAction removal and Cancel-focus behavior.

getShadows tests: Added assertions for focusRingSubtle and focusRingMuted in dark theme context.

Best practices compliance:

  • E2E tests use expect (not assert) ✅
  • E2E tests use stable locators (get_by_test_id, get_by_role) ✅
  • Unit tests use userEvent for interactions ✅
  • Unit tests include negative assertions ✅
  • Parameterized tests where appropriate ✅
  • Tests have descriptive names and docstrings ✅

Backwards Compatibility

No breaking changes for end users:

  1. defaultAction prop removal: Only affects the internal ClearCacheProps interface — not part of the public API. No references remain in the codebase (verified via search).

  2. Clear cache dialog focus change: Focus now lands on "Cancel" instead of "Clear caches." This is a deliberate UX improvement following WAI-ARIA best practices for destructive confirmation dialogs. Users who previously relied on Enter to immediately confirm will now need to Tab to the confirm button. This is intentional and safer.

  3. Keyboard behavior: The menu now intercepts keyboard events that previously had no special handling. This is purely additive — no existing keyboard workflows are broken.

  4. focusRingMuted theme change: Bug fix — the muted focus ring was invisible on dark backgrounds. Now adapts correctly.

  5. BaseButton props: Two new optional ARIA props added. Existing consumers are unaffected (both default to undefined).

Security & Risk

No security concerns identified:

  • All changes are UI/accessibility related
  • No new network requests, data handling, or external system integrations
  • The document.querySelector call is scoped to a specific data-testid attribute, not user-controllable input
  • No changes to authentication, authorization, or data flow

Regression risk: Low. The changes are well-isolated to the MainMenu component and its immediate dependencies. The 50ms setTimeout for focus return is the main "fragile" point, but it's thoroughly tested and well-documented.

Accessibility

This PR is specifically an accessibility improvement. Assessment of the implementation:

  • WAI-ARIA menu-button pattern: Correctly implemented per WAI-ARIA APG Menu Button
  • Semantic roles: role="menu", role="menuitem", role="separator"
  • Keyboard patterns: ArrowUp/Down, Home/End, Enter/Space, Escape, Tab/Shift+Tab all follow the spec ✅
  • Focus management: Focus moves into menu on open, returns to trigger on Escape, advances on Tab ✅
  • Roving tabindex: Correct implementation (focused=0, others=-1) ✅
  • Disabled items: Properly skipped during keyboard navigation; marked with aria-disabled
  • Visible focus indicator: focusRingMuted box-shadow on :focus-visible
  • Decorative elements: Separators marked with aria-hidden="true"
  • Destructive dialog focus: Cancel button gets initial focus per WAI-ARIA guidance ✅
  • Dark theme focus ring: Fixed to be visible ✅

Recommendations

  1. (Minor, non-blocking) Consider clearing the focus-return timer in handlePopoverOpen and before setting a new timer in handlePopoverClose to prevent an extremely unlikely race condition. See Code Quality section above for details.

  2. Everything else looks solid. The implementation is thorough, well-tested, and follows established accessibility patterns.

Verdict

APPROVED: Excellent accessibility PR that implements the WAI-ARIA menu-button pattern correctly with comprehensive test coverage across unit and E2E layers. The code is well-structured, thoroughly documented, and backwards compatible. The one minor timer race observation is non-blocking.


This is an automated AI review using opus-4.6-thinking. Please verify the feedback and use your judgment.

@mayagbarnes mayagbarnes marked this pull request as ready for review February 11, 2026 05:10
Copy link
Copy Markdown
Collaborator

@sfc-gh-bnisco sfc-gh-bnisco left a comment

Choose a reason for hiding this comment

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

LGTM after addressing a few comments!

// Focus the item at a given index and update state for tabIndex tracking.
// Called directly from keyboard handlers rather than going through a
// state → render → effect cycle.
const focusAndSetIndex = (index: number): void => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

suggestion: I think we need to call focusAndSetIndex on click/focus of each element, or else the focusIndex state will be incorrect when a user interacts with a mouse and then a keyboard.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense, updated!

@mayagbarnes mayagbarnes merged commit 4f0862b into feature/new-app-menu Feb 12, 2026
39 checks passed
@mayagbarnes mayagbarnes deleted the menu-access branch February 12, 2026 22:02
sfc-gh-mbarnes pushed a commit that referenced this pull request Feb 12, 2026
Adds full WAI-ARIA menu pattern support to the `MainMenu`, including
keyboard navigation, proper ARIA semantics, and focus management.

**Keyboard navigation**
- **Open menu**: `Enter` / `Space` on the menu button (handles legacy
`Spacebar` / `Space` key values)
- **Navigate items**: `ArrowDown` / `ArrowUp` with wrapping at
boundaries
- **Jump to ends**: `Home` / `End`
- **Activate item**: `Enter` / `Space` on focused menu item
- **Close menu**: `Escape` returns focus to the trigger; `Tab` /
`Shift+Tab` close the menu and advance focus to the next/previous
tabbable element
- **Disabled items**: All items are focusable and navigable, including
disabled ones (per [WAI-ARIA menuitem role
guidance](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/menuitem_role)).
Disabled items use `aria-disabled` instead of the native `disabled`
attribute; clicks are blocked via the `onClick` handler.

**ARIA attributes**
- Menu button: `aria-haspopup="menu"`, `aria-expanded` (tracks
open/close state), `aria-label="Main menu"`
- Menu container: `role="menu"`, `aria-label="Main menu"`
- Menu items: `role="menuitem"`, `aria-disabled` for disabled items
(native `disabled` removed to keep items focusable)
- Dividers: `role="separator"`, `aria-hidden="true"`
- Roving tabindex: focused item gets `tabIndex={0}`, all others get
`tabIndex={-1}`

**Focus management**
- Focus moves to the first menu item when the menu opens
- Focus returns to the menu button after `Escape` or item click, via
react-focus-lock's `returnFocus` callback
- `Tab` / `Shift+Tab` close the menu and programmatically advance focus
to the next/previous tabbable element using
`focusNextElement`/`focusPrevElement` from `focus-lock`
- **WebKit limitation**: On Safari, focus restoration after menu close
may not work because react-focus-lock invokes the `returnFocus` callback
after BaseWeb's close animation timer, which puts the `.focus()` call
outside the user-activation context

**Clear cache dialog (WAI-ARIA best practice)**
- Moves `autoFocus` from the destructive "Clear caches" button to the
non-destructive "Cancel" button, per WAI-ARIA guidance for confirmation
dialogs
- Removes the now-unnecessary `defaultAction` prop from
`ClearCacheProps`

**Theme fix**
- `focusRingMuted` shadow now uses `gray10` on dark backgrounds (was
using `gray90`, which was invisible)
mayagbarnes added a commit that referenced this pull request Feb 17, 2026
Adds a theme toggle (System / Light / Dark) directly into the main menu as `menuitemradio` elements, building on the accessibility refactor in #13878.

**Changes:**
- Three radio items (System, Light, Dark) appear at the top of the menu,
inside a `role="group"` container with `aria-label="Theme"`.
- Each item uses `role="menuitemradio"` with `aria-checked` to reflect
the active theme.
- Selecting a theme calls `setTheme` via `ThemeContext` without closing
the menu, so users can preview different themes.
- Keyboard navigation (ArrowUp/Down, Home/End, Enter/Space) flows
seamlessly between radio items and action items in a single roving
tabindex.
sfc-gh-mbarnes pushed a commit that referenced this pull request Feb 19, 2026
Adds full WAI-ARIA menu pattern support to the `MainMenu`, including
keyboard navigation, proper ARIA semantics, and focus management.

**Keyboard navigation**
- **Open menu**: `Enter` / `Space` on the menu button (handles legacy
`Spacebar` / `Space` key values)
- **Navigate items**: `ArrowDown` / `ArrowUp` with wrapping at
boundaries
- **Jump to ends**: `Home` / `End`
- **Activate item**: `Enter` / `Space` on focused menu item
- **Close menu**: `Escape` returns focus to the trigger; `Tab` /
`Shift+Tab` close the menu and advance focus to the next/previous
tabbable element
- **Disabled items**: All items are focusable and navigable, including
disabled ones (per [WAI-ARIA menuitem role
guidance](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/menuitem_role)).
Disabled items use `aria-disabled` instead of the native `disabled`
attribute; clicks are blocked via the `onClick` handler.

**ARIA attributes**
- Menu button: `aria-haspopup="menu"`, `aria-expanded` (tracks
open/close state), `aria-label="Main menu"`
- Menu container: `role="menu"`, `aria-label="Main menu"`
- Menu items: `role="menuitem"`, `aria-disabled` for disabled items
(native `disabled` removed to keep items focusable)
- Dividers: `role="separator"`, `aria-hidden="true"`
- Roving tabindex: focused item gets `tabIndex={0}`, all others get
`tabIndex={-1}`

**Focus management**
- Focus moves to the first menu item when the menu opens
- Focus returns to the menu button after `Escape` or item click, via
react-focus-lock's `returnFocus` callback
- `Tab` / `Shift+Tab` close the menu and programmatically advance focus
to the next/previous tabbable element using
`focusNextElement`/`focusPrevElement` from `focus-lock`
- **WebKit limitation**: On Safari, focus restoration after menu close
may not work because react-focus-lock invokes the `returnFocus` callback
after BaseWeb's close animation timer, which puts the `.focus()` call
outside the user-activation context

**Clear cache dialog (WAI-ARIA best practice)**
- Moves `autoFocus` from the destructive "Clear caches" button to the
non-destructive "Cancel" button, per WAI-ARIA guidance for confirmation
dialogs
- Removes the now-unnecessary `defaultAction` prop from
`ClearCacheProps`

**Theme fix**
- `focusRingMuted` shadow now uses `gray10` on dark backgrounds (was
using `gray90`, which was invisible)
sfc-gh-mbarnes pushed a commit that referenced this pull request Feb 19, 2026
Adds a theme toggle (System / Light / Dark) directly into the main menu as `menuitemradio` elements, building on the accessibility refactor in #13878.

**Changes:**
- Three radio items (System, Light, Dark) appear at the top of the menu,
inside a `role="group"` container with `aria-label="Theme"`.
- Each item uses `role="menuitemradio"` with `aria-checked` to reflect
the active theme.
- Selecting a theme calls `setTheme` via `ThemeContext` without closing
the menu, so users can preview different themes.
- Keyboard navigation (ArrowUp/Down, Home/End, Enter/Space) flows
seamlessly between radio items and action items in a single roving
tabindex.
sfc-gh-mbarnes pushed a commit that referenced this pull request Feb 24, 2026
Adds full WAI-ARIA menu pattern support to the `MainMenu`, including
keyboard navigation, proper ARIA semantics, and focus management.

**Keyboard navigation**
- **Open menu**: `Enter` / `Space` on the menu button (handles legacy
`Spacebar` / `Space` key values)
- **Navigate items**: `ArrowDown` / `ArrowUp` with wrapping at
boundaries
- **Jump to ends**: `Home` / `End`
- **Activate item**: `Enter` / `Space` on focused menu item
- **Close menu**: `Escape` returns focus to the trigger; `Tab` /
`Shift+Tab` close the menu and advance focus to the next/previous
tabbable element
- **Disabled items**: All items are focusable and navigable, including
disabled ones (per [WAI-ARIA menuitem role
guidance](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/menuitem_role)).
Disabled items use `aria-disabled` instead of the native `disabled`
attribute; clicks are blocked via the `onClick` handler.

**ARIA attributes**
- Menu button: `aria-haspopup="menu"`, `aria-expanded` (tracks
open/close state), `aria-label="Main menu"`
- Menu container: `role="menu"`, `aria-label="Main menu"`
- Menu items: `role="menuitem"`, `aria-disabled` for disabled items
(native `disabled` removed to keep items focusable)
- Dividers: `role="separator"`, `aria-hidden="true"`
- Roving tabindex: focused item gets `tabIndex={0}`, all others get
`tabIndex={-1}`

**Focus management**
- Focus moves to the first menu item when the menu opens
- Focus returns to the menu button after `Escape` or item click, via
react-focus-lock's `returnFocus` callback
- `Tab` / `Shift+Tab` close the menu and programmatically advance focus
to the next/previous tabbable element using
`focusNextElement`/`focusPrevElement` from `focus-lock`
- **WebKit limitation**: On Safari, focus restoration after menu close
may not work because react-focus-lock invokes the `returnFocus` callback
after BaseWeb's close animation timer, which puts the `.focus()` call
outside the user-activation context

**Clear cache dialog (WAI-ARIA best practice)**
- Moves `autoFocus` from the destructive "Clear caches" button to the
non-destructive "Cancel" button, per WAI-ARIA guidance for confirmation
dialogs
- Removes the now-unnecessary `defaultAction` prop from
`ClearCacheProps`

**Theme fix**
- `focusRingMuted` shadow now uses `gray10` on dark backgrounds (was
using `gray90`, which was invisible)
sfc-gh-mbarnes pushed a commit that referenced this pull request Feb 24, 2026
Adds a theme toggle (System / Light / Dark) directly into the main menu as `menuitemradio` elements, building on the accessibility refactor in #13878.

**Changes:**
- Three radio items (System, Light, Dark) appear at the top of the menu,
inside a `role="group"` container with `aria-label="Theme"`.
- Each item uses `role="menuitemradio"` with `aria-checked` to reflect
the active theme.
- Selecting a theme calls `setTheme` via `ThemeContext` without closing
the menu, so users can preview different themes.
- Keyboard navigation (ArrowUp/Down, Home/End, Enter/Space) flows
seamlessly between radio items and action items in a single roving
tabindex.
mayagbarnes added a commit that referenced this pull request Feb 24, 2026
This PR is the culmination of the following PRs approved into this feature branch:
- [Core `MainMenu` Refactor](#13833)
- [`MainMenu` accessibility improvements](#13878)
- [Add theme switcher to `MainMenu`](#13937)
- [Add auto-rerun toggle to `MainMenu`](#13988)
- [Add version footer to `MainMenu`](#14028)
- [Remove `SettingsDialog`](#14048)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants