Skip to content

feat(menu-item): file structure, API, and accessibility - #6874

Open
Rajdeepc wants to merge 13 commits into
rajdeep/menu-item-migrationfrom
rajdeepchandra/feat-menu-item-api
Open

Rajdeepc wants to merge 13 commits into
rajdeep/menu-item-migrationfrom
rajdeepchandra/feat-menu-item-api

Conversation

@Rajdeepc

@Rajdeepc Rajdeepc commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Phases 2–4 for swc-menu-item, one commit each. API and a11y only; no row chrome (next story).

What's here

Commit Files
Phase 2 — structure core/components/menu/MenuItem.types.ts, swc/components/menu/{menu-item.css,swc-menu-item.ts}, barrel exports
Phase 3 — API core/components/menu/MenuItem.base.ts, swc/components/menu/MenuItem.ts, stories
Phase 4 — a11y role + aria-disabled wiring in MenuItem.base.ts, tests

Public surface: size (s/m/l/xl, default m), disabled, and three named slots: label, visual, description. That's it. Enter and Space activate a row by dispatching a click, so keyboard and pointer activation reach the same consumer handler.

What to review

1. DisabledMixin is deliberately not used — MenuItem.base.ts

It produces the right ARIA, but it also writes tabindex, which FocusgroupNavigationController owns for these rows. I mapped disabled → aria-disabled by hand instead (3 lines). This settles Q20 via the fallback agreed in the plan review. Review question: agree the mixin should stay unused here?

2. role is set in connectedCallback, not render — MenuItem.base.ts

swc-menu collects rows on slotchange, which can run before the row first renders, so the role can't wait for one. An author-supplied role wins, so menuitemcheckbox/menuitemradio can arrive later without touching this. Review question: is connectedCallback the right hook?

3. Authoring ergonomics of the label slot — settled in review

Named label slot kept. I briefly moved it to the default slot on a misreading of React Spectrum S2, which in fact takes Text slot="label" alongside Text slot="description"; reverted. With description shipping beside it, an unnamed label is ambiguous and has no handle for grid placement in the row layout. Closes Q23.

4. The activation fix reaches into swc-menu — menu/Menu.base.ts

Raised in review: only Enter was handled, and it merely closed the menu without dispatching a click, so a consumer's @click on a row never ran from the keyboard. MenuItemBase now turns Enter and Space into a this.click(); swc-menu treats Space like Enter and skips aria-disabled rows for keyboard and pointer activation. That last part is a behaviour change to swc-menu: clicking a disabled row used to close the menu. Menu's own 29 tests still pass. Review question: is that swc-menu change in scope here, or should it be split out?

5. Breaking changes, implemented by omission — nothing to read, but worth confirming nothing is missing

No href/target/download/rel/referrerpolicy (link rows → swc-menu-link). No selected/value (menuitem can't carry aria-selected; selection → checkbox/radio rows via aria-checked). No hasSubmenu/open, setRole(), dispatchUpdate(). icon slot renamed to visual; the label stays in the default slot. All 45 --mod-menu-item-* dropped.

6. description joins the accessible name — open question for a11y review

A menuitem names itself from its contents, so a row's description is announced as part of its name, not as a separate description. 1st-gen behaves identically, so this is parity. Logged as Q25 in the plan. Review question: is name-from-contents right here, or is it worth naming the row from its label alone?

Verified

  • Build, eslint, prettier clean; full gen2 suite green (1253 tests), menu-item 8/8, menu 29/29
  • Neither new test is vacuous, confirmed by breaking the code and watching them fail:
    • disabled-row reachability fails when skipDisabled is set to true in Menu.base.ts
    • keyboard activation fails with the consumer click handler runs: expected false to be true when the row's keydown listener is dropped, and with the menu stays open: expected false to be true when the disabled guard is removed

Not verified — needs a human

The ticket's screen reader matrix (JAWS / NVDA / VoiceOver) and WCAG 2.2 AA sign-off. I can't run those, and the automated tests above don't substitute for them. This phase isn't complete until someone does that pass.

On the ticket's paths

The ticket says 2nd-gen/packages/swc/components/menu/menu-item/ with menu-item.ts / menu-item.base.ts. It predates the gen2 rename, and no gen2 component nests a sub-element in its own subdirectory. It was right that rows belong under menu/ though, which the first three commits got wrong and the last one fixes: rows now sit beside Menu.ts, the way AccordionItem.ts sits beside Accordion.ts and Tab.ts beside Tabs.ts. There is no separate Menu item Storybook entry any more; rows are documented in menu.mdx and storied, tested and VRT'd in the menu's own files. The base classes stay in core, not swc. The a11y doc is at 03_components/menu-item/, not under menu/.

Author's checklist

  • I have read the CONTRIBUTING and PULL_REQUESTS documents.
  • I have reviewed at the Accessibility Practices for this feature, see: Aria Practices
  • I have added automated tests to cover my changes.
  • I have included a well-written changeset if my change needs to be published.
  • I have included updated documentation if my change required it.

Accessibility testing checklist

  • Keyboard — open Components → Menu item → Docs → States. The story renders the menu already open, so there is no trigger to click, and note that Menu → States is a different story, about the menu's own open state. Arrow through the rows: focus should start on the first row and reach the disabled row rather than skip it. Chrome paints no ring on the first row after a mouse open; that is :focus-visible, not misplaced focus, and the row chrome story fixes it.
  • Screen reader — JAWS, NVDA, VoiceOver on the same story. Expect each row announced as a menu item with its label, and the disabled row announced as unavailable. Outstanding.

Rajdeep Chandra and others added 3 commits October 28, 2026 14:21
Phase 2. Core holds the types and the base class; swc holds the concrete
element, its registration, and a layout-only stylesheet. Row chrome is
the next story.

Paths follow the repo's own convention rather than the ticket's, which
still describes 2nd-gen/ and a menu/menu-item/ nesting that predate the
gen2 rename and the flat component layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 3. The surface is the migration plan's Phase A table: `size`
(s/m/l/xl, default m) and `disabled`, with a named `label` slot and an
optional leading `visual` slot. JSDoc on everything public.

Breaking changes from the plan, all implemented by omission:

- no `href`/`target`/`download`/`rel`/`referrerpolicy`; link rows are
  swc-menu-link's job, so this element never renders an anchor
- no `selected`/`value`; the menuitem role cannot carry aria-selected and
  selection belongs to the checkbox and radio rows via aria-checked
- no `hasSubmenu`/`open`, no `setRole()`, no `dispatchUpdate()`
- the default slot becomes a named `label` slot, and `icon` becomes
  `visual`
- all 45 --mod-menu-item-* custom properties dropped

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 4. role="menuitem" is set in connectedCallback, not at render: the
owning menu collects rows on slotchange, which can run before this
element first renders. An author-supplied role wins, so menuitemcheckbox
and menuitemradio rows can arrive later without a change here.

`disabled` maps to aria-disabled and never the native attribute, so the
row keeps its place in the menu's roving set and stays arrow-reachable
(B7). Mapped by hand rather than through DisabledMixin, settling Q20: the
mixin also writes tabindex, which FocusgroupNavigationController owns.

Arrow-key movement, tab stops and focus restoration stay with swc-menu;
the row only has to be discoverable and keep its role.

The reachability test dispatches its key from the row rather than through
userEvent.keyboard. That helper tracks its own notion of focus and stops
reaching the menu once the row has a shadow root, which made an earlier
version of this test fail for reasons that had nothing to do with the
component. Verified the assertion still fails when skipDisabled is
flipped to true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rajdeepc
Rajdeepc requested a review from a team as a code owner October 28, 2026 08:54
@changeset-bot

changeset-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: b199d3f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

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

@Rajdeepc Rajdeepc added a11y Issues or PRs related to accessibility ready-for-review labels Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📚 Branch Preview Links

🔍 Gen1 Visual Regression Test Results

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

Deployed to Azure Blob Storage: pr-6874

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

@rubencarvalho rubencarvalho added Component:Menu Status:Ready for review PR ready for review or re-review. and removed a11y Issues or PRs related to accessibility ready-for-review labels Oct 28, 2026
@Rajdeepc Rajdeepc self-assigned this Oct 28, 2026
@aramos-adobe

Copy link
Copy Markdown
Contributor

[nit] The PR description suggests we should look at Components → Menu item → States but that exists in Menu 😁

@aramos-adobe

aramos-adobe commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor
Screen.Recording.2026-10-28.at.11.47.29.AM.mov

Clicking the trigger then pressing the down arrow key moves to the second menu item not the first. But if you tab to the button trigger, press Enter | Space then press Down arrow key it moves to the first menu item

Edit:
It shows the first item as focused in Safari at least

Screen.Recording.2026-10-28.at.12.41.05.PM.mov

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

I am ok with skipping DisabledMixin here, and connectedCallback for the role makes sense to me. left two questions, nothing huge

@rubencarvalho

rubencarvalho commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

also, regarding file structure, this is showing up as a top level component in the Storybook, wouldn't we want it inside the menu?
Screenshot 2026-10-29 at 12 00 36

I guess this is a bigger question - but it feels like we've been taking that approach for all other components

Rajdeep Chandra and others added 3 commits October 29, 2026 18:56
Review feedback. The accessibility analysis requires Enter and Space to
activate a row (WCAG 2.1.1), but only Enter was handled, and it merely
closed the menu: no click was ever dispatched, so a consumer's handler on
the row never ran from the keyboard at all. Enter on a disabled row closed
the menu too.

`MenuItemBase` now turns both keys into a click, which is the same path
pointer activation already takes, and swallows them on a disabled row.
`swc-menu` treats Space like Enter and skips aria-disabled rows for both
keyboard and pointer activation.

Also drops the redundant `defaultSize: 'm'` (SizedMixin's own default) and
uses the `ariaDisabled` reflection property in place of the hand-written
attribute calls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review feedback. The label is the row's main content, so it belongs in the
default slot; named slots are for what sits alongside it. This matches
swc-badge and swc-status-light, 1st-gen sp-menu-item, and React Spectrum
S2, whose MenuItem takes children for the label and names only description.

Rows are now `<swc-menu-item>Cut</swc-menu-item>`, which also closes Q23:
the named slot was the heavier form with nothing to show for it.

Adds a test for the keyboard open path after a review recording suggested
the first arrow press skips a row. Focus lands on the first row either way;
what differs is whether the browser paints a ring for programmatic focus.
Recorded in the plan so the row chrome story keys that treatment off
`:host(:focus)` rather than `:focus-visible`, as 1st-gen does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review feedback. swc-menu-item was showing up as a top-level Storybook
entry beside Menu, which is not how gen2 treats a sub-element that only
exists inside its parent: swc-accordion-item and swc-tab live beside their
parent and share one Storybook entry and one docs page. Components that get
their own folder, close-button and infield-button among them, are the ones
reusable across parents. A menu row is not.

Moves MenuItem.base.ts and MenuItem.types.ts into core/components/menu, and
MenuItem.ts, menu-item.css and swc-menu-item.ts into swc/components/menu,
re-exported from the menu barrels. Drops the separate core subpath export.
Stories, tests and docs fold into the menu's own, so rows are documented in
menu.mdx under an Item states section.

This also lets swc-menu drop its placeholder rows for the real element
across stories, tests and VRT. Two menu tests had to stop driving keys
through userEvent.keyboard, which tracks its own notion of focus and stops
reaching the menu once a row has a shadow root. The Tab trap step now
asserts defaultPrevented rather than only that focus did not move, which it
would have reported either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rajdeepc

Copy link
Copy Markdown
Contributor Author

also, regarding file structure, this is showing up as a top level component in the Storybook, wouldn't we want it inside the menu? Screenshot 2026-10-29 at 12 00 36

I guess this is a bigger question - but it feels like we've been taking that approach for all other components

gen2 has two conventions, and I think I matched the wrong one. close-button, infield-button and color-handle get their own folder because they are reused across parents. AccordionItem and Tab sit beside their parent with one Storybook entry because they only exist inside it. A menu row is the second kind.

So rows now ship inside components/menu/: MenuItem.base.ts and MenuItem.types.ts in core, MenuItem.ts, menu-item.css and swc-menu-item.ts in swc, all re-exported from the menu barrels. The separate ./components/menu-item subpath export is gone, and there is no Menu item sidebar entry; rows are documented in menu.mdx under Item states.

@aramos-adobe aramos-adobe 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.

Nice work on this 🚀

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

Requesting changes so it also dismisses my approval.

After the change of putting the menu item inside menu, we're not seeing the API of the menu item in the docs...

Rajdeep Chandra and others added 2 commits October 30, 2026 16:16
Review feedback, three threads.

Folding rows into the menu folder dropped swc-menu-item from the docs
page: DocsFooter renders the meta's own `component` unless the meta lists
its sibling elements. Declares `additionalApiTables: ['swc-menu-item']`,
the same way accordion and tabs surface theirs.

Adds the `description` slot, pulled into this phase rather than deferred.
It is how similarly-named options are told apart, which is an
accessibility concern in its own right, and 1st-gen `sp-menu-item` and
React Spectrum S2 both carry it. The label and its description stack in a
wrapper; typography and colour belong to the row chrome story.

Note that a `menuitem` names itself from its contents, so the description
joins the row's accessible name rather than being announced separately,
which is also how 1st-gen behaves. Logged as Q25 for the a11y reviewer.

Also documents `visual` as taking an icon **or a thumbnail**, which was
the intent behind renaming it from `icon` but was not written down
anywhere a reader would find it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverses the slot half of 1c6a714. I moved the label to the default
slot on the basis that React Spectrum S2 names only `description`. That
was wrong: RSP takes `Text slot="label"` alongside `Text slot="description"`,
which this plan's own API summary already recorded and I misread.

With `description` shipping beside it, an unnamed label is both ambiguous
and harder to place, since each part needs a handle to assign to a grid
area in the row layout. Rows are `<swc-menu-item><span slot="label">Cut
</span></swc-menu-item>` again, and Q23 closes the other way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rajdeepc
Rajdeepc requested review from 5t3ph, nikkimk and rubencarvalho and removed request for 5t3ph October 30, 2026 10:59
Rajdeep Chandra and others added 2 commits November 2, 2026 22:29
…cription

A `menuitem` takes its accessible name from its contents, so the description
was being folded into the row's name instead of announced as a description.
Point `ariaLabelledByElements` at the `label` slot and
`ariaDescribedByElements` at the `description` slot instead, matching React
Spectrum. Element references rather than ID refs, so consumer markup needs no
`id` attributes.

Closes Q25 in the menu-item migration plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `if (!this.hasAttribute('role'))` guard let an author swap in
`menuitemcheckbox` or `menuitemradio`, promising visuals and behavior the row
does not have. Selection rows ship as their own components, so set the role
unconditionally, as B24 in the migration plan already specified.

Also records the dropped `sp-menu-item.setRole()` in the consumer migration
guide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rajdeepc
Rajdeepc requested a review from 5t3ph November 2, 2026 17:08
<div class="swc-MenuItem-text">
<slot name="label"></slot>
<slot name="description"></slot>
<slot name="label" @slotchange=${this.handleTextSlotChange}></slot>

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.

Also trigger handleTextSlotChange on first render in case slots are empty at first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6b7e04dc5d, and it turned out to matter for more than safety: an entirely empty row fires no slotchange at all, so first render is the only thing that reaches the new missing-label warning. That is the case the test pins.

];
this.ariaLabelledByElements = label.length > 0 ? label : null;
this.ariaDescribedByElements = description.length > 0 ? description : null;
}

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.

