diff --git a/lib/rules/template-no-redundant-role.js b/lib/rules/template-no-redundant-role.js index abaf75cf2d..a3248869bd 100644 --- a/lib/rules/template-no-redundant-role.js +++ b/lib/rules/template-no-redundant-role.js @@ -37,6 +37,25 @@ const ALLOWED_ELEMENT_ROLES = [ { name: 'input', role: 'combobox' }, ]; +// Per HTML-AAM, is a combobox by default per HTML-AAM (section 4). When + // `multiple` is present or `size > 1`, it maps to "listbox" instead; + // that case is handled at the call site via selectHasComboboxImplicitRole. + combobox: ['select'], columnheader: ['th'], complementary: ['aside'], contentinfo: ['footer'], @@ -125,7 +148,8 @@ module.exports = { let roleValue; if (roleAttr.value && roleAttr.value.type === 'GlimmerTextNode') { - roleValue = roleAttr.value.chars || ''; + // ARIA role tokens are compared ASCII-case-insensitively. + roleValue = (roleAttr.value.chars || '').toLowerCase(); } else { // Skip dynamic role values return; @@ -141,6 +165,18 @@ module.exports = { return; } + // 's implicit + // role actually is "combobox" (no `multiple`, and `size` absent or <= 1). + // Otherwise the implicit role is "listbox", so the explicit "combobox" + // is not redundant and this rule should not flag it. + if ( + node.tag === 'select' && + roleValue === 'combobox' && + !selectHasComboboxImplicitRole(node) + ) { + return; + } + const isRedundant = elementsWithRole.includes(node.tag) && !ALLOWED_ELEMENT_ROLES.some((e) => e.name === node.tag && e.role === roleValue); diff --git a/tests/audit/no-redundant-roles/peer-parity.js b/tests/audit/no-redundant-roles/peer-parity.js new file mode 100644 index 0000000000..a9dbad0102 --- /dev/null +++ b/tests/audit/no-redundant-roles/peer-parity.js @@ -0,0 +1,210 @@ +// Audit fixture — peer-plugin parity for `ember/template-no-redundant-role`. +// +// Source files (context/ checkouts): +// - eslint-plugin-jsx-a11y-main/src/rules/no-redundant-roles.js +// - eslint-plugin-jsx-a11y-main/__tests__/src/rules/no-redundant-roles-test.js +// - eslint-plugin-vuejs-accessibility-main/src/rules/no-redundant-roles.ts +// - eslint-plugin-lit-a11y/lib/rules/no-redundant-role.js +// +// These tests are NOT part of the main suite and do not run in CI. They encode +// the CURRENT behavior of our rule. Each divergence from an upstream plugin is +// annotated as "DIVERGENCE —". + +'use strict'; + +const rule = require('../../../lib/rules/template-no-redundant-role'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +ruleTester.run('audit:no-redundant-roles (gts)', rule, { + valid: [ + // === Upstream parity (valid in all plugins + ours) === + // No role attribute. + '', + // Role differs from implicit. + '', + '', + // jsx-a11y/lit-a11y default exception: nav[role="navigation"] is allowed. + // Our ALLOWED_ELEMENT_ROLES also permits this. + '', + // form[role="search"] — different from implicit "form" role. + '', + // Dynamic role — we skip. + '', + + // === DIVERGENCE —