From a9c4ae90b7e31349deb29211a110b42fba1c6a70 Mon Sep 17 00:00:00 2001 From: Zama Khan Mohammed Date: Fri, 1 Mar 2019 20:30:25 -0600 Subject: [PATCH] fix(rule): don't check keyup events for some elements (#772) --- package.json | 1 + src/templateClickEventsHaveKeyEventsRule.ts | 21 ++ src/util/attributesComparator.ts | 24 +++ src/util/getAttributeValue.ts | 13 +- src/util/getLiteralValue.ts | 8 + src/util/isHiddenFromScreenReader.ts | 24 +++ src/util/isInteractiveElement.ts | 94 +++++++++ src/util/isPresentationRole.ts | 7 + ...mplateClickEventsHaveKeyEventsRule.spec.ts | 187 +++++++++++++++++- yarn.lock | 6 + 10 files changed, 377 insertions(+), 8 deletions(-) create mode 100644 src/util/attributesComparator.ts create mode 100644 src/util/getLiteralValue.ts create mode 100644 src/util/isHiddenFromScreenReader.ts create mode 100644 src/util/isInteractiveElement.ts create mode 100644 src/util/isPresentationRole.ts diff --git a/package.json b/package.json index 148e93656..b78b7bbda 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "dependencies": { "app-root-path": "^2.1.0", "aria-query": "^3.0.0", + "axobject-query": "^2.0.2", "css-selector-tokenizer": "^0.7.0", "cssauron": "^1.4.0", "damerau-levenshtein": "^1.0.4", diff --git a/src/templateClickEventsHaveKeyEventsRule.ts b/src/templateClickEventsHaveKeyEventsRule.ts index fd078e79d..289d28cdc 100644 --- a/src/templateClickEventsHaveKeyEventsRule.ts +++ b/src/templateClickEventsHaveKeyEventsRule.ts @@ -1,8 +1,14 @@ import { ElementAst } from '@angular/compiler'; +import { dom } from 'aria-query'; import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib'; import { SourceFile } from 'typescript/lib/typescript'; import { NgWalker } from './angular/ngWalker'; import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor'; +import { isInteractiveElement } from './util/isInteractiveElement'; +import { isPresentationRole } from './util/isPresentationRole'; +import { isHiddenFromScreenReader } from './util/isHiddenFromScreenReader'; + +const domElements = new Set(dom.keys()); export class Rule extends Rules.AbstractRule { static readonly metadata: IRuleMetadata = { @@ -37,6 +43,21 @@ class TemplateClickEventsHaveKeyEventsVisitor extends BasicTemplateAstVisitor { if (!hasClick) { return; } + + if (!domElements.has(el.name)) { + // Do not test components, as we do not know what + // low-level DOM element this maps to. + return; + } + + if (isPresentationRole(el) || isHiddenFromScreenReader(el)) { + return; + } + + if (isInteractiveElement(el)) { + return; + } + const hasKeyEvent = el.outputs.some(output => output.name === 'keyup' || output.name === 'keydown' || output.name === 'keypress'); if (hasKeyEvent) { diff --git a/src/util/attributesComparator.ts b/src/util/attributesComparator.ts new file mode 100644 index 000000000..6e6037c32 --- /dev/null +++ b/src/util/attributesComparator.ts @@ -0,0 +1,24 @@ +import { ElementAst, AttrAst, BoundElementPropertyAst } from '@angular/compiler'; +import { getAttributeValue } from './getAttributeValue'; +import { getLiteralValue } from './getLiteralValue'; + +export function attributesComparator(baseAttributes: any = [], el: ElementAst): boolean { + const attributes: Array = [...el.attrs, ...el.inputs]; + return baseAttributes.every( + (baseAttr): boolean => + attributes.some( + (attribute): boolean => { + if (el.name === 'a' && attribute.name === 'routerLink') { + return true; + } + if (baseAttr.name !== attribute.name) { + return false; + } + if (baseAttr.value && baseAttr.value !== getLiteralValue(getAttributeValue(el, baseAttr.name))) { + return false; + } + return true; + } + ) + ); +} diff --git a/src/util/getAttributeValue.ts b/src/util/getAttributeValue.ts index fdfd2c9ac..296837114 100644 --- a/src/util/getAttributeValue.ts +++ b/src/util/getAttributeValue.ts @@ -1,4 +1,5 @@ -import { ElementAst } from '@angular/compiler'; +import { ElementAst, ASTWithSource, LiteralPrimitive } from '@angular/compiler'; +import { PROPERTY } from './isHiddenFromScreenReader'; export const getAttributeValue = (element: ElementAst, property: string) => { const attr = element.attrs.find(attr => attr.name === property); @@ -6,7 +7,13 @@ export const getAttributeValue = (element: ElementAst, property: string) => { if (attr) { return attr.value; } - if (input) { - return (input.value).ast.value; + if (!input || !(input.value instanceof ASTWithSource)) { + return undefined; } + + if (input.value.ast instanceof LiteralPrimitive) { + return input.value.ast.value; + } + + return PROPERTY; }; diff --git a/src/util/getLiteralValue.ts b/src/util/getLiteralValue.ts new file mode 100644 index 000000000..1653af21b --- /dev/null +++ b/src/util/getLiteralValue.ts @@ -0,0 +1,8 @@ +export const getLiteralValue = value => { + if (value === 'true') { + return true; + } else if (value === 'false') { + return false; + } + return value; +}; diff --git a/src/util/isHiddenFromScreenReader.ts b/src/util/isHiddenFromScreenReader.ts new file mode 100644 index 000000000..347402caa --- /dev/null +++ b/src/util/isHiddenFromScreenReader.ts @@ -0,0 +1,24 @@ +import { ElementAst } from '@angular/compiler'; +import { getAttributeValue } from './getAttributeValue'; +import { getLiteralValue } from './getLiteralValue'; + +export const PROPERTY = ['PROPERTY']; +/** + * Returns boolean indicating that the aria-hidden prop + * is present or the value is true. Will also return true if + * there is an input with type='hidden'. + * + *
is equivalent to the DOM as