Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 48 additions & 0 deletions 1st-gen/packages/action-button/src/ActionButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,18 @@ export class ActionButton extends SizedMixin(ButtonBase, {
return [...super.styles, buttonStyles, cornerTriangleStyles];
}

/**
* @deprecated The `emphasized` property is deprecated and will be removed
* in a future release.
*/
@property({ type: Boolean, reflect: true })
public emphasized = false;

/**
* @deprecated Hold affordance support has been deferred. The
* `hold-affordance` attribute and `longpress` event will be removed in a
* future release.
*/
@property({ type: Boolean, reflect: true, attribute: 'hold-affordance' })
public holdAffordance = false;

Expand All @@ -74,6 +83,9 @@ export class ActionButton extends SizedMixin(ButtonBase, {
/**
* Whether an Action Button with `role='button'`
* should also be `aria-pressed='true'`
*
* @deprecated The `selected` property is deprecated and will be removed in
* a future release. Use `swc-toggle-button` for selectable button behavior.
*/
@property({ type: Boolean, reflect: true })
public selected = false;
Expand All @@ -82,6 +94,10 @@ export class ActionButton extends SizedMixin(ButtonBase, {
* Whether to automatically manage the `selected`
* attribute on interaction and whether `aria-pressed="false"`
* should be used when `selected === false`
*
* @deprecated The `toggles` property is deprecated and will be removed in
* a future release. Use `swc-toggle-button` or `swc-toggle-button-group`
* for toggle button behavior.
*/
@property({ type: Boolean, reflect: true })
public toggles = false;
Expand Down Expand Up @@ -241,9 +257,41 @@ export class ActionButton extends SizedMixin(ButtonBase, {
this.setAttribute('aria-expanded', this.selected ? 'true' : 'false');
}
}
if (changes.has('selected') && this.selected && 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-button/',
{ level: 'deprecation' }
);
}
}
if (changes.has('toggles') && this.toggles && window.__swc?.DEBUG) {
window.__swc.warn(
this,
`The "toggles" attribute on <${this.localName}> is deprecated and will be removed in a future release.`,
'https://opensource.adobe.com/spectrum-web-components/components/action-button/',
{ level: 'deprecation' }
);
}
if (changes.has('emphasized') && this.emphasized && 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-button/',
{ level: 'deprecation' }
);
}
if (changes.has('holdAffordance')) {
if (this.holdAffordance) {
if (window.__swc?.DEBUG) {
window.__swc.warn(
this,
`The "hold-affordance" attribute on <${this.localName}> is deprecated and will be removed in a future release.`,
'https://opensource.adobe.com/spectrum-web-components/components/action-button/',
{ level: 'deprecation' }
);
}
this.addEventListener(
'pointerdown',
this.handlePointerdownHoldAffordance
Expand Down
95 changes: 51 additions & 44 deletions 2nd-gen/packages/swc/components/action-button/ActionButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import { CSSResultArray, html, PropertyValues, TemplateResult } from 'lit';
import { CSSResultArray, html, TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
Expand All @@ -26,6 +26,8 @@ import styles from './action-button.css';

/**
* A compact action button for toolbars, action groups, and icon-first chrome.
* Supports sizes `xs`–`xl`; `xs` is an action-button-specific addition not
* available on `swc-button`.
*
* @element swc-action-button
* @since 0.0.1
Expand All @@ -51,51 +53,38 @@ export class ActionButton extends ButtonBase {
static override readonly VALID_SIZES: readonly ActionButtonSize[] =
ACTION_BUTTON_VALID_SIZES;

/**
* Size of the button. Supports the full `xs`–`xl` range; `xs` is an
* action-button-specific addition not available on `swc-button`.
*/
@property({ type: String })
public override get size(): ActionButtonSize {
return this._size ?? 'm';
}

public override set size(value: ActionButtonSize) {
const normalized = (
value ? (value as string).toLocaleLowerCase() : value
) as ActionButtonSize;
const validSize: ActionButtonSize = ACTION_BUTTON_VALID_SIZES.includes(
normalized
)
? normalized
: 'm';
const oldSize = this._size ?? 'm';
if (oldSize === validSize) {
return;
}
this._size = validSize;
this.setAttribute('size', validSize);
this.requestUpdate('size', oldSize);
}

private _size: ActionButtonSize | null = null;

// ───────────────────
// API ADDITIONS
// ───────────────────

/**
* Applies the quiet (low-emphasis) visual treatment.
*/
/** Applies the quiet (low-emphasis) visual treatment. */
@property({ type: Boolean, reflect: true })
public quiet: boolean = false;

/**
* Static color treatment for display over colored or image backgrounds.
*/
/** Static color treatment for display over colored or image backgrounds. */
@property({ type: String, reflect: true, attribute: 'static-color' })
public staticColor?: ActionButtonStaticColor;

/**
* @internal
* Forwarded to the inner `<button>` for menu-trigger patterns. After the
* attribute is read, it is stripped from the host so assistive technologies
* in browse mode do not encounter duplicate ARIA state on both the host
* element and the inner button.
*/
@property({ type: String, attribute: 'aria-haspopup' })
protected ariaHasPopup?: string;

/**
* @internal
* Forwarded to the inner `<button>` for menu-trigger patterns. After the
* attribute is read, it is stripped from the host so assistive technologies
* in browse mode do not encounter duplicate ARIA state on both the host
* element and the inner button.
*/
@property({ type: String, attribute: 'aria-expanded' })
protected ariaExpanded?: string;

// ──────────────────────────────
// RENDERING & STYLING
// ──────────────────────────────
Expand All @@ -104,6 +93,30 @@ export class ActionButton extends ButtonBase {
return [styles];
}

// Guard against re-entrant attributeChangedCallback during ARIA passthrough:
// removeAttribute fires a second callback with value=null; skipping super
// there prevents Lit from clearing the property we just read.
private _ariaForwardingInProgress = false;

/** @internal */
override attributeChangedCallback(
name: string,
old: string | null,
value: string | null
): void {
const isAriaPassthrough =
name === 'aria-haspopup' || name === 'aria-expanded';
if (isAriaPassthrough && this._ariaForwardingInProgress) {
return;
}
super.attributeChangedCallback(name, old, value);
if (isAriaPassthrough && value !== null) {
this._ariaForwardingInProgress = true;
this.removeAttribute(name);
this._ariaForwardingInProgress = false;
}
}

protected override render(): TemplateResult {
return html`
<button
Expand All @@ -122,6 +135,8 @@ export class ActionButton extends ButtonBase {
aria-label=${ifDefined(
this.pending ? this.getPendingAccessibleName() : this.accessibleLabel
)}
aria-haspopup=${ifDefined(this.ariaHasPopup)}
aria-expanded=${ifDefined(this.ariaExpanded)}
>
<slot name="icon"></slot>
<span class="swc-ActionButton-label">
Expand All @@ -130,12 +145,4 @@ export class ActionButton extends ButtonBase {
</button>
`;
}

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');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
| ------------------- | ------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Accordion | ✓ | | | | | | |
| Action Bar | | | | | | | |
| Action Button | ✓ | ✓ | ✓ | | | | |
| Action Button | ✓ | ✓ | ✓ | | | | |
| Action Group | ✓ | | | | | | |
| Action Menu | | | | | | | |
| Alert Banner | ✓ | ✓ | | | | | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ These decisions are derived from the 1st-gen implementation, the current depreca
| `active` | internal | n/a | internal | **Confirmed internal.** Retained as an internal property inherited from `ButtonBase`. With hold-affordance deferred, no consumer-triggered mechanism sets this in initial scope. CSS `:active` on the inner `<button>` handles pressed-state styling without requiring a JS property. |
| `name` | removed | n/a | removed | **Confirmed removal** alongside `type`. `swc-action-button` is not form-associated; `name` has no applicable semantics. |

**Passthrough host attributes:** `aria-haspopup` and `aria-expanded` set on the host element are forwarded to the internal `<button>` via `getForwardedButtonAttributes()` — **Confirmed.** This supports menu trigger patterns (`<swc-action-button aria-haspopup="true" aria-expanded="false">`). No dedicated component property is needed; `ButtonBase` owns the forwarding logic. The forwarding list must be verified and tested in Phase 3 (API).
**Passthrough host attributes:** `aria-haspopup` and `aria-expanded` set on the host element are forwarded to the internal `<button>` and stripped from the host — **Implemented in Phase 3.** `ActionButton` observes both attributes via `@property`, renders them onto the inner `<button>` via `ifDefined`, and overrides `attributeChangedCallback` to remove them from the host after Lit reads them. A `_ariaForwardingInProgress` guard prevents the re-entrant `removeAttribute` callback from clearing the properties. See resolved decisions table.

#### Slots (2nd-gen)

Expand Down Expand Up @@ -468,13 +468,13 @@ What `swc-action-button` adds on top of `ButtonBase`:

#### Naming and semantics

- [ ] Align implementation with [Action button accessibility migration analysis](./accessibility-migration-analysis.md)
- [ ] Icon-only usage requires `accessible-label`; emit `__swc.warn()` when absent
- [ ] Pending state: `aria-disabled="true"` on inner `<button>`, focusable, busy-suffix accessible name (`SWC-459` parity)
- [x] Align implementation with [Action button accessibility migration analysis](./accessibility-migration-analysis.md)
- [x] Icon-only usage requires `accessible-label`; emit `__swc.warn()` when absent (inherited from `ButtonBase.update()`)
- [x] Pending state: `aria-disabled="true"` on inner `<button>`, focusable, busy-suffix accessible name (inherited from `ButtonBase`)
- [ ] Pending state: animated icon only, never `swc-progress-circle` for inline pending
- [ ] Pending state: no `aria-live="assertive"`; consumers use `role="status"` externally if needed
- [ ] Menu trigger: forward `aria-haspopup` / `aria-expanded` from host to the internal `<button>`
- [ ] No `aria-pressed`, `role="radio"`, or `role="checkbox"` on `swc-action-button`
- [x] Menu trigger: forward `aria-haspopup` / `aria-expanded` from host to the internal `<button>`
- [x] No `aria-pressed`, `role="radio"`, or `role="checkbox"` on `swc-action-button`
- [ ] Confirm host element is not separately announced by AT alongside the internal button

#### State verification
Expand Down Expand Up @@ -613,7 +613,7 @@ What `swc-action-button` adds on top of `ButtonBase`:

| # | Decision | Resolution |
|---|---|---|
| — | None resolved yet | — |
| Phase 3 | `aria-haspopup` / `aria-expanded` host-attribute retention | Resolved in Phase 3 (PR feedback). `ActionButton.attributeChangedCallback` override strips both attributes from the host after Lit reads them, forwarding values to the inner `<button>` via `ariaHasPopup` / `ariaExpanded` properties. Guard flag `_ariaForwardingInProgress` prevents re-entrant clearing on the `removeAttribute` callback. |

### Deferred follow-up tickets

Expand All @@ -626,6 +626,9 @@ What `swc-action-button` adds on top of `ButtonBase`:
| TBD | Badge slot and corner-overlay lockup (A6) | Icon+Badge and Avatar+Badge produce a distinct visual lockup. Badge text accessible name composition requires a11y review (@nikkimk) before shipping. | [A6](#additive--ships-when-ready-zero-breakage-for-consumers-already-on-2nd-gen) |
| TBD | Avatar slot and accessible name composition (A7) | Avatar accessible name and its relationship to the button's composite accessible name requires a11y review (@nikkimk) before shipping. | [A7](#additive--ships-when-ready-zero-breakage-for-consumers-already-on-2nd-gen) |
| TBD (under SWC-2039) | Cross-root ARIA mapping | Shared with `swc-button` dependency on `ElementInternals` / tooling path. | [Deferred semantics note](#deferred-semantics-note-2nd-gen) |
| ~~Phase 4~~ | ~~`aria-haspopup` / `aria-expanded` host-attribute retention~~ | Resolved in Phase 3. See resolved decisions table. | [2nd-gen API decisions — passthrough host attributes](#properties--attributes) |
| Phase 5 | Pending width-lock CSS property name | `ButtonBase.update()` sets `--_swc-button-pending-inline-size` on the inner `<button>` when the pending delay fires. Phase 5 action-button CSS must reference this same property name for the width-lock to activate, not `--_swc-action-button-pending-inline-size`. If a component-scoped name is preferred, `ActionButton` must override the pending timer logic to write under the action-button name instead. | [Pending state](#pending-state-new-in-2nd-gen) |
Comment thread
cdransf marked this conversation as resolved.
| Phase 5 | Shared pending stylesheet (`button-pending.css`) | Consider extracting pending-state styles into a standalone `button-pending.css` alongside the existing `button-base.css` shareable stylesheet. `swc-action-button` and `swc-button` both implement the pending visual; shared styles (spinner keyframes, width-lock, reduced-motion, WHCM) should not be duplicated. Evaluate during Phase 5 when both stylesheets are being authored. | [Pending state](#pending-state-new-in-2nd-gen) |

---

Expand Down
Loading