From 2364f6a04d76f67de19c83ee2486513346d0df4f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 18:04:00 -0400 Subject: [PATCH] heading-has-content --- src/validate/html/a11y.ts | 31 +++++++++++++++++-- src/validate/js/index.ts | 6 ++-- .../a11y-aria-unsupported-element/input.html | 2 ++ .../warnings.json | 19 ++++++++++++ .../a11y-heading-has-content/input.html | 2 ++ .../a11y-heading-has-content/warnings.json | 19 ++++++++++++ 6 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 test/validator/samples/a11y-aria-unsupported-element/input.html create mode 100644 test/validator/samples/a11y-aria-unsupported-element/warnings.json create mode 100644 test/validator/samples/a11y-heading-has-content/input.html create mode 100644 test/validator/samples/a11y-heading-has-content/warnings.json diff --git a/src/validate/html/a11y.ts b/src/validate/html/a11y.ts index dcd3e1acce..bf58147350 100644 --- a/src/validate/html/a11y.ts +++ b/src/validate/html/a11y.ts @@ -11,6 +11,8 @@ const ariaAttributeSet = new Set(ariaAttributes); const ariaRoles = 'alert alertdialog application article banner button checkbox columnheader combobox command complementary composite contentinfo definition dialog directory document form grid gridcell group heading img input landmark link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation note option presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search section sectionhead select separator slider spinbutton status structure tab tablist tabpanel textbox timer toolbar tooltip tree treegrid treeitem widget window'.split(' '); const ariaRoleSet = new Set(ariaRoles); +const invisibleElements = new Set(['meta', 'html', 'script', 'style']); + export default function a11y( validator: Validator, node: Node, @@ -27,6 +29,11 @@ export default function a11y( node.attributes.forEach((attribute: Node) => { // aria-props if (attribute.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); @@ -39,6 +46,11 @@ export default function a11y( // aria-role if (attribute.name === 'role') { + if (invisibleElements.has(node.name)) { + // aria-unsupported-elements + validator.warn(`A11y: <${node.name}> should not have role attribute`, attribute.start); + } + const value = getStaticAttributeValue(node, 'role'); if (value && !ariaRoleSet.has(value)) { const match = fuzzymatch(value, ariaRoles); @@ -63,6 +75,12 @@ export default function a11y( } } + function shouldHaveContent() { + if (node.children.length === 0) { + validator.warn(`A11y: <${node.name}> element should have child content`, node.start); + } + } + if (node.name === 'a') { // anchor-is-valid const href = attributeMap.get('href'); @@ -76,9 +94,7 @@ export default function a11y( } // anchor-has-content - if (node.children.length === 0) { - validator.warn(`A11y: element should have child content`, node.start); - } + shouldHaveContent(); } if (node.name === 'img') shouldHaveOneOf(['alt']); @@ -88,6 +104,15 @@ export default function a11y( shouldHaveOneOf(['alt', 'aria-label', 'aria-labelledby'], 'input type="image"'); } + // heading-has-content + if (/^h[1-6]$/.test(node.name)) { + shouldHaveContent(); + + if (attributeMap.has('aria-hidden')) { + validator.warn(`A11y: <${node.name}> element should not be hidden`, attributeMap.get('aria-hidden').start); + } + } + if (node.name === 'figcaption') { const parent = elementStack[elementStack.length - 1]; if (parent) { diff --git a/src/validate/js/index.ts b/src/validate/js/index.ts index 0b7547f64c..01754b7d00 100644 --- a/src/validate/js/index.ts +++ b/src/validate/js/index.ts @@ -57,14 +57,12 @@ export default function validateJs(validator: Validator, js: Node) { const match = fuzzymatch(prop.key.name, validPropList); if (match) { validator.error( - `Unexpected property '${prop.key - .name}' (did you mean '${match}'?)`, + `Unexpected property '${prop.key.name}' (did you mean '${match}'?)`, prop.start ); } else if (/FunctionExpression/.test(prop.value.type)) { validator.error( - `Unexpected property '${prop.key - .name}' (did you mean to include it in 'methods'?)`, + `Unexpected property '${prop.key.name}' (did you mean to include it in 'methods'?)`, prop.start ); } else { diff --git a/test/validator/samples/a11y-aria-unsupported-element/input.html b/test/validator/samples/a11y-aria-unsupported-element/input.html new file mode 100644 index 0000000000..0a674c4284 --- /dev/null +++ b/test/validator/samples/a11y-aria-unsupported-element/input.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/test/validator/samples/a11y-aria-unsupported-element/warnings.json b/test/validator/samples/a11y-aria-unsupported-element/warnings.json new file mode 100644 index 0000000000..5e2c358271 --- /dev/null +++ b/test/validator/samples/a11y-aria-unsupported-element/warnings.json @@ -0,0 +1,19 @@ +[ + { + "message": "A11y: should not have aria-* attributes", + "loc": { + "line": 1, + "column": 6 + }, + "pos": 6 + }, + + { + "message": "A11y: should not have role attribute", + "loc": { + "line": 2, + "column": 6 + }, + "pos": 33 + } +] diff --git a/test/validator/samples/a11y-heading-has-content/input.html b/test/validator/samples/a11y-heading-has-content/input.html new file mode 100644 index 0000000000..1414af825d --- /dev/null +++ b/test/validator/samples/a11y-heading-has-content/input.html @@ -0,0 +1,2 @@ +

+

invisible header

\ No newline at end of file diff --git a/test/validator/samples/a11y-heading-has-content/warnings.json b/test/validator/samples/a11y-heading-has-content/warnings.json new file mode 100644 index 0000000000..15bb3a162a --- /dev/null +++ b/test/validator/samples/a11y-heading-has-content/warnings.json @@ -0,0 +1,19 @@ +[ + { + "message": "A11y:

element should have child content", + "loc": { + "line": 1, + "column": 0 + }, + "pos": 0 + }, + + { + "message": "A11y:

element should not be hidden", + "loc": { + "line": 2, + "column": 4 + }, + "pos": 14 + } +]