Skip to content

Commit acae0a2

Browse files
authored
Merge pull request #207 from jessebeach/axmodel-query-integration
Axmodel query integration
2 parents b44ac6d + 761e7fc commit acae0a2

14 files changed

+530
-233
lines changed

README.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ You can also enable all the recommended rules at once. Add `plugin:jsx-a11y/reco
104104
- [iframe-has-title](docs/rules/iframe-has-title.md): Enforce iframe elements have a title attribute.
105105
- [img-has-alt](docs/rules/img-has-alt.md): Enforce that `<img>` JSX elements use the `alt` prop.
106106
- [img-redundant-alt](docs/rules/img-redundant-alt.md): Enforce `<img>` alt prop does not contain the word "image", "picture", or "photo".
107+
- [interactive-supports-focus](docs/rules/interactive-supports-focus.md): Enforce that elements with interactive handlers like `onClick` must be focusable.
107108
- [label-has-for](docs/rules/label-has-for.md): Enforce that `<label>` elements have the `htmlFor` prop.
108109
- [lang](docs/rules/lang.md): Enforce lang attribute has a valid value.
109110
- [mouse-events-have-key-events](docs/rules/mouse-events-have-key-events.md): Enforce that `onMouseOver`/`onMouseOut` are accompanied by `onFocus`/`onBlur` for keyboard-only users.
@@ -112,9 +113,7 @@ You can also enable all the recommended rules at once. Add `plugin:jsx-a11y/reco
112113
- [no-distracting-elements](docs/rules/no-distracting-elements.md): Enforce distracting elements are not used.
113114
- [no-onchange](docs/rules/no-onchange.md): Enforce usage of `onBlur` over `onChange` on select menus for accessibility.
114115
- [no-redundant-roles](docs/rules/no-redundant-roles.md): Enforce explicit role property is not the same as implicit/default role property on element.
115-
- [no-static-element-interactions](docs/rules/no-static-element-interactions.md): Enforce non-interactive elements have no interactive handlers.
116-
- [onclick-has-focus](docs/rules/onclick-has-focus.md): Enforce that elements with `onClick` handlers must be focusable.
117-
- [onclick-has-role](docs/rules/onclick-has-role.md): Enforce that non-interactive, visible elements (such as `<div>`) that have click handlers use the role attribute.
116+
- [no-static-element-interactions](docs/rules/no-static-element-interactions.md): Enforce that non-interactive, visible elements (such as `<div>`) that have click handlers use the role attribute.
118117
- [role-has-required-aria-props](docs/rules/role-has-required-aria-props.md): Enforce that elements with ARIA roles must have all required attributes for that role.
119118
- [role-supports-aria-props](docs/rules/role-supports-aria-props.md): Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`.
120119
- [scope](docs/rules/scope.md): Enforce `scope` prop is only used on `<th>` elements.
@@ -129,6 +128,39 @@ script to scaffold the new files.
129128
$ ./scripts/create-rule.js my-new-rule
130129
```
131130

131+
## Some background on WAI-ARIA, the AX Tree and Browsers
132+
133+
### Accessibility API
134+
An operating system will provide an accessibility API that maps application state and content onto input/output controllers such as a screen reader, braille device, keyboard, etc.
135+
136+
These APIs were developed as computer interfaces shifted from buffers (which are text based and inherently quite accessible) to graphical user interfaces (GUIs). The first attempts to make GUIs accessible involved raster image parsing to recognize characters, words, etc. This information was stored in a parallel buffer and made accessible to assistive technology (AT) devices.
137+
138+
As GUIs became more complex, the raster parsing approach became untenable. Accessibility APIs were developed to replace them. Check out [NSAccessibility (AXAPI)](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Protocols/NSAccessibility_Protocol/index.html) for an example. See [Core Accessibility API Mappings 1.1](https://www.w3.org/TR/core-aam-1.1/) for more details.
139+
140+
### Browsers
141+
Browsers support an Accessibility API on a per operating system basis. For instance Firefox implements the MSAA accessibility API on Windows, but does not implement the AXAPI on OSX.
142+
143+
### The Accessibility (AX) Tree & DOM
144+
From the [W3 Core Accessibility API Mappings 1.1](https://www.w3.org/TR/core-aam-1.1/#intro_treetypes)
145+
146+
> The accessibility tree and the DOM tree are parallel structures. Roughly speaking the accessibility tree is a subset of the DOM tree. It includes the user interface objects of the user agent and the objects of the document. Accessible objects are created in the accessibility tree for every DOM element that should be exposed to an assistive technology, either because it may fire an accessibility event or because it has a property, relationship or feature which needs to be exposed. Generally if something can be trimmed out it will be, for reasons of performance and simplicity. For example, a <span> with just a style change and no semantics may not get its own accessible object, but the style change will be exposed by other means.
147+
148+
Browser vendors are beginning to expose the AX Tree through inspection tools. Chrome has an experiment available to enable their inspection tool.
149+
150+
You can also see a text-based version of the AX Tree in Chrome in the stable release version.
151+
152+
#### Viewing the AX Tree in Chrome
153+
1. Navigate to `chrome://accessibility/` in Chrome.
154+
1. Toggle the `accessibility off` link for any tab that you want to inspect.
155+
1. A link labeled `show accessibility tree` will appear; click this link.
156+
1. Balk at the wall of text that gets displayed, but then regain your conviction.
157+
1. Use the browser's find command to locate strings and values in the wall of text.
158+
159+
### Pulling it all together
160+
A browser constructs an AX Tree as a subset of the DOM. ARIA heavily informs the properties of this AX Tree. This AX Tree is exposed to the system level Accessibility API which mediates assistive technology agents.
161+
162+
We model ARIA in the [aria-query](https://github.com/a11yance/aria-query) project. We model AXObjects (that comprise the AX Tree) in the [axobject-query](https://github.com/A11yance/axobject-query) project. The goal of the WAI-ARIA specification is to be a complete complete declarative interface to the AXObject model. The [in-draft 1.2 version](https://github.com/w3c/aria/issues?q=is%3Aissue+is%3Aopen+label%3A%22ARIA+1.2%22) is moving towards this goal. But until then, we must consider the semantics constructs affored by ARIA as well as those afforded by the AXObject model (AXAPI) in order to determine how HTML can be used to express user interface affordances to assistive technology users.
163+
132164
## License
133165

134166
eslint-plugin-jsx-a11y is licensed under the [MIT License](LICENSE.md).

__mocks__/genInteractives.js

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ const roleNames = [...roles.keys()];
1515
const interactiveElementsMap = {
1616
a: [{prop: 'href', value: '#'}],
1717
area: [{prop: 'href', value: '#'}],
18+
audio: [],
1819
button: [],
20+
canvas: [],
21+
embed: [],
22+
link: [],
1923
input: [],
2024
'input[type=\"button\"]': [{prop: 'type', value: 'button'}],
2125
'input[type=\"checkbox\"]': [{prop: 'type', value: 'checkbox'}],
@@ -42,20 +46,33 @@ const interactiveElementsMap = {
4246
menuitem:[],
4347
option: [],
4448
select: [],
45-
'table[role="grid"]': [{prop: 'role', value: 'grid'}],
46-
'td[role="gridcell"]': [{prop: 'role', value: 'gridcell'}],
49+
// Whereas ARIA makes a distinction between cell and gridcell, the AXObject
50+
// treats them both as CellRole and since gridcell is interactive, we consider
51+
// cell interactive as well.
52+
// td: [],
53+
th: [],
4754
tr: [],
4855
textarea: [],
56+
video: [],
4957
};
5058

5159
const nonInteractiveElementsMap = {
52-
area: [],
60+
abbr: [],
5361
article: [],
62+
blockquote: [],
63+
br: [],
64+
caption: [],
5465
dd: [],
66+
details: [],
5567
dfn: [],
68+
dialog: [],
69+
dir: [],
70+
dl: [],
5671
dt: [],
5772
fieldset: [],
73+
figcaption: [],
5874
figure: [],
75+
footer: [],
5976
form: [],
6077
frame: [],
6178
h1: [],
@@ -65,17 +82,28 @@ const nonInteractiveElementsMap = {
6582
h5: [],
6683
h6: [],
6784
hr: [],
85+
iframe: [],
6886
img: [],
69-
'input[type=\"hidden\"]': [{prop: 'type', value: 'hidden'}],
87+
label: [],
88+
legend: [],
7089
li: [],
7190
main: [],
91+
mark: [],
92+
marquee: [],
93+
menu: [],
94+
meter: [],
7295
nav: [],
7396
ol: [],
97+
p: [],
98+
pre: [],
99+
progress: [],
100+
ruby: [],
74101
table: [],
75-
td: [],
76102
tbody: [],
103+
td: [],
77104
tfoot: [],
78105
thead: [],
106+
time: [],
79107
ul: [],
80108
};
81109

__tests__/src/rules/click-events-have-key-events-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ ruleTester.run('click-events-have-key-events', rule, {
4646
{ code: '<option onClick={() => void 0} className="foo" />' },
4747
{ code: '<select onClick={() => void 0} className="foo" />' },
4848
{ code: '<textarea onClick={() => void 0} className="foo" />' },
49-
{ code: '<a tabIndex="0" onClick={() => void 0} />' },
5049
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
5150
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
5251
{ code: '<input onClick={() => void 0} type="hidden" />;' },
@@ -70,5 +69,6 @@ ruleTester.run('click-events-have-key-events', rule, {
7069
errors: [expectedError],
7170
},
7271
{ code: '<a onClick={() => void 0} />', errors: [expectedError] },
72+
{ code: '<a tabIndex="0" onClick={() => void 0} />', errors: [expectedError] },
7373
].map(parserOptionsMapper),
7474
});

__tests__/src/rules/no-interactive-element-to-noninteractive-role-test.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ const alwaysValid = [
3434
{ code: '<TestComponent onClick={doFoo} />' },
3535
{ code: '<Button onClick={doFoo} />' },
3636
/* Interactive elements */
37-
{ code: '<a tabIndex="0" role="button" />' },
3837
{ code: '<a href="http://x.y.z" role="button" />' },
3938
{ code: '<a href="http://x.y.z" tabIndex="0" role="button" />' },
4039
{ code: '<button className="foo" role="button" />' },
@@ -63,7 +62,6 @@ const alwaysValid = [
6362
{ code: '<input type="url" role="button" />' },
6463
{ code: '<input type="week" role="button" />' },
6564
{ code: '<input type="hidden" role="button" />' },
66-
{ code: '<input type="hidden" role="img" />' },
6765
/* End all flavors of input */
6866
{ code: '<menuitem role="button" />;' },
6967
{ code: '<option className="foo" role="button" />' },
@@ -72,7 +70,9 @@ const alwaysValid = [
7270
{ code: '<tr role="button" />;' },
7371
/* HTML elements with neither an interactive or non-interactive valence (static) */
7472
{ code: '<a role="button" />' },
75-
{ code: '<a role="button" />;' },
73+
{ code: '<a role="img" />;' },
74+
{ code: '<a tabIndex="0" role="button" />' },
75+
{ code: '<a tabIndex="0" role="img" />' },
7676
{ code: '<acronym role="button" />;' },
7777
{ code: '<address role="button" />;' },
7878
{ code: '<applet role="button" />;' },
@@ -292,7 +292,6 @@ const alwaysValid = [
292292

293293
const neverValid = [
294294
/* Interactive elements */
295-
{ code: '<a tabIndex="0" role="img" />', errors: [expectedError] },
296295
{ code: '<a href="http://x.y.z" role="img" />', errors: [expectedError] },
297296
{ code: '<a href="http://x.y.z" tabIndex="0" role="img" />', errors: [expectedError] },
298297
/* All flavors of input */
@@ -305,6 +304,7 @@ const neverValid = [
305304
{ code: '<input type="datetime-local" role="img" />', errors: [expectedError] },
306305
{ code: '<input type="email" role="img" />', errors: [expectedError] },
307306
{ code: '<input type="file" role="img" />', errors: [expectedError] },
307+
{ code: '<input type="hidden" role="img" />', errors: [expectedError] },
308308
{ code: '<input type="image" role="img" />', errors: [expectedError] },
309309
{ code: '<input type="month" role="img" />', errors: [expectedError] },
310310
{ code: '<input type="number" role="img" />', errors: [expectedError] },
@@ -326,7 +326,6 @@ const neverValid = [
326326
{ code: '<textarea className="foo" role="img" />', errors: [expectedError] },
327327
{ code: '<tr role="img" />;', errors: [expectedError] },
328328
/* Interactive elements */
329-
{ code: '<a tabIndex="0" role="listitem" />', errors: [expectedError] },
330329
{ code: '<a href="http://x.y.z" role="listitem" />', errors: [expectedError] },
331330
{ code: '<a href="http://x.y.z" tabIndex="0" role="listitem" />', errors: [expectedError] },
332331
/* All flavors of input */

__tests__/src/rules/no-noninteractive-element-interactions-test.js

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ ruleTester.run('no-noninteractive-element-handlers', rule, {
6161
{ code: '<a tabIndex="0" onClick={() => void 0} />' },
6262
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
6363
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
64+
{ code: '<area onClick={() => {}} />;' },
6465
{ code: '<button onClick={() => void 0} className="foo" />' },
6566
{ code: '<menuitem onClick={() => {}} />;' },
6667
{ code: '<option onClick={() => void 0} className="foo" />' },
@@ -79,11 +80,8 @@ ruleTester.run('no-noninteractive-element-handlers', rule, {
7980
{ code: '<bdo onClick={() => {}} />;' },
8081
{ code: '<big onClick={() => {}} />;' },
8182
{ code: '<blink onClick={() => {}} />;' },
82-
{ code: '<blockquote onClick={() => {}} />;' },
8383
{ code: '<body onClick={() => {}} />;' },
84-
{ code: '<br onClick={() => {}} />;' },
8584
{ code: '<canvas onClick={() => {}} />;' },
86-
{ code: '<caption onClick={() => {}} />;' },
8785
{ code: '<center onClick={() => {}} />;' },
8886
{ code: '<cite onClick={() => {}} />;' },
8987
{ code: '<code onClick={() => {}} />;' },
@@ -93,8 +91,6 @@ ruleTester.run('no-noninteractive-element-handlers', rule, {
9391
{ code: '<data onClick={() => {}} />;' },
9492
{ code: '<datalist onClick={() => {}} />;' },
9593
{ code: '<del onClick={() => {}} />;' },
96-
{ code: '<details onClick={() => {}} />;' },
97-
{ code: '<dir onClick={() => {}} />;' },
9894
{ code: '<div />;' },
9995
{ code: '<div className="foo" />;' },
10096
{ code: '<div className="foo" {...props} />;' },
@@ -105,47 +101,33 @@ ruleTester.run('no-noninteractive-element-handlers', rule, {
105101
{ code: '<div onClick={() => void 0} {...props} />;' },
106102
{ code: '<div onClick={null} />;' },
107103
{ code: '<div onKeyUp={() => void 0} aria-hidden={false} />;' },
108-
{ code: '<dl onClick={() => {}} />;' },
109104
{ code: '<em onClick={() => {}} />;' },
110105
{ code: '<embed onClick={() => {}} />;' },
111-
{ code: '<figcaption onClick={() => {}} />;' },
112106
{ code: '<font onClick={() => {}} />;' },
113-
{ code: '<footer onClick={() => {}} />;' },
114107
{ code: '<frameset onClick={() => {}} />;' },
115108
{ code: '<head onClick={() => {}} />;' },
116109
{ code: '<header onClick={() => {}} />;' },
117110
{ code: '<hgroup onClick={() => {}} />;' },
118111
{ code: '<html onClick={() => {}} />;' },
119112
{ code: '<i onClick={() => {}} />;' },
120-
{ code: '<iframe onClick={() => {}} />;' },
121113
{ code: '<ins onClick={() => {}} />;' },
122114
{ code: '<kbd onClick={() => {}} />;' },
123115
{ code: '<keygen onClick={() => {}} />;' },
124-
{ code: '<label onClick={() => {}} />;' },
125-
{ code: '<legend onClick={() => {}} />;' },
126116
{ code: '<link onClick={() => {}} />;' },
127117
{ code: '<main onClick={null} />;' },
128118
{ code: '<map onClick={() => {}} />;' },
129-
{ code: '<mark onClick={() => {}} />;' },
130-
{ code: '<marquee onClick={() => {}} />;' },
131-
{ code: '<menu onClick={() => {}} />;' },
132119
{ code: '<meta onClick={() => {}} />;' },
133-
{ code: '<meter onClick={() => {}} />;' },
134120
{ code: '<noembed onClick={() => {}} />;' },
135121
{ code: '<noscript onClick={() => {}} />;' },
136122
{ code: '<object onClick={() => {}} />;' },
137123
{ code: '<optgroup onClick={() => {}} />;' },
138124
{ code: '<output onClick={() => {}} />;' },
139-
{ code: '<p onClick={() => {}} />;' },
140125
{ code: '<param onClick={() => {}} />;' },
141126
{ code: '<picture onClick={() => {}} />;' },
142-
{ code: '<pre onClick={() => {}} />;' },
143-
{ code: '<progress onClick={() => {}} />;' },
144127
{ code: '<q onClick={() => {}} />;' },
145128
{ code: '<rp onClick={() => {}} />;' },
146129
{ code: '<rt onClick={() => {}} />;' },
147130
{ code: '<rtc onClick={() => {}} />;' },
148-
{ code: '<ruby onClick={() => {}} />;' },
149131
{ code: '<s onClick={() => {}} />;' },
150132
{ code: '<samp onClick={() => {}} />;' },
151133
{ code: '<script onClick={() => {}} />;' },
@@ -161,7 +143,6 @@ ruleTester.run('no-noninteractive-element-handlers', rule, {
161143
{ code: '<summary onClick={() => {}} />;' },
162144
{ code: '<sup onClick={() => {}} />;' },
163145
{ code: '<th onClick={() => {}} />;' },
164-
{ code: '<time onClick={() => {}} />;' },
165146
{ code: '<title onClick={() => {}} />;' },
166147
{ code: '<track onClick={() => {}} />;' },
167148
{ code: '<tt onClick={() => {}} />;' },
@@ -220,14 +201,21 @@ ruleTester.run('no-noninteractive-element-handlers', rule, {
220201
invalid: [
221202
/* HTML elements with an inherent, non-interactive role */
222203
{ code: '<main onClick={() => void 0} />;', errors: [expectedError] },
223-
{ code: '<area onClick={() => {}} />;', errors: [expectedError] },
224204
{ code: '<article onClick={() => {}} />;', errors: [expectedError] },
225205
{ code: '<article onDblClick={() => void 0} />;', errors: [expectedError] },
206+
{ code: '<blockquote onClick={() => {}} />;', errors: [expectedError] },
207+
{ code: '<br onClick={() => {}} />;', errors: [expectedError] },
208+
{ code: '<caption onClick={() => {}} />;', errors: [expectedError] },
226209
{ code: '<dd onClick={() => {}} />;', errors: [expectedError] },
210+
{ code: '<details onClick={() => {}} />;', errors: [expectedError] },
227211
{ code: '<dfn onClick={() => {}} />;', errors: [expectedError] },
212+
{ code: '<dl onClick={() => {}} />;', errors: [expectedError] },
213+
{ code: '<dir onClick={() => {}} />;', errors: [expectedError] },
228214
{ code: '<dt onClick={() => {}} />;', errors: [expectedError] },
229215
{ code: '<fieldset onClick={() => {}} />;', errors: [expectedError] },
216+
{ code: '<figcaption onClick={() => {}} />;', errors: [expectedError] },
230217
{ code: '<figure onClick={() => {}} />;', errors: [expectedError] },
218+
{ code: '<footer onClick={() => {}} />;', errors: [expectedError] },
231219
{ code: '<form onClick={() => {}} />;', errors: [expectedError] },
232220
{ code: '<frame onClick={() => {}} />;', errors: [expectedError] },
233221
{ code: '<h1 onClick={() => {}} />;', errors: [expectedError] },
@@ -237,15 +225,27 @@ ruleTester.run('no-noninteractive-element-handlers', rule, {
237225
{ code: '<h5 onClick={() => {}} />;', errors: [expectedError] },
238226
{ code: '<h6 onClick={() => {}} />;', errors: [expectedError] },
239227
{ code: '<hr onClick={() => {}} />;', errors: [expectedError] },
228+
{ code: '<iframe onClick={() => {}} />;', errors: [expectedError] },
240229
{ code: '<img onClick={() => {}} />;', errors: [expectedError] },
230+
{ code: '<label onClick={() => {}} />;', errors: [expectedError] },
231+
{ code: '<legend onClick={() => {}} />;', errors: [expectedError] },
241232
{ code: '<li onClick={() => {}} />;', errors: [expectedError] },
233+
{ code: '<mark onClick={() => {}} />;', errors: [expectedError] },
234+
{ code: '<marquee onClick={() => {}} />;', errors: [expectedError] },
235+
{ code: '<menu onClick={() => {}} />;', errors: [expectedError] },
236+
{ code: '<meter onClick={() => {}} />;', errors: [expectedError] },
242237
{ code: '<nav onClick={() => {}} />;', errors: [expectedError] },
243238
{ code: '<ol onClick={() => {}} />;', errors: [expectedError] },
239+
{ code: '<p onClick={() => {}} />;', errors: [expectedError] },
240+
{ code: '<pre onClick={() => {}} />;', errors: [expectedError] },
241+
{ code: '<progress onClick={() => {}} />;', errors: [expectedError] },
242+
{ code: '<ruby onClick={() => {}} />;', errors: [expectedError] },
244243
{ code: '<table onClick={() => {}} />;', errors: [expectedError] },
245244
{ code: '<tbody onClick={() => {}} />;', errors: [expectedError] },
246245
{ code: '<td onClick={() => {}} />;', errors: [expectedError] },
247246
{ code: '<tfoot onClick={() => {}} />;', errors: [expectedError] },
248247
{ code: '<thead onClick={() => {}} />;', errors: [expectedError] },
248+
{ code: '<time onClick={() => {}} />;', errors: [expectedError] },
249249
{ code: '<ol onClick={() => {}} />;', errors: [expectedError] },
250250
{ code: '<ul onClick={() => {}} />;', errors: [expectedError] },
251251
/* HTML elements attributed with a non-interactive role */

0 commit comments

Comments
 (0)