From 31fc15e127bf0e2bb89541c45f7745f95795fe84 Mon Sep 17 00:00:00 2001 From: mhatvan Date: Fri, 26 Jun 2020 11:20:55 +0200 Subject: [PATCH 1/7] feat(a11y): add click-events-have-key-events rule Signed-off-by: mhatvan --- src/compiler/compile/nodes/Element.ts | 22 ++++++++++++++++++- .../input.svelte | 15 +++++++++++++ .../warnings.json | 17 ++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 test/validator/samples/a11y-click-events-have-key-events/input.svelte create mode 100644 test/validator/samples/a11y-click-events-have-key-events/warnings.json diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 48ea0ec3e1a9..9d5328ecb51a 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -434,12 +434,17 @@ export default class Element extends Node { } validate_attributes_a11y() { - const { component, attributes } = this; + const { component, attributes, handlers } = this; const attribute_map = new Map(); + const handlers_map = new Map(); + attributes.forEach(attribute => ( attribute_map.set(attribute.name, attribute) )); + handlers.forEach(handler => ( + handlers_map.set(handler.name, handler) + )); attributes.forEach(attribute => { if (attribute.is_spread) return; @@ -550,6 +555,21 @@ export default class Element extends Node { } }); + // click-events-have-key-events + if (handlers_map.has("click")) { + const hasKeyEvent = + handlers_map.has("keydown") || + handlers_map.has("keyup") || + handlers_map.has("keypress"); + + if (!hasKeyEvent) { + component.warn(this, { + code: `a11y-click-events-have-key-events`, + message: `A11y: on:click event must be accompanied by on:keydown, on:keyup or on:keypress event` + }); + } + } + // no-noninteractive-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'); diff --git a/test/validator/samples/a11y-click-events-have-key-events/input.svelte b/test/validator/samples/a11y-click-events-have-key-events/input.svelte new file mode 100644 index 000000000000..4517d6709a22 --- /dev/null +++ b/test/validator/samples/a11y-click-events-have-key-events/input.svelte @@ -0,0 +1,15 @@ + + + +
+ + +
+
+
+
+
+
+
diff --git a/test/validator/samples/a11y-click-events-have-key-events/warnings.json b/test/validator/samples/a11y-click-events-have-key-events/warnings.json new file mode 100644 index 000000000000..a08b52c114fe --- /dev/null +++ b/test/validator/samples/a11y-click-events-have-key-events/warnings.json @@ -0,0 +1,17 @@ +[ + { + "code": "a11y-click-events-have-key-events", + "message": "A11y: on:click event must be accompanied by on:keydown, on:keyup or on:keypress event", + "end": { + "character": 85, + "column": 23, + "line": 6 + }, + "start": { + "character": 62, + "column": 0, + "line": 6 + }, + "pos": 62 + } +] From 8a80cc66e7101831d3d97430495663b5c21393b2 Mon Sep 17 00:00:00 2001 From: mhatvan Date: Wed, 2 Sep 2020 14:04:54 +0200 Subject: [PATCH 2/7] Fine-tune click-events-have-key-events rule Signed-off-by: mhatvan --- src/compiler/compile/nodes/Element.ts | 73 ++++++++-- .../input.svelte | 34 ++++- .../warnings.json | 132 +++++++++++++++++- 3 files changed, 219 insertions(+), 20 deletions(-) diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 9d5328ecb51a..abe48e2f6deb 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -556,18 +556,67 @@ export default class Element extends Node { }); // click-events-have-key-events - if (handlers_map.has("click")) { - const hasKeyEvent = - handlers_map.has("keydown") || - handlers_map.has("keyup") || - handlers_map.has("keypress"); - - if (!hasKeyEvent) { - component.warn(this, { - code: `a11y-click-events-have-key-events`, - message: `A11y: on:click event must be accompanied by on:keydown, on:keyup or on:keypress event` - }); - } + if (handlers_map.has('click')) { + + const a11y_interactive = new Set([ + 'a', + 'button', + 'select' + ]); + + if (a11y_interactive.has(this.name)) { + return; + } + + if (this.name === 'input') { + const input_type = attribute_map.get('type'); + const input_type_interactive = new Set(['button', 'checkbox', 'color', 'file', 'image', 'radio', 'reset', 'submit']); + + if (input_type && input_type_interactive.has(input_type.get_static_value())) { + return; + } + } + + if (this.attributes.find((attr) => attr.is_spread)) { + return; + } + + const aria_hidden_attribute = attribute_map.get('aria-hidden'); + const aria_hidden_value = + aria_hidden_attribute && aria_hidden_attribute.get_static_value(); + + // aria-hidden value is string, check its boolean value with JSON.parse() + if (aria_hidden_value && JSON.parse(aria_hidden_value)) { + return; + } + + const type_attribute = attribute_map.get('type'); + const type_value = type_attribute && type_attribute.get_static_value(); + + if (type_value && type_value === 'hidden') { + return; + } + + const role_attribute = attribute_map.get('role'); + const role_value = role_attribute && role_attribute.get_static_value(); + const presentation_role_value = + role_value === 'presentation' || role_value === 'none'; + + if (presentation_role_value) { + return; + } + + const hasKeyEvent = + handlers_map.has('keydown') || + handlers_map.has('keyup') || + handlers_map.has('keypress'); + + if (!hasKeyEvent) { + component.warn(this, { + code: `a11y-click-events-have-key-events`, + message: `A11y: visible, non-interactive elements with on:click event must be accompanied by a on:keydown, on:keyup or on:keypress event.` + }); + } } // no-noninteractive-tabindex diff --git a/test/validator/samples/a11y-click-events-have-key-events/input.svelte b/test/validator/samples/a11y-click-events-have-key-events/input.svelte index 4517d6709a22..b54392acecfa 100644 --- a/test/validator/samples/a11y-click-events-have-key-events/input.svelte +++ b/test/validator/samples/a11y-click-events-have-key-events/input.svelte @@ -1,15 +1,45 @@
+
+
+ +
+
+
+
+