Skip to content

docs(menu): add 2nd-gen migration plan - #6606

Open
Rajdeepc wants to merge 16 commits into
mainfrom
rajdeepchandra/docs-menu-migration-plan-swc-1980
Open

docs(menu): add 2nd-gen migration plan#6606
Rajdeepc wants to merge 16 commits into
mainfrom
rajdeepchandra/docs-menu-migration-plan-swc-1980

Conversation

@Rajdeepc

@Rajdeepc Rajdeepc commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Description

Adds the Phase 1 (Preparation) migration plan for swc-menu at CONTRIBUTOR-DOCS/03_project-planning/03_components/menu/migration-plan.md.

This is a documentation-only change; no runtime code is touched.

Scope of this ticket (read this first)

The first pass is a deliberately simple menu:

  • Trigger + PlacementController-anchored surface + shadow-internal role="menu" list (not <swc-popover> — that's a role="dialog" component; menu/listbox/combobox build on the shared PlacementController directly, per the popover a11y doc's own design update)
  • Plain swc-menu-item children only — a label plus one leading visual, icon or image (not both, not combined with description/value/badge/unavailable content)
  • Keyboard navigation between items via roving tabindex (FocusgroupNavigationController, already shipped and used by Tabs): vertical arrow keys, Home/End, wrap-around, disabled items stay focusable
  • Disabled rows and link items (href) are in scope

Explicitly not in this pass (real, documented, just not now):

  • No submenus, no swc-menu-group, no divider/separator
  • No selection (single/multiple mode) at all
  • Richer item content — Unavailable state, highlight badge, external-link indicator — deferred alongside the above

See Implementation phasing for the full three-phase breakdown (A: this pass, B: submenus/groups/dividers, C: selection) and Changes overview for the three short decision tables (Must ship / Additive / Deferred).

Plan summary (for reviewers)

Breaking changes, this pass only (B2, B4–B10):

  • Link items drop the proxy double-change pattern that caused double activation (B2, fixes SWC-923)
  • Keyboard controller swap: RovingTabindexControllerFocusgroupNavigationController (B4, already shipped)
  • --mod-* CSS surface removed (B5)
  • Internal role="menu" moves into shadow DOM, off the custom element host (B6)
  • Disabled-row ARIA standardized — stays focusable/in the roving set, which is the controller's own default (B7)
  • No interactive descendants inside menu item content (B8)
  • ignore property dropped (B9); getNeighboringFocusableElement() dropped as public API (B10)

Additive, this pass: xl size (A1), image as an alternative to icon (A4).

Deferred to later phases (documented, not scheduled): swc-menu-group breaking its 1st-gen inheritance (B1), submenus (B3), selection at both menu and group level (A2), Unavailable state (A3), highlight badge (A5), external-link indicator (A6), printable-character navigation (A7), mobile drilldown — dropped with no replacement (A8). swc-menu-separator is not being built at all; swc-divider is composed directly instead.

Open questions: down to one real blocker — Q4, whether disabled rows should stay focusable (WAI-ARIA APG, and the FocusgroupNavigationController default) or match React Spectrum's disabledKeys (removes them from focus entirely). Nine other questions are resolved and recorded with their reasoning (architecture model, xl size, mobile drilldown scope, trigger API, etc.); three are cross-component follow-ups that don't block this plan.

Where to start reading: Scope of this ticket above → TL;DRBlockers and open questions.

Motivation and context

Required planning output ahead of swc-menu implementation work, per the "Analyze component and create migration plan" ticket for the Menu epic.

Related issue(s)

  • Epic: SWC-1980 (Menu migration epic)
  • Prerequisite ticket: SWC-1981 (Menu accessibility recommendations, already incorporated into this plan)

Screenshots (if appropriate)

N/A — documentation only.

Author's checklist

  • I have read the CONTRIBUTING and PULL_REQUESTS documents.
  • I have reviewed the Accessibility Practices for this feature (this PR documents a11y expectations sourced from the existing Menu accessibility migration analysis and WAI-ARIA APG; it does not implement runtime behavior).
  • I have added automated tests to cover my changes. — N/A, documentation only.
  • I have included a well-written changeset if my change needs to be published. — N/A, no publishable package changes.
  • I have included updated documentation if my change required it.

Reviewer's checklist

  • Includes a Github Issue with appropriate flag or Jira ticket number without a link
  • Includes thoughtfully written changeset if changes suggested include patch, minor, or major features
  • Automated tests cover all use cases and follow best practices for writing
  • Validated on all supported browsers
  • All VRTs are approved before the author can update Golden Hash

Manual review test cases

  • Scope matches what's actually buildable this pass

    1. Read Scope of this ticket above
    2. Open the Changes overview tables
    3. Expect "Must ship" and "Additive" to contain only things a simple label+icon/image menu with roving-tabindex navigation needs — nothing about submenus, groups, dividers, or selection
  • Plan matches source material

    1. Open CONTRIBUTOR-DOCS/03_project-planning/03_components/menu/migration-plan.md
    2. Spot-check the 1st-gen API surface tables against 1st-gen/packages/menu/src/Menu.ts, MenuItem.ts, MenuGroup.ts, MenuDivider.ts
    3. Expect property/method/event/slot names to match source exactly
  • Remaining open question is the right one to block on

    1. Read Blockers and open questions
    2. Expect Q4 (disabled-row focus behavior) to read as genuinely needing a named decision, and the 9 resolved items to read as settled with clear reasoning, not hand-waved

Device review

  • Did it pass in Desktop?
  • Did it pass in (emulated) Mobile?
  • Did it pass in (emulated) iPad?

Accessibility testing checklist

  • Keyboard — N/A, documentation only; no interactive component ships in this PR. The plan documents the intended keyboard model for this pass concretely: FocusgroupNavigationController with direction: 'vertical', wrap: true, skipDisabled: false (the controller's own default), Home/End built in, Escape to close.
  • Screen reader — N/A, documentation only. The plan documents the intended role="menu"/menuitem semantics and shadow-DOM placement for the future implementation to satisfy.

Documents the 1st-gen and React Spectrum S2 Menu API surfaces, dependencies,
breaking changes, and a phased migration checklist ahead of the swc-menu
implementation work under SWC-1980.
@Rajdeepc Rajdeepc added the Component:Documentation Issues or PRs involving changes to docs or docs website. Component prefix is for Jira integration. label Aug 12, 2026
@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: a543004

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.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

@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-6606

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.

@coveralls

coveralls commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 32253792956

Warning

No base build found for commit 4031500 on main.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 96.241%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 39268
Covered Lines: 37993
Line Coverage: 96.75%
Relevant Branches: 6462
Covered Branches: 6018
Branch Coverage: 93.13%
Branches in Coverage %: Yes
Coverage Strength: 460.48 hits per line

💛 - Coveralls

Rajdeep Chandra added 7 commits August 12, 2026 20:04
Ships swc-menu without selection first (Phase A), deferring the
selection engine to a follow-up ticket (Phase B) so reviewers validate
a smaller surface per PR. Holds regardless of how Q11 (picker/combobox
selection dependency) resolves.
swc-menu anchors via PlacementController directly, not <swc-popover> --
Popover already shipped (PR #6356) and is explicitly excluded from
menu/listbox/combobox use per its own a11y doc's design update.
Removes the false Popover sequencing blocker entirely.

Also: adds B7 disabled-focus conflict, B9/B10 dropped API, an Events
(2nd-gen) section, a full 1st-gen member disposition table, corrects
Q1's Figma-file framing, strengthens the Figma evidence behind Q2
(selection scope), and renumbers Q1-Q15 accordingly.
Selection lives entirely on Menu in 1st-gen, not MenuItem, so unlike
B1/B3/B8 there's no cross-plan ownership question. When Q2 resolves,
A2 promotes to Must-ship here directly rather than spinning up a
separate migration-plan.md.
Cross-checked the actual Menu item component variants (node
125485:44694) against the property-table inference already in the
plan -- matches exactly. Adds one detail: the Unavailable contextual-
help bubble is its own sized (S/M/L/XL) component, not fixed-size.
Design confirmed no newer replacement exists for the deprecated-titled
S2/Web file. Unblocks Q3 (xl size confirmation).
…onent

Regroups Q2-Q15 into "Blocking for swc-menu" (its own properties,
architecture, sizing, mobile fate, Jira) and "Cross-component
follow-ups" (menu-item/menu-group/menu-separator/action-menu docs and
existence questions this plan doesn't own). Q1 stays resolved at the
top. Renumbered and cross-checked programmatically.
…city

Resolves 6 questions rather than leaving them open:
- Q3: PlacementController anchoring, confirmed (already the plan's
  own strongest evidence -- Tooltip precedent + popover a11y doc).
- Q5: selection dependency stays Additive/Phase B; no consumer has a
  scheduled migration forcing it earlier.
- Q6: no `trigger` property in Phase A -- no consumer request, matches
  1st-gen, addable later without a breaking change.
- Q11: swc-menu-group breaks from MenuGroup extends Menu -- confirmed,
  free to do now, expensive to do later.
- Q12: no shared base class with swc-action-menu yet -- it doesn't
  exist; premature abstraction risk. Revisit once both exist.
- Q14: swc-menu-separator is not built. swc-divider already does
  everything MenuDivider ever did (wraps divider.css, role=separator).
  This narrows epic scope and conflicts with the existing
  menu-separator a11y doc -- flagged for epic-level confirmation
  rather than buried.

Propagated across every reference site (Dependencies, Changes
overview, 2nd-gen API decisions, Architecture, Migration checklist,
Disposition table) and re-verified programmatically.
| `open` | `boolean` | `false` | `open` | **Inferred**, matching the menu-button pattern's open/close state, parallel to `swc-action-menu`. |
| `disabled` | `boolean` | `false` | `disabled` | **Inferred** carry-forward for the trigger. |
| `trigger` | `'press' \| 'longPress' \| 'contextMenu'` | `'press'` | `trigger` | **Open question** ([Q6](#blocking-for-swc-menu)) — React Spectrum S2's `MenuTrigger.trigger` has no 1st-gen equivalent (1st-gen composes the trigger externally). Depends on whether `swc-menu` owns trigger-invocation semantics itself or defers entirely to how the consumer wires the trigger slot. |
| `shouldCloseOnSelect` | `boolean` | `true` (**inferred**, matches 1st-gen's default close-on-select for non-`selects` items) | `should-close-on-select` or similar | **Open question**, tied to [Q2](#blocking-for-swc-menu)/[Q5](#blocking-for-swc-menu) — only meaningful once selection ships, since 1st-gen's unconditional `close` event today only fires for non-selectable items. React Spectrum S2 exposes this at both `Menu` and `MenuItem` level. |

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.

Do we need to offer this API?

@Rajdeepc Rajdeepc Aug 19, 2026

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.

I confirmed we dont need to offer this API. Leaving it out for Phase A; it can be added additively later without a breaking change if a concrete need shows up.


#### Slots (2nd-gen)

| Slot | Content | Notes |

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.

So not slot for the actual label? Then the "label" description on the properties above is incorrect. How do we put text on the trigger button?

@Rajdeepc Rajdeepc Aug 19, 2026

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.

Yes this was a gap. Added a label slot for visible trigger content (text, icon, or both), matching sp-action-menu's precedent. The label property stays, but now does double duty.

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.

We have dropped the "accessible name fallback" chain in other instances and I vote we do so here as well, it's complication we don't need to own when our new standard is use of accessible-label if it needs an aria-label for some reason (which should be lower probability for this type of menu).

| # | What changes | 1st-gen behavior | 2nd-gen behavior | Consumer migration path |
| --- | --- | --- | --- | --- |
| **B1** | `swc-menu-group` no longer extends the full menu host | `MenuGroup extends Menu` — inherits selection engine, roving tabindex, `value`, etc., and overrides `ownRole`/`controlsRovingTabindex` to suppress most of it | `swc-menu-group` is a plain grouping/labeling primitive (per a11y analysis); no selection or roving-tabindex API of its own | Consumers using `sp-menu-group` only for grouping + `header` slot see no functional change; any (unsupported) reliance on inherited `Menu` API on a group instance breaks. Source: [Menu group a11y doc](../menu-group/accessibility-migration-analysis.md#what-swc-menu-group-is-2nd-gen). |
| **B2** | Link items drop the proxy double-activation pattern | `href` items fire `change` via `Menu`'s click handling in addition to native anchor navigation ([SWC-923](https://jira.corp.adobe.com/browse/SWC-923)) | Real `<a href>` descendant is the only activation path; no proxy `change` dispatch for link rows | Consumers listening for `change` on link-style items must switch to listening on the anchor itself or the native `click`/navigation. |

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.

Link styling may become difficult, if we say tags will just be slotted.

@Rajdeepc Rajdeepc Aug 19, 2026

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.

Doesn't block swc-menu's Phase A, but it needs to be solved before menu-item ships link rows.
Its a real risk and it's not this plan's implementation to own — swc-menu-item's own migration plan owns how link rows render. Filed it as Cross-component follow-ups pointing at the menu-item plan owner: if the link-style row works by having the consumer slot a real <a href> into the item's light DOM (per the menu-item a11y doc), styling that anchor to match the row's hover state, full-row hit target, focus ring, and icon alignment is materially harder than 1st-gen's shadow-internal <a class="anchor"> render.

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.

Just a note: We can probably use the CSS link expansion trick (also used for Card) to increase the slotted links surface to cover the menu item without it actually needing to wrap the other elements, but we may need to test that out.


| Property | Type | Default | Attribute | Notes |
| --- | --- | --- | --- | --- |
| `label` | `string` | `''` | `label` | **Inferred** carry-forward from 1st-gen; accessible name for the trigger. |

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.

Is this the trigger button text?

@Rajdeepc Rajdeepc Aug 19, 2026

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.

It was meant to be the accessible name only, but you're right that leaves no way to render anything visible. Added a label slot for the actual visible content; this property row now documents its fallback-text/accessible-name role instead.


| Slot | Content | Notes |
| --- | --- | --- |
| default | `swc-menu-item`, `swc-menu-group`, `swc-menu-separator` only | **Confirmed** per a11y analysis; verify enforcement in 2nd-gen source once implemented. |

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.

Gen1 ActionMenu had a "label" slot, we could do something like that? Alternatively calling it "trigger" slot.

@Rajdeepc Rajdeepc Aug 19, 2026

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.

Going with your first suggestion — a label slot, matching sp-action-menu's exactly (checked ActionMenu.ts also it has @slot label for visible content plus a label property used as accessible-name fallback). Let's keep it to a single label slot for now rather than also adding sp-action-menu's separate icon/label-only slots. Thanks for catching this.

Rajdeep Chandra added 6 commits August 17, 2026 17:30
React Spectrum's MenuSection supports independent selectionMode per
section (plain + single-select + multi-select coexisting), but the
Figma "Changes in S2 Menu" note already quoted in Q2 recommends
against mixing checkbox sections with other menu types -- these two
sources disagree on whether mixing should even be allowed.

Separately, even if allowed, the Q11 decision to break `MenuGroup
extends Menu` removes the mechanism 1st-gen used to give each group
independent selection scope for free, so Phase B needs to design
per-group selection scope deliberately (e.g. a `selection-mode`
property on swc-menu-group) rather than inherit it.

Folded into Q2, A2, and a new "Mixed menus" note under Behavioral
semantics.
Matches React Spectrum's Menu.selectionMode + MenuSection.selectionMode
split: swc-menu owns a top-level mode for ungrouped items plus the
aggregate value/selectedItems across the whole menu; swc-menu-group
gets its own independent selection-mode property rather than
inheriting one. Together these structurally enable mixed menus.

This means Phase B genuinely spans two plans -- corrected the earlier
claim that selection stays entirely in this plan because it's
"Menu-owned." Propagated to the header note, A2, the disposition
table, and the Phase B checklist.
Splits implementation into three phases instead of two:
- Phase A (this pass): trigger + plain swc-menu-item children only --
  label plus one leading visual (icon or image, not both). No
  submenus, no swc-menu-group, no divider/separator, no selection.
- Phase B (structural completeness, not scheduled): submenus (B3),
  swc-menu-group (B1), swc-divider as separator (Q14), and the richer
  item content (A3 Unavailable, A5 highlight badge, A6 external link).
- Phase C (selection, not scheduled): single selection mode for the
  whole menu, matching React Spectrum's own foundational
  Menu.selectionMode pattern -- confirmed via a docs re-check that
  section-level/mixed selection is documented there as an advanced
  pattern layered on top, not the basic case. Per-group selectionMode
  moves out to Phase C+ / future, not designed into this pass.

Tagged every B/A item and checklist bullet that assumed submenu/group/
divider support was in scope for this pass.
…nController's real API

Reviewed react-spectrum.adobe.com/Menu#slots and #content, plus the
FocusgroupNavigationController's own docs, to make the roving-tabindex
model concrete instead of a citation to "the controller":

- direction: 'vertical' -- matches the controller's own documented
  Vertical menu example directly.
- wrap: true -- decided; the controller defaults to false (toolbar-
  oriented), menus need the override for ArrowDown-wraps-to-first
  convention.
- skipDisabled: false -- confirmed to be the controller's own default,
  not an override swc-menu has to implement. This makes B7 (disabled
  rows stay focusable) the path of least resistance, not just a
  preference -- strengthens Q4 accordingly.
- Home/End -- built in, no config.
- Focus-on-open -- setActiveItem() + queueMicrotask(focus) is the
  controller's own documented pattern, not something to invent.
- A7 (printable character nav) -- confirmed cheap: the controller
  already exposes focusFirstItemByTextPrefix() for this exact case.

Also cross-checked Content/Slots against React Spectrum's own basic
MenuItem example (icon + label + description + keyboard shortcut) --
noted that Phase A's icon-or-image-only scope is narrower than React
Spectrum's basic case by explicit choice, not because it's gated
there as advanced.
- Q2 (selection scope vs a11y doc): decided not blocking -- selection
  stays Additive/Phase C regardless of when that doc gets corrected.
- Q8 (mobile drilldown replacement): decided out of scope -- no
  replacement committed; A8 stands as the record.
- Q7 (xl size Figma confirmation) and Q9 (gen1 Jira issues table):
  dropped entirely, no longer tracked.

Only Q4 (B7 disabled-focus conflict) remains genuinely blocking for
swc-menu. Tightened every "once Q2 resolves" gating phrase into
"whenever Phase C is scheduled," since Q2 no longer needs resolving
to proceed. Re-verified all Q cross-references programmatically.
Collapsed the verbose B/A decision tables into three compact tables:
Must ship (Phase A items only, one line each), Additive (A1/A4, Phase A
only), and a new Deferred to later phases table holding everything
else (B1, B3, A2, A3, A5-A8) without the accumulated inline
justification prose. Also fixed a pre-existing bug where A1 pointed
at the Must-ship anchor instead of Additive.

Renamed section headings (Must ship / Additive / Deferred to later
phases) to short, anchor-friendly text and repointed all ~35 B#/A#
cross-references throughout the document accordingly. Re-verified
with the nav script.
@Rajdeepc Rajdeepc self-assigned this Aug 18, 2026
@Rajdeepc Rajdeepc added Status:Ready for review PR ready for review or re-review. High priority PR review PR is a high priority and should be reviewed ASAP labels Aug 18, 2026
@Rajdeepc
Rajdeepc marked this pull request as ready for review August 18, 2026 05:07
@Rajdeepc
Rajdeepc requested a review from a team as a code owner August 18, 2026 05:07
Ruben caught a real gap: carrying `label` forward as an accessible-name-only
property leaves no way to render visible content on swc-menu's own trigger,
since 1st-gen sp-menu never had a built-in trigger to put content on. Adds a
`label` slot matching sp-action-menu's precedent (Q16), and flags the
separate concern he raised about styling consumer-slotted `<a>` link items
as a menu-item-owned follow-up (Q17).
@Rajdeepc
Rajdeepc requested a review from rubencarvalho August 19, 2026 10:01
@5t3ph 5t3ph self-assigned this Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Component:Documentation Issues or PRs involving changes to docs or docs website. Component prefix is for Jira integration. High priority PR review PR is a high priority and should be reviewed ASAP Status:Ready for review PR ready for review or re-review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants