Skip to content

[New] add isInteractiveRole helper util, see #601#602

Open
johanrd wants to merge 4 commits into
A11yance:mainfrom
johanrd:isInteractiveRole
Open

[New] add isInteractiveRole helper util, see #601#602
johanrd wants to merge 4 commits into
A11yance:mainfrom
johanrd:isInteractiveRole

Conversation

@johanrd
Copy link
Copy Markdown

@johanrd johanrd commented Apr 27, 2026

See #601

Implements a helper util for the query "Is this role interactive?" — filter concrete roles whose superClass chain includes widget.

Inspired by eslint-plugins independently implementing the same superClass.some(chain => chain.includes('widget')) loop. Three of them (angular-eslint, lit-a11y, eslint-plugin-ember) also independently arrived at the same two special cases: exclude progressbar (its value is always readonly), include toolbar (supports aria-activedescendant in practice):

@johanrd johanrd marked this pull request as draft April 27, 2026 08:49
@codesandbox-ci
Copy link
Copy Markdown

codesandbox-ci Bot commented Apr 27, 2026

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@johanrd johanrd force-pushed the isInteractiveRole branch from 53ba5b1 to f08b489 Compare April 27, 2026 09:03
Comment thread src/isInteractiveRole.js Outdated
const OVERRIDE_NON_INTERACTIVE: Set<string> = new Set(['progressbar']);

// Roles that are not widget descendants but do accept user input in practice.
const interactiveRoles: Set<string> = new Set(['toolbar']);
Copy link
Copy Markdown
Author

@johanrd johanrd Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ljharb not sure about using a Set here, as browserslist lists both "ie 11" and "node": ">= 0.4". This is possible to work around, sure, but wanted to check in first, as Set seems to be part of the public api anyways, e.g in readme:

The roles are provided in a Set.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's actually more of a "Setlike" - see

const elementRoleMap: TAriaQueryMap<
ElementARIARoleRelations,
ARIARoleRelationConcept,
RoleSet,
> = {
entries: function (): ElementARIARoleRelations {
return elementRoles;
},
forEach: function (
fn: (RoleSet, ARIARoleRelationConcept, ElementARIARoleRelations) => void,
thisArg: any = null,
): void {
for (let [key, values] of elementRoles) {
fn.call(thisArg, values, key, elementRoles);
}
},
get: function (key: ARIARoleRelationConcept): ?RoleSet {
const item = elementRoles.filter(tuple => (
key.name === tuple[0].name
&& ariaRoleRelationConceptAttributeEquals(key.attributes, tuple[0].attributes)
))[0];
return item && item[1];
},
has: function (key: ARIARoleRelationConcept): boolean {
return !!elementRoleMap.get(key);
},
keys: function (): Array<ARIARoleRelationConcept> {
return elementRoles.map(([key]) => key);
},
values: function (): Array<RoleSet> {
return elementRoles.map(([, values]) => values);
},
};
. So indeed we can't use a native Set.

@johanrd johanrd marked this pull request as ready for review April 28, 2026 09:07
@johanrd johanrd requested a review from ljharb April 28, 2026 09:07
Comment thread src/isInteractiveRole.js Outdated
}

export default function isInteractiveRole(role: string): boolean {
return interactiveRoles.indexOf(role) !== -1;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'd probably be more performant to build up a null object, rather than an array?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, thanks – addressed now.

Went with a plain {} (rather than Object.create(null) to avoid FlowIgnore or cast as any ((Flow seemed to reject Object.create(null) against { [string]: true }). But happy to switch to Object.create(null) with a suppression or cast if that is preferred.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ __proto__: null } is always better than Object.create(null), fwiw.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, changed to '{ proto: null }' now.

@johanrd johanrd force-pushed the isInteractiveRole branch from d8d7538 to b7029e4 Compare April 30, 2026 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants