From fed93ab9e1da9b54852c17ebe1dc721e52bbb27b Mon Sep 17 00:00:00 2001 From: Nguyen Tran <88808276+ngtr6788@users.noreply.github.com> Date: Mon, 20 Mar 2023 08:18:54 -0400 Subject: [PATCH] feat: add a11y `interactive-supports-focus` (#8392) #820 --- .../content/docs/06-accessibility-warnings.md | 11 + src/compiler/compile/compiler_warnings.ts | 4 + src/compiler/compile/nodes/Element.ts | 44 ++- src/compiler/compile/utils/a11y.ts | 18 ++ .../input.svelte | 32 ++ .../warnings.json | 278 ++++++++++++++++++ 6 files changed, 386 insertions(+), 1 deletion(-) 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/site/content/docs/06-accessibility-warnings.md b/site/content/docs/06-accessibility-warnings.md index bc793d80e7..0d025d797d 100644 --- a/site/content/docs/06-accessibility-warnings.md +++ b/site/content/docs/06-accessibility-warnings.md @@ -137,6 +137,17 @@ Enforce that attributes important for accessibility have a valid value. For exam --- +### `a11y-interactive-supports-focus` + +Enforce that elements with an interactive role and interactive handlers (mouse or key press) must be focusable or tabbable. + +```sv + +
{}} /> +``` + +--- + ### `a11y-label-has-associated-control` Enforce that a label tag has a text label and an associated control. diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index 710377374f..a851bc24c2 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-interactive-supports-focus', + 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 2fd91ac495..7fcb586b6a 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,33 @@ const a11y_labelable = new Set([ 'textarea' ]); +const a11y_interactive_handlers = new Set([ + // Keyboard events + 'keypress', + 'keydown', + 'keyup', + + // Click events + 'click', + 'contextmenu', + 'dblclick', + 'drag', + 'dragend', + 'dragenter', + 'dragexit', + 'dragleave', + 'dragover', + 'dragstart', + 'drop', + 'mousedown', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup' +]); + const a11y_nested_implicit_semantics = new Map([ ['header', 'banner'], ['footer', 'contentinfo'] @@ -603,6 +630,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 d0564b419e..4409f80262 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]) => { 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 0000000000..9caa9ccfaf --- /dev/null +++ b/test/validator/samples/a11y-interactive-supports-focus/input.svelte @@ -0,0 +1,32 @@ + +
{}} /> +
{}} /> +
{}} /> +
{}} /> +