a11y validation - click-events-have-key-events, redudant-alt

pull/3725/head
Tan Li Hau 6 years ago
parent 31e8480cb6
commit 3ecd3d84b3

@ -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<string, Attribute>
) {
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<string, EventHandler>
) {
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<string, Attribute>
) {
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 dont 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;
}
}

@ -0,0 +1,8 @@
<script>
function handleKey(){}
</script>
<div on:click={() => {}} />
<div on:click={() => {}} on:keydown={handleKey} />
<div on:click={() => {}} on:keyup={handleKey} />
<div on:click={() => {}} on:keypress={handleKey} />

@ -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
}
}
]

@ -0,0 +1,13 @@
<script>
let photo = '';
</script>
<!-- success -->
<img src="foo" alt="Foo eating a sandwich." />
<img src="bar" aria-hidden alt="Picture of me taking a photo of an image" />
<img src="baz" alt={`Baz taking a ${photo}`} />
<!-- fail -->
<img src="foo" alt="Photo of foo being weird." />
<img src="bar" alt="Image of me at a bar!" />
<img src="baz" alt="Picture of baz fixing a bug." />
<img src="baz" alt={'Baz taking a photo'} />
<img src="baz" alt={`Baz taking a ${photo} photo`} />

@ -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 dont 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 dont 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 dont 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 dont 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 dont 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
}
}
]

@ -1 +1,17 @@
[]
[
{
"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
}
}
]

@ -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",

Loading…
Cancel
Save