Skip to content

Commit b15f149

Browse files
authored
docs(a11y): tabs a11y migration (#6163)
* chore: updated a11y migration docs rules * docs(tabs): a11y igration docs for tabs * Delete .cursor/rules/accessibility-migration-analysis.mdc
1 parent cb0d969 commit b15f149

4 files changed

Lines changed: 279 additions & 0 deletions

File tree

CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
- [Swatch Group migration roadmap](swatch-group/rendering-and-styling-migration-analysis.md)
9191
- Switch
9292
- [Switch migration roadmap](switch/rendering-and-styling-migration-analysis.md)
93+
- Tabs
94+
- [Tabs accessibility migration analysis](tabs/accessibility-migration-analysis.md)
95+
- [Tabs migration roadmap](tabs/rendering-and-styling-migration-analysis.md)
9396
- Tag
9497
- [Tag migration roadmap](tag/rendering-and-styling-migration-analysis.md)
9598
- Tags
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
<!-- Generated breadcrumbs - DO NOT EDIT -->
2+
3+
[CONTRIBUTOR-DOCS](../../../README.md) / [Project planning](../../README.md) / [Components](../README.md) / Tabs / Tabs accessibility migration analysis
4+
5+
<!-- Document title (editable) -->
6+
7+
# Tabs accessibility migration analysis
8+
9+
<!-- Generated TOC - DO NOT EDIT -->
10+
11+
<details open>
12+
<summary><strong>In this doc</strong></summary>
13+
14+
- [Overview](#overview)
15+
- [Also read](#also-read)
16+
- [What it is](#what-it-is)
17+
- [When to use something else](#when-to-use-something-else)
18+
- [What it is not](#what-it-is-not)
19+
- [ARIA and WCAG context](#aria-and-wcag-context)
20+
- [Pattern in the APG](#pattern-in-the-apg)
21+
- [First-gen (`sp-tabs`) activation and keyboard](#first-gen-sp-tabs-activation-and-keyboard)
22+
- [Guidelines that apply](#guidelines-that-apply)
23+
- [Related 1st-gen accessibility (Jira)](#related-1st-gen-accessibility-jira)
24+
- [Recommendations: `<swc-tabs>`](#recommendations-swc-tabs)
25+
- [ARIA roles, states, and properties](#aria-roles-states-and-properties)
26+
- [Shadow DOM and cross-root ARIA Issues](#shadow-dom-and-cross-root-aria-issues)
27+
- [Accessibility tree expectations](#accessibility-tree-expectations)
28+
- [Keyboard and focus](#keyboard-and-focus)
29+
- [Testing](#testing)
30+
- [Automated tests](#automated-tests)
31+
- [Summary checklist](#summary-checklist)
32+
- [References](#references)
33+
34+
</details>
35+
36+
<!-- Document content (editable) -->
37+
38+
## Overview
39+
40+
This doc describes how `swc-tabs` (with `swc-tab` and `swc-tab-panel`) should behave for accessibility in 2nd-gen, targeting **WCAG 2.2 Level AA**. It aligns with the WAI-ARIA tabs pattern, documents **automatic vs manual activation**, and ties tablist keyboard behavior to the proposed `FocusgroupNavigationController` ([spectrum-web-components#6129](https://github.com/adobe/spectrum-web-components/pull/6129)).
41+
42+
### Also read
43+
44+
[Tabs migration roadmap](./rendering-and-styling-migration-analysis.md) for layout, CSS, and DOM migration (placeholder until expanded).
45+
46+
### What it is
47+
48+
- **Tabbed interface:** a `tablist` containing `tab` controls and associated `tabpanel` regions. Only one panel is visible at a time; the selected tab exposes `aria-selected` and is wired to its panel with `aria-controls` (tab → panel) and `aria-labelledby` (panel → tab) per APG.
49+
50+
### When to use something else
51+
52+
- Same-page sections that do not need desktop-style tab keyboard semantics are often better as a table of contents and in-page links to headings or regions. [Inclusive Components on tabbed interfaces](https://inclusive-components.design/tabbed-interfaces/) argues that progressive enhancement from TOC + sections can be simpler and more robust than ARIA tabs when the UX does not require true tab ergonomics.
53+
- Site-wide navigation should not be styled or implemented as tabs if users would expect links and full-page navigation instead ([Inclusive Components](https://inclusive-components.design/tabbed-interfaces/)).
54+
- Single-page application views or routes should be communicated as distinct pages or regions (focus management, `<title>`, links), not as `tabpanel` semantics, unless the UI is explicitly a tabs widget ([Inclusive Components — When panels are views](https://inclusive-components.design/tabbed-interfaces/)).
55+
56+
### What it is not
57+
58+
- **Not a menu:** `tablist`, `tab`, and `tabpanel` use different roles and keys than `menu` / `menuitem`. For command menus, use the [APG Menu button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/).
59+
60+
---
61+
62+
## ARIA and WCAG context
63+
64+
### Pattern in the APG
65+
66+
- The [Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) is the normative APG reference. Two examples differ by activation model:
67+
- **Automatic:** [Tabs with automatic activation](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/) — when a tab receives focus (for example via Left / Right Arrow), it is activated and its panel is shown immediately. APG recommends this only when all panel content is already in the DOM and can be shown without delay (see *Deciding When to Make Selection Automatically Follow Focus* linked from the example).
68+
- **Manual:** [Tabs with manual activation](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/) — Arrow keys move focus between tabs without changing the selected tab; the user activates the focused tab with Space or Enter (or click). Use this when panels are expensive to render, lazy-loaded, or not all present in the DOM at once.
69+
- [Deque University — Tabpanel](https://dequeuniversity.com/library/aria/tabpanel) illustrates `role="tablist"`, `role="tab"` with `aria-controls` and `aria-selected`, and `role="tabpanel"` as a compact reference for tab–panel relationships (still verify keys and activation against APG).
70+
- [Inclusive Components — Tabbed interfaces](https://inclusive-components.design/tabbed-interfaces/) covers roving `tabindex` (Tab skips inactive tabs; arrows move within the tablist), focusable `tabpanel` (or first focusable child) so screen reader users do not miss panel content, responsive layouts (avoid tabs-to-accordion hybrid complexity), and when many tabs make accordions preferable.
71+
72+
### First-gen (`sp-tabs`) activation and keyboard
73+
74+
`<sp-tabs>` maps APG manual vs automatic activation to the boolean `auto` attribute / property (default `false`). The JSDoc points authors to [Deciding When to Make Selection Automatically Follow Focus](https://w3c.github.io/aria-practices/#kbd_selection_follows_focus) (same idea as the APG examples).
75+
76+
```116:125:1st-gen/packages/tabs/src/Tabs.ts
77+
/**
78+
* Whether to activate a tab on keyboard focus or not.
79+
*
80+
* By default a tab is activated via a "click" interaction. This is specifically intended for when
81+
* tab content cannot be displayed instantly, e.g. not all of the DOM content is available, etc.
82+
* To learn more about "Deciding When to Make Selection Automatically Follow Focus", visit:
83+
* https://w3c.github.io/aria-practices/#kbd_selection_follows_focus
84+
*/
85+
@property({ type: Boolean })
86+
public auto = false;
87+
```
88+
89+
- **Manual pattern** (`auto` omitted or `false`) — matches [tabs with manual activation](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/): `RovingTabindexController` moves focus between `sp-tab` elements with arrow keys, but `elementEnterAction` does not change selection when `auto` is false. The user activates the focused tab with Enter or Space (or click). `onKeyDown` on the tablist calls `selectTarget` for Enter and Space only.
90+
91+
```206:230:1st-gen/packages/tabs/src/Tabs.ts
92+
rovingTabindexController = new RovingTabindexController<Tab>(this, {
93+
focusInIndex: (elements) => {
94+
let focusInIndex = 0;
95+
const firstFocusableElement = elements.find((el, index) => {
96+
const focusInElement = this.selected
97+
? el.value === this.selected
98+
: !el.disabled;
99+
focusInIndex = index;
100+
return focusInElement;
101+
});
102+
return firstFocusableElement ? focusInIndex : -1;
103+
},
104+
direction: () => 'both',
105+
elementEnterAction: (el) => {
106+
if (!this.auto) {
107+
return;
108+
}
109+
110+
this.shouldAnimate = true;
111+
this.selectTarget(el);
112+
},
113+
elements: () => this.tabs,
114+
isFocusableElement: (el) => !this.disabled && !el.disabled,
115+
listenerScope: () => this.tabList,
116+
});
117+
```
118+
119+
```476:484:1st-gen/packages/tabs/src/Tabs.ts
120+
private onKeyDown = (event: KeyboardEvent): void => {
121+
if (event.code === 'Enter' || event.code === 'Space') {
122+
event.preventDefault();
123+
const target = event.target as HTMLElement;
124+
if (target) {
125+
this.selectTarget(target);
126+
}
127+
}
128+
};
129+
```
130+
131+
- **Automatic pattern** (`auto` true) — matches [tabs with automatic activation](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/): when roving focus enters a tab, `elementEnterAction` runs `selectTarget(el)`, so selection (and visible panel) follows arrow navigation without a separate Enter or Space. Click still selects via `onClick`.
132+
133+
**2nd-gen note:** 1st-gen uses `RovingTabindexController` from `@spectrum-web-components/reactive-controllers`. 2nd-gen should use `FocusgroupNavigationController` ([#6129](https://github.com/adobe/spectrum-web-components/pull/6129)) for arrow navigation inside the tablist while preserving the same `auto` semantics (selection on focus move vs selection only on activate).
134+
135+
### Guidelines that apply
136+
137+
| Idea | Plain meaning |
138+
| --- | --- |
139+
| [Tabs pattern (APG)](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) | `tablist`, `tab`, `tabpanel`; `aria-selected`; `aria-controls`; `aria-labelledby` on panels; roving `tabindex` on tabs per APG examples. |
140+
| [Info and relationships (WCAG 1.3.1)](https://www.w3.org/TR/WCAG22/#info-and-relationships) | The selected tab and visible panel relationship must be exposed in the accessibility tree. |
141+
| [Keyboard (WCAG 2.1.1)](https://www.w3.org/TR/WCAG22/#keyboard) | All tab selection and navigation within the widget must work without a pointer. |
142+
| [Focus order (WCAG 2.4.3)](https://www.w3.org/TR/WCAG22/#focus-order) | Tab from the tablist should move to meaningful content (`tabpanel` with `tabindex="0"` when needed, or first focusable in panel) per APG guidance. |
143+
| [Name, role, value (WCAG 4.1.2)](https://www.w3.org/TR/WCAG22/#name-role-value) | Each tab needs a name; state (`aria-selected`) must update; panels hidden with `hidden` or equivalent must not leave stale state exposed. |
144+
145+
**Bottom line:** Choose automatic vs manual activation from real performance and DOM shape; implement APG keyboard tables for the chosen model; keep tab↔panel references valid in the composed tree (see Shadow DOM below).
146+
147+
---
148+
149+
## Related 1st-gen accessibility (Jira)
150+
151+
| Jira | Type | Status (snapshot) | Resolution (snapshot) | Summary |
152+
| --- | --- | --- | --- | --- |
153+
||||| No issues listed. |
154+
155+
---
156+
157+
## Recommendations: `<swc-tabs>`
158+
159+
### ARIA roles, states, and properties
160+
161+
| Topic | What to do |
162+
| --- | --- |
163+
| **Prescribed structure** | `swc-tabs` (or its internal wrapper) exposes `role="tablist"` with an accessible name (`aria-label` or `aria-labelledby`). Each `swc-tab` exposes `role="tab"`; each `swc-tab-panel` exposes `role="tabpanel"`. Do not map this component to different ARIA widgets (`menu`, `listbox`) via author overrides on the host. |
164+
| **`aria-selected`** | `true` on the active tab; `false` on all others. Must stay in sync with the visible panel. |
165+
| **`aria-controls`** | On each tab, reference the `id` of the associated `tabpanel` (same root as ID resolution requires — see Shadow DOM). |
166+
| **`aria-labelledby`** | On each `tabpanel`, reference the `id` of its controlling tab for an accessible name. |
167+
| **Hidden panels** | Inactive panels use `hidden` (or visibility/display patterns that hide from AT) per APG; do not leave inert content falsely exposed. |
168+
| **`tabindex` on `tabpanel`** | Per [automatic activation example](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/), `tabindex="0"` on `tabpanel` helps users move from tablist into panel content when the first element inside is not focusable. [Manual example](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/) may omit `tabpanel` tabindex when the first child is focusable — match APG for your story variants. |
169+
| **Vertical orientation** | If the tablist is vertical, set `aria-orientation="vertical"` and arrow key mapping consistent with APG (Up/Down or Left/Right per spec and docs). |
170+
| **Activation mode (API)** | Mirror 1st-gen `auto` (boolean, default off = manual): `auto` false — arrows move focus only; Enter, Space, or click select; `auto` true — selection follows focus when arrows move between tabs. Document and test both modes; default manual matches lazy or heavy panels. |
171+
172+
### Shadow DOM and cross-root ARIA Issues
173+
174+
`aria-controls` and `aria-labelledby` rely on **ID references** that must resolve in the document tree. If tabs and panels split across shadow roots without a supported cross-root strategy, IDs may not resolve as expected. Prefer a composition where references resolve in the same document subtree as the referencing node, or document the 2nd-gen plan (ElementInternals, explicit light DOM slots, synchronized ids on light children, etc.) once implementation exists. Until then, treat cross-root IDREF as a design constraint to solve in the rendering migration.
175+
176+
### Accessibility tree expectations
177+
178+
**Tablist**
179+
180+
- Role: `tablist` with a discernible name.
181+
- Children: `tab` elements with names from text or `aria-label`.
182+
183+
**Selected tab**
184+
185+
- `aria-selected="true"`, in tab order per APG roving pattern (typically only the selected tab has `tabindex="0"`, others `-1` — verify against the chosen APG example).
186+
187+
**Tabpanel**
188+
189+
- Role: `tabpanel`; name from `aria-labelledby` pointing at the tab.
190+
- Visibility: inactive panels hidden from AT and sight per pattern.
191+
192+
### Keyboard and focus
193+
194+
- Implement APG keyboard behavior for the chosen activation model ([automatic](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/) vs [manual](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/)): Tab into/out of tablist; arrow keys within tablist; Home / End where APG shows them; Space / Enter for manual activation.
195+
- Use `FocusgroupNavigationController` ([#6129](https://github.com/adobe/spectrum-web-components/pull/6129)) for linear arrow navigation and roving `tabindex` within the `tablist` where it matches the controller’s model. Tab moving focus from tablist to panel (and Shift+Tab back) is focus traversal between groups, not only intra-group arrows — implement in the tabs component (or a small tabs-specific helper) unless a generalized “exit group on Tab” behavior is added to shared infrastructure for multiple widgets.
196+
197+
#### Extending focus infrastructure vs component logic
198+
199+
When implementation gaps appear, classify missing behavior:
200+
201+
| If the behavior is… | Prefer… |
202+
| --- | --- |
203+
| **Shared broadly** (Home/End, wrap, orientation for most roving tabindex / focus groups) | Extend `FocusgroupNavigationController` (or a shared core utility) so menus, toolbars, tabs, etc. stay consistent. |
204+
| **Shared narrowly** (e.g. tabs + toolbar, but not every group) | Add a small composable helper or optional controller mode rather than duplicating in each component. |
205+
| **Tabs-specific** (automatic vs manual activation, selection following focus, sync with `aria-selected`, Tab to `tabpanel`) | Keep in `swc-tabs` (or a tabs-only controller) unless refactoring reveals a reusable abstraction worth promoting upward. |
206+
207+
**Activation semantics:** Automatic activation is selection on focus change inside the tablist; manual activation decouples focused tab from `aria-selected` until the user confirms — that state machine is tabs-specific even if arrows use the shared controller.
208+
209+
---
210+
211+
## Testing
212+
213+
### Automated tests
214+
215+
| Kind of test | What to check |
216+
| --- | --- |
217+
| **Unit** | Selection state mirrors `aria-selected`; only one selected tab; panel visibility matches selection; activation mode flag behaves as documented. |
218+
| **aXe + Storybook** | WCAG 2.x rules on tabs stories (horizontal / vertical if supported). |
219+
| **Playwright ARIA snapshots** | Selected vs unselected tabs `tabpanel` visibility; relationship attributes present. Separate stories or snapshots for automatic vs manual activation. |
220+
| **Playwright keyboard** | Tab into tablist and out to panel; arrows within tablist; Home/End if implemented; manual mode: Space/Enter changes selection; automatic mode: arrow moves selection with focus. |
221+
| **Contrast / focus** | Selected vs unselected tabs and focus ring discernible (non-text contrast / focus appearance per design system). |
222+
223+
---
224+
225+
## Summary checklist
226+
227+
- [ ] `tablist` / `tab` / `tabpanel` roles and labelling match APG.
228+
- [ ] Automatic vs manual activation documented and tested; default matches content loading model.
229+
- [ ] Roving `tabindex` and arrows use `FocusgroupNavigationController` where appropriate ([#6129](https://github.com/adobe/spectrum-web-components/pull/6129)); tabs-only logic stays in component unless promoted to shared infra deliberately.
230+
- [ ] `aria-controls` / `aria-labelledby` valid in composed tree (no silent cross-root ID failures).
231+
- [ ] `tabpanel` focus behavior matches APG for focusable vs non-focusable first content.
232+
- [ ] Automated snapshots and keyboard tests cover both activation modes where both ship.
233+
- [ ] Manual smoke with VoiceOver and NVDA (or project standard).
234+
235+
---
236+
237+
## References
238+
239+
- [WAI-ARIA APG: Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
240+
- [WAI-ARIA APG: Example of tabs with automatic activation](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/)
241+
- [WAI-ARIA APG: Example of tabs with manual activation](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/)
242+
- [Deque University: Tabpanel](https://dequeuniversity.com/library/aria/tabpanel)
243+
- [Inclusive Components: Tabbed interfaces](https://inclusive-components.design/tabbed-interfaces/)
244+
- [spectrum-web-components PR #6129 — Focusgroup navigation controller](https://github.com/adobe/spectrum-web-components/pull/6129)
245+
- [WCAG 2.2](https://www.w3.org/TR/WCAG22/)
246+
- [Tabs migration roadmap](./rendering-and-styling-migration-analysis.md)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!-- Generated breadcrumbs - DO NOT EDIT -->
2+
3+
[CONTRIBUTOR-DOCS](../../../README.md) / [Project planning](../../README.md) / [Components](../README.md) / Tabs / Tabs migration roadmap
4+
5+
<!-- Document title (editable) -->
6+
7+
# Tabs migration roadmap
8+
9+
<!-- Generated TOC - DO NOT EDIT -->
10+
11+
<details open>
12+
<summary><strong>In this doc</strong></summary>
13+
14+
- [Overview](#overview)
15+
- [Resources](#resources)
16+
17+
</details>
18+
19+
<!-- Document content (editable) -->
20+
21+
## Overview
22+
23+
This file is a **placeholder** for **`swc-tabs`**, **`swc-tab`**, **`swc-tab-panel`**, and related **2nd-gen** rendering, CSS, and DOM migration planning. Until it is expanded, treat **`1st-gen/packages/tabs`** (`<sp-tabs>`, `<sp-tab>`, `<sp-tab-panel>`) as the reference.
24+
25+
For **accessibility** targets, APG **automatic** vs **manual** activation, and **focus** **management**, see [Tabs accessibility migration analysis](./accessibility-migration-analysis.md).
26+
27+
## Resources
28+
29+
- [Tabs accessibility migration analysis](./accessibility-migration-analysis.md)

CONTRIBUTOR-DOCS/03_project-planning/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
- Swatch
6060
- Swatch Group
6161
- Switch
62+
- Tabs
6263
- Tag
6364
- Tags
6465
- Textfield

0 commit comments

Comments
 (0)