From 51c2e0581b33f05c455cc2d2a09b0b3fe0d94eb0 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 07bca42ca0bd..acb967149982 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 ad653daa632c..186181e9132f 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 3641280f191f..aa396bac0ae8 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 176d060faf47..1c718fae80e5 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 000000000000..f7ebe13def6a --- /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 000000000000..bf5f650ba56d --- /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 000000000000..ffa7dde65db1 --- /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 000000000000..0e5e1a1976c9 --- /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 000000000000..4e524fe10761 --- /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 000000000000..9c4817292537 --- /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 bacbaef868047d21add49638c4ae422a15c48be7 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 000000000000..70beea0ac336 --- /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 186181e9132f..d36b00b83345 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 aa396bac0ae8..c3243c6e645d 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 000000000000..ca81f90ae895 --- /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 000000000000..157bec1f9b10 --- /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 6133976fec85d9656f1d1c19f8d3f57da09ee6ed 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 70beea0ac336..359b57567069 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 f7ebe13def6a..000000000000 --- 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 bf5f650ba56d..000000000000 --- 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 000000000000..7b14a80c9f14 --- /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 000000000000..c63418f1bfb0 --- /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 a90e07208638466de258293abb5c6086ead9dfd1 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 e602af60aee1..b945c902b999 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 a994c6b58b04..837e0945dbfa 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 8878af17d09c..492cebd5b44f 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 f26c3e341bce..114482f6492e 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 4cf577614524..ab661bfe06ed 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 5249879f40e3..ef3651e1bf7f 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 02d38343d3b7..000000000000 --- 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 000000000000..67bea122593a --- /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 359b57567069..c7d50f17e35d 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 073cbc0460200711bb9f7318f05c929e4be639f1 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 94f831b6530f..780afda754b1 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 317cd6ad63c4..60ca5c2f15b3 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 ecbb9be9042a..c57250e9f264 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 b707ddcd3d18..0e75dc09d8c2 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 9ba64bae1aa9..bd1a063877b8 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 bf35272e52db..d7b251494085 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 57a46aa8b988..8667a5d67298 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 b2d31b62bf02..b64c7e683856 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 a7cb0c6f0254..1ef76c5aae28 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 14d17bad2017..8d5359704233 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 f3d5a2423c61..cdea1b775efb 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 95d70a38a550..7e11a597b4bc 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 63684f82f3a5..c448b8fab3c0 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 a77fb14f41d1..19599726aa07 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 b82f36551573..8fdfa69489a6 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 3e96153e96ac..ece28404c547 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 018e56b330fa..3e7c5425c64f 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 f862ea60fc92..9fdca3bcd0f6 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 24b0d8b8bb25..7e694cc03a14 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 8353abcf7c10..69d5484a33a4 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 794c772a261a..2d94be8c91e3 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 25e1c0e41e19..b71ab726c78f 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 b3ae1daa0f84..4e47a8fc3fe5 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 5ba0e80dffa4..630419b2dd86 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 b2cd05e09ac5..dbca449ebf30 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 591c265398cb..c104be1b5772 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 58f498a9bbc9..f713e2638732 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 067c9589e582..47b9478140ad 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 0802e2aec606..dcb2035c99e7 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 ec867f2375ce..bfdde4e85123 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 9a256e499751..7fe227ad5d23 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 c9dfd69c3243..8f9a06161797 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 000000000000..f17638af769c --- /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 000000000000..7bddf9730beb --- /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 4e524fe10761..000000000000 --- 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 9c4817292537..000000000000 --- 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 125191a843ef4ce41b74b335a4bb44ec3fc0e0d6 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 c7d50f17e35d..4cb1c0be8e0f 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 000000000000..21c6327960b7 --- /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 000000000000..5c0ce2c49e87 --- /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 c62a74e8aee14090ba85e2653b69386954673f19 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 4cb1c0be8e0f..dcd3e1acce7f 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 000000000000..3089d84e5b69 --- /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 000000000000..152a27daa82e --- /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 2364f6a04d76f67de19c83ee2486513346d0df4f 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 dcd3e1acce7f..bf5814735023 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 0b7547f64c08..01754b7d00d9 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 000000000000..0a674c42844d --- /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 000000000000..5e2c358271b7 --- /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 000000000000..1414af825d24 --- /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 000000000000..15bb3a162abd --- /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 d5b8d2fd489a278fce2878a51ffa163e0d344256 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 bf5814735023..df1a644bf6ce 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 000000000000..2b5060b80eae --- /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 000000000000..8f69f1441578 --- /dev/null +++ b/test/validator/samples/a11y-iframe-has-title/warnings.json @@ -0,0 +1,10 @@ +[ + { + "message": "A11y: