From 9e1ae7202cc8231e610718291d9814529e007bfd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 14:18:26 -0400 Subject: [PATCH 01/14] a11y checks (#374) --- src/interfaces.ts | 2 +- src/validate/html/index.ts | 27 ++--------- src/validate/html/validateElement.ts | 45 ++++++++++++++++++- test/validator/index.js | 2 +- .../samples/a11y-a-without-href/input.html | 1 + .../samples/a11y-a-without-href/warnings.json | 8 ++++ .../a11y-figcaption-wrong-place/input.html | 19 ++++++++ .../a11y-figcaption-wrong-place/warnings.json | 18 ++++++++ .../samples/a11y-img-without-alt/input.html | 1 + .../a11y-img-without-alt/warnings.json | 8 ++++ 10 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 test/validator/samples/a11y-a-without-href/input.html create mode 100644 test/validator/samples/a11y-a-without-href/warnings.json create mode 100644 test/validator/samples/a11y-figcaption-wrong-place/input.html create mode 100644 test/validator/samples/a11y-figcaption-wrong-place/warnings.json create mode 100644 test/validator/samples/a11y-img-without-alt/input.html create mode 100644 test/validator/samples/a11y-img-without-alt/warnings.json diff --git a/src/interfaces.ts b/src/interfaces.ts index 07bca42ca0..acb9671499 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -54,7 +54,7 @@ export interface CompileOptions { cascade?: boolean; hydratable?: boolean; legacy?: boolean; - customElement: CustomElementOptions | true; + customElement?: CustomElementOptions | true; onerror?: (error: Error) => void; onwarn?: (warning: Warning) => void; diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index ad653daa63..186181e913 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -1,4 +1,3 @@ -import * as namespaces from '../../utils/namespaces'; import validateElement from './validateElement'; import validateWindow from './validateWindow'; import fuzzymatch from '../utils/fuzzymatch' @@ -6,36 +5,20 @@ import flattenReference from '../../utils/flattenReference'; import { Validator } from '../index'; import { Node } from '../../interfaces'; -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|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/; - const meta = new Map([[':Window', validateWindow]]); export default function validateHtml(validator: Validator, html: Node) { - let elementDepth = 0; - const refs = new Map(); const refCallees: Node[] = []; + const elementStack: Node[] = []; function visit(node: Node) { if (node.type === 'Element') { - if ( - elementDepth === 0 && - validator.namespace !== namespaces.svg && - svg.test(node.name) - ) { - validator.warn( - `<${node.name}> is an SVG element – did you forget to add { namespace: 'svg' } ?`, - node.start - ); - } - if (meta.has(node.name)) { return meta.get(node.name)(validator, node, refs, refCallees); } - elementDepth += 1; - - validateElement(validator, node, refs, refCallees); + validateElement(validator, node, refs, refCallees, elementStack); } else if (node.type === 'EachBlock') { if (validator.helpers.has(node.context)) { let c = node.expression.end; @@ -53,16 +36,14 @@ export default function validateHtml(validator: Validator, html: Node) { } if (node.children) { + if (node.type === 'Element') elementStack.push(node); node.children.forEach(visit); + if (node.type === 'Element') elementStack.pop(); } if (node.else) { visit(node.else); } - - if (node.type === 'Element') { - elementDepth -= 1; - } } html.children.forEach(visit); diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts index 3641280f19..aa396bac0a 100644 --- a/src/validate/html/validateElement.ts +++ b/src/validate/html/validateElement.ts @@ -1,8 +1,17 @@ +import * as namespaces from '../../utils/namespaces'; import validateEventHandler from './validateEventHandler'; import { Validator } from '../index'; import { Node } from '../../interfaces'; -export default function validateElement(validator: Validator, node: Node, refs: Map, refCallees: Node[]) { +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|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/; + +export default function validateElement( + validator: Validator, + node: Node, + refs: Map, + refCallees: Node[], + elementStack: Node[] +) { const isComponent = node.name === ':Self' || validator.components.has(node.name); @@ -11,6 +20,13 @@ export default function validateElement(validator: Validator, node: Node, refs: validator.warn(`${node.name} component is not defined`, node.start); } + if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) { + validator.warn( + `<${node.name}> is an SVG element – did you forget to add { namespace: 'svg' } ?`, + node.start + ); + } + if (node.name === 'slot') { const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name'); if (nameAttribute) { @@ -44,6 +60,8 @@ export default function validateElement(validator: Validator, node: Node, refs: let hasOutro: boolean; let hasTransition: boolean; + const attributeMap: Map = new Map(); + node.attributes.forEach((attribute: Node) => { if (attribute.type === 'Ref') { if (!refs.has(attribute.name)) refs.set(attribute.name, []); @@ -161,6 +179,8 @@ export default function validateElement(validator: Validator, node: Node, refs: ); } } else if (attribute.type === 'Attribute') { + attributeMap.set(attribute.name, attribute); + if (attribute.name === 'value' && node.name === 'textarea') { if (node.children.length) { validator.error( @@ -178,6 +198,29 @@ export default function validateElement(validator: Validator, node: Node, refs: } } }); + + // a11y + if (node.name === 'a' && !attributeMap.has('href')) { + validator.warn(`A11y: element should have an href attribute`, node.start); + } + + if (node.name === 'img' && !attributeMap.has('alt')) { + validator.warn(`A11y: element should have an alt attribute`, node.start); + } + + if (node.name === 'figcaption') { + const parent = elementStack[elementStack.length - 1]; + if (parent) { + if (parent.name !== 'figure') { + validator.warn(`A11y:
must be an immediate child of
`, node.start); + } else { + const index = parent.children.indexOf(node); + if (index !== 0 && index !== parent.children.length - 1) { + validator.warn(`A11y:
must be first or last child of
`, node.start); + } + } + } + } } function checkTypeAttribute(validator: Validator, node: Node) { diff --git a/test/validator/index.js b/test/validator/index.js index 176d060faf..1c718fae80 100644 --- a/test/validator/index.js +++ b/test/validator/index.js @@ -2,7 +2,7 @@ import * as fs from "fs"; import assert from "assert"; import { svelte, tryToLoadJson } from "../helpers.js"; -describe("validate", () => { +describe.only("validate", () => { fs.readdirSync("test/validator/samples").forEach(dir => { if (dir[0] === ".") return; diff --git a/test/validator/samples/a11y-a-without-href/input.html b/test/validator/samples/a11y-a-without-href/input.html new file mode 100644 index 0000000000..f7ebe13def --- /dev/null +++ b/test/validator/samples/a11y-a-without-href/input.html @@ -0,0 +1 @@ +not actually a link \ No newline at end of file diff --git a/test/validator/samples/a11y-a-without-href/warnings.json b/test/validator/samples/a11y-a-without-href/warnings.json new file mode 100644 index 0000000000..bf5f650ba5 --- /dev/null +++ b/test/validator/samples/a11y-a-without-href/warnings.json @@ -0,0 +1,8 @@ +[{ + "message": "A11y: element should have an href attribute", + "loc": { + "line": 1, + "column": 0 + }, + "pos": 0 +}] \ No newline at end of file diff --git a/test/validator/samples/a11y-figcaption-wrong-place/input.html b/test/validator/samples/a11y-figcaption-wrong-place/input.html new file mode 100644 index 0000000000..ffa7dde65d --- /dev/null +++ b/test/validator/samples/a11y-figcaption-wrong-place/input.html @@ -0,0 +1,19 @@ +
+ a picture of a foo + +
+ a foo in its natural habitat +
+ +

this should not be here

+
+ +
+ a picture of a foo + +
+
+ this element should be a child of the figure +
+
+
\ No newline at end of file diff --git a/test/validator/samples/a11y-figcaption-wrong-place/warnings.json b/test/validator/samples/a11y-figcaption-wrong-place/warnings.json new file mode 100644 index 0000000000..0e5e1a1976 --- /dev/null +++ b/test/validator/samples/a11y-figcaption-wrong-place/warnings.json @@ -0,0 +1,18 @@ +[ + { + "message": "A11y:
must be first or last child of
", + "loc": { + "line": 4, + "column": 1 + }, + "pos": 57 + }, + { + "message": "A11y:
must be an immediate child of
", + "loc": { + "line": 15, + "column": 2 + }, + "pos": 252 + } +] diff --git a/test/validator/samples/a11y-img-without-alt/input.html b/test/validator/samples/a11y-img-without-alt/input.html new file mode 100644 index 0000000000..4e524fe107 --- /dev/null +++ b/test/validator/samples/a11y-img-without-alt/input.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/validator/samples/a11y-img-without-alt/warnings.json b/test/validator/samples/a11y-img-without-alt/warnings.json new file mode 100644 index 0000000000..9c48172925 --- /dev/null +++ b/test/validator/samples/a11y-img-without-alt/warnings.json @@ -0,0 +1,8 @@ +[{ + "message": "A11y: element should have an alt attribute", + "loc": { + "line": 1, + "column": 0 + }, + "pos": 0 +}] \ No newline at end of file From defa982029d9a947265c50719f26b55f11b4c64e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 14:42:09 -0400 Subject: [PATCH 02/14] anchor-has-content --- src/validate/html/a11y.ts | 50 +++++++++++++++++++ src/validate/html/index.ts | 3 ++ src/validate/html/validateElement.ts | 27 ---------- .../a11y-anchor-has-content/input.html | 1 + .../a11y-anchor-has-content/warnings.json | 8 +++ 5 files changed, 62 insertions(+), 27 deletions(-) create mode 100644 src/validate/html/a11y.ts create mode 100644 test/validator/samples/a11y-anchor-has-content/input.html create mode 100644 test/validator/samples/a11y-anchor-has-content/warnings.json diff --git a/src/validate/html/a11y.ts b/src/validate/html/a11y.ts new file mode 100644 index 0000000000..70beea0ac3 --- /dev/null +++ b/src/validate/html/a11y.ts @@ -0,0 +1,50 @@ +import * as namespaces from '../../utils/namespaces'; +import validateEventHandler from './validateEventHandler'; +import { Validator } from '../index'; +import { Node } from '../../interfaces'; + +export default function a11y( + validator: Validator, + node: Node, + elementStack: Node[] +) { + if (node.type === 'Text') { + // accessible-emoji + return; + } + + if (node.type !== 'Element') return; + + const attributeMap = new Map(); + node.attributes.forEach((attribute: Node) => { + attributeMap.set(attribute.name, attribute); + }); + + if (node.name === 'a') { + if (!attributeMap.has('href')) { + validator.warn(`A11y: element should have an href attribute`, node.start); + } + + if (!node.children.length) { + validator.warn(`A11y: element should have child content`, node.start); + } + } + + if (node.name === 'img' && !attributeMap.has('alt')) { + validator.warn(`A11y: element should have an alt attribute`, node.start); + } + + if (node.name === 'figcaption') { + const parent = elementStack[elementStack.length - 1]; + if (parent) { + if (parent.name !== 'figure') { + validator.warn(`A11y:
must be an immediate child of
`, node.start); + } else { + const index = parent.children.indexOf(node); + if (index !== 0 && index !== parent.children.length - 1) { + validator.warn(`A11y:
must be first or last child of
`, node.start); + } + } + } + } +} \ No newline at end of file diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index 186181e913..d36b00b833 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -1,5 +1,6 @@ import validateElement from './validateElement'; import validateWindow from './validateWindow'; +import a11y from './a11y'; import fuzzymatch from '../utils/fuzzymatch' import flattenReference from '../../utils/flattenReference'; import { Validator } from '../index'; @@ -13,6 +14,8 @@ export default function validateHtml(validator: Validator, html: Node) { const elementStack: Node[] = []; function visit(node: Node) { + a11y(validator, node, elementStack); + if (node.type === 'Element') { if (meta.has(node.name)) { return meta.get(node.name)(validator, node, refs, refCallees); diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts index aa396bac0a..c3243c6e64 100644 --- a/src/validate/html/validateElement.ts +++ b/src/validate/html/validateElement.ts @@ -60,8 +60,6 @@ export default function validateElement( let hasOutro: boolean; let hasTransition: boolean; - const attributeMap: Map = new Map(); - node.attributes.forEach((attribute: Node) => { if (attribute.type === 'Ref') { if (!refs.has(attribute.name)) refs.set(attribute.name, []); @@ -179,8 +177,6 @@ export default function validateElement( ); } } else if (attribute.type === 'Attribute') { - attributeMap.set(attribute.name, attribute); - if (attribute.name === 'value' && node.name === 'textarea') { if (node.children.length) { validator.error( @@ -198,29 +194,6 @@ export default function validateElement( } } }); - - // a11y - if (node.name === 'a' && !attributeMap.has('href')) { - validator.warn(`A11y: element should have an href attribute`, node.start); - } - - if (node.name === 'img' && !attributeMap.has('alt')) { - validator.warn(`A11y: element should have an alt attribute`, node.start); - } - - if (node.name === 'figcaption') { - const parent = elementStack[elementStack.length - 1]; - if (parent) { - if (parent.name !== 'figure') { - validator.warn(`A11y:
must be an immediate child of
`, node.start); - } else { - const index = parent.children.indexOf(node); - if (index !== 0 && index !== parent.children.length - 1) { - validator.warn(`A11y:
must be first or last child of
`, node.start); - } - } - } - } } function checkTypeAttribute(validator: Validator, node: Node) { diff --git a/test/validator/samples/a11y-anchor-has-content/input.html b/test/validator/samples/a11y-anchor-has-content/input.html new file mode 100644 index 0000000000..ca81f90ae8 --- /dev/null +++ b/test/validator/samples/a11y-anchor-has-content/input.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/validator/samples/a11y-anchor-has-content/warnings.json b/test/validator/samples/a11y-anchor-has-content/warnings.json new file mode 100644 index 0000000000..157bec1f9b --- /dev/null +++ b/test/validator/samples/a11y-anchor-has-content/warnings.json @@ -0,0 +1,8 @@ +[{ + "message": "A11y: element should have child content", + "loc": { + "line": 1, + "column": 0 + }, + "pos": 0 +}] \ No newline at end of file From 6842c37ab6238bed2ee60028659af22aa1cd7ac5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 15:01:15 -0400 Subject: [PATCH 03/14] anchor-is-valid --- src/validate/html/a11y.ts | 17 +++++++++++- .../samples/a11y-a-without-href/input.html | 1 - .../samples/a11y-a-without-href/warnings.json | 8 ------ .../samples/a11y-anchor-is-valid/input.html | 3 +++ .../a11y-anchor-is-valid/warnings.json | 26 +++++++++++++++++++ 5 files changed, 45 insertions(+), 10 deletions(-) delete mode 100644 test/validator/samples/a11y-a-without-href/input.html delete mode 100644 test/validator/samples/a11y-a-without-href/warnings.json create mode 100644 test/validator/samples/a11y-anchor-is-valid/input.html create mode 100644 test/validator/samples/a11y-anchor-is-valid/warnings.json diff --git a/src/validate/html/a11y.ts b/src/validate/html/a11y.ts index 70beea0ac3..359b575670 100644 --- a/src/validate/html/a11y.ts +++ b/src/validate/html/a11y.ts @@ -21,10 +21,18 @@ export default function a11y( }); if (node.name === 'a') { - if (!attributeMap.has('href')) { + // anchor-is-valid + const href = attributeMap.get('href'); + if (href) { + const value = getValue(href); + if (value === '' || value === '#') { + validator.warn(`A11y: '${value}' is not a valid href attribute`, href.start); + } + } else { validator.warn(`A11y: element should have an href attribute`, node.start); } + // anchor-has-content if (!node.children.length) { validator.warn(`A11y: element should have child content`, node.start); } @@ -47,4 +55,11 @@ export default function a11y( } } } +} + +function getValue(attribute: Node) { + if (attribute.value.length === 0) return ''; + if (attribute.value.length === 1 && attribute.value[0].type === 'Text') return attribute.value[0].data; + + return null; } \ No newline at end of file diff --git a/test/validator/samples/a11y-a-without-href/input.html b/test/validator/samples/a11y-a-without-href/input.html deleted file mode 100644 index f7ebe13def..0000000000 --- a/test/validator/samples/a11y-a-without-href/input.html +++ /dev/null @@ -1 +0,0 @@ -not actually a link \ No newline at end of file diff --git a/test/validator/samples/a11y-a-without-href/warnings.json b/test/validator/samples/a11y-a-without-href/warnings.json deleted file mode 100644 index bf5f650ba5..0000000000 --- a/test/validator/samples/a11y-a-without-href/warnings.json +++ /dev/null @@ -1,8 +0,0 @@ -[{ - "message": "A11y: element should have an href attribute", - "loc": { - "line": 1, - "column": 0 - }, - "pos": 0 -}] \ No newline at end of file diff --git a/test/validator/samples/a11y-anchor-is-valid/input.html b/test/validator/samples/a11y-anchor-is-valid/input.html new file mode 100644 index 0000000000..7b14a80c9f --- /dev/null +++ b/test/validator/samples/a11y-anchor-is-valid/input.html @@ -0,0 +1,3 @@ +not actually a link +invalid +invalid \ No newline at end of file diff --git a/test/validator/samples/a11y-anchor-is-valid/warnings.json b/test/validator/samples/a11y-anchor-is-valid/warnings.json new file mode 100644 index 0000000000..c63418f1bf --- /dev/null +++ b/test/validator/samples/a11y-anchor-is-valid/warnings.json @@ -0,0 +1,26 @@ +[ + { + "message": "A11y: element should have an href attribute", + "loc": { + "line": 1, + "column": 0 + }, + "pos": 0 + }, + { + "message": "A11y: '' is not a valid href attribute", + "loc": { + "line": 2, + "column": 3 + }, + "pos": 30 + }, + { + "message": "A11y: '#' is not a valid href attribute", + "loc": { + "line": 3, + "column": 3 + }, + "pos": 53 + } +] From 508069873fc83f3081871fa8019c197692c4c722 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 17:18:48 -0400 Subject: [PATCH 04/14] alt-text --- src/generators/dom/preprocess.ts | 2 +- .../dom/visitors/Element/Attribute.ts | 2 +- .../dom/visitors/Element/Binding.ts | 2 +- .../dom/visitors/Element/Element.ts | 2 +- .../dom/visitors/Element/StyleAttribute.ts | 2 +- src/generators/dom/visitors/Slot.ts | 2 +- .../shared/getStaticAttributeValue.ts | 15 ---------- src/utils/getStaticAttributeValue.ts | 17 +++++++++++ src/validate/html/a11y.ts | 30 +++++++++++++++---- 9 files changed, 48 insertions(+), 26 deletions(-) delete mode 100644 src/generators/shared/getStaticAttributeValue.ts create mode 100644 src/utils/getStaticAttributeValue.ts diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index e602af60ae..b945c902b9 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -1,7 +1,7 @@ import Block from './Block'; import { trimStart, trimEnd } from '../../utils/trim'; import { assign } from '../../shared/index.js'; -import getStaticAttributeValue from '../shared/getStaticAttributeValue'; +import getStaticAttributeValue from '../../utils/getStaticAttributeValue'; import { DomGenerator } from './index'; import { Node } from '../../interfaces'; import { State } from './interfaces'; diff --git a/src/generators/dom/visitors/Element/Attribute.ts b/src/generators/dom/visitors/Element/Attribute.ts index a994c6b58b..837e0945db 100644 --- a/src/generators/dom/visitors/Element/Attribute.ts +++ b/src/generators/dom/visitors/Element/Attribute.ts @@ -3,7 +3,7 @@ import deindent from '../../../../utils/deindent'; import visitStyleAttribute, { optimizeStyle } from './StyleAttribute'; import { stringify } from '../../../../utils/stringify'; import getExpressionPrecedence from '../../../../utils/getExpressionPrecedence'; -import getStaticAttributeValue from '../../../shared/getStaticAttributeValue'; +import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue'; import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; diff --git a/src/generators/dom/visitors/Element/Binding.ts b/src/generators/dom/visitors/Element/Binding.ts index 8878af17d0..492cebd5b4 100644 --- a/src/generators/dom/visitors/Element/Binding.ts +++ b/src/generators/dom/visitors/Element/Binding.ts @@ -1,6 +1,6 @@ import deindent from '../../../../utils/deindent'; import flattenReference from '../../../../utils/flattenReference'; -import getStaticAttributeValue from '../../../shared/getStaticAttributeValue'; +import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue'; import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; diff --git a/src/generators/dom/visitors/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts index f26c3e341b..114482f649 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -8,7 +8,7 @@ import visitEventHandler from './EventHandler'; import visitBinding from './Binding'; import visitRef from './Ref'; import * as namespaces from '../../../../utils/namespaces'; -import getStaticAttributeValue from '../../../shared/getStaticAttributeValue'; +import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue'; import addTransitions from './addTransitions'; import { DomGenerator } from '../../index'; import Block from '../../Block'; diff --git a/src/generators/dom/visitors/Element/StyleAttribute.ts b/src/generators/dom/visitors/Element/StyleAttribute.ts index 4cf5776145..ab661bfe06 100644 --- a/src/generators/dom/visitors/Element/StyleAttribute.ts +++ b/src/generators/dom/visitors/Element/StyleAttribute.ts @@ -2,7 +2,7 @@ import attributeLookup from './lookup'; import deindent from '../../../../utils/deindent'; import { stringify } from '../../../../utils/stringify'; import getExpressionPrecedence from '../../../../utils/getExpressionPrecedence'; -import getStaticAttributeValue from '../../../shared/getStaticAttributeValue'; +import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue'; import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; diff --git a/src/generators/dom/visitors/Slot.ts b/src/generators/dom/visitors/Slot.ts index 5249879f40..ef3651e1bf 100644 --- a/src/generators/dom/visitors/Slot.ts +++ b/src/generators/dom/visitors/Slot.ts @@ -2,7 +2,7 @@ import { DomGenerator } from '../index'; import deindent from '../../../utils/deindent'; import visit from '../visit'; import Block from '../Block'; -import getStaticAttributeValue from '../../shared/getStaticAttributeValue'; +import getStaticAttributeValue from '../../../utils/getStaticAttributeValue'; import { Node } from '../../../interfaces'; import { State } from '../interfaces'; diff --git a/src/generators/shared/getStaticAttributeValue.ts b/src/generators/shared/getStaticAttributeValue.ts deleted file mode 100644 index 02d38343d3..0000000000 --- a/src/generators/shared/getStaticAttributeValue.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Node } from '../../interfaces'; - -export default function getStaticAttributeValue(node: Node, name: string) { - const attribute = node.attributes.find( - (attr: Node) => attr.name.toLowerCase() === name - ); - if (!attribute) return null; - - if (attribute.value.length !== 1 || attribute.value[0].type !== 'Text') { - // TODO catch this in validation phase, give a more useful error (with location etc) - throw new Error(`'${name}' must be a static attribute`); - } - - return attribute.value[0].data; -} diff --git a/src/utils/getStaticAttributeValue.ts b/src/utils/getStaticAttributeValue.ts new file mode 100644 index 0000000000..67bea12259 --- /dev/null +++ b/src/utils/getStaticAttributeValue.ts @@ -0,0 +1,17 @@ +import { Node } from '../interfaces'; + +export default function getStaticAttributeValue(node: Node, name: string) { + const attribute = node.attributes.find( + (attr: Node) => attr.name.toLowerCase() === name + ); + + if (!attribute) return null; + + if (attribute.value.length === 0) return ''; + + if (attribute.value.length === 1 && attribute.value[0].type === 'Text') { + return attribute.value[0].data; + } + + return null; +} diff --git a/src/validate/html/a11y.ts b/src/validate/html/a11y.ts index 359b575670..c7d50f17e3 100644 --- a/src/validate/html/a11y.ts +++ b/src/validate/html/a11y.ts @@ -1,4 +1,5 @@ import * as namespaces from '../../utils/namespaces'; +import getStaticAttributeValue from '../../utils/getStaticAttributeValue'; import validateEventHandler from './validateEventHandler'; import { Validator } from '../index'; import { Node } from '../../interfaces'; @@ -20,11 +21,27 @@ export default function a11y( attributeMap.set(attribute.name, attribute); }); + function shouldHaveOneOf(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 ? + attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` : + attributes[0]; + + console.log(`warning about ${name}: ${sequence}`) + validator.warn(`A11y: <${name}> element should have ${article} ${sequence} attribute`, node.start); + } + + else { + console.log('ok', node.name); + } + } + if (node.name === 'a') { // anchor-is-valid const href = attributeMap.get('href'); - if (href) { - const value = getValue(href); + if (attributeMap.has('href')) { + const value = getStaticAttributeValue(node, 'href'); if (value === '' || value === '#') { validator.warn(`A11y: '${value}' is not a valid href attribute`, href.start); } @@ -33,13 +50,16 @@ export default function a11y( } // anchor-has-content - if (!node.children.length) { + if (node.children.length === 0) { validator.warn(`A11y: element should have child content`, node.start); } } - if (node.name === 'img' && !attributeMap.has('alt')) { - validator.warn(`A11y: element should have an alt attribute`, node.start); + 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 === 'input' && getStaticAttributeValue(node, 'type') === 'image') { + shouldHaveOneOf(['alt', 'aria-label', 'aria-labelledby'], 'input type="image"'); } if (node.name === 'figcaption') { From 9f3f35e37bc4ac55e5d53c0d4de5615c07dd5a68 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 17:18:59 -0400 Subject: [PATCH 05/14] update some tests --- .../expected-bundle.js | 8 ++-- .../expected.js | 8 ++-- .../css-media-query/expected-bundle.js | 8 ++-- test/js/samples/css-media-query/expected.js | 8 ++-- .../custom-element-basic/expected-bundle.js | 6 +-- .../samples/custom-element-basic/expected.js | 6 +-- .../custom-element-slot/expected-bundle.js | 22 +++++----- .../samples/custom-element-slot/expected.js | 22 +++++----- .../custom-element-styled/expected-bundle.js | 6 +-- .../samples/custom-element-styled/expected.js | 6 +-- .../expected-bundle.js | 20 ++++----- .../each-block-changed-check/expected.js | 20 ++++----- .../event-handlers-custom/expected-bundle.js | 6 +-- .../samples/event-handlers-custom/expected.js | 6 +-- .../if-block-no-update/expected-bundle.js | 12 ++--- .../js/samples/if-block-no-update/expected.js | 12 ++--- .../if-block-simple/expected-bundle.js | 6 +-- test/js/samples/if-block-simple/expected.js | 6 +-- .../expected-bundle.js | 6 +-- .../expected.js | 6 +-- .../expected-bundle.js | 6 +-- .../inline-style-optimized-url/expected.js | 6 +-- .../inline-style-optimized/expected-bundle.js | 6 +-- .../inline-style-optimized/expected.js | 6 +-- .../expected-bundle.js | 12 ++--- .../inline-style-unoptimized/expected.js | 12 ++--- .../expected-bundle.js | 6 +-- .../input-without-blowback-guard/expected.js | 6 +-- .../legacy-input-type/expected-bundle.js | 6 +-- test/js/samples/legacy-input-type/expected.js | 6 +-- .../expected-bundle.js | 44 +++++++++---------- .../use-elements-as-anchors/expected.js | 44 +++++++++---------- .../samples/a11y-alt-text/input.html | 9 ++++ .../samples/a11y-alt-text/warnings.json | 37 ++++++++++++++++ .../samples/a11y-img-without-alt/input.html | 1 - .../a11y-img-without-alt/warnings.json | 8 ---- 36 files changed, 226 insertions(+), 189 deletions(-) create mode 100644 test/validator/samples/a11y-alt-text/input.html create mode 100644 test/validator/samples/a11y-alt-text/warnings.json delete mode 100644 test/validator/samples/a11y-img-without-alt/input.html delete mode 100644 test/validator/samples/a11y-img-without-alt/warnings.json diff --git a/test/js/samples/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js index 94f831b653..780afda754 100644 --- a/test/js/samples/collapses-text-around-comments/expected-bundle.js +++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js @@ -199,17 +199,17 @@ function create_main_fragment(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText(state.foo); this.hydrate(); }, hydrate: function(nodes) { - encapsulateStyles( p ); + encapsulateStyles(p); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, @@ -220,7 +220,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop diff --git a/test/js/samples/collapses-text-around-comments/expected.js b/test/js/samples/collapses-text-around-comments/expected.js index 317cd6ad63..60ca5c2f15 100644 --- a/test/js/samples/collapses-text-around-comments/expected.js +++ b/test/js/samples/collapses-text-around-comments/expected.js @@ -24,17 +24,17 @@ function create_main_fragment(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText(state.foo); this.hydrate(); }, hydrate: function(nodes) { - encapsulateStyles( p ); + encapsulateStyles(p); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, @@ -45,7 +45,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop diff --git a/test/js/samples/css-media-query/expected-bundle.js b/test/js/samples/css-media-query/expected-bundle.js index ecbb9be904..c57250e9f2 100644 --- a/test/js/samples/css-media-query/expected-bundle.js +++ b/test/js/samples/css-media-query/expected-bundle.js @@ -187,22 +187,22 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); this.hydrate(); }, hydrate: function(nodes) { - encapsulateStyles( div ); + encapsulateStyles(div); }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); }, update: noop, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/css-media-query/expected.js b/test/js/samples/css-media-query/expected.js index b707ddcd3d..0e75dc09d8 100644 --- a/test/js/samples/css-media-query/expected.js +++ b/test/js/samples/css-media-query/expected.js @@ -16,22 +16,22 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); this.hydrate(); }, hydrate: function(nodes) { - encapsulateStyles( div ); + encapsulateStyles(div); }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); }, update: noop, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/custom-element-basic/expected-bundle.js b/test/js/samples/custom-element-basic/expected-bundle.js index 9ba64bae1a..bd1a063877 100644 --- a/test/js/samples/custom-element-basic/expected-bundle.js +++ b/test/js/samples/custom-element-basic/expected-bundle.js @@ -176,14 +176,14 @@ function create_main_fragment(state, component) { return { create: function() { - h1 = createElement( 'h1' ); + h1 = createElement("h1"); text = createText("Hello "); text_1 = createText(state.name); text_2 = createText("!"); }, mount: function(target, anchor) { - insertNode( h1, target, anchor ); + insertNode(h1, target, anchor); appendNode(text, h1); appendNode(text_1, h1); appendNode(text_2, h1); @@ -196,7 +196,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( h1 ); + detachNode(h1); }, destroy: noop diff --git a/test/js/samples/custom-element-basic/expected.js b/test/js/samples/custom-element-basic/expected.js index bf35272e52..d7b2514940 100644 --- a/test/js/samples/custom-element-basic/expected.js +++ b/test/js/samples/custom-element-basic/expected.js @@ -5,14 +5,14 @@ function create_main_fragment(state, component) { return { create: function() { - h1 = createElement( 'h1' ); + h1 = createElement("h1"); text = createText("Hello "); text_1 = createText(state.name); text_2 = createText("!"); }, mount: function(target, anchor) { - insertNode( h1, target, anchor ); + insertNode(h1, target, anchor); appendNode(text, h1); appendNode(text_1, h1); appendNode(text_2, h1); @@ -25,7 +25,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( h1 ); + detachNode(h1); }, destroy: noop diff --git a/test/js/samples/custom-element-slot/expected-bundle.js b/test/js/samples/custom-element-slot/expected-bundle.js index 57a46aa8b9..8667a5d672 100644 --- a/test/js/samples/custom-element-slot/expected-bundle.js +++ b/test/js/samples/custom-element-slot/expected-bundle.js @@ -180,13 +180,13 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); - slot = createElement( 'slot' ); - p = createElement( 'p' ); + div = createElement("div"); + slot = createElement("slot"); + p = createElement("p"); text = createText("default fallback content"); text_2 = createText("\n\n\t"); - slot_1 = createElement( 'slot' ); - p_1 = createElement( 'p' ); + slot_1 = createElement("slot"); + p_1 = createElement("p"); text_3 = createText("foo fallback content"); this.hydrate(); }, @@ -196,20 +196,20 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); - appendNode( slot, div ); - appendNode( p, slot ); + insertNode(div, target, anchor); + appendNode(slot, div); + appendNode(p, slot); appendNode(text, p); appendNode(text_2, div); - appendNode( slot_1, div ); - appendNode( p_1, slot_1 ); + appendNode(slot_1, div); + appendNode(p_1, slot_1); appendNode(text_3, p_1); }, update: noop, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/custom-element-slot/expected.js b/test/js/samples/custom-element-slot/expected.js index b2d31b62bf..b64c7e6838 100644 --- a/test/js/samples/custom-element-slot/expected.js +++ b/test/js/samples/custom-element-slot/expected.js @@ -5,13 +5,13 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); - slot = createElement( 'slot' ); - p = createElement( 'p' ); + div = createElement("div"); + slot = createElement("slot"); + p = createElement("p"); text = createText("default fallback content"); text_2 = createText("\n\n\t"); - slot_1 = createElement( 'slot' ); - p_1 = createElement( 'p' ); + slot_1 = createElement("slot"); + p_1 = createElement("p"); text_3 = createText("foo fallback content"); this.hydrate(); }, @@ -21,20 +21,20 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); - appendNode( slot, div ); - appendNode( p, slot ); + insertNode(div, target, anchor); + appendNode(slot, div); + appendNode(p, slot); appendNode(text, p); appendNode(text_2, div); - appendNode( slot_1, div ); - appendNode( p_1, slot_1 ); + appendNode(slot_1, div); + appendNode(p_1, slot_1); appendNode(text_3, p_1); }, update: noop, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/custom-element-styled/expected-bundle.js b/test/js/samples/custom-element-styled/expected-bundle.js index a7cb0c6f02..1ef76c5aae 100644 --- a/test/js/samples/custom-element-styled/expected-bundle.js +++ b/test/js/samples/custom-element-styled/expected-bundle.js @@ -176,14 +176,14 @@ function create_main_fragment(state, component) { return { create: function() { - h1 = createElement( 'h1' ); + h1 = createElement("h1"); text = createText("Hello "); text_1 = createText(state.name); text_2 = createText("!"); }, mount: function(target, anchor) { - insertNode( h1, target, anchor ); + insertNode(h1, target, anchor); appendNode(text, h1); appendNode(text_1, h1); appendNode(text_2, h1); @@ -196,7 +196,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( h1 ); + detachNode(h1); }, destroy: noop diff --git a/test/js/samples/custom-element-styled/expected.js b/test/js/samples/custom-element-styled/expected.js index 14d17bad20..8d53597042 100644 --- a/test/js/samples/custom-element-styled/expected.js +++ b/test/js/samples/custom-element-styled/expected.js @@ -5,14 +5,14 @@ function create_main_fragment(state, component) { return { create: function() { - h1 = createElement( 'h1' ); + h1 = createElement("h1"); text = createText("Hello "); text_1 = createText(state.name); text_2 = createText("!"); }, mount: function(target, anchor) { - insertNode( h1, target, anchor ); + insertNode(h1, target, anchor); appendNode(text, h1); appendNode(text_1, h1); appendNode(text_2, h1); @@ -25,7 +25,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( h1 ); + detachNode(h1); }, destroy: noop diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js index f3d5a2423c..cdea1b775e 100644 --- a/test/js/samples/each-block-changed-check/expected-bundle.js +++ b/test/js/samples/each-block-changed-check/expected-bundle.js @@ -202,7 +202,7 @@ function create_main_fragment(state, component) { } text = createText("\n\n"); - p = createElement( 'p' ); + p = createElement("p"); text_1 = createText(state.foo); }, @@ -212,7 +212,7 @@ function create_main_fragment(state, component) { } insertNode(text, target, anchor); - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text_1, p); }, @@ -248,7 +248,7 @@ function create_main_fragment(state, component) { } detachNode(text); - detachNode( p ); + detachNode(p); }, destroy: function() { @@ -262,11 +262,11 @@ function create_each_block(state, each_block_value, comment, i, component) { return { create: function() { - div = createElement( 'div' ); - strong = createElement( 'strong' ); + div = createElement("div"); + strong = createElement("strong"); text = createText(i); text_1 = createText("\n\n\t\t"); - span = createElement( 'span' ); + span = createElement("span"); text_2 = createText(text_2_value); text_3 = createText(" wrote "); text_4 = createText(text_4_value); @@ -282,11 +282,11 @@ function create_each_block(state, each_block_value, comment, i, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); - appendNode( strong, div ); + insertNode(div, target, anchor); + appendNode(strong, div); appendNode(text, strong); appendNode(text_1, div); - appendNode( span, div ); + appendNode(span, div); appendNode(text_2, span); appendNode(text_3, span); appendNode(text_4, span); @@ -314,7 +314,7 @@ function create_each_block(state, each_block_value, comment, i, component) { unmount: function() { detachAfter(raw_before); - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index 95d70a38a5..7e11a597b4 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -18,7 +18,7 @@ function create_main_fragment(state, component) { } text = createText("\n\n"); - p = createElement( 'p' ); + p = createElement("p"); text_1 = createText(state.foo); }, @@ -28,7 +28,7 @@ function create_main_fragment(state, component) { } insertNode(text, target, anchor); - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text_1, p); }, @@ -64,7 +64,7 @@ function create_main_fragment(state, component) { } detachNode(text); - detachNode( p ); + detachNode(p); }, destroy: function() { @@ -78,11 +78,11 @@ function create_each_block(state, each_block_value, comment, i, component) { return { create: function() { - div = createElement( 'div' ); - strong = createElement( 'strong' ); + div = createElement("div"); + strong = createElement("strong"); text = createText(i); text_1 = createText("\n\n\t\t"); - span = createElement( 'span' ); + span = createElement("span"); text_2 = createText(text_2_value); text_3 = createText(" wrote "); text_4 = createText(text_4_value); @@ -98,11 +98,11 @@ function create_each_block(state, each_block_value, comment, i, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); - appendNode( strong, div ); + insertNode(div, target, anchor); + appendNode(strong, div); appendNode(text, strong); appendNode(text_1, div); - appendNode( span, div ); + appendNode(span, div); appendNode(text_2, span); appendNode(text_3, span); appendNode(text_4, span); @@ -130,7 +130,7 @@ function create_each_block(state, each_block_value, comment, i, component) { unmount: function() { detachAfter(raw_before); - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/event-handlers-custom/expected-bundle.js b/test/js/samples/event-handlers-custom/expected-bundle.js index 63684f82f3..c448b8fab3 100644 --- a/test/js/samples/event-handlers-custom/expected-bundle.js +++ b/test/js/samples/event-handlers-custom/expected-bundle.js @@ -191,7 +191,7 @@ function create_main_fragment(state, component) { return { create: function() { - button = createElement( 'button' ); + button = createElement("button"); text = createText("foo"); this.hydrate(); }, @@ -204,14 +204,14 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( button, target, anchor ); + insertNode(button, target, anchor); appendNode(text, button); }, update: noop, unmount: function() { - detachNode( button ); + detachNode(button); }, destroy: function() { diff --git a/test/js/samples/event-handlers-custom/expected.js b/test/js/samples/event-handlers-custom/expected.js index a77fb14f41..19599726aa 100644 --- a/test/js/samples/event-handlers-custom/expected.js +++ b/test/js/samples/event-handlers-custom/expected.js @@ -20,7 +20,7 @@ function create_main_fragment(state, component) { return { create: function() { - button = createElement( 'button' ); + button = createElement("button"); text = createText("foo"); this.hydrate(); }, @@ -33,14 +33,14 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( button, target, anchor ); + insertNode(button, target, anchor); appendNode(text, button); }, update: noop, unmount: function() { - detachNode( button ); + detachNode(button); }, destroy: function() { diff --git a/test/js/samples/if-block-no-update/expected-bundle.js b/test/js/samples/if-block-no-update/expected-bundle.js index b82f365515..8fdfa69489 100644 --- a/test/js/samples/if-block-no-update/expected-bundle.js +++ b/test/js/samples/if-block-no-update/expected-bundle.js @@ -218,17 +218,17 @@ function create_if_block(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("foo!"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -240,17 +240,17 @@ function create_if_block_1(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("not foo!"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop diff --git a/test/js/samples/if-block-no-update/expected.js b/test/js/samples/if-block-no-update/expected.js index 3e96153e96..ece28404c5 100644 --- a/test/js/samples/if-block-no-update/expected.js +++ b/test/js/samples/if-block-no-update/expected.js @@ -43,17 +43,17 @@ function create_if_block(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("foo!"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -65,17 +65,17 @@ function create_if_block_1(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("not foo!"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop diff --git a/test/js/samples/if-block-simple/expected-bundle.js b/test/js/samples/if-block-simple/expected-bundle.js index 018e56b330..3e7c5425c6 100644 --- a/test/js/samples/if-block-simple/expected-bundle.js +++ b/test/js/samples/if-block-simple/expected-bundle.js @@ -221,17 +221,17 @@ function create_if_block(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("foo!"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop diff --git a/test/js/samples/if-block-simple/expected.js b/test/js/samples/if-block-simple/expected.js index f862ea60fc..9fdca3bcd0 100644 --- a/test/js/samples/if-block-simple/expected.js +++ b/test/js/samples/if-block-simple/expected.js @@ -46,17 +46,17 @@ function create_if_block(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("foo!"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop diff --git a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js index 24b0d8b8bb..7e694cc03a 100644 --- a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js +++ b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js @@ -172,7 +172,7 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); this.hydrate(); }, @@ -182,7 +182,7 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); }, update: function(changed, state) { @@ -196,7 +196,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/inline-style-optimized-multiple/expected.js b/test/js/samples/inline-style-optimized-multiple/expected.js index 8353abcf7c..69d5484a33 100644 --- a/test/js/samples/inline-style-optimized-multiple/expected.js +++ b/test/js/samples/inline-style-optimized-multiple/expected.js @@ -5,7 +5,7 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); this.hydrate(); }, @@ -15,7 +15,7 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); }, update: function(changed, state) { @@ -29,7 +29,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/inline-style-optimized-url/expected-bundle.js b/test/js/samples/inline-style-optimized-url/expected-bundle.js index 794c772a26..2d94be8c91 100644 --- a/test/js/samples/inline-style-optimized-url/expected-bundle.js +++ b/test/js/samples/inline-style-optimized-url/expected-bundle.js @@ -172,7 +172,7 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); this.hydrate(); }, @@ -181,7 +181,7 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); }, update: function(changed, state) { @@ -191,7 +191,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/inline-style-optimized-url/expected.js b/test/js/samples/inline-style-optimized-url/expected.js index 25e1c0e41e..b71ab726c7 100644 --- a/test/js/samples/inline-style-optimized-url/expected.js +++ b/test/js/samples/inline-style-optimized-url/expected.js @@ -5,7 +5,7 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); this.hydrate(); }, @@ -14,7 +14,7 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); }, update: function(changed, state) { @@ -24,7 +24,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/inline-style-optimized/expected-bundle.js b/test/js/samples/inline-style-optimized/expected-bundle.js index b3ae1daa0f..4e47a8fc3f 100644 --- a/test/js/samples/inline-style-optimized/expected-bundle.js +++ b/test/js/samples/inline-style-optimized/expected-bundle.js @@ -172,7 +172,7 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); this.hydrate(); }, @@ -181,7 +181,7 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); }, update: function(changed, state) { @@ -191,7 +191,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/inline-style-optimized/expected.js b/test/js/samples/inline-style-optimized/expected.js index 5ba0e80dff..630419b2dd 100644 --- a/test/js/samples/inline-style-optimized/expected.js +++ b/test/js/samples/inline-style-optimized/expected.js @@ -5,7 +5,7 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); this.hydrate(); }, @@ -14,7 +14,7 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); }, update: function(changed, state) { @@ -24,7 +24,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); }, destroy: noop diff --git a/test/js/samples/inline-style-unoptimized/expected-bundle.js b/test/js/samples/inline-style-unoptimized/expected-bundle.js index b2cd05e09a..dbca449ebf 100644 --- a/test/js/samples/inline-style-unoptimized/expected-bundle.js +++ b/test/js/samples/inline-style-unoptimized/expected-bundle.js @@ -172,9 +172,9 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); text = createText("\n"); - div_1 = createElement( 'div' ); + div_1 = createElement("div"); this.hydrate(); }, @@ -184,9 +184,9 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); insertNode(text, target, anchor); - insertNode( div_1, target, anchor ); + insertNode(div_1, target, anchor); }, update: function(changed, state) { @@ -200,9 +200,9 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); detachNode(text); - detachNode( div_1 ); + detachNode(div_1); }, destroy: noop diff --git a/test/js/samples/inline-style-unoptimized/expected.js b/test/js/samples/inline-style-unoptimized/expected.js index 591c265398..c104be1b57 100644 --- a/test/js/samples/inline-style-unoptimized/expected.js +++ b/test/js/samples/inline-style-unoptimized/expected.js @@ -5,9 +5,9 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); text = createText("\n"); - div_1 = createElement( 'div' ); + div_1 = createElement("div"); this.hydrate(); }, @@ -17,9 +17,9 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); insertNode(text, target, anchor); - insertNode( div_1, target, anchor ); + insertNode(div_1, target, anchor); }, update: function(changed, state) { @@ -33,9 +33,9 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); detachNode(text); - detachNode( div_1 ); + detachNode(div_1); }, destroy: noop diff --git a/test/js/samples/input-without-blowback-guard/expected-bundle.js b/test/js/samples/input-without-blowback-guard/expected-bundle.js index 58f498a9bb..f713e26387 100644 --- a/test/js/samples/input-without-blowback-guard/expected-bundle.js +++ b/test/js/samples/input-without-blowback-guard/expected-bundle.js @@ -180,7 +180,7 @@ function create_main_fragment(state, component) { return { create: function() { - input = createElement( 'input' ); + input = createElement("input"); this.hydrate(); }, @@ -190,7 +190,7 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( input, target, anchor ); + insertNode(input, target, anchor); input.checked = state.foo; }, @@ -200,7 +200,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( input ); + detachNode(input); }, destroy: function() { diff --git a/test/js/samples/input-without-blowback-guard/expected.js b/test/js/samples/input-without-blowback-guard/expected.js index 067c9589e5..47b9478140 100644 --- a/test/js/samples/input-without-blowback-guard/expected.js +++ b/test/js/samples/input-without-blowback-guard/expected.js @@ -9,7 +9,7 @@ function create_main_fragment(state, component) { return { create: function() { - input = createElement( 'input' ); + input = createElement("input"); this.hydrate(); }, @@ -19,7 +19,7 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( input, target, anchor ); + insertNode(input, target, anchor); input.checked = state.foo; }, @@ -29,7 +29,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( input ); + detachNode(input); }, destroy: function() { diff --git a/test/js/samples/legacy-input-type/expected-bundle.js b/test/js/samples/legacy-input-type/expected-bundle.js index 0802e2aec6..dcb2035c99 100644 --- a/test/js/samples/legacy-input-type/expected-bundle.js +++ b/test/js/samples/legacy-input-type/expected-bundle.js @@ -174,7 +174,7 @@ function create_main_fragment(state, component) { return { create: function() { - input = createElement( 'input' ); + input = createElement("input"); this.hydrate(); }, @@ -183,13 +183,13 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( input, target, anchor ); + insertNode(input, target, anchor); }, update: noop, unmount: function() { - detachNode( input ); + detachNode(input); }, destroy: noop diff --git a/test/js/samples/legacy-input-type/expected.js b/test/js/samples/legacy-input-type/expected.js index ec867f2375..bfdde4e851 100644 --- a/test/js/samples/legacy-input-type/expected.js +++ b/test/js/samples/legacy-input-type/expected.js @@ -5,7 +5,7 @@ function create_main_fragment(state, component) { return { create: function() { - input = createElement( 'input' ); + input = createElement("input"); this.hydrate(); }, @@ -14,13 +14,13 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( input, target, anchor ); + insertNode(input, target, anchor); }, update: noop, unmount: function() { - detachNode( input ); + detachNode(input); }, destroy: noop diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js index 9a256e4997..7fe227ad5d 100644 --- a/test/js/samples/use-elements-as-anchors/expected-bundle.js +++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js @@ -190,17 +190,17 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); if (if_block) if_block.create(); text = createText("\n\n\t"); - p = createElement( 'p' ); + p = createElement("p"); text_1 = createText("this can be used as an anchor"); text_2 = createText("\n\n\t"); if (if_block_1) if_block_1.create(); text_3 = createText("\n\n\t"); if (if_block_2) if_block_2.create(); text_4 = createText("\n\n\t"); - p_1 = createElement( 'p' ); + p_1 = createElement("p"); text_5 = createText("so can this"); text_6 = createText("\n\n\t"); if (if_block_3) if_block_3.create(); @@ -210,17 +210,17 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); if (if_block) if_block.mount(div, null); appendNode(text, div); - appendNode( p, div ); + appendNode(p, div); appendNode(text_1, p); appendNode(text_2, div); if (if_block_1) if_block_1.mount(div, null); appendNode(text_3, div); if (if_block_2) if_block_2.mount(div, null); appendNode(text_4, div); - appendNode( p_1, div ); + appendNode(p_1, div); appendNode(text_5, p_1); appendNode(text_6, div); if (if_block_3) if_block_3.mount(div, null); @@ -292,7 +292,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); if (if_block) if_block.unmount(); if (if_block_1) if_block_1.unmount(); if (if_block_2) if_block_2.unmount(); @@ -317,17 +317,17 @@ function create_if_block(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("a"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -339,17 +339,17 @@ function create_if_block_1(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("b"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -361,17 +361,17 @@ function create_if_block_2(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("c"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -383,17 +383,17 @@ function create_if_block_3(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("d"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -405,17 +405,17 @@ function create_if_block_4(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("e"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop diff --git a/test/js/samples/use-elements-as-anchors/expected.js b/test/js/samples/use-elements-as-anchors/expected.js index c9dfd69c32..8f9a061617 100644 --- a/test/js/samples/use-elements-as-anchors/expected.js +++ b/test/js/samples/use-elements-as-anchors/expected.js @@ -15,17 +15,17 @@ function create_main_fragment(state, component) { return { create: function() { - div = createElement( 'div' ); + div = createElement("div"); if (if_block) if_block.create(); text = createText("\n\n\t"); - p = createElement( 'p' ); + p = createElement("p"); text_1 = createText("this can be used as an anchor"); text_2 = createText("\n\n\t"); if (if_block_1) if_block_1.create(); text_3 = createText("\n\n\t"); if (if_block_2) if_block_2.create(); text_4 = createText("\n\n\t"); - p_1 = createElement( 'p' ); + p_1 = createElement("p"); text_5 = createText("so can this"); text_6 = createText("\n\n\t"); if (if_block_3) if_block_3.create(); @@ -35,17 +35,17 @@ function create_main_fragment(state, component) { }, mount: function(target, anchor) { - insertNode( div, target, anchor ); + insertNode(div, target, anchor); if (if_block) if_block.mount(div, null); appendNode(text, div); - appendNode( p, div ); + appendNode(p, div); appendNode(text_1, p); appendNode(text_2, div); if (if_block_1) if_block_1.mount(div, null); appendNode(text_3, div); if (if_block_2) if_block_2.mount(div, null); appendNode(text_4, div); - appendNode( p_1, div ); + appendNode(p_1, div); appendNode(text_5, p_1); appendNode(text_6, div); if (if_block_3) if_block_3.mount(div, null); @@ -117,7 +117,7 @@ function create_main_fragment(state, component) { }, unmount: function() { - detachNode( div ); + detachNode(div); if (if_block) if_block.unmount(); if (if_block_1) if_block_1.unmount(); if (if_block_2) if_block_2.unmount(); @@ -142,17 +142,17 @@ function create_if_block(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("a"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -164,17 +164,17 @@ function create_if_block_1(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("b"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -186,17 +186,17 @@ function create_if_block_2(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("c"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -208,17 +208,17 @@ function create_if_block_3(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("d"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop @@ -230,17 +230,17 @@ function create_if_block_4(state, component) { return { create: function() { - p = createElement( 'p' ); + p = createElement("p"); text = createText("e"); }, mount: function(target, anchor) { - insertNode( p, target, anchor ); + insertNode(p, target, anchor); appendNode(text, p); }, unmount: function() { - detachNode( p ); + detachNode(p); }, destroy: noop diff --git a/test/validator/samples/a11y-alt-text/input.html b/test/validator/samples/a11y-alt-text/input.html new file mode 100644 index 0000000000..f17638af76 --- /dev/null +++ b/test/validator/samples/a11y-alt-text/input.html @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/validator/samples/a11y-alt-text/warnings.json b/test/validator/samples/a11y-alt-text/warnings.json new file mode 100644 index 0000000000..7bddf9730b --- /dev/null +++ b/test/validator/samples/a11y-alt-text/warnings.json @@ -0,0 +1,37 @@ +[ + { + "message": "A11y: element should have an alt attribute", + "loc": { + "line": 1, + "column": 0 + }, + "pos": 0 + }, + + { + "message": "A11y: element should have an alt, aria-label or aria-labelledby attribute", + "loc": { + "line": 4, + "column": 1 + }, + "pos": 28 + }, + + { + "message": "A11y: element should have a title, aria-label or aria-labelledby attribute", + "loc": { + "line": 7, + "column": 0 + }, + "pos": 43 + }, + + { + "message": "A11y: element should have an alt, aria-label or aria-labelledby attribute", + "loc": { + "line": 9, + "column": 0 + }, + "pos": 62 + } +] diff --git a/test/validator/samples/a11y-img-without-alt/input.html b/test/validator/samples/a11y-img-without-alt/input.html deleted file mode 100644 index 4e524fe107..0000000000 --- a/test/validator/samples/a11y-img-without-alt/input.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/validator/samples/a11y-img-without-alt/warnings.json b/test/validator/samples/a11y-img-without-alt/warnings.json deleted file mode 100644 index 9c48172925..0000000000 --- a/test/validator/samples/a11y-img-without-alt/warnings.json +++ /dev/null @@ -1,8 +0,0 @@ -[{ - "message": "A11y: element should have an alt attribute", - "loc": { - "line": 1, - "column": 0 - }, - "pos": 0 -}] \ No newline at end of file From 9f2481c90b45fb6d3f73aab46393a436221f0d9c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 17:35:34 -0400 Subject: [PATCH 06/14] aria-props --- src/validate/html/a11y.ts | 20 ++++++++++++++----- .../samples/a11y-aria-props/input.html | 1 + .../samples/a11y-aria-props/warnings.json | 19 ++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 test/validator/samples/a11y-aria-props/input.html create mode 100644 test/validator/samples/a11y-aria-props/warnings.json diff --git a/src/validate/html/a11y.ts b/src/validate/html/a11y.ts index c7d50f17e3..4cb1c0be8e 100644 --- a/src/validate/html/a11y.ts +++ b/src/validate/html/a11y.ts @@ -1,9 +1,13 @@ import * as namespaces from '../../utils/namespaces'; import getStaticAttributeValue from '../../utils/getStaticAttributeValue'; +import fuzzymatch from '../utils/fuzzymatch'; import validateEventHandler from './validateEventHandler'; import { Validator } from '../index'; import { Node } from '../../interfaces'; +const ariaAttributes = 'activedescendant atomic autocomplete busy checked controls describedby disabled dropeffect expanded flowto grabbed haspopup hidden invalid label labelledby level live multiline multiselectable orientation owns posinset pressed readonly relevant required selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); +const ariaSet = new Set(ariaAttributes); + export default function a11y( validator: Validator, node: Node, @@ -18,6 +22,17 @@ export default function a11y( const attributeMap = new Map(); node.attributes.forEach((attribute: Node) => { + if (attribute.name.startsWith('aria-')) { + const name = attribute.name.slice(5); + if (!ariaSet.has(name)) { + const match = fuzzymatch(name, ariaAttributes); + let message = `A11y: Unknown aria attribute 'aria-${name}'`; + if (match) message += ` (did you mean '${match}'?)`; + + validator.warn(message, attribute.start); + } + } + attributeMap.set(attribute.name, attribute); }); @@ -28,13 +43,8 @@ export default function a11y( attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` : attributes[0]; - console.log(`warning about ${name}: ${sequence}`) validator.warn(`A11y: <${name}> element should have ${article} ${sequence} attribute`, node.start); } - - else { - console.log('ok', node.name); - } } if (node.name === 'a') { diff --git a/test/validator/samples/a11y-aria-props/input.html b/test/validator/samples/a11y-aria-props/input.html new file mode 100644 index 0000000000..21c6327960 --- /dev/null +++ b/test/validator/samples/a11y-aria-props/input.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/validator/samples/a11y-aria-props/warnings.json b/test/validator/samples/a11y-aria-props/warnings.json new file mode 100644 index 0000000000..5c0ce2c49e --- /dev/null +++ b/test/validator/samples/a11y-aria-props/warnings.json @@ -0,0 +1,19 @@ +[ + { + "message": "A11y: Unknown aria attribute 'aria-labeledby' (did you mean 'labelledby'?)", + "loc": { + "line": 1, + "column": 20 + }, + "pos": 20 + }, + + { + "message": "A11y: element should have an alt, aria-label or aria-labelledby attribute", + "loc": { + "column": 0, + "line": 1 + }, + "pos": 0 + } +] From 3fce4d400faf82484a0a20041ea02a1f847f2ddc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 17:51:46 -0400 Subject: [PATCH 07/14] aria-role --- src/validate/html/a11y.ts | 20 +++++++++++++++++-- .../samples/a11y-aria-role/input.html | 1 + .../samples/a11y-aria-role/warnings.json | 10 ++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 test/validator/samples/a11y-aria-role/input.html create mode 100644 test/validator/samples/a11y-aria-role/warnings.json diff --git a/src/validate/html/a11y.ts b/src/validate/html/a11y.ts index 4cb1c0be8e..dcd3e1acce 100644 --- a/src/validate/html/a11y.ts +++ b/src/validate/html/a11y.ts @@ -6,7 +6,10 @@ import { Validator } from '../index'; import { Node } from '../../interfaces'; const ariaAttributes = 'activedescendant atomic autocomplete busy checked controls describedby disabled dropeffect expanded flowto grabbed haspopup hidden invalid label labelledby level live multiline multiselectable orientation owns posinset pressed readonly relevant required selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); -const ariaSet = new Set(ariaAttributes); +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); export default function a11y( validator: Validator, @@ -22,9 +25,10 @@ export default function a11y( const attributeMap = new Map(); node.attributes.forEach((attribute: Node) => { + // aria-props if (attribute.name.startsWith('aria-')) { const name = attribute.name.slice(5); - if (!ariaSet.has(name)) { + if (!ariaAttributeSet.has(name)) { const match = fuzzymatch(name, ariaAttributes); let message = `A11y: Unknown aria attribute 'aria-${name}'`; if (match) message += ` (did you mean '${match}'?)`; @@ -33,6 +37,18 @@ export default function a11y( } } + // aria-role + if (attribute.name === 'role') { + const value = getStaticAttributeValue(node, 'role'); + if (value && !ariaRoleSet.has(value)) { + const match = fuzzymatch(value, ariaRoles); + let message = `A11y: Unknown role '${value}'`; + if (match) message += ` (did you mean '${match}'?)`; + + validator.warn(message, attribute.start); + } + } + attributeMap.set(attribute.name, attribute); }); diff --git a/test/validator/samples/a11y-aria-role/input.html b/test/validator/samples/a11y-aria-role/input.html new file mode 100644 index 0000000000..3089d84e5b --- /dev/null +++ b/test/validator/samples/a11y-aria-role/input.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/test/validator/samples/a11y-aria-role/warnings.json b/test/validator/samples/a11y-aria-role/warnings.json new file mode 100644 index 0000000000..152a27daa8 --- /dev/null +++ b/test/validator/samples/a11y-aria-role/warnings.json @@ -0,0 +1,10 @@ +[ + { + "message": "A11y: Unknown role 'toooltip' (did you mean 'tooltip'?)", + "loc": { + "line": 1, + "column": 5 + }, + "pos": 5 + } +] From 636016b26c33ccbbe8c5e3fea082e1b530e35162 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 18:04:00 -0400 Subject: [PATCH 08/14] 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 + } +] From 275a3ec4601f8729bcfc1bec9b24c56ac702120b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 3 Sep 2017 18:16:03 -0400 Subject: [PATCH 09/14] no-access-key --- src/validate/html/a11y.ts | 34 +++++++++++++------ .../samples/a11y-iframe-has-title/input.html | 1 + .../a11y-iframe-has-title/warnings.json | 10 ++++++ .../samples/a11y-no-access-key/input.html | 1 + .../samples/a11y-no-access-key/warnings.json | 8 +++++ 5 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 test/validator/samples/a11y-iframe-has-title/input.html create mode 100644 test/validator/samples/a11y-iframe-has-title/warnings.json create mode 100644 test/validator/samples/a11y-no-access-key/input.html create mode 100644 test/validator/samples/a11y-no-access-key/warnings.json 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: