Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dde23ed
chore(actiongroup): set up feature branch
rise-erpelding Jun 23, 2026
c11399e
chore(actiongroup): set up feature branch
rise-erpelding Jun 23, 2026
d875ab7
chore(actiongroup): set up feature branch
rise-erpelding Jun 23, 2026
f4a38a8
chore(actiongroup): setup file structure
rise-erpelding Jun 23, 2026
be92ef9
feat(actiongroup): set up api
rise-erpelding Jun 23, 2026
62f0941
feat(actiongroup): a11y implementation
rise-erpelding Jun 25, 2026
cd204d4
fix: keep aria-disabled discoverable
Jun 29, 2026
23fa7f2
fix(actionbutton): apply aria-disabled to children of disabled action…
Jun 29, 2026
950b11f
docs: updates to the action group plan
Jun 29, 2026
1f5ab7c
docs: update storybook controls
Jun 29, 2026
66c26d7
docs: add comment about aria-disabled
Jun 29, 2026
c9fcc6a
chore(actiongroup): add deprecation warnings for selects, vertical, e…
Jul 6, 2026
6a68db5
fix(actiongroup): preserve delegatesFocus
Jul 6, 2026
eb09837
fix: remove label attribute in favor of default slot on action buttons
Jul 7, 2026
9e0e789
fix: remove references to 1st-gen
Jul 7, 2026
990f98d
fix: change label to accessible-label
Jul 7, 2026
27e96b0
fix: suppress aria-disabled click activation
Jul 7, 2026
317dd65
chore: add changeset
Jul 7, 2026
ecb5fa8
feat(actionbutton): add styling for each corner radius
Jul 8, 2026
0555348
fix(actionbutton): inline size 100
Jul 8, 2026
cd079f5
feat(actiongroup): adjustments to ts
Jul 8, 2026
3b75bb4
feat(actiongroup): initial styles
Jul 8, 2026
647ca0c
docs(actiongroup): storybook updates needed for style validation
Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/action-group-2nd-gen-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@adobe/spectrum-wc': minor
'@spectrum-web-components/core': minor
'@spectrum-web-components/action-group': patch
---

Add the 2nd-gen `<swc-action-group>` file structure, API, and accessibility semantics, migrated from the Spectrum 1 `<sp-action-group>`.

- **API**: `accessible-label`, `disabled`, `orientation` (`horizontal` / `vertical`), `compact`, `quiet`, `justified`, `size`, `static-color`; children are collected via the default slot (`swc-action-button` / `swc-action-menu`).
- **Accessibility**: host always `role="group"` (never `toolbar` or `radiogroup`); `accessible-label` reflects to `aria-label`; `aria-orientation="vertical"` set only for vertical layout; group `disabled` sets `aria-disabled="true"` on the host and propagates `aria-disabled` (not native `disabled`) to all children so they remain keyboard-reachable and discoverable per [APG: Focusability of disabled controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols); `FocusgroupNavigationController` wired with `skipDisabled: false` so `aria-disabled` children stay in the arrow-key sequence.
- **`ButtonBase` (core)**: click activation is now suppressed for any button whose host carries `aria-disabled="true"`, matching the existing suppression for `disabled` and `pending`. This is shared behavior for every `ButtonBase` subclass, not just `swc-action-button`.
- **`swc-action-button`**: added disabled-appearance CSS for the externally-set `aria-disabled` case (e.g. when disabled via a parent `swc-action-group`), including forced-colors system-color overrides.
- **1st-gen `sp-action-group`**: added `@deprecated` JSDoc to `vertical`, `selects`, `selected`, and `emphasized`, plus a runtime `window.__swc.warn()` deprecation notice on the `selected` setter, ahead of removal in 2nd-gen.
89 changes: 86 additions & 3 deletions 1st-gen/packages/action-group/src/ActionGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,30 @@ export class ActionGroup extends SizedMixin(SpectrumElement, {
@property({ type: Boolean, reflect: true })
public compact = false;

/**
* @deprecated The `emphasized` property is deprecated and will be removed
* in a future release.
*/
@property({ type: Boolean, reflect: true })
public emphasized = false;
public set emphasized(value: boolean) {
const oldValue = this._emphasized;
this._emphasized = value;
this.requestUpdate('emphasized', oldValue);
if (window.__swc?.DEBUG) {
window.__swc.warn(
this,
`The "emphasized" attribute on <${this.localName}> is deprecated and will be removed in a future release.`,
'https://opensource.adobe.com/spectrum-web-components/components/action-group/',
{ level: 'deprecation' }
);
}
}

public get emphasized(): boolean {
return this._emphasized;
}

private _emphasized = false;

@property({ type: Boolean, reflect: true })
public justified = false;
Expand All @@ -114,20 +136,81 @@ export class ActionGroup extends SizedMixin(SpectrumElement, {
@property({ type: Boolean, reflect: true })
public quiet = false;

/**
* @deprecated The `selects` property is deprecated and will be removed in
* a future release. Selection UX will be available via `swc-toggle-button-group`
* (toggle and multi-select) and `swc-segmented-control` (exclusive choice)
* in the 2nd-gen library.
*/
@property({ type: String })
public selects: undefined | 'single' | 'multiple';
public set selects(value: undefined | 'single' | 'multiple') {
const oldValue = this._selects;
this._selects = value;
this.requestUpdate('selects', oldValue);
if (window.__swc?.DEBUG) {
window.__swc.warn(
this,
`The "selects" attribute on <${this.localName}> is deprecated and will be removed in a future release. Selection UX is available via \`swc-toggle-button-group\` (toggle and multi-select) and \`swc-segmented-control\` (exclusive choice) in the 2nd-gen library.`,
'https://opensource.adobe.com/spectrum-web-components/components/action-group/',
{ level: 'deprecation' }
);
}
}

public get selects(): undefined | 'single' | 'multiple' {
return this._selects;
}

private _selects: undefined | 'single' | 'multiple' = undefined;

@property({ reflect: true, attribute: 'static-color' })
public staticColor?: 'white' | 'black';

/**
* @deprecated The `vertical` attribute is deprecated and will be removed in
* a future release. Use `orientation="vertical"` on `swc-action-group`
* instead.
*/
@property({ type: Boolean, reflect: true })
public vertical = false;
public set vertical(value: boolean) {
const oldValue = this._vertical;
this._vertical = value;
this.requestUpdate('vertical', oldValue);
if (window.__swc?.DEBUG) {
window.__swc.warn(
this,
`The "vertical" attribute on <${this.localName}> is deprecated and will be removed in a future release. Use \`orientation="vertical"\` on \`swc-action-group\` instead.`,
'https://opensource.adobe.com/spectrum-web-components/components/action-group/',
{ level: 'deprecation' }
);
}
}

public get vertical(): boolean {
return this._vertical;
}

private _vertical = false;

private _selected: string[] = EMPTY_SELECTION;

/**
* @deprecated The `selected` property is deprecated and will be removed in
* a future release. Selection UX is available via `swc-toggle-button-group`
* (toggle and multi-select) and `swc-segmented-control` (exclusive choice)
* in the 2nd-gen library.
*/
set selected(selected: string[]) {
this.requestUpdate('selected', this._selected);
this._selected = selected;
if (window.__swc?.DEBUG) {
window.__swc.warn(
this,
`The "selected" attribute on <${this.localName}> is deprecated and will be removed in a future release.`,
'https://opensource.adobe.com/spectrum-web-components/components/action-group/',
{ level: 'deprecation' }
);
}
this.updateComplete.then(() => {
this.applySelects();
this.manageChildren();
Expand Down
211 changes: 211 additions & 0 deletions 2nd-gen/packages/core/components/action-group/ActionGroup.base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/**
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { PropertyValues } from 'lit';
import { property, queryAssignedElements } from 'lit/decorators.js';

import { SpectrumElement } from '@spectrum-web-components/core/element/index.js';
import { SizedMixin } from '@spectrum-web-components/core/mixins/index.js';

import {
ACTION_GROUP_ORIENTATIONS,
ACTION_GROUP_STATIC_COLORS,
ACTION_GROUP_VALID_SIZES,
type ActionGroupOrientation,
type ActionGroupSize,
} from './ActionGroup.types.js';

/**
* An action group clusters related action buttons together with composite
* keyboard navigation: one Tab stop into the strip, arrow keys move among
* `swc-action-button` and `swc-action-menu` children.
*
* This base class owns the accessibility semantics, `accessible-label` →
* `aria-label` management, `disabled` state contract, and child collection logic.
* Rendering and styling live in the concrete SWC subclass.
*
* @slot - One or more `swc-action-button` or `swc-action-menu` elements.
*
* @attribute {ActionGroupSize} size - The size of the group and its children.
*/
export abstract class ActionGroupBase extends SizedMixin(SpectrumElement, {
validSizes: ACTION_GROUP_VALID_SIZES,
noDefaultSize: true,
}) {
static override shadowRootOptions = {
...SpectrumElement.shadowRootOptions,
delegatesFocus: true,
};

/**
* The size of the action group. Propagated to all slotted children.
*/
declare public size: ActionGroupSize;

// ──────────────────
// SHARED API
// ──────────────────

/**
* @internal
*
* Valid orientation values for validation.
*/
static readonly ORIENTATIONS: readonly string[] = ACTION_GROUP_ORIENTATIONS;

/**
* @internal
*
* Valid static-color values for validation.
*/
static readonly STATIC_COLORS: readonly string[] = ACTION_GROUP_STATIC_COLORS;

/**
* The layout direction of the action group.
*
* When set to `"vertical"`, the group stacks children vertically and
* `aria-orientation="vertical"` is applied to the host. When `"horizontal"`
* (the default), `aria-orientation` is omitted because horizontal is the
* implicit default for `role="group"`.
*
* @default horizontal
*/
@property({ type: String, reflect: true })
public orientation: ActionGroupOrientation = 'horizontal';

/**
* Accessible label for the group. Reflected to `aria-label` on the host.
*
* Providing a label is recommended whenever the strip has a distinct
* purpose (e.g. "Text formatting" or "Alignment"). An empty value removes
* the `aria-label` attribute.
*/
@property({ type: String, attribute: 'accessible-label' })
public accessibleLabel = '';

/**
* Whether the group and all of its children are disabled.
*
* Sets `aria-disabled="true"` on the host and propagates `aria-disabled`
* to each managed child. Children remain keyboard-reachable so that screen
* reader users can still discover the group — native `disabled` is not
* applied to children.
*/
@property({ type: Boolean, reflect: true })
public disabled = false;

// ──────────────────────
// IMPLEMENTATION
// ──────────────────────

/**
* @internal
*
* The managed children: all `swc-action-button` and `swc-action-menu`
* elements assigned to the default slot. Populated by the concrete
* subclass via `@queryAssignedElements`.
*/
@queryAssignedElements({ flatten: true })
protected managedChildren!: HTMLElement[];

/**
* Focuses the first enabled child in the group.
*
* The SWC concrete class overrides this with `FocusgroupNavigationController`,
* restoring focus to the last active item when memory is enabled. This
* implementation serves as a fallback for non-SWC subclasses.
*/
public override focus(options?: FocusOptions): void {
const firstEnabled = this.managedChildren?.find(
(el) =>
!el.hasAttribute('disabled') &&
el.getAttribute('aria-disabled') !== 'true'
);
firstEnabled?.focus(options);
}

/**
* @internal
*
* Handles slot-change events. Propagates `aria-disabled` to newly slotted
* children when the group is disabled. Concrete subclasses call
* `super.handleSlotchange()` and then propagate visual attributes.
*/
protected handleSlotchange(): void {
if (this.disabled) {
this.propagateDisabledToChildren();
}
}

protected override firstUpdated(changed: PropertyValues<this>): void {
super.firstUpdated(changed);
this.setAttribute('role', 'group');
}

protected override updated(changed: PropertyValues<this>): void {
super.updated(changed);

if (changed.has('accessibleLabel')) {
if (this.accessibleLabel) {
this.setAttribute('aria-label', this.accessibleLabel);
} else {
this.removeAttribute('aria-label');
}
}

if (changed.has('disabled')) {
if (this.disabled) {
this.setAttribute('aria-disabled', 'true');
} else {
this.removeAttribute('aria-disabled');
}
this.propagateDisabledToChildren();
}

if (changed.has('orientation')) {
if (this.orientation === 'vertical') {
this.setAttribute('aria-orientation', 'vertical');
} else {
this.removeAttribute('aria-orientation');
}
}

if (window.__swc?.DEBUG) {
const constructor = this.constructor as typeof ActionGroupBase;
if (!constructor.ORIENTATIONS.includes(this.orientation)) {
window.__swc.warn(
this,
`<${this.localName}> element expects the "orientation" attribute to be one of the following: ${constructor.ORIENTATIONS.join(', ')}.`,
'https://opensource.adobe.com/spectrum-web-components/components/action-group/',
{ issues: [...constructor.ORIENTATIONS] }
);
}
}
}

private propagateDisabledToChildren(): void {
// `aria-disabled` is set on the child host element. For AT to announce the
// child as disabled, each child component must forward the host attribute to
// its inner interactive element. `swc-action-button` already does this
// (observes `aria-disabled` and forwards to its inner <button>).
// `swc-action-menu` will need the same treatment when it is built — it must
// forward `aria-disabled` from its host to its inner trigger element and
// apply disabled appearance via :host([aria-disabled="true"]) CSS.
for (const child of this.managedChildren ?? []) {
if (this.disabled) {
child.setAttribute('aria-disabled', 'true');
} else {
child.removeAttribute('aria-disabled');
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import type { ElementSize } from '@spectrum-web-components/core/mixins/index.js';

// ──────────────────
// SHARED
// ──────────────────

export const ACTION_GROUP_VALID_SIZES = [
'xs',
's',
'm',
'l',
'xl',
] as const satisfies readonly ElementSize[];

export const ACTION_GROUP_ORIENTATIONS = ['horizontal', 'vertical'] as const;

export const ACTION_GROUP_STATIC_COLORS = ['white', 'black'] as const;

// ──────────────────
// TYPES
// ──────────────────

export type ActionGroupSize = (typeof ACTION_GROUP_VALID_SIZES)[number];
export type ActionGroupOrientation = (typeof ACTION_GROUP_ORIENTATIONS)[number];
export type ActionGroupStaticColor =
(typeof ACTION_GROUP_STATIC_COLORS)[number];
13 changes: 13 additions & 0 deletions 2nd-gen/packages/core/components/action-group/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
export * from './ActionGroup.base.js';
export * from './ActionGroup.types.js';
Loading
Loading