Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added allowedInvalidRoles option to aria-role #828

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
});
},
});
},
};