diff --git a/.ai/README.md b/.ai/README.md index f6066d0813f..53f3cca0c4f 100644 --- a/.ai/README.md +++ b/.ai/README.md @@ -224,6 +224,15 @@ Skills are used on-demand. When a task matches a skill’s purpose, the agent re - Use when: On the analyze-rendering-and-styling step for one or more components; creating one markdown file per component at `CONTRIBUTOR-DOCS/03_project-planning/03_components/[component-name]/rendering-and-styling-migration-analysis.md` - Provides: Workflow summary (specs from CSS + SWC, three-way DOM comparison, CSS⇒SWC mapping table, summary). Full instructions in `CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_analyze-rendering-and-styling/cursor_prompt.md` +#### Consumer migration guide + +- **purpose**: Create per-component migration guides for application developers upgrading from 1st-gen Spectrum Web Components to 2nd-gen components +- **How to invoke**: Say “create a consumer migration guide for [component]”, “write an upgrade guide for [component]”, or “document how consumers migrate [component] from 1st-gen to 2nd-gen”. +- Use when: Writing one Storybook-renderable MDX file per component at `2nd-gen/packages/swc/components/[component-name]/consumer-migration-guide.mdx` with code updates, styling guidance, accessibility notes, testing changes, and rollout advice +- Provides: Workflow summary (verified source inputs, required section order, before/after examples, migration checklist, rollout guidance). Full instructions in `.ai/skills/consumer-migration-guide/references/consumer-migration-guide-prompt.md` + +#### Washing machine migration workflow + #### Migration — phase 1: prep (`migration-prep`) - **purpose**: Understand the component, plan breaking changes, and define scope before any refactoring begins diff --git a/.ai/skills/consumer-migration-guide/SKILL.md b/.ai/skills/consumer-migration-guide/SKILL.md new file mode 100644 index 00000000000..c9705627423 --- /dev/null +++ b/.ai/skills/consumer-migration-guide/SKILL.md @@ -0,0 +1,96 @@ +--- +name: consumer-migration-guide +description: Use when creating a per-component migration guide for application developers upgrading from Spectrum 1 Web Components to Spectrum 2 components. +globs: 2nd-gen/packages/swc/components/*/consumer-migration-guide.mdx +alwaysApply: false +--- + +# Component migration: consumer guide + +Create per-component migration guidance for application developers upgrading app code from Spectrum 1 Web Components to Spectrum 2 components. The output should be practical and consumer-facing: what changed, what to update, how to test it, and how to roll it out safely. + +## When to use this skill + +- The user asks for a migration guide, upgrade guide, or consumer-facing migration doc for one or more components +- You are documenting what application developers need to change when replacing a Spectrum 1 component with its Spectrum 2 equivalent +- The guide needs rollout advice in addition to code updates, such as styling, accessibility, testing, and fallback considerations + +## How to invoke + +- Say "create a consumer migration guide for [component]" +- Or say "write an upgrade guide for [component]" or "document how consumers migrate [component] from Spectrum 1 to Spectrum 2" +- If the request is about maintainer-facing implementation analysis, use the migration-analysis skills instead + +## Quick reference + +### Output + +- **One `.mdx` file per component** at: + `2nd-gen/packages/swc/components/[component-name]/consumer-migration-guide.mdx` +- The file is Storybook-renderable MDX. Start every guide with this template so it picks up the `Components` title prefix wired in `2nd-gen/packages/swc/.storybook/main.ts`: + + ```mdx + import { Meta } from '@storybook/addon-docs/blocks'; + + + + # [Component name] consumer migration guide + ``` + + Use sentence case for `[Component name]` (for example `Badge`, `Action button`). Do **not** include `Components/` in the `` — `titlePrefix` already adds it, so the doc renders at `Components/[Component name]/Consumer migration guide`. + +- Consumer migration guides live alongside the Spectrum 2 component source so the doc ships with the component code. Do **not** add them to `CONTRIBUTOR-DOCS/`. +- **Do not link to project-planning / `CONTRIBUTOR-DOCS` docs** from the guide. Those are maintainer-facing; consumers don't need them. Link only to public consumer docs (e.g. the Spectrum 1 README on npm or the Spectrum 2 component Storybook page) when a link genuinely helps. +- **Nav:** The guide lives in the component directory, so the `CONTRIBUTOR-DOCS` `update-nav.js` script does not manage it. Do not register it in `CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md`, and do not include auto-generated breadcrumbs or TOC markers intended for that script. +- **MDX gotchas:** Keep bare tag names (``, ``, etc.) wrapped in backticks in prose, and keep HTML/JS examples inside fenced code blocks. Avoid loose `{` / `}` outside code blocks; MDX parses them as JS expressions. + +### Required source inputs + +Verify claims against the real implementation and docs before writing: + +- **Spectrum 1 docs and source:** `1st-gen/packages/[component-name]/README.md`, public element files such as `sp-*.ts`, stories, and tests when needed +- **Spectrum 2 docs and source:** `2nd-gen/packages/swc/components/[component-name]/src/`, stories, tests, and any package README or docs that describe the public API +- **Related migration docs:** the component's `rendering-and-styling-migration-analysis.md` and `accessibility-migration-analysis.md` when present + +### Important + +- Write for **application developers upgrading their code**, not only for component maintainers +- Prefer **before/after examples**, explicit upgrade actions, and rollout guidance over implementation detail +- Ask clarifying questions for uncertain mappings instead of guessing + +### Scope: minimal, public API only + +The guide must be **short, direct, and consumer-focused**. Optimize for scannability: a consumer should be able to complete a simple migration in under 5 minutes of reading. Prefer tables, short numbered steps, and before/after snippets over prose. Cut anything that is not strictly required to update product code. + +**Spectrum 2 supersedes Spectrum 1.** Verify every claim — especially styling hooks and public custom property names — against the actual Spectrum 2 source (`2nd-gen/packages/swc/components/[component-name]/` and `2nd-gen/packages/core/components/[component-name]/`). Do not carry Spectrum 1 conventions (e.g. `--mod-*` prefixes) into the guide unless the Spectrum 2 implementation actually uses them. + +**Testing is out of scope.** Do not include sections on test selector updates, ARIA snapshot changes, or VRT approval. Consumers own their own tests; the guide's job is to explain what changed in the component, not how to re-test a consumer's app. + +**Accessibility bullets do not duplicate code examples.** If an a11y action is already covered as a numbered step in `## Update your code` (which must include before/after snippets), the Accessibility bullet links to that step instead of repeating the snippet. Only include a code example in `## Accessibility` for an action that is not covered in `## Update your code`. + +**Do not include an `### Unchanged` sub-section in `## What changed`.** Unchanged API requires no consumer action and adds noise. Limit `## What changed` to `### Renamed`, `### Added in Spectrum 2`, and `### Removed in Spectrum 2` (omit any sub-section with no entries). + +**Include (public API):** + +- Tag name and import path changes +- Attributes, properties, and their values +- Slots and slot names +- Events +- Supported CSS custom properties (`--mod-*` and documented theming hooks) +- Accessibility expectations that affect the consumer's markup (e.g. `aria-label` on icon-only variants) +- Behavior changes the consumer can observe (focus, wrapping, positioning) + +**Exclude (implementation detail):** + +- Internal shadow DOM structure or how it changed +- Internal class-name renames (e.g. `spectrum-*` → `swc-*`); a single short "do not target internal classes or shadow DOM" caution is enough +- `::part()` shadow parts unless one is explicitly part of the public API +- Maintainer-facing rationale, migration sequencing, or implementation notes — link to the `rendering-and-styling-migration-analysis.md` instead + +**Structure the steps logically.** In "Update your code", present numbered steps in the order a consumer would actually perform them (for example: 1. update imports, 2. rename tags, 3. fix any consumer-facing accessibility gaps, 4. optionally adopt new attributes). Do not split content into parallel subsections when a short numbered flow reads better. + +## Full instructions + +For the exact document structure, required sections, source-verification expectations, writing rules, and checklist format, read: + +**.ai/skills/consumer-migration-guide/references/consumer-migration-guide-prompt.md** diff --git a/.ai/skills/consumer-migration-guide/references/consumer-migration-guide-prompt.md b/.ai/skills/consumer-migration-guide/references/consumer-migration-guide-prompt.md new file mode 100644 index 00000000000..ec2664a6d06 --- /dev/null +++ b/.ai/skills/consumer-migration-guide/references/consumer-migration-guide-prompt.md @@ -0,0 +1,216 @@ +[Consumer migration guide skill](../SKILL.md) / Full prompt + +# Spectrum consumer migration guide prompt + +For the **[COMPONENT_NAME]** component(s), create one consumer-facing migration guide per component at `2nd-gen/packages/swc/components/[component-name]/consumer-migration-guide.mdx`. + +The file must be **MDX**, not plain Markdown. Storybook's config (`2nd-gen/packages/swc/.storybook/main.ts`) picks up `**/*.mdx` under `../components` with `titlePrefix: 'Components'`, so the guide renders at `Components/[Component name]/Consumer migration guide`. + +The guide ships alongside the Spectrum 2 component source. Do **not** create or move this file under `CONTRIBUTOR-DOCS/`. + +These guides are for **application developers upgrading their code** from Spectrum 1 to Spectrum 2. The only question each section should answer is: **"What do I change in my product code?"** + +## Scope and length + +Keep the guide **short, direct, and scannable**. A consumer should be able to complete a simple migration in under 5 minutes of reading. Prefer a single change table, short numbered steps, and before/after snippets over prose. + +### Include (public API only) + +- Tag name and import path changes +- Attributes, properties, and accepted values +- Slots and content-rendering properties +- Events +- Supported CSS custom properties (documented `--mod-*` and other public theming hooks) +- Accessibility changes that affect consumer markup (e.g. where to place `aria-label`, when to hide decorative icons) +- Observable behavior changes (focus, wrapping, positioning) + +### Exclude + +- Internal shadow DOM structure or diffs between versions +- Internal class-name renames (e.g. `spectrum-*` → `swc-*`). One brief "do not target internal classes or shadow DOM" caution is enough +- `::part()` shadow parts unless a part is explicitly public API +- Maintainer-facing migration rationale or sequencing +- **Links to `CONTRIBUTOR-DOCS/` project-planning docs.** Those are maintainer-facing. Do not include them in the guide. + +### Structure steps logically + +In "Update your code", present **numbered steps in the order the consumer performs them** (for example: 1. update imports, 2. rename tags, 3. fix consumer-facing accessibility gaps, 4. optionally adopt new attributes). Do not split into parallel subsections when a short numbered flow reads better. + +## Source verification + +Before writing, verify claims against: + +- `1st-gen/packages/[component-name]/README.md` and public element files (`sp-*.ts`) +- `2nd-gen/packages/swc/components/[component-name]/src/`, stories, and tests + +If a claim is not confirmed by source, omit it. + +### Source precedence + +When sources disagree, follow this order of authority: + +1. **The shipped Spectrum 2 source** (`2nd-gen/packages/swc/components/[component-name]/` and `2nd-gen/packages/core/components/[component-name]/`) — ground truth for what the component actually does. +2. **The CSS style guide** (`CONTRIBUTOR-DOCS/02_style-guide/` and `.ai/rules/styles.md`) — recommendations here **outweigh** anything in a component's `rendering-and-styling-migration-analysis.md`. The analysis docs are early, component-specific planning artifacts; the style guide is the canonical, cross-component rule set and supersedes them when they conflict (for example on custom-property naming, prefixing, and public-vs-private boundaries). +3. **`rendering-and-styling-migration-analysis.md`** and other maintainer-facing analysis docs — use only for context and rationale. If an analysis doc suggests a public API shape that the Spectrum 2 source or the CSS style guide contradicts, trust the source and the style guide, not the analysis. + +## File and heading format + +Start every guide with this exact template: + +```mdx +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# [Component name] consumer migration guide +``` + +- Use sentence case for `[Component name]` (for example `Badge`, `Action button`). +- Do **not** prefix `` with `Components/` — `titlePrefix` supplies it. +- Use sentence case for all other headings. +- Prefer tables, bullets, and fenced code blocks over prose. + +**MDX rules:** + +- Wrap bare tag names in backticks in prose (`` `` ``, `` `` ``) +- Put all HTML/JS examples inside fenced code blocks +- Do not leave loose `{` / `}` outside code blocks +- Inside JSX elements (e.g. callout `
`s), wrap any `` content that contains `*`, `_`, or `<`/`>` in a JSX expression literal (e.g. `{'--mod-badge-*'}`). Otherwise MDX parses the markdown inside the JSX children and throws an "Expected the closing tag" indexing error. +- Do not include `CONTRIBUTOR-DOCS` breadcrumbs or TOCs + +## Required sections + +Use exactly this **H2** order. Omit any section that has no component-specific content rather than writing filler. + +1. `# [Component name] consumer migration guide` — H1, followed by **one sentence** summarizing the migration. +2. `## What changed` — up to three tables (`### Renamed`, `### Added in Spectrum 2`, `### Removed in Spectrum 2`). Omit any sub-section with no entries. **Never** include an `### Unchanged` sub-section. +3. `## Update your code` — numbered steps in the order the consumer performs them. Every step includes a before/after snippet. +4. `## Accessibility` — consumer-facing a11y actions only. Do not repeat code examples already shown in `## Update your code` — link back to the relevant step instead. Skip if nothing changed. +5. `## Styling` — only public CSS custom properties and a one-line "don't target internals" caution. Skip if nothing changed. +6. `## Checklist` — `- [ ]` task list of the concrete actions the consumer must take. + +Do **not** add: `Overview`, `Before you migrate`, `Migration in one sentence`, `Who this guide is for`, `Also read`, `Testing`, `References`, `Unchanged`, or separator `---` rules between every section. Keep the document tight. + +**Testing is out of scope.** Consumers are responsible for updating their own tests; the guide should not include test-update instructions, snapshot guidance, or VRT approval steps. + +## Section requirements + +### H1 + one-sentence summary + +Example: + +```mdx +# Badge consumer migration guide + +Replace `` with `` and update the import. The public API is unchanged. +``` + +### `## What changed` + +Use up to three `###` sub-section tables — **only include a sub-section if it has entries**. Each sub-section is a table focused on one kind of change: + +- **`### Renamed`** — tag, import path, property prefixes, or other 1:1 renames. Columns: `Area | Spectrum 1 | Spectrum 2`. +- **`### Added in Spectrum 2`** — new attributes, variants, slots, or custom properties the consumer may adopt. Columns: `Addition | Notes`. +- **`### Removed in Spectrum 2`** — removed public API with replacement guidance. Columns: `Removed | Replacement`. + +Do **not** include an `### Unchanged` sub-section. Unchanged API requires no consumer action and adds noise. + +Cover only **public API** — tags, import paths, attributes and accepted values, slots, events, documented CSS custom properties, and consumer-observable behavior. Do not list shadow-DOM internals, private `--_*` properties, internal class renames, or attributes set on shadow-DOM elements that consumers cannot select. + +### `## Update your code` + +Numbered `###` subheadings, in the order the consumer performs them. **Every step must include a minimal before/after snippet** using `` / `` comments inside a single fenced code block. Typical order: + +1. Update the import +2. Rename the tag +3. Fix consumer-facing accessibility gaps (only if applicable) +4. (Optional) Adopt new attributes (only if applicable) + +Skip any step that does not apply — do not write "no changes needed" filler. For optional adoption steps, the "before" can be the Spectrum 1 markup without the new attribute and the "after" is the Spectrum 2 markup with it. + +### `## Accessibility` + +Bulleted list of consumer-facing actions. **Do not duplicate code examples that already appear in `## Update your code`.** If the a11y action is represented as a numbered step above (e.g. "add `aria-label` to icon-only badges"), the bullet should summarize the rule and link to the step (`See [step N](#n-step-slug)`) — no snippet. Only include a code example for a11y actions that are **not** covered in `## Update your code`. Non-code bullets (e.g. "refactor interactive uses to X") remain text-only. + +### `## Styling` + +Document the **Spectrum 2 component's actual public custom properties** — not Spectrum 1's. The Spectrum 2 implementation supersedes Spectrum 1: verify the real property names, prefixes, and behavior directly in `2nd-gen/packages/swc/components/[component-name]/[component].css` and `2nd-gen/packages/core/components/[component-name]/`. Do **not** carry over Spectrum 1 `--mod-*` names unless the Spectrum 2 CSS actually uses them. + +Cover only: + +- The public custom properties the Spectrum 2 component exposes, as a **table** with `Custom property | Description | Notes` columns. Use the Notes column to call out scope constraints (e.g. "semantic variants only", "outline variants only", exclusions mandated by the CSS style guide). Leave Notes empty for properties with no constraint. +- Include this JSX comment immediately above the table so future passes can replace the hand-written descriptions with the canonical copy once it lands: + + ```mdx + {/* @todo Replace the Description column with the `@cssproperty` JSDoc descriptions from ``'s CEM entry once they are added in a follow-up PR. */} + ``` + +- **Required amber "breaking change" callout at the top** (immediately after the section intro sentence, before the property list) **if** the Spectrum 1 component used a different custom-property prefix (e.g. `--mod-*`) and Spectrum 2 does not. Tells consumers their Spectrum 1 overrides won't apply. Template: + + ```mdx +
+ ⚠️ Breaking change. Spectrum 1{' '} + {'--mod-[component]-*'} properties{' '} + do not apply to {''}. Remove + or replace every {'--mod-[component]-*'} override with the{' '} + {'--swc-[component]-*'} equivalents below. Not every Spectrum 1 + property has a 1:1 replacement, so read the list below carefully. +
+ ``` + +- **Required red "do not target internals" callout at the bottom** of the section, always — regardless of whether custom properties changed. Template: + + ```mdx +
+ 🚫 Do not target internals. Internal classes,{' '} + {'--_swc-[component]-*'} private properties, and shadow DOM are{' '} + not public API. Styling applied to them will break without + warning on minor releases. +
+ ``` + + Replace `[component]` in each template with the Spectrum 2 tag root (e.g. `badge` → ``, `--_swc-badge-*`, `--mod-badge-*`). Both callouts use inline-styled JSX divs, not blockquotes — blockquotes are not visually distinct enough for consumer-critical warnings. + + Immediately before the first callout in the section, include this JSX comment so every guide carries the same follow-up task: + + ```mdx + {/* @todo Replace the inline-styled callouts in this section with `` once it is migrated to Spectrum 2. */} + ``` + +Skip this section only if the component has no public styling hooks in either version. + +### `## Checklist` + +`- [ ]` task list of the concrete actions the consumer must take. Each item should map 1:1 to a step above. Keep it to the minimum viable list — no "consider" or "review" filler. + +## Writing style + +- Use imperative voice: "Replace", "Rename", "Add", "Do not". +- Cut adjectives, hedges, and any sentence that does not tell the consumer what to do. +- Do not duplicate information across sections. +- Do not include rollout playbooks, fallback strategies, or staged-migration advice unless the component genuinely requires it. + +## Quality bar + +Before finishing, confirm: + +- The guide fits on roughly one screen when scrolled, or reads in under 5 minutes. +- Every sentence tells the consumer something they must do, check, or know to migrate. +- No shadow DOM, internal class, or maintainer-facing content is present. +- No links to `CONTRIBUTOR-DOCS/` are present. diff --git a/.vscode/settings.json b/.vscode/settings.json index b89efb845eb..febc39c7838 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,6 +60,7 @@ "disableable", "effectful", "focusable", + "Focusgroup", "haspopup", "highcontrast", "labelledby", diff --git a/2nd-gen/packages/swc/.storybook/main.ts b/2nd-gen/packages/swc/.storybook/main.ts index d53de4dc142..cef907e71aa 100644 --- a/2nd-gen/packages/swc/.storybook/main.ts +++ b/2nd-gen/packages/swc/.storybook/main.ts @@ -58,6 +58,11 @@ const stories: StorybookConfig['stories'] = [ */ if (storybookMode !== 'ci-a11y') { stories.push( + { + directory: '../components', + files: '**/*.mdx', + titlePrefix: 'Components', + }, { directory: '../../core', files: '**/*.mdx', diff --git a/2nd-gen/packages/swc/components/badge/consumer-migration-guide.mdx b/2nd-gen/packages/swc/components/badge/consumer-migration-guide.mdx new file mode 100644 index 00000000000..6f750bd18be --- /dev/null +++ b/2nd-gen/packages/swc/components/badge/consumer-migration-guide.mdx @@ -0,0 +1,173 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Badge consumer migration guide + +Replace `` with `` and update the import. The public API is unchanged. + +## What changed + +### Renamed + +| Area | Spectrum 1 (`sp-badge`) | Spectrum 2 (`swc-badge`) | +| ---------------- | -------------------------------------- | ----------------------------------------- | +| Tag | `sp-badge` | `swc-badge` | +| Import path | `@spectrum-web-components/badge` | `@adobe/spectrum-wc/badge` | +| CSS custom props | `--mod-badge-*` / `--spectrum-badge-*` | `--swc-badge-*` (see [Styling](#styling)) | + +### Added in Spectrum 2 + +| Addition | Notes | +| ------------------------------------------------------------------ | ------------------------------------------------------------------------------ | +| `subtle` boolean attribute | Softer background fill; works with all variants | +| `outline` boolean attribute | Transparent background with border; **semantic variants only** | +| Color variants: `pink`, `turquoise`, `brown`, `cinnamon`, `silver` | Non-semantic color options | +| Custom properties for new styles | `--swc-badge-outline-background-color`, `--swc-badge-outline-label-icon-color` | + +### Removed in Spectrum 2 + +| Removed | Replacement | +| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--mod-badge-*` custom properties (e.g. `--mod-badge-background-color-positive`) | `--swc-badge-*` for semantic variants only — semantic per-variant overrides collapse into one `--swc-badge-background-color`. Non-semantic variants (`gray`, `red`, `seafoam`, etc.) no longer expose per-color overrides; retoken globally instead. | +| `--highcontrast-badge-border-color` | No 1:1 replacement — Spectrum 2 badge has no forced-colors override hook | +| Top-level exports `BADGE_VARIANTS`, `FIXED_VALUES`, `BadgeVariant`, `FixedValues` | Use `Badge.VARIANTS`, `Badge.FIXED_VALUES`, or `typeof Badge.prototype.variant` | + +## Update your code + +### 1. Update the import + +```js +// Before +import '@spectrum-web-components/badge/sp-badge.js'; +// After +import '@adobe/spectrum-wc/badge'; +``` + +### 2. Rename the tag + +```html + +Approved + +Approved +``` + +### 3. Fix consumer-facing accessibility gaps + +Add `aria-label` to icon-only badges so assistive tech announces a name: + +```html + + + + + + + + +``` + +Add `aria-hidden="true"` to decorative icons paired with text so screen readers don't announce the icon and label as duplicate content: + +```html + + + + Approved + + + + + Approved + +``` + +### 4. (Optional) Adopt new styles + +The `outline` attribute can only be used with semantic variants (`accent`, `informative`, `neutral`, `positive`, `notice`, `negative`). + +```html + +Active +Approved + +Active +Approved +``` + +## Accessibility + +- **Icon-only badges** must have `aria-label` on the host. See [step 3](#3-fix-consumer-facing-accessibility-gaps). +- **Decorative icons** paired with text should use `aria-hidden="true"` so screen readers don't announce duplicate content. See [step 3](#3-fix-consumer-facing-accessibility-gaps). +- Badges are non-interactive. Refactor any focusable or clickable badges to ``, ``, or a native button. + +## Styling + +Spectrum 2 exposes a new set of public CSS custom properties on ``. + +{/* @todo Replace the inline-styled callouts in this section with `` once it is migrated to Spectrum 2. */} + +
+ ⚠️ Breaking change. Spectrum 1 {'--mod-badge-*'}{' '} + properties do not apply to {''}. + Remove or replace every {'--mod-badge-*'} override with the{' '} + {'--swc-badge-*'} equivalents below. Not every Spectrum 1 + property has a 1:1 replacement, so read the list below carefully. +
+ +Public custom properties: + +{/* @todo Replace the Description column with the `@cssproperty` JSDoc descriptions from ``'s CEM entry once they are added in a follow-up PR. */} + +| Custom property | Description | Notes | +| -------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--swc-badge-background-color` | Background color of the badge. | **Semantic variants only** (`accent`, `neutral`, `informative`, `negative`, `positive`, `notice`).

Non-semantic color variants (`gray`, `red`, `orange`, `yellow`, `chartreuse`, `celery`, `green`, `seafoam`, `cyan`, `blue`, `indigo`, `purple`, `fuchsia`, `magenta`, `pink`, `turquoise`, `brown`, `cinnamon`, `silver`) apply their background via internal selectors and are not overrideable here — retoken globally instead. | +| `--swc-badge-label-icon-color` | Foreground color applied to the label text and icon. | | +| `--swc-badge-border-color` | Border color of the badge. | | +| `--swc-badge-corner-radius` | Corner radius of the badge. | Fixed-edge positioning intentionally forces the affected corners to `0`; your override still applies to the remaining corners. | +| `--swc-badge-font-size` | Font size of the label. | | +| `--swc-badge-line-height` | Line height of the label. | | +| `--swc-badge-height` | Minimum block size of the badge. | | +| `--swc-badge-gap` | Gap between the icon and label. | | +| `--swc-badge-padding-block` | Block (top/bottom) padding inside the badge. | | +| `--swc-badge-padding-inline` | Inline (start/end) padding inside the badge. | | +| `--swc-badge-with-icon-padding-inline` | Inline padding applied when the badge contains an icon. | | +| `--swc-badge-icon-size` | Size of the slotted icon. | | +| `--swc-badge-outline-background-color` | Background color used when `outline` is set. | Outline + semantic variants only. | +| `--swc-badge-outline-label-icon-color` | Label and icon color used when `outline` is set. | Outline + semantic variants only. | + +
+ 🚫 Do not target internals. Internal classes,{' '} + {'--_swc-badge-*'} private properties, and shadow DOM are{' '} + not public API. Styling applied to them will break without + warning on minor releases. +
+ +## Checklist + +- [ ] Update imports +- [ ] Rename all `` to `` +- [ ] Replace `--mod-badge-*` overrides on semantic variants with `--swc-badge-*`; retoken globally for non-semantic color variants +- [ ] Add `aria-label` to icon-only badges +- [ ] Add `aria-hidden="true"` to decorative icons +- [ ] Refactor any interactive badges diff --git a/CONTRIBUTOR-DOCS/03_project-planning/05_strategies/focus-management-strategy-rfc.md b/CONTRIBUTOR-DOCS/03_project-planning/05_strategies/focus-management-strategy-rfc.md index e8141c751de..8100dc6a801 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/05_strategies/focus-management-strategy-rfc.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/05_strategies/focus-management-strategy-rfc.md @@ -41,6 +41,7 @@ - [Category B: Focus delegates to an inner element (use `delegatesFocus: true`)](#category-b-focus-delegates-to-an-inner-element-use-delegatesfocus-true) - [Category C: Focus group containers (use `FocusgroupNavigationController`)](#category-c-focus-group-containers-use-focusgroupnavigationcontroller) - [7. What's Removed and Why](#7-whats-removed-and-why) + - [If you're looking for](#if-youre-looking-for) - [8. 1st-Gen vs 2nd-Gen Comparison](#8-1st-gen-vs-2nd-gen-comparison) - [9. Open Questions](#9-open-questions) - [Appendix A: Code Sketches](#appendix-a-code-sketches) @@ -81,21 +82,21 @@ Accepting this strategy resolves known accessibility defects, improves the consu ### Consumer Experience -5. **Removes "stranded focus" states.** In 1st-gen, clicking on non-interactive regions of a component (padding, decorative areas) can leave focus in an ambiguous state because `Focusable` relies on JavaScript `focus()`/`blur()` overrides to route clicks. With `delegatesFocus: true`, the browser natively forwards any click on the host to the first focusable child — no JavaScript routing needed, no edge cases where focus lands nowhere. +1. **Removes "stranded focus" states.** In 1st-gen, clicking on non-interactive regions of a component (padding, decorative areas) can leave focus in an ambiguous state because `Focusable` relies on JavaScript `focus()`/`blur()` overrides to route clicks. With `delegatesFocus: true`, the browser natively forwards any click on the host to the first focusable child — no JavaScript routing needed, no edge cases where focus lands nowhere. -6. **Ensures consistent behavior across browsers.** 1st-gen carries Safari-specific workarounds (e.g., `SAFARI_FOCUS_RING_CLASS` in Picker's `MobileController`) and Firefox-era `delegatesFocus` fallbacks. These platform-specific code paths create inconsistencies that surface as product bugs. The 2nd-gen approach relies on browser features that have been stable across all targets for 4+ years, eliminating the need for platform branching. +2. **Ensures consistent behavior across browsers.** 1st-gen carries Safari-specific workarounds (e.g., `SAFARI_FOCUS_RING_CLASS` in Picker's `MobileController`) and Firefox-era `delegatesFocus` fallbacks. These platform-specific code paths create inconsistencies that surface as product bugs. The 2nd-gen approach relies on browser features that have been stable across all targets for 4+ years, eliminating the need for platform branching. -7. **Makes disabled components behave predictably.** Because `aria-disabled` doesn't block click events (unlike native `disabled`), 1st-gen has no enforced pattern for guarding click handlers — some components check, some don't. `DisabledMixin` establishes a clear contract: the mixin handles host-level ARIA and tabindex; the component guards its own interaction handlers. This prevents the consumer-facing bug where clicking a "disabled" button still triggers its action. +3. **Makes disabled components behave predictably.** Because `aria-disabled` doesn't block click events (unlike native `disabled`), 1st-gen has no enforced pattern for guarding click handlers — some components check, some don't. `DisabledMixin` establishes a clear contract: the mixin handles host-level ARIA and tabindex; the component guards its own interaction handlers. This prevents the consumer-facing bug where clicking a "disabled" button still triggers its action. ### Author Maintenance -8. **Reduces the component authoring surface.** A 1st-gen focusable component must: extend `Focusable`, implement a `focusElement` getter (runtime-only enforcement), understand when `selfManageFocusElement` applies, avoid conflicting with `manipulatingTabindex`, and manually re-dispatch focus/blur events. A 2nd-gen component adds `delegatesFocus: true` (one line) and optionally mixes in `DisabledMixin`. The focusElement getter, tabIndex interception, and polyfill coordination are gone entirely. +1. **Reduces the component authoring surface.** A 1st-gen focusable component must: extend `Focusable`, implement a `focusElement` getter (runtime-only enforcement), understand when `selfManageFocusElement` applies, avoid conflicting with `manipulatingTabindex`, and manually re-dispatch focus/blur events. A 2nd-gen component adds `delegatesFocus: true` (one line) and optionally mixes in `DisabledMixin`. The focusElement getter, tabIndex interception, and polyfill coordination are gone entirely. -9. **Eliminates the runtime-only `focusElement` contract.** The 1st-gen `focusElement` getter throws at runtime if not implemented — there is no compile-time enforcement. This means missing or incorrect implementations are only caught during manual testing. 2nd-gen eliminates this contract: `delegatesFocus` delegates to the first focusable child by template order, which is verifiable by reading the template. +2. **Eliminates the runtime-only `focusElement` contract.** The 1st-gen `focusElement` getter throws at runtime if not implemented — there is no compile-time enforcement. This means missing or incorrect implementations are only caught during manual testing. 2nd-gen eliminates this contract: `delegatesFocus` delegates to the first focusable child by template order, which is verifiable by reading the template. -10. **Unblocks migration of all 24 focusable components.** Every component extending `Focusable` is blocked until the replacement primitives exist. This proposal delivers those primitives and categorizes all 24 components into three migration patterns (A/B/C in [§6](#6-component-migration-guide)), providing a concrete path for each. +3. **Unblocks migration of all 24 focusable components.** Every component extending `Focusable` is blocked until the replacement primitives exist. This proposal delivers those primitives and categorizes all 24 components into three migration patterns (A/B/C in [§6](#6-component-migration-guide)), providing a concrete path for each. -11. **Aligns with the platform trajectory.** `FocusgroupNavigationController` mirrors the [Open UI `focusgroup` attribute](https://open-ui.org/components/focusgroup.explainer/) so that when browsers ship native focus-group behavior, the controller can be progressively deprecated rather than wholesale replaced — reducing future migration cost. +4. **Aligns with the platform trajectory.** `FocusgroupNavigationController` mirrors the [Open UI `focusgroup` attribute](https://open-ui.org/components/focusgroup.explainer/) so that when browsers ship native focus-group behavior, the controller can be progressively deprecated rather than wholesale replaced — reducing future migration cost. --- @@ -215,6 +216,7 @@ class MyTextfield extends DisabledMixin(SpectrumElement) { **When to use:** Any interactive component that can be disabled — buttons, inputs, links, menu items, sliders, etc. **What it does:** + - Adds `disabled` as a reflected boolean property - Sets `aria-disabled="true"` when disabled - Removes element from tab order (`tabindex="-1"`) when disabled @@ -231,6 +233,7 @@ class MyTextfield extends DisabledMixin(SpectrumElement) { | Screen readers | Announces disabled + unavailable | Announces disabled, element still discoverable | For **web components**, `aria-disabled` is the right default because: + - Custom elements are not native form controls — the browser's `disabled` attribute has no built-in effect on a custom element host. We'd be reimplementing the behavior either way. - Keeping the host discoverable (focusable + announced) is generally better UX — screen reader users can still find the element, understand what it does, and learn why it's disabled via surrounding context or tooltips. - Components that wrap a native form control (e.g., textfield wrapping ``) should **also** set the native `disabled` on the inner element to get correct platform behavior (no value submission, native styling). The mixin handles the host; the component's `render()` handles the inner element. @@ -251,6 +254,7 @@ See [Appendix A.1](#a1-disabledmixin) for the full implementation sketch. This is the primary mechanism for focus delegation in 2nd-gen, replacing the entire `Focusable` base class and its `focusElement` getter pattern. **What it does:** + - Calling `.focus()` on the host automatically focuses the first focusable child in the shadow DOM — custom `focus()` and `blur()` overrides become redundant - Clicks anywhere on the host element — including padding areas and decorative regions outside the inner control — automatically forward focus to the first focusable child, eliminating "stranded focus" states - The host matches `:focus` and `:focus-within` when any inner element is focused, enabling unified focus styling via CSS alone (no JavaScript class toggling needed) @@ -277,7 +281,7 @@ class MyTextfield extends DisabledMixin(SpectrumElement) { 1. **Do not set `tabindex` on the host element.** When `delegatesFocus` is active, adding `tabindex="0"` to the host creates **two tab stops** — the host receives focus first, then the inner element. This breaks keyboard navigation. `DisabledMixin` should only set `tabindex="-1"` (to remove from tab order when disabled), never `tabindex="0"`. -2. **Focus/blur events do not bubble out of the shadow root.** `delegatesFocus` handles focus *routing* but not event *bubbling*. Native `focus` and `blur` events are trapped inside the shadow boundary. Components that need to expose `focus`/`blur` events to consumers must re-dispatch them as composed, bubbling events from the host: +2. **Focus/blur events do not bubble out of the shadow root.** `delegatesFocus` handles focus _routing_ but not event _bubbling_. Native `focus` and `blur` events are trapped inside the shadow boundary. Components that need to expose `focus`/`blur` events to consumers must re-dispatch them as composed, bubbling events from the host: ```typescript private _handleFocus(event: FocusEvent): void { @@ -356,6 +360,7 @@ Use this flowchart to determine which approach a component should use: **When to use:** Composite widgets that should appear as a **single tab stop** with arrow key navigation between children — following WAI-ARIA patterns for toolbars, tablists, menu bars, listboxes, and grids. **What it handles:** + - Arrow key navigation across four direction modes: `horizontal`, `vertical`, `both`, and `grid` - RTL-aware — horizontal arrow keys respect the host's `dir` attribute - Home/End key jumps (Ctrl+Home / Ctrl+End in grid mode jump to first/last cell) @@ -395,6 +400,7 @@ Tab out, then Tab back in → returns to B (memory: true): ``` **Examples:** + - `` — roving across action buttons (horizontal) - `` — roving across radios with auto-selection via `onActiveItemChange` - `` — roving across tab elements @@ -498,6 +504,7 @@ public hasVisibleFocusInTree(): boolean { > CSS selector strings matching focusable and tabbable DOM elements per the HTML spec. Two exports: + - `focusableSelector` — matches elements that can receive focus programmatically (via `.focus()`) - `tabbableSelector` — subset reachable via Tab key (excludes `tabindex="-1"`) @@ -505,7 +512,7 @@ Two exports: See [Appendix A.5](#a5-focusable-selectorsts) for the full selector definitions. -#### `first-focusable-in.ts` *(low priority)* +#### `first-focusable-in.ts` _(low priority)_ > Finds the first focusable element in a DOM subtree or among a slot's assigned nodes. @@ -526,11 +533,11 @@ See [Appendix A.5](#a5-focusable-selectorsts) for the full selector definitions. ### Phase 2: Controller -4. Add `FocusgroupNavigationController` to `controllers/` (consolidates 1st-gen `FocusGroupController` + `RovingTabindexController`, aligned with Open UI `focusgroup`) +Add `FocusgroupNavigationController` to `controllers/` (consolidates 1st-gen `FocusGroupController` + `RovingTabindexController`, aligned with Open UI `focusgroup`) ### Phase 3: Mixin -5. Add `DisabledMixin` to `mixins/` +Add `DisabledMixin` to `mixins/` ### Phase 4: Component Migration @@ -649,7 +656,7 @@ The following 1st-gen concepts are **not carried forward** to 2nd-gen. Each remo - **`selfManageFocusElement`** — A boolean getter override used by exactly two components (ActionMenu and Picker) to opt out of `Focusable`'s automatic tabIndex management. It exists because `Focusable`'s tabIndex interception conflicts with `RovingTabindexController` when both try to manage the same element's tabIndex. The conflict is structural to the inheritance approach — `Focusable` assumes it owns tabIndex, but the controller also needs to set it. In 2nd-gen, `Focusable`'s tabIndex interception is gone entirely, so the conflict cannot occur and the escape hatch is unnecessary. -- **1st-gen `FocusGroupController` / `RovingTabindexController`** — Two separate controllers with overlapping responsibilities (`FocusGroupController` as a base class, `RovingTabindexController` as a subclass). The split forced components to understand which class to use and created an inconsistent API surface. Superseded by `FocusgroupNavigationController`, which consolidates both into a single controller aligned with the Open UI `focusgroup` attribute, adding bounding-rect grid layout, RTL support, typeahead, and page navigation. See the [design evolution note in §4.4](#design-evolution-rovingtabindexcontroller--focusgroupnavigationcontroller) for the full API mapping. +- **1st-gen `FocusGroupController` / `RovingTabindexController`** — Two separate controllers with overlapping responsibilities (`FocusGroupController` as a base class, `RovingTabindexController` as a subclass). The split forced components to understand which class to use and created an inconsistent API surface. Superseded by `FocusgroupNavigationController`, which consolidates both into a single controller aligned with the Open UI `focusgroup` attribute, adding bounding-rect grid layout, RTL support, typeahead, and page navigation. See [§4.4](#44-focusgroupnavigationcontroller) for the full API mapping. - **`focus-visible.ts`** — The entire polyfill loader file: script injection, `data-js-focus-visible` attribute management, and global event listeners for tracking focus method (keyboard vs pointer). All of this is replaced by the browser's native `:focus-visible` pseudo-class, which requires no JavaScript. @@ -657,7 +664,7 @@ The following 1st-gen concepts are **not carried forward** to 2nd-gen. Each remo - **Autofocus synthetic KeyboardEvent hack** — 1st-gen dispatched a synthetic `KeyboardEvent` followed by two `requestAnimationFrame` waits to trick the polyfill into showing a focus ring on autofocused elements. Browsers now correctly apply `:focus-visible` to programmatically focused elements, making the hack unnecessary. -#### If you're looking for... +### If you're looking for | 1st-gen API | 2nd-gen replacement | Notes | |---|---|---| @@ -667,7 +674,7 @@ The following 1st-gen concepts are **not carried forward** to 2nd-gen. Each remo | `selfManageFocusElement` | Not needed | Conflict no longer exists | | `[focusable]` attribute | Not needed | Host is natively focusable via delegation | | `FocusGroupController` | `FocusgroupNavigationController` | See [§4.4](#44-focusgroupnavigationcontroller) | -| `RovingTabindexController` (1st-gen) | `FocusgroupNavigationController` | See [§4.4 evolution note](#design-evolution-rovingtabindexcontroller--focusgroupnavigationcontroller) | +| `RovingTabindexController` (1st-gen) | `FocusgroupNavigationController` | See [§4.4](#44-focusgroupnavigationcontroller) | | `FocusVisiblePolyfillMixin` | Native `:focus-visible` | No import needed | | `hasVisibleFocusInTree()` | Same method, simplified | Uses `getActiveElement()` internally | @@ -696,7 +703,7 @@ The following 1st-gen concepts are **not carried forward** to 2nd-gen. Each remo - Not included in the initial `FocusgroupNavigationController`. Will be added (or extracted to a subclass) when a virtualizing component is migrated. 3. **Components where the focus target is not the first focusable child:** - - SidenavItem, BreadcrumbItem, and AccordionItem currently delegate to inner elements that may not be first in the shadow DOM. + - SidenavItem, BreadcrumbItem, and AccordionItem currently delegate to inner elements that may not be first in the shadow DOM. - **During migration, their templates should be restructured so the intended focus target comes first.** - If restructuring isn't feasible for a specific component, that component can override `focus()` directly as a one-off. @@ -920,7 +927,7 @@ class SpTabs extends SpectrumElement { ### A.5 `focusable-selectors.ts` -Referenced from [§4.5 focusable-selectors.ts](#focusable-selectorsts). +Referenced from [§4.5 focusable-selectors.ts](#45-utilities). ```typescript /** Matches elements that can receive focus programmatically (via .focus()). */ @@ -949,7 +956,7 @@ export const tabbableSelector = focusableSelector ### A.6 `get-active-element.ts` -Referenced from [§4.5 get-active-element.ts](#get-active-elementts). +Referenced from [§4.5 get-active-element.ts](#45-utilities). ```typescript /** @@ -971,7 +978,7 @@ export function getActiveElement( ### A.7 Updated `hasVisibleFocusInTree()` -Referenced from [§4.5 get-active-element.ts](#get-active-elementts) and [§2](#2-what-exists-in-2nd-gen-today). +Referenced from [§4.5 get-active-element.ts](#45-utilities) and [§2](#2-what-exists-in-2nd-gen-today). Shows how the existing `SpectrumMixin` method simplifies by using `getActiveElement()`: @@ -1017,6 +1024,7 @@ public hasVisibleFocusInTree(): boolean { This section documents the target architecture for when overlay/dialog/dropdown components are migrated. It is not part of the core focus management work. **Target approach:** + - Use `.showModal()` for modals (native focus trap + Escape handling + `::backdrop`) - Use `inert` attribute on background content for non-dialog modals - Restore focus to trigger element on close via a `returnFocus()` utility