diff --git a/.changeset/pending-reusable-primitives.md b/.changeset/pending-reusable-primitives.md new file mode 100644 index 00000000000..ef9447db3df --- /dev/null +++ b/.changeset/pending-reusable-primitives.md @@ -0,0 +1,9 @@ +--- +'@spectrum-web-components/core': minor +'@adobe/spectrum-wc': patch +--- + +Extract the pending (busy) state into reusable, decoupled 2nd-gen core primitives so any pending-capable component can adopt it. + +- **`@spectrum-web-components/core`**: adds `PendingController` (`/controllers/pending-controller`) for the pending state (delayed activation, inline-size freeze, derived busy accessible name), the render-only `renderPendingSpinner` directive (`/directives/pending-spinner`), and `PendingMixin` (`/mixins`) which wires the controller, the `pending` / `pending-label` properties, and click suppression. `ButtonBase` no longer owns pending state. +- **`@adobe/spectrum-wc`**: `swc-button` and `swc-action-button` now consume these primitives via `PendingMixin`. No public API change — `pending` / `pending-label` and the busy behavior are unchanged. diff --git a/1st-gen/tools/base/src/version.d.ts b/1st-gen/tools/base/src/version.d.ts index 71718368164..89f22b9c39f 100644 --- a/1st-gen/tools/base/src/version.d.ts +++ b/1st-gen/tools/base/src/version.d.ts @@ -12,8 +12,8 @@ /** * The version of the 1st-gen Spectrum Web Components library. */ -export declare const version = "1.12.1"; +export declare const version = "1.12.2"; /** * The version of the core base package. */ -export declare const coreVersion = "0.1.0"; +export declare const coreVersion = "0.3.0"; diff --git a/1st-gen/tools/base/src/version.ts b/1st-gen/tools/base/src/version.ts index f918df20145..f7f58271b7a 100644 --- a/1st-gen/tools/base/src/version.ts +++ b/1st-gen/tools/base/src/version.ts @@ -17,9 +17,9 @@ /** * The version of the 1st-gen Spectrum Web Components library. */ -export const version = '1.12.1'; +export const version = '1.12.2'; /** * The version of the core base package. */ -export const coreVersion = '0.1.0'; +export const coreVersion = '0.3.0'; diff --git a/2nd-gen/packages/core/components/button/Button.base.ts b/2nd-gen/packages/core/components/button/Button.base.ts index 783f7939c99..daf4e69e9b6 100644 --- a/2nd-gen/packages/core/components/button/Button.base.ts +++ b/2nd-gen/packages/core/components/button/Button.base.ts @@ -11,7 +11,7 @@ */ import { PropertyValues } from 'lit'; -import { property, state } from 'lit/decorators.js'; +import { property } from 'lit/decorators.js'; import { SpectrumElement } from '@spectrum-web-components/core/element/index.js'; import { @@ -60,13 +60,6 @@ export abstract class ButtonBase extends SizedMixin( @property({ type: Boolean, reflect: true }) public disabled: boolean = false; - /** - * Whether the button is in a pending (busy) state. The button remains - * focusable but activation is suppressed. - */ - @property({ type: Boolean, reflect: true }) - public pending: boolean = false; - /** * Accessible label forwarded to the internal ` + `; + } +} +``` + +Include the shared `pending-spinner.css` fragment (`swc/stylesheets/_lit-styles/pending-spinner.css`) in the host's styles so the rendered spinner is themed with Spectrum tokens. + +## Behaviors + +### Delayed activation + +When `pending` becomes `true`, the controller waits `delay` ms before setting `pendingActive`. If `pending` returns to `false` before the delay elapses, the visual never activates. This avoids a distracting flash for fast operations while still announcing the busy state to assistive technology right away. The demo below uses a shortened 250 ms delay. + + + +### Derived accessible name + +`getPendingAccessibleName()` prefers an explicit `pendingLabel`. When none is set, it appends `", busy"` to the name returned by `resolveAccessibleName` (for example, `"Save, busy"`), and falls back to `"Busy"` when no name is resolvable. The second host below overrides the derived name with an explicit `pending-label`. + + + +### Focus retained on re-render + +When `pendingActive` transitions from `false` to `true`, the controller calls `host.requestUpdate()`, which triggers a Lit re-render. Because lit-html patches the DOM in place rather than replacing elements, the focused ` + `; + } +} diff --git a/2nd-gen/packages/core/controllers/pending-controller/stories/pending-controller.stories.ts b/2nd-gen/packages/core/controllers/pending-controller/stories/pending-controller.stories.ts new file mode 100644 index 00000000000..65874023b98 --- /dev/null +++ b/2nd-gen/packages/core/controllers/pending-controller/stories/pending-controller.stories.ts @@ -0,0 +1,143 @@ +/** + * 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 { html } from 'lit'; +import type { Meta, StoryObj } from '@storybook/web-components'; + +import './demo-hosts.js'; + +// ──────────────── +// METADATA +// ──────────────── + +const args = { + pending: false, + pendingLabel: '', + label: 'Save', +}; + +const argTypes = { + pending: { + control: 'boolean', + description: + 'Whether the host is busy. The spinner activates after the delay; ARIA updates immediately.', + table: { category: 'Host', defaultValue: { summary: 'false' } }, + }, + pendingLabel: { + control: 'text', + description: + 'Explicit busy label. When empty, the controller derives `"