Skip to content

Commit 6351937

Browse files
committed
docs(combobox): added option and option-group
1 parent 5910ad1 commit 6351937

6 files changed

Lines changed: 411 additions & 9 deletions

File tree

.claude/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(node update-nav.js)"
5+
]
6+
}
7+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@
122122
- [Opacity checkerboard accessibility migration analysis](opacity-checkerboard/accessibility-migration-analysis.md)
123123
- [Opacity Checkerboard migration plan](opacity-checkerboard/migration-plan.md)
124124
- [Opacity Checkerboard migration roadmap](opacity-checkerboard/rendering-and-styling-migration-analysis.md)
125+
- Option
126+
- [Option accessibility migration analysis](option/accessibility-migration-analysis.md)
127+
- Option Group
128+
- [Option group accessibility migration analysis](option-group/accessibility-migration-analysis.md)
125129
- Picker Button
126130
- [Picker button migration roadmap](picker-button/rendering-and-styling-migration-analysis.md)
127131
- Popover

CONTRIBUTOR-DOCS/03_project-planning/03_components/combobox/accessibility-migration-analysis.md

Lines changed: 24 additions & 9 deletions
Large diffs are not rendered by default.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<!-- Generated breadcrumbs - DO NOT EDIT -->
2+
3+
[CONTRIBUTOR-DOCS](../../../README.md) / [Project planning](../../README.md) / [Components](../README.md) / Option Group / Option group accessibility migration analysis
4+
5+
<!-- Document title (editable) -->
6+
7+
# Option group 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+
- [Related](#related)
20+
- [ARIA and WCAG context](#aria-and-wcag-context)
21+
- [Pattern in the APG](#pattern-in-the-apg)
22+
- [Guidelines that apply](#guidelines-that-apply)
23+
- [Related 1st-gen accessibility (Jira)](#related-1st-gen-accessibility-jira)
24+
- [Recommendations: `<swc-option-group>`](#recommendations-swc-option-group)
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+
- [Manual screen reader testing](#manual-screen-reader-testing)
32+
- [Summary checklist](#summary-checklist)
33+
- [References](#references)
34+
35+
</details>
36+
37+
<!-- Document content (editable) -->
38+
39+
## Overview
40+
41+
This doc tells you how **`swc-option-group`** should work for **accessibility**. It matches the goal of **WCAG 2.2 Level AA**. `swc-option-group` is a new 2nd-gen component — there is no 1st-gen `sp-option-group`, and 1st-gen `sp-combobox` had no grouping at all. It is a labeled container that groups related [`swc-option`](../option/accessibility-migration-analysis.md) elements inside a listbox, implementing the APG [grouped listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-grouped/) pattern for [`swc-combobox`](../combobox/accessibility-migration-analysis.md).
42+
43+
Like `swc-option`, its reason for existing is **role ownership**: it carries `role="group"` on its own host and names that group, so a combobox in a different shadow root can present grouped options with correct structure and correct group labels, without re-rendering shadow-DOM copies.
44+
45+
### Also read
46+
47+
- [Combobox accessibility migration analysis](../combobox/accessibility-migration-analysis.md) — the consumer; options and option groups are its accepted children.
48+
- [Option accessibility migration analysis](../option/accessibility-migration-analysis.md) — the `swc-option` rows this component groups; the sibling-label and unique-`value` rules are shared and defined there and in the combobox doc.
49+
- [Menu group accessibility migration analysis](../menu-group/accessibility-migration-analysis.md) — the `role="group"` sibling in the menu family; same grouping idea, different (menu) context.
50+
51+
### What it is
52+
53+
- A **labeled group of options** inside a listbox: `role="group"` on the host, an accessible name for the group (from a slotted label or a label property), and one or more `swc-option` children in its default slot.
54+
- **The role lives on the `swc-option-group` host**, set via `ElementInternals` (`internals.role = 'group'`), for the same reason `swc-option` owns `role="option"`: the real, author-supplied element carries the correct role, so grouping survives across the shadow boundary between the combobox input and its options.
55+
- A structural container only. It groups and labels; it is never itself selectable, focusable, or the target of `aria-activedescendant`.
56+
57+
### When to use something else
58+
59+
- A **flat list of options** with no categories — put `swc-option`s directly in the combobox; no group is needed.
60+
- A **command menu** with sections — use [`swc-menu-group`](../menu-group/accessibility-migration-analysis.md) (`role="group"` among `menuitem`s), not `swc-option-group`. Groups of options and groups of commands are different patterns.
61+
- A **visual divider** with no group semantics — use a separator element, not an empty group.
62+
63+
### What it is not
64+
65+
- Not an option. `swc-option-group` is never selectable and never carries `aria-selected`; its label is not a choosable value. Do not give it a `value` or let it become an `aria-activedescendant` target.
66+
- Not a nesting container for more groups. The APG grouped-listbox pattern is one level deep: `listbox``group``option`. Do not nest `swc-option-group` inside another `swc-option-group`.
67+
- Not a menu group. It must not carry `menu`/`menuitem`-family semantics.
68+
69+
### Related
70+
71+
- [`swc-combobox`](../combobox/accessibility-migration-analysis.md) and [`swc-option`](../option/accessibility-migration-analysis.md) — the container and the leaf this component sits between.
72+
- [`swc-menu-group`](../menu-group/accessibility-migration-analysis.md) — the menu-family analogue.
73+
74+
---
75+
76+
## ARIA and WCAG context
77+
78+
### Pattern in the APG
79+
80+
- `swc-option-group` implements the group layer of the APG [grouped listbox example](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-grouped/): the `listbox` contains `group` elements, each `group` is named (via `aria-labelledby` pointing at its label, or `aria-label`), and each group's `option` children are its selectable rows. The group label itself is **not** an option and is not part of the selectable set.
81+
- Grouping is presentational structure plus a name; it does not change how selection or active-descendant movement works. Arrowing moves from option to option across group boundaries as one linear sequence; the group only adds a spoken "group, [name]" context and a heading in the reading order.
82+
- **Single host role.** `swc-option-group` exposes exactly one role, `group`, on its host — never conditional, never an option. This satisfies the single-host-role policy; the group is not a value-bearing control, so (like `swc-option`) the role sitting on the host is correct and is what lets grouping resolve across the shadow boundary.
83+
84+
### Guidelines that apply
85+
86+
| Idea | Plain meaning |
87+
| --- | --- |
88+
| [Info and relationships (1.3.1)](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) | The group and its label must be programmatically associated (`role="group"` + `aria-labelledby`/`aria-label`), and the options must be exposed as children of the group, so AT conveys the category structure — not just visual spacing or a bold heading. |
89+
| [Name, role, value (4.1.2)](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) | The group exposes `role="group"` and an accessible name. The label is a name for the group, not a selectable node. |
90+
| [Language of parts (3.1.2)](https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html) | A group label (and its option children) in another language must carry `lang` on the real element so AT pronounces the category name and rows correctly. |
91+
| [Headings and labels (2.4.6)](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html) | Group labels must be descriptive and distinct, so a listener can tell one category from another. |
92+
93+
**Bottom line:** `swc-option-group` adds one thing — a named `role="group"` wrapper around a subset of options — and its accessibility job is to make that name and the group→option containment real and cross-root-safe, without becoming selectable itself.
94+
95+
---
96+
97+
## Related 1st-gen accessibility (Jira)
98+
99+
| Jira | Type | Status (snapshot) | Resolution (snapshot) | Summary | Notes |
100+
| --- | --- | --- | --- | --- | --- |
101+
| [SWC-592](https://jira.corp.adobe.com/browse/SWC-592) | Bug | To Do | Unresolved | Combobox a11y issues speaking the options | Grouping adds category context to option announcements; net-new in 2nd-gen (1st-gen combobox had no groups) |
102+
| [SWC-1373](https://jira.corp.adobe.com/browse/SWC-1373) | Story | To Do | Unresolved | Spike: compare semantics across menu, action menu, combobox, and picker | Informs how option groups relate to menu groups |
103+
104+
---
105+
106+
## Recommendations: `<swc-option-group>`
107+
108+
Component tag may change until API freeze. `swc-option-group` is new in 2nd-gen; there is no 1st-gen equivalent to preserve compatibility with.
109+
110+
### ARIA roles, states, and properties
111+
112+
| Topic | What to do |
113+
| --- | --- |
114+
| **Host role** | `group`, set on the host via `ElementInternals` (`internals.role = 'group'`). Keep it on the real, author-supplied element so grouping survives across the boundary between the combobox input and its options. Do not use `menu`/`listbox` roles, and do not make the group itself an `option`. |
115+
| **Group name** | Every group must be named. Provide the name via a slotted label rendered inside the group's shadow root (associated same-root with `aria-labelledby`) or an `accessible-label`-style string property that sets `aria-label`. An unnamed group defeats the purpose; dev-warn on a group with no accessible name (matching the project's `window.__swc.warn` pattern). |
116+
| **Label is not selectable** | The group label element is presentational: it must not be an `option`, must not be focusable, and must not be an `aria-activedescendant` target. It participates in the reading order as the group's name only. |
117+
| **Options are the children** | The group's default slot holds `swc-option` children. The group does not re-render or proxy them; they remain the author's real elements (owning their own `value`, `lang`, `aria-selected`, `aria-disabled`), so the combobox references them directly. See [`swc-option`](../option/accessibility-migration-analysis.md). |
118+
| **Sibling-label distinctness within the group** | Options within one group are siblings for the distinct-label rule: two options in the same group must not have identical computed label text. Options in *different* groups may legitimately repeat a label because the group name disambiguates them. The combobox owns value-uniqueness across the whole widget and sibling-label distinctness within each option-parent scope (the combobox itself and each group); see [`swc-combobox`](../combobox/accessibility-migration-analysis.md#aria-roles-states-and-properties). |
119+
| **No nested groups** | Do not support a `swc-option-group` inside another; the grouped-listbox pattern is one level deep. |
120+
| **`lang` passthrough** | Author `lang` on the group or on any option child must remain on the real element carrying the role, per [WCAG 3.1.2](https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html). |
121+
122+
### Shadow DOM and cross-root ARIA Issues
123+
124+
The group's own label renders in its shadow root and associates same-root (`aria-labelledby`/`aria-label`), so the group has no internal cross-root problem. The cross-root relationship is the same one the option has with the combobox: the combobox input, which may live in a different shadow root, references the **options** (not the group) via `ariaActiveDescendantElement`, and those references resolve because the options are the author's real elements. The group adds a containment layer that AT reads structurally; it does not need an IDREF from the input.
125+
126+
What `swc-option-group` must guarantee: `role="group"` and the group name live on the referenceable host, and the option children are not re-rendered as shadow copies (which would strip their `value`/`lang`/state and break the combobox's references). Expected axe-core note: the same `aria-required-children` false positive the combobox and listbox trigger applies through the group when options are slotted or cross-root; document it as a written exclusion on the composed story per the [forms strategy axe policy](../../05_strategies/forms-strategy-rfc.md#34-axe-core-policy), not a silent disable.
127+
128+
### Accessibility tree expectations
129+
130+
- **Named group:** role `group` with an accessible name; contains `option` children; not selectable; not the active descendant.
131+
- **Grouped options:** each option exposes role `option` and its own name/state as usual; AT conveys it as within the named group.
132+
- **Localized group:** the group label (and any localized option) carries its `lang`, so AT pronounces the category name and rows in the right language.
133+
- **Arrowing across groups:** the active option moves linearly across group boundaries; the group name is announced as context when focus enters a new group, and the group itself is never the active descendant.
134+
135+
### Keyboard and focus
136+
137+
`swc-option-group` is **not focusable** and handles no keys. It is a structural container: DOM focus stays on the combobox input, and the parent moves the active option (via `aria-activedescendant`) linearly across group boundaries. The group label is never a Tab stop, never an active descendant, and never selectable. If `swc-option-group` is later reused inside a standalone roving-`tabindex` listbox, the parent listbox — not the group — would manage option `tabindex`; the group still holds no focus and handles no keys.
138+
139+
---
140+
141+
## Testing
142+
143+
### Automated tests
144+
145+
| Kind of test | What to check |
146+
| --- | --- |
147+
| **Unit** | `internals.role` is `group` on the host; the group has an accessible name (and dev-warns when it does not); the group label is not exposed as an `option` and is not focusable; option children keep their own `value`/`lang`/`aria-selected`/`aria-disabled` (not re-rendered by the group); no nested-group support. |
148+
| **aXe + Storybook** | A grouped combobox story asserting `role="group"` with a name and `option` children within it, carrying the composed `aria-required-children` exclusion with a written `// reason:`. A localized-group story asserting `lang` survives on the group label and options. All grouped stories use distinct sibling labels within each group and globally-unique option `value`s; one dedicated dev-warning story pairs two identical sibling options within a group to assert the warning fires. |
149+
| **Playwright ARIA snapshots** | Within a composed `swc-combobox`: named `group` nodes each containing their `option` children with correct names and states, and correct `activedescendant` targeting as the user arrows across group boundaries. |
150+
151+
### Manual screen reader testing
152+
153+
Test `swc-option-group` through its composed parent (`swc-combobox`), using [Screen reader testing](../../../../2nd-gen/packages/swc/.storybook/guides/accessibility-guides/screen_reader_testing.mdx) in NVDA, JAWS, and VoiceOver. Confirm that: entering a group announces the group name as context, arrowing moves across group boundaries without getting stuck on a label, each option is announced within its group, and a localized group name and rows are spoken in the correct language. Verify cross-root exposure in **Firefox**, where element-reference ARIA is least consistent.
154+
155+
---
156+
157+
## Summary checklist
158+
159+
- [ ] `swc-option-group` sets `role="group"` on its host via `ElementInternals`; grouping survives across the combobox/option shadow boundary.
160+
- [ ] Every group has an accessible name; an unnamed group dev-warns.
161+
- [ ] The group label is presentational — never an `option`, never focusable, never an `aria-activedescendant` target.
162+
- [ ] Option children keep their own `value`, `lang`, and selected/disabled state; the group does not re-render them.
163+
- [ ] Sibling-label distinctness is scoped within the group; options in different groups may repeat a label because the group name disambiguates; globally-unique option `value`s and the dev warning are enforced by the combobox.
164+
- [ ] No nested option groups (grouped listbox is one level deep).
165+
- [ ] Group and option `lang` survive on the real elements ([WCAG 3.1.2](https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html)).
166+
- [ ] Stories use distinct sibling labels and unique values; one dev-warning story deliberately violates the rule; consumer docs explain the requirement.
167+
- [ ] Manual SR testing is done through the composed `swc-combobox`, including a localized group and Firefox.
168+
169+
## References
170+
171+
- [Combobox accessibility migration analysis (this repo)](../combobox/accessibility-migration-analysis.md) and [Option accessibility migration analysis (this repo)](../option/accessibility-migration-analysis.md) — the container and leaf this component sits between.
172+
- [Menu group accessibility migration analysis (this repo)](../menu-group/accessibility-migration-analysis.md) — the menu-family `role="group"` analogue.
173+
- [Forms strategy: 2nd-gen proposal (this repo)](../../05_strategies/forms-strategy-rfc.md) — role placement and cross-root ARIA policy.
174+
- [WAI-ARIA](https://www.w3.org/TR/wai-aria-1.2/), [WCAG 2.2](https://www.w3.org/TR/WCAG22/), [APG: read me first](https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/)
175+
- [APG: listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) and [grouped listbox example](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-grouped/)
176+
- [React Spectrum: ComboBox — sections](https://react-spectrum.adobe.com/ComboBox) (`ComboBoxSection` / `Header` grouping).
177+
- [2nd-gen Storybook: Screen reader testing](../../../../2nd-gen/packages/swc/.storybook/guides/accessibility-guides/screen_reader_testing.mdx)

0 commit comments

Comments
 (0)