[feat] Add a11y rule to check no tabindex in nointeractive element (#6693)

* [feature] add util module to check element is interactive element

* [feature] add util module to check role is interactive role

* [feature] add a11y checker for no-nointeractive-tabindex

* [chore] add test for no-nointeractive-tabindex

* [chore] fix tabindex-no-positive test div -> button

* [refactor] bundle up two filter into one

* Refactor: export a11y-no-nointeractive-tabindex warning from compiler_warning

* slight refactor to use existing utils

Co-authored-by: tanhauhau <lhtan93@gmail.com>
pull/7838/head
Shinobu Hayashi 2 years ago committed by GitHub
parent 7331c06a74
commit 2cd661156e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -179,6 +179,10 @@ export default {
code: 'a11y-missing-content', code: 'a11y-missing-content',
message: `A11y: <${name}> element should have child content` message: `A11y: <${name}> element should have child content`
}), }),
a11y_no_nointeractive_tabindex: {
code: 'a11y-no-nointeractive-tabindex',
message: 'A11y: not interactive element cannot have positive tabIndex value'
},
redundant_event_modifier_for_touch: { redundant_event_modifier_for_touch: {
code: 'redundant-event-modifier', code: 'redundant-event-modifier',
message: 'Touch event handlers that don\'t use the \'event\' object are passive by default' message: 'Touch event handlers that don\'t use the \'event\' object are passive by default'

@ -24,7 +24,7 @@ import { Literal } from 'estree';
import compiler_warnings from '../compiler_warnings'; import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query';
import { is_interactive_element, is_non_interactive_roles, is_presentation_role } from '../utils/a11y'; import { is_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles } from '../utils/a11y';
const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const aria_attribute_set = new Set(aria_attributes); const aria_attribute_set = new Set(aria_attributes);
@ -549,8 +549,15 @@ export default class Element extends Node {
} }
} }
}); });
}
// no-nointeractive-tabindex
if (!is_interactive_element(this.name, attribute_map) && !is_interactive_roles(attribute_map.get('role')?.get_static_value() as ARIARoleDefintionKey)) {
const tab_index = attribute_map.get('tabindex');
if (tab_index && (!tab_index.is_static || Number(tab_index.get_static_value()) >= 0)) {
component.warn(this, compiler_warnings.a11y_no_nointeractive_tabindex);
}
}
}
validate_special_cases() { validate_special_cases() {
const { component, attributes, handlers } = this; const { component, attributes, handlers } = this;

@ -51,6 +51,10 @@ export function is_non_interactive_roles(role: ARIARoleDefintionKey) {
return non_interactive_roles.has(role); return non_interactive_roles.has(role);
} }
export function is_interactive_roles(role: ARIARoleDefintionKey) {
return interactive_roles.has(role);
}
const presentation_roles = new Set(['presentation', 'none']); const presentation_roles = new Set(['presentation', 'none']);
export function is_presentation_role(role: ARIARoleDefintionKey) { export function is_presentation_role(role: ARIARoleDefintionKey) {

@ -0,0 +1,14 @@
<!-- valid -->
<button />
<button tabindex='0' />
<button tabindex='{0}' />
<div />
<div tabindex='-1' />
<div role='button' tabindex='0' />
<div role='article' tabindex='-1' />
<article tabindex='-1' />
<!-- invalid -->
<div tabindex='0' />
<div role='article' tabindex='0' />
<article tabindex='0' />
<article tabindex='{0}' />

@ -0,0 +1,62 @@
[
{
"code": "a11y-no-nointeractive-tabindex",
"end": {
"character": 241,
"column": 20,
"line": 11
},
"message": "A11y: not interactive element cannot have positive tabIndex value",
"pos": 221,
"start": {
"character": 221,
"column": 0,
"line": 11
}
},
{
"code": "a11y-no-nointeractive-tabindex",
"end": {
"character": 277,
"column": 35,
"line": 12
},
"message": "A11y: not interactive element cannot have positive tabIndex value",
"pos": 242,
"start": {
"character": 242,
"column": 0,
"line": 12
}
},
{
"code": "a11y-no-nointeractive-tabindex",
"end": {
"character": 302,
"column": 24,
"line": 13
},
"message": "A11y: not interactive element cannot have positive tabIndex value",
"pos": 278,
"start": {
"character": 278,
"column": 0,
"line": 13
}
},
{
"code": "a11y-no-nointeractive-tabindex",
"end": {
"character": 329,
"column": 26,
"line": 14
},
"message": "A11y: not interactive element cannot have positive tabIndex value",
"pos": 303,
"start": {
"character": 303,
"column": 0,
"line": 14
}
}
]

@ -2,7 +2,7 @@
let foo; let foo;
</script> </script>
<div tabindex='-1'/> <button tabindex='-1'/>
<div tabindex='0'/> <button tabindex='0'/>
<div tabindex='1'/> <button tabindex='1'/>
<div tabindex='{foo}'/> <button tabindex='{foo}'/>

@ -4,14 +4,14 @@
"message": "A11y: avoid tabindex values above zero", "message": "A11y: avoid tabindex values above zero",
"start": { "start": {
"line": 7, "line": 7,
"column": 5, "column": 8,
"character": 76 "character": 85
}, },
"end": { "end": {
"line": 7, "line": 7,
"column": 17, "column": 20,
"character": 88 "character": 97
}, },
"pos": 76 "pos": 85
} }
] ]

Loading…
Cancel
Save