diff --git a/.ai/rules/code-conformance.md b/.ai/rules/code-conformance.md index 511f538b86d..7a8c0da97ec 100644 --- a/.ai/rules/code-conformance.md +++ b/.ai/rules/code-conformance.md @@ -71,6 +71,7 @@ Reference: [Linting tools](../../CONTRIBUTOR-DOCS/02_style-guide/03_linting-tool - Forced-colors media query is present and correct (if applicable) - High-contrast and other media queries are sorted to the bottom of the file - No hard-coded values where design tokens are available +- Every class selector in CSS files (`.swc-*`) has a matching `class="..."` in the component's `render()` method; orphaned selectors mean styles are silently dead. Cross-check both directions: CSS → template and template → CSS. Use `grep -oE '\.[a-z][a-zA-Z-]+'` on the CSS and `grep -oE 'class="[^"]*"'` on the TypeScript to produce lists to compare. ## Test files diff --git a/.ai/skills/migration-styling/SKILL.md b/.ai/skills/migration-styling/SKILL.md index c9c08c2817d..b9dbb1f324a 100644 --- a/.ai/skills/migration-styling/SKILL.md +++ b/.ai/skills/migration-styling/SKILL.md @@ -66,6 +66,8 @@ Common case: confirming that subcomponent class names follow the single-hyphen s If a rename is needed, make the template change first, confirm the component still renders correctly in Storybook, then write the CSS. +**Step 3b — Audit `:host` for visual styles.** After aligning class names, scan every declaration you plan to put on `:host` against Rule 1 of the tldr. Ask for each property: is this layout-participation (how the host fits into its parent's flow) or visual (how the component looks)? If a visual style has no internal wrapper to move it to, add one to `render()` before writing CSS. + **Step 4 — Execute the phase.** Follow **[Phase 5: Styling](../../../CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_washing-machine-workflow.md#phase-5-styling)** in the washing machine workflow doc — it covers what to do, what to check, common problems, and the quality gate for this phase. **Step 5 — Document exposed custom properties.** After writing the CSS, add a `@cssprop` JSDoc tag to the SWC component class (`2nd-gen/packages/swc/components/[component]/[Component].ts`) for every exposed `--swc-*` property. Place all `@cssprop` tags on the primary SWC class export (not the core base class). Each tag should name the property and give a one-line description of what it controls, including its default token where relevant. diff --git a/.ai/skills/migration-styling/references/tldr-component-css-guidelines.md b/.ai/skills/migration-styling/references/tldr-component-css-guidelines.md index ee1bf66e1e2..e66ed7b0ff0 100644 --- a/.ai/skills/migration-styling/references/tldr-component-css-guidelines.md +++ b/.ai/skills/migration-styling/references/tldr-component-css-guidelines.md @@ -72,9 +72,18 @@ ### 1. `:host` vs Component Class -Put only layout-participation styles on `:host`. Put actual visuals on `.swc-ComponentName` or internal parts. +Put layout-participation styles on `:host` (`display`, `inline-size`, `min-*`/`max-*`, `position`, custom property definitions). Put visual styles on `.swc-ComponentName` or an internal part. -**Exception**: three categories of styles may legitimately live on `:host` — each for a distinct reason: UA resets when the browser applies default styles directly to the host element (for example, the native popover stylesheet); transition properties (`opacity`, `transition-*`, `transition-behavior: allow-discrete`) when the host is itself the transition target (for example, when `@starting-style` applies to the host); and positioning surface (`position: absolute`, `inset: auto`, dimension constraints) when an external controller such as a placement controller writes coordinates directly to the host element. +**Exception**: three categories of styles may legitimately live on `:host`, each for a distinct reason: UA resets when the browser applies default styles directly to the host element (for example, the native popover stylesheet); transition properties (`opacity`, `transition-*`, `transition-behavior: allow-discrete`) when the host is itself the transition target (for example, when `@starting-style` applies to the host); and positioning surface (`position: absolute`, `inset: auto`, dimension constraints) when an external controller such as a placement controller writes coordinates directly to the host element. + +Two non-obvious cases to flag explicitly: + +- **`padding` on `:host`** — feels like layout but is visual spacing; move it to the internal class. +- **`cursor: pointer`** — do not set it anywhere; the project relies on browser defaults. + +Also check that `display: flex` or `display: grid` on `:host` is actually laying out **direct children of `:host`**, not internal children already wrapped inside a container element. Flex/grid properties (`flex: 1 1 auto`, `align-self`) only activate when their **immediate parent** is the flex/grid container — if the element is inside a wrapper div, the flex context must be on that wrapper, not on `:host`. + +**`:host:has()` is unreliable across browsers.** Safari and Firefox do not consistently support `:has()` relative to a shadow host boundary. Move all `:has()` selectors to the internal wrapper: `.swc-Component:has(...)` instead of `:host:has(...)`. Custom properties cascade identically either way. See [01_component-css#state-implementation-patterns](../../../../CONTRIBUTOR-DOCS/02_style-guide/01_css/01_component-css.md#state-implementation-patterns). → See [01_component-css#when-to-use-host](../../../../CONTRIBUTOR-DOCS/02_style-guide/01_css/01_component-css.md#when-to-use-host) @@ -136,6 +145,9 @@ Keep selector specificity at or below `(0,1,0)`. If you need a compounded select ### 7. Forced colors Only add `@media (forced-colors: active)` if browser defaults are not conveying correct semantic intent, and always put it at the end of the component stylesheet. + +Semantic HTML elements (` + `; + return html` +
+
+ ${this.renderHeadingWrapper(button)} + ${when( + this.slotContentIsPresent, + () => html` +
+ +
+ ` + )} +
+
+
+ +
+
+
+ `; + } +} diff --git a/2nd-gen/packages/swc/components/accordion/accordion-item.css b/2nd-gen/packages/swc/components/accordion/accordion-item.css new file mode 100644 index 00000000000..8fbaa6d3a50 --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/accordion-item.css @@ -0,0 +1,276 @@ +/** + * 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. + */ + +:host { + display: block; + inline-size: 100%; +} + +* { + box-sizing: border-box; +} + +/* ── Item wrapper ───────────────────────────────────────────────────── */ + +.swc-AccordionItem { + --_swc-accordion-item-header-background: transparent; + --_swc-accordion-item-header-text-color: token("neutral-content-color-default"); + + inline-size: 100%; + border-block-end: token("border-width-100") solid var(--swc-accordion-item-divider-color, var(--swc-accordion-item-border-color, token("gray-200"))); +} + +:host(:first-child) .swc-AccordionItem { + border-block-start: token("border-width-100") solid var(--swc-accordion-item-divider-color, var(--swc-accordion-item-border-color, token("gray-200"))); +} + +/* ── Header row (heading + actions) ─────────────────────────────────── */ + +.swc-AccordionItem-row { + display: flex; + gap: token("spacing-100"); + align-items: center; +} + +/* ── Heading reset (h2–h6 wrapping the button) ──────────────────────── */ + +.swc-AccordionItem-heading { + flex: 1 1 auto; + min-inline-size: 0; + padding: 0; + margin: 0; + font: inherit; +} + +/* ── Header button ──────────────────────────────────────────────────── */ + +.swc-AccordionItem-header { + --_swc-accordion-item-padding-top: var(--swc-accordion-item-padding-top, token("accordion-top-to-text-medium")); + --_swc-accordion-item-padding-bottom: var(--swc-accordion-item-padding-bottom, token("accordion-bottom-to-text-medium")); + --_swc-accordion-item-disclosure-indicator-gap: var(--swc-accordion-item-disclosure-indicator-gap, token("accordion-disclosure-indicator-to-text-medium")); + --_swc-accordion-item-edge-to-content-area: var(--swc-accordion-item-edge-to-content-area, token("accordion-edge-to-content-area-medium")); + --_swc-accordion-item-header-font-size: var(--swc-accordion-item-header-font-size, token("font-size-200")); + + display: flex; + gap: var(--_swc-accordion-item-disclosure-indicator-gap); + align-items: center; + inline-size: 100%; + padding-block: var(--_swc-accordion-item-padding-top) var(--_swc-accordion-item-padding-bottom); + padding-inline: var(--_swc-accordion-item-edge-to-content-area); + font-family: inherit; + font-size: var(--_swc-accordion-item-header-font-size); + font-weight: token("bold-font-weight"); + line-height: token("line-height-100"); + color: var(--_swc-accordion-item-header-text-color); + text-align: start; + background-color: var(--_swc-accordion-item-header-background); + border: 0; + border-radius: var(--swc-accordion-item-header-corner-radius, 0); + appearance: none; +} + +/* The container sets the color tokens so they cascade into .swc-AccordionItem-header; the + outline and border-radius stay on the button so the focus ring matches + the hover footprint (toggle area only, independent of the actions slot). */ +.swc-AccordionItem:has(.swc-AccordionItem-header:focus-visible) { + --_swc-accordion-item-header-background: token("transparent-black-100"); + --_swc-accordion-item-header-text-color: token("neutral-content-color-key-focus"); +} + +.swc-AccordionItem-header:focus-visible { + border-radius: var(--swc-accordion-item-focus-indicator-corner-radius, token("corner-radius-medium-size-medium")); + outline: token("focus-indicator-thickness") solid token("focus-indicator-color"); + outline-offset: calc(token("accordion-focus-indicator-gap") * -1); +} + +.swc-AccordionItem:has(.swc-AccordionItem-header:hover) { + --_swc-accordion-item-header-background: token("transparent-black-100"); + --_swc-accordion-item-header-text-color: token("neutral-content-color-hover"); +} + +.swc-AccordionItem:has(.swc-AccordionItem-header:active) { + --_swc-accordion-item-header-background: token("transparent-black-300"); + --_swc-accordion-item-header-text-color: token("neutral-content-color-down"); +} + +.swc-AccordionItem:has(.swc-AccordionItem-header[aria-disabled="true"]), +.swc-AccordionItem:has(.swc-AccordionItem-header[aria-disabled="true"]:hover) { + --_swc-accordion-item-header-background: transparent; + --_swc-accordion-item-header-text-color: token("disabled-content-color"); +} + +/* ── Disclosure indicator (chevron) ─────────────────────────────────── */ + +/* Default size (m): Chevron100 */ +.swc-AccordionItem-indicator { + --swc-icon-inline-size: token("chevron-icon-size-100"); + --swc-icon-block-size: token("chevron-icon-size-100"); + + flex-shrink: 0; + rotate: 0deg; + transition: rotate token("animation-duration-100") token("animation-ease-in-out"); +} + +/* Mirror for RTL so the chevron points left */ +:dir(rtl) .swc-AccordionItem-indicator { + scale: -1 1; +} + +/* Size: small, Chevron75 */ +:host([size="s"]) .swc-AccordionItem-indicator { + --swc-icon-inline-size: token("chevron-icon-size-75"); + --swc-icon-block-size: token("chevron-icon-size-75"); +} + +/* Size: large, Chevron200 */ +:host([size="l"]) .swc-AccordionItem-indicator { + --swc-icon-inline-size: token("chevron-icon-size-200"); + --swc-icon-block-size: token("chevron-icon-size-200"); +} + +/* Size: extra-large, Chevron300 */ +:host([size="xl"]) .swc-AccordionItem-indicator { + --swc-icon-inline-size: token("chevron-icon-size-300"); + --swc-icon-block-size: token("chevron-icon-size-300"); +} + +:host([open]) .swc-AccordionItem-indicator { + rotate: 90deg; +} + +/* RTL + open: rotate the opposite direction */ +:host([open]):dir(rtl) .swc-AccordionItem-indicator { + rotate: -90deg; +} + +/* ── Item title (heading slot wrapper) ──────────────────────────────── */ + +.swc-AccordionItem-label { + flex: 1; +} + +/* + * Override light-DOM heading styles (browser defaults, host-document resets) + * that would otherwise win over the inherited header font. Applies to + * phrasing content like or slotted into the label. + * The :not([class]) guard leaves intentionally-classed elements untouched. + * !important is required to win over any light-DOM styles targeting the + * slotted element (e.g. global h* resets from the host document). + */ +::slotted([slot="label"]:not([class])) { + margin: 0 !important; + font: inherit !important; +} + +/* ── Direct actions (inline with heading, outside the toggle area) ───── */ + +.swc-AccordionItem-actions { + display: flex; + flex: 0 0 auto; + gap: token("spacing-100"); + align-items: center; + margin-inline-end: var(--swc-accordion-item-edge-to-content-area, token("accordion-edge-to-content-area-medium")); +} + +/* ── Content panel ──────────────────────────────────────────────────── */ + +.swc-AccordionItem-content { + display: none; +} + +:host([open]) .swc-AccordionItem-content { + display: block; +} + +/* Inner wrapper holds padding and typography. The outer container (.swc-AccordionItem-content) + is padding-free so height: 0 collapses it cleanly in the @supports animation block. */ +.swc-AccordionItem-contentBody { + --_swc-accordion-item-content-padding-inline: var(--swc-accordion-item-content-padding-inline, token("accordion-content-area-edge-to-content-medium")); + + padding-block: token("accordion-content-area-top-to-content") token("accordion-content-area-bottom-to-content"); + padding-inline: var(--_swc-accordion-item-content-padding-inline); + font-size: token("font-size-100"); + font-weight: token("regular-font-weight"); + line-height: token("line-height-100"); + color: token("neutral-subdued-content-color-default"); +} + +/* Zero out default browser margins at the panel edges. :nth-child(1 of :not([slot])) + skips named-slot siblings (span[slot="label"], etc.) and targets the first + default-slot child regardless of element type. */ +.swc-AccordionItem-contentBody ::slotted(:nth-child(1 of :not([slot]))) { + margin-block-start: 0; +} + +.swc-AccordionItem-contentBody ::slotted(:last-child) { + margin-block-end: 0; +} + +/* ── Size: small ────────────────────────────────────────────────────── */ + +:host([size="s"]) { + --swc-accordion-item-focus-indicator-corner-radius: token("corner-radius-medium-size-small"); + --swc-accordion-item-padding-top: token("accordion-top-to-text-small"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-small"); + --swc-accordion-item-disclosure-indicator-gap: token("accordion-disclosure-indicator-to-text-small"); + --swc-accordion-item-edge-to-content-area: token("accordion-edge-to-content-area-small"); + --swc-accordion-item-header-font-size: token("font-size-100"); + --swc-accordion-item-content-padding-inline: token("accordion-content-area-edge-to-content-small"); +} + +/* ── Size: large ────────────────────────────────────────────────────── */ + +:host([size="l"]) { + --swc-accordion-item-focus-indicator-corner-radius: token("corner-radius-medium-size-large"); + --swc-accordion-item-padding-top: token("accordion-top-to-text-large"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-large"); + --swc-accordion-item-disclosure-indicator-gap: token("accordion-disclosure-indicator-to-text-large"); + --swc-accordion-item-edge-to-content-area: token("accordion-edge-to-content-area-large"); + --swc-accordion-item-header-font-size: token("font-size-300"); + --swc-accordion-item-content-padding-inline: token("accordion-content-area-edge-to-content-large"); +} + +/* ── Size: extra-large ──────────────────────────────────────────────── */ + +:host([size="xl"]) { + --swc-accordion-item-focus-indicator-corner-radius: token("corner-radius-medium-size-extra-large"); + --swc-accordion-item-padding-top: token("accordion-top-to-text-extra-large"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-extra-large"); + --swc-accordion-item-disclosure-indicator-gap: token("accordion-disclosure-indicator-to-text-extra-large"); + --swc-accordion-item-edge-to-content-area: token("accordion-edge-to-content-area-extra-large"); + --swc-accordion-item-header-font-size: token("font-size-400"); + --swc-accordion-item-content-padding-inline: token("accordion-content-area-edge-to-content-extra-large"); +} + +/* ── Progressive enhancement: height animation ─────────────────────── + calc-size() allows animating height: auto, which CSS transitions + could not previously target. Supported in Chrome/Edge 129+; all + other browsers keep the current instant show/hide behavior. */ + +@supports (height: calc-size(auto, size)) { + .swc-AccordionItem-content { + display: block; + height: 0; + overflow: hidden; + transition: height token("animation-duration-100") token("animation-ease-in-out"); + } + + :host([open]) .swc-AccordionItem-content { + height: calc-size(auto, size); + } + + @media (prefers-reduced-motion: reduce) { + .swc-AccordionItem-content { + transition: none; + } + } +} 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 00000000000..0c76a695f75 --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/accordion.css @@ -0,0 +1,80 @@ +/** + * 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. + */ + +:host { + display: block; + min-inline-size: var(--swc-accordion-min-inline-size, token("accordion-minimum-width")); +} + +.swc-Accordion { + --_swc-accordion-corner-radius: token("corner-radius-medium-size-medium"); +} + +/* Corner radius per size: cascades to child items so quiet mode resolves correctly */ +:host([size="s"]) .swc-Accordion { + --_swc-accordion-corner-radius: token("corner-radius-medium-size-small"); +} + +:host([size="l"]) .swc-Accordion { + --_swc-accordion-corner-radius: token("corner-radius-medium-size-large"); +} + +:host([size="xl"]) .swc-Accordion { + --_swc-accordion-corner-radius: token("corner-radius-medium-size-extra-large"); +} + +/* Quiet: removes dividers and rounds header corners (visible on hover and focus) */ +:host([quiet]) ::slotted(swc-accordion-item) { + --swc-accordion-item-border-color: transparent; + --swc-accordion-item-header-corner-radius: var(--_swc-accordion-corner-radius); +} + +/* Compact & spacious density: overrides cascade to slotted items */ +:host([density="compact"]) ::slotted(swc-accordion-item) { + --swc-accordion-item-padding-top: token("accordion-top-to-text-compact-medium"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-compact-medium"); +} + +:host([density="spacious"]) ::slotted(swc-accordion-item) { + --swc-accordion-item-padding-top: token("accordion-top-to-text-spacious-medium"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-spacious-medium"); +} + +:host([density="compact"][size="s"]) ::slotted(swc-accordion-item) { + --swc-accordion-item-padding-top: token("accordion-top-to-text-compact-small"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-compact-small"); +} + +:host([density="compact"][size="l"]) ::slotted(swc-accordion-item) { + --swc-accordion-item-padding-top: token("accordion-top-to-text-compact-large"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-compact-large"); +} + +:host([density="compact"][size="xl"]) ::slotted(swc-accordion-item) { + --swc-accordion-item-padding-top: token("accordion-top-to-text-compact-extra-large"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-compact-extra-large"); +} + +:host([density="spacious"][size="s"]) ::slotted(swc-accordion-item) { + --swc-accordion-item-padding-top: token("accordion-top-to-text-spacious-small"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-spacious-small"); +} + +:host([density="spacious"][size="l"]) ::slotted(swc-accordion-item) { + --swc-accordion-item-padding-top: token("accordion-top-to-text-spacious-large"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-spacious-large"); +} + +:host([density="spacious"][size="xl"]) ::slotted(swc-accordion-item) { + --swc-accordion-item-padding-top: token("accordion-top-to-text-spacious-extra-large"); + --swc-accordion-item-padding-bottom: token("accordion-bottom-to-text-spacious-extra-large"); +} 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 00000000000..f8f7131062c --- /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 00000000000..64838054b1e --- /dev/null +++ b/2nd-gen/packages/swc/components/accordion/stories/accordion.stories.ts @@ -0,0 +1,512 @@ +/** + * 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 '@adobe/spectrum-wc/components/button/swc-button.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 content = { + personal: html` +

Manage your name, email address, and contact details.

+ `, + billing: html` +

+ Your billing address is used to verify your payment method and calculate + taxes. +

+ `, + shipping: html` +

Physical products and documents are sent to this address.

+ `, + payment: html` +

Contact your administrator to update payment information.

+ `, +}; + +const defaultItems = html` + + Personal information + ${content.personal} + + + Billing address + ${content.billing} + + + Shipping address + ${content.shipping} + +`; + +/** + * 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. + * + * Items support an **`actions` slot** for placing interactive controls (such + * as an edit button) directly in the header row, outside the toggle button so + * they remain independently clickable. + */ +const meta: Meta = { + title: 'Accordion', + component: 'swc-accordion', + args, + argTypes, + render: (args) => template(args, defaultItems), + parameters: { + layout: 'padded', + 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'], +}; + +// ────────────────────────── +// ANATOMY STORIES +// ────────────────────────── + +const anatomyItems = html` + + Personal information + ${content.personal} + + + Billing address + + Edit + + ${content.billing} + +`; + +/** + * A `` item exposes three content surfaces: + * + * - **`label` slot**: The heading text shown in the collapsed and expanded header + * - **`actions` slot**: Optional interactive controls placed outside the toggle + * button so they remain independently clickable (see the second item below) + * - **Default slot**: The panel body revealed when the item expands + */ +export const Anatomy: Story = { + render: (args) => template(args, anatomyItems), + tags: ['anatomy'], +}; + +// ────────────────────────── +// OPTIONS STORIES +// ────────────────────────── + +/** + * Accordions come in four sizes to fit different layout contexts. + */ +export const Sizes: Story = { + render: (args) => html` +
+ ${ACCORDION_VALID_SIZES.map( + (size) => html` + ${template( + { ...args, size }, + html` + + Personal information (size=${size}) + ${content.personal} + + ` + )} + ` + )} +
+ `, + args: { density: 'regular' }, + tags: ['options'], + parameters: { 'section-order': 1 }, +}; + +/** + * Density controls the vertical spacing between items and within each header. + * + * - **Compact**: Tighter spacing for data-dense interfaces + * - **Regular**: The default, suitable for most contexts + * - **Spacious**: More breathing room for content-focused layouts + */ +export const Density: Story = { + render: (args) => html` +
+ ${ACCORDION_DENSITIES.map( + (density) => html` + ${template( + { ...args, density }, + html` + + + Personal information (density=${density}) + + ${content.personal} + + ` + )} + ` + )} +
+ `, + args: { density: 'regular' }, + tags: ['options'], + parameters: { 'section-order': 2 }, +}; + +/** + * The `quiet` variant removes the divider borders for a lighter visual style, + * and adds rounded corners on hover for a contained feel. + */ +export const Quiet: Story = { + render: (args) => template({ ...args, quiet: true }, defaultItems), + args: { density: 'regular' }, + tags: ['options'], + parameters: { 'section-order': 3 }, +}; + +/** + * The `level` attribute sets the heading level (h2–h6) applied to every item + * header. Use it to fit the accordion into the surrounding page heading + * hierarchy without changing the visual style. + */ +export const HeadingLevel: Story = { + render: (args) => template({ ...args, level: 2 }, defaultItems), + args: { density: 'regular' }, + tags: ['options'], + parameters: { 'section-order': 4 }, +}; + +// ────────────────────────── +// STATES STORIES +// ────────────────────────── + +const stateItems = html` + + Personal information + ${content.personal} + + + Billing address + ${content.billing} + + + Payment method + ${content.payment} + +`; + +/** + * Items can be in a default (collapsed), open (expanded), or disabled state. + * A disabled item remains in the tab order but its toggle is blocked. + */ +export const ItemStates: Story = { + render: (args) => template({ ...args, 'allow-multiple': true }, stateItems), + args: { density: 'regular' }, + tags: ['states'], + parameters: { 'section-order': 1 }, +}; + +/** + * Setting `disabled` on the `` parent disables all items at + * once without overwriting individual item disabled state. Re-enabling the + * accordion restores each item's original state. + */ +export const DisabledAccordion: Story = { + render: (args) => template({ ...args, disabled: true }, defaultItems), + args: { density: 'regular' }, + tags: ['states'], + parameters: { 'section-order': 2 }, +}; + +/** + * When the accordion host is `disabled`, all items become non-interactive + * regardless of their individual `disabled` state. Per-item `disabled` is + * preserved underneath: toggle the **disabled** control off and the third + * item remains disabled while the first two become interactive again. + * + * This separation means an item that was already disabled before the + * host was disabled does not silently become enabled when the host + * is re-enabled. + */ +export const MixedDisabledStates: Story = { + render: (args) => + template( + { ...args, disabled: true, 'allow-multiple': true }, + html` + + Personal information + ${content.personal} + + + Billing address + ${content.billing} + + + Payment method + ${content.payment} + + ` + ), + args: { density: 'regular' }, + tags: ['states'], + parameters: { 'section-order': 3 }, +}; + +// ────────────────────────────── +// BEHAVIORS STORIES +// ────────────────────────────── + +const directActionsItems = html` + + Personal information + + Edit + + ${content.personal} + + + Billing address + + Edit + + ${content.billing} + + + Shipping address + + Edit + + ${content.shipping} + +`; + +/** + * Place interactive controls in the **`actions` slot** to render them inline + * with the heading, outside the toggle button. Because the actions are siblings + * of the toggle button rather than children of it, interacting with them does + * not expand or collapse the item. + */ +export const DirectActions: Story = { + render: (args) => template(args, directActionsItems), + args: { density: 'regular' }, + tags: ['behaviors'], + parameters: { 'section-order': 1 }, +}; + +const allowMultipleItems = html` + + Personal information + ${content.personal} + + + Billing address + ${content.billing} + + + Shipping address + ${content.shipping} + +`; + +/** + * By default only one item may be open at a time; opening a new item closes + * the previously open one. Set `allow-multiple` to allow any number of items + * to be open simultaneously. + */ +export const AllowMultiple: Story = { + render: (args) => + template({ ...args, 'allow-multiple': true }, allowMultipleItems), + args: { density: 'regular' }, + tags: ['behaviors'], + parameters: { 'section-order': 2 }, +}; + +/** + * Every expand or collapse dispatches a `swc-accordion-item-toggle` event from + * the item. The event bubbles and is composed, so a single listener on the + * `` can observe all items. It is cancelable; calling + * `preventDefault()` reverts the open state. + * + * Open the **Actions** panel in Storybook to observe events as you interact + * with the accordion below. + */ +export const ToggleEvent: Story = { + render: (args) => template(args, defaultItems), + args: { density: 'regular' }, + tags: ['behaviors'], + parameters: { 'section-order': 3 }, +}; + +// ──────────────────────────────── +// ACCESSIBILITY STORIES +// ──────────────────────────────── + +const a11yItems = html` + + Personal information + + Edit + + ${content.personal} + + + Billing address + ${content.billing} + + + Payment method + ${content.payment} + +`; + +/** + * ### Features + * + * The `` and `` elements implement the + * [WAI-ARIA Accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/): + * + * #### Keyboard navigation + * + * - Tab: Moves focus into and between item header buttons + * - Enter: Activates the focused header button (browser-native click behavior) + * - Space: Toggles the focused item and prevents page scroll + * + * #### ARIA implementation + * + * 1. **Heading wrapper**: Each header button is wrapped in an `h2`–`h6` element matching + * the accordion's `level` attribute + * 2. **`aria-expanded`**: Set to `"true"` on open items, `"false"` on closed items + * 3. **`aria-controls`**: Points from the header button to the panel (`id="content"`) + * 4. **Panel role**: The panel has `role="region"` and `aria-labelledby` pointing to the + * header button, making it a labeled landmark + * 5. **`aria-disabled`**: Set on the header button (not the native `disabled` attribute) + * so disabled items remain keyboard-reachable + * 6. **`aria-hidden`**: Set to `"true"` on closed panels to remove them from the accessibility tree + * 7. **`inert`**: Added to disabled-item panels to block interaction with their contents + * + * ### Best practices + * + * - Set a `level` that continues the existing page heading hierarchy without skipping levels + * - Provide meaningful, unique label text for each item so screen reader users can + * navigate the heading list + * - When using the `actions` slot, include the item subject in the action's accessible + * name (e.g., "Edit personal information" rather than "Edit") so it is unambiguous out of context + * - Always set `density` explicitly; use `regular` when unsure + */ +export const Accessibility: Story = { + render: (args) => template(args, a11yItems), + args: { density: 'regular', level: 3 }, + tags: ['a11y'], +}; 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 00000000000..cd9853027e7 --- /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 00000000000..d51f7f1b1e5 --- /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 00000000000..8b137891791 --- /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 00000000000..72956bf4b1d --- /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 b30f66ddd2c..8d6cf638b2f 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'; diff --git a/CONTRIBUTOR-DOCS/02_style-guide/01_css/01_component-css.md b/CONTRIBUTOR-DOCS/02_style-guide/01_css/01_component-css.md index 100fc4e49fd..9d6b9fe5bcc 100644 --- a/CONTRIBUTOR-DOCS/02_style-guide/01_css/01_component-css.md +++ b/CONTRIBUTOR-DOCS/02_style-guide/01_css/01_component-css.md @@ -345,6 +345,22 @@ States reflect user interaction or component condition. Attach them to `:host` w **Derived states are not on `:host`**: If a state is computed from slot content (e.g. icon-only), it is not a consumer-settable attribute and must not appear in the state table above. Express it as a class modifier on the internal element via `classMap`. See [When to use classes vs attributes](#when-to-use-classes-vs-attributes). +**`:host:has()` cross-browser compatibility**: Avoid `:host:has(.some-internal-selector)`. This selector pattern has inconsistent behavior in shadow DOM across Safari and Firefox — the browser must evaluate `:has()` relative to a shadow host boundary, which is not yet uniformly supported. Move the `:has()` to the internal wrapper instead: + +```css +/* ❌ Avoid — :has() relative to :host is unreliable across browsers */ +:host:has(.swc-Component-header:hover) { + --_swc-component-bg: token("transparent-black-100"); +} + +/* ✅ Use — :has() on the internal wrapper works consistently */ +.swc-Component:has(.swc-Component-header:hover) { + --_swc-component-bg: token("transparent-black-100"); +} +``` + +Custom properties set on the internal wrapper still cascade correctly to its descendants, so the behavior is identical. + ## Size variant patterns Size variants (s, m, l, xl) use `:host([size="..."])` and update custom properties. Do not add size classes to `render()`. @@ -434,7 +450,7 @@ Forced colors mode (Windows High Contrast, etc.) replaces colors with system val **Rules**: -1. **Check first**: Do not add forced-colors styles if the browser already makes the component visible. +1. **Check first**: Do not add forced-colors styles if the browser already makes the component visible. Semantic HTML elements (`
+
+
+ ``` + + 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 +191,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 +236,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 | S2 modifier `.spectrum-Accordion--noInlinePadding` removes **header** inline padding. **Not** exposed as an API attribute. Parity is via **`--swc-accordion-item-edge-to-content-area`** on `swc-accordion-item` (symmetric `padding-inline` on the header button); set to `0` for edge-to-edge alignment. Storybook demo and `@cssprop` documentation **TBD**. | ### 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 +280,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. | --- @@ -287,6 +297,51 @@ Follow the [washing machine — core vs SWC](../../02_workstreams/02_2nd-gen-com --- +## CSS custom property migration reference + +### `--mod-*` to `--swc-*` mapping + +2nd-gen exposes a narrower customization surface than 1st-gen. Most visual values are driven by Spectrum 2 design tokens internally and are not overridable via custom properties. + +| 1st-gen property | 2nd-gen property | Notes | +|---|---|---| +| `--mod-accordion-item-width` | `--swc-accordion-min-inline-size` | Renamed; sets the minimum inline size of the accordion host | +| `--mod-accordion-divider-color` | `--swc-accordion-item-divider-color` | Renamed; controls the border color of each item's top and bottom dividers | +| `--mod-accordion-component-edge-to-text` | `--swc-accordion-item-content-padding-inline` | Exposed; controls the content panel's inline padding. Overridden per `size` on the item host | +| `--mod-accordion-background-color-*` | Not exposed | Driven by internal tokens via `:has()` state selectors | +| `--mod-accordion-corner-radius` | `--swc-accordion-item-corner-radius` | Exposed; controls the border-radius of the header button in `:focus-visible`. Defaults to the size-scaled corner-radius token; overridden per `size` attribute on the item host | +| `--mod-accordion-item-header-color-*` | Not exposed | Driven by internal tokens via `:has()` state selectors | +| `--mod-accordion-focus-indicator-*` | Not exposed | Driven by global focus indicator tokens | +| `--mod-accordion-item-content-*` (typography) | Not exposed | Driven by global typography tokens | +| `--mod-accordion-item-header-font-size` | `--swc-accordion-item-header-font-size` | Exposed; overridden per `size` on the item host. Other header typography (weight, line-height) remains token-driven | +| `--mod-accordion-item-header-*` (other typography) | Not exposed | Driven by global typography tokens | +| `--mod-accordion-disclosure-indicator-*` (dimensions) | Not exposed | Chevron icon dimensions controlled internally via `--swc-icon-*` on the chevron element | +| `--mod-accordion-divider-thickness` | Not exposed | Driven by `token("border-width-100")` | +| `--mod-accordion-min-block-size` | Not exposed | Minimum height is set by block-padding tokens and content height | +| `--mod-accordion-item-header-top/bottom-to-text-space` | `--swc-accordion-item-padding-top` / `--swc-accordion-item-padding-bottom` | Exposed; header block padding. Overridden per `size` on the item host; compact/spacious `density` overrides from `swc-accordion` via `::slotted(swc-accordion-item)` | +| `--mod-accordion-edge-to-content-area-*` | `--swc-accordion-item-edge-to-content-area` | Exposed; symmetric inline padding on the header button. Overridden per `size`; set to `0` for no-inline-padding parity | +| `--mod-accordion-disclosure-indicator-to-text-*` | `--swc-accordion-item-disclosure-indicator-gap` | Exposed; gap between chevron and label. Overridden per `size` | +| `--mod-accordion-edge-to-*-space` (other) | Not exposed | Legacy 1st-gen spacing mods without a 1:1 2nd-gen override | +| `--mod-accordion-item-content-area-*-to-content` | Not exposed | Block padding only (`accordion-content-area-top-to-content`, `accordion-content-area-bottom-to-content`); driven by fixed tokens, not overridable | + +### Density × size padding matrix + +Header block padding is determined by `density` (on the accordion host) and `size` (set on the host and propagated to items). + +| Density | Size s | Size m (default) | Size l | Size xl | +|---|---|---|---|---| +| **regular** | `accordion-top/bottom-to-text-small` | `accordion-top/bottom-to-text-medium` | `accordion-top/bottom-to-text-large` | `accordion-top/bottom-to-text-extra-large` | +| **compact** | `accordion-top/bottom-to-text-compact-small` | `accordion-top/bottom-to-text-compact-medium` | `accordion-top/bottom-to-text-compact-large` | `accordion-top/bottom-to-text-compact-extra-large` | +| **spacious** | `accordion-top/bottom-to-text-spacious-small` | `accordion-top/bottom-to-text-spacious-medium` | `accordion-top/bottom-to-text-spacious-large` | `accordion-top/bottom-to-text-spacious-extra-large` | + +> `top/bottom` is shorthand; each cell represents two tokens: `accordion-top-to-text-*` and `accordion-bottom-to-text-*`. + +**Regular** density: `swc-accordion-item` sets `--swc-accordion-item-padding-top` and `--swc-accordion-item-padding-bottom` on `:host([size])` (medium uses the private-wrapper fallback on `.swc-AccordionItem-header`). + +**Compact** and **spacious** density: `swc-accordion` overrides the same exposed properties on slotted items via `:host([density="…"]) ::slotted(swc-accordion-item)` and `:host([density="…"][size="…"]) ::slotted(swc-accordion-item)` (medium uses the base `:host([density="…"])` rule; s/l/xl use compound density + size selectors). + +--- + ## Migration checklist Gates align with [01_washing-machine-workflow.md](../../02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_washing-machine-workflow.md). @@ -294,7 +349,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 @@ -312,8 +367,8 @@ 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)) +- [x] S2 CSS integrated; stylelint clean +- [x] Document token / `--mod-*` → S2 (or `--swc-*`) mapping for consumers; include **`density` × `size`** matrix ### Accessibility @@ -329,8 +384,10 @@ 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)) -- [ ] Do not document arrow-key navigation between headers for 2nd-gen (contrast with legacy README) +- [x] JSDoc, usage docs, Storybook stories (**no** `noInlinePadding`-style attribute) +- [ ] Storybook story and `@cssprop` docs for no-inline-padding parity via `--swc-accordion-item-edge-to-content-area` +- [x] Document remaining exposed `--swc-accordion-item-*` custom properties in `@cssprop` (Storybook API panel) +- [x] Do not document arrow-key navigation between headers for 2nd-gen (contrast with legacy README) ### Review @@ -348,11 +405,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 +428,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 5f32eaa712f..00000000000 --- 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 |