|
| 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