diff --git a/__tests__/src/rules/no-redundant-roles-test.js b/__tests__/src/rules/no-redundant-roles-test.js index bdcb57bb..76661925 100644 --- a/__tests__/src/rules/no-redundant-roles-test.js +++ b/__tests__/src/rules/no-redundant-roles-test.js @@ -42,6 +42,10 @@ const alwaysValid = [ { code: '' }, { code: '', settings: componentsSettings }, { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, ]; const neverValid = [ @@ -54,6 +58,10 @@ const neverValid = [ { code: '', errors: [expectedError('select', 'listbox')] }, { code: '', errors: [expectedError('select', 'listbox')] }, { code: '', errors: [expectedError('select', 'listbox')] }, + { code: '', errors: [expectedError('input', 'spinbutton')] }, + { code: '', errors: [expectedError('input', 'searchbox')] }, + { code: '', errors: [expectedError('input', 'combobox')] }, + { code: '', errors: [expectedError('input', 'combobox')] }, ]; ruleTester.run(`${ruleName}:recommended`, rule, { diff --git a/__tests__/src/rules/role-supports-aria-props-test.js b/__tests__/src/rules/role-supports-aria-props-test.js index 339b9405..971350a1 100644 --- a/__tests__/src/rules/role-supports-aria-props-test.js +++ b/__tests__/src/rules/role-supports-aria-props-test.js @@ -367,15 +367,24 @@ ruleTester.run('role-supports-aria-props', rule, { { code: '' }, { code: '' }, { code: '' }, + // when `type="number"`, the implicit role is `spinbutton` + { code: '' }, + { code: '' }, // these will have role of `textbox`, { code: '' }, { code: '' }, - { code: '' }, { code: '' }, { code: '' }, { code: '' }, + // when `type="search"`, the implicit role is `searchbox` + { code: '' }, + + // when list attribute is present, the implicit role is `combobox` + { code: '' }, + { code: '' }, + // Allow null/undefined values regardless of role { code: '
' }, { code: '' }, @@ -534,6 +543,14 @@ ruleTester.run('role-supports-aria-props', rule, { code: '', errors: [errorMessage('aria-invalid', 'button', 'input', true)], }, + { + code: '', + errors: [errorMessage('aria-autocomplete', 'spinbutton', 'input', true)], + }, + { + code: '', + errors: [errorMessage('aria-expanded', 'searchbox', 'input', true)], + }, { code: '', errors: [errorMessage('aria-invalid', 'menuitem', 'menuitem', true)], diff --git a/__tests__/src/util/implicitRoles/input-test.js b/__tests__/src/util/implicitRoles/input-test.js index a0c4277f..12a8cd7e 100644 --- a/__tests__/src/util/implicitRoles/input-test.js +++ b/__tests__/src/util/implicitRoles/input-test.js @@ -4,6 +4,55 @@ import JSXAttributeMock from '../../../../__mocks__/JSXAttributeMock'; import getImplicitRoleForInput from '../../../../src/util/implicitRoles/input'; test('isAbstractRole', (t) => { + t.test('works for inputs with no corresponding role', (st) => { + st.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'color')]), + '', + ); + + st.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'date')]), + '', + ); + + st.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'datetime-local')]), + '', + ); + + st.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'file')]), + '', + ); + + st.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'hidden')]), + '', + ); + + st.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'month')]), + '', + ); + + st.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'password')]), + '', + ); + + st.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'time')]), + '', + ); + + st.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'week')]), + '', + ); + + st.end(); + }); + t.test('works for buttons', (st) => { st.equal( getImplicitRoleForInput([JSXAttributeMock('type', 'button')]), @@ -46,17 +95,25 @@ test('isAbstractRole', (t) => { 'works for ranges', ); + t.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'number')]), + 'spinbutton', + 'works for number inputs', + ); + + t.equal( + getImplicitRoleForInput([JSXAttributeMock('type', 'search')]), + 'searchbox', + 'works for search inputs', + ); + t.test('works for textboxes', (st) => { st.equal( getImplicitRoleForInput([JSXAttributeMock('type', 'email')]), 'textbox', ); st.equal( - getImplicitRoleForInput([JSXAttributeMock('type', 'password')]), - 'textbox', - ); - st.equal( - getImplicitRoleForInput([JSXAttributeMock('type', 'search')]), + getImplicitRoleForInput([JSXAttributeMock('type', 'text')]), 'textbox', ); st.equal( @@ -71,6 +128,57 @@ test('isAbstractRole', (t) => { st.end(); }); + t.test('works for inputs with list attribute', (st) => { + st.equal( + getImplicitRoleForInput([ + JSXAttributeMock('type', 'search'), + JSXAttributeMock('list', 'example'), + ]), + 'combobox', + ); + + st.equal( + getImplicitRoleForInput([ + JSXAttributeMock('type', 'email'), + JSXAttributeMock('list', 'example'), + ]), + 'combobox', + ); + + st.equal( + getImplicitRoleForInput([ + JSXAttributeMock('type', 'tel'), + JSXAttributeMock('list', 'example'), + ]), + 'combobox', + ); + + st.equal( + getImplicitRoleForInput([ + JSXAttributeMock('type', 'url'), + JSXAttributeMock('list', 'example'), + ]), + 'combobox', + ); + + st.equal( + getImplicitRoleForInput([ + JSXAttributeMock('type', 'invalid'), + JSXAttributeMock('list', 'example'), + ]), + 'combobox', + ); + + st.equal( + getImplicitRoleForInput([ + JSXAttributeMock('list', 'example'), + ]), + 'combobox', + ); + + st.end(); + }); + t.equal( getImplicitRoleForInput([JSXAttributeMock('type', '')]), 'textbox', diff --git a/src/util/implicitRoles/input.js b/src/util/implicitRoles/input.js index bd2b452c..7865b4de 100644 --- a/src/util/implicitRoles/input.js +++ b/src/util/implicitRoles/input.js @@ -2,14 +2,30 @@ import { getProp, getLiteralPropValue } from 'jsx-ast-utils'; /** * Returns the implicit role for an input tag. + * + * @see https://www.w3.org/TR/html-aria/#el-input-text-list + * `input` with type = text, search, tel, url, email, or with a missing or invalid type + * with a list attribute will have an implicit role=combobox. */ export default function getImplicitRoleForInput(attributes) { const type = getProp(attributes, 'type'); + const hasListAttribute = !!getProp(attributes, 'list'); if (type) { const value = getLiteralPropValue(type) || ''; switch (typeof value === 'string' && value.toUpperCase()) { + case 'COLOR': + case 'DATE': + case 'DATETIME-LOCAL': + case 'FILE': + case 'HIDDEN': + case 'MONTH': + case 'PASSWORD': + case 'TIME': + case 'WEEK': + /** No corresponding role */ + return ''; case 'BUTTON': case 'IMAGE': case 'RESET': @@ -21,15 +37,18 @@ export default function getImplicitRoleForInput(attributes) { return 'radio'; case 'RANGE': return 'slider'; + case 'NUMBER': + return 'spinbutton'; + case 'SEARCH': + return hasListAttribute ? 'combobox' : 'searchbox'; case 'EMAIL': - case 'PASSWORD': - case 'SEARCH': // with [list] selector it's combobox - case 'TEL': // with [list] selector it's combobox - case 'URL': // with [list] selector it's combobox + case 'TEL': + case 'TEXT': + case 'URL': default: - return 'textbox'; + return hasListAttribute ? 'combobox' : 'textbox'; } } - return 'textbox'; + return hasListAttribute ? 'combobox' : 'textbox'; }