From 0df4e0390acea057079b4d4e3371f2c91e941207 Mon Sep 17 00:00:00 2001 From: Cory Dransfeldt <445732+cdransf@users.noreply.github.com> Date: Tue, 26 May 2026 14:58:21 -0700 Subject: [PATCH 1/2] feat(action-button): adds migration plan for the S2 action button component (#6327) * feat(action-button): adds migration plan for the S2 action button component * chore(action-button): address feedback * chore(action-button): address feedback --- .../03_components/README.md | 1 + .../action-button/migration-plan.md | 677 ++++++++++++++++++ 2 files changed, 678 insertions(+) create mode 100644 CONTRIBUTOR-DOCS/03_project-planning/03_components/action-button/migration-plan.md diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md index a710563fe4a..cde58b33c4f 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md @@ -24,6 +24,7 @@ - [Accordion migration roadmap](accordion/rendering-and-styling-migration-analysis.md) - Action Button - [Action button accessibility migration analysis](action-button/accessibility-migration-analysis.md) + - [Action button migration plan](action-button/migration-plan.md) - [Action button migration roadmap](action-button/rendering-and-styling-migration-analysis.md) - Action Group - [Action group migration roadmap](action-group/rendering-and-styling-migration-analysis.md) diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/action-button/migration-plan.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/action-button/migration-plan.md new file mode 100644 index 00000000000..f0471a64189 --- /dev/null +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/action-button/migration-plan.md @@ -0,0 +1,677 @@ + + +[CONTRIBUTOR-DOCS](../../../README.md) / [Project planning](../../README.md) / [Components](../README.md) / Action Button / Action button migration plan + + + +# Action button migration plan + + + +
+In this doc + +- [TL;DR](#tldr) + - [Most blocking open questions](#most-blocking-open-questions) +- [1st-gen API surface](#1st-gen-api-surface) + - [Properties / attributes](#properties--attributes) + - [Methods](#methods) + - [Events](#events) + - [Slots](#slots) + - [CSS custom properties](#css-custom-properties) + - [Shadow DOM output (rendered HTML)](#shadow-dom-output-rendered-html) +- [Dependencies](#dependencies) +- [Migration sequencing and prerequisites](#migration-sequencing-and-prerequisites) + - [Prerequisites](#prerequisites) + - [Dependencies this migration creates](#dependencies-this-migration-creates) + - [`spectrum-css` setup](#spectrum-css-setup) + - [Recommended order](#recommended-order) +- [Changes overview](#changes-overview) + - [Must ship — breaking or a11y-required](#must-ship--breaking-or-a11y-required) + - [Additive — ships when ready, zero breakage for consumers already on 2nd-gen](#additive--ships-when-ready-zero-breakage-for-consumers-already-on-2nd-gen) +- [2nd-gen API decisions](#2nd-gen-api-decisions) + - [Public API](#public-api) + - [Visual matrix](#visual-matrix) + - [Pending state (new in 2nd-gen)](#pending-state-new-in-2nd-gen) + - [Accessibility semantics notes (2nd-gen)](#accessibility-semantics-notes-2nd-gen) + - [Deferred semantics note (2nd-gen)](#deferred-semantics-note-2nd-gen) +- [Architecture: core vs SWC split](#architecture-core-vs-swc-split) + - [Shared semantics reuse](#shared-semantics-reuse) +- [Migration checklist](#migration-checklist) + - [Preparation (this ticket)](#preparation-this-ticket) + - [Setup](#setup) + - [API](#api) + - [Styling](#styling) + - [Accessibility](#accessibility) + - [Testing](#testing) + - [Documentation](#documentation) + - [Review](#review) +- [Blockers and open questions](#blockers-and-open-questions) + - [Design](#design) + - [Architecture and behavior](#architecture-and-behavior) + - [Scope and prerequisites](#scope-and-prerequisites) + - [Resolved decisions](#resolved-decisions) + - [Deferred follow-up tickets](#deferred-follow-up-tickets) +- [Breaking changes to verify](#breaking-changes-to-verify) +- [References](#references) + +
+ + + +> **SWC-2039** · Planning output. Must be reviewed before implementation begins. + +--- + +## TL;DR + +- `swc-action-button` extends `ButtonBase` from `2nd-gen/packages/core/components/button/` — no separate `ActionButton.base.ts` in core is needed since no 2nd-gen component inherits from `swc-action-button` +- `selected`, `toggles`, and `aria-pressed` are removed from `swc-action-button`; toolbar-style toggles move to `swc-toggle-button` / `swc-toggle-button-group` +- `emphasized` is removed because it only applies to the selected state, which is removed +- `hold-affordance` / `longpress` are deferred until a later date; Storybook and migration copy must say so explicitly +- `href` / link API is removed entirely; navigation uses native `` elements +- `pending` ships with the initial release; `ButtonBase` provides the logic and the visual implementation can be copied from `swc-button` +- `accessible-label` replaces `label` (inherited from `ButtonBase`) +- `size` includes `xs` (not available on `swc-button`), requiring `ACTION_BUTTON_VALID_SIZES` in `ActionButton.types.ts` +- `quiet` and `static-color` are retained as the primary visual differentiators for this component +- `value` is **confirmed**; retained for identification within action groups (JS property defaults to `''`; falls back to `textContent` when empty) + +### Most blocking open questions + +- **Q2** — Consumer migration copy for `swc-action-button` references `swc-toggle-button` / `swc-toggle-button-group`, which are not yet available in 2nd-gen. Either hold the migration-guide note or add an explicit "coming soon" caveat before documenting the toggle migration path. +- **Q1** (provisional, not blocking implementation) — The Figma PNG from `S2 / Web (Desktop scale)` has not been provided. The visual matrix in this plan is inferred from analysis docs. Confirm before Phase 5 (styling). + +--- + +## 1st-gen API surface + +**Source:** [`1st-gen/packages/action-button/src/ActionButton.ts`](../../../../1st-gen/packages/action-button/src/ActionButton.ts) +**Version:** `@spectrum-web-components/action-button@1.12.1` +**Custom element tag:** `sp-action-button` +**Extends:** `SizedMixin(ButtonBase, { validSizes: ['xs', 's', 'm', 'l', 'xl'], noDefaultSize: true })` + +### Properties / attributes + +| Property | Type | Default | Attribute | Notes | +|---|---|---|---|---| +| `emphasized` | `boolean` | `false` | `emphasized` | Adds visual emphasis to the selected state only. | +| `holdAffordance` | `boolean` | `false` | `hold-affordance` | Shows a corner triangle indicator; wires 300 ms pointer timer and keyboard path for `longpress`. | +| `quiet` | `boolean` | `false` | `quiet` | Applies quiet styling (no background/border at rest). | +| `role` | `string` | `'button'` | `role` | Overrides the ARIA role. Used by 1st-gen `sp-action-group` to reassign `radio` / `checkbox` on children. | +| `selected` | `boolean` | `false` | `selected` | Whether the button is selected. Managed by `toggles` or set externally. | +| `toggles` | `boolean` | `false` | `toggles` | When true, automatically manages `selected` on click and exposes `aria-pressed`. | +| `staticColor` | `'white' \| 'black' \| undefined` | `undefined` | `static-color` | Static color variant for use over images or colored backgrounds. | +| `value` | `string` | `''` (falls back to `textContent`) | `value` | Used for identification in action groups. | +| `size` | `'xs' \| 's' \| 'm' \| 'l' \| 'xl'` | `'m'` (no attribute) | `size` | T-shirt sizes; no default attribute from `noDefaultSize: true`. | +| `disabled` | `boolean` | `false` | `disabled` | Inherited from `Focusable`; removes focusability. | +| `autofocus` | `boolean` | `false` | `autofocus` | Inherited from `Focusable`. | +| `tabIndex` | `number` | managed | `tabindex` | Inherited from `Focusable`. | +| `label` | `string \| undefined` | `undefined` | `label` | Inherited from `LikeAnchor`; mirrored to host `aria-label`. Accessible name for icon-only usage. | +| `href` | `string \| undefined` | `undefined` | `href` | Deprecated link mode (dev warning in 1st-gen). | +| `target` | link target union | `undefined` | `target` | Deprecated with `href`. | +| `download` | `string \| undefined` | `undefined` | `download` | Deprecated with `href`. | +| `referrerpolicy` | `string \| undefined` | `undefined` | `referrerpolicy` | Deprecated with `href`. | +| `rel` | `string \| undefined` | `undefined` | `rel` | Deprecated with `href`. | +| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | `type` | Inherited from `ButtonBase`. | +| `name` | `string` | `undefined` | `name` | Inherited from `ButtonBase`; form field name. | +| `active` | `boolean` | `false` | `active` | Reflected pressed-state flag used during keyboard hold interaction. | + +### Methods + +| Method | Signature | Notes | +|---|---|---| +| `click()` | `(): void` | Delegates to `Focusable.click()`; no special pending guard in 1st-gen (1st-gen has no pending state). | + +### Events + +| Event | Detail | Notes | +|---|---|---| +| `change` | `Event` (cancelable) | Fired when `selected` changes via `toggles`. Calling `preventDefault()` reverts `selected`. | +| `longpress` | `CustomEvent<{ source: 'pointer' \| 'keyboard' }>` | Fired after 300 ms pointer hold or `Space` / `Alt+ArrowDown` key hold when `hold-affordance` is set. | + +### Slots + +| Slot | Content | Notes | +|---|---|---| +| default | Visible text label | Required for accessible name unless `label` / `accessible-label` is set. | +| `icon` | Leading icon element | Optional; component detects icon presence via slot content inspection. | + +### CSS custom properties + +The 1st-gen implementation uses `--spectrum-actionbutton-*` and `--mod-actionbutton-*` token chains via imported `action-button.css`, `spectrum-action-button.css`, and `action-button-overrides.css`. The modifier surface covers sizing and spacing, typography, and a full color/state/variant matrix. This full modifier surface will **not** be carried forward to 2nd-gen. A small reviewed set of `--swc-action-button-*` properties will be exposed instead. + +### Shadow DOM output (rendered HTML) + +```html + + + + + + + + + + +``` + +The host element itself carries `role="button"` and is the primary focus target. There is no inner native ` + `; + } + + protected override update(changes: PropertyValues): void { + super.update(changes); + // Counteracts SizedMixin's auto-reflect of size="m" when no size was explicitly set. + if (this._size === null) { + this.removeAttribute('size'); + } + } +} diff --git a/2nd-gen/packages/swc/components/action-button/action-button.css b/2nd-gen/packages/swc/components/action-button/action-button.css new file mode 100644 index 00000000000..54453fa85ca --- /dev/null +++ b/2nd-gen/packages/swc/components/action-button/action-button.css @@ -0,0 +1,17 @@ +/** + * 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. + */ + +.swc-ActionButton { + display: inline-flex; + align-items: center; + justify-content: center; +} diff --git a/2nd-gen/packages/swc/components/action-button/index.ts b/2nd-gen/packages/swc/components/action-button/index.ts new file mode 100644 index 00000000000..bafdabe9120 --- /dev/null +++ b/2nd-gen/packages/swc/components/action-button/index.ts @@ -0,0 +1,12 @@ +/** + * 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 './ActionButton.js'; diff --git a/2nd-gen/packages/swc/components/action-button/stories/action-button.stories.ts b/2nd-gen/packages/swc/components/action-button/stories/action-button.stories.ts new file mode 100644 index 00000000000..d0a2c999b68 --- /dev/null +++ b/2nd-gen/packages/swc/components/action-button/stories/action-button.stories.ts @@ -0,0 +1,63 @@ +/** + * 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 { Meta, StoryObj as Story } from '@storybook/web-components'; +import { getStorybookHelpers } from '@wc-toolkit/storybook-helpers'; + +import '@adobe/spectrum-wc/components/action-button/swc-action-button.js'; + +// ──────────────── +// METADATA +// ──────────────── + +const { args, argTypes, template } = getStorybookHelpers('swc-action-button'); + +/** + * A compact action button for toolbars, action groups, and icon-first chrome. + */ +const meta: Meta = { + title: 'Action Button', + component: 'swc-action-button', + args, + argTypes, + render: (args) => template(args), + parameters: { + docs: { + subtitle: 'Compact button for toolbars and action groups', + }, + }, + tags: ['migrated'], +}; + +export default meta; + +// ──────────────────── +// AUTODOCS STORY +// ──────────────────── + +export const Playground: Story = { + args: { + 'default-slot': 'Edit', + }, + tags: ['autodocs', 'dev'], +}; + +// ────────────────────────── +// OVERVIEW STORY +// ────────────────────────── + +export const Overview: Story = { + args: { + 'default-slot': 'Edit', + }, + tags: ['overview'], +}; diff --git a/2nd-gen/packages/swc/components/action-button/swc-action-button.ts b/2nd-gen/packages/swc/components/action-button/swc-action-button.ts new file mode 100644 index 00000000000..accc8d2e1c7 --- /dev/null +++ b/2nd-gen/packages/swc/components/action-button/swc-action-button.ts @@ -0,0 +1,22 @@ +/** + * 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 { defineElement } from '@spectrum-web-components/core/element/index.js'; + +import { ActionButton } from './ActionButton.js'; + +declare global { + interface HTMLElementTagNameMap { + 'swc-action-button': ActionButton; + } +} + +defineElement('swc-action-button', ActionButton); diff --git a/2nd-gen/packages/swc/components/action-button/test/action-button.a11y.spec.ts b/2nd-gen/packages/swc/components/action-button/test/action-button.a11y.spec.ts new file mode 100644 index 00000000000..7c15a5c2d7b --- /dev/null +++ b/2nd-gen/packages/swc/components/action-button/test/action-button.a11y.spec.ts @@ -0,0 +1,20 @@ +/** + * 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. + */ + +/** + * Accessibility tests for ActionButton component (2nd generation). + * + * ARIA snapshot tests validate the accessibility tree structure. + * aXe WCAG compliance and color contrast validation are run via + * test-storybook (see .storybook/test-runner.ts). Both are included + * in the `test:a11y` command. + */ diff --git a/2nd-gen/packages/swc/components/action-button/test/action-button.test.ts b/2nd-gen/packages/swc/components/action-button/test/action-button.test.ts new file mode 100644 index 00000000000..c5ff2b57984 --- /dev/null +++ b/2nd-gen/packages/swc/components/action-button/test/action-button.test.ts @@ -0,0 +1,30 @@ +/** + * 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 { Meta, StoryObj as Story } from '@storybook/web-components'; + +import meta, { Overview } from '../stories/action-button.stories.js'; + +export default { + ...meta, + title: 'Action Button/Tests', + parameters: { + ...meta.parameters, + docs: { disable: true, page: null }, + }, + tags: ['!autodocs', 'dev'], +} as Meta; + +// Placeholder: play function with component-specific assertions is added in Phase 6. +export const OverviewTest: Story = { + ...Overview, +}; diff --git a/CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/01_status.md b/CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/01_status.md index dc0c12588c1..f213e5fcbd9 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/01_status.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/01_status.md @@ -22,7 +22,7 @@ | ------------------- | ------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Accordion | ✓ | | | | | | | | Action Bar | | | | | | | | -| Action Button | ✓ | | | | | | | +| Action Button | ✓ | ✓ | ✓ | | | | | | Action Group | ✓ | | | | | | | | Action Menu | | | | | | | | | Alert Banner | ✓ | ✓ | | | | | | diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/action-button/migration-plan.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/action-button/migration-plan.md index f0471a64189..f242ffb0e84 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/03_components/action-button/migration-plan.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/action-button/migration-plan.md @@ -74,7 +74,7 @@ - `accessible-label` replaces `label` (inherited from `ButtonBase`) - `size` includes `xs` (not available on `swc-button`), requiring `ACTION_BUTTON_VALID_SIZES` in `ActionButton.types.ts` - `quiet` and `static-color` are retained as the primary visual differentiators for this component -- `value` is **confirmed**; retained for identification within action groups (JS property defaults to `''`; falls back to `textContent` when empty) +- `value` is **deferred**; removed from the initial release — `swc-action-group` does not support the same selection semantics as 1st-gen, form-association use cases are inconsistent with `swc-button`, and `value` via form association has known issues; tracked in SWC-2042 ### Most blocking open questions @@ -256,7 +256,7 @@ These decisions are derived from the 1st-gen implementation, the current depreca | `size` | `'xs' \| 's' \| 'm' \| 'l' \| 'xl'` | `'m'` | `size` | **Confirmed.** Includes `xs` — differs from `swc-button` which starts at `s`. Requires `ACTION_BUTTON_VALID_SIZES` in `ActionButton.types.ts`. No default attribute (`noDefaultSize: true`): `getAttribute('size')` returns `null` until a consumer sets it explicitly; the JS property defaults to `'m'` but is not reflected to the DOM automatically. | | `quiet` | `boolean` | `false` | `quiet` | **Confirmed.** Retained as a primary visual differentiator (no background/border at rest). Unlike Button's deprecated `quiet`, this is a first-class visual treatment for action-button. | | `staticColor` | `'white' \| 'black' \| undefined` | `undefined` | `static-color` | **Confirmed.** Static color for use over images or colored backgrounds. Supported with both default and `quiet` treatments. | -| `value` | `string` | `''` | `value` | **Confirmed.** Retained for identification within action groups. JS property defaults to `''`; when the stored value is empty (`''`), the component reads `textContent` as the effective value for action-group identification. Consumers who rely on the fallback must not also set `value=""` and expect `textContent` to win. | +| `value` | deferred | n/a | deferred | **Deferred (SWC-2042).** `swc-action-group` does not support the same selection semantics as 1st-gen, so the primary consumer use case does not exist yet. Form-association use cases are inconsistent with `swc-button` not having `value`, and `value` via form association has known issues. Removed from initial release; tracked in SWC-2042. | | `disabled` | `boolean` | `false` | `disabled` | **Confirmed.** Inherited from `ButtonBase`. Maps to native `disabled` on the internal `