Could we write these to internals instead of this? Setting this.ariaLabelledByElements writes to the host's aria-labelledby, so it'll overwrite (or with null, remove) anything a consumer sets there. With internals they're just defaults and consumer attributes still win, same as _syncLabelling in the labelling mixin. Also swapped to assignedElements({ flatten: true }) so this still works if the slots get forwarded through another component.

Suggested change
}
@query('slot[name="label"]')
private labelSlot?: HTMLSlotElement;
@query('slot[name="description"]')
private descriptionSlot?: HTMLSlotElement;
protected handleTextSlotChange(): void {
// `menuitem` names from contents, so without explicit references the
// description is folded into the name instead of announced as a description.
const label = this.labelSlot?.assignedElements({ flatten: true }) ?? [];
const description =
this.descriptionSlot?.assignedElements({ flatten: true }) ?? [];
this.internals.ariaLabelledByElements = label.length > 0 ? label : null;
this.internals.ariaDescribedByElements =
description.length > 0 ? description : null;
}

This means a switch for the host to have internals from attachInternals() (where role = 'menuitem' is set).

If you don't want to switch to internals, then this would be the alternative to not override consumer-set attributes, but @nikkimk should weigh-in that concern:

  // Skip when a consumer set IDREFs on the host. Element reflection leaves
  // the attribute as "", so a non-empty value means it isn't ours.
  if (!this.getAttribute('aria-labelledby')) {
    this.ariaLabelledByElements = label.length > 0 ? label : null;
  }
  if (!this.getAttribute('aria-describedby')) {
    this.ariaDescribedByElements = description.length > 0 ? description : null;
  }

One edge case: if there's a description but no label, the description still gets folded into the name. Might be worth a debug warning, or falling back to internals.ariaLabel.

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.

I agree with @5t3ph's code suggestion here. I think if there is a description but no label, that would be a dev warning as we should always have a label.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All three taken in 6b7e04dc5d: references moved onto internals (the host keeps no aria-labelledby/aria-describedby, and a consumer value now survives, which a test pins), slots read via assignedElements({ flatten: true }), and a missing-label warning via the shared validateRequiredSlot rather than an ariaLabel fallback, per @nikkimk.

One deliberate omission: I kept role as setAttribute rather than moving it to internals.role, because on internals an author attribute would win and that reopens the role="menuitemcheckbox" override you asked me to remove in the comment above. Happy to move it if you would rather have the platform layering there too.

Rajdeep Chandra and others added 2 commits November 3, 2026 11:41
Writing `ariaLabelledByElements` to the host reflects onto `aria-labelledby`,
so the references the component supplies clobbered anything a consumer had set
there. Move them onto `ElementInternals`, where they act as defaults a
consumer's own attributes still override.

Also resolve the slots with `assignedElements({ flatten: true })` so a row
whose slots are forwarded through another component still names itself, run
the handler on first render since a slot that starts empty never fires
`slotchange`, and warn when a row has no `label`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rajdeepc
Rajdeepc requested a review from 5t3ph November 3, 2026 06:59
@Rajdeepc

Rajdeepc commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @5t3ph and @nikkimk for the review! It caught three issues I would otherwise have shipped, all fixed in 6b7e04dc5d.

  • aria-labelledby clobbering: A genuine bug where writing to this overwrote consumer-provided values. internals correctly keeps these as overridable defaults.
  • Flattened slots: assignedElements({ flatten: true }) handles forwarded slots through wrapper components, preventing rows from silently losing their names.
  • First-render validation: Empty rows never fire slotchange, so the initial validation is necessary to catch missing labels. I also corrected the test to cover this case and verified it fails when the fix is removed.

I went with @nikkimk's shared validateRequiredSlot approach rather than an ariaLabel fallback, since silently falling back would hide authoring mistakes.

One open question: I kept role on setAttribute rather than internals.role, since an author attribute takes precedence on internals and reintroduces the menuitemcheckbox override. Happy to switch if we prefer platform layering here.

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

Left some notes inline. Couple of things: the @example in Menu.ts still uses default-slot rows, menu.mdx line 34 still says swc-menu-item doesn't exist yet (and line 75 doesn't mention Space).

Might also be worth a check in -Firefox- (actually on all browsers), I'm getting a weird behaviour for Space: focus is already back on the trigger before keyup, and then the browser has clicked the newly focused button on keyup before, which would reopen the menu.

