Skip to content

Commit 14a3131

Browse files
committed
Unify role type and element interactivity API
1 parent 6c6d1d1 commit 14a3131

File tree

2 files changed

+49
-53
lines changed

2 files changed

+49
-53
lines changed

src/compiler/compile/nodes/Element.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,13 @@ import { Literal } from 'estree';
2424
import compiler_warnings from '../compiler_warnings';
2525
import compiler_errors from '../compiler_errors';
2626
import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query';
27-
import { is_interactive_element, is_non_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element } from '../utils/a11y';
27+
import { role_type, RoleType, element_interactivity, ElementInteractivity, is_presentation_role, is_hidden_from_screen_reader, is_semantic_role_element } from '../utils/a11y';
2828

2929
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(' ');
3030
const aria_attribute_set = new Set(aria_attributes);
3131

3232
const aria_roles = roles.keys();
3333
const aria_role_set = new Set(aria_roles);
34-
const aria_role_abstract_set = new Set(roles.keys().filter(role => roles.get(role).abstract));
3534

3635
const a11y_required_attributes = {
3736
a: ['href'],
@@ -497,7 +496,7 @@ export default class Element extends Node {
497496

498497
if (typeof value === 'string') {
499498
value.split(regex_any_repeated_whitespaces).forEach((current_role: ARIARoleDefintionKey) => {
500-
if (current_role && aria_role_abstract_set.has(current_role)) {
499+
if (current_role && role_type(current_role) === RoleType.Abstract) {
501500
component.warn(attribute, compiler_warnings.a11y_no_abstract_role(current_role));
502501
} else if (current_role && !aria_role_set.has(current_role)) {
503502
const match = fuzzymatch(current_role, aria_roles);
@@ -534,12 +533,14 @@ export default class Element extends Node {
534533
}
535534

536535
// no-interactive-element-to-noninteractive-role
537-
if (is_interactive_element(this.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role))) {
536+
if (element_interactivity(this.name, attribute_map) === ElementInteractivity.Interactive
537+
&& (role_type(current_role) === RoleType.NonInteractive || is_presentation_role(current_role))) {
538538
component.warn(this, compiler_warnings.a11y_no_interactive_element_to_noninteractive_role(current_role, this.name));
539539
}
540540

541541
// no-noninteractive-element-to-interactive-role
542-
if (is_non_interactive_element(this.name, attribute_map) && is_interactive_roles(current_role)) {
542+
if (element_interactivity(this.name, attribute_map) === ElementInteractivity.NonInteractive
543+
&& role_type(current_role) === RoleType.Interactive) {
543544
component.warn(this, compiler_warnings.a11y_no_noninteractive_element_to_interactive_role(current_role, this.name));
544545
}
545546
});
@@ -579,7 +580,7 @@ export default class Element extends Node {
579580
if (
580581
!is_hidden_from_screen_reader(this.name, attribute_map) &&
581582
(!role || is_non_presentation_role) &&
582-
!is_interactive_element(this.name, attribute_map) &&
583+
element_interactivity(this.name, attribute_map) !== ElementInteractivity.Interactive &&
583584
!this.attributes.find(attr => attr.is_spread)
584585
) {
585586
const has_key_event =
@@ -597,7 +598,10 @@ export default class Element extends Node {
597598
}
598599

599600
// no-noninteractive-tabindex
600-
if (!is_interactive_element(this.name, attribute_map) && !is_interactive_roles(attribute_map.get('role')?.get_static_value() as ARIARoleDefintionKey)) {
601+
if (element_interactivity(this.name, attribute_map) !== ElementInteractivity.Interactive
602+
&& role_type(
603+
attribute_map.get('role')?.get_static_value() as ARIARoleDefintionKey
604+
) !== RoleType.Interactive) {
601605
const tab_index = attribute_map.get('tabindex');
602606
if (tab_index && (!tab_index.is_static || Number(tab_index.get_static_value()) >= 0)) {
603607
component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex);

src/compiler/compile/utils/a11y.ts

+38-46
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query';
88
import Attribute from '../nodes/Attribute';
99

10-
const non_abstract_roles = [...roles_map.keys()].filter((name) => !roles_map.get(name).abstract && name !== 'generic');
10+
const aria_roles = roles_map.keys();
11+
const abstract_roles = new Set(aria_roles.filter(role => roles_map.get(role).abstract));
12+
const non_abstract_roles = aria_roles.filter((name) => !abstract_roles.has(name));
1113

1214
const non_interactive_roles = new Set(
1315
non_abstract_roles
@@ -32,12 +34,23 @@ const interactive_roles = new Set(
3234
non_abstract_roles.filter((name) => !non_interactive_roles.has(name))
3335
);
3436

35-
export function is_non_interactive_roles(role: ARIARoleDefintionKey) {
36-
return non_interactive_roles.has(role);
37+
export enum RoleType {
38+
Interactive = 'interactive',
39+
NonInteractive = 'non-interactive',
40+
Abstract = 'abstract',
41+
Invalid = 'invalid',
3742
}
3843

39-
export function is_interactive_roles(role: ARIARoleDefintionKey) {
40-
return interactive_roles.has(role);
44+
export function role_type(role: ARIARoleDefintionKey): RoleType {
45+
if (interactive_roles.has(role)) {
46+
return RoleType.Interactive;
47+
} else if (non_interactive_roles.has(role)) {
48+
return RoleType.NonInteractive;
49+
} else if (abstract_roles.has(role)) {
50+
return RoleType.Abstract;
51+
} else {
52+
return RoleType.Invalid;
53+
}
4154
}
4255

4356
const presentation_roles = new Set(['presentation', 'none']);
@@ -65,7 +78,7 @@ export function is_hidden_from_screen_reader(tag_name: string, attribute_map: Ma
6578
const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = [];
6679

6780
elementRoles.entries().forEach(([schema, roles]) => {
68-
if ([...roles].every((role) => non_interactive_roles.has(role))) {
81+
if ([...roles].every((role) => role !== 'generic' && non_interactive_roles.has(role))) {
6982
non_interactive_element_role_schemas.push(schema);
7083
}
7184
});
@@ -102,7 +115,6 @@ elementAXObjects.entries().forEach(([schema, ax_object]) => {
102115
}
103116
});
104117

105-
106118
function match_schema(
107119
schema: ARIARoleRelationConcept,
108120
tag_name: string,
@@ -123,70 +135,50 @@ function match_schema(
123135
});
124136
}
125137

126-
export function is_interactive_element(
127-
tag_name: string,
128-
attribute_map: Map<string, Attribute>
129-
): boolean {
130-
if (
138+
export enum ElementInteractivity {
139+
Interactive = 'interactive',
140+
NonInteractive = 'non-interactive',
141+
Static = 'static',
142+
}
143+
144+
export function element_interactivity(
145+
tag_name: string,
146+
attribute_map: Map<string, Attribute>
147+
): ElementInteractivity {
148+
if (
131149
interactive_element_role_schemas.some((schema) =>
132150
match_schema(schema, tag_name, attribute_map)
133151
)
134152
) {
135-
return true;
153+
return ElementInteractivity.Interactive;
136154
}
137155

138-
if (
156+
if (
157+
tag_name !== 'header' &&
139158
non_interactive_element_role_schemas.some((schema) =>
140159
match_schema(schema, tag_name, attribute_map)
141160
)
142161
) {
143-
return false;
162+
return ElementInteractivity.NonInteractive;
144163
}
145164

146-
if (
165+
if (
147166
interactive_element_ax_object_schemas.some((schema) =>
148167
match_schema(schema, tag_name, attribute_map)
149168
)
150169
) {
151-
return true;
170+
return ElementInteractivity.Interactive;
152171
}
153172

154-
return false;
155-
}
156-
157-
export function is_non_interactive_element(
158-
tag_name: string,
159-
attribute_map: Map<string, Attribute>
160-
): boolean {
161-
if (tag_name === 'header') {
162-
return false;
163-
}
164-
165173
if (
166-
non_interactive_element_role_schemas.some((schema) =>
167-
match_schema(schema, tag_name, attribute_map)
168-
)
169-
) {
170-
return true;
171-
}
172-
173-
if (
174-
interactive_element_role_schemas.some((schema) =>
175-
match_schema(schema, tag_name, attribute_map)
176-
)
177-
) {
178-
return false;
179-
}
180-
181-
if (
182174
non_interactive_element_ax_object_schemas.some((schema) =>
183175
match_schema(schema, tag_name, attribute_map)
184176
)
185177
) {
186-
return true;
178+
return ElementInteractivity.NonInteractive;
187179
}
188180

189-
return false;
181+
return ElementInteractivity.Static;
190182
}
191183

192184
export function is_semantic_role_element(role: ARIARoleDefintionKey, tag_name: string, attribute_map: Map<string, Attribute>) {

0 commit comments

Comments
 (0)