From 697cd0d4539a4855fae6a12748b138a5a8eeb546 Mon Sep 17 00:00:00 2001
From: Nikki Massaro <5090492+nikkimk@users.noreply.github.com>
Date: Wed, 1 Apr 2026 09:35:09 -0400
Subject: [PATCH 1/7] docs: illustrated message a11y migration docs (#6116)
---
.../03_components/README.md | 1 +
.../accessibility-migration-analysis.md | 162 ++++++++++++++++++
...endering-and-styling-migration-analysis.md | 3 +
3 files changed, 166 insertions(+)
create mode 100644 CONTRIBUTOR-DOCS/03_project-planning/03_components/illustrated-message/accessibility-migration-analysis.md
diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md
index 4106284b062..92a561aa8a1 100644
--- a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md
+++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md
@@ -51,6 +51,7 @@
- Help Text
- [Help text migration roadmap](help-text/rendering-and-styling-migration-analysis.md)
- Illustrated Message
+ - [Illustrated message accessibility migration analysis](illustrated-message/accessibility-migration-analysis.md)
- [Illustrated message migration roadmap](illustrated-message/rendering-and-styling-migration-analysis.md)
- Infield Button
- [In-field button migration roadmap](infield-button/rendering-and-styling-migration-analysis.md)
diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/illustrated-message/accessibility-migration-analysis.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/illustrated-message/accessibility-migration-analysis.md
new file mode 100644
index 00000000000..03d6d125997
--- /dev/null
+++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/illustrated-message/accessibility-migration-analysis.md
@@ -0,0 +1,162 @@
+
+
+[CONTRIBUTOR-DOCS](../../../README.md) / [Project planning](../../README.md) / [Components](../README.md) / Illustrated Message / Illustrated message accessibility migration analysis
+
+
+
+# Illustrated message accessibility migration analysis
+
+
+
+
+In this doc
+
+- [Overview](#overview)
+ - [Also read](#also-read)
+ - [What `swc-illustrated-message` is](#what-swc-illustrated-message-is)
+ - [Heading hierarchy and page context](#heading-hierarchy-and-page-context)
+- [ARIA and WCAG context](#aria-and-wcag-context)
+ - [Pattern in the APG](#pattern-in-the-apg)
+ - [Guidelines that apply](#guidelines-that-apply)
+- [Recommendations: ``](#recommendations-swc-illustrated-message)
+ - [Heading slot, `heading` attribute, and `heading-level`](#heading-slot-heading-attribute-and-heading-level)
+ - [Other content and slots](#other-content-and-slots)
+ - [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)
+- [Known 1st-gen issues](#known-1st-gen-issues)
+- [Testing](#testing)
+ - [Automated tests](#automated-tests)
+- [Summary checklist](#summary-checklist)
+- [References](#references)
+
+
+
+
+
+## Overview
+
+This doc defines how `swc-illustrated-message` should work for accessibility and heading semantics. It targets WCAG 2.2 Level AA.
+
+### Also read
+
+- [Illustrated message migration roadmap](./rendering-and-styling-migration-analysis.md) for layout, CSS, DOM, and Spectrum 2 gaps.
+
+### What `swc-illustrated-message` is
+
+- A composed empty state or explanatory block: illustration (often SVG), a title-like line, description text, and optionally actions.
+- It can appear anywhere on a page—inside a dialog, under a page `h1`, as the only content of a section, nested in cards, etc.
+
+### Heading hierarchy and page context
+
+The component cannot know which `h2`–`h6` level is correct for the page; authors must set that explicitly. The only supported pattern is: title text comes from the `heading` attribute and/or the `heading` slot (see below), and the semantic heading element is always created in shadow DOM.
+
+Do not use `h1` for the illustrated message title. `h1` is for the primary page (or dialog / sheet title outside this block). This component exposes `h2`–`h6` only via `heading-level` (`2`–`6`).
+
+2nd-gen must implement:
+
+- `heading-level` property (attribute `heading-level`): integers `2`–`6`, default `2`. The shadow tree renders exactly one `
` … `
` matching that value. Values outside `2`–`6` (including `1`) must be clamped or coerced to `2`–`6` (for example `1` → `2`), or rejected in types with a documented default—pick one policy and document it in Storybook.
+- `heading` slot: accepts a `span` only (or equivalent documented phrasing: a single `span` wrapper as the slotted node). Do not allow slotted `
`–`
`; authors must not put heading elements in light DOM for this slot. Implementation may validate in dev and warn or ignore invalid slotted tags.
+- Optional `heading` attribute: when used, its text is rendered as the title inside the shadow heading (alongside or instead of slot content per product rules—document the precedence if both exist).
+
+This differs from putting a real heading in the slot (as in accordion item titles) and from 1st-gen, which always wraps the slot in `
` with no level control. Accordion still allows `level` `1`–`6` on the parent ([SWC-1466](https://jira.corp.adobe.com/browse/SWC-1466), [PR #5969](https://github.com/adobe/spectrum-web-components/pull/5969)); illustrated message uses `heading-level` `2`–`6` only (no `h1`).
+
+Documentation and Storybook must tell authors to set `heading-level` from document outline, not from visual preference alone.
+
+---
+
+## ARIA and WCAG context
+
+### Pattern in the APG
+
+- The APG does not define an “illustrated message” widget. Treat it as structured content: headings, text, optional controls.
+
+### Guidelines that apply
+
+| Idea | Plain meaning |
+|------|----------------|
+| [Info and relationships (WCAG 1.3.1)](https://www.w3.org/TR/WCAG22/#info-and-relationships) | The programmatic heading level must reflect the document outline. Avoid a second `h1` inside this pattern—keep one top-level heading per page view. |
+| [Headings and labels (WCAG 2.4.6)](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html) | The title should describe topic purpose; `heading-level` should match sibling and parent headings (`h2`–`h6`). |
+| [Name, role, value (WCAG 4.1.2)](https://www.w3.org/TR/WCAG22/#name-role-value) | Action buttons and links need discernible names; decorative illustrations should not pollute the accessibility tree. |
+
+Bottom line: Authors choose `heading-level` (`2`–`6`, i.e. `h2`–`h6`) to match the page. The component always emits the corresponding heading in shadow DOM; the slot does not supply the heading tag.
+
+---
+
+## Recommendations: ``
+
+### Heading slot, `heading` attribute, and `heading-level`
+
+| Topic | What to do |
+|-------|------------|
+| No `h1` | Never render `
`. Do not accept `heading-level="1"`. `h1` belongs to the page, shell, or dialog title, not this block. |
+| `heading-level` | Required behavior: `2`–`6`, default `2`. Clamp or coerce out-of-range values; document behavior for invalid input (same spirit as accordion `getHeadingLevel()`). |
+| `heading` slot | Span only: document that the slot must contain a `span` (or stricter: exactly one root `span`). No slotted `
`–`
`. |
+| Shadow heading | Single heading element in shadow DOM; tag is `
+
+ Select a file
+ from your computer to get started.
+
+ `
+ )}
+ `,
+ tags: ['behaviors'],
+};
+DescriptionWithLink.storyName = 'Description with link';
+
// ────────────────────────────────
// ACCESSIBILITY STORIES
// ────────────────────────────────
/**
- * SVGs slotted into the illustration slot should declare their accessibility
- * intent explicitly:
+ * ### Features
+ *
+ * The `` element implements the following
+ * accessibility features:
+ *
+ * 1. **Heading ownership**: The consumer provides the `
`–`
` element
+ * directly in the `heading` slot, preserving full control over heading
+ * level and ensuring it participates in the document outline.
+ * 2. **Illustration intent**: SVGs in the default slot must declare their
+ * accessibility intent explicitly via `aria-hidden` or `role="img"`.
+ * 3. **No redundant ARIA**: The component adds no `role` or `aria-*`
+ * attributes of its own; semantics come entirely from slotted content.
+ *
+ * ### Best practices
*
- * - **Decorative** (most common): add `aria-hidden="true"` so screen readers
- * skip the graphic entirely.
- * - **Informative**: add `role="img"` and `aria-label` (or an inline ``)
- * so screen readers announce the illustration's meaning.
+ * - Decorative illustrations (most common): add `aria-hidden="true"` so
+ * screen readers skip the graphic and move directly to the heading.
+ * - Informative illustrations: add `role="img"` and `aria-label` (or an
+ * inline ``) so screen readers announce the illustration's meaning
+ * before reading the heading and description.
+ * - Choose the heading level (`h2`–`h6`) that fits the surrounding document
+ * outline, not based on visual size.
*/
export const IllustrationAccessibility: Story = {
render: (args) => html`
@@ -207,7 +345,7 @@ export const IllustrationAccessibility: Story = {
The icon above uses
aria-hidden="true"
- — screen readers skip the illustration entirely and move on to the
+ so screen readers skip the illustration entirely and move on to the
heading and description.
`
@@ -224,7 +362,7 @@ export const IllustrationAccessibility: Story = {
role="img"
and
aria-label
- — screen readers announce its meaning before reading the heading and
+ so screen readers announce its meaning before reading the heading and
description.
`
diff --git a/2nd-gen/packages/swc/components/illustrated-message/test/illustrated-message.a11y.spec.ts b/2nd-gen/packages/swc/components/illustrated-message/test/illustrated-message.a11y.spec.ts
index 6be85799553..e912cbdae61 100644
--- a/2nd-gen/packages/swc/components/illustrated-message/test/illustrated-message.a11y.spec.ts
+++ b/2nd-gen/packages/swc/components/illustrated-message/test/illustrated-message.a11y.spec.ts
@@ -49,4 +49,71 @@ test.describe('IllustratedMessage - ARIA Snapshots', () => {
// the shadow root and must not appear inside the shadow DOM.
await expect(root.locator('h2[slot="heading"]')).toBeVisible();
});
+
+ test('should have correct heading for each size variant', async ({
+ page,
+ }) => {
+ const root = await gotoStory(
+ page,
+ 'components-illustrated-message--sizes',
+ 'swc-illustrated-message'
+ );
+ await expect(root).toMatchAriaSnapshot(`
+ - heading "Small" [level=2]
+ - heading "Medium" [level=2]
+ - heading "Large" [level=2]
+ `);
+ });
+
+ test('should have correct heading for each orientation variant', async ({
+ page,
+ }) => {
+ const root = await gotoStory(
+ page,
+ 'components-illustrated-message--orientation',
+ 'swc-illustrated-message'
+ );
+ await expect(root).toMatchAriaSnapshot(`
+ - heading "Vertical (default)" [level=2]
+ - heading "Horizontal" [level=2]
+ `);
+ });
+
+ test('should hide decorative illustration from assistive technology', async ({
+ page,
+ }) => {
+ const root = await gotoStory(
+ page,
+ 'components-illustrated-message--illustration-accessibility',
+ 'swc-illustrated-message'
+ );
+ // The first component has aria-hidden="true" on its SVG.
+ // It must not produce an img role in the accessibility tree.
+ const first = root.locator('swc-illustrated-message').first();
+ await expect(first.getByRole('img')).toHaveCount(0);
+ // Heading is still accessible.
+ await expect(first).toMatchAriaSnapshot(`
+ - heading "Illustrated message title" [level=2]
+ `);
+ });
+
+ test('should expose informative illustration to assistive technology', async ({
+ page,
+ }) => {
+ const root = await gotoStory(
+ page,
+ 'components-illustrated-message--illustration-accessibility',
+ 'swc-illustrated-message'
+ );
+ // The second component has role="img" + aria-label on its SVG.
+ // It must appear as a named image in the accessibility tree.
+ const second = root.locator('swc-illustrated-message').nth(1);
+ await expect(
+ second.getByRole('img', { name: 'Cloud storage illustration' })
+ ).toBeVisible();
+ await expect(second).toMatchAriaSnapshot(`
+ - img "Cloud storage illustration"
+ - heading "Illustrated message title" [level=2]
+ `);
+ });
});
diff --git a/2nd-gen/packages/swc/components/illustrated-message/test/illustrated-message.test.ts b/2nd-gen/packages/swc/components/illustrated-message/test/illustrated-message.test.ts
index 68c2c75f759..58eb479fc70 100644
--- a/2nd-gen/packages/swc/components/illustrated-message/test/illustrated-message.test.ts
+++ b/2nd-gen/packages/swc/components/illustrated-message/test/illustrated-message.test.ts
@@ -17,9 +17,17 @@ import { IllustratedMessage } from '@adobe/spectrum-wc/illustrated-message';
import '@adobe/spectrum-wc/illustrated-message';
-import { getComponent, withWarningSpy } from '../../../utils/test-utils.js';
-import meta from '../stories/illustrated-message.stories.js';
-import { Overview } from '../stories/illustrated-message.stories.js';
+import {
+ getComponent,
+ getComponents,
+ withWarningSpy,
+} from '../../../utils/test-utils.js';
+import meta, {
+ IllustrationAccessibility,
+ Orientation,
+ Overview,
+ Sizes,
+} from '../stories/illustrated-message.stories.js';
// This file defines dev-only test stories that reuse the main story metadata.
export default {
@@ -44,7 +52,7 @@ export const OverviewTest: Story = {
'swc-illustrated-message'
);
- await step('consumer owns the heading element in light DOM', async () => {
+ await step('confirms heading element lives in light DOM', async () => {
const headingInLight =
illustratedMessage.querySelector('[slot="heading"]');
expect(headingInLight, 'heading element in light DOM').not.toBeNull();
@@ -57,10 +65,163 @@ export const OverviewTest: Story = {
},
};
+export const DefaultValuesTest: Story = {
+ render: () => html`
+
+ `,
+ play: async ({ canvasElement, step }) => {
+ const illustratedMessage = await getComponent(
+ canvasElement,
+ 'swc-illustrated-message'
+ );
+
+ await step('reflects default size "m" as attribute', async () => {
+ expect(illustratedMessage.size, 'default size property').toBe('m');
+ expect(
+ illustratedMessage.getAttribute('size'),
+ 'default size attribute'
+ ).toBe('m');
+ });
+
+ await step(
+ 'reflects default orientation "vertical" as attribute',
+ async () => {
+ expect(
+ illustratedMessage.orientation,
+ 'default orientation property'
+ ).toBe('vertical');
+ expect(
+ illustratedMessage.getAttribute('orientation'),
+ 'default orientation attribute'
+ ).toBe('vertical');
+ }
+ );
+ },
+};
+
+// ──────────────────────────────────────────────────────────────
+// TEST: Properties / Attributes
+// ──────────────────────────────────────────────────────────────
+
+export const PropertyMutationTest: Story = {
+ ...Overview,
+ play: async ({ canvasElement, step }) => {
+ const illustratedMessage = await getComponent(
+ canvasElement,
+ 'swc-illustrated-message'
+ );
+
+ await step(
+ 'size reflects to attribute after direct property mutation',
+ async () => {
+ illustratedMessage.size = 's';
+ await illustratedMessage.updateComplete;
+ expect(
+ illustratedMessage.getAttribute('size'),
+ 'size attribute reflects "s"'
+ ).toBe('s');
+
+ illustratedMessage.size = 'l';
+ await illustratedMessage.updateComplete;
+ expect(
+ illustratedMessage.getAttribute('size'),
+ 'size attribute reflects "l"'
+ ).toBe('l');
+
+ illustratedMessage.size = 'm';
+ await illustratedMessage.updateComplete;
+ expect(
+ illustratedMessage.getAttribute('size'),
+ 'size attribute restores to "m"'
+ ).toBe('m');
+ }
+ );
+
+ await step(
+ 'orientation reflects to attribute after direct property mutation',
+ async () => {
+ illustratedMessage.orientation = 'horizontal';
+ await illustratedMessage.updateComplete;
+ expect(
+ illustratedMessage.getAttribute('orientation'),
+ 'orientation attribute reflects "horizontal"'
+ ).toBe('horizontal');
+
+ illustratedMessage.orientation = 'vertical';
+ await illustratedMessage.updateComplete;
+ expect(
+ illustratedMessage.getAttribute('orientation'),
+ 'orientation attribute reflects "vertical"'
+ ).toBe('vertical');
+ }
+ );
+ },
+};
+
// ──────────────────────────────────────────────────────────────
-// TEST: Heading slot — valid usage
+// TEST: Slots
// ──────────────────────────────────────────────────────────────
+export const DefaultSlotIllustrationTest: Story = {
+ render: () => html`
+
+
+