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: