Skip to content

feat(actiongroup): migrate action group - #6444

Open
rise-erpelding wants to merge 8 commits into
mainfrom
swc-2212/migrate-action-group
Open

feat(actiongroup): migrate action group#6444
rise-erpelding wants to merge 8 commits into
mainfrom
swc-2212/migrate-action-group

Conversation

@rise-erpelding

@rise-erpelding rise-erpelding commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Description

Migrates the action group component to gen 2.

Motivation and context

Part of the broader 2nd-gen migration (epic SWC-2212). swc-action-group groups related swc-action-button/swc-action-menu controls with roving-tabindex keyboard navigation, replacing sp-action-group. The selects/selected exclusive-choice behavior from 1st-gen is intentionally dropped here; that will live in the upcoming swc-segmented-control instead.

Related issue(s)

  • Addresses epic SWC-2212

Child issues (check off as they're merged)

Related bugs resolved by Gen2 (check off as addressed)

Follow up tickets created related to this work

  • SWC-2464 and subtask SWC-2465 Add truncate property to action button and propagate truncate to action button children in a justified action group
  • SWC-2509 Document action group collapse pattern for S2

Screenshots

image

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.

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

  • Roving tabindex and arrow-key navigation

    1. Go to the Action GroupAnatomy story
    2. Tab into the group, then use Arrow Left/Arrow Right (or Arrow Up/Arrow Down on the Orientation vertical example) to move between children, including wrapping from the last child back to the first
    3. Expect only one child to ever be a Tab stop; a second Tab should leave the group entirely rather than moving to the next child
  • Group vs. child disabled state

    1. Go to the Action GroupDisabled story
    2. Compare setting disabled on the group (all children get aria-disabled but stay in the arrow-key sequence) against disabling one swc-action-button child directly (that child drops out of focus/arrow-key navigation, the rest of the group is unaffected)
    3. Expect the two cases to behave differently as described above, and no child to be clickable/activatable while disabled
  • Toolbar composition and icon-only labeling

    1. Go to the Action GroupToolbar Composition story and the Anatomy story's icon-only child
    2. Inspect the accessibility tree (e.g. Chrome DevTools Accessibility pane)
    3. Expect a toolbar landmark wrapping named group-role clusters, and the icon-only button's accessible-label exposed as its accessible name

Device review

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

Accessibility testing checklist

Required: Complete each applicable item and document your testing steps (replace the placeholders with your component-specific instructions).

  • Keyboard (required — document steps below) — What to test for: Focus order is logical; Tab reaches the component and all interactive descendants; Enter/Space activate where appropriate; arrow keys work for tabs, menus, sliders, etc.; no focus traps; Escape dismisses when applicable; focus indicator is visible.

    1. On the Anatomy story, Tab in (focus should land on the first enabled child, or the last-focused child on a repeat visit)
    2. Use Arrow Left/Right (horizontal) or Arrow Up/Down (vertical, via Orientation) to move among children, and Home/End to jump to the first/last child
    3. Press Enter and Space on a focused child to confirm activation, then Tab again to confirm focus leaves the group as a single stop, with a visible focus indicator throughout
  • Screen reader (required — document steps below) — What to test for: Role and name are announced correctly; state changes (e.g. expanded, selected) are announced; labels and relationships are clear; no unnecessary or duplicate announcements.

    1. With VoiceOver/NVDA running, navigate to the Anatomy and Toolbar Composition stories
    2. Confirm the group is announced with role "group" and its label (when set) as its name, the icon-only child announces its accessible-label, and the toolbar wrapper is announced as a toolbar landmark containing named groups
    3. Set disabled on the group and confirm each child announces a disabled state while remaining discoverable during navigation (not removed from the accessibility tree)

@rise-erpelding rise-erpelding self-assigned this Jun 23, 2026
@changeset-bot

changeset-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4782780

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

This PR includes changesets to release 85 packages
Name Type
@adobe/spectrum-wc Minor
@adobe/spectrum-wc-core Minor
@spectrum-web-components/action-group Patch
@spectrum-web-components/action-bar Patch
@spectrum-web-components/vrt-compare Patch
@spectrum-web-components/bundle Patch
documentation Patch
@spectrum-web-components/accordion Patch
@spectrum-web-components/action-button Patch
@spectrum-web-components/action-menu Patch
@spectrum-web-components/alert-banner Patch
@spectrum-web-components/alert-dialog Patch
@spectrum-web-components/asset Patch
@spectrum-web-components/avatar Patch
@spectrum-web-components/badge Patch
@spectrum-web-components/breadcrumbs Patch
@spectrum-web-components/button-group Patch
@spectrum-web-components/button Patch
@spectrum-web-components/card Patch
@spectrum-web-components/checkbox Patch
@spectrum-web-components/clear-button Patch
@spectrum-web-components/close-button Patch
@spectrum-web-components/coachmark Patch
@spectrum-web-components/color-area Patch
@spectrum-web-components/color-field Patch
@spectrum-web-components/color-handle Patch
@spectrum-web-components/color-loupe Patch
@spectrum-web-components/color-slider Patch
@spectrum-web-components/color-wheel Patch
@spectrum-web-components/combobox Patch
@spectrum-web-components/contextual-help Patch
@spectrum-web-components/dialog Patch
@spectrum-web-components/divider Patch
@spectrum-web-components/dropzone Patch
@spectrum-web-components/field-group Patch
@spectrum-web-components/field-label Patch
@spectrum-web-components/help-text Patch
@spectrum-web-components/icon Patch
@spectrum-web-components/icons-ui Patch
@spectrum-web-components/icons-workflow Patch
@spectrum-web-components/icons Patch
@spectrum-web-components/iconset Patch
@spectrum-web-components/illustrated-message Patch
@spectrum-web-components/infield-button Patch
@spectrum-web-components/link Patch
@spectrum-web-components/menu Patch
@spectrum-web-components/meter Patch
@spectrum-web-components/modal Patch
@spectrum-web-components/number-field Patch
@spectrum-web-components/overlay Patch
@spectrum-web-components/picker-button Patch
@spectrum-web-components/picker Patch
@spectrum-web-components/popover Patch
@spectrum-web-components/progress-bar Patch
@spectrum-web-components/progress-circle Patch
@spectrum-web-components/radio Patch
@spectrum-web-components/search Patch
@spectrum-web-components/sidenav Patch
@spectrum-web-components/slider Patch
@spectrum-web-components/split-view Patch
@spectrum-web-components/status-light Patch
@spectrum-web-components/swatch Patch
@spectrum-web-components/switch Patch
@spectrum-web-components/table Patch
@spectrum-web-components/tabs Patch
@spectrum-web-components/tags Patch
@spectrum-web-components/textfield Patch
@spectrum-web-components/thumbnail Patch
@spectrum-web-components/toast Patch
@spectrum-web-components/tooltip Patch
@spectrum-web-components/top-nav Patch
@spectrum-web-components/tray Patch
@spectrum-web-components/underlay Patch
@spectrum-web-components/custom-vars-viewer Patch
@spectrum-web-components/story-decorator Patch
@spectrum-web-components/base Patch
@spectrum-web-components/grid Patch
@spectrum-web-components/opacity-checkerboard Patch
@spectrum-web-components/reactive-controllers Patch
@spectrum-web-components/shared Patch
@spectrum-web-components/styles Patch
@spectrum-web-components/theme Patch
@spectrum-web-components/truncated Patch
@spectrum-web-components/eslint-plugin Patch
@spectrum-web-components/stylelint-header-plugin 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

@rise-erpelding rise-erpelding added Status:WIP PR is a work in progress or draft Component:Action group gen2 These issues or PRs map to our 2nd generation work to modernizing infrastructure. labels Jun 23, 2026
@coveralls

coveralls commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 32280033838

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

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.248%

Details

  • Patch coverage: 86 of 86 lines across 1 file are fully covered (100%).

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: 39351
Covered Lines: 38076
Line Coverage: 96.76%
Relevant Branches: 6470
Covered Branches: 6026
Branch Coverage: 93.14%
Branches in Coverage %: Yes
Coverage Strength: 459.37 hits per line

💛 - Coveralls

@rise-erpelding
rise-erpelding force-pushed the swc-2212/migrate-action-group branch from f169a5d to da08b85 Compare June 23, 2026 21:56
@github-actions

github-actions Bot commented Jun 23, 2026

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-6444

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.

@rise-erpelding
rise-erpelding force-pushed the swc-2212/migrate-action-group branch 2 times, most recently from 49fb3e7 to 827a2a4 Compare June 29, 2026 23:26
@rise-erpelding
rise-erpelding force-pushed the swc-2212/migrate-action-group branch from 827a2a4 to a969bf6 Compare July 6, 2026 12:14
@rise-erpelding
rise-erpelding force-pushed the swc-2212/migrate-action-group branch from 91f0ed3 to e109f49 Compare July 14, 2026 12:31
@rise-erpelding
rise-erpelding force-pushed the swc-2212/migrate-action-group branch 2 times, most recently from 2022776 to 8abe6f3 Compare July 29, 2026 18:35
@rise-erpelding
rise-erpelding marked this pull request as ready for review July 29, 2026 19:53
@rise-erpelding
rise-erpelding requested a review from a team as a code owner July 29, 2026 19:53
@rise-erpelding
rise-erpelding marked this pull request as draft July 29, 2026 19:53
@rise-erpelding
rise-erpelding force-pushed the swc-2212/migrate-action-group branch 5 times, most recently from fca3b28 to a443606 Compare August 10, 2026 14:08
@rise-erpelding
rise-erpelding force-pushed the swc-2212/migrate-action-group branch from a443606 to 3bee943 Compare August 11, 2026 23:17
@rise-erpelding rise-erpelding added the run_vrt Triggers the Chromatic VRT run for 2nd-gen label Aug 12, 2026
@rise-erpelding
rise-erpelding force-pushed the swc-2212/migrate-action-group branch from 99d0559 to 23e2fca Compare August 13, 2026 13:12
rise-erpelding and others added 3 commits August 19, 2026 07:15
Co-authored-by: Rise Erpelding <for27065@adobe.com>
Co-authored-by: Stephanie Eckles <seckles@adobe.com>
@rise-erpelding
rise-erpelding force-pushed the swc-2212/migrate-action-group branch from f8f749c to e18e15b Compare August 19, 2026 14:16
@rise-erpelding rise-erpelding added Status:Ready for review PR ready for review or re-review. and removed Status:WIP PR is a work in progress or draft labels Aug 19, 2026
@rise-erpelding
rise-erpelding marked this pull request as ready for review August 19, 2026 16:34
text-decoration: none;
background-color: var(--swc-action-button-background-color-default, token("gray-100"));
border: var(--_swc-action-button-border-width) solid var(--swc-action-button-border-color-default, transparent);
border-radius: var(--swc-action-button-border-radius, token("corner-radius-medium-size-medium"));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Adjusting border radii in action button in order to restyle action buttons in compact action group

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] could hold in a private var vs repeating:

---_swc-action-button-border-radius: var(--swc-action-button-border-radius, token("corner-radius-medium-size-medium"));

@5t3ph 5t3ph 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.

A few final things for polish, and one found bug with the global styles conversion that I've filed separately but you might want to exclude for now.

* An action group clusters related actions together with consistent
* spacing, sizing, and orientation.
*
* Unlike [button group](../?path=/docs/components-button-group--docs), where

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.

Should we add a similar cross-reference to button group?


<Canvas of={ActionGroupStories.Orientations} />

### Quiet

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 could probably reasonably remove the compact example since that's mostly useful for VRTs so is covered separately.

text-decoration: none;
background-color: var(--swc-action-button-background-color-default, token("gray-100"));
border: var(--_swc-action-button-border-width) solid var(--swc-action-button-border-color-default, transparent);
border-radius: var(--swc-action-button-border-radius, token("corner-radius-medium-size-medium"));

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] could hold in a private var vs repeating:

---_swc-action-button-border-radius: var(--swc-action-button-border-radius, token("corner-radius-medium-size-medium"));

/* Same inflated specificity — matches when aria-disabled is set externally
(e.g. by a disabled swc-action-group). Kept separate from :disabled so the
button stays focusable and shows a focus ring for keyboard users. */
:host([aria-disabled="true"]) .swc-ActionButton:is(*, :hover) {

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.

Was there an issue in combining selectors to merge this with the previous existing disabled styles rule (as in a comma to include both selector clauses)?

* @cssprop --swc-action-button-min-block-size - Minimum block size. Defaults to the medium component height token.
* @cssprop --swc-action-button-border-radius - Corner radius. Defaults to `corner-radius-medium-size-medium`.
* @cssprop --swc-action-button-border-radius - Corner radius applied to all four corners. Defaults to `corner-radius-medium-size-medium`.
* @cssprop --swc-action-button-border-start-start-radius - Start-start corner radius override. Defaults to `--swc-action-button-border-radius`. Used by `swc-action-group` compact mode to reset interior corners.

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 you think we need the implementation detail of "Used by..." since this is also consumer facing docs?

}

:host([compact]:not([quiet])[orientation="vertical"]) ::slotted(:not(:first-child, :last-child)) {
--swc-action-button-border-start-start-radius: 0;

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] since all are reset, could pass to --swc-action-button-border-radius instead (same for line 111)

* Propagates visual attributes (`quiet`, `size`, `staticColor`) to slotted
* children. Called on slot change and when any of these properties update.
*
* Note: `compact` affects children via CSS custom property cascade

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] cleanup comment to remove "Phase 5" mention


## Accessibility

- **Host role** no longer switches between `toolbar`, `radiogroup`, and `group`. It is always `role="group"`. Update any code or tests that asserted a specific host role. If you relied on the toolbar role for a landmark, wrap the group in your own `role="toolbar"` element instead.

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 add a link to the main docs page toolbar example since it covers aria-orientation too?

.swc-ActionButton {
border-color: ButtonBorder;
}
.swc-ActionButton--true {

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.

Whoops! Looks like a bug with the global styles conversion (also affecting line 86 here). I've filed the details (SWC-2511), but up to you if you want to exclude the aria-disabled styles for now.

- [x] Status table in workstream doc updated
- [x] PR created with description referencing Epic SWC-2212
- [ ] Peer engineer sign-off
- [ ] All `TODO` comments added to code during implementation are audited and filed as follow-up Jira tickets under Epic SWC-2212 (see Deferred implementation tickets below) — do this once, at the end, after all `TODO`s for the migration are in

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 noted a couple of these, maybe double-check?


public override connectedCallback(): void {
super.connectedCallback();
this.childObserver = new MutationObserver(() => {

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.

Low priority / a11y edge: swc-action-group can lose its keyboard tab stop after being detached and re-attached with unchanged children (Vue , drag-drop between containers, virtualized rows).

childObserver only starts observing in handleSlotchange, but slotchange doesn't re-fire on reconnect, so the rebuilt observer never re-arms.

Not a blocker. Suggested Fix: pull the observe loop out of handleSlotchange into a helper and also call it on reconnect (connectedCallback → this.updateComplete.then(() => this.observeChildren())).

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 test can surface the issue

export const ReconnectionObserverTest: Story = {
  render: () => html`
    <swc-action-group accessible-label="Edit actions">
      <swc-action-button>Cut</swc-action-button>
      <swc-action-button>Copy</swc-action-button>
      <swc-action-button>Paste</swc-action-button>
    </swc-action-group>
  `,
  play: async ({ canvasElement, step }) => {
    const group = await getComponent<ActionGroup>(
      canvasElement,
      'swc-action-group'
    );
    const buttons = Array.from(
      canvasElement.querySelectorAll('swc-action-button')
    ) as (HTMLElement & {
      disabled: boolean;
      updateComplete: Promise<boolean>;
    })[];

    await step('first child is the initial tab stop', async () => {
      expect(buttons[0].tabIndex).toBe(0);
    });

    await step('detach and re-attach the group', async () => {
      const parent = group.parentElement as HTMLElement;
      parent.removeChild(group);
      parent.appendChild(group); // unchanged children → no slotchange fires
      await group.updateComplete;
      await new Promise((r) => requestAnimationFrame(r));
    });

    await step(
      'disabling the active child after reconnect still moves the tab stop',
      async () => {
        buttons[0].disabled = true;
        await buttons[0].updateComplete;
        await new Promise((r) => requestAnimationFrame(r));

        expect(buttons[0].tabIndex, 'disabled child is no longer the tab stop').not.toBe(0);
        expect(buttons.filter((b) => b.tabIndex === 0).length, 'one tab stop remains').toBe(1);
        expect(buttons[1].tabIndex, 'next child becomes the tab stop').toBe(0);
      }
    );

    await step('strip stays keyboard-operable', async () => {
      buttons[1].focus();
      await userEvent.keyboard('{ArrowRight}');
      expect(document.activeElement).toBe(buttons[2]);
    });
  },
};

changed.has('staticColor') ||
// `compact` is pre-wired here so children re-render when the attribute
// changes, but the attribute is not propagated via JS — it affects layout
// via CSS cascade on ::slotted() selectors (wired in Phase 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.

We can probably remove any references to Phases, since it doesn't have much value post migration.

* children. Called on slot change and when any of these properties update.
*
* Note: `compact` affects children via CSS custom property cascade
* (`::slotted(:first-child)` / `::slotted(:last-child)` in Phase 5) rather

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.

Same thought: We can probably remove any references to Phases, since it doesn't have much value post migration.

* they can be forwarded to the inner `<button>`, preventing duplicate ARIA
* state from appearing on both the host and the native element. The
* `_ariaForwardingInProgress` guard stops the re-entrant callback triggered
* by `removeAttribute` from re-entering this branch.

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 comment is now stale with the addition of aria-disabled and it not stripping as the other two do. Can we update the JSDoc?

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

Labels

Component:Action group gen2 These issues or PRs map to our 2nd generation work to modernizing infrastructure. run_vrt Triggers the Chromatic VRT run for 2nd-gen 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