Skip to content

core(menu): Implement w3c menu pattern for ix-menu#2573

Open
lzeiml wants to merge 35 commits into
mainfrom
feat/4304-menu-a11y
Open

core(menu): Implement w3c menu pattern for ix-menu#2573
lzeiml wants to merge 35 commits into
mainfrom
feat/4304-menu-a11y

Conversation

@lzeiml

@lzeiml lzeiml commented May 28, 2026

Copy link
Copy Markdown
Collaborator

IX-4304

💡 What is the current behavior?

Currently ix-menu does not have proper keyboard-navigation according to w3c menubar pattern: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/

🆕 What is the new behavior?

Implements the w3c keyboard-navigation and roles for better screenreader support.

🏁 Checklist

A pull request can only be merged if all of these conditions are met (where applicable):

  • 🦮 Accessibility (a11y) features were implemented
  • 🗺️ Internationalization (i18n) - no hard coded strings
  • 📲 Responsiveness - components handle viewport changes and content overflow gracefully
  • 📕 Add or update a Storybook story
  • 📄 Documentation was reviewed/updated siemens/ix-docs
  • 🧪 Unit tests were added/updated and pass (pnpm test)
  • 📸 Visual regression tests were added/updated and pass (Guide)
  • 🧐 Static code analysis passes (pnpm lint)
  • 🏗️ Successful compilation (pnpm build, changes pushed)

👨‍💻 Help & support

Summary by CodeRabbit

  • New Features
    • Added accessibility i18n props for ix-menu and enhanced menubar support with roving tabIndex, improved Arrow/Home/End navigation, and utility controls grouping.
    • Introduced public setTabIndex() APIs for menu items/categories and improved category labeling support; added typings for ix-playground.
  • Bug Fixes / Accessibility
    • Refined dropdown open/close and focus restore behavior, updated ARIA roles/attributes (e.g., tooltip, menu item state), and improved keyboard activation handling.
    • Updated expand/collapse icons to use aria-expanded.
  • Tests
    • Updated Playwright and visual e2e tests to match updated menuitem roles and interactions.
  • Chores
    • Refreshed public typings (including Angular proxy inputs) and added an accessibility patch changeset.

@changeset-bot

changeset-bot Bot commented May 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0f2746b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@siemens/ix Patch
@siemens/ix-angular Patch
@siemens/ix-docs Patch
@siemens/ix-react Patch
@siemens/ix-vue Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented May 28, 2026

Copy link
Copy Markdown

Deploy Preview for ix-storybook ready!

Name Link
🔨 Latest commit 0f2746b
🔍 Latest deploy log https://app.netlify.com/projects/ix-storybook/deploys/6a43b96247fece000803acfd
😎 Deploy Preview https://deploy-preview-2573--ix-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces keyboard navigation and a roving tab index mechanism for the ix-menu component, updating ix-menu-item and ix-menu-category to support dynamic tab index configuration. A critical review comment points out that using closest('ix-menu') to identify direct menu children is too broad and will break focusability for nested items in slots like ix-menu-settings, suggesting a direct parent check instead.

Comment thread packages/core/src/components/menu-item/menu-item.tsx Outdated
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements menubar keyboard navigation and ARIA updates for ix-menu, plus related ix-menu-item and ix-menu-category focus behavior changes, typings, styling, tests, fixtures, and a changeset.

Changes

Menubar accessibility implementation

Layer / File(s) Summary
Public API and typings
.changeset/fair-falcons-search.md, packages/core/src/components.d.ts, packages/angular/src/components.ts
Adds menu i18n props, setTabIndex() methods, menuCategoryLabel, and ix-playground typings across Stencil, JSX, Angular, and changeset surfaces.
Menu navigation and layout
packages/core/src/components/menu/menu.tsx, packages/core/src/components/menu/menu.scss, packages/core/src/components/toast/tests/toast.ct.ts, testing/visual-testing/tests/menu-about-news/menu-about-news.e2e.ts, testing/visual-testing/tests/menu/link/index.html
Turns ix-menu into a vertical menubar with roving tabindex, overflow tracking, utility-item grouping, focus-visible styling, updated test locators, and revised menu link markup.
Menu item ARIA and role handling
packages/core/src/components/menu-item/menu-item.tsx
Refactors ix-menu-item to inherit ARIA attributes, compute effective roles and labels, manage host tab index, and update rendering for menu/category context.
Category dropdown focus and labeling
packages/core/src/components/menu-category/menu-category.tsx, packages/core/src/components/menu-category/test/menu-category.ct.ts
Adds generated category IDs, dropdown show/hide focus handling, category-label plumbing, and arrow-key navigation coverage for the dropdown regression test.

🎯 4 (Complex) | ⏱️ ~60 minutes

🐰 I hopped through the menu with cheer,
Tabs turned to arrows, crisp and clear.
Labels and focus now lead the way,
Menubar moonbeams guide the day. ✨

  • danielleroux
  • nuke-ellington
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: implementing the W3C menu pattern for ix-menu.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/4304-menu-a11y

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lzeiml lzeiml marked this pull request as ready for review June 12, 2026 07:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/core/src/components.d.ts (1)

2595-2690: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the required @since docs for the new public API.

setTabIndex and menuCategoryLabel are new externally visible API surface, but these generated typings don’t carry the version metadata this repo requires for new props/methods.

As per coding guidelines, new public API additions must include JSDoc with a @since tag in the source declarations so the generated types stay consistent. ``

Also applies to: 9107-9170, 11757-11771

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components.d.ts` around lines 2595 - 2690, The generated
typings are missing `@since` tags for newly added public API; add JSDoc `@since`
entries to the source declarations for the methods/props named setTabIndex and
menuCategoryLabel so the generator emits the version metadata. Locate the
component/source declaration files that define the IxMenuCategory/IxMenuItem
APIs (the functions/properties setTabIndex and menuCategoryLabel) and prepend
appropriate /** `@since` x.y.z */ JSDoc to each symbol (method signature for
setTabIndex and the menuCategoryLabel prop declaration) using the correct
release version.

Source: Coding guidelines

packages/core/src/components/menu-category/test/menu-category.ct.ts (1)

9-9: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing accessibility test using makeAxeBuilder.

Per coding guidelines, every .ct.ts file must include an 'accessibility' test using makeAxeBuilder to verify that accessibilityScanResults.violations returns an empty array. This test file has a 'renders' test but is missing the required accessibility test.

💡 Suggested accessibility test
import { makeAxeBuilder } from '`@utils/test`';

regressionTest('accessibility', async ({ mount, page }) => {
  await mount(`
    <ix-application>
      <ix-menu>
        <ix-menu-category label="Category label">
          <ix-menu-item>Test Item 1</ix-menu-item>
          <ix-menu-item>Test Item 2</ix-menu-item>
        </ix-menu-category>
      </ix-menu>
    </ix-application>
  `);

  const element = page.locator('ix-menu-category');
  await expect(element).toHaveClass(/hydrated/);

  const results = await makeAxeBuilder({ page }).analyze();
  expect(results.violations).toHaveLength(0);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/menu-category/test/menu-category.ct.ts` at line
9, Add a new regressionTest named "accessibility" to menu-category.ct.ts that
uses makeAxeBuilder to scan the mounted component; specifically, mount the same
markup used by the existing "renders" test (an ix-application with ix-menu and
ix-menu-category containing two ix-menu-item children), await the component to
hydrate via page.locator('ix-menu-category'), run makeAxeBuilder({ page
}).analyze(), and assert that results.violations has length 0 using expect;
ensure you import makeAxeBuilder from '`@utils/test`' and keep the test signature
consistent with the existing regressionTest(async ({ mount, page }) => { ... }).

Source: Coding guidelines

packages/core/src/components/toast/tests/toast.ct.ts (2)

19-171: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the required axe accessibility regression for this .ct.ts file.

This file has a 'renders' test, but it still lacks the mandatory 'accessibility' test using makeAxeBuilder() and an empty accessibilityScanResults.violations assertion. That leaves the menu-role change in this path uncovered by the repo’s required a11y gate. As per coding guidelines, "Ensure accessibility is tested with component tests using axe, verifying that accessibilityScanResults.violations returns an empty array", "Every test file should include an 'accessibility' test using makeAxeBuilder", and "Every test file should include a 'renders' test verifying basic hydration".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/toast/tests/toast.ct.ts` around lines 19 - 171,
Add a new regressionTest named 'accessibility' that mounts the component (reuse
the same mount pattern used in the 'renders' test), uses makeAxeBuilder(page) to
run an axe scan (include the toast element/selector), stores the result in
accessibilityScanResults, and assert accessibilityScanResults.violations is an
empty array; reference the existing regressionTest helper and makeAxeBuilder,
and place the test alongside the other tests in this file so the repo's a11y
gate is satisfied.

Source: Coding guidelines


71-72: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace the fixed sleeps with state-based waits.

These waitForTimeout(...) calls make the toast tests timing-sensitive and violate the component-test rules. Please wait on observable state instead—e.g. toast/menu visibility, attribute changes, or expect.poll() for the pause/resume API—so the test follows the component rather than the clock. As per coding guidelines, "Don't use arbitrary timeouts or waitForTimeout in tests" and "Use expect.poll() when a value requires async resolution or retries, but prefer Playwright's built-in auto-retry assertions whenever possible".

Also applies to: 107-108, 115-116

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/toast/tests/toast.ct.ts` around lines 71 - 72,
The tests currently use page.waitForTimeout(...) (e.g., the fixed 500ms sleeps)
which makes them timing-sensitive; replace each sleep with state-based waits
that observe the component: use Playwright's auto-retry assertions like await
expect(menuLocator).toBeVisible() / toBeHidden() or await
expect(toastLocator).toHaveAttribute(...) for attribute changes, and use
expect.poll() for the pause/resume API checks instead of fixed timeouts; update
the places calling page.waitForTimeout to wait on the toast/menu locators or the
pause/resume observable state (referencing the existing page.waitForTimeout
calls, the toast/menu locators, and the pause/resume checks) so tests wait on
component state not the clock.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/fair-falcons-search.md:
- Line 5: Update the changeset description string that currently reads "Improved
**ix-menu** accessibility by implementing the w3c menubar pattern, adding better
keyboard navigation and screenreader suppport." — fix the typo "suppport" →
"support" and capitalize the acronym "w3c" → "W3C" so the sentence becomes
"Improved **ix-menu** accessibility by implementing the W3C menubar pattern,
adding better keyboard navigation and screenreader support." Reference the same
changeset entry text in .changeset/fair-falcons-search.md to make the edits.
- Around line 1-5: The changeset for ix-menu is marked as patch but the
component adds new public API (new `@Prop/`@Event/@Method and exported interfaces
under packages/core/src/components/menu/*), so update
.changeset/fair-falcons-search.md to change the release type from patch to minor
and clearly list the `@siemens/ix` package; only add a "Fixes `#4304`" footer if the
PR/commit is actually linked to issue `#4304` (confirm the PR/issue connection
before adding that line).

In `@packages/core/src/components/menu-category/menu-category.tsx`:
- Around line 34-38: The helper createSequentialId currently increments its
parameter (sequenceId++) and callers also pass a post-incremented variable,
causing a redundant mutation; change createSequentialId to not mutate its input
(return `${prefix}-${sequenceId}`) and leave call-sites such as the usage with
categorySequenceId in menu-category.tsx and similar in menu-item.tsx using
sequenceId++ intact so the increment only happens at the call site; update the
createSequentialId implementation (and any docs/tests) to reflect the
non-mutating behavior.

In `@packages/core/src/components/menu-item/menu-item.tsx`:
- Around line 130-134: The new public API surface includes setTabIndex() (the
async method decorated with `@Method`() that sets hostTabIndex) but lacks
JSDoc/@since; either make it intentionally public by adding a JSDoc block above
setTabIndex() that documents the method and includes a `@since` tag with the
correct version, description and intended usage, or if it is internal plumbing
for roving tabindex, remove the `@Method`() decorator (and keep the method private
or rename to _setTabIndex) so it does not appear on the Stencil public surface;
update references to setTabIndex accordingly.
- Around line 218-233: getAriaLabel can produce "undefined …" when neither label
nor menuCategoryLabel is set; update getAriaLabel to compute a safe baseLabel
(e.g., const baseLabel = this.label ?? this.menuCategoryLabel ??
this.hostElement.textContent?.trim()) and use that instead of this.label when
building the distinct-tooltip branch; if baseLabel is falsy, return just
this.tooltipText (or undefined if tooltipText is also falsy) so you never
concatenate "undefined" with the tooltipText. Ensure changes are made inside the
getAriaLabel method and reference inheritAriaAttributes, tooltipText, label,
menuCategoryLabel, and hostElement.textContent.

In `@packages/core/src/components/menu/menu.tsx`:
- Around line 639-666: Fix the spelling typo in the comment above
updateMenuItemPositionMetadata: change "sepparated" to "separated" to improve
readability; locate the comment that reads "// item positions and menu size has
to be set manually because slotted items and utility controls are sepparated
into two groups" and update it accordingly (function:
updateMenuItemPositionMetadata, variable: allMenuItems).
- Around line 753-756: The switch branch handling 'End' indexes the last element
with items[items.length - 1]; change it to use the modern .at(-1) accessor for
clarity and safety: update the call to this.updateRovingTabIndex(items,
items.at(-1) ? items.length - 1 : items.length - 1) (or keep the index logic but
use items.at(-1) for retrieving the element to focus) and replace
items[items.length - 1].focus() with const last = items.at(-1); if (last) {
this.updateRovingTabIndex(items, items.length - 1); last.focus(); } to avoid
manual length arithmetic and null access in the case handlers.

---

Outside diff comments:
In `@packages/core/src/components.d.ts`:
- Around line 2595-2690: The generated typings are missing `@since` tags for newly
added public API; add JSDoc `@since` entries to the source declarations for the
methods/props named setTabIndex and menuCategoryLabel so the generator emits the
version metadata. Locate the component/source declaration files that define the
IxMenuCategory/IxMenuItem APIs (the functions/properties setTabIndex and
menuCategoryLabel) and prepend appropriate /** `@since` x.y.z */ JSDoc to each
symbol (method signature for setTabIndex and the menuCategoryLabel prop
declaration) using the correct release version.

In `@packages/core/src/components/menu-category/test/menu-category.ct.ts`:
- Line 9: Add a new regressionTest named "accessibility" to menu-category.ct.ts
that uses makeAxeBuilder to scan the mounted component; specifically, mount the
same markup used by the existing "renders" test (an ix-application with ix-menu
and ix-menu-category containing two ix-menu-item children), await the component
to hydrate via page.locator('ix-menu-category'), run makeAxeBuilder({ page
}).analyze(), and assert that results.violations has length 0 using expect;
ensure you import makeAxeBuilder from '`@utils/test`' and keep the test signature
consistent with the existing regressionTest(async ({ mount, page }) => { ... }).

In `@packages/core/src/components/toast/tests/toast.ct.ts`:
- Around line 19-171: Add a new regressionTest named 'accessibility' that mounts
the component (reuse the same mount pattern used in the 'renders' test), uses
makeAxeBuilder(page) to run an axe scan (include the toast element/selector),
stores the result in accessibilityScanResults, and assert
accessibilityScanResults.violations is an empty array; reference the existing
regressionTest helper and makeAxeBuilder, and place the test alongside the other
tests in this file so the repo's a11y gate is satisfied.
- Around line 71-72: The tests currently use page.waitForTimeout(...) (e.g., the
fixed 500ms sleeps) which makes them timing-sensitive; replace each sleep with
state-based waits that observe the component: use Playwright's auto-retry
assertions like await expect(menuLocator).toBeVisible() / toBeHidden() or await
expect(toastLocator).toHaveAttribute(...) for attribute changes, and use
expect.poll() for the pause/resume API checks instead of fixed timeouts; update
the places calling page.waitForTimeout to wait on the toast/menu locators or the
pause/resume observable state (referencing the existing page.waitForTimeout
calls, the toast/menu locators, and the pause/resume checks) so tests wait on
component state not the clock.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0b8b7e6-499a-484c-8a08-0f77eb6d5033

📥 Commits

Reviewing files that changed from the base of the PR and between 7c34939 and 7cdff0b.

⛔ Files ignored due to path filters (4)
  • packages/react/src/components/components.server.ts is excluded by !packages/react/src/components/**
  • packages/vue/src/components/ix-menu-item.ts is excluded by !packages/vue/src/components/**
  • testing/visual-testing/__screenshots__/tests/menu/menu.e2e.ts/menu-link---should-render-menu-items-with-a-href-1-chromium---classic-dark-linux.png is excluded by !**/*.png
  • testing/visual-testing/__screenshots__/tests/menu/menu.e2e.ts/menu-link---should-render-menu-items-with-a-href-1-chromium---classic-light-linux.png is excluded by !**/*.png
📒 Files selected for processing (10)
  • .changeset/fair-falcons-search.md
  • packages/core/src/components.d.ts
  • packages/core/src/components/menu-category/menu-category.tsx
  • packages/core/src/components/menu-category/test/menu-category.ct.ts
  • packages/core/src/components/menu-item/menu-item.tsx
  • packages/core/src/components/menu/menu.scss
  • packages/core/src/components/menu/menu.tsx
  • packages/core/src/components/toast/tests/toast.ct.ts
  • testing/visual-testing/tests/menu-about-news/menu-about-news.e2e.ts
  • testing/visual-testing/tests/menu/link/index.html

Comment thread .changeset/fair-falcons-search.md Outdated
Comment thread .changeset/fair-falcons-search.md Outdated
Comment thread packages/core/src/components/menu-category/menu-category.tsx
Comment thread packages/core/src/components/menu-item/menu-item.tsx
Comment thread packages/core/src/components/menu-item/menu-item.tsx
Comment thread packages/core/src/components/menu/menu.tsx Outdated
Comment thread packages/core/src/components/menu/menu.tsx
@lzeiml

lzeiml commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

remaining sonar issues are not relevant:

  • menubar does not have to be focusable directly, at least according to W3C
  • group role is needed to satisfy axe a11y check
  • tabIndex on div is needed to implement desired scrolling behaviour when a lot of items are present

Comment thread packages/core/src/components/menu-item/menu-item.tsx Outdated
Comment thread packages/core/src/components/menu/menu.tsx Outdated
Comment thread packages/core/src/components/menu/menu.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/core/src/components/menu/menu.tsx (1)

110-145: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Version the new ix-menu props in the source JSDoc.

i18nAriaLabelMenu and i18nNavigationHint are new public props, but their source JSDoc omits @since, so the generated API docs cannot show when they were introduced. Please add the correct version tags here before regenerating the typings. As per path instructions, packages/core/**: For new component/public API additions (component, prop, method, event), require JSDoc @SInCE tags and flag missing/incorrect versions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/menu/menu.tsx` around lines 110 - 145, The new
public props on the ix-menu component are missing source JSDoc version tags, so
update the JSDoc for i18nAriaLabelMenu and i18nNavigationHint in menu.tsx to
include the correct `@since` annotations before regenerating typings. Use the
existing prop declarations in the Menu component to locate the additions, and
ensure the tags match the version when these props were introduced so the
generated API docs can surface them properly.

Source: Path instructions

packages/core/src/components.d.ts (1)

2618-2618: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add @since metadata to the new ix-menu-item / ix-menu-category APIs.

The generated declarations expose setTabIndex() and menuCategoryLabel without any JSDoc version metadata. Because these are new public APIs, the source component members need the correct @since tags before regenerating this file. As per path instructions, packages/core/**: For new component/public API additions (component, prop, method, event), require JSDoc @SInCE tags and flag missing/incorrect versions.

Also applies to: 2689-2689, 2699-2699

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components.d.ts` at line 2618, The generated declarations
for the new ix-menu-item and ix-menu-category public APIs are missing version
metadata, so update the source component members before regenerating this file.
Add the correct JSDoc `@since` tags to the relevant public API symbols, including
setTabIndex() and menuCategoryLabel, and verify the component/member
declarations in ix-menu-item and ix-menu-category carry the proper version
annotations so the generated typings include them.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/angular/src/components.ts`:
- Around line 1561-1569: The Angular proxy metadata is out of sync with the
Stencil component contract: this wrapper only adds ix-menu inputs, but the new
ix-menu-item.menuCategoryLabel, ix-menu-item.setTabIndex(), and
ix-menu-category.setTabIndex() APIs are missing from the generated proxies.
Regenerate the Angular output target so the components.ts metadata for
ix-menu-item and ix-menu-category includes the new inputs/methods, rather than
editing the wrapper manually.

In `@packages/core/src/components/menu/menu.tsx`:
- Around line 650-655: The bottom-slot mutation path in updateBottomSlotState
only refreshes hasBottomSlotItems, so changes to bottom content can leave the
overflow/shadow state stale. Update this method in menu.tsx to also re-run
handleOverflowIndicator() after bottom-slot children change, so
isMenuItemsOverflow and the shadow flags are recalculated immediately whenever
onSlotchange detects a bottom-slot update.

---

Outside diff comments:
In `@packages/core/src/components.d.ts`:
- Line 2618: The generated declarations for the new ix-menu-item and
ix-menu-category public APIs are missing version metadata, so update the source
component members before regenerating this file. Add the correct JSDoc `@since`
tags to the relevant public API symbols, including setTabIndex() and
menuCategoryLabel, and verify the component/member declarations in ix-menu-item
and ix-menu-category carry the proper version annotations so the generated
typings include them.

In `@packages/core/src/components/menu/menu.tsx`:
- Around line 110-145: The new public props on the ix-menu component are missing
source JSDoc version tags, so update the JSDoc for i18nAriaLabelMenu and
i18nNavigationHint in menu.tsx to include the correct `@since` annotations before
regenerating typings. Use the existing prop declarations in the Menu component
to locate the additions, and ensure the tags match the version when these props
were introduced so the generated API docs can surface them properly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 83a09914-6f53-4428-a76a-86d9ab549234

📥 Commits

Reviewing files that changed from the base of the PR and between ee252ec and 06068ec.

⛔ Files ignored due to path filters (3)
  • packages/angular/standalone/src/components.ts is excluded by !packages/angular/standalone/src/components.ts
  • packages/react/src/components/components.server.ts is excluded by !packages/react/src/components/**
  • packages/vue/src/components/ix-menu.ts is excluded by !packages/vue/src/components/**
📒 Files selected for processing (4)
  • packages/angular/src/components.ts
  • packages/core/src/components.d.ts
  • packages/core/src/components/menu/menu.scss
  • packages/core/src/components/menu/menu.tsx

Comment thread packages/angular/src/components.ts
Comment thread packages/core/src/components/menu/menu.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/core/src/components/menu/menu.tsx (2)

691-699: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear stale ARIA position metadata from categories too.

items can include ix-menu-category, but the cleanup query only removes aria-posinset/aria-setsize from ix-menu-item. Hidden/disabled categories can keep stale set positions after recomputation.

Proposed fix
     const allMenuItems = [
       ...Array.from(
         this.hostElement.querySelectorAll<HTMLIxMenuItemElement>('ix-menu-item')
       ),
+      ...Array.from(
+        this.hostElement.querySelectorAll<HTMLIxMenuCategoryElement>(
+          'ix-menu-category'
+        )
+      ),
       ...Array.from(
         this.hostElement.shadowRoot?.querySelectorAll<HTMLIxMenuItemElement>(
           '.menu-utility-controls > ix-menu-item'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/menu/menu.tsx` around lines 691 - 699, The stale
ARIA cleanup in menu recomputation only targets ix-menu-item elements, so hidden
or disabled ix-menu-category nodes can keep outdated aria-posinset and
aria-setsize values. Update the cleanup logic in the menu component around
allMenuItems to include ix-menu-category alongside ix-menu-item when clearing
position metadata, and ensure the recompute path applies to both item types
consistently.

316-320: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep the active menu item, not the menubar container, as the tab stop.

resetRovingTabIndex() makes every item -1 while .menu-navigation remains tabIndex={0}, so tabbing into the menu focuses the composite container without an active menuitem. Either keep one item tabbable for the roving-tabindex model, or fully switch to container focus with aria-activedescendant.

As per path instructions, prioritize accessibility regressions; the PR objective is to implement the W3C menubar pattern.

Also applies to: 673-684, 916-920

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/menu/menu.tsx` around lines 316 - 320, The
menubar focus model is incorrect because `resetRovingTabIndex()` sets all menu
items to -1 while `.menu-navigation` stays tabbable, so focus lands on the
container instead of an active `menuitem`. Update `menu.tsx` in
`componentDidLoad` and the related roving-tabindex logic used by
`getAllFocusableItems()`/`resetRovingTabIndex()` so one item remains the tab
stop, or else switch the `menu-navigation` pattern fully to container focus with
`aria-activedescendant`. Make the same accessibility-focused fix wherever this
tab index setup is repeated in the menu component.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/components/menu/menu.tsx`:
- Around line 110-120: The new public i18n props on Menu need JSDoc `@since` tags
added to their comments so the generated API docs include the upcoming version.
Update the documentation above the i18nAriaLabelMenu and i18nNavigationHint
`@Prop` declarations in menu.tsx, keeping the tags aligned with these new public
API additions and using the correct upcoming version.

---

Outside diff comments:
In `@packages/core/src/components/menu/menu.tsx`:
- Around line 691-699: The stale ARIA cleanup in menu recomputation only targets
ix-menu-item elements, so hidden or disabled ix-menu-category nodes can keep
outdated aria-posinset and aria-setsize values. Update the cleanup logic in the
menu component around allMenuItems to include ix-menu-category alongside
ix-menu-item when clearing position metadata, and ensure the recompute path
applies to both item types consistently.
- Around line 316-320: The menubar focus model is incorrect because
`resetRovingTabIndex()` sets all menu items to -1 while `.menu-navigation` stays
tabbable, so focus lands on the container instead of an active `menuitem`.
Update `menu.tsx` in `componentDidLoad` and the related roving-tabindex logic
used by `getAllFocusableItems()`/`resetRovingTabIndex()` so one item remains the
tab stop, or else switch the `menu-navigation` pattern fully to container focus
with `aria-activedescendant`. Make the same accessibility-focused fix wherever
this tab index setup is repeated in the menu component.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ec0cb808-f49c-4187-89cc-f9ce74c606f7

📥 Commits

Reviewing files that changed from the base of the PR and between 06068ec and b0def67.

📒 Files selected for processing (1)
  • packages/core/src/components/menu/menu.tsx

Comment thread packages/core/src/components/menu/menu.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/components/menu/menu.tsx (1)

728-749: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude non-rendered default items from roving navigation.

When the menu is collapsed on sm, the default slot is not rendered anymore (Lines 934-936), but getAllFocusableItems() still walks the light DOM and isVisible() treats those entries as navigable. Arrow/Home/End can then move focus to invisible items, and aria-posinset / aria-setsize are computed against the wrong set.

♻️ Suggested change
   private getAllFocusableItems(): (
     | HTMLIxMenuItemElement
     | HTMLIxMenuCategoryElement
   )[] {
     const isNavigable = (el: HTMLElement) =>
       !el.hasAttribute('disabled') &&
       !el.hasAttribute('hidden') &&
       this.isVisible(el);

     const lightItems = Array.from(
       this.hostElement.querySelectorAll<
         HTMLIxMenuItemElement | HTMLIxMenuCategoryElement
       >(':scope > ix-menu-item, :scope > ix-menu-category')
-    ).filter(isNavigable);
+    ).filter((el) => {
+      if (!isNavigable(el)) {
+        return false;
+      }
+
+      if (!this.isHiddenFromViewport()) {
+        return true;
+      }
+
+      const slot = el.getAttribute('slot');
+      return slot === 'home' || slot === 'bottom';
+    });

     const utilityItems = Array.from(
       this.hostElement.shadowRoot?.querySelectorAll<HTMLIxMenuItemElement>(
         '.menu-utility-controls > ix-menu-item'
       ) ?? []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/menu/menu.tsx` around lines 728 - 749, Exclude
non-rendered default slot items from roving navigation in
getAllFocusableItems(); when the menu is collapsed on sm and the default content
is not rendered, light DOM query results should be filtered out so
Arrow/Home/End and aria-posinset/aria-setsize only use actually rendered items.
Update the logic in menu.tsx around getAllFocusableItems() and the related
visibility checks so only rendered, navigable items from the active slot/shadow
DOM are returned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/components.d.ts`:
- Around line 3170-3171: The generated public types still include IxPlayground
even though ix-playground is marked as development-only in stencil.config.ts via
excludeDevelopmentComponents. Update the component type generation so
IxPlayground is omitted from components.d.ts for the public distribution, and
verify the exclusion is applied wherever the public API typings are assembled.

---

Outside diff comments:
In `@packages/core/src/components/menu/menu.tsx`:
- Around line 728-749: Exclude non-rendered default slot items from roving
navigation in getAllFocusableItems(); when the menu is collapsed on sm and the
default content is not rendered, light DOM query results should be filtered out
so Arrow/Home/End and aria-posinset/aria-setsize only use actually rendered
items. Update the logic in menu.tsx around getAllFocusableItems() and the
related visibility checks so only rendered, navigable items from the active
slot/shadow DOM are returned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c37cf98f-ea97-4983-825b-51140feb8ca5

📥 Commits

Reviewing files that changed from the base of the PR and between b0def67 and 58cbcc2.

⛔ Files ignored due to path filters (2)
  • packages/react/src/components/components.server.ts is excluded by !packages/react/src/components/**
  • packages/vue/src/components/ix-menu.ts is excluded by !packages/vue/src/components/**
📒 Files selected for processing (3)
  • packages/core/src/components.d.ts
  • packages/core/src/components/menu/menu-expand-icon.tsx
  • packages/core/src/components/menu/menu.tsx

Comment thread packages/core/src/components.d.ts Outdated
@lzeiml

lzeiml commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

regarding open sonar issues:
aria-description does in fact work, and it is not flagged by storybook.
<dialog> instead of <section> breaks the overlay

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants