diff --git a/src/validate/html/a11y.ts b/src/validate/html/a11y.ts
index bf58147350..df1a644bf6 100644
--- a/src/validate/html/a11y.ts
+++ b/src/validate/html/a11y.ts
@@ -27,17 +27,19 @@ export default function a11y(
const attributeMap = new Map();
node.attributes.forEach((attribute: Node) => {
+ const name = attribute.name.toLowerCase();
+
// aria-props
- if (attribute.name.startsWith('aria-')) {
+ if (name.startsWith('aria-')) {
if (invisibleElements.has(node.name)) {
// aria-unsupported-elements
validator.warn(`A11y: <${node.name}> should not have aria-* attributes`, attribute.start);
}
- const name = attribute.name.slice(5);
- if (!ariaAttributeSet.has(name)) {
- const match = fuzzymatch(name, ariaAttributes);
- let message = `A11y: Unknown aria attribute 'aria-${name}'`;
+ const type = name.slice(5);
+ if (!ariaAttributeSet.has(type)) {
+ const match = fuzzymatch(type, ariaAttributes);
+ let message = `A11y: Unknown aria attribute 'aria-${type}'`;
if (match) message += ` (did you mean '${match}'?)`;
validator.warn(message, attribute.start);
@@ -45,7 +47,7 @@ export default function a11y(
}
// aria-role
- if (attribute.name === 'role') {
+ if (name === 'role') {
if (invisibleElements.has(node.name)) {
// aria-unsupported-elements
validator.warn(`A11y: <${node.name}> should not have role attribute`, attribute.start);
@@ -61,10 +63,15 @@ export default function a11y(
}
}
+ // no-access-key
+ if (name === 'accesskey') {
+ validator.warn(`A11y: Avoid using the accessKey attribute`, attribute.start);
+ }
+
attributeMap.set(attribute.name, attribute);
});
- function shouldHaveOneOf(attributes: string[], name = node.name) {
+ function shouldHaveAttribute(attributes: string[], name = node.name) {
if (attributes.length === 0 || !attributes.some((name: string) => attributeMap.has(name))) {
const article = /^[aeiou]/.test(attributes[0]) ? 'an' : 'a';
const sequence = attributes.length > 1 ?
@@ -97,11 +104,11 @@ export default function a11y(
shouldHaveContent();
}
- if (node.name === 'img') shouldHaveOneOf(['alt']);
- if (node.name === 'area') shouldHaveOneOf(['alt', 'aria-label', 'aria-labelledby']);
- if (node.name === 'object') shouldHaveOneOf(['title', 'aria-label', 'aria-labelledby']);
+ if (node.name === 'img') shouldHaveAttribute(['alt']);
+ if (node.name === 'area') shouldHaveAttribute(['alt', 'aria-label', 'aria-labelledby']);
+ if (node.name === 'object') shouldHaveAttribute(['title', 'aria-label', 'aria-labelledby']);
if (node.name === 'input' && getStaticAttributeValue(node, 'type') === 'image') {
- shouldHaveOneOf(['alt', 'aria-label', 'aria-labelledby'], 'input type="image"');
+ shouldHaveAttribute(['alt', 'aria-label', 'aria-labelledby'], 'input type="image"');
}
// heading-has-content
@@ -113,6 +120,11 @@ export default function a11y(
}
}
+ // iframe-has-title
+ if (node.name === 'iframe') {
+ shouldHaveAttribute(['title']);
+ }
+
if (node.name === 'figcaption') {
const parent = elementStack[elementStack.length - 1];
if (parent) {
diff --git a/test/validator/samples/a11y-iframe-has-title/input.html b/test/validator/samples/a11y-iframe-has-title/input.html
new file mode 100644
index 0000000000..2b5060b80e
--- /dev/null
+++ b/test/validator/samples/a11y-iframe-has-title/input.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/validator/samples/a11y-iframe-has-title/warnings.json b/test/validator/samples/a11y-iframe-has-title/warnings.json
new file mode 100644
index 0000000000..8f69f14415
--- /dev/null
+++ b/test/validator/samples/a11y-iframe-has-title/warnings.json
@@ -0,0 +1,10 @@
+[
+ {
+ "message": "A11y: