Skip to content

Commit c7fba1e

Browse files
committed
test: add interactive-roles util test + document progressbar divergence
Covers the full util surface: - Canonical widget roles (button, link, combobox, etc.) — in set - Composite-widget containers (listbox, grid, tablist, tree, treegrid) — in set - toolbar manual override — in set (documented exception) - tooltip exclusion — NOT in set (WAI-ARIA 1.2 §5.3.3) - progressbar included (documented divergence from lit-a11y) - Set-size pin (35 roles) — surfaces aria-query taxonomy drift as test failure - COMPOSITE_WIDGET_CHILDREN sanity (listbox/option, grid/gridcell transitivity, treegrid/row+treeitem superClass inheritance, radiogroup/radio, submenu) Also extend the util JSDoc to document progressbar inclusion vs lit-a11y's readonly-value-based exclusion — previously only noted in the PR body.
1 parent 338b36c commit c7fba1e

2 files changed

Lines changed: 148 additions & 2 deletions

File tree

lib/utils/interactive-roles.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,25 @@ const { roles } = require('aria-query');
55
// peer-plugin behavior here):
66
// https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/util/isInteractiveRole.js
77
//
8+
// Authority: aria-query widget taxonomy, one manual addition, one manual
9+
// exclusion. Both documented below.
10+
//
811
// `toolbar` is added explicitly — it does not descend from `widget` per
9-
// aria-query's taxonomy, but supports `aria-activedescendant` and is widget-
10-
// like in practice. jsx-a11y adds it for the same reason.
12+
// aria-query's taxonomy (its superClass is structure-based), but supports
13+
// `aria-activedescendant` and is widget-like in practice per WAI-ARIA APG's
14+
// toolbar pattern. jsx-a11y adds it for the same reason.
1115
//
1216
// `tooltip` is intentionally NOT added. Per WAI-ARIA 1.2 §5.3.3 — Document
1317
// Structure Roles (https://www.w3.org/TR/wai-aria-1.2/#tooltip), tooltip is
1418
// a document-structure role, not a widget; the spec says "document structures
1519
// are not usually interactive." jsx-a11y agrees.
20+
//
21+
// `progressbar` IS included (it descends from widget → range per aria-query).
22+
// lit-a11y explicitly excludes it on the grounds that `progressbar.value` is
23+
// always readonly, so the role isn't operable by the user. This is a real
24+
// design disagreement; we side with aria-query's taxonomy (and jsx-a11y) to
25+
// keep the authority single-sourced. Authors wanting lit-a11y behavior can
26+
// layer a rule-level exclusion if needed.
1627
module.exports.INTERACTIVE_ROLES = buildInteractiveRoleSet();
1728

