Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ai/rules/code-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions .ai/skills/migration-styling/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 (`<button>`, `<input>`, `<a>`) get correct forced-colors treatment automatically — `ButtonText`, focus `Highlight`, disabled `GrayText` — without any CSS override. Only non-semantic elements (a decorative `<div>` or a `<span>` using `background-color` as a visual indicator) need explicit overrides. Do not carry over forced-colors rules from 1st-gen Spectrum CSS without first verifying the 2nd-gen component uses non-semantic markup that requires them.

→ See [01_component-css#forced-colors-requirements](../../../../CONTRIBUTOR-DOCS/02_style-guide/01_css/01_component-css.md#forced-colors-requirements)

### 8. Browser API selectors
Expand Down
208 changes: 208 additions & 0 deletions 2nd-gen/packages/core/components/accordion/Accordion.base.ts
Original file line number Diff line number Diff line change
@@ -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' }
);
}
}
}
33 changes: 33 additions & 0 deletions 2nd-gen/packages/core/components/accordion/Accordion.types.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading
Loading