diff --git a/src/compile/Component.ts b/src/compile/Component.ts index ff6db38b5f..bebafa48f7 100644 --- a/src/compile/Component.ts +++ b/src/compile/Component.ts @@ -754,7 +754,7 @@ export default class Component { const deps = dependencies.get(key); deps.forEach(visit); - computations.push({ key, deps, hasRestParam: false }); + this.computations.push({ key, deps, hasRestParam: false }); const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key); }; @@ -764,7 +764,7 @@ export default class Component { ); if (fullStateComputations.length > 0) { - computations.push(...fullStateComputations); + this.computations.push(...fullStateComputations); } } @@ -787,8 +787,8 @@ export default class Component { if (templateProperties.methods) { addDeclaration('methods', templateProperties.methods.value); - templateProperties.methods.value.properties.forEach(prop => { - this.methods.add(prop.key.name); + templateProperties.methods.value.properties.forEach(property => { + this.methods.add(getName(property.key)); }); } diff --git a/src/compile/nodes/Animation.ts b/src/compile/nodes/Animation.ts index e07e64e48d..4df3fec66f 100644 --- a/src/compile/nodes/Animation.ts +++ b/src/compile/nodes/Animation.ts @@ -36,14 +36,7 @@ export default class Animation extends Node { }); } - // TODO reinstate this... it's tricky because we're - // in the process of *creating* block.children - // if (block.children.length > 1) { - // component.error(this, { - // code: `invalid-animation`, - // message: `An element that use the animate directive must be the sole child of a keyed each block` - // }); - // } + block.hasAnimation = true; this.expression = info.expression ? new Expression(component, this, scope, info.expression) diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index bbf2e63ec7..60e814afa4 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -20,6 +20,7 @@ export default class EachBlock extends Node { key: Expression; scope: TemplateScope; contexts: Array<{ name: string, tail: string }>; + hasAnimation: boolean; children: Node[]; else?: ElseBlock; @@ -37,6 +38,13 @@ export default class EachBlock extends Node { unpackDestructuring(this.contexts, info.context, ''); this.contexts.forEach(context => { + if (component.helpers.has(context.key.name)) { + component.warn(context.key, { + code: `each-context-clash`, + message: `Context clashes with a helper. Rename one or the other to eliminate any ambiguity` + }); + } + this.scope.add(context.key.name, this.expression.dependencies); }); @@ -50,8 +58,20 @@ export default class EachBlock extends Node { this.scope.add(this.index, dependencies); } + this.hasAnimation = false; + this.children = mapChildren(component, this, this.scope, info.children); + if (this.hasAnimation) { + if (this.children.length !== 1) { + const child = this.children.find(child => !!child.animation); + component.error(child.animation, { + code: `invalid-animation`, + message: `An element that use the animate directive must be the sole child of a keyed each block` + }); + } + } + this.warnIfEmptyBlock(); // TODO would be better if EachBlock, IfBlock etc extended an abstract Block class this.else = info.else diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index 325e93c7cf..bbea56e55e 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -287,6 +287,8 @@ export default class Element extends Node { const attributeMap = new Map(); this.attributes.forEach(attribute => { + if (attribute.isSpread) return; + const name = attribute.name.toLowerCase(); // aria-props @@ -310,6 +312,13 @@ export default class Element extends Node { message }); } + + if (name === 'aria-hidden' && /^h[1-6]$/.test(this.name)) { + component.warn(attribute, { + code: `a11y-hidden`, + message: `A11y: <${this.name}> element should not be hidden` + }); + } } // aria-role diff --git a/src/compile/nodes/shared/Node.ts b/src/compile/nodes/shared/Node.ts index cb4e66151f..d338c378cd 100644 --- a/src/compile/nodes/shared/Node.ts +++ b/src/compile/nodes/shared/Node.ts @@ -171,8 +171,10 @@ export default class Node { } warnIfEmptyBlock() { + if (!this.component.options.dev) return; if (!/Block$/.test(this.type) || !this.children) return; if (this.children.length > 1) return; + const child = this.children[0]; if (!child || (child.type === 'Text' && !/[^ \r\n\f\v\t]/.test(child.data))) { diff --git a/src/validate/html/a11y.ts b/src/validate/html/a11y.ts deleted file mode 100644 index 88f7dbe51d..0000000000 --- a/src/validate/html/a11y.ts +++ /dev/null @@ -1,75 +0,0 @@ -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 current describedby details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); -const ariaAttributeSet = new Set(ariaAttributes); - -const ariaRoles = 'alert alertdialog application article banner button cell checkbox columnheader combobox command complementary composite contentinfo definition dialog directory document feed figure form grid gridcell group heading img input landmark link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search searchbox section sectionhead select separator slider spinbutton status structure switch tab table tablist tabpanel term 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, - elementStack: Node[] -) { - if (node.type === 'Text') { - // accessible-emoji - return; - } - - 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 ? - attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` : - attributes[0]; - - validator.warn(node, { - code: `a11y-missing-attribute`, - message: `A11y: <${name}> element should have ${article} ${sequence} attribute` - }); - } - } - - if (/^h[1-6]$/.test(node.name)) { - if (attributeMap.has('aria-hidden')) { - validator.warn(attributeMap.get('aria-hidden'), { - code: `a11y-hidden`, - message: `A11y: <${node.name}> element should not be hidden` - }); - } - } - - if (node.name === 'figcaption') { - const parent = elementStack[elementStack.length - 1]; - if (parent) { - if (parent.name !== 'figure') { - validator.warn(node, { - code: `a11y-structure`, - message: `A11y:
must be an immediate child of
` - }); - } else { - const children = parent.children.filter(node => { - if (node.type === 'Comment') return false; - if (node.type === 'Text') return /\S/.test(node.data); - return true; - }); - - const index = children.indexOf(node); - - if (index !== 0 && index !== children.length - 1) { - validator.warn(node, { - code: `a11y-structure`, - message: `A11y:
must be first or last child of
` - }); - } - } - } - } -} \ No newline at end of file diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index 3a6b6aa306..0e3741fc65 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -1,6 +1,3 @@ -import a11y from './a11y'; -import fuzzymatch from '../utils/fuzzymatch' -import flattenReference from '../../utils/flattenReference'; import { Validator } from '../index'; import { Node } from '../../interfaces'; import unpackDestructuring from '../../utils/unpackDestructuring'; @@ -13,17 +10,8 @@ function isEmptyBlock(node: Node) { } export default function validateHtml(validator: Validator, html: Node) { - const refs = new Map(); - const refCallees: Node[] = []; - const stack: Node[] = []; - const elementStack: Node[] = []; - function visit(node: Node) { - if (node.type === 'Element') { - a11y(validator, node, elementStack); - } - - else if (node.type === 'EachBlock') { + if (node.type === 'EachBlock') { const contexts = []; unpackDestructuring(contexts, node.context, ''); @@ -36,31 +24,6 @@ export default function validateHtml(validator: Validator, html: Node) { } }); } - - if (validator.options.dev && isEmptyBlock(node)) { - validator.warn(node, { - code: `empty-block`, - message: 'Empty block' - }); - } - - if (node.children) { - if (node.type === 'Element') elementStack.push(node); - stack.push(node); - node.children.forEach(visit); - stack.pop(); - if (node.type === 'Element') elementStack.pop(); - } - - if (node.else) { - visit(node.else); - } - - if (node.type === 'AwaitBlock') { - visit(node.pending); - visit(node.then); - visit(node.catch); - } } html.children.forEach(visit); diff --git a/src/validate/html/validateComponent.ts b/src/validate/html/validateComponent.ts deleted file mode 100644 index 4e4b8de935..0000000000 --- a/src/validate/html/validateComponent.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as namespaces from '../../utils/namespaces'; -import validateEventHandler from './validateEventHandler'; -import validate, { Validator } from '../index'; -import { Node } from '../../interfaces'; -import isValidIdentifier from '../../utils/isValidIdentifier'; - -export default function validateComponent( - validator: Validator, - node: Node, - refs: Map, - refCallees: Node[], - stack: Node[], - elementStack: Node[] -) { - node.attributes.forEach((attribute: Node) => { - if (attribute.type === 'Action') { - validator.error(attribute, { - code: `invalid-action`, - message: `Actions can only be applied to DOM elements, not components` - }); - } else if (attribute.type === 'Class') { - validator.error(attribute, { - code: `invalid-class`, - message: `Classes can only be applied to DOM elements, not components` - }); - } - }); -}