diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index b3e2e36797..7b1a3b3d29 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -179,6 +179,10 @@ export default { code: 'a11y-missing-content', message: `A11y: <${name}> element should have child content` }), + a11y_no_nointeractive_tabindex: { + code: 'a11y-no-nointeractive-tabindex', + message: 'A11y: not interactive element cannot have positive tabIndex value' + }, redundant_event_modifier_for_touch: { code: 'redundant-event-modifier', message: 'Touch event handlers that don\'t use the \'event\' object are passive by default' diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index df0e649122..b5f9caa6b8 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -24,7 +24,7 @@ import { Literal } from 'estree'; import compiler_warnings from '../compiler_warnings'; import compiler_errors from '../compiler_errors'; import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; -import { is_interactive_element, is_non_interactive_roles, is_presentation_role } from '../utils/a11y'; +import { is_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles } from '../utils/a11y'; const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const aria_attribute_set = new Set(aria_attributes); @@ -549,8 +549,15 @@ export default class Element extends Node { } } }); - } + // no-nointeractive-tabindex + if (!is_interactive_element(this.name, attribute_map) && !is_interactive_roles(attribute_map.get('role')?.get_static_value() as ARIARoleDefintionKey)) { + const tab_index = attribute_map.get('tabindex'); + if (tab_index && (!tab_index.is_static || Number(tab_index.get_static_value()) >= 0)) { + component.warn(this, compiler_warnings.a11y_no_nointeractive_tabindex); + } + } + } validate_special_cases() { const { component, attributes, handlers } = this; diff --git a/src/compiler/compile/utils/a11y.ts b/src/compiler/compile/utils/a11y.ts index 1e06608b54..5b300eac13 100644 --- a/src/compiler/compile/utils/a11y.ts +++ b/src/compiler/compile/utils/a11y.ts @@ -51,6 +51,10 @@ export function is_non_interactive_roles(role: ARIARoleDefintionKey) { return non_interactive_roles.has(role); } +export function is_interactive_roles(role: ARIARoleDefintionKey) { + return interactive_roles.has(role); +} + const presentation_roles = new Set(['presentation', 'none']); export function is_presentation_role(role: ARIARoleDefintionKey) { diff --git a/test/validator/samples/a11y-no-nointeractive-tabindex/input.svelte b/test/validator/samples/a11y-no-nointeractive-tabindex/input.svelte new file mode 100644 index 0000000000..e9ac0d3c9f --- /dev/null +++ b/test/validator/samples/a11y-no-nointeractive-tabindex/input.svelte @@ -0,0 +1,14 @@ + +