Socket
Socket
Sign inDemoInstall

eslint-plugin-jsx-a11y

Package Overview
Dependencies
195
Maintainers
4
Versions
81
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 5.1.0 to 6.0.0

__tests__/src/rules/anchor-is-valid-test.js

347

__tests__/src/rules/interactive-supports-focus-test.js

@@ -11,5 +11,12 @@ /* eslint-env jest */

import includes from 'array-includes';
import { RuleTester } from 'eslint';
import {
eventHandlers,
eventHandlersByType,
} from 'jsx-ast-utils';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/interactive-supports-focus';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';

@@ -22,126 +29,226 @@ // -----------------------------------------------------------------------------

const expectedError = {
message: 'Elements with interactive roles must be focusable.',
type: 'JSXOpeningElement',
};
function template(strings, ...keys) {
return (...values) => keys.reduce(
(acc, k, i) => acc + (values[k] || '') + strings[i + 1],
strings[0],
);
}
ruleTester.run('interactive-supports-focus', rule, {
const ruleName = 'interactive-supports-focus';
const type = 'JSXOpeningElement';
const codeTemplate = template`<${0} role="${1}" ${2}={() => void 0} />`;
const tabindexTemplate =
template`<${0} role="${1}" ${2}={() => void 0} tabIndex="0" />`;
const tabbableTemplate = template`Elements with the '${0}' interactive role must be tabbable.`;
const focusableTemplate = template`Elements with the '${0}' interactive role must be focusable.`;
const recommendedOptions =
(configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {});
const strictOptions =
(configs.strict.rules[`jsx-a11y/${ruleName}`][1] || {});
const alwaysValid = [
{ code: '<div />' },
{ code: '<div aria-hidden onClick={() => void 0} />' },
{ code: '<div aria-hidden={true == true} onClick={() => void 0} />' },
{ code: '<div aria-hidden={true === true} onClick={() => void 0} />' },
{ code: '<div aria-hidden={hidden !== false} onClick={() => void 0} />' },
{ code: '<div aria-hidden={hidden != false} onClick={() => void 0} />' },
{ code: '<div aria-hidden={1 < 2} onClick={() => void 0} />' },
{ code: '<div aria-hidden={1 <= 2} onClick={() => void 0} />' },
{ code: '<div aria-hidden={2 > 1} onClick={() => void 0} />' },
{ code: '<div aria-hidden={2 >= 1} onClick={() => void 0} />' },
{ code: '<div onClick={() => void 0} />;' },
{ code: '<div onClick={() => void 0} tabIndex={undefined} />;' },
{ code: '<div onClick={() => void 0} tabIndex="bad" />;' },
{ code: '<div onClick={() => void 0} role={undefined} />;' },
{ code: '<div role="section" onClick={() => void 0} />' },
{ code: '<div onClick={() => void 0} aria-hidden={false} />;' },
{ code: '<div onClick={() => void 0} {...props} />;' },
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input type="hidden" onClick={() => void 0} tabIndex="-1" />' },
{ code: '<input type="hidden" onClick={() => void 0} tabIndex={-1} />' },
{ code: '<input onClick={() => void 0} />' },
{ code: '<input onClick={() => void 0} role="combobox" />' },
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<area href="#" onClick={() => void 0} className="foo" />' },
{ code: '<area onClick={() => void 0} className="foo" />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<a onClick="showNextPage();">Next page</a>' },
{ code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>' },
{ code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>' },
{ code: '<a onClick={() => void 0} />' },
{ code: '<a tabIndex="0" onClick={() => void 0} />' },
{ code: '<a tabIndex={dynamicTabIndex} onClick={() => void 0} />' },
{ code: '<a tabIndex={0} onClick={() => void 0} />' },
{ code: '<a role="button" href="#" onClick={() => void 0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex={0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" role="button" />' },
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<input onClick={() => void 0} type="hidden" />;' },
{ code: '<span onClick="submitForm();">Submit</span>' },
{ code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>' },
{ code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>' },
{ code: '<span onClick="doSomething();" tabIndex="0">Click me!</span>' },
{ code: '<span onClick="doSomething();" tabIndex={0}>Click me!</span>' },
{ code: '<span onClick="doSomething();" tabIndex="-1">Click me too!</span>' },
{
code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>',
},
{ code: '<section onClick={() => void 0} />;' },
{ code: '<main onClick={() => void 0} />;' },
{ code: '<article onClick={() => void 0} />;' },
{ code: '<header onClick={() => void 0} />;' },
{ code: '<footer onClick={() => void 0} />;' },
{ code: '<div role="button" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="checkbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="link" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitem" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitemcheckbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitemradio" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="option" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="radio" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="spinbutton" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="switch" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="tab" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="textbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;' },
{ code: '<Input onClick={() => void 0} type="hidden" />;' },
];
const interactiveRoles = [
'button',
'checkbox',
'link',
'gridcell',
'menuitem',
'menuitemcheckbox',
'menuitemradio',
'option',
'radio',
'searchbox',
'slider',
'spinbutton',
'switch',
'tab',
'textbox',
'treeitem',
];
const recommendedRoles = [
'button',
'checkbox',
'link',
'searchbox',
'spinbutton',
'switch',
'textbox',
];
const strictRoles = [
'button',
'checkbox',
'link',
'progressbar',
'searchbox',
'slider',
'spinbutton',
'switch',
'textbox',
];
const staticElements = [
'div',
];
const triggeringHandlers = [
...eventHandlersByType.mouse,
...eventHandlersByType.keyboard,
];
const passReducer = (roles, handlers, messageTemplate) =>
staticElements.reduce((elementAcc, element) =>
elementAcc.concat(roles.reduce((roleAcc, role) =>
roleAcc.concat(handlers
.map(handler => ({
code: messageTemplate(element, role, handler),
}),
),
), []),
), []);
const failReducer = (roles, handlers, messageTemplate) =>
staticElements.reduce((elementAcc, element) =>
elementAcc.concat(roles.reduce((roleAcc, role) =>
roleAcc.concat(handlers
.map(handler => ({
code: codeTemplate(element, role, handler),
errors: [{
type,
message: messageTemplate(role),
}],
}),
),
), []),
), []);
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: [
{ code: '<div />' },
{ code: '<div aria-hidden onClick={() => void 0} />' },
{ code: '<div aria-hidden={true == true} onClick={() => void 0} />' },
{ code: '<div aria-hidden={true === true} onClick={() => void 0} />' },
{ code: '<div aria-hidden={hidden !== false} onClick={() => void 0} />' },
{ code: '<div aria-hidden={hidden != false} onClick={() => void 0} />' },
{ code: '<div aria-hidden={1 < 2} onClick={() => void 0} />' },
{ code: '<div aria-hidden={1 <= 2} onClick={() => void 0} />' },
{ code: '<div aria-hidden={2 > 1} onClick={() => void 0} />' },
{ code: '<div aria-hidden={2 >= 1} onClick={() => void 0} />' },
{ code: '<div onClick={() => void 0} />;' },
{ code: '<div onClick={() => void 0} tabIndex={undefined} />;' },
{ code: '<div onClick={() => void 0} tabIndex="bad" />;' },
{ code: '<div onClick={() => void 0} role={undefined} />;' },
{ code: '<div role="section" onClick={() => void 0} />' },
{ code: '<div onClick={() => void 0} aria-hidden={false} />;' },
{ code: '<div onClick={() => void 0} {...props} />;' },
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input type="hidden" onClick={() => void 0} tabIndex="-1" />' },
{ code: '<input type="hidden" onClick={() => void 0} tabIndex={-1} />' },
{ code: '<input onClick={() => void 0} />' },
{ code: '<input onClick={() => void 0} role="combobox" />' },
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<area href="#" onClick={() => void 0} className="foo" />' },
{ code: '<area onClick={() => void 0} className="foo" />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<a onClick="showNextPage();">Next page</a>' },
{ code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>' },
{ code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>' },
{ code: '<a onClick={() => void 0} />' },
{ code: '<a tabIndex="0" onClick={() => void 0} />' },
{ code: '<a tabIndex={dynamicTabIndex} onClick={() => void 0} />' },
{ code: '<a tabIndex={0} onClick={() => void 0} />' },
{ code: '<a role="button" href="#" onClick={() => void 0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex={0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" role="button" />' },
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<input onClick={() => void 0} type="hidden" />;' },
{ code: '<span onClick="submitForm();">Submit</span>', errors: [expectedError] },
{ code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>' },
{ code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>' },
{ code: '<span onClick="doSomething();" tabIndex="0">Click me!</span>' },
{ code: '<span onClick="doSomething();" tabIndex={0}>Click me!</span>' },
{ code: '<span onClick="doSomething();" tabIndex="-1">Click me too!</span>' },
{
code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>',
},
{ code: '<section onClick={() => void 0} />;' },
{ code: '<main onClick={() => void 0} />;' },
{ code: '<article onClick={() => void 0} />;' },
{ code: '<header onClick={() => void 0} />;' },
{ code: '<footer onClick={() => void 0} />;' },
{ code: '<div role="button" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="checkbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="link" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitem" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitemcheckbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitemradio" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="option" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="radio" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="spinbutton" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="switch" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="tab" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="textbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;' },
{ code: '<Input onClick={() => void 0} type="hidden" />;' },
].map(parserOptionsMapper),
...alwaysValid,
...passReducer(
interactiveRoles,
eventHandlers.filter(handler => !includes(triggeringHandlers, handler)),
codeTemplate,
),
...passReducer(
interactiveRoles.filter(role => !includes(recommendedRoles, role)),
eventHandlers.filter(handler => includes(triggeringHandlers, handler)),
tabindexTemplate,
),
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: [
...failReducer(recommendedRoles, triggeringHandlers, tabbableTemplate),
...failReducer(
interactiveRoles.filter(role => !includes(recommendedRoles, role)),
triggeringHandlers,
focusableTemplate,
),
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: [
...alwaysValid,
...passReducer(
interactiveRoles,
eventHandlers.filter(handler => !includes(triggeringHandlers, handler)),
codeTemplate,
),
...passReducer(
interactiveRoles.filter(role => !includes(strictRoles, role)),
eventHandlers.filter(handler => includes(triggeringHandlers, handler)),
tabindexTemplate,
),
]
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
invalid: [
// onClick
{ code: '<span role="button" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<a role="button" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="button" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="checkbox" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="link" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="gridcell" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="menuitem" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="menuitemcheckbox" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="menuitemradio" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="option" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="radio" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="searchbox" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="slider" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="spinbutton" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="switch" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="tab" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="textbox" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<div role="treeitem" onClick={() => void 0} />', errors: [expectedError] },
// onKeyPress
{ code: '<span role="button" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<a role="button" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="button" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="checkbox" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="link" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="gridcell" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="menuitem" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="menuitemcheckbox" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="menuitemradio" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="option" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="radio" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="searchbox" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="slider" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="spinbutton" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="switch" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="tab" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="textbox" onKeyPress={() => void 0} />', errors: [expectedError] },
{ code: '<div role="treeitem" onKeyPress={() => void 0} />', errors: [expectedError] },
// Other interactive handlers
{ code: '<div role="button" onKeyDown={() => void 0} />', errors: [expectedError] },
{ code: '<div role="button" onKeyUp={() => void 0} />', errors: [expectedError] },
{ code: '<div role="button" onMouseDown={() => void 0} />', errors: [expectedError] },
{ code: '<div role="button" onMouseUp={() => void 0} />', errors: [expectedError] },
].map(parserOptionsMapper),
...failReducer(strictRoles, triggeringHandlers, tabbableTemplate),
...failReducer(
interactiveRoles.filter(role => !includes(strictRoles, role)),
triggeringHandlers,
focusableTemplate,
),
]
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
});

@@ -12,2 +12,3 @@ /* eslint-env jest */

import { RuleTester } from 'eslint';
import assign from 'object.assign';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';

@@ -23,4 +24,3 @@ import rule from '../../../src/rules/label-has-for';

const expectedError = {
message: 'Form controls using a label to identify them must be ' +
'programmatically associated with the control using htmlFor',
message: 'Form label must have associated control',
type: 'JSXOpeningElement',

@@ -32,2 +32,14 @@ };

}];
const optionsRequiredNesting = [{
required: 'nesting',
}];
const optionsRequiredSome = [{
required: { some: ['nesting', 'id'] },
}];
const optionsRequiredEvery = [{
required: { every: ['nesting', 'id'] },
}];
const optionsChildrenAllowed = [{
allowChildren: true,
}];

@@ -37,25 +49,37 @@ ruleTester.run('label-has-for', rule, {

// DEFAULT ELEMENT 'label' TESTS
{ code: '<label htmlFor="foo" />' },
{ code: '<label htmlFor={"foo"} />' },
{ code: '<label htmlFor={foo} />' },
{ code: '<label htmlFor={`${id}`} />' },
{ code: '<div />' },
{ code: '<label htmlFor="foo">Test!</label>' },
{ code: '<label htmlFor="foo"><input /></label>' },
{ code: '<Label />' }, // lower-case convention refers to real HTML elements.
{ code: '<Label htmlFor="foo" />' },
{ code: '<Descriptor />' },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>' },
{ code: '<UX.Layout>test</UX.Layout>' },
// CUSTOM ELEMENT ARRAY OPTION TESTS
{ code: '<Label htmlFor="foo" />', options: array },
{ code: '<Label htmlFor={"foo"} />', options: array },
{ code: '<Label htmlFor={foo} />', options: array },
{ code: '<Label htmlFor={`${id}`} />', options: array },
{ code: '<Label htmlFor="foo" />', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<Label htmlFor={"foo"} />', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<Label htmlFor={foo} />', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<Label htmlFor={`${id}`} />', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<div />', options: array },
{ code: '<Label htmlFor="foo">Test!</Label>', options: array },
{ code: '<Descriptor htmlFor="foo" />', options: array },
{ code: '<Descriptor htmlFor={"foo"} />', options: array },
{ code: '<Descriptor htmlFor={foo} />', options: array },
{ code: '<Descriptor htmlFor={`${id}`} />', options: array },
{ code: '<div />', options: array },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: array },
{ code: '<Label htmlFor="something"><input /></Label>', options: array },
{ code: '<Label htmlFor="foo">Test!</Label>', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor="foo" />', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor={"foo"} />', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor={foo} />', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor={`${id}`} />', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: [assign({}, array[0], optionsRequiredSome[0])] },
{ code: '<label htmlFor="foo" />', options: optionsRequiredSome },
{ code: '<label htmlFor={"foo"} />', options: optionsRequiredSome },
{ code: '<label htmlFor={foo} />', options: optionsRequiredSome },
{ code: '<label htmlFor={`${id}`} />', options: optionsRequiredSome },
{ code: '<label htmlFor="foo">Test!</label>', options: optionsRequiredSome },
{ code: '<label><input /></label>', options: optionsRequiredSome },
{ code: '<label><input /></label>', options: optionsRequiredNesting },
{ code: '<label htmlFor="input"><input /></label>', options: optionsRequiredEvery },
{ code: '<label><input /></label>', options: optionsChildrenAllowed },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: [assign({}, array, optionsChildrenAllowed)] },
{ code: '<label>Test!</label>', options: optionsChildrenAllowed },
{ code: '<label htmlFor="foo">Test!</label>', options: optionsChildrenAllowed },
{ code: '<label>{children}</label>', options: optionsChildrenAllowed },
{ code: '<label htmlFor="children">{children}</label>', options: optionsChildrenAllowed },
].map(parserOptionsMapper),

@@ -69,4 +93,26 @@ invalid: [

{ code: '<label {...props}>Foo</label>', errors: [expectedError] },
// CUSTOM ELEMENT ARRAY OPTION TESTS
{ code: '<label><input /></label>', errors: [expectedError] },
{ code: '<label>{children}</label>', errors: [expectedError] },
{ code: '<label htmlFor="foo" />', errors: [expectedError] },
{ code: '<label htmlFor={"foo"} />', errors: [expectedError] },
{ code: '<label htmlFor={foo} />', errors: [expectedError] },
{ code: '<label htmlFor={`${id}`} />', errors: [expectedError] },
{ code: '<label htmlFor="foo">Test!</label>', errors: [expectedError] },
//
// // CUSTOM ELEMENT ARRAY OPTION TESTS
{ code: '<Label></Label>', errors: [expectedError], options: array },
{ code: '<Label htmlFor="foo" />', errors: [expectedError], options: array },
{ code: '<Label htmlFor={"foo"} />', errors: [expectedError], options: array },
{ code: '<Label htmlFor={foo} />', errors: [expectedError], options: array },
{ code: '<Label htmlFor={`${id}`} />', errors: [expectedError], options: array },
{ code: '<Label htmlFor="foo">Test!</Label>', errors: [expectedError], options: array },
{ code: '<Descriptor htmlFor="foo" />', errors: [expectedError], options: array },
{ code: '<Descriptor htmlFor={"foo"} />', errors: [expectedError], options: array },
{ code: '<Descriptor htmlFor={foo} />', errors: [expectedError], options: array },
{ code: '<Descriptor htmlFor={`${id}`} />', errors: [expectedError], options: array },
{
code: '<Descriptor htmlFor="foo">Test!</Descriptor>',
errors: [expectedError],
options: array,
},
{ code: '<Label id="foo" />', errors: [expectedError], options: array },

@@ -110,3 +156,9 @@ {

},
{ code: '<label>{children}</label>', errors: [expectedError], options: array },
{ code: '<label htmlFor="foo" />', errors: [expectedError], options: optionsRequiredNesting },
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredNesting },
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredSome },
{ code: '<label>{children}</label>', errors: [expectedError], options: optionsRequiredSome },
{ code: '<label>{children}</label>', errors: [expectedError], options: optionsRequiredNesting },
].map(parserOptionsMapper),
});

@@ -0,1 +1,13 @@

6.0.0 / 2017-06-05
=================
- [new] Add rule `anchor-is-valid`. See documentation for configuration options. Thanks @AlmeroSteyn.
- [breaking] `href-no-hash` replaced with `anchor-is-valid` in the recommended and strict configs. Use the `invalidHref` aspect (active by default) in `anchor-is-valid` to continue to apply the behavior provided by `href-no-hash`.
- [breaking] Removed support for ESLint peer dependency at version ^2.10.2.
- [update] The rule `label-has-for` now allows inputs nested in label tags. Previously it was strict about requiring a `for` attribute. Thanks @ignatiusreza and @mjaltamirano.
- [update] New configuration for `interactive-supports-focus`. Recommended and strict configs for now contain a trimmed-down whitelist of roles that will be checked.
- [fix] Incompatibility between node version 4 and 5. Thanks @evilebottnawi.
- [fix] Missing README entry for `media-has-caption`. Thanks @ismail-syed.
- [fix] README updates explaining recommended and strict configs. Thanks @Donaldini.
- [fix] Updated to aria-query@0.7.0, which includes new ARIA 1.1 properties. Previously, the `aria-props` rule incorrectly threw errors for these new properties.
5.0.3 / 2017-05-16

@@ -19,3 +31,3 @@ ==================

- [breaking] Rule `onclick-has-role` is removed. Replaced with `no-static-element-interactions` and `no-noninteractive-element-interactions`.
- [breaking] Rule `onclick-has-focus` is removed. Replaced with `interactive-supports-focus`.
- [breaking] Rule `onclick-has-focus` is removed. Replaced with `interactive-supports-focus`.
- [new] - Add rule `media-has-caption` rule

@@ -26,3 +38,3 @@ - [new] - Add `ignoreNonDOM` option to `no-autofocus`.

- [new] - Add rule `no-noninteractive-tabindex`
- [new] - Configs split into "recommended" and "strict".
- [new] - Configs split into "recommended" and "strict".
- [enhanced] - Configuration options added to `no-static-element-interactions` and `no-noninteractive-element-interactions`. Options allow for fine-tuning of elements and event handlers to check.

@@ -29,0 +41,0 @@

@@ -7,4 +7,6 @@ # interactive-supports-focus

### Case: This element is a stand-alone control like a button, a link or a form element
### Case: I got the error "Elements with the '${role}' interactive role must be tabbable". How can I fix this?
This element is a stand-alone control like a button, a link or a form element. A user should be able to reach this element by pressing the tab key on their keyboard.
Add the `tabIndex` property to your component. A value of zero indicates that this element can be tabbed to.

@@ -22,8 +24,7 @@

Generally buttons, links and form elements should be reachable via tab key presses.
An element that can be tabbed to is said to be in the _tab ring_.
Generally buttons, links and form elements should be reachable via tab key presses. An element that can be tabbed to is said to be in the _tab ring_.
### Case: This element is part of a group of buttons, links, menu items, etc
### Case: I got the error "Elements with the '${role}' interactive role must be focusable". How can I fix this?
One item in a group should have a tabindex of zero, the rest should have a tabindex of -1. A value of zero makes the element _tabbable_. A value of -1 makes the element _focusable_.
This element is part of a group of buttons, links, menu items, etc. Or this element is part of a composite widget. Composite widgets prescribe standard [keyboard interaction patterns](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav). Within a group of similar elements -- like a button bar -- or within a composite widget, elements that can be focused are given a tabindex of -1. This makes the element *focusable* but not *tabbable*. Generally one item in a group should have a tabindex of zero so that a user can tab to the component. Once an element in the component has focus, your key management behaviors will control traversal within the component's pieces. As the UI author, you will need to implement the key handling behaviors such as listening for traversal key (up/down/left/right) presses and moving the page focus between the focusable elements in your widget.

@@ -62,4 +63,64 @@ ```

This rule takes no arguments.
This rule takes an options object with the key `tabbable`. The value is an array of interactive ARIA roles that should be considered tabbable, not just focusable. Any interactive role not included in this list will be flagged as needing to be focusable (tabindex of -1).
```
'jsx-a11y/interactive-supports-focus': [
'error',
{
tabbable: [
'button',
'checkbox',
'link',
'searchbox',
'spinbutton',
'switch',
'textbox',
],
},
]
```
The recommended options list interactive roles that act as form elements. Generally, elements with a role like `menuitem` are a part of a composite widget. Focus in a composite widget is controlled and moved programmatically to satisfy the prescribed [keyboard interaction pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav) for the widget.
The list of possible values includes:
```
[
'button',
'checkbox',
'columnheader',
'combobox',
'grid',
'gridcell',
'link',
'listbox',
'menu',
'menubar',
'menuitem',
'menuitemcheckbox',
'menuitemradio',
'option',
'progressbar',
'radio',
'radiogroup',
'row',
'rowheader',
'searchbox',
'slider',
'spinbutton',
'switch',
'tab',
'tablist',
'textbox',
'toolbar',
'tree',
'treegrid',
'treeitem',
'doc-backlink',
'doc-biblioref',
'doc-glossref',
'doc-noteref',
]
```
### Succeed

@@ -66,0 +127,0 @@ ```jsx

# label-has-for
Enforce label tags have htmlFor attribute. Form controls using a label to identify them must have only one label that is programmatically associated with the control using: label htmlFor=[ID of control].
Enforce label tags have associated control.
There are two supported ways to associate a label with a control:
- nesting: by wrapping a control in a label tag
- id: by using the prop `htmlFor` as in `htmlFor=[ID of control]`
To fully cover 100% of assistive devices, you're encouraged to validate for both nesting and id.
## Rule details

@@ -14,3 +21,7 @@

"components": [ "Label" ],
}],
"required": {
"every": [ "nesting", "id" ]
},
"allowChildren": false,
}],
}

@@ -47,2 +58,18 @@ }

The `required` option (defaults to `"required": { "every": ["nesting", "id"] }`) determines which checks are activated. You're allowed to pass in one of the following types:
- string: must be one of the acceptable strings (`"nesting"` or `"id"`)
- object, must have one of the following properties:
- some: an array of acceptable strings, will pass if ANY of the requested checks passed
- every: an array of acceptable strings, will pass if ALL of the requested checks passed
The `allowChildren` option (defaults to `false`) determines whether `{children}` content is allowed to be passed into a `label` element. For example, the following pattern, by default, is not allowed:
```js
<label>{children}</label>
```
However, if `allowChildren` is set to `true`, no error will be raised. If you want to pass in `{children}` content without raising an error, because you cannot be sure what `{children}` will render, then set `allowChildren` to `true`.
Note that passing props as spread attribute without `htmlFor` explicitly defined will cause this rule to fail. Explicitly pass down `htmlFor` prop for rule to pass. The prop must have an actual value to pass. Use `Label` component above as a reference. **It is a good thing to explicitly pass props that you expect to be passed for self-documentation.** For example:

@@ -77,4 +104,6 @@

```jsx
<input type="text" id="firstName" />
<label htmlFor="firstName">First Name</label>
<label htmlFor="firstName">
<input type="text" id="firstName" />
First Name
</label>
```

