Skip to content

Commit

Permalink
[New] aria-role: add allowedInvalidRoles option
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg authored and ljharb committed Dec 2, 2021
1 parent 30deacb commit 566011b
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 29 deletions.
8 changes: 8 additions & 0 deletions __tests__/src/rules/aria-role-test.js
Expand Up @@ -40,6 +40,10 @@ const invalidTests = createTests(invalidRoles).map((test) => {
return invalidTest;
});

const allowedInvalidRoles = [{
allowedInvalidRoles: ['invalid-role', 'other-invalid-role'],
}];

const ignoreNonDOMSchema = [{
ignoreNonDOM: true,
}];
Expand All @@ -57,6 +61,9 @@ ruleTester.run('aria-role', rule, {
{ code: '<div role="doc-abstract" />' },
{ code: '<div role="doc-appendix doc-bibliography" />' },
{ code: '<Bar baz />' },
{ code: '<img role="invalid-role" />', options: allowedInvalidRoles },
{ code: '<img role="invalid-role tabpanel" />', options: allowedInvalidRoles },
{ code: '<img role="invalid-role other-invalid-role" />', options: allowedInvalidRoles },
{ code: '<Foo role="bar" />', options: ignoreNonDOMSchema },
{ code: '<fakeDOM role="bar" />', options: ignoreNonDOMSchema },
{ code: '<img role="presentation" />', options: ignoreNonDOMSchema },
Expand All @@ -72,6 +79,7 @@ ruleTester.run('aria-role', rule, {
{ code: '<div role="tabpanel row range"></div>', errors: [errorMessage] },
{ code: '<div role="doc-endnotes range"></div>', errors: [errorMessage] },
{ code: '<div role />', errors: [errorMessage] },
{ code: '<div role="unknown-invalid-role" />', errors: [errorMessage], options: allowedInvalidRoles },
{ code: '<div role={null}></div>', errors: [errorMessage] },
{ code: '<Foo role="datepicker" />', errors: [errorMessage] },
{ code: '<Foo role="Button" />', errors: [errorMessage] },
Expand Down
3 changes: 3 additions & 0 deletions docs/rules/aria-role.md
Expand Up @@ -10,12 +10,15 @@ This rule takes one optional object argument of type object:
{
"rules": {
"jsx-a11y/aria-role": [ 2, {
"allowedInvalidRoles": ["text"],
"ignoreNonDOM": true
}],
}
}
```

`allowedInvalidRules` is an optional string array of custom roles that should be allowed in addition to the ARIA spec, such as for cases when you [need to use a non-standard role](https://axesslab.com/text-splitting).

For the `ignoreNonDOM` option, this determines if developer created components are checked.

### Succeed
Expand Down
67 changes: 38 additions & 29 deletions src/rules/aria-role.js
Expand Up @@ -14,6 +14,13 @@ import { generateObjSchema } from '../util/schemas';
const errorMessage = 'Elements with ARIA roles must use a valid, non-abstract ARIA role.';

const schema = generateObjSchema({
allowedInvalidRoles: {
items: {
type: 'string',
},
type: 'array',
uniqueItems: true,
},
ignoreNonDOM: {
type: 'boolean',
default: false,
Expand All @@ -28,42 +35,44 @@ export default {
schema: [schema],
},

create: (context) => ({
JSXAttribute: (attribute) => {
// Determine if ignoreNonDOM is set to true
// If true, then do not run rule.
const options = context.options[0] || {};
const ignoreNonDOM = !!options.ignoreNonDOM;
create: (context) => {
const options = context.options[0] || {};
const ignoreNonDOM = !!options.ignoreNonDOM;
const allowedInvalidRoles = new Set(options.allowedInvalidRoles || []);
const validRoles = new Set([...roles.keys()].filter((role) => roles.get(role).abstract === false));

if (ignoreNonDOM) {
const type = elementType(attribute.parent);
if (!dom.get(type)) {
return;
return ({
JSXAttribute: (attribute) => {
// If ignoreNonDOM and the parent isn't DOM, don't run rule.
if (ignoreNonDOM) {
const type = elementType(attribute.parent);
if (!dom.get(type)) {
return;
}
}
}

// Get prop name
const name = propName(attribute).toUpperCase();
// Get prop name
const name = propName(attribute).toUpperCase();

if (name !== 'ROLE') { return; }
if (name !== 'ROLE') { return; }

const value = getLiteralPropValue(attribute);
const value = getLiteralPropValue(attribute);

// If value is undefined, then the role attribute will be dropped in the DOM.
// If value is null, then getLiteralAttributeValue is telling us that the
// value isn't in the form of a literal.
if (value === undefined || value === null) { return; }
// If value is undefined, then the role attribute will be dropped in the DOM.
// If value is null, then getLiteralAttributeValue is telling us that the
// value isn't in the form of a literal.
if (value === undefined || value === null) { return; }

const values = String(value).split(' ');
const validRoles = [...roles.keys()].filter((role) => roles.get(role).abstract === false);
const isValid = values.every((val) => validRoles.indexOf(val) > -1);
const values = String(value).split(' ');
const isValid = values.every((val) => allowedInvalidRoles.has(val) || validRoles.has(val));

if (isValid === true) { return; }
if (isValid === true) { return; }

context.report({
node: attribute,
message: errorMessage,
});
},
}),
context.report({
node: attribute,
message: errorMessage,
});
},
});
},
};

2 comments on commit 566011b

@Julia1996
Copy link

Choose a reason for hiding this comment

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

Hi @ljharb and @JoshuaKGoldberg
Thank you for this new feature, it's very useful! Could you, please, release a new version so I can update my dependencies and use this feature?

@ljharb
Copy link
Member

@ljharb ljharb commented on 566011b May 18, 2022

Choose a reason for hiding this comment

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

@Julia1996 theres no timeline for a release, but I’ll try to cut one in the near future.

Please sign in to comment.