Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Add react-a11y-aria-unsupported-elements rule #243

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Rule Name | Description | Since
`prefer-type-cast` | Prefer the tradition type casts instead of the new 'as-cast' syntax. For example, prefer '<string>myVariable' instead of 'myVariable as string'. Rule ignores any file ending in .tsx. If you prefer the opposite and want to see the 'as type' casts, then enable the tslint rule named 'no-angle-bracket-type-assertion'| 2.0.4
`promise-must-complete` | When a Promise instance is created, then either the reject() or resolve() parameter must be called on it within all code branches in the scope. For more examples see the [feature request](https://github.com/Microsoft/tslint-microsoft-contrib/issues/34). | 1.0
`react-iframe-missing-sandbox` | React iframes must specify a sandbox attribute. If specified as an empty string, this attribute enables extra restrictions on the content that can appear in the inline frame. The value of the attribute can either be an empty string (all the restrictions are applied), or a space-separated list of tokens that lift particular restrictions. You many not use both allow-scripts and allow-same-origin at the same time, as that allows the embedded document to programmatically remove the sandbox attribute in some scenarios. | 2.0.10
`react-a11y-aria-unsupported-elements` | For accessibility of your website, enforce that elements that do not support ARIA roles, states, and properties do not have those attributes. | 2.0.11
`react-a11y-img-has-alt` | Enforce that an `img` element contains the `alt` attribute or `role='presentation'` for decorative image. All images must have `alt` text to convey their purpose and meaning to **screen reader users**. Besides, the `alt` attribute specifies an alternate text for an image, if the image cannot be displayed. This rule accepts as a parameter a string array for tag names other than img to also check. For example, if you use a custom tag named 'Image' then configure the rule with: `[true, ['Image']]`<br/>References:<br/>[Web Content Accessibility Guidelines 1.0](https://www.w3.org/TR/WCAG10/wai-pageauth.html#tech-text-equivalent)<br/>[ARIA Presentation Role](https://www.w3.org/TR/wai-aria/roles#presentation) | 2.0.11
`react-a11y-lang` | For accessibility of your website, HTML elements must have a lang attribute and the attribute must be a valid language code.<br/>References:<br/>* [H58: Using language attributes to identify changes in the human language](https://www.w3.org/TR/WCAG20-TECHS/H58.html)<br/>* [lang attribute must have a valid value](https://dequeuniversity.com/rules/axe/1.1/valid-lang)<br/>[List of ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) | 2.0.11
`react-a11y-anchors` | For accessibility of your website, anchor element link text should be at least 4 characters long. Links with the same HREF should have the same link text. Links that point to different HREFs should have different link text. Links with images and text content, the alt attribute should be unique to the text content or empty. An an anchor element's href prop value must not be just #. <br/>References:<br/>[WCAG Rule 38: Link text should be as least four 4 characters long](http://oaa-accessibility.org/wcag20/rule/38/)<br/>[WCAG Rule 39: Links with the same HREF should have the same link text](http://oaa-accessibility.org/wcag20/rule/39/)<br/>[WCAG Rule 41: Links that point to different HREFs should have different link text](http://oaa-accessibility.org/wcag20/rule/41/)<br/>[WCAG Rule 43: Links with images and text content, the alt attribute should be unique to the text content or empty](http://oaa-accessibility.org/wcag20/rule/43/)<br/> | 2.0.11
Expand Down
81 changes: 81 additions & 0 deletions src/reactA11yAriaUnsupportedElementsRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Enforce that elements that do not support ARIA roles, states, and properties do not have those attributes.
*/
import * as ts from 'typescript';
import * as Lint from 'tslint/lib/lint';

import { ExtendedMetadata } from './utils/ExtendedMetadata';
import { getJsxAttributesFromJsxElement} from './utils/JsxAttribute';
import { IDom } from './utils/attributes/IDom';
import { IAria } from './utils/attributes/IAria';

// tslint:disable:no-require-imports no-var-requires
const DOM_SCHEMA: IDom[] = require('./utils/attributes/domSchema.json');
const ARIA_SCHEMA: IAria[] = require('./utils/attributes/ariaSchema.json');
// tslint:enable:no-require-imports no-var-requires

export function getFailureString(tagName: string, ariaAttributeNames: string[]): string {
return `This element ${tagName} does not support ARIA roles, states and properties. `
+ `Try removing attribute(s): ${ariaAttributeNames.join(', ')}.`;
}

export class Rule extends Lint.Rules.AbstractRule {
public static metadata: ExtendedMetadata = {
ruleName: 'react-a11y-aria-unsupported-elements',
type: 'maintainability',
description: 'Enforce that elements that do not support ARIA roles, states, and properties do not have those attributes.',
options: null,
issueClass: 'Non-SDL',
issueType: 'Warning',
severity: 'Important',
level: 'Opportunity for Excellence',
group: 'Accessibility'
};

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return sourceFile.languageVariant === ts.LanguageVariant.JSX
? this.applyWithWalker(new ReactA11yAriaUnsupportedElementsWalker(sourceFile, this.getOptions()))
: [];
}
}

class ReactA11yAriaUnsupportedElementsWalker extends Lint.RuleWalker {
public visitJsxElement(node: ts.JsxElement): void {
this.checkJsxOpeningElement(node.openingElement);
super.visitJsxElement(node);
}

public visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void {
this.checkJsxOpeningElement(node);
super.visitJsxSelfClosingElement(node);
}

private checkJsxOpeningElement(node: ts.JsxOpeningElement): void {
const tagName: string = node.tagName.getText();

if (!DOM_SCHEMA[tagName]) {
return;
}

const unsupportedAria: boolean = DOM_SCHEMA[tagName].unsupportedAria !== undefined
? DOM_SCHEMA[tagName].unsupportedAria
: false;

if (!unsupportedAria) {
return;
}

const checkAttributeNames: string[] = Object.keys(ARIA_SCHEMA).concat('role');
const attributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement(node);
const invalidAttributeNames: string[] =
checkAttributeNames.filter((attributeName: string): boolean => !!attributes[attributeName]);

if (invalidAttributeNames.length > 0) {
this.addFailure(this.createFailure(
node.getStart(),
node.getWidth(),
getFailureString(tagName, invalidAttributeNames)
));
}
}
}
6 changes: 6 additions & 0 deletions src/utils/attributes/IDom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Interface of dom
*/
export interface IDom {
unsupportedAria: boolean;
}
145 changes: 145 additions & 0 deletions src/utils/attributes/domSchema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
{
"a": {},
"abbr": {},
"address": {},
"area": {},
"article": {},
"aside": {},
"audio": {},
"b": {},
"base": {
"unsupportedAria": true
},
"bdi": {},
"bdo": {},
"big": {},
"blockquote": {},
"body": {},
"br": {},
"button": {},
"canvas": {},
"caption": {},
"cite": {},
"code": {},
"col": {
"unsupportedAria": true
},
"colgroup": {
"unsupportedAria": true
},
"data": {},
"datalist": {},
"dd": {},
"del": {},
"details": {},
"dfn": {},
"dialog": {},
"div": {},
"dl": {},
"dt": {},
"em": {},
"embed": {},
"fieldset": {},
"figcaption": {},
"figure": {},
"footer": {},
"form": {},
"h1": {},
"h2": {},
"h3": {},
"h4": {},
"h5": {},
"h6": {},
"head": {
"unsupportedAria": true
},
"header": {},
"hgroup": {},
"hr": {},
"html": {
"unsupportedAria": true
},
"i": {},
"iframe": {},
"img": {},
"input": {},
"ins": {},
"kbd": {},
"keygen": {},
"label": {},
"legend": {},
"li": {},
"link": {
"unsupportedAria": true
},
"main": {},
"map": {},
"mark": {},
"menu": {},
"menuitem": {},
"meta": {
"unsupportedAria": true
},
"meter": {},
"nav": {},
"noscript": {
"unsupportedAria": true
},
"object": {},
"ol": {},
"optgroup": {},
"option": {},
"output": {},
"p": {},
"param": {
"unsupportedAria": true
},
"picture": {
"unsupportedAria": true
},
"pre": {},
"progress": {},
"q": {},
"rp": {},
"rt": {},
"ruby": {},
"s": {},
"samp": {},
"script": {
"unsupportedAria": true
},
"section": {},
"select": {},
"small": {},
"source": {
"unsupportedAria": true
},
"span": {},
"strong": {},
"style": {
"unsupportedAria": true
},
"sub": {},
"summary": {},
"sup": {},
"table": {},
"tbody": {},
"td": {},
"textarea": {},
"tfoot": {},
"th": {},
"thead": {},
"time": {},
"title": {
"unsupportedAria": true
},
"tr": {},
"track": {
"unsupportedAria": true
},
"u": {},
"ul": {},
"var": {},
"video": {},
"wbr": {}
}
56 changes: 56 additions & 0 deletions tests/ReactA11yAriaUnsupportedElementsRuleTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { TestHelper } from './TestHelper';
import { getFailureString } from '../src/reactA11yAriaUnsupportedElementsRule';

/**
* Unit test for react-a11y-aria-unsupported-elements rule.
*/
describe('reactA11yAriaUnsupportedElementsRule', () => {
const ruleName: string = 'react-a11y-aria-unsupported-elements';

it('should pass when tag name is not dom elements', (): void => {
const script: string = `
import React = require('react);

const a = <DIV aria-label/>;
const b = <DIV role></DIV>;
`;
TestHelper.assertNoViolation(ruleName, script);
});

it('should pass when tag name is supported aria element', (): void => {
const script: string = `
import React = require('react);

const a = <div />;
const b = <div aria-label role { ...this.props }></div>;
`;
TestHelper.assertNoViolation(ruleName, script);
});

it('should fail when unsupported aria elements have aria-* or role attributes', (): void => {
const script: string = `
import React = require('react');

const a = <base aria-label role { ...this.props }></base>;
const b = <base aria-label role { ...this.props } />;
`;
TestHelper.assertViolations(
ruleName,
script,
[
{
name: 'file.tsx',
ruleName: ruleName,
startPosition: { character: 23, line: 4 },
failure: getFailureString('base', ['aria-label', 'role'])
},
{
name: 'file.tsx',
ruleName: ruleName,
startPosition: { character: 23, line: 5 },
failure: getFailureString('base', ['aria-label', 'role'])
}
]
);
});
});
1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"rules": {
"react-a11y-aria-unsupported-elements": true,
"react-a11y-img-has-alt": true,
"react-a11y-props": true,
"react-a11y-role-has-required-aria-props": true,
Expand Down