This repository has been archived by the owner on Jul 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 199
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rule): Add new rule - react-a11y-media-captions (#850)
* add new reactA11yMediaCaptionsRule * add react-a11y-media-caption to readme * add unit tests fix behavior of validateMediaType * fix according to comments add unit tests * modify get JsxExpression text to look only for string literals * modify to check string literal inside JsxExpression add spread attributes logic to track elements * replace getAttributeText with getStringLiteral * add uni test to indicate no variable as expression * git l
- Loading branch information
1 parent
34f3347
commit 8f86920
Showing
6 changed files
with
207 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import * as ts from 'typescript'; | ||
import * as Lint from 'tslint'; | ||
import * as tsutils from 'tsutils'; | ||
|
||
import { ExtendedMetadata } from './utils/ExtendedMetadata'; | ||
import { getAllAttributesFromJsxElement, getStringLiteral } from './utils/JsxAttribute'; | ||
|
||
const VIDEO_ELEMENT_NAME: string = 'video'; | ||
const AUDIO_ELEMENT_NAME: string = 'audio'; | ||
const TRACK_ELEMENT_NAME: string = 'track'; | ||
const KIND_ATTRIBUTE_NAME: string = 'kind'; | ||
const CAPTIONS_KIND_NAME: string = 'captions'; | ||
const DESCRIPTION_KIND_NAME: string = 'description'; | ||
const NO_CAPTIONS_ERROR_MESSAGE: string = 'Video and audio elements must have a captions track'; | ||
const NO_AUDIO_DESCRIPTION_MESSAGE: string = 'Video elements must have an audio description track'; | ||
|
||
export class Rule extends Lint.Rules.AbstractRule { | ||
public static metadata: ExtendedMetadata = { | ||
ruleName: 'react-a11y-media-captions', | ||
type: 'functionality', | ||
description: 'Enforce that video and audio elements have captions and descriptions.', | ||
options: null, // tslint:disable-line:no-null-keyword | ||
optionsDescription: '', | ||
typescriptOnly: false, | ||
issueClass: 'Non-SDL', | ||
issueType: 'Error', | ||
severity: 'Important', | ||
level: 'Opportunity for Excellence', | ||
group: 'Accessibility' | ||
}; | ||
|
||
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { | ||
return sourceFile.languageVariant === ts.LanguageVariant.JSX ? this.applyWithFunction(sourceFile, walk) : []; | ||
} | ||
} | ||
|
||
function walk(ctx: Lint.WalkContext<void>) { | ||
function cb(node: ts.Node): void { | ||
if (tsutils.isJsxSelfClosingElement(node) && (isVideoElement(node.tagName) || isAudioElement(node.tagName))) { | ||
ctx.addFailureAtNode(node, NO_CAPTIONS_ERROR_MESSAGE); | ||
} | ||
if (tsutils.isJsxElement(node) && (isVideoElement(node.openingElement.tagName) || isAudioElement(node.openingElement.tagName))) { | ||
if (!containsTrackElementWithSpreadAttributes(node.openingElement)) { | ||
validateMediaType(node.openingElement, ctx); | ||
return; | ||
} | ||
} | ||
return ts.forEachChild(node, cb); | ||
} | ||
return ts.forEachChild(ctx.sourceFile, cb); | ||
} | ||
|
||
function isVideoElement(tagName: ts.JsxTagNameExpression): boolean { | ||
const elementName = tagName.getText(); | ||
return elementName === VIDEO_ELEMENT_NAME; | ||
} | ||
|
||
function isAudioElement(tagName: ts.JsxTagNameExpression): boolean { | ||
const elementName = tagName.getText(); | ||
return elementName === AUDIO_ELEMENT_NAME; | ||
} | ||
|
||
function hasSpreadAttributes(nodeArray: ts.NodeArray<ts.JsxAttributeLike> | undefined): boolean { | ||
return !!(nodeArray && nodeArray.find(node => tsutils.isJsxSpreadAttribute(node))); | ||
} | ||
|
||
function isTrackElementNode(node: ts.Node): boolean { | ||
return ( | ||
(tsutils.isJsxElement(node) && node.openingElement.tagName.getText() === TRACK_ELEMENT_NAME) || | ||
(tsutils.isJsxSelfClosingElement(node) && node.tagName.getText() === TRACK_ELEMENT_NAME) | ||
); | ||
} | ||
|
||
function containsTrackElementWithSpreadAttributes(node: ts.JsxOpeningElement): boolean { | ||
function cb(childNode: ts.Node): boolean { | ||
if (isTrackElementNode(childNode)) { | ||
const attributes = getAllAttributesFromJsxElement(childNode); | ||
return hasSpreadAttributes(attributes); | ||
} | ||
return false; | ||
} | ||
return !!ts.forEachChild(node.parent, cb); | ||
} | ||
|
||
function validateMediaType(node: ts.JsxOpeningElement, ctx: Lint.WalkContext<void>): void { | ||
const validateDescription = isVideoElement(node.tagName); | ||
let foundCaptions = false; | ||
let foundDescription = false; | ||
function cb(childNode: ts.Node): void { | ||
if (isTrackElementNode(childNode)) { | ||
const attributes = getAllAttributesFromJsxElement(childNode); | ||
if (attributes) { | ||
const kindAttribute = <ts.JsxAttribute>( | ||
attributes.find(att => tsutils.isJsxAttribute(att) && att.name.getText() === KIND_ATTRIBUTE_NAME) | ||
); | ||
if (!foundCaptions) { | ||
foundCaptions = !!(kindAttribute && getStringLiteral(kindAttribute) === CAPTIONS_KIND_NAME); | ||
} | ||
if (!foundDescription && validateDescription) { | ||
foundDescription = !!(kindAttribute && getStringLiteral(kindAttribute) === DESCRIPTION_KIND_NAME); | ||
} | ||
} | ||
} | ||
return ts.forEachChild(childNode, cb); | ||
} | ||
ts.forEachChild(node.parent, cb); | ||
|
||
if (!foundCaptions) { | ||
ctx.addFailureAtNode(node, NO_CAPTIONS_ERROR_MESSAGE); | ||
} | ||
if (!foundDescription && validateDescription) { | ||
ctx.addFailureAtNode(node, NO_AUDIO_DESCRIPTION_MESSAGE); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
|
||
const SomeComponent = () => | ||
<video width="320" height="240"> | ||
<source src="sitting_and_smiling_234.mp4" type="video/mp4"/> | ||
<track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English"/> | ||
<track src="sitting_and_smiling_234.vtt" kind="captions" srclang="en" label="English"/> | ||
<track src="myvideo_en.vtt" kind="description" srclang="en" label="English"/> | ||
</video>; | ||
|
||
const SomeComponent = () => | ||
<video width="320" height="240"> | ||
<source src="sitting_and_smiling_232.mp4" type="video/mp4"/> | ||
<track src="subtitles_en.vtt" kind={'subtitles'} srclang="en" label="English"/> | ||
<track src="sitting_and_smiling_232.vtt" kind={'captions'} srclang="en" label="English"/> | ||
<track src="myvideo_en.vtt" kind={'description'} srclang="en" label="English"/> | ||
</video>; | ||
|
||
const captionsKind = 'captions'; | ||
const SomeComponent = () => | ||
<audio> | ||
~~~~~~~[Video and audio elements must have a captions track] | ||
<source src="beck_loser.mp3" type="video/mp4"/> | ||
<track src="beck_loser.vtt" kind={ captionsKind } srclang="en" label="English"/> | ||
</audio>; | ||
|
||
const SomeComponent = () => | ||
<audio> | ||
<source src="beck_loser.mp3" type="video/mp4"/> | ||
<track src="beck_loser.vtt" kind="captions" srclang="en" label="English"/> | ||
</audio>; | ||
|
||
const SomeComponent = () => | ||
<video width="320" height="240"> | ||
<source src="sitting_and_smiling_260.mp4" type="video/mp4"/> | ||
<track {...props} /> | ||
<track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English"/> | ||
</video>; | ||
|
||
const SomeComponent = () => | ||
|
||
<video width="320" height="240"> | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Video elements must have an audio description track] | ||
<source src="sitting_and_smiling_230.mp4" type="video/mp4"/> | ||
<track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English"/> | ||
<track src="sitting_and_smiling_230.vtt" kind="captions" srclang="en" label="English"/> | ||
</video>; | ||
|
||
const SomeComponent = () => | ||
<video width="320" height="240"> | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Video and audio elements must have a captions track] | ||
<source src="sitting_and_smiling_236.mp4" type="video/mp4"/> | ||
<track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English"/> | ||
<track src="sitting_and_smiling_236.vtt" kind="description" srclang="en" label="English"/> | ||
</video>; | ||
|
||
const SomeComponent = () => | ||
<video width="320" height="240"> | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Video and audio elements must have a captions track] | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Video elements must have an audio description track] | ||
<source src="sitting_and_smiling_224.mp4" type="video/mp4"/> | ||
<track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English"/> | ||
</video>; | ||
|
||
const SomeComponent = () => | ||
<audio> | ||
~~~~~~~ [Video and audio elements must have a captions track] | ||
<source src="beck_loser.mp3" type="video/mp4"/> | ||
</audio>;s | ||
|
||
const SomeComponent = () => | ||
<audio source src="rami-fortis-april-fools.mp3" type="mp3" /> | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Video and audio elements must have a captions track] | ||
|
||
const SomeComponent = () => | ||
<video source src="sitting_and_smiling_242.mp4" type="video/mp4" /> | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Video and audio elements must have a captions track] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"rules": { | ||
"react-a11y-media-captions": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters