From 3c0bfb25e26cbc598e21ddf17dc199ba8276e336 Mon Sep 17 00:00:00 2001 From: Anthony Le Goas Date: Wed, 3 Feb 2021 13:55:01 +0100 Subject: [PATCH] check noninteractive roles on interactive elements --- src/compiler/compile/nodes/Element.ts | 15 + src/compiler/compile/utils/aria_roles.ts | 9 + src/compiler/compile/utils/elements.ts | 5 + .../input.svelte | 54 ++ .../warnings.json | 602 ++++++++++++++++++ 5 files changed, 685 insertions(+) create mode 100644 src/compiler/compile/utils/aria_roles.ts create mode 100644 src/compiler/compile/utils/elements.ts create mode 100644 test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/input.svelte create mode 100644 test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/warnings.json diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 07f17390182e..e7115dad0c02 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -17,6 +17,9 @@ import Let from './Let'; import TemplateScope from './shared/TemplateScope'; import { INode } from './interfaces'; import Component from '../Component'; +import { ARIARoleDefintionKey } from 'aria-query'; +import { noninteractive_roles } from '../utils/aria_roles'; +import { interactive_elements } from '../utils/elements'; const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/; @@ -596,6 +599,18 @@ export default class Element extends Node { }); } } + + if (interactive_elements.has(this.name)) { + if (attribute_map.has('role')) { + const roleValue = this.attributes.find(a => a.name === 'role').get_static_value().toString() as ARIARoleDefintionKey; + if (noninteractive_roles.has(roleValue)) { + component.warn(this, { + code: 'a11y-no-interactive-element-to-noninteractive-role', + message: `A11y: <${this.name}> cannot have role ${roleValue}` + }); + } + } + } } validate_bindings_foreign() { diff --git a/src/compiler/compile/utils/aria_roles.ts b/src/compiler/compile/utils/aria_roles.ts new file mode 100644 index 000000000000..304e76cc9738 --- /dev/null +++ b/src/compiler/compile/utils/aria_roles.ts @@ -0,0 +1,9 @@ +import { roles as rolesMap } from 'aria-query'; + +const roles = [...rolesMap.keys()]; + +const noninteractive_roles = new Set(roles + .filter((name) => !rolesMap.get(name).abstract) + .filter((name) => !rolesMap.get(name).superClass.some((c) => c.includes('widget')))); + +export { noninteractive_roles }; diff --git a/src/compiler/compile/utils/elements.ts b/src/compiler/compile/utils/elements.ts new file mode 100644 index 000000000000..eee1ec271ad7 --- /dev/null +++ b/src/compiler/compile/utils/elements.ts @@ -0,0 +1,5 @@ +const interactive_elements = new Set([ + 'a', 'button', 'input', 'select', 'textarea' +]); + +export { interactive_elements }; diff --git a/test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/input.svelte b/test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/input.svelte new file mode 100644 index 000000000000..ad12bcfd069d --- /dev/null +++ b/test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/input.svelte @@ -0,0 +1,54 @@ + +link +link +link +link +link +link +link +link +link + + + + + + + + + + + + + + + + + + + + + + + + + + + + +