diff --git a/.ai/skills/accessibility-migration-analysis/SKILL.md b/.ai/skills/accessibility-migration-analysis/SKILL.md index 7e305f52c75..0e0c7e0f4d4 100644 --- a/.ai/skills/accessibility-migration-analysis/SKILL.md +++ b/.ai/skills/accessibility-migration-analysis/SKILL.md @@ -246,20 +246,16 @@ In the **body**, point to the **Browse mode (document/scan mode)** section. Add ## Pull request -When the analysis doc is complete and ready for review, generate a GitHub PR description using the template below. +When the analysis doc is complete and ready for review, generate a GitHub PR description using the template below. Always output the filled-in description inside a single fenced `markdown` code block so the user can copy it in one action. ### Variable substitution rules -| Placeholder | Value | Example | -| ------------------------------------------ | --------------------------------------------------------------------- | -------------------------------- | -| `{{component-name}}` | Package name, kebab-case | `dropzone`, `picker-button` | -| `{{branch-name}}` | GitHub branch, format `/swc---a11y` | `nikkimk/swc-2146-dropzone-a11y` | -| `{{component-a11y-migration-JIRA-ticket}}` | Jira ID for this component's a11y migration analysis ticket | `SWC-2146` | -| `{{component-readable-name}}` | Human-readable name, first letter capitalized | `Dropzone`, `Color handle` | - -The description's first bullet ends "for the 2nd-gen color handle" in the source template — replace "color handle" with `{{component-readable-name}}` when filling in. - -The Manual review test cases section contains a fixed reference to the color handle PR (`nikkimk/swc-2138-color-handle-a11y`). Keep that reference as-is; it is an anchor PR that all reviewers in this migration wave are expected to read. +| Placeholder | Value | Example | +| ------------------------------------------ | --------------------------------------------------------------------- | ----------------------- | +| `{{component-name}}` | Package name, kebab-case | `icon`, `picker-button` | +| `{{branch-name}}` | GitHub branch, format `/swc---a11y` | `nikkimk/icon-a11y` | +| `{{component-a11y-migration-JIRA-ticket}}` | Jira ID for this component's a11y migration analysis ticket | `SWC-2146` | +| `{{component-readable-name}}` | Human-readable name, first letter capitalized | `Icon`, `Color handle` | ### PR settings diff --git a/2nd-gen/packages/swc/.storybook/preview.ts b/2nd-gen/packages/swc/.storybook/preview.ts index 5dd361ece90..5ce221116b1 100644 --- a/2nd-gen/packages/swc/.storybook/preview.ts +++ b/2nd-gen/packages/swc/.storybook/preview.ts @@ -425,6 +425,8 @@ const preview = { ], 'Help text', ['Rendering and styling migration analysis'], + 'Icon', + ['Accessibility migration analysis'], 'Illustrated message', [ 'Accessibility migration analysis', diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md index a304e75d7c9..c922e1d7cb8 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md @@ -84,6 +84,8 @@ - [Grid migration roadmap](grid/rendering-and-styling-migration-analysis.md) - Help Text - [Help text migration roadmap](help-text/rendering-and-styling-migration-analysis.md) +- Icon + - [Icon accessibility migration analysis](icon/accessibility-migration-analysis.md) - Illustrated Message - [Illustrated message accessibility migration analysis](illustrated-message/accessibility-migration-analysis.md) - [`sp-illustrated-message` Migration Plan](illustrated-message/migration-plan.md) diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/icon/accessibility-migration-analysis.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/icon/accessibility-migration-analysis.md new file mode 100644 index 00000000000..a17d8be3971 --- /dev/null +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/icon/accessibility-migration-analysis.md @@ -0,0 +1,225 @@ + + +[CONTRIBUTOR-DOCS](../../../README.md) / [Project planning](../../README.md) / [Components](../README.md) / Icon / Icon accessibility migration analysis + + + +# Icon accessibility migration analysis + + + +
+In this doc + +- [Overview](#overview) + - [Also read](#also-read) + - [The generic frame (`swc-icon`)](#the-generic-frame-swc-icon) + - [Per-icon workflow elements](#per-icon-workflow-elements) + - [What it is not](#what-it-is-not) + - [Related](#related) +- [ARIA and WCAG context](#aria-and-wcag-context) + - [Pattern in the APG](#pattern-in-the-apg) + - [Guidelines that apply](#guidelines-that-apply) +- [Related 1st-gen accessibility (Jira)](#related-1st-gen-accessibility-jira) +- [Recommendations: `` and workflow icons](#recommendations-swc-icon-and-workflow-icons) + - [ARIA roles, states, and properties](#aria-roles-states-and-properties) + - [Shadow DOM and cross-root ARIA Issues](#shadow-dom-and-cross-root-aria-issues) + - [Accessibility tree expectations](#accessibility-tree-expectations) + - [Keyboard and focus](#keyboard-and-focus) +- [Testing](#testing) + - [Automated tests](#automated-tests) + - [Manual screen reader testing](#manual-screen-reader-testing) +- [Summary checklist](#summary-checklist) +- [References](#references) + +
+ + + +## Overview + +This document covers the accessibility requirements and recommendations for **``** (the generic SVG frame) and **per-icon workflow elements** such as ``. Both are built on `IconBase` and share identical ARIA behavior. The target standard is **WCAG 2.2 Level AA**. + +### Also read + +[Icon migration roadmap](./rendering-and-styling-migration-analysis.md) for layout, CSS, and DOM changes. + +### The generic frame (`swc-icon`) + +`` accepts an SVG in its default slot and controls whether that SVG is exposed to assistive technology or hidden from it: + +- **Decorative** (no `label`): the common case. The icon is inside an already-labeled control such as a button or menu item. Both host and SVG receive `aria-hidden="true"` so the icon does not generate a duplicate announcement. +- **Meaningful** (with `label`): an exotic, discouraged use case. Only appropriate when the icon genuinely stands alone with no ancestor providing a role and accessible name. The slotted SVG is exposed as an image with the `label` value as its accessible name. + +### Per-icon workflow elements + +Per-icon workflow elements (``, ``, etc.) extend `IconBase` and bake their SVG content in during element definition rather than accepting it via slot. From an accessibility standpoint, the behavior is identical to ``: + +- `label` absent → decorative, both host and SVG are hidden. +- `label` present → meaningful, SVG exposes as `img` with the label as its accessible name. + +One authoring difference matters for accessibility: **the element tag name describes shape, not function.** `` tells the browser what shape to render; it says nothing about why the icon is there. + +In practice, workflow icons are almost always decorative. The parent element — a ``, ``, or menu item — carries the `label` that assistive technology announces, and the icon itself needs no `label` at all. Standalone use of a workflow icon with `label` set is exotic and discouraged; confirm there is no viable ancestor element that can carry the role and accessible name before reaching for it. When a valid standalone use case exists, `label` must describe the function in context — `label="Add to favorites"`, not `label="Star"`. + +### What it is not + +- A button, link, or interactive element. To add click behavior to an icon, wrap it in `` or another interactive control; do not add event listeners directly to `` or a per-icon workflow element. +- A font icon or CSS pseudo-element. Both element types render real SVG markup. + +### Related + +- **UI icon functions** (internal): consumed directly by components such as `` and ``; not used by authors. Accessibility is component-controlled. + +--- + +## ARIA and WCAG context + +### Pattern in the APG + +The [APG](https://www.w3.org/WAI/ARIA/apg/) does not define a named pattern for a generic SVG icon wrapper. Relevant guidance comes from the APG's treatment of images and decorative content: + +- A **decorative** icon inside an already-labeled control must be hidden from assistive technology to prevent duplicate announcement. +- A **standalone meaningful** icon needs `role="img"` and an accessible name (`aria-label`), because SVG elements do not have a reliable implicit ARIA role across all browsers and assistive technologies. This pattern is technically correct but represents an exotic, discouraged use case; the strongly preferred pattern is an ancestor element that carries the role and accessible name. +- A **custom element** has no implicit ARIA role; `role="img"` must be applied explicitly to the element that carries the semantics. + +### Guidelines that apply + +| Idea | Plain meaning | +|------|---------------| +| [Non-text content (WCAG 1.1.1)](https://www.w3.org/TR/WCAG22/#non-text-content) | Every meaningful image needs a text alternative. Decorative images must be hidden from assistive technology. | +| [Use of color (WCAG 1.4.1)](https://www.w3.org/TR/WCAG22/#use-of-color) | Color alone must not convey meaning. The accessible name, not the icon's color, must carry semantic intent. | +| [Contrast — minimum (WCAG 1.4.3)](https://www.w3.org/TR/WCAG22/#contrast-minimum) | Text rendered inside SVG must meet 4.5:1 against its background. | +| [Non-text contrast (WCAG 1.4.11)](https://www.w3.org/TR/WCAG22/#non-text-contrast) | The icon fill must meet 3:1 contrast against the adjacent background for UI components. | + +**Bottom line:** Icons should almost always be decorative, relying on an ancestor element such as `` or `` to carry the role and accessible name. Using an icon as the sole communication of meaning — setting `label` and exposing `role="img"` with no labeled ancestor — is an exotic use case that should be discouraged unless a valid use case can be confirmed. When such a use case exists, the icon must have `label` set; leaving it empty while the icon is the only conveyance of meaning fails WCAG 1.1.1. + +--- + +## Related 1st-gen accessibility (Jira) + +| Jira | Type | Status (snapshot) | Resolution (snapshot) | Summary | +|------|------|-------------------|------------------------|---------| +| [SWC-875](https://jira.corp.adobe.com/browse/SWC-875) | Epic | Done | Fixed | Update icon components for accessibility | +| [SWC-1337](https://jira.corp.adobe.com/browse/SWC-1337) | Bug | To Do | Unresolved | Different icons depending on import path — may deliver different SVG shapes, which changes what assistive technology announces | + +--- + +## Recommendations: `` and workflow icons + +### ARIA roles, states, and properties + +| Topic | What to do | +|-------|------------| +| **One semantic role** | Icons are decorative by default. The parent element — ``, ``, a menu item, or similar — carries the accessible meaning, and the icon itself should have no `label` (and therefore `aria-hidden="true"`). Only in the rare case where an icon stands completely alone, with no labeled parent providing context, does it need `label` set and `role="img"` applied to the SVG. **Do not** set `role="button"`, `role="link"`, or any interactive role on the icon host. | +| **Host element role** | The host carries no explicit ARIA role; it is a transparent custom-element container. The SVG is the semantic anchor for meaningful icons. This differs from the RFC's stated "host owns semantics" intent: `role="img"` and `aria-label` are applied to the SVG rather than the host. Both approaches produce a correct accessibility tree; the SVG-centric approach avoids needing a separate step to hide the SVG. The ARIA snapshot confirms the correct outcome: `img "Search"`. | +| **Decorative (no `label`)** | `updateHostAccessibility()` sets `aria-hidden="true"` on the host. `updateSlottedIcon()` sets `aria-hidden="true"` on the SVG and removes `aria-label`. Both are hidden. This is the default and the most common case. | +| **Meaningful (with `label`)** | An exotic, discouraged use case. Before using it, confirm there is no viable ancestor element that can carry the role and accessible name. When a valid standalone use case exists: `updateSlottedIcon()` sets `role="img"` and `aria-label` equal to the `label` value on the SVG, then removes `aria-hidden` from the SVG. `updateHostAccessibility()` removes `aria-hidden` from the host. The host becomes a transparent container; the SVG carries the role and name. | +| **Label is authoritative** | `updateSlottedIcon()` overwrites whatever ARIA attributes the SVG already carries. If an icon function emits its own `aria-label` or `role`, the frame replaces them with the host's `label` value (or hides them when `label` is empty). This resolves the RFC's "A11y suppression mechanism" open question via overwrite-on-slot. | +| **Per-icon element SVG source** | Per-icon workflow elements bake their SVG during element definition rather than accepting it via slot. `updateSlottedIcon()` still runs via `firstUpdated`, so the same ARIA attributes are applied to the baked SVG. No separate handling is needed. | +| **Workflow icon label content** | The element tag name describes shape, not function. `` says nothing about why the icon is present. When the icon is meaningful, `label` must describe the function in context: `label="Add to favorites"`, not `label="Star"`. When the icon is decorative (inside a labeled button), omit `label` entirely. | +| **`size`** | Visual only. Not mapped to any ARIA state or property. | +| **CSS custom properties** | `--swc-icon-color`, `--swc-icon-inline-size`, `--swc-icon-block-size` are visual only. No ARIA mapping. | +| **Docs expectation** | Document that icons should almost always be decorative and that the ancestor element carries the accessible name. Document that standalone use with `label` is exotic and discouraged; authors should confirm no ancestor can provide the role and name before using it. When standalone use is unavoidable, `label` must describe function in context, not icon shape. Document that `size` and color properties carry no ARIA meaning. Do not list Tab, Space, or Enter as valid icon keys. | + +### Shadow DOM and cross-root ARIA Issues + +None. The slotted SVG is in the light DOM. ARIA attributes are set directly on it via `querySelector` within `updateSlottedIcon()`. No `aria-labelledby` or `aria-describedby` ID references cross shadow boundaries. `` is not a form-associated element. + +### Accessibility tree expectations + +**Decorative icon (no `label`)** + +Host has `aria-hidden="true"` and is not in the accessibility tree. The SVG also has `aria-hidden="true"`. Assistive technology sees nothing for this element. Applies to both `` and per-icon workflow elements. + +**Meaningful `` (with `label`) — exotic, discouraged use case** + +Only appropriate when the icon has no ancestor providing a role and accessible name, and a valid standalone use case has been confirmed. Host has no `aria-hidden` and no explicit role; it is a transparent container. The SVG has `role="img"` and `aria-label="