// Enter would otherwise re-fire on the trigger the menu restores focus
// to, and Space would scroll the page under the open surface.
event.preventDefault();
if (!this.disabled) {

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.

this covers Enter/Space, but a mouse click on a disabled row still goes through. on the preview storybook (Item states), clicking the disabled row still runs a click listener on it, the menu just stays open. 1st-gen blocks this with a capture click listener (handleClickCapture in 1st-gen MenuItem.ts), could we do the same here? and maybe a test that actually clicks the disabled row, since the current one only presses Enter

}
if (event.key === 'Enter' && this.isMenuItemEventTarget(event)) {
// Else the deferred default-action re-activates the refocused trigger.
if (event.key === 'Enter' || event.key === ' ') {

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.

this runs on document capture, so with a real Enter/Space the menu closes first (swc-close fires, focus goes back to the trigger) and only then does the row's keydown call click(). with the mouse it's row click first, then close. what if this branch only did preventDefault and the row's click closed the menu through handleItemActivate, so both paths match? (I think the tests can't see this, since events dispatched from script don't let the update run between listeners)

// Enter and Space activate a row. Turning them into a click is what makes
// keyboard and pointer activation reach the same consumer handler.
private readonly handleKeyDown = (event: KeyboardEvent): void => {
if (event.key !== 'Enter' && event.key !== ' ') {

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.

maybe ignore event.repeat here (after preventDefault)? if you hold Enter on a row it runs, focus goes back to the trigger, the next repeat reopens the menu, and the one after that runs row 1 again

].map(
(label) => html`
<swc-menu-item role="menuitem">${label}</swc-menu-item>
<swc-menu-item>${label}</swc-menu-item>

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.

this one still puts the text in the default slot, so these 15 rows render empty now and the long list overflow snapshot won't really show a long list. <span slot="label">${label}</span>?

| `ignore` property | No replacement. |
| `getNeighboringFocusableElement()` method | No public replacement; arrow-key navigation is handled internally. |
| `role="menu"` on the host element | Moved to the shadow-internal surface. Do not select `sp-menu[role="menu"]`-style host selectors. |
| `setRole()` on menu items | No replacement. A row is always `role="menuitem"`; checkbox and radio rows arrive as dedicated components. |

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.

swc-menu-item ships now, so a few spots in this guide are out of date: line 7 says sp-menu-item hasn't migrated, line 26 only imports swc-menu.js (the accordion and tabs guides list the child import too), and the After examples on 79 and 98 use <swc-menu-item>Deselect</swc-menu-item>, which renders an empty row now. could we fix those in this PR and leave the full row section for the docs phase?

* this class supplies the styles and the render template.
*
* @element swc-menu-item
* @since 2.0.0-beta.5

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.

swc is already on 2.0.0-beta.6, so this should probably be the next version

- [x] A disabled row remains reachable by arrow keys inside `swc-menu` ([B20](#must-ship)); this is the test that settles [Q20](#blockers-and-open-questions)
- [x] `label`, `visual` and `description` slots render in the expected order
- [x] `swc-menu` discovers rows via `:scope > swc-menu-item` with real elements, not placeholders
- [x] <kbd>Enter</kbd> and <kbd>Space</kbd> reach a consumer `click` handler on an enabled row, and neither activates a disabled one

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.

this is ticked, but there's no test for Space on a disabled row, or for Enter reaching an enabled row's click handler (TabTrapAndEnterClosesTest only checks that the menu closes)

| **Q24** | Should a row be able to mark itself as the current one? `menuitem` cannot carry `aria-selected`, but `aria-current` is available and would cover "this is where you are" without implying selection. Needs a product answer on whether menus need it at all before any `current` property is designed. | No | Open |
| **Q23** | Authoring ergonomics of the named `label` slot: `<swc-menu-item><span slot="label">Cut</span></swc-menu-item>` is heavier than the 1st-gen default-slot form, and every existing `swc-menu` story and test uses the lighter one. Confirm the trade is still wanted now that the cost is visible. | No | Open, decided in principle under [B22](#must-ship) |
| **Q23** | Authoring ergonomics of the named `label` slot. | No | **Closed.** Named slot kept: with `description` alongside it, an unnamed label is both ambiguous and harder to place in a grid. See [B22](#must-ship) |
| **Q25** | A `menuitem` takes its accessible name from its contents, so a `description` is announced as part of the row's name rather than as a separate description. 1st-gen behaves the same way. Wiring it as a real description instead would mean naming the row from its label alone, which the default slot makes awkward since the label can be a bare text node. Worth doing, or is name-from-contents the right answer here? | No | **Closed.** Reviewer confirmed React Spectrum references the label as `aria-labelledby` and the description as `aria-describedby`, so the row is named from its label alone. `MenuItemBase.handleTextSlotChange()` sets `ariaLabelledByElements` and `ariaDescribedByElements` from the slotted elements; the named `label` slot ([Q23](#open-questions)) is what makes this possible, since element references need an element |

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.

tiny one: #open-questions doesn't exist here, the heading anchor is #blockers-and-open-questions

tags: ['states'],
};

export const ItemContent: Story = {

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.

nit: this is tagged options but sits under the STATES banner, could move it up next to Sizes

* pattern: `role="menuitem"` on the host, with the owning `swc-menu` driving
* arrow-key navigation and roving tabindex across its rows.
*
* @slot label - Text of the row. Required; referenced as the row's accessible

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.

nit: the jsdoc guide asks for each @slot on one line (07_jsdoc-standards.md). the CEM output looks fine today, same thing in MenuItem.ts

This branch has not been deployed

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

Labels

Component:Menu Status:Ready for review PR ready for review or re-review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants