Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add getting accessible name API to the element #395

Merged
merged 3 commits into from
Mar 6, 2022
Merged
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 packages/@markuplint/ml-core/markuplint-recommended.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"no-refer-to-non-existent-id": true,
"no-use-event-handler-attr": false,
"permitted-contents": true,
"require-accessible-name": true,
"required-attr": true,
"required-element": false,
"required-h1": true,
Expand Down
1 change: 1 addition & 0 deletions packages/@markuplint/ml-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@markuplint/ml-spec": "2.0.0",
"@markuplint/parser-utils": "2.0.0",
"debug": "^4.3.3",
"dom-accessibility-api": "^0.5.13",
"postcss-selector-parser": "^6.0.9",
"tslib": "^2.3.1"
},
Expand Down
27 changes: 27 additions & 0 deletions packages/@markuplint/ml-core/src/ml-dom/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ export default class MLDOMDocument<T extends RuleConfigValue, O = null> {
*/
readonly nodeStore = new NodeStore();

/**
* Window object for calling the `getComputedStyle` and the `getPropertyValue` that are needed by **Accessible Name and Description Computation**.
* But it always returns the empty object.
* (It may improve to possible to compute the name from the `style` attribute in the future.)
*/
readonly defaultView = {
getComputedStyle(_el: MLDOMElement<any, any>) {
return {
getPropertyValue(_propName: string) {
return {};
},
};
},
};

#filename?: string;

/**
Expand Down Expand Up @@ -189,6 +204,18 @@ export default class MLDOMDocument<T extends RuleConfigValue, O = null> {
);
}

querySelectorAll(query: string) {
return this.matchNodes(query);
}

querySelector(query: string) {
return this.querySelectorAll(query)[0] || null;
}

getElementById(id: string) {
return this.querySelector(`#${id}`);
}

toString() {
const html: string[] = [];
for (const node of this.nodeList) {
Expand Down
85 changes: 85 additions & 0 deletions packages/@markuplint/ml-core/src/ml-dom/helper/accname.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { MLDOMElement } from '../tokens';

import { parse } from '@markuplint/html-parser';

import { Document } from '../';
import { convertRuleset } from '../../';
import { dummySchemas } from '../../test';

import { getAccname } from './accname';
import { createNode } from './create-node';

function c(sourceCode: string) {
const ast = parse(sourceCode);
const astNode = ast.nodeList[0];
const ruleset = convertRuleset({});
const document = new Document(ast, ruleset, dummySchemas());
const node = createNode(astNode, document);
if (node.type === 'Element') {
return node;
}
throw new Error();
}

test('Get accessible name', () => {
expect(getAccname(c('<button>label</button>'))).toBe('label');
expect(getAccname(c('<div>text</div>'))).toBe('');
expect(getAccname(c('<div aria-label="label">text</div>'))).toBe('label');
expect(getAccname(c('<span aria-label="label">text</span>'))).toBe('label');
expect(getAccname(c('<img alt="alterative-text" />'))).toBe('alterative-text');
expect(getAccname(c('<img title="title" />'))).toBe('title');
expect(
getAccname(c('<div><label for="a">label</label><input id="a" /></div>').children[1] as MLDOMElement<any, any>),
).toBe('label');
});

test('Invisible element', () => {
expect(getAccname(c('<button>label</button>'))).toBe('label');
expect(getAccname(c('<button aria-disabled="true">label</button>'))).toBe('label');
expect(getAccname(c('<button aria-hidden="true">label</button>'))).toBe('');
expect(getAccname(c('<button hidden>label</button>'))).toBe('');
expect(getAccname(c('<button style="display: none">label</button>'))).toBe('label'); // Unsupport the style attribute yet.
});

/**
* @see https://www.w3.org/TR/accname-1.1/#ex-1-example-1-element1-id-el1-aria-labelledby-el3-element2-id-el2-aria-labelledby-el1-element3-id-el3-hello-element3
*/
test('accname-1.1 Expample 1', () => {
const complex = c(`<div>
<input id="el1" aria-labelledby="el3" />
<input id="el2" aria-labelledby="el1" />
<h1 id="el3"> hello </h1>
</div>`);
expect(getAccname(complex.children[0] as MLDOMElement<any, any>)).toBe('hello');
expect(getAccname(complex.children[1] as MLDOMElement<any, any>)).toBe('');
expect(getAccname(complex.children[2] as MLDOMElement<any, any>)).toBe('hello');
});

/**
* https://www.w3.org/TR/accname-1.1/#ex-2-example-2-h1-files-h1-ul-li-a-id-file_row1-href-files-documentation-pdf-documentation-pdf-a-span-role-button-tabindex-0-id-del_row1-aria-label-delete-aria-labelledby-del_row1-file_row1-span-li-li-a-id-file_row2-href-files-holidayletter-pdf-holidayletter-pdf-a-span-role-button-tabindex-0-id-del_row2-aria-label-delete-aria-labelledby-del_row2-file_row2-span-li-ul
*/
test('accname-1.1 Expample 2', () => {
const complex = c(`<ul>
<li>
<a id="file_row1" href="./files/Documentation.pdf">Documentation.pdf</a>
<span role="button" tabindex="0" id="del_row1" aria-label="Delete" aria-labelledby="del_row1 file_row1"></span>
</li>
<li>
<a id="file_row2" href="./files/HolidayLetter.pdf">HolidayLetter.pdf</a>
<span role="button" tabindex="0" id="del_row2" aria-label="Delete" aria-labelledby="del_row2 file_row2"></span>
</li>
</ul>`);
const spans = complex.querySelectorAll('span');
expect(getAccname(spans[0] as MLDOMElement<any, any>)).toBe('Delete Documentation.pdf');
expect(getAccname(spans[1] as MLDOMElement<any, any>)).toBe('Delete HolidayLetter.pdf');
});

/**
* https://www.w3.org/TR/accname-1.1/#ex-3-example-3-div-role-checkbox-aria-checked-false-flash-the-screen-span-role-textbox-aria-multiline-false-5-span-times-div
*/
test('accname-1.1 Expample 3', () => {
const complex = c(
'<div role="checkbox" aria-checked="false">Flash the screen <span role="textbox" aria-multiline="false"> 5 </span> times</div>',
);
expect(getAccname(complex)).toBe('Flash the screen 5 times');
});
18 changes: 18 additions & 0 deletions packages/@markuplint/ml-core/src/ml-dom/helper/accname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type MLDOMAbstractElement from '../tokens/abstract-element';

import { computeAccessibleName } from 'dom-accessibility-api';

import { log } from '../../debug';

const accnameLog = log.extend('accname');

export function getAccname(el: MLDOMAbstractElement<any, any>) {
try {
// @ts-ignore
const name = computeAccessibleName(el);
return name;
} catch (err) {
accnameLog('Error: %O', err);
return '';
}
}
2 changes: 1 addition & 1 deletion packages/@markuplint/ml-core/src/ml-dom/helper/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function attributesToDebugMaps(attributes: (MLDOMAttribute | MLDOMPreprocessorSp
r.push(` ${tokenDebug(n.equal, 'equal')}`);
r.push(` ${tokenDebug(n.spacesAfterEqual, 'aE')}`);
r.push(` ${tokenDebug(n.startQuote, 'sQ')}`);
r.push(` ${tokenDebug(n.value, 'value')}`);
r.push(` ${tokenDebug(n.valueNode, 'value')}`);
r.push(` ${tokenDebug(n.endQuote, 'eQ')}`);
r.push(` isDirective: ${!!n.isDirective}`);
r.push(` isDynamicValue: ${!!n.isDynamicValue}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ContentModel } from '@markuplint/ml-spec';
import { getNS } from '@markuplint/ml-spec';

import { createSelector } from '../helper';
import { getAccname } from '../helper/accname';
import { syncWalk } from '../helper/walkers';

import MLDOMNode from './node';
Expand All @@ -17,6 +18,7 @@ export default abstract class MLDOMAbstractElement<
O = null,
E extends MLASTElement | MLASTOmittedElement = MLASTElement | MLASTOmittedElement,
> extends MLDOMNode<T, O, E> {
readonly nodeType = 1;
readonly nodeName: string;
readonly attributes: (MLDOMAttribute | MLDOMPreprocessorSpecificAttribute)[] = [];
readonly hasSpreadAttr: boolean = false;
Expand All @@ -42,6 +44,10 @@ export default abstract class MLDOMAbstractElement<
this.isInFragmentDocument = document.isFragment;
}

get tagName() {
return this.nodeName;
}

get childNodes(): AnonymousNode<T, O>[] {
const astChildren = this._astToken.childNodes || [];
return astChildren.map(node => this.nodeStore.getNode<typeof node, T, O>(node));
Expand Down Expand Up @@ -107,7 +113,7 @@ export default abstract class MLDOMAbstractElement<
const classList: string[] = [];
const classAttrs = this.getAttributeToken('class');
for (const classAttr of classAttrs) {
const value = classAttr.attrType === 'html-attr' ? classAttr.value.raw : classAttr.potentialValue;
const value = classAttr.attrType === 'html-attr' ? classAttr.value : classAttr.potentialValue;
classList.push(
...value
.split(/\s+/g)
Expand All @@ -126,6 +132,10 @@ export default abstract class MLDOMAbstractElement<
return this.#fixedNodeName;
}

get textContent(): string {
return this.childNodes.map(child => child.textContent || '').join('');
}

querySelectorAll(selector: string) {
const matchedNodes: (MLDOMElement<T, O> | MLDOMText<T, O>)[] = [];
const selecor = createSelector(selector);
Expand Down Expand Up @@ -164,12 +174,16 @@ export default abstract class MLDOMAbstractElement<
return attrs;
}

getAttributeNode(attrName: string) {
return this.getAttributeToken(attrName)[0] || null;
}

getAttribute(attrName: string) {
attrName = attrName.toLowerCase();
for (const attr of this.attributes) {
if (attr.potentialName === attrName) {
if (attr.attrType === 'html-attr') {
return attr.value ? attr.value.raw : null;
return attr.value ? attr.value : null;
} else {
return attr.potentialValue;
}
Expand Down Expand Up @@ -234,6 +248,10 @@ export default abstract class MLDOMAbstractElement<
return false;
}

getAccessibleName(): string {
return getAccname(this);
}

toNormalizeString(): string {
if (this.#normalizedString) {
return this.#normalizedString;
Expand Down
22 changes: 14 additions & 8 deletions packages/@markuplint/ml-core/src/ml-dom/tokens/attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import type { MLASTHTMLAttr, MLToken } from '@markuplint/ml-ast';
import MLDOMToken from './token';

export default class MLDOMAttribute extends MLDOMToken<MLASTHTMLAttr> {
readonly nodeType = 2;
readonly attrType = 'html-attr';
readonly name: MLDOMToken<MLToken>;
readonly spacesBeforeName: MLDOMToken<MLToken>;
readonly spacesBeforeEqual: MLDOMToken<MLToken>;
readonly equal: MLDOMToken<MLToken>;
readonly spacesAfterEqual: MLDOMToken<MLToken>;
readonly startQuote: MLDOMToken<MLToken>;
readonly value: MLDOMToken<MLToken>;
readonly valueNode: MLDOMToken<MLToken>;
readonly endQuote: MLDOMToken<MLToken>;
readonly isDynamicValue?: true;
readonly isDirective?: true;
Expand All @@ -27,7 +28,7 @@ export default class MLDOMAttribute extends MLDOMToken<MLASTHTMLAttr> {
this.equal = new MLDOMToken(this._astToken.equal);
this.spacesAfterEqual = new MLDOMToken(this._astToken.spacesAfterEqual);
this.startQuote = new MLDOMToken(this._astToken.startQuote);
this.value = new MLDOMToken(this._astToken.value);
this.valueNode = new MLDOMToken(this._astToken.value);
this.endQuote = new MLDOMToken(this._astToken.endQuote);
this.isDynamicValue = astToken.isDynamicValue;
this.isDirective = astToken.isDirective;
Expand All @@ -43,7 +44,7 @@ export default class MLDOMAttribute extends MLDOMToken<MLASTHTMLAttr> {
raw.push(this.equal.raw);
raw.push(this.spacesAfterEqual.raw);
raw.push(this.startQuote.raw);
raw.push(this.value.raw);
raw.push(this.valueNode.raw);
raw.push(this.endQuote.raw);
}
return raw.join('');
Expand Down Expand Up @@ -73,6 +74,11 @@ export default class MLDOMAttribute extends MLDOMToken<MLASTHTMLAttr> {
return this.endQuote.endCol;
}

get value() {
const value = this.getValue();
return value.raw;
}

getName() {
return {
line: this.name.startLine,
Expand All @@ -84,10 +90,10 @@ export default class MLDOMAttribute extends MLDOMToken<MLASTHTMLAttr> {

getValue() {
return {
line: this.value.startLine,
col: this.value.startCol,
potential: this.value.raw,
raw: this.value.raw,
line: this.valueNode.startLine,
col: this.valueNode.startCol,
potential: this.valueNode.raw,
raw: this.valueNode.raw,
};
}

Expand All @@ -100,7 +106,7 @@ export default class MLDOMAttribute extends MLDOMToken<MLASTHTMLAttr> {
this.name.originRaw +
this.equal.originRaw +
this.startQuote.originRaw +
this.value.originRaw +
this.valueNode.originRaw +
this.endQuote.originRaw
);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/@markuplint/ml-core/src/ml-dom/tokens/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@ export default class MLDOMComment<T extends RuleConfigValue, O = null>
extends MLDOMNode<T, O, MLASTComment>
implements IMLDOMComment
{
readonly nodeType = 8;
readonly type = 'Comment';

get textContent() {
return this.raw;
}
}
5 changes: 5 additions & 0 deletions packages/@markuplint/ml-core/src/ml-dom/tokens/doctype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class MLDOMDoctype<T extends RuleConfigValue, O = null>
extends MLDOMNode<T, O, MLASTNode>
implements IMLDOMDoctype
{
readonly nodeType = 10;
readonly type = 'Doctype';

#name: string;
Expand All @@ -33,4 +34,8 @@ export default class MLDOMDoctype<T extends RuleConfigValue, O = null>
get systemId() {
return this.#systemId;
}

get textContent() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default class MLDOMElementCloseTag<T extends RuleConfigValue, O = null>
extends MLDOMNode<T, O, MLASTElementCloseTag>
implements IMLDOMElementCloseTag
{
readonly nodeType = 1;
readonly type = 'ElementCloseTag';
readonly nodeName: string;
readonly startTag: MLDOMElement<T, O>;
Expand Down
Loading