From 3ecd3d84b33dd5766bd956b9c0716d0a0c7a1e6c Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Sun, 20 Oct 2019 09:44:03 +0800 Subject: [PATCH] a11y validation - click-events-have-key-events, redudant-alt --- .../compile/utils/validate-a11y/Element.ts | 130 +++++++++++++----- .../input.svelte | 8 ++ .../warnings.json | 17 +++ .../samples/a11y-redudant-alt/input.svelte | 13 ++ .../samples/a11y-redudant-alt/warnings.json | 77 +++++++++++ .../warnings.json | 18 ++- .../event-modifiers-redundant/warnings.json | 15 ++ 7 files changed, 240 insertions(+), 38 deletions(-) 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 create mode 100644 test/validator/samples/a11y-redudant-alt/input.svelte create mode 100644 test/validator/samples/a11y-redudant-alt/warnings.json diff --git a/src/compiler/compile/utils/validate-a11y/Element.ts b/src/compiler/compile/utils/validate-a11y/Element.ts index c58f5d4b41..d7bb5a93d9 100644 --- a/src/compiler/compile/utils/validate-a11y/Element.ts +++ b/src/compiler/compile/utils/validate-a11y/Element.ts @@ -1,16 +1,24 @@ import Element from '../../nodes/Element'; - -const validators = [ - no_distracting_elements, - structure, - no_missing_attribute, - required_content, -]; +import Attribute from '../../nodes/Attribute'; +import EventHandler from '../../nodes/EventHandler'; export default function validateA11y(element: Element) { - for (const validator of validators) { - validator(element); - } + const attribute_map = new Map(); + const handler_map = new Map(); + + element.attributes.forEach(attribute => { + attribute_map.set(attribute.name, attribute); + }); + element.handlers.forEach(handler => { + handler_map.set(handler.name, handler); + }); + + no_distracting_elements(element); + structure(element); + no_missing_attribute(element, attribute_map); + required_content(element); + no_missing_handlers(element, handler_map); + img_redundant_alt(element, attribute_map); } const a11y_distracting_elements = new Set(['blink', 'marquee']); @@ -78,15 +86,15 @@ const a11y_required_attributes = { iframe: ['title'], img: ['alt'], object: ['title', 'aria-label', 'aria-labelledby'], -}; - -function no_missing_attribute(element: Element) { - const attribute_map = new Map(); - element.attributes.forEach(attribute => { - attribute_map.set(attribute.name, attribute); - }); + // input + ['input type="image"']: ['alt', 'aria-label', 'aria-labelledby'], +}; +function no_missing_attribute( + element: Element, + attribute_map: Map +) { if (element.name === 'a') { const attribute = attribute_map.get('href') || attribute_map.get('xlink:href'); @@ -107,32 +115,22 @@ function no_missing_attribute(element: Element) { }); } } else { - const required_attributes = a11y_required_attributes[element.name]; + let name = element.name; + if (element.name === 'input') { + const type = attribute_map.get('type'); + if (type && type.get_static_value() === 'image') { + name = 'input type="image"'; + } + } + + const required_attributes = a11y_required_attributes[name]; if (required_attributes) { const has_attribute = required_attributes.some(name => attribute_map.has(name) ); if (!has_attribute) { - should_have_attribute(element, required_attributes); - } - } - - if (element.name === 'input') { - const type = attribute_map.get('type'); - if (type && type.get_static_value() === 'image') { - const required_attributes = ['alt', 'aria-label', 'aria-labelledby']; - const has_attribute = required_attributes.some(name => - attribute_map.has(name) - ); - - if (!has_attribute) { - should_have_attribute( - element, - required_attributes, - 'input type="image"' - ); - } + should_have_attribute(element, required_attributes, name); } } } @@ -174,3 +172,61 @@ function required_content(element: Element) { }); } } + +function no_missing_handlers( + element: Element, + handler_map: Map +) { + if ( + handler_map.has('click') && + !( + handler_map.has('keypress') || + handler_map.has('keydown') || + handler_map.has('keyup') + ) + ) { + element.component.warn(element, { + code: `a11y-click-events-have-key-events`, + message: `A11y: Visible, non-interactive elements with click handlers must have at least one keyboard listener.`, + }); + } +} + +const a11y_redundant_alt = /image|photo|picture/i; +function img_redundant_alt( + element: Element, + attribute_map: Map +) { + if (element.name === 'img') { + const alt_attribute = attribute_map.get('alt'); + if (alt_attribute && !attribute_map.has('aria-hidden')) { + for (const chunk of alt_attribute.chunks) { + if (contain_text(chunk, a11y_redundant_alt)) { + element.component.warn(alt_attribute, { + code: `a11y-img-redundant-alt`, + message: + 'A11y: Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt prop.', + }); + break; + } + } + } + } +} + +function contain_text(node, regex: RegExp) { + switch (node.type) { + case 'Text': + return regex.test(node.data); + case 'Literal': + return regex.test(node.value); + case 'Expression': + return contain_text(node.node, regex); + case 'TemplateLiteral': + return node.quasis.some(quasi => contain_text(quasi, regex)); + case 'TemplateElement': + return regex.test(node.value.cooked); + default: + return false; + } +} 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 0000000000..6855b6d94e --- /dev/null +++ b/test/validator/samples/a11y-click-events-have-key-events/input.svelte @@ -0,0 +1,8 @@ + + +
{}} /> +
{}} on:keydown={handleKey} /> +
{}} on:keyup={handleKey} /> +
{}} on:keypress={handleKey} /> 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 0000000000..64dc24b971 --- /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", + "end": { + "character": 72, + "column": 27, + "line": 5 + }, + "message": "A11y: Visible, non-interactive elements with click handlers must have at least one keyboard listener.", + "pos": 45, + "start": { + "character": 45, + "column": 0, + "line": 5 + } + } +] diff --git a/test/validator/samples/a11y-redudant-alt/input.svelte b/test/validator/samples/a11y-redudant-alt/input.svelte new file mode 100644 index 0000000000..acde14b020 --- /dev/null +++ b/test/validator/samples/a11y-redudant-alt/input.svelte @@ -0,0 +1,13 @@ + + +Foo eating a sandwich. +Picture of me taking a photo of an image +{`Baz + +Photo of foo being weird. +Image of me at a bar! +Picture of baz fixing a bug. +{'Baz +{`Baz diff --git a/test/validator/samples/a11y-redudant-alt/warnings.json b/test/validator/samples/a11y-redudant-alt/warnings.json new file mode 100644 index 0000000000..1beb251f97 --- /dev/null +++ b/test/validator/samples/a11y-redudant-alt/warnings.json @@ -0,0 +1,77 @@ +[ + { + "code": "a11y-img-redundant-alt", + "end": { + "character": 286, + "column": 46, + "line": 9 + }, + "message": "A11y: Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt prop.", + "pos": 255, + "start": { + "character": 255, + "column": 15, + "line": 9 + } + }, + { + "code": "a11y-img-redundant-alt", + "end": { + "character": 332, + "column": 42, + "line": 10 + }, + "message": "A11y: Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt prop.", + "pos": 305, + "start": { + "character": 305, + "column": 15, + "line": 10 + } + }, + { + "code": "a11y-img-redundant-alt", + "end": { + "character": 385, + "column": 49, + "line": 11 + }, + "message": "A11y: Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt prop.", + "pos": 351, + "start": { + "character": 351, + "column": 15, + "line": 11 + } + }, + { + "code": "a11y-img-redundant-alt", + "end": { + "character": 430, + "column": 41, + "line": 12 + }, + "message": "A11y: Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt prop.", + "pos": 404, + "start": { + "character": 404, + "column": 15, + "line": 12 + } + }, + { + "code": "a11y-img-redundant-alt", + "end": { + "character": 484, + "column": 50, + "line": 13 + }, + "message": "A11y: Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt prop.", + "pos": 449, + "start": { + "character": 449, + "column": 15, + "line": 13 + } + } +] diff --git a/test/validator/samples/catch-declares-error-variable/warnings.json b/test/validator/samples/catch-declares-error-variable/warnings.json index 0637a088a0..be80e759f7 100644 --- a/test/validator/samples/catch-declares-error-variable/warnings.json +++ b/test/validator/samples/catch-declares-error-variable/warnings.json @@ -1 +1,17 @@ -[] \ No newline at end of file +[ + { + "code": "a11y-click-events-have-key-events", + "end": { + "character": 173, + "column": 33, + "line": 11 + }, + "message": "A11y: Visible, non-interactive elements with click handlers must have at least one keyboard listener.", + "pos": 39, + "start": { + "character": 39, + "column": 0, + "line": 5 + } + } +] diff --git a/test/validator/samples/event-modifiers-redundant/warnings.json b/test/validator/samples/event-modifiers-redundant/warnings.json index e3b9110427..69e1faee22 100644 --- a/test/validator/samples/event-modifiers-redundant/warnings.json +++ b/test/validator/samples/event-modifiers-redundant/warnings.json @@ -1,4 +1,19 @@ [ + { + "code": "a11y-click-events-have-key-events", + "end": { + "character": 152, + "column": 50, + "line": 11 + }, + "message": "A11y: Visible, non-interactive elements with click handlers must have at least one keyboard listener.", + "pos": 102, + "start": { + "character": 102, + "column": 0, + "line": 11 + } + }, { "message": "The passive modifier only works with wheel and touch events", "code": "redundant-event-modifier",