1829
// Composite-widget child map — for each composite-widget parent role, the
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
'use strict';
2+
3+
const {
4+
INTERACTIVE_ROLES,
5+
COMPOSITE_WIDGET_CHILDREN,
6+
} = require('../../../lib/utils/interactive-roles');
7+
8+
describe('INTERACTIVE_ROLES', () => {
9+
describe('canonical widget roles (ARIA 1.2 widget taxonomy)', () => {
10+
for (const role of [
11+
'button',
12+
'checkbox',
13+
'combobox',
14+
'link',
15+
'menuitem',
16+
'menuitemcheckbox',
17+
'menuitemradio',
18+
'option',
19+
'radio',
20+
'scrollbar',
21+
'searchbox',
22+
'slider',
23+
'spinbutton',
24+
'switch',
25+
'tab',
26+
'textbox',
27+
'treeitem',
28+
]) {
29+
it(`includes "${role}"`, () => {
30+
expect(INTERACTIVE_ROLES.has(role)).toBe(true);
31+
});
32+
}
33+
});
34+
35+
describe('composite widget containers (widget-descended per aria-query)', () => {
36+
for (const role of ['listbox', 'menu', 'menubar', 'grid', 'tablist', 'tree', 'treegrid']) {
37+
it(`includes "${role}"`, () => {
38+
expect(INTERACTIVE_ROLES.has(role)).toBe(true);
39+
});
40+
}
41+
});
42+
43+
describe('manual override', () => {
44+
it('includes "toolbar" (not widget-descended per aria-query; added per APG convention)', () => {
45+
expect(INTERACTIVE_ROLES.has('toolbar')).toBe(true);
46+
});
47+
});
48+
49+
describe('manual exclusion', () => {
50+
it('does NOT include "tooltip" (structure role per WAI-ARIA 1.2 §5.3.3)', () => {
51+
expect(INTERACTIVE_ROLES.has('tooltip')).toBe(false);
52+
});
53+
});
54+
55+
describe('contested inclusions (documented divergences from peers)', () => {
56+
it('includes "progressbar" (widget-descended per aria-query; diverges from lit-a11y which excludes it as readonly-valued)', () => {
57+
expect(INTERACTIVE_ROLES.has('progressbar')).toBe(true);
58+
});
59+
});
60+
61+
describe('non-widget roles excluded', () => {
62+
// Spot-check a handful of roles that should NOT be in the interactive set
63+
// because they're abstract, structural, or landmark-typed.
64+
for (const role of [
65+
'article', // document-structure
66+
'banner', // landmark
67+
'main', // landmark
68+
'navigation', // landmark
69+
'region', // landmark
70+
'complementary', // landmark
71+
'contentinfo', // landmark
72+
'form', // landmark
73+
'search', // landmark (role, not element)
74+
'heading', // document-structure
75+
'img', // document-structure
76+
'list', // document-structure
77+
'listitem', // document-structure
78+
'paragraph', // document-structure
79+
'separator', // document-structure (context-dependent; aria-query taxonomy says structure)
80+
'presentation', // role
81+
'none', // role
82+
'widget', // abstract — excluded by filter
83+
'structure', // abstract — excluded by filter
84+
'window', // abstract — excluded by filter
85+
]) {
86+
it(`does NOT include "${role}"`, () => {
87+
expect(INTERACTIVE_ROLES.has(role)).toBe(false);
88+
});
89+
}
90+
});
91+
92+
describe('set size (drift detection)', () => {
93+
// Pins the current set size to surface aria-query's taxonomy changes as
94+
// visible diffs rather than silent shifts. Update this number (with a
95+
// commit message naming which role was added/removed) when aria-query
96+
// is bumped.
97+
it('has 35 roles', () => {
98+
expect(INTERACTIVE_ROLES.size).toBe(35);
99+
});
100+
});
101+
});
102+
103+
describe('COMPOSITE_WIDGET_CHILDREN', () => {
104+
it('is a Map', () => {
105+
expect(COMPOSITE_WIDGET_CHILDREN).toBeInstanceOf(Map);
106+
});
107+
108+
it('maps "listbox" to include "option"', () => {
109+
expect(COMPOSITE_WIDGET_CHILDREN.get('listbox')?.has('option')).toBe(true);
110+
});
111+
112+
it('maps "tablist" to include "tab"', () => {
113+
expect(COMPOSITE_WIDGET_CHILDREN.get('tablist')?.has('tab')).toBe(true);
114+
});
115+
116+
it('maps "tree" to include "treeitem"', () => {
117+
expect(COMPOSITE_WIDGET_CHILDREN.get('tree')?.has('treeitem')).toBe(true);
118+
});
119+
120+
it('maps "grid" to include "row" and transitively "gridcell"', () => {
121+
const gridChildren = COMPOSITE_WIDGET_CHILDREN.get('grid');
122+
expect(gridChildren?.has('row')).toBe(true);
123+
expect(gridChildren?.has('gridcell')).toBe(true);
124+
});
125+
126+
it('maps "treegrid" to include both grid-row and tree-treeitem (superClass inheritance)', () => {
127+
const treeGridChildren = COMPOSITE_WIDGET_CHILDREN.get('treegrid');
128+
expect(treeGridChildren?.has('row')).toBe(true);
129+
expect(treeGridChildren?.has('treeitem')).toBe(true);
130+
});
131+
132+
it('maps "radiogroup" to include "radio"', () => {
133+
expect(COMPOSITE_WIDGET_CHILDREN.get('radiogroup')?.has('radio')).toBe(true);
134+
});
135+
});

0 commit comments

Comments
 (0)