From 057a70aab239cd7d5941aabd98a4fc6f9be2a4d4 Mon Sep 17 00:00:00 2001 From: rise-erpelding <54716846+rise-erpelding@users.noreply.github.com> Date: Thu, 14 May 2026 15:29:26 -0500 Subject: [PATCH 1/2] docs(accordion): refine planning docs (#6269) Co-authored-by: Nikki Massaro --- .../03_components/README.md | 1 - .../accessibility-migration-analysis.md | 62 ++++- .../03_components/accordion/migration-plan.md | 38 +-- ...endering-and-styling-migration-analysis.md | 225 ------------------ 4 files changed, 77 insertions(+), 249 deletions(-) delete mode 100644 CONTRIBUTOR-DOCS/03_project-planning/03_components/accordion/rendering-and-styling-migration-analysis.md diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md index 67bc9e67ca..789b325014 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md @@ -21,7 +21,6 @@ - Accordion - [Accordion accessibility migration analysis](accordion/accessibility-migration-analysis.md) - [Accordion migration plan](accordion/migration-plan.md) - - [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 roadmap](action-button/rendering-and-styling-migration-analysis.md) diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/accordion/accessibility-migration-analysis.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/accordion/accessibility-migration-analysis.md index daf1ef1006..1ff4994bb1 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/03_components/accordion/accessibility-migration-analysis.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/accordion/accessibility-migration-analysis.md @@ -23,6 +23,7 @@ - [Related 1st-gen accessibility (Jira)](#related-1st-gen-accessibility-jira) - [Recommendations: `` / ``](#recommendations-swc-accordion--swc-accordion-item) - [ARIA roles, states, and properties](#aria-roles-states-and-properties) + - [Header label, optional actions, and Figma vs React Spectrum](#header-label-optional-actions-and-figma-vs-react-spectrum) - [Shadow DOM and cross-root ARIA Issues](#shadow-dom-and-cross-root-aria-issues) - [Accessibility tree expectations](#accessibility-tree-expectations) - [Keyboard and focus](#keyboard-and-focus) @@ -37,14 +38,16 @@ ## Overview -This doc explains how **`swc-accordion`** and **`swc-accordion-item`** should behave for **accessibility**. The target is **WCAG 2.2 Level AA**. It complements the accordion **rendering-and-styling** migration doc and reflects the **accordion** pattern in the [ARIA Authoring Practices Guide (APG)](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/), Deque’s **single** vs **multiselect** examples, and [Heydon Pickering’s collapsible sections](https://inclusive-components.design/collapsible-sections/) guidance (**` +
+
+ ``` + + Any content inside `` — including a sibling div to the button — can bleed into the heading's accessible name, so the actions container must live outside the heading element entirely. **Tab** order stays **disclosure** → **slotted actions** → **panel** when expanded, matching intent from the design thread. +- **Slots:** one for the **section label** (projected into the disclosure **` + +
The bellows is the expandable section in the middle of the accordion.
+
+``` + +- If **`aria-describedby`** targets live in **light DOM** and the disclosure **` + + +
@@ -182,7 +188,7 @@ Inherited: `SizedMixin(Focusable)` — `tabIndex` / `focus` / `blur` / `click` d |---|---| | **Upstream 2nd-gen components** | Accordion does not require another incomplete 2nd-gen composite; it uses **core** + **base** + **icons** patterns. Follow the [badge migration reference](../../02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_washing-machine-workflow.md#reference-badge-migration) for core/SWC layout. | | **Cross-component API alignment** | **`level`** / heading naming should stay aligned with **illustrated message** and **card** when those specs exist ([accessibility migration analysis](./accessibility-migration-analysis.md)). **`quiet`** / host **`disabled`** naming should match other 2nd-gen components that expose the same Spectrum concepts ([React Spectrum alignment considerations](#react-spectrum-alignment-considerations)). | -| **Step 1 (analyze rendering and styling)** | [Rendering roadmap](./rendering-and-styling-migration-analysis.md) is **in progress** — finish S2 selector and token pass with **spectrum-css** `spectrum-two` beside this repo before treating Step 1 as complete. | +| **Step 1 (analyze rendering and styling)** | Complete — key findings and DOM changes are captured in this plan. | --- @@ -227,18 +233,20 @@ No 2nd-gen package yet — this section records **planned** decisions from analy |---|---|---| | Accordion | `allow-multiple` (or aligned name), **public** **`level`** (`2`–`6`), **`density`**, `size` | **`level`** is the **only** author-facing control for heading depth for all items. **`size`** propagates to assigned items (same as 1st-gen). See **`density`** row. | | Accordion — `density` | Reflected string **`compact`** \| **`regular`** \| **`spacious`** | Align with [React Spectrum **`density`**](https://react-spectrum.adobe.com/Accordion) and S2: **`regular`** is the default spacing (1st-gen **omitted** / legacy default maps here). **TypeScript** and docs should list **all three** values even though **`regular`** is default. **Dev warning** when the attribute is **omitted** is **recommended** (same spirit as Badge **`variant`**) so authors stay explicit—confirm at API freeze. Host-only (1st-gen does **not** assign **`density`** on **`AccordionItem`** in script). | -| Accordion — `quiet` | Boolean; reflected attribute **`quiet`** | Parity with [React Spectrum **`isQuiet`**](https://react-spectrum.adobe.com/Accordion). **Accordion host only** — propagate effective quiet styling to assigned items internally. **Do not** expose **per-item** **`quiet`**: quiet removes dividers between rows; mixing styles per item is **visually chaotic** and **contradicts** Figma usage guidance ([rendering-and-styling migration analysis — Figma](./rendering-and-styling-migration-analysis.md#figma--s2-web-desktop-scale)). | +| Accordion — `quiet` | Boolean; reflected attribute **`quiet`** | Parity with [React Spectrum **`isQuiet`**](https://react-spectrum.adobe.com/Accordion). **Accordion host only** — propagate effective quiet styling to assigned items internally. **Do not** expose **per-item** **`quiet`**: mixing default and quiet items is **visually incompatible**; the quiet hover state uses rounded corners, which creates corner gaps when placed inside a default accordion that uses dividers. Prefer one style family per accordion instance. | | Accordion — `disabled` | Boolean; reflected attribute **`disabled`** | Parity with RS **`isDisabled`** on **`Accordion`**: **accordion-wide** disable — every item non-interactive (no expand/collapse), same **a11y** posture as item-level disable ([accessibility migration analysis](./accessibility-migration-analysis.md): header **`aria-disabled`**, panel **`inert`**). When the host is **`disabled`**, that gate **wins** over per-item **`disabled`** being false. When the host clears **`disabled`**, each item’s own **`disabled`** applies again unchanged. For **visual** disabled state on descendants, prefer **container queries** or host-driven styling so you do **not** reflect host **`disabled`** onto every child **solely** for CSS—only use per-item flags where behavior or a11y requires it. | | Item | `open`, `disabled` | Same semantics as today unless renamed for consistency. **No** public **`quiet`** on the item. | | Item (implementation) | **`protected` `heading`** (`2`–`6`) | **Not** public API—not reflected, not set by consumers. Parent **`level`** assigns **`heading`** on each slotted item (core/SWC lifecycle). | | Heading text | Slotted (see [Shadow DOM output](#shadow-dom-output-rendered-html)) | **Rationale:** a string **`label`** cannot mirror phrasing content (``, ``) into the header’s accessible name the way slotted light DOM can; matches [accessibility migration analysis](./accessibility-migration-analysis.md). **Breaking** vs 1st-gen **`label`**: **clean break** — 2nd-gen does **not** expose **`label`**; authors migrate markup to the heading slot only. | | Events | Renamed toggle event | Exact string TBD. | +| Direct actions (item header affordances) | `slot="actions"` on `swc-accordion-item` (working name — not frozen); open-ended, any content may be slotted | (spectrum-css container class `.spectrum-Accordion-itemDirectActions`) rendered as a **sibling to the `` element** (not inside it); placing it inside `` would bleed its text content into the heading's accessible name. `slotchange` observer hides the container when empty. `stopPropagation` on the container prevents slot clicks from toggling the accordion. Specific supported content (`swc-action-button`, `swc-switch`) are open questions; see [Open — API and scope](#open--api-and-scope). | +| `noInlinePadding` modifier | Not a public attribute | The spectrum-css modifier class `.spectrum-Accordion--noInlinePadding` controls inline padding removal (new in S2). **Not** exposed as an API attribute — expose the relevant padding via `--swc-accordion-*` custom properties so consumers can remove or adjust inline padding directly. | ### React Spectrum alignment considerations [React Spectrum S2 — Accordion](https://react-spectrum.adobe.com/Accordion) exposes **`isQuiet`** and **`isDisabled`** on the **`Accordion`** root (and **`isQuiet`** / **`isDisabled`** on **`AccordionItem`**). 1st-gen **`sp-accordion`** has **neither** host **`quiet`** nor host **`disabled`**; only **`sp-accordion-item`** supports **`disabled`**. For 2nd-gen, treat the following as **API planning targets** so Spectrum authors can mirror React examples in markup: -1. **`quiet`:** Map RS **`isQuiet`** to boolean **`quiet`** on **`swc-accordion`** (Spectrum 2 CSS uses the same name). Propagate styling to items **internally**; **do not** add a public **per-item** **`quiet`** API ([rendering-and-styling migration analysis](./rendering-and-styling-migration-analysis.md#figma--s2-web-desktop-scale)). +1. **`quiet`:** Map RS **`isQuiet`** to boolean **`quiet`** on **`swc-accordion`** (Spectrum 2 CSS uses the same name). Propagate styling to items **internally**; **do not** add a public **per-item** **`quiet`** API. 2. **Accordion-wide `disabled`:** Map RS **`isDisabled`** on **`Accordion`** to **`disabled`** on **`swc-accordion`**. Implementation should drive the same behavior as “every item disabled” without authors having to set **`disabled`** on each item: block toggles, apply quiet/disabled visuals per design, and apply the **disabled item** a11y matrix on **every** header/panel pair. **Controlled-mode** authors who set **`open`** on items should not be able to expand while the host stays **`disabled`** (treat like item-level guardrails today, extended to the whole set). @@ -269,7 +277,6 @@ For **`allow-multiple` false**, the parent must keep **at most one** item **`ope | Document | Use | |---|---| | [Accessibility migration analysis](./accessibility-migration-analysis.md) | WCAG 2.2 AA target, disabled matrix, keyboard, testing expectations. | -| [Rendering-and-styling migration analysis](./rendering-and-styling-migration-analysis.md) | S2 CSS and token mapping — **in progress**; complete S2 sections before Phase 4. | | [Washing machine workflow](../../02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_washing-machine-workflow.md) | Phase order and quality gates. | --- @@ -294,7 +301,7 @@ Gates align with [01_washing-machine-workflow.md](../../02_workstreams/02_2nd-ge ### Preparation (this ticket) - [ ] Preparation inputs tracked (Figma, epic id, S2 rendering pass) per team process—no separate contributor prep doc for accordion -- [ ] This plan + accessibility analysis + expanded rendering roadmap reviewed +- [ ] This plan + accessibility analysis reviewed - [ ] Breaking changes and consumer migration notes agreed - [ ] Open questions in [Blockers and open questions](#blockers-and-open-questions) resolved or ticketed - [ ] Plan reviewed by at least one other engineer @@ -313,7 +320,7 @@ Gates align with [01_washing-machine-workflow.md](../../02_workstreams/02_2nd-ge ### Styling - [ ] S2 CSS integrated; stylelint clean -- [ ] Document token / `--mod-*` → S2 (or `--swc-*`) mapping for consumers; include **`density` × `size`** matrix ([rendering-and-styling migration analysis](./rendering-and-styling-migration-analysis.md#summary-of-changes)) +- [ ] Document token / `--mod-*` → S2 (or `--swc-*`) mapping for consumers; include **`density` × `size`** matrix ### Accessibility @@ -329,7 +336,8 @@ Gates align with [01_washing-machine-workflow.md](../../02_workstreams/02_2nd-ge ### Documentation -- [ ] JSDoc, usage docs, Storybook stories (include a spacing / **custom properties** story for “no inline padding” style parity—**no** **`show paddings`**-style attribute; see [rendering roadmap — Figma](./rendering-and-styling-migration-analysis.md#figma--s2-web-desktop-scale)) +- [ ] JSDoc, usage docs, Storybook stories (include a spacing / **custom properties** story for “no inline padding” style parity; **no** `noInlinePadding`-style attribute) +- [ ] Add a Storybook story demonstrating inline padding customization via `--swc-accordion-*` custom properties (no `noInlinePadding`-style attribute) - [ ] Do not document arrow-key navigation between headers for 2nd-gen (contrast with legacy README) ### Review @@ -348,11 +356,12 @@ Gates align with [01_washing-machine-workflow.md](../../02_workstreams/02_2nd-ge | **Standalone item** | **Direction:** **`swc-accordion-item`** without a parent stays **supported** with reasonable defaults (**`protected` `heading`** defaults to **`3`**, same as today’s accordion default)—matches existing tests and story patterns. Ticket any change if product requires parent-only usage. | | Heading slot content | Text-only vs inline phrasing (``, ``) in heading slot. | | Toggle event | Exact `swc-*` event name. | -| Rendering doc | Who expands [rendering-and-styling migration analysis](./rendering-and-styling-migration-analysis.md) with S2 paths before Phase 4? | | Chevron / disclosure icon | Prefer **`swc-icon`** internally; finalize icon asset/name against S2. | | Accordion host **`disabled`** | Confirm **controlled** **`open`** cannot expand while host **`disabled`**; prefer **container-query** / host styling for descendant disabled visuals ([Public API](#public-api) **`disabled`** note). | | **`density`** dev warning | Confirm **omit-attribute** warning ships with accordion (recommended; same spirit as Badge **`variant`**). | -| **`RadioController`** scope | Ship **inside** `AccordionBase` first vs extract to **core** for **radio group**, **`role="menuitemradio"`** menus, **tabs**, and/or coordinate with a **refactor** of **`FocusgroupNavigationController`** (split “selection flags” vs “focus roving”)? **Depends on** menu / radio / tabs migration timing and whether teams want one shared **selection-sync** API vs local loops per component. | +| **Direct actions — disabled state** | **Decided:** Do **not** propagate `disabled` to slotted actions content. The actions slot may hold affordances whose purpose is precisely to explain or resolve why the item is disabled (e.g., an “Upgrade” button, or an action button attached to a popover describing deprecation). Coupling disablement would remove those affordances exactly when they are most needed. | +| **Direct actions — content constraints** | Slot is open-ended by design. Confirm whether to add a dev-mode warning for unsupported content types when `swc-action-button` and `swc-switch` are available, or leave fully unrestricted. | +| **`RadioController`** scope | Ship **inside** `AccordionBase` first vs extract to **core** for **radio group**, **`role=”menuitemradio”`** menus, **tabs**, and/or coordinate with a **refactor** of **`FocusgroupNavigationController`** (split “selection flags” vs “focus roving”)? **Depends on** menu / radio / tabs migration timing and whether teams want one shared **selection-sync** API vs local loops per component. | **Not a blocker:** Missing 2nd-gen package before implementation starts is expected. @@ -370,7 +379,6 @@ Gates align with [01_washing-machine-workflow.md](../../02_workstreams/02_2nd-ge - [Washing machine workflow](../../02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_washing-machine-workflow.md) - [Migration project planning (epics / tickets)](../../02_workstreams/02_2nd-gen-component-migration/03_migration-project-planning.md) - [Accessibility migration analysis](./accessibility-migration-analysis.md) -- [Rendering and styling migration analysis](./rendering-and-styling-migration-analysis.md) - [1st-gen source — `Accordion.ts`](../../../../1st-gen/packages/accordion/src/Accordion.ts) - [1st-gen source — `AccordionItem.ts`](../../../../1st-gen/packages/accordion/src/AccordionItem.ts) - [1st-gen tests directory](../../../../1st-gen/packages/accordion/test/) diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/accordion/rendering-and-styling-migration-analysis.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/accordion/rendering-and-styling-migration-analysis.md deleted file mode 100644 index 5f32eaa712..0000000000 --- a/CONTRIBUTOR-DOCS/03_project-planning/03_components/accordion/rendering-and-styling-migration-analysis.md +++ /dev/null @@ -1,225 +0,0 @@ - - -[CONTRIBUTOR-DOCS](../../../README.md) / [Project planning](../../README.md) / [Components](../README.md) / Accordion / Accordion migration roadmap - - - -# Accordion migration roadmap - - - -
-In this doc - -- [Component specifications](#component-specifications) - - [CSS (1st-gen / Spectrum 1)](#css-1st-gen--spectrum-1) - - [Figma — S2 Web (desktop scale)](#figma--s2-web-desktop-scale) - - [SWC (1st-gen)](#swc-1st-gen) -- [Comparison](#comparison) - - [DOM structure changes](#dom-structure-changes) - - [CSS => SWC mapping](#css--swc-mapping) -- [Summary of changes](#summary-of-changes) -- [Resources](#resources) - -
- - - -Spectrum 2 CSS-to-SWC migration notes for **`swc-accordion`** and **`swc-accordion-item`**. For accessibility behavior (APG accordion pattern, headings, keyboard), see [Accordion accessibility migration analysis](./accessibility-migration-analysis.md). - -**Status:** Phase 1 prep — 1st-gen inventory captured below. **Next:** With **spectrum-css** checked out on **`spectrum-two`** beside this repo, complete selector extraction from component `metadata.json`, three-way DOM comparison, and token mapping per [Analyze rendering and styling](../../02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_analyze-rendering-and-styling/README.md). - ---- - -## Component specifications - -### CSS (1st-gen / Spectrum 1) - -1st-gen packages import generated Spectrum CSS: - -- [`1st-gen/packages/accordion/src/spectrum-accordion.css`](../../../../1st-gen/packages/accordion/src/spectrum-accordion.css) — host tokens, size/density variants, `--spectrum-logical-rotation` for disclosure. -- [`1st-gen/packages/accordion/src/spectrum-accordion-item.css`](../../../../1st-gen/packages/accordion/src/spectrum-accordion-item.css) — `:host`, `#heading`, `#header`, `#content`, `.iconContainer`, state variants (`[open]`, `[disabled]`, hover, focus-visible). - -
-Modifier highlights (`--mod-*` / `--spectrum-accordion-*`) - -Representative tokens consumed in 1st-gen (not exhaustive — extract full list from Spectrum CSS when completing this doc): - -- `--mod-accordion-item-width`, `--mod-accordion-item-height`, `--mod-accordion-min-block-size` -- `--mod-accordion-divider-color`, `--mod-accordion-divider-thickness` -- `--mod-accordion-disclosure-indicator-height`, `--mod-accordion-edge-to-disclosure-indicator-space`, `--mod-accordion-disclosure-indicator-to-text-space`, `--mod-accordion-edge-to-text-space` -- `--mod-accordion-item-header-*` (padding, font, line-height, colors) -- `--mod-accordion-item-content-*` (padding, font, color) -- `--mod-accordion-background-color-*`, `--mod-accordion-corner-radius`, `--mod-accordion-focus-indicator-*` -- System overrides in [`accordion-overrides.css`](../../../../1st-gen/packages/accordion/src/accordion-overrides.css) - -
- -**TBD (Spectrum 2):** Mirror this section from `spectrum-css` **`spectrum-two`** accordion component (`metadata.json`, `index.css`, stories template) when the sibling checkout is available. Use [Figma — S2 Web (desktop scale)](#figma--s2-web-desktop-scale) below as the **design** source until that extraction is complete; reconcile any delta with `metadata.json` line by line (do not assume Figma and CSS stay in lockstep without verification). - -### Figma — S2 Web (desktop scale) - -**Canonical link (dev mode):** [S2 — Web (Desktop scale) — Accordion](https://www.figma.com/design/Mngz9H7WZLbrCvGQf3GnsY/S2---Web--Desktop-scale-?node-id=39469-5419&p=f&m=dev) (`node-id=39469-5419`). - -The following is transcribed from the **published Accordion** page in that file (export reviewed May 2025; doc stamp **Last updated May 16, 2025** / Kami F.). Treat Figma as **visual and variant inventory**; **coded** selectors and tokens still come from **spectrum-css** `spectrum-two` when available. - -#### Definition - -An accordion shows a list of items that can be expanded or collapsed to reveal more content. The pattern can support **zero, one, or multiple** expanded items at a time (aligns quantity-of-open with product API such as [`allowsMultipleExpanded`](https://react-spectrum.adobe.com/Accordion), not with “quiet vs default” styling). - -
-Accordion (root) — properties in Figma - -| Figma property | Role (from doc copy) | -|---|---| -| **Quiet** | Boolean — change appearance / communication of status | -| **Variant** | Variant control on the accordion set (see item matrix for spacing/style family) | - -Figma defaults shown in the property table include **Quiet = False** for the accordion-level control where listed. - -
- -
-Accordion item — properties in Figma - -| Figma property | Role (from doc copy) | -|---|---| -| **State** | **Open** (and interactive states shown in grids — **Hover**, **Down**, **Disabled**) | -| **Quiet** | Boolean — aligns with accordion “quiet” visual family | -| **Density** | **Compact**, **Regular**, **Spacious** — “change density” between items | -| **Show paddings** | Toggle — show/hide padding guides (design-tool only; corresponds to **no inline padding** style in Spectrum CSS — **not** a public SWC prop) | -| **Show direct actions** | Toggle — show/hide **direct actions** region (optional chrome next to title) | -| **Show switch** | Toggle — show/hide **switch** in the header row | -| **Show action button** | Toggle — show/hide **action button** | -| **@ Title** | Text — title string | -| **Instance swap** | Local component swaps where applicable | - -**Migration implication:** Optional header affordances (**direct actions**, **switch**, **action button**) match the direction of richer headers in React Spectrum S2 (**`AccordionItemHeader`**, action controls). 1st-gen SWC uses a single header label + chevron only; **2nd-gen** may need **named slots** or internal structure once S2 template and a11y review land (see [migration plan](./migration-plan.md) and [accessibility analysis](./accessibility-migration-analysis.md)). - -**Density note:** Figma places **Density** on the **item** component with three steps (**Compact** / **Regular** / **Spacious**). 1st-gen reflects **`density`** on **`sp-accordion`** only (`compact` \| `spacious` \| unset). Reconcile host vs item during Step 1 when comparing `metadata.json` to this file — [migration plan — `density`](./migration-plan.md#public-api) defines reflected **`regular`** and full typing parity with React Spectrum. - -**Show paddings:** Treat as documentation and **CSS custom-property** coverage only ([migration plan](./migration-plan.md) — spacing overrides + a dedicated Storybook story, **no** component attribute). - -
- -
-Sizes, states, and styles (Figma matrices) - -- **Sizes:** **S**, **M**, **L**, **XL** (maps to existing SWC **`size`** scale). -- **States (interactive):** **Default**, **Hover**, **Down**, **Disabled** across the published grids. -- **Style families:** **Default** vs **Quiet**, crossed with **open/closed** and **density** rows (**Compact**, **Regular**, **Spacious**) in the canvases. - -**Figma caveat:** Keyboard **focus** state is **not** represented in the file; the spec points authors to Spectrum **coded** components and site docs for focus treatment. - -
- -#### Usage guidelines (from Figma — do not mix) - -- **Do not mix default and quiet accordion items inside one accordion.** Default accordions must not contain quiet items, and quiet accordions must not contain default items — the doc states this avoids **conflicting interaction behaviors**. -- **Quiet hover and dividers:** The quiet accordion item hover state uses **rounded corners**. Using that inside a **default** accordion (with dividers) produces **gaps at corners** that the default divider treatment does not fill; only **keyboard focus** outline is expected to read similarly against dividers. **Prefer one style family per accordion** for both UX and markup/CSS predictability. - -This reinforces **accordion-wide `quiet`** (and consistent items) as the primary authoring model for 2nd-gen; avoid advertising **per-item `quiet`** that could violate this guidance unless Spectrum explicitly documents an exception ([migration plan](./migration-plan.md)). - -### SWC (1st-gen) - -
-Attributes / properties (`sp-accordion`) - -- `allow-multiple` (boolean) -- `density` (`compact` | `spacious`) -- `level` (number, default 3) -- `size` (`s` | `m` | `l` | `xl`) - -
- -
-Attributes / properties (`sp-accordion-item`) - -- `open` (boolean) -- `label` (string) -- `disabled` (boolean) -- `level` (number — overwritten by parent when slotted) -- `size` (from parent) - -
- -
-Slots - -- **`sp-accordion`:** default — `sp-accordion-item` children. -- **`sp-accordion-item`:** default — panel body. - -
- -
-Nested components / assets - -- 1st-gen: `sp-icon-chevron100` from `@spectrum-web-components/icons-ui` and chevron styles from `@spectrum-web-components/icon` -- 2nd-gen: prefer **`swc-icon`** internally for the disclosure indicator ([migration plan](./migration-plan.md)) - -
- ---- - -## Comparison - -### DOM structure changes - -
-Spectrum Web Components (1st-gen `AccordionItem`) - -Conceptual shadow output: - -```html -

- - -

-
- -
-``` - -**`sp-accordion`:** single default ``; parent coordinates items via events and assigned nodes. - -
- -
-Spectrum 2 (TBD — CSS template) - -Paste or link the S2 template markup from **spectrum-css** `spectrum-two` when available. Compare heading/button/slot structure and class names to the above. - -**Design reference (until template is pasted):** [Figma — S2 Web (desktop scale)](#figma--s2-web-desktop-scale) — sizes **S–XL**, **Default/Quiet**, **Compact/Regular/Spacious** density, optional header actions/switch/button, and **do-not-mix** quiet vs default usage. - -
- -### CSS => SWC mapping - -**TBD.** Populate during Step 1 QA: map S2 selectors and `--mod-*` successors to 2nd-gen host parts, internal nodes, and any supported `--swc-accordion-*` surface per [component custom property exposure](../../../02_style-guide/01_css/02_custom-properties.md#component-custom-property-exposure). - ---- - -## Summary of changes - -| Area | 1st-gen today | 2nd-gen direction (high level) | -|---|---|---| -| Styling source | Spectrum 1 generated CSS in package | Spectrum 2 tokens from **spectrum-css** `spectrum-two`; narrow public customization. | -| DOM | `#heading` / `#header` / `#content`, optional chevron wrapper | Align with S2 template; preserve APG shape from [accessibility migration analysis](./accessibility-migration-analysis.md). | -| Quiet vs default | No `quiet` on accordion or item | **Figma / S2:** accordion-level **Quiet** and item-level quiet must **not** be mixed with the opposite style inside one accordion ([Figma section](#figma--s2-web-desktop-scale)). Implement **`quiet`** as a single family per instance ([migration plan](./migration-plan.md)). | -| Header chrome | Title + chevron only | **Figma** shows optional **direct actions**, **switch**, **action button** toggles — likely **slots** or subregions; align with [React Spectrum `AccordionItemHeader`](https://react-spectrum.adobe.com/Accordion) when scoping Phase 5/7. | -| Density / size | `density` + `size` on accordion, chevron scales by size | **Figma** item **Density:** **Compact** / **Regular** / **Spacious**; sizes **S–XL**. Reconcile host vs item propagation with `metadata.json` + [migration plan `density`](./migration-plan.md#public-api). | -| States | `open`, `disabled`, hover/focus in CSS | Match **Default / Hover / Down / Disabled** from Figma; **focus-visible** not in Figma — follow APG + [accessibility analysis](./accessibility-migration-analysis.md). | -| Custom properties | Broad `--spectrum-accordion-*` / `--mod-*` | Replace with S2 equivalents; document breaking token renames in [migration plan](./migration-plan.md). | - ---- - -## Resources - -| Resource | Link | -|---|---| -| Figma — S2 Web (Desktop scale), Accordion | [figma.com/design/Mngz9H7WZLbrCvGQf3GnsY](https://www.figma.com/design/Mngz9H7WZLbrCvGQf3GnsY/S2---Web--Desktop-scale-?node-id=39469-5419&p=f&m=dev) | -| 1st-gen accordion package | [`1st-gen/packages/accordion/`](../../../../1st-gen/packages/accordion/) | -| Migration plan | [migration-plan.md](./migration-plan.md) | -| Accessibility analysis | [accessibility-migration-analysis.md](./accessibility-migration-analysis.md) | -| Analyze rendering and styling (workflow) | [README](../../02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_analyze-rendering-and-styling/README.md) | -| Spectrum CSS (external) | [github.com/adobe/spectrum-css](https://github.com/adobe/spectrum-css) — use **`spectrum-two`** branch | From f74121329212a112f507d6ac6c95fd5971ccd3b3 Mon Sep 17 00:00:00 2001 From: rise-erpelding <54716846+rise-erpelding@users.noreply.github.com> Date: Thu, 21 May 2026 15:43:09 -0500 Subject: [PATCH 2/2] feat: set up accordion file structure, API, and a11y (#6300) * chore: scaffold core package * chore: scaffold swc package * chore: add Chevron300Icon for xl accordion * chore: set up storybook story * chore: commit preview.ts updates * feat: implement AccordionItem API and render template * feat: implement Accordion API and propagation * feat: implement open and toggle logic * feat: wire up dynamic heading levels * feat: implement disabled item functionality * fix: handle space keyboard behavior (SWC-1487) * refactor: address missing migration-setup items * refactor: rename methods, align file names * refactor: typing, heading rendering, sizing * refactor: move toggle() into base, smooth a11y behavior * refactor: use assignedItems() helper * test: add a11y tests * docs: add a11y docs * fix: freeze accordion open state when disabled * feat: 1st-gen deprecation warnings/tests * docs: jsdoc comments * chore: changeset * fix: revert 1st-gen refactor work * chore(accordion): defer tests, deprecations, and changeset to part two Co-Authored-By: Claude Sonnet 4.6 * fix: class selectors * refactor: use ObserveSlotPresence instead of bespoke method * refactor: remove stop propagation on actions container * refactor: add open/close events --------- Co-authored-by: Claude Sonnet 4.6 --- .../components/accordion/Accordion.base.ts | 208 ++++++++++++++++++ .../components/accordion/Accordion.types.ts | 33 +++ .../accordion/AccordionItem.base.ts | 183 +++++++++++++++ .../core/components/accordion/index.ts | 15 ++ 2nd-gen/packages/core/package.json | 7 + 2nd-gen/packages/swc/.storybook/preview.ts | 6 +- .../swc/components/accordion/Accordion.ts | 45 ++++ .../swc/components/accordion/AccordionItem.ts | 149 +++++++++++++ .../swc/components/accordion/accordion.css | 13 ++ .../swc/components/accordion/index.ts | 14 ++ .../accordion/stories/accordion.stories.ts | 127 +++++++++++ .../accordion/swc-accordion-item.ts | 23 ++ .../swc/components/accordion/swc-accordion.ts | 23 ++ .../swc/components/accordion/test/.gitkeep | 1 + .../icon/elements/Chevron300Icon.ts | 22 ++ .../swc/components/icon/elements/index.ts | 1 + 16 files changed, 865 insertions(+), 5 deletions(-) create mode 100644 2nd-gen/packages/core/components/accordion/Accordion.base.ts create mode 100644 2nd-gen/packages/core/components/accordion/Accordion.types.ts create mode 100644 2nd-gen/packages/core/components/accordion/AccordionItem.base.ts create mode 100644 2nd-gen/packages/core/components/accordion/index.ts create mode 100644 2nd-gen/packages/swc/components/accordion/Accordion.ts create mode 100644 2nd-gen/packages/swc/components/accordion/AccordionItem.ts create mode 100644 2nd-gen/packages/swc/components/accordion/accordion.css create mode 100644 2nd-gen/packages/swc/components/accordion/index.ts create mode 100644 2nd-gen/packages/swc/components/accordion/stories/accordion.stories.ts create mode 100644 2nd-gen/packages/swc/components/accordion/swc-accordion-item.ts create mode 100644 2nd-gen/packages/swc/components/accordion/swc-accordion.ts create mode 100644 2nd-gen/packages/swc/components/accordion/test/.gitkeep create mode 100644 2nd-gen/packages/swc/components/icon/elements/Chevron300Icon.ts diff --git a/2nd-gen/packages/core/components/accordion/Accordion.base.ts b/2nd-gen/packages/core/components/accordion/Accordion.base.ts new file mode 100644 index 0000000000..883ad39f1e --- /dev/null +++ b/2nd-gen/packages/core/components/accordion/Accordion.base.ts @@ -0,0 +1,208 @@ +/** + * 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 } 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 { + ACCORDION_VALID_SIZES, + type AccordionDensity, + type AccordionHeadingLevel, + type AccordionSize, + SWC_ACCORDION_ITEM_TOGGLE_EVENT, +} from './Accordion.types.js'; +import { AccordionItemBase } from './AccordionItem.base.js'; + +/** + * Base class for accordion components. Manages item propagation, heading + * level, density, and the exclusive-open constraint. + * + * @attribute {boolean} allowMultiple - Reflected as `allow-multiple`. When set, + * multiple items may be open at the same time. + * @attribute {number} level - Heading level (2–6) applied to every item header. + * Values outside that range are clamped. + * @attribute {AccordionSize} size - Size applied to all items. + * @attribute {AccordionDensity} density - Vertical spacing between items. + * @attribute {boolean} quiet - Renders the accordion in its quiet visual variant. + * @attribute {boolean} disabled - Disables all items in the accordion. + * + * @slot - One or more `swc-accordion-item` elements. + */ +export abstract class AccordionBase extends SizedMixin(SpectrumElement, { + validSizes: ACCORDION_VALID_SIZES, + defaultSize: 'm', +}) { + // ────────────────── + // PUBLIC API + // ────────────────── + + /** + * When set, multiple items may be open at the same time. By default only + * one item can be open. + */ + @property({ type: Boolean, reflect: true, attribute: 'allow-multiple' }) + public allowMultiple: boolean = false; + + /** + * Heading level applied to every item header (2–6). Defaults to 3. + * Values outside that range are clamped. + */ + @property({ type: Number, reflect: true }) + public level: AccordionHeadingLevel = 3; + + /** + * Size applied to all items. Defaults to `m`. + */ + declare public size: AccordionSize; + + /** + * Controls vertical spacing between items. + * + * @default regular + */ + @property({ type: String, reflect: true }) + public density: AccordionDensity = 'regular'; + + /** + * Renders the accordion in its quiet (no-border) visual variant. + */ + @property({ type: Boolean, reflect: true }) + public quiet: boolean = false; + + /** + * Disables all items in the accordion. Individual items may also be + * disabled independently. + */ + @property({ type: Boolean, reflect: true }) + public disabled: boolean = false; + + // ────────────────────── + // IMPLEMENTATION + // ────────────────────── + + private assignedItems(): AccordionItemBase[] { + const slot = this.renderRoot?.querySelector('slot'); + if (!slot) { + return []; + } + return slot + .assignedElements({ flatten: true }) + .filter((el): el is AccordionItemBase => el instanceof AccordionItemBase); + } + + private closeSiblingsOnOpen = (event: Event): void => { + if (this.disabled) { + event.preventDefault(); + return; + } + if (this.allowMultiple) { + return; + } + const toggling = event.target; + if (!(toggling instanceof AccordionItemBase)) { + return; + } + // Defer until after dispatch returns so that a canceled toggle (where the + // item reverts open back to false) does not incorrectly close siblings. + queueMicrotask(() => { + if (!toggling.open) { + return; + } + for (const item of this.assignedItems()) { + if (item !== toggling) { + item.open = false; + } + } + }); + }; + + protected syncAccordionItems(): void { + for (const item of this.assignedItems()) { + item.setManagedHeading(this.level); + item.size = this.size; + item.setManagedParentDisabled(this.disabled); + } + } + + private enforceExclusiveOpen(): void { + let foundOpen = false; + for (const item of this.assignedItems()) { + if (item.open) { + if (foundOpen) { + item.open = false; + } else { + foundOpen = true; + } + } + } + } + + public override connectedCallback(): void { + super.connectedCallback(); + this.addEventListener( + SWC_ACCORDION_ITEM_TOGGLE_EVENT, + this.closeSiblingsOnOpen + ); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this.removeEventListener( + SWC_ACCORDION_ITEM_TOGGLE_EVENT, + this.closeSiblingsOnOpen + ); + } + + protected override update(changedProperties: PropertyValues): void { + if (changedProperties.has('level')) { + const clamped = Math.min( + 6, + Math.max(2, this.level) + ) as AccordionHeadingLevel; + if (this.level !== clamped) { + this.level = clamped; + } + } + if ( + changedProperties.has('level') || + changedProperties.has('size') || + changedProperties.has('disabled') + ) { + this.syncAccordionItems(); + } + // changedProperties.get() returns the previous value; this fires only when + // disabled transitions from true → false (re-enable). + if ( + changedProperties.has('disabled') && + changedProperties.get('disabled') === true && + !this.allowMultiple + ) { + this.enforceExclusiveOpen(); + } + super.update(changedProperties); + } + + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + if (window.__swc?.DEBUG && !this.hasAttribute('density')) { + window.__swc.warn( + this, + `<${this.localName}> should have an explicit "density" attribute set. Defaulting to "regular".`, + 'https://opensource.adobe.com/spectrum-web-components/components/accordion/', + { type: 'api', level: 'low' } + ); + } + } +} diff --git a/2nd-gen/packages/core/components/accordion/Accordion.types.ts b/2nd-gen/packages/core/components/accordion/Accordion.types.ts new file mode 100644 index 0000000000..fbf4da59f9 --- /dev/null +++ b/2nd-gen/packages/core/components/accordion/Accordion.types.ts @@ -0,0 +1,33 @@ +/** + * 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'; + +export const ACCORDION_VALID_SIZES = [ + 's', + 'm', + 'l', + 'xl', +] as const satisfies readonly ElementSize[]; +export type AccordionSize = (typeof ACCORDION_VALID_SIZES)[number]; + +export const ACCORDION_DENSITIES = ['compact', 'regular', 'spacious'] as const; +export type AccordionDensity = (typeof ACCORDION_DENSITIES)[number]; + +export const ACCORDION_HEADING_LEVELS = [2, 3, 4, 5, 6] as const; +export type AccordionHeadingLevel = (typeof ACCORDION_HEADING_LEVELS)[number]; + +export const SWC_ACCORDION_ITEM_TOGGLE_EVENT = 'swc-accordion-item-toggle'; +export const SWC_ACCORDION_ITEM_OPEN_EVENT = 'swc-open'; +export const SWC_ACCORDION_ITEM_CLOSE_EVENT = 'swc-close'; +export const SWC_ACCORDION_ITEM_AFTER_OPEN_EVENT = 'swc-after-open'; +export const SWC_ACCORDION_ITEM_AFTER_CLOSE_EVENT = 'swc-after-close'; diff --git a/2nd-gen/packages/core/components/accordion/AccordionItem.base.ts b/2nd-gen/packages/core/components/accordion/AccordionItem.base.ts new file mode 100644 index 0000000000..950f61cf9c --- /dev/null +++ b/2nd-gen/packages/core/components/accordion/AccordionItem.base.ts @@ -0,0 +1,183 @@ +/** + * 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 { property, state } from 'lit/decorators.js'; + +import { SpectrumElement } from '@spectrum-web-components/core/element/index.js'; +import { ObserveSlotPresence } from '@spectrum-web-components/core/mixins/observe-slot-presence.js'; + +import { + type AccordionHeadingLevel, + type AccordionSize, + SWC_ACCORDION_ITEM_AFTER_CLOSE_EVENT, + SWC_ACCORDION_ITEM_AFTER_OPEN_EVENT, + SWC_ACCORDION_ITEM_CLOSE_EVENT, + SWC_ACCORDION_ITEM_OPEN_EVENT, + SWC_ACCORDION_ITEM_TOGGLE_EVENT, +} from './Accordion.types.js'; + +/** + * Base class for accordion item components. Manages open/disabled state, + * heading level (set by the parent accordion), and the toggle event. + * + * @attribute {boolean} open - Whether the accordion item panel is expanded. + * @attribute {boolean} disabled - Whether the accordion item is disabled. + * @attribute {AccordionSize} size - Size of the item. Inherited from the parent + * accordion when slotted; controls the chevron icon when used standalone. + * + * @slot label - The heading text for this accordion item. + * @slot actions - Optional actions rendered adjacent to the heading, outside + * the toggle button so they remain independently interactive. + * @slot - The panel content revealed when the item is open. + */ +export abstract class AccordionItemBase extends ObserveSlotPresence( + SpectrumElement, + '[slot="actions"]' +) { + // ────────────────── + // PUBLIC API + // ────────────────── + + /** + * Whether the accordion item panel is expanded. + */ + @property({ type: Boolean, reflect: true }) + public get open(): boolean { + return this._open; + } + + public set open(value: boolean) { + if (this.hasUpdated && !this.mayExpand() && value !== this._open) { + return; + } + if (value === this._open) { + return; + } + const oldValue = this._open; + this._open = value; + if (value) { + this.setAttribute('open', ''); + } else { + this.removeAttribute('open'); + } + this.requestUpdate('open', oldValue); + } + + /** + * Whether the accordion item is disabled. A disabled item keeps its header + * in the tab order but blocks toggling. + */ + @property({ type: Boolean, reflect: true }) + public disabled: boolean = false; + + /** + * The size of the item. Inherited from the parent accordion; controls which + * chevron icon is displayed. Has no effect when the item is used standalone. + */ + @property({ type: String, reflect: true }) + public size?: AccordionSize; + + private _open = false; + + // ────────────────────── + // INTERNAL STATE + // ────────────────────── + + /** + * @internal + * Heading level (2–6) propagated by the parent accordion. Defaults to 3 + * for standalone items. + */ + @state() + protected headingLevel: AccordionHeadingLevel = 3; + + /** + * @internal + * Set by the parent accordion when its own `disabled` is true. Causes the + * item to render as disabled (aria-disabled + inert panel) without clobbering + * the item's own `disabled` property, so the per-item state is preserved + * when the accordion is re-enabled. + */ + @state() + protected parentDisabled: boolean = false; + + // ────────────────────── + // IMPLEMENTATION + // ────────────────────── + + /** + * @internal + * Whether the item may change `open` (expand or collapse). When false, the + * `open` setter and `toggle()` leave state unchanged. + */ + protected mayExpand(): boolean { + return !this.disabled && !this.parentDisabled; + } + + /** + * @internal + * Toggles the item open state. Guards for disabled, flips `open`, dispatches + * the toggle event, and reverts if the event is canceled. On success, dispatches + * `swc-after-open` or `swc-after-close` after the next render cycle. + */ + protected toggle(): void { + if (!this.mayExpand()) { + return; + } + this.open = !this.open; + const toggleEvent = new Event(SWC_ACCORDION_ITEM_TOGGLE_EVENT, { + bubbles: true, + composed: true, + cancelable: true, + }); + if (!this.dispatchEvent(toggleEvent)) { + this.open = !this.open; + return; + } + const isOpen = this.open; + this.dispatchEvent( + new Event( + isOpen ? SWC_ACCORDION_ITEM_OPEN_EVENT : SWC_ACCORDION_ITEM_CLOSE_EVENT, + { + bubbles: true, + composed: true, + } + ) + ); + void this.updateComplete.then(() => { + this.dispatchEvent( + new Event( + isOpen + ? SWC_ACCORDION_ITEM_AFTER_OPEN_EVENT + : SWC_ACCORDION_ITEM_AFTER_CLOSE_EVENT, + { bubbles: true, composed: true } + ) + ); + }); + } + + /** + * @internal + * Synchronizes parent-managed heading level onto the item. + */ + public setManagedHeading(heading: AccordionHeadingLevel): void { + this.headingLevel = heading; + } + + /** + * @internal + * Synchronizes parent-managed disabled state onto the item. + */ + public setManagedParentDisabled(disabled: boolean): void { + this.parentDisabled = disabled; + } +} diff --git a/2nd-gen/packages/core/components/accordion/index.ts b/2nd-gen/packages/core/components/accordion/index.ts new file mode 100644 index 0000000000..058e18a567 --- /dev/null +++ b/2nd-gen/packages/core/components/accordion/index.ts @@ -0,0 +1,15 @@ +/** + * 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 './Accordion.base.js'; +export * from './AccordionItem.base.js'; +export * from './Accordion.types.js'; diff --git a/2nd-gen/packages/core/package.json b/2nd-gen/packages/core/package.json index 86b4009e99..b4626a67e8 100644 --- a/2nd-gen/packages/core/package.json +++ b/2nd-gen/packages/core/package.json @@ -19,6 +19,10 @@ "types": "./dist/components/alert-banner/index.d.ts", "import": "./dist/components/alert-banner/index.js" }, + "./components/accordion": { + "types": "./dist/components/accordion/index.d.ts", + "import": "./dist/components/accordion/index.js" + }, "./components/asset": { "types": "./dist/components/asset/index.d.ts", "import": "./dist/components/asset/index.js" @@ -148,6 +152,9 @@ "components/alert-banner": [ "dist/components/alert-banner/index.d.ts" ], + "components/accordion": [ + "dist/components/accordion/index.d.ts" + ], "components/asset": [ "dist/components/asset/index.d.ts" ], diff --git a/2nd-gen/packages/swc/.storybook/preview.ts b/2nd-gen/packages/swc/.storybook/preview.ts index 73e4673e0a..68e75baf54 100644 --- a/2nd-gen/packages/swc/.storybook/preview.ts +++ b/2nd-gen/packages/swc/.storybook/preview.ts @@ -324,11 +324,7 @@ const preview = { 'Components', [ 'Accordion', - [ - 'Accessibility migration analysis', - 'Migration plan', - 'Rendering and styling migration analysis', - ], + ['Accessibility migration analysis', 'Migration plan'], 'Action button', ['Rendering and styling migration analysis'], 'Action group', diff --git a/2nd-gen/packages/swc/components/accordion/Accordion.ts b/2nd-gen/packages/swc/components/accordion/Accordion.ts new file mode 100644 index 0000000000..92d112150e --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/Accordion.ts @@ -0,0 +1,45 @@ +/** + * 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 { CSSResultArray, html, TemplateResult } from 'lit'; + +import { AccordionBase } from '@spectrum-web-components/core/components/accordion'; + +import styles from './accordion.css'; + +/** + * An accordion component that groups expandable content sections. + * + * @element swc-accordion + * @since 2.0.0 + * + * @example + * + * + * Section heading + * Panel content goes here. + * + * + */ +export class Accordion extends AccordionBase { + public static override get styles(): CSSResultArray { + return [styles]; + } + + protected override render(): TemplateResult { + return html` +
+ +
+ `; + } +} diff --git a/2nd-gen/packages/swc/components/accordion/AccordionItem.ts b/2nd-gen/packages/swc/components/accordion/AccordionItem.ts new file mode 100644 index 0000000000..2979fe29ef --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/AccordionItem.ts @@ -0,0 +1,149 @@ +/** + * 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 { CSSResultArray, html, TemplateResult } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { when } from 'lit/directives/when.js'; +import { html as staticHtml, unsafeStatic } from 'lit/static-html.js'; + +import { AccordionItemBase } from '@spectrum-web-components/core/components/accordion'; + +import '../icon/swc-icon.js'; + +import { Chevron75Icon } from '../icon/elements/Chevron75Icon.js'; +import { Chevron100Icon } from '../icon/elements/Chevron100Icon.js'; +import { Chevron200Icon } from '../icon/elements/Chevron200Icon.js'; +import { Chevron300Icon } from '../icon/elements/Chevron300Icon.js'; + +import styles from './accordion.css'; + +/** + * An accordion item component that wraps a single expandable content section. + * + * @element swc-accordion-item + * @since 2.0.0 + * + * @fires swc-accordion-item-toggle - Dispatched when the item open state is + * toggled. Cancelable; `preventDefault()` reverts the change. + * @fires swc-open - Dispatched when the item begins opening. + * @fires swc-close - Dispatched when the item begins closing. + * @fires swc-after-open - Dispatched after the item has fully opened. + * @fires swc-after-close - Dispatched after the item has fully closed. + * + * @example + * + * Section heading + * Panel content goes here. + * + */ +export class AccordionItem extends AccordionItemBase { + // ────────────────────────────── + // STYLING + // ────────────────────────────── + + public static override get styles(): CSSResultArray { + return [styles]; + } + + // ────────────────────────────── + // DELEGATION + // ────────────────────────────── + + public override focus(options?: FocusOptions): void { + this.shadowRoot?.getElementById('header')?.focus(options); + } + + public override click(): void { + this.shadowRoot?.getElementById('header')?.click(); + } + + // ────────────────────────────── + // RENDERING + // ────────────────────────────── + + private chevronForSize(): TemplateResult { + switch (this.size) { + case 's': + return Chevron75Icon(); + case 'l': + return Chevron200Icon(); + case 'xl': + return Chevron300Icon(); + case 'm': + default: + return Chevron100Icon(); + } + } + + private handleHeaderKeydown(event: Event): void { + if ((event as KeyboardEvent).key === ' ') { + // Space requires preventDefault to suppress page scroll; toggle is then + // called explicitly. Enter is not handled here — the browser's native + // button activation fires a click event, which calls toggle() via the + // @click binding. + event.preventDefault(); + this.toggle(); + } + } + + private renderHeadingWrapper(content: TemplateResult): TemplateResult { + const tag = unsafeStatic(`h${this.headingLevel}`); + return staticHtml`<${tag} class="swc-AccordionItem-heading">${content}`; + } + + protected override render(): TemplateResult { + const button = html` + + `; + return html` +
+ ${this.renderHeadingWrapper(button)} + ${when( + this.slotContentIsPresent, + () => html` +
+ +
+ ` + )} +
+ +
+
+ `; + } +} diff --git a/2nd-gen/packages/swc/components/accordion/accordion.css b/2nd-gen/packages/swc/components/accordion/accordion.css new file mode 100644 index 0000000000..40f33594a2 --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/accordion.css @@ -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. + */ + +/* S2 token imports and selector definitions land here in Phase 4. */ diff --git a/2nd-gen/packages/swc/components/accordion/index.ts b/2nd-gen/packages/swc/components/accordion/index.ts new file mode 100644 index 0000000000..f8f7131062 --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/index.ts @@ -0,0 +1,14 @@ +/** + * 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 './Accordion.js'; +export * from './AccordionItem.js'; diff --git a/2nd-gen/packages/swc/components/accordion/stories/accordion.stories.ts b/2nd-gen/packages/swc/components/accordion/stories/accordion.stories.ts new file mode 100644 index 0000000000..4284e11925 --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/stories/accordion.stories.ts @@ -0,0 +1,127 @@ +/** + * 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 as Story } from '@storybook/web-components'; +import { getStorybookHelpers } from '@wc-toolkit/storybook-helpers'; + +import '@adobe/spectrum-wc/components/accordion/swc-accordion.js'; +import '@adobe/spectrum-wc/components/accordion/swc-accordion-item.js'; + +import { + ACCORDION_DENSITIES, + ACCORDION_VALID_SIZES, + SWC_ACCORDION_ITEM_TOGGLE_EVENT, +} from '../../../../core/components/accordion/Accordion.types.js'; + +// ──────────────── +// METADATA +// ──────────────── + +const { args, argTypes, template } = getStorybookHelpers('swc-accordion'); + +argTypes.density = { + ...argTypes.density, + control: { type: 'select' }, + options: [...ACCORDION_DENSITIES], + table: { + ...argTypes.density?.table, + category: 'attributes', + defaultValue: { summary: 'regular' }, + }, +}; + +argTypes.size = { + ...argTypes.size, + control: { type: 'select' }, + options: ['', ...ACCORDION_VALID_SIZES], + table: { + ...argTypes.size?.table, + category: 'attributes', + }, +}; + +argTypes.level = { + ...argTypes.level, + control: { type: 'number', min: 2, max: 6 }, + table: { + ...argTypes.level?.table, + category: 'attributes', + defaultValue: { summary: '3' }, + }, +}; + +const defaultItems = html` + + Alchemy + Alchemy is an ancient branch of natural philosophy, a philosophical and + protoscientific tradition that was historically practiced in China, India, + the Muslim world, and Europe. + + + Astrology + Astrology is a range of divinatory practices, recognized as pseudoscientific + since the 18th century, that propose that information about human affairs + and terrestrial events may be discerned by studying the apparent positions + of celestial objects. + + + Natural magic + Natural magic in the context of early modern Europe was a branch of + philosophy that treated occult forces as resulting from natural causes, not + from divine or demonic intervention. + +`; + +/** + * An accordion groups related content sections, each behind a header that can + * be expanded or collapsed. Only one section is open at a time by default; + * set `allow-multiple` to let any number of sections be open simultaneously. + */ +const meta: Meta = { + title: 'Accordion', + component: 'swc-accordion', + args, + argTypes, + render: (args) => template(args, defaultItems), + parameters: { + actions: { handles: [SWC_ACCORDION_ITEM_TOGGLE_EVENT] }, + docs: { + subtitle: 'Groups related content sections behind expandable headers.', + }, + }, + tags: ['migrated'], +}; + +export default meta; + +// ──────────────────── +// AUTODOCS STORY +// ──────────────────── + +export const Playground: Story = { + args: { + density: 'regular', + }, + tags: ['autodocs', 'dev'], +}; + +// ────────────────────────── +// OVERVIEW STORY +// ────────────────────────── + +export const Overview: Story = { + args: { + density: 'regular', + }, + tags: ['overview'], +}; diff --git a/2nd-gen/packages/swc/components/accordion/swc-accordion-item.ts b/2nd-gen/packages/swc/components/accordion/swc-accordion-item.ts new file mode 100644 index 0000000000..cd9853027e --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/swc-accordion-item.ts @@ -0,0 +1,23 @@ +/** + * 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 { AccordionItem } from './AccordionItem.js'; + +declare global { + interface HTMLElementTagNameMap { + 'swc-accordion-item': AccordionItem; + } +} + +defineElement('swc-accordion-item', AccordionItem); diff --git a/2nd-gen/packages/swc/components/accordion/swc-accordion.ts b/2nd-gen/packages/swc/components/accordion/swc-accordion.ts new file mode 100644 index 0000000000..d51f7f1b1e --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/swc-accordion.ts @@ -0,0 +1,23 @@ +/** + * 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 { Accordion } from './Accordion.js'; + +declare global { + interface HTMLElementTagNameMap { + 'swc-accordion': Accordion; + } +} + +defineElement('swc-accordion', Accordion); diff --git a/2nd-gen/packages/swc/components/accordion/test/.gitkeep b/2nd-gen/packages/swc/components/accordion/test/.gitkeep new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/test/.gitkeep @@ -0,0 +1 @@ + diff --git a/2nd-gen/packages/swc/components/icon/elements/Chevron300Icon.ts b/2nd-gen/packages/swc/components/icon/elements/Chevron300Icon.ts new file mode 100644 index 0000000000..72956bf4b1 --- /dev/null +++ b/2nd-gen/packages/swc/components/icon/elements/Chevron300Icon.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 { html, TemplateResult } from 'lit'; + +export const Chevron300Icon = (): TemplateResult => { + return html` + + + + `; +}; diff --git a/2nd-gen/packages/swc/components/icon/elements/index.ts b/2nd-gen/packages/swc/components/icon/elements/index.ts index b30f66ddd2..8d6cf638b2 100644 --- a/2nd-gen/packages/swc/components/icon/elements/index.ts +++ b/2nd-gen/packages/swc/components/icon/elements/index.ts @@ -20,6 +20,7 @@ export * from './Chevron50Icon.js'; export * from './Chevron75Icon.js'; export * from './Chevron100Icon.js'; export * from './Chevron200Icon.js'; +export * from './Chevron300Icon.js'; export * from './CornerTriangle300Icon.js'; export * from './Cross75Icon.js'; export * from './Cross100Icon.js';