@@ -81,0 +110,0 @@

@@ -10,2 +10,3 @@ 'use strict';

'anchor-has-content': require('./rules/anchor-has-content'),
'anchor-is-valid': require('./rules/anchor-is-valid'),
'aria-activedescendant-has-tabindex': require('./rules/aria-activedescendant-has-tabindex'),

@@ -18,3 +19,2 @@ 'aria-props': require('./rules/aria-props'),

'heading-has-content': require('./rules/heading-has-content'),
'href-no-hash': require('./rules/href-no-hash'),
'html-has-lang': require('./rules/html-has-lang'),

@@ -54,2 +54,3 @@ 'iframe-has-title': require('./rules/iframe-has-title'),

'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',

@@ -62,7 +63,8 @@ 'jsx-a11y/aria-props': 'error',

'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/href-no-hash': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/iframe-has-title': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/interactive-supports-focus': 'error',
'jsx-a11y/interactive-supports-focus': ['error', {
tabbable: ['button', 'checkbox', 'link', 'searchbox', 'spinbutton', 'switch', 'textbox']
}],
'jsx-a11y/label-has-for': 'error',

@@ -113,2 +115,3 @@ 'jsx-a11y/media-has-caption': 'error',

'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',

@@ -121,7 +124,8 @@ 'jsx-a11y/aria-props': 'error',

'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/href-no-hash': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/iframe-has-title': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/interactive-supports-focus': 'error',
'jsx-a11y/interactive-supports-focus': ['error', {
tabbable: ['button', 'checkbox', 'link', 'progressbar', 'searchbox', 'slider', 'spinbutton', 'switch', 'textbox']
}],
'jsx-a11y/label-has-for': 'error',

@@ -128,0 +132,0 @@ 'jsx-a11y/media-has-caption': 'error',

@@ -44,3 +44,3 @@ 'use strict';

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } /**
* @fileoverview Enforce that elements with onClick handlers must be focusable.
* @fileoverview Enforce that elements with onClick handlers must be tabbable.
* @author Ethan Cohen

@@ -54,5 +54,11 @@ *

var errorMessage = 'Elements with interactive roles must be focusable.';
var schema = (0, _schemas.generateObjSchema)();
var schema = (0, _schemas.generateObjSchema)({
tabbable: (0, _schemas.enumArraySchema)([].concat(_toConsumableArray(_ariaQuery.roles.keys())).filter(function (name) {
return !_ariaQuery.roles.get(name).abstract;
}).filter(function (name) {
return _ariaQuery.roles.get(name).superClass.some(function (klasses) {
return (0, _arrayIncludes2.default)(klasses, 'widget');
});
}))
});
var domElements = [].concat(_toConsumableArray(_ariaQuery.dom.keys()));

@@ -71,2 +77,3 @@

JSXOpeningElement: function JSXOpeningElement(node) {
var tabbable = context.options && context.options[0] && context.options[0].tabbable || [];
var attributes = node.attributes;

@@ -89,6 +96,16 @@ var type = (0, _jsxAstUtils.elementType)(node);

if (hasInteractiveProps && (0, _isInteractiveRole2.default)(type, attributes) && !(0, _isInteractiveElement2.default)(type, attributes) && !(0, _isNonInteractiveElement2.default)(type, attributes) && !(0, _isNonInteractiveRole2.default)(type, attributes) && !hasTabindex) {
context.report({
node: node,
message: errorMessage
});
var role = (0, _jsxAstUtils.getLiteralPropValue)((0, _jsxAstUtils.getProp)(attributes, 'role'));
if ((0, _arrayIncludes2.default)(tabbable, role)) {
// Always tabbable, tabIndex = 0
context.report({
node: node,
message: 'Elements with the \'' + role + '\' interactive role must be tabbable.'
});
} else {
// Focusable, tabIndex = -1 or 0
context.report({
node: node,
message: 'Elements with the \'' + role + '\' interactive role must be focusable.'
});
}
}

@@ -95,0 +112,0 @@ }

@@ -7,7 +7,13 @@ 'use strict';

/**
* @fileoverview Enforce label tags have htmlFor attribute.
* @author Ethan Cohen
*/
var _hasAccessibleChild = require('../util/hasAccessibleChild');
var _hasAccessibleChild2 = _interopRequireDefault(_hasAccessibleChild);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var errorMessage = 'Form label must have associated control'; /**
* @fileoverview Enforce label tags have htmlFor attribute.
* @author Ethan Cohen
*/
// ----------------------------------------------------------------------------

@@ -17,6 +23,51 @@ // Rule Definition

var errorMessage = 'Form controls using a label to identify them must be ' + 'programmatically associated with the control using htmlFor';
var enumValues = ['nesting', 'id'];
var schema = {
type: 'object',
properties: {
components: _schemas.arraySchema,
required: {
oneOf: [{ type: 'string', enum: enumValues }, (0, _schemas.generateObjSchema)({ some: (0, _schemas.enumArraySchema)(enumValues) }, ['some']), (0, _schemas.generateObjSchema)({ every: (0, _schemas.enumArraySchema)(enumValues) }, ['every'])]
},
allowChildren: { type: 'boolean' }
}
};
var schema = (0, _schemas.generateObjSchema)({ components: _schemas.arraySchema });
var validateNesting = function validateNesting(node) {
return node.parent.children.some(function (child) {
return child.type === 'JSXElement';
});
};
var validateId = function validateId(node) {
var htmlForAttr = (0, _jsxAstUtils.getProp)(node.attributes, 'htmlFor');
var htmlForValue = (0, _jsxAstUtils.getPropValue)(htmlForAttr);
return htmlForAttr !== false && !!htmlForValue;
};
var validate = function validate(node, required, allowChildren) {
if (allowChildren === true) {
return (0, _hasAccessibleChild2.default)(node.parent);
}
if (required === 'nesting') {
return validateNesting(node);
}
return validateId(node);
};
var isValid = function isValid(node, required, allowChildren) {
if (Array.isArray(required.some)) {
return required.some.some(function (rule) {
return validate(node, rule, allowChildren);
});
} else if (Array.isArray(required.every)) {
return required.every.every(function (rule) {
return validate(node, rule, allowChildren);
});
}
return validate(node, required, allowChildren);
};
module.exports = {

@@ -41,7 +92,6 @@ meta: {

var htmlForAttr = (0, _jsxAstUtils.getProp)(node.attributes, 'htmlFor');
var htmlForValue = (0, _jsxAstUtils.getPropValue)(htmlForAttr);
var isInvalid = htmlForAttr === false || !htmlForValue;
var required = options.required || { every: ['nesting', 'id'] };
var allowChildren = options.allowChildren || false;
if (isInvalid) {
if (!isValid(node, required, allowChildren)) {
context.report({

@@ -48,0 +98,0 @@ node: node,

@@ -23,2 +23,3 @@ 'use strict';

var enumeratedList = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
var minItems = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
return Object.assign({}, arraySchema, {

@@ -28,3 +29,4 @@ items: {

enum: enumeratedList
}
},
minItems: minItems
});

@@ -39,6 +41,8 @@ };

var properties = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var required = arguments[1];
return {
type: 'object',
properties: properties
properties: properties,
required: required
};
};
{
"name": "eslint-plugin-jsx-a11y",
"version": "5.1.0",
"version": "6.0.0",
"description": "Static AST checker for accessibility rules on JSX elements.",

@@ -21,31 +21,34 @@ "keywords": [

"build": "rimraf lib && babel src --out-dir lib --copy-files",
"prepublish": "npm run lint && npm run flow && npm run test && npm run build",
"coveralls": "cat ./reports/lcov.info | coveralls",
"create": "node ./scripts/create-rule",
"flow": "if [ ! -e ./.flowconfig ]; then echo \"Could not find .flowconfig\"; else flow; test $? -eq 0 -o $? -eq 2; fi",
"lint:fix": "npm run lint -- --fix",
"lint": "eslint --config .eslintrc src __tests__ __mocks__ scripts",
"lint:fix": "npm run lint -- --fix",
"prepublish": "npm run lint && npm run flow && npm run test && npm run build",
"pretest": "npm run lint:fix && npm run flow",
"test": "jest --coverage __tests__/**/*",
"create": "node ./scripts/create-rule"
"test:ci": "npm test -- --ci --runInBand",
"test": "jest --coverage __tests__/**/*"
},
"devDependencies": {
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-eslint": "^7.0.0",
"babel-jest": "^20.0.0",
"babel-plugin-transform-flow-strip-types": "^6.21.0",
"babel-plugin-transform-object-rest-spread": "^6.20.2",
"babel-polyfill": "^6.16.0",
"babel-preset-es2015": "^6.14.0",
"coveralls": "^2.11.8",
"eslint": "^3.12.2",
"eslint-config-airbnb-base": "^11.0.0",
"eslint-plugin-flowtype": "^2.32.1",
"eslint-plugin-import": "^2.2.0",
"babel-cli": "^6.24.1",
"babel-core": "^6.25.0",
"babel-eslint": "^7.2.3",
"babel-jest": "^20.0.3",
"babel-plugin-transform-es2015-template-literals": "^6.22.0",
"babel-plugin-transform-flow-strip-types": "^6.22.0",
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"coveralls": "^2.13.1",
"eslint": "^3.19.0",
"eslint-config-airbnb-base": "^11.2.0",
"eslint-plugin-flowtype": "^2.34.0",
"eslint-plugin-import": "^2.3.0",
"expect": "^1.20.2",
"flow-bin": "^0.45.0",
"jest": "^20.0.0",
"flow-bin": "^0.47.0",
"jest": "^20.0.4",
"jscodeshift": "^0.3.30",
"minimist": "^1.2.0",
"rimraf": "^2.5.2",
"object.assign": "^4.0.4",
"rimraf": "^2.6.1",
"to-ast": "^1.0.0"

@@ -58,3 +61,3 @@ },

"dependencies": {
"aria-query": "^0.5.0",
"aria-query": "^0.7.0",
"array-includes": "^3.0.3",

@@ -68,3 +71,3 @@ "ast-types-flow": "0.0.7",

"peerDependencies": {
"eslint": "^2.10.2 || ^3 || ^4"
"eslint": "^3 || ^4"
},

@@ -71,0 +74,0 @@ "jest": {

@@ -82,3 +82,3 @@ <p align="center">

You can also enable all the recommended or strict rules at once.
You can also enable all the recommended or strict rules at once.
Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:

@@ -99,2 +99,3 @@

- [anchor-has-content](docs/rules/anchor-has-content.md): Enforce all anchors to contain accessible content.
- [anchor-is-valid](docs/rules/anchor-is-valid.md): Enforce all anchors are valid, navigable elements.
- [aria-activedescendant-has-tabindex](docs/rules/aria-activedescendant-has-tabindex.md): Enforce elements with aria-activedescendant are tabbable.

@@ -107,3 +108,2 @@ - [aria-props](docs/rules/aria-props.md): Enforce all `aria-*` props are valid.

- [heading-has-content](docs/rules/heading-has-content.md): Enforce heading (`h1`, `h2`, etc) elements contain accessible content.
- [href-no-hash](docs/rules/href-no-hash.md): Enforce an anchor element's `href` prop value is not just `#`.
- [html-has-lang](docs/rules/html-has-lang.md): Enforce `<html>` element has `lang` prop.

@@ -139,2 +139,3 @@ - [iframe-has-title](docs/rules/iframe-has-title.md): Enforce iframe elements have a title attribute.

[anchor-has-content](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-has-content.md) | error | error
[anchor-is-valid](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md) | error | error
[aria-activedescendant-has-tabindex](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-activedescendant-has-tabindex.md) | error | error

@@ -147,3 +148,2 @@ [aria-props](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-props.md) | error | error

[heading-has-content](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/heading-has-content.md) | error | error
[href-no-hash](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/href-no-hash.md) | error | error
[html-has-lang](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/html-has-lang.md) | error | error

@@ -150,0 +150,0 @@ [iframe-has-title](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/iframe-has-title.md) | error | error

@@ -8,2 +8,3 @@ /* eslint-disable global-require */

'anchor-has-content': require('./rules/anchor-has-content'),
'anchor-is-valid': require('./rules/anchor-is-valid'),
'aria-activedescendant-has-tabindex': require('./rules/aria-activedescendant-has-tabindex'),

@@ -16,3 +17,2 @@ 'aria-props': require('./rules/aria-props'),

'heading-has-content': require('./rules/heading-has-content'),
'href-no-hash': require('./rules/href-no-hash'),
'html-has-lang': require('./rules/html-has-lang'),

@@ -52,2 +52,3 @@ 'iframe-has-title': require('./rules/iframe-has-title'),

'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',

@@ -60,7 +61,19 @@ 'jsx-a11y/aria-props': 'error',

'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/href-no-hash': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/iframe-has-title': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/interactive-supports-focus': 'error',
'jsx-a11y/interactive-supports-focus': [
'error',
{
tabbable: [
'button',
'checkbox',
'link',
'searchbox',
'spinbutton',
'switch',
'textbox',
],
},
],
'jsx-a11y/label-has-for': 'error',

@@ -156,2 +169,3 @@ 'jsx-a11y/media-has-caption': 'error',

'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',

@@ -164,7 +178,21 @@ 'jsx-a11y/aria-props': 'error',

'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/href-no-hash': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/iframe-has-title': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/interactive-supports-focus': 'error',
'jsx-a11y/interactive-supports-focus': [
'error',
{
tabbable: [
'button',
'checkbox',
'link',
'progressbar',
'searchbox',
'slider',
'spinbutton',
'switch',
'textbox',
],
},
],
'jsx-a11y/label-has-for': 'error',

@@ -171,0 +199,0 @@ 'jsx-a11y/media-has-caption': 'error',

/**
* @fileoverview Enforce that elements with onClick handlers must be focusable.
* @fileoverview Enforce that elements with onClick handlers must be tabbable.
* @author Ethan Cohen

@@ -7,7 +7,11 @@ * @flow

import { dom } from 'aria-query';
import {
dom,
roles,
} from 'aria-query';
import {
getProp,
elementType,
eventHandlersByType,
getLiteralPropValue,
hasAnyProp,

@@ -18,3 +22,6 @@ } from 'jsx-ast-utils';

import type { ESLintContext } from '../../flow/eslint';
import { generateObjSchema } from '../util/schemas';
import {
enumArraySchema,
generateObjSchema,
} from '../util/schemas';
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';

@@ -32,6 +39,11 @@ import isInteractiveElement from '../util/isInteractiveElement';

const errorMessage =
'Elements with interactive roles must be focusable.';
const schema = generateObjSchema();
const schema = generateObjSchema({
tabbable: enumArraySchema(
[...roles.keys()]
.filter(name => !roles.get(name).abstract)
.filter(name => roles.get(name).superClass.some(
klasses => includes(klasses, 'widget')),
),
),
});
const domElements = [...dom.keys()];

@@ -50,6 +62,13 @@

create: (context: ESLintContext) => ({
create: (context: ESLintContext & {
options: {
tabbable: Array<string>
}
}) => ({
JSXOpeningElement: (
node: JSXOpeningElement,
) => {
const tabbable = (
context.options && context.options[0] && context.options[0].tabbable
) || [];
const attributes = node.attributes;

@@ -85,6 +104,16 @@ const type = elementType(node);

) {
context.report({
node,
message: errorMessage,
});
const role = getLiteralPropValue(getProp(attributes, 'role'));
if (includes(tabbable, role)) {
// Always tabbable, tabIndex = 0
context.report({
node,
message: `Elements with the '${role}' interactive role must be tabbable.`,
});
} else {
// Focusable, tabIndex = -1 or 0
context.report({
node,
message: `Elements with the '${role}' interactive role must be focusable.`,
});
}
}

@@ -91,0 +120,0 @@ },

@@ -11,9 +11,52 @@ /**

import { getProp, getPropValue, elementType } from 'jsx-ast-utils';
import { generateObjSchema, arraySchema } from '../util/schemas';
import { generateObjSchema, arraySchema, enumArraySchema } from '../util/schemas';
import hasAccessibleChild from '../util/hasAccessibleChild';
const errorMessage = 'Form controls using a label to identify them must be ' +
'programmatically associated with the control using htmlFor';
const errorMessage = 'Form label must have associated control';
const schema = generateObjSchema({ components: arraySchema });
const enumValues = ['nesting', 'id'];
const schema = {
type: 'object',
properties: {
components: arraySchema,
required: {
oneOf: [
{ type: 'string', enum: enumValues },
generateObjSchema({ some: enumArraySchema(enumValues) }, ['some']),
generateObjSchema({ every: enumArraySchema(enumValues) }, ['every']),
],
},
allowChildren: { type: 'boolean' },
},
};
const validateNesting = node => node.parent.children.some(child => child.type === 'JSXElement');
const validateId = (node) => {
const htmlForAttr = getProp(node.attributes, 'htmlFor');
const htmlForValue = getPropValue(htmlForAttr);
return htmlForAttr !== false && !!htmlForValue;
};
const validate = (node, required, allowChildren) => {
if (allowChildren === true) {
return hasAccessibleChild(node.parent);
}
if (required === 'nesting') {
return validateNesting(node);
}
return validateId(node);
};
const isValid = (node, required, allowChildren) => {
if (Array.isArray(required.some)) {
return required.some.some(rule => validate(node, rule, allowChildren));
} else if (Array.isArray(required.every)) {
return required.every.every(rule => validate(node, rule, allowChildren));
}
return validate(node, required, allowChildren);
};
module.exports = {

@@ -37,7 +80,6 @@ meta: {

const htmlForAttr = getProp(node.attributes, 'htmlFor');
const htmlForValue = getPropValue(htmlForAttr);
const isInvalid = htmlForAttr === false || !htmlForValue;
const required = options.required || { every: ['nesting', 'id'] };
const allowChildren = options.allowChildren || false;
if (isInvalid) {
if (!isValid(node, required, allowChildren)) {
context.report({

@@ -44,0 +86,0 @@ node,

@@ -16,8 +16,10 @@ /**

*/
export const enumArraySchema = (enumeratedList = []) => Object.assign({}, arraySchema, {
items: {
type: 'string',
enum: enumeratedList,
},
});
export const enumArraySchema = (enumeratedList = [], minItems = 0) =>
Object.assign({}, arraySchema, {
items: {
type: 'string',
enum: enumeratedList,
},
minItems,
});

@@ -28,5 +30,6 @@ /**

*/
export const generateObjSchema = (properties = {}) => ({
export const generateObjSchema = (properties = {}, required) => ({
type: 'object',
properties,
required,
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc