diff --git a/.changeset/action-group-2nd-gen-migration.md b/.changeset/action-group-2nd-gen-migration.md new file mode 100644 index 00000000000..1c1dd42cb01 --- /dev/null +++ b/.changeset/action-group-2nd-gen-migration.md @@ -0,0 +1,13 @@ +--- +'@adobe/spectrum-wc': minor +'@spectrum-web-components/core': minor +'@spectrum-web-components/action-group': patch +--- + +Add the 2nd-gen `` file structure, API, and accessibility semantics, migrated from the Spectrum 1 ``. + +- **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. diff --git a/1st-gen/packages/action-group/src/ActionGroup.ts b/1st-gen/packages/action-group/src/ActionGroup.ts index 8d2176de984..f9c50fd2a72 100644 --- a/1st-gen/packages/action-group/src/ActionGroup.ts +++ b/1st-gen/packages/action-group/src/ActionGroup.ts @@ -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; @@ -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(); diff --git a/2nd-gen/packages/core/components/action-group/ActionGroup.base.ts b/2nd-gen/packages/core/components/action-group/ActionGroup.base.ts new file mode 100644 index 00000000000..813b8905d2e --- /dev/null +++ b/2nd-gen/packages/core/components/action-group/ActionGroup.base.ts @@ -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): void { + super.firstUpdated(changed); + this.setAttribute('role', 'group'); + } + + protected override updated(changed: PropertyValues): 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