From 56ceeb3aa9e214f8ad2ca5cf81b5f3cd7072d905 Mon Sep 17 00:00:00 2001 From: Nguyen Tran Date: Fri, 17 Mar 2023 13:30:12 -0400 Subject: [PATCH 1/8] Implement interactive-supports-focus --- src/compiler/compile/compiler_warnings.ts | 4 ++++ src/compiler/compile/nodes/Element.ts | 26 ++++++++++++++++++++++- src/compiler/compile/utils/a11y.ts | 18 ++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index 710377374f0e..9c0f8957aaec 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -166,6 +166,10 @@ export default { code: 'a11y-img-redundant-alt', message: 'A11y: Screenreaders already announce elements as an image.' }, + a11y_interactive_supports_focus: (role: string) => ({ + code: 'a11y-no-noninteractive-element-interactions', + message: `A11y: Elements with the '${role}' interactive role must have a tabindex value.` + }), a11y_label_has_associated_control: { code: 'a11y-label-has-associated-control', message: 'A11y: A form label must be associated with a control.' diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 2fd91ac49599..6146781d2b04 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -25,7 +25,7 @@ import { Literal } from 'estree'; import compiler_warnings from '../compiler_warnings'; import compiler_errors from '../compiler_errors'; import { ARIARoleDefinitionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; -import { is_interactive_element, is_non_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element, is_abstract_role } from '../utils/a11y'; +import { is_interactive_element, is_non_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element, is_abstract_role, is_static_element, has_disabled_attribute } 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); @@ -75,6 +75,15 @@ const a11y_labelable = new Set([ 'textarea' ]); +const a11y_interactive_handlers = new Set([ + 'click', + 'mousedown', + 'mouseup', + 'keypress', + 'keydown', + 'keyup' +]); + const a11y_nested_implicit_semantics = new Map([ ['header', 'banner'], ['footer', 'contentinfo'] @@ -603,6 +612,21 @@ export default class Element extends Node { } } + // interactive-supports-focus + if ( + !has_disabled_attribute(attribute_map) && + !is_hidden_from_screen_reader(this.name, attribute_map) && + !is_presentation_role(current_role) && + is_interactive_roles(current_role) && + is_static_element(this.name, attribute_map) && + !attribute_map.get('tabindex') + ) { + const has_interactive_handlers = handlers.some((handler) => a11y_interactive_handlers.has(handler.name)); + if (has_interactive_handlers) { + component.warn(this, compiler_warnings.a11y_interactive_supports_focus(current_role)); + } + } + // no-interactive-element-to-noninteractive-role if (is_interactive_element(this.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role))) { component.warn(this, compiler_warnings.a11y_no_interactive_element_to_noninteractive_role(current_role, this.name)); diff --git a/src/compiler/compile/utils/a11y.ts b/src/compiler/compile/utils/a11y.ts index d0564b419e02..4409f802623d 100644 --- a/src/compiler/compile/utils/a11y.ts +++ b/src/compiler/compile/utils/a11y.ts @@ -68,6 +68,24 @@ export function is_hidden_from_screen_reader(tag_name: string, attribute_map: Ma return aria_hidden_value === true || aria_hidden_value === 'true'; } +export function has_disabled_attribute(attribute_map: Map) { + const disabled_attr = attribute_map.get('disabled'); + const disabled_attr_value = disabled_attr && disabled_attr.get_static_value(); + if (disabled_attr_value) { + return true; + } + + const aria_disabled_attr = attribute_map.get('aria-disabled'); + if (aria_disabled_attr) { + const aria_disabled_attr_value = aria_disabled_attr.get_static_value(); + if (aria_disabled_attr_value === true) { + return true; + } + } + + return false; +} + const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = []; elementRoles.entries().forEach(([schema, roles]) => { From 9ee2272436df1133ab4d2bec08da5a780abea990 Mon Sep 17 00:00:00 2001 From: Nguyen Tran Date: Fri, 17 Mar 2023 13:39:55 -0400 Subject: [PATCH 2/8] Add some elementary tests for the rule --- .../a11y-interactive-supports-focus/input.svelte | 11 +++++++++++ .../a11y-interactive-supports-focus/warnings.json | 1 + 2 files changed, 12 insertions(+) create mode 100644 test/validator/samples/a11y-interactive-supports-focus/input.svelte create mode 100644 test/validator/samples/a11y-interactive-supports-focus/warnings.json diff --git a/test/validator/samples/a11y-interactive-supports-focus/input.svelte b/test/validator/samples/a11y-interactive-supports-focus/input.svelte new file mode 100644 index 000000000000..3f8be03722b0 --- /dev/null +++ b/test/validator/samples/a11y-interactive-supports-focus/input.svelte @@ -0,0 +1,11 @@ + +
{}} /> +
{}} /> +
{}} /> +