From d13725de3faf771eded3adaf123cf7315bffe725 Mon Sep 17 00:00:00 2001
From: LB
Date: Wed, 23 Oct 2024 14:48:47 +1000
Subject: [PATCH] [Fix] `no-redundant-roles`: Refine implicit role of `select`
to include `combobox` scenarios
Encode implicit roles for `select` elements based on roles defined in https://www.w3.org/TR/html-aria/#el-select
- `select` (with a multiple attribute or a size attribute having value greater than 1) will have the implicit role 'listbox'
- `select` (with NO multiple attribute and NO size attribute having value greater than 1) will have the implicit role 'combobox'
Fixes #949
---
.../src/rules/no-redundant-roles-test.js | 21 ++-
.../src/util/implicitRoles/select-test.js | 158 ++++++++++++++++++
src/util/implicitRoles/select.js | 18 +-
3 files changed, 193 insertions(+), 4 deletions(-)
create mode 100644 __tests__/src/util/implicitRoles/select-test.js
diff --git a/__tests__/src/rules/no-redundant-roles-test.js b/__tests__/src/rules/no-redundant-roles-test.js
index 068e54049..288d0903a 100644
--- a/__tests__/src/rules/no-redundant-roles-test.js
+++ b/__tests__/src/rules/no-redundant-roles-test.js
@@ -41,12 +41,31 @@ const alwaysValid = [
{ code: '' },
{ code: '' },
{ code: '', settings: componentsSettings },
+ { code: '' },
+ { code: '' },
+ { code: '' },
];
const neverValid = [
- { code: '', errors: [expectedError('button', 'button')] },
{ code: '', errors: [expectedError('body', 'document')] },
+ // button - treated as button by default
+ { code: '', errors: [expectedError('button', 'button')] },
{ code: '', settings: componentsSettings, errors: [expectedError('button', 'button')] },
+ // select - treated as combobox by default
+ { code: '', errors: [expectedError('select', 'combobox')] },
+ { code: '', errors: [expectedError('select', 'combobox')] },
+ { code: '', errors: [expectedError('select', 'combobox')] },
+ { code: '', errors: [expectedError('select', 'combobox')] },
+ { code: '', errors: [expectedError('select', 'combobox')] },
+ { code: '', errors: [expectedError('select', 'combobox')] },
+ { code: '', errors: [expectedError('select', 'combobox')] },
+ { code: '', errors: [expectedError('select', 'combobox')] },
+ { code: '', errors: [expectedError('select', 'combobox')] },
+ // select - treated as listbox when multiple OR size > 1
+ { code: '', errors: [expectedError('select', 'listbox')] },
+ { code: '', errors: [expectedError('select', 'listbox')] },
+ { code: '', errors: [expectedError('select', 'listbox')] },
+ { code: '', errors: [expectedError('select', 'listbox')] },
];
ruleTester.run(`${ruleName}:recommended`, rule, {
diff --git a/__tests__/src/util/implicitRoles/select-test.js b/__tests__/src/util/implicitRoles/select-test.js
new file mode 100644
index 000000000..8ad755313
--- /dev/null
+++ b/__tests__/src/util/implicitRoles/select-test.js
@@ -0,0 +1,158 @@
+import test from 'tape';
+
+import JSXAttributeMock from '../../../../__mocks__/JSXAttributeMock';
+import getImplicitRoleForSelect from '../../../../src/util/implicitRoles/select';
+
+test('isAbstractRole', (t) => {
+ t.test('works for combobox', (st) => {
+ st.equal(
+ getImplicitRoleForSelect([]),
+ 'combobox',
+ 'defaults to combobox',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('multiple', null)]),
+ 'combobox',
+ 'is combobox when multiple attribute is set to not be present',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('multiple', undefined)]),
+ 'combobox',
+ 'is combobox when multiple attribute is set to not be present',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('multiple', false)]),
+ 'combobox',
+ 'is combobox when multiple attribute is set to boolean false',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('multiple', '')]),
+ 'combobox',
+ 'is listbox when multiple attribute is falsey (empty string)',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', '1')]),
+ 'combobox',
+ 'is combobox when size is not greater than 1',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', 1)]),
+ 'combobox',
+ 'is combobox when size is not greater than 1',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', 0)]),
+ 'combobox',
+ 'is combobox when size is not greater than 1',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', '0')]),
+ 'combobox',
+ 'is combobox when size is not greater than 1',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', '-1')]),
+ 'combobox',
+ 'is combobox when size is not greater than 1',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', '')]),
+ 'combobox',
+ 'is combobox when size is a valid number',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', 'true')]),
+ 'combobox',
+ 'is combobox when size is a valid number',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', true)]),
+ 'combobox',
+ 'is combobox when size is a valid number',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', NaN)]),
+ 'combobox',
+ 'is combobox when size is a valid number',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', '')]),
+ 'combobox',
+ 'is combobox when size is a valid number',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', undefined)]),
+ 'combobox',
+ 'is combobox when size is a valid number',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', false)]),
+ 'combobox',
+ 'is combobox when size is a valid number',
+ );
+
+ st.end();
+ });
+
+ t.test('works for listbox based on multiple attribute', (st) => {
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('multiple', true)]),
+ 'listbox',
+ 'is listbox when multiple is boolean true',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('multiple', 'multiple')]),
+ 'listbox',
+ 'is listbox when multiple is truthy (string)',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('multiple', 'true')]),
+ 'listbox',
+ 'is listbox when multiple is truthy (string) - React will warn about this',
+ );
+
+ st.end();
+ });
+
+ t.test('works for listbox based on size attribute', (st) => {
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', 2)]),
+ 'listbox',
+ 'is listbox when size is greater than 1',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', '3')]),
+ 'listbox',
+ 'is listbox when size is greater than 1',
+ );
+
+ st.equal(
+ getImplicitRoleForSelect([JSXAttributeMock('size', 40)]),
+ 'listbox',
+ 'is listbox when size is greater than 1',
+ );
+
+ st.end();
+ });
+
+ t.end();
+});
diff --git a/src/util/implicitRoles/select.js b/src/util/implicitRoles/select.js
index 9f23296c0..0cbe957bc 100644
--- a/src/util/implicitRoles/select.js
+++ b/src/util/implicitRoles/select.js
@@ -1,6 +1,18 @@
+import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
+
/**
- * Returns the implicit role for a select tag.
+ * Returns the implicit role for a select tag depending on attributes.
+ *
+ * @see https://www.w3.org/TR/html-aria/#el-select
*/
-export default function getImplicitRoleForSelect() {
- return 'listbox';
+export default function getImplicitRoleForSelect(attributes) {
+ const multiple = getProp(attributes, 'multiple');
+ if (multiple && getLiteralPropValue(multiple)) {
+ return 'listbox';
+ }
+
+ const size = getProp(attributes, 'size');
+ const sizeValue = size && getLiteralPropValue(size);
+
+ return sizeValue > 1 ? 'listbox' : 'combobox';
}