Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions __tests__/src/rules/control-has-associated-label-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,12 @@ const alwaysValid = [
{ code: '<thead />' },
{ code: '<time />' },
{ code: '<ul />' },
// includeRoles - valid when label is present
{ code: '<div role="alert">Important message</div>' },
{ code: '<div role="alert" aria-label="Warning" />' },
{ code: '<div role="dialog" aria-label="Confirm action" />' },
{ code: '<div role="dialog">Are you sure?</div>' },
// Non-interactive Roles
{ code: '<div role="alert" />' },
{ code: '<div role="alertdialog" />' },
{ code: '<div role="application" />' },
{ code: '<div role="article" />' },
Expand All @@ -171,7 +175,6 @@ const alwaysValid = [
{ code: '<div role="complementary" />' },
{ code: '<div role="contentinfo" />' },
{ code: '<div role="definition" />' },
{ code: '<div role="dialog" />' },
{ code: '<div role="directory" />' },
{ code: '<div role="document" />' },
{ code: '<div role="feed" />' },
Expand Down Expand Up @@ -284,6 +287,9 @@ const neverValid = [
{ code: '<div role="switch" />', errors: [expectedError] },
{ code: '<div role="tab" />', errors: [expectedError] },
{ code: '<div role="textbox" />', errors: [expectedError] },
// includeRoles - these become invalid when includeRoles: ['alert', 'dialog'] is configured
{ code: '<div role="alert" />', errors: [expectedError] },
{ code: '<div role="dialog" />', errors: [expectedError] },
];

const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
Expand Down Expand Up @@ -318,10 +324,31 @@ ruleTester.run(`${ruleName}:no-config`, rule, {
valid: parsers.all([].concat(
{ code: '<input type="hidden" />' },
{ code: '<input type="text" aria-hidden="true" />' },
// alert and dialog are valid without config (no includeRoles)
{ code: '<div role="alert" />' },
{ code: '<div role="dialog" />' },
))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<input type="text" />', errors: [expectedError] },
))
.map(parserOptionsMapper),
});

ruleTester.run(`${ruleName}:includeRoles`, rule, {
valid: parsers.all([].concat(
{ code: '<div role="alert">Important message</div>', options: [{ includeRoles: ['alert'] }] },
{ code: '<div role="alert" aria-label="Warning" />', options: [{ includeRoles: ['alert'] }] },
{ code: '<div role="dialog" aria-label="Confirm" />', options: [{ includeRoles: ['dialog'] }] },
{ code: '<div role="dialog">Content</div>', options: [{ includeRoles: ['dialog'] }] },
// Roles NOT in includeRoles should still pass without label
{ code: '<div role="alert" />', options: [{ includeRoles: ['dialog'] }] },
))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div role="alert" />', options: [{ includeRoles: ['alert'] }], errors: [expectedError] },
{ code: '<div role="dialog" />', options: [{ includeRoles: ['dialog'] }], errors: [expectedError] },
{ code: '<div role="alert"><span /></div>', options: [{ includeRoles: ['alert'] }], errors: [expectedError] },
))
.map(parserOptionsMapper),
});
5 changes: 5 additions & 0 deletions docs/rules/control-has-associated-label.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ This rule takes one optional object argument of type object:
"tree",
"treegrid",
],
"includeRoles": [
"alert",
"dialog",
],
"depth": 3,
}],
}
Expand All @@ -95,6 +99,7 @@ This rule takes one optional object argument of type object:
- `controlComponents` is a list of custom React Components names that will render down to an interactive element.
- `ignoreElements` is an array of elements that should not be considered control (interactive) elements and therefore they do not require a text label.
- `ignoreRoles` is an array of ARIA roles that should not be considered control (interactive) roles and therefore they do not require a text label.
- `includeRoles` is an array of ARIA roles that are not interactive by default but should still require an accessible text label. For example, `['alert', 'dialog']` will enforce that elements with these roles have a label. If a role appears in both `ignoreRoles` and `includeRoles`, `ignoreRoles` takes precedence.
- `depth` (default 2, max 25) is an integer that determines how deep within a `JSXElement` the rule should look for text content or an element with a label to determine if the interactive element will have an accessible label.

### Succeed
Expand Down
5 changes: 4 additions & 1 deletion src/rules/control-has-associated-label.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const schema = generateObjSchema({
controlComponents: arraySchema,
ignoreElements: arraySchema,
ignoreRoles: arraySchema,
includeRoles: arraySchema,
depth: {
description: 'JSX tree depth limit to check for accessible label',
type: 'integer',
Expand All @@ -54,6 +55,7 @@ export default ({
controlComponents = [],
ignoreElements = [],
ignoreRoles = [],
includeRoles = [],
} = options;

const newIgnoreElements = new Set([].concat(ignoreElements, ignoreList));
Expand All @@ -77,6 +79,7 @@ export default ({
const nodeIsInteractiveElement = isInteractiveElement(tag, props);
const nodeIsInteractiveRole = isInteractiveRole(tag, props);
const nodeIsControlComponent = controlComponents.indexOf(tag) > -1;
const nodeIsIncludedRole = includes(includeRoles, role);

if (nodeIsHiddenFromScreenReader) {
return;
Expand All @@ -90,7 +93,7 @@ export default ({
&& nodeIsInteractiveRole
)
|| nodeIsControlComponent

|| nodeIsIncludedRole
) {
// Prevent crazy recursion.
const recursionDepth = Math.min(
Expand Down
Loading