Skip to content

Commit

Permalink
Changes how we detect different node types to avoid depending on real…
Browse files Browse the repository at this point in the history
…m-gnostic prototypes.

(a.k.a avoid isntanceof <ElemType>, because <ElemType> is different in every document)

This allows accname to also handle nodes from a same-origin iframe.

Also provides a custom matchers for better errors in tests.

PiperOrigin-RevId: 338474185
  • Loading branch information
engelsdamien authored and AlexLloyd0 committed Oct 22, 2020
1 parent cbddce6 commit 466d7e9
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 51 deletions.
32 changes: 15 additions & 17 deletions src/lib/compute_text_alternative.ts
Expand Up @@ -11,6 +11,7 @@ import {getValueIfRange, getValueIfTextbox, TEXT_INPUT_TYPES} from './rule2E';
import {allowsNameFromContent, getCssContent, inlineTags} from './rule2F';
import {rule2G} from './rule2G';
import {rule2I} from './rule2I';
import {hasTagName, isHTMLElement, isSVGElement} from './util';

// taze: SVG types from //javascript/externs:svg_lib

Expand Down Expand Up @@ -87,7 +88,7 @@ export function computeTextAlternative(
* not satisfied.
*/
function rule2B(node: Node, context = getDefaultContext()): string|null {
if (!(node instanceof HTMLElement)) {
if (!isHTMLElement(node)) {
return null;
}

Expand Down Expand Up @@ -126,7 +127,7 @@ function rule2B(node: Node, context = getDefaultContext()): string|null {
* otherwise.
*/
function rule2C(node: Node, context = getDefaultContext()): string|null {
if (!(node instanceof HTMLElement)) {
if (!isHTMLElement(node)) {
return null;
}

Expand Down Expand Up @@ -192,15 +193,15 @@ function rule2D(node: Node, context: Context = getDefaultContext()): string|
null {
// <title>s define text alternatives for <svg>s
// See: https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd
if (node instanceof SVGElement) {
if (isSVGElement(node)) {
for (const child of node.childNodes) {
if (child instanceof SVGTitleElement) {
if (isSVGElement(child) && hasTagName(child, 'title')) {
return child.textContent;
}
}
}

if (!(node instanceof HTMLElement)) {
if (!isHTMLElement(node)) {
return null;
}

Expand All @@ -221,15 +222,15 @@ function rule2D(node: Node, context: Context = getDefaultContext()): string|

// If input is not <label>led, use native attribute /
// element information to compute a text alternative
if (node instanceof HTMLInputElement) {
if (hasTagName(node, 'input')) {
const inputTextAlternative = getUnlabelledInputText(node);
if (inputTextAlternative) {
return inputTextAlternative;
}
}

// <caption>s define text alternatives for <table>s
if (node instanceof HTMLTableElement) {
if (hasTagName(node, 'table')) {
const captionElem = node.querySelector('caption');
if (captionElem) {
context.inherited.partOfName = true;
Expand All @@ -241,8 +242,7 @@ function rule2D(node: Node, context: Context = getDefaultContext()): string|
}

// <figcaption>s define text alternatives for <figure>s
// Checking tagName due to lack of HTMLFigureElement
if (node.tagName === 'FIGURE') {
if (hasTagName(node, 'figure')) {
const figcaptionElem = node.querySelector('figcaption');
if (figcaptionElem) {
context.inherited.partOfName = true;
Expand All @@ -254,7 +254,7 @@ function rule2D(node: Node, context: Context = getDefaultContext()): string|
}

// <legend>s define text alternatives for <fieldset>s
if (node instanceof HTMLFieldSetElement) {
if (hasTagName(node, 'fieldset')) {
const legendElem = node.querySelector('legend');
if (legendElem) {
context.inherited.partOfName = true;
Expand All @@ -268,8 +268,7 @@ function rule2D(node: Node, context: Context = getDefaultContext()): string|
// alt attributes define text alternatives for
// <img>s and <area>s
const altAttribute = node.getAttribute('alt');
if (altAttribute &&
(node instanceof HTMLImageElement || node instanceof HTMLAreaElement)) {
if (altAttribute && (hasTagName(node, 'img') || hasTagName(node, 'area'))) {
return altAttribute;
}

Expand Down Expand Up @@ -298,8 +297,7 @@ function getValueIfComboboxOrListbox(

// Combobox role implied by input type and presence of list attribute,
// chosen option is the input value.
if (node instanceof HTMLInputElement &&
TEXT_INPUT_TYPES.includes(node.type) &&
if (hasTagName(node, 'input') && TEXT_INPUT_TYPES.includes(node.type) &&
(node.hasAttribute('list') || nodeRole === 'combobox')) {
return node.value;
}
Expand All @@ -314,7 +312,7 @@ function getValueIfComboboxOrListbox(
node.querySelectorAll('[role="option"][aria-selected="true"]'));
}
// A <select> element is always implicitly either a listbox or a combobox
else if (node instanceof HTMLSelectElement) {
else if (hasTagName(node, 'select')) {
selectedOptions = Array.from(node.selectedOptions);
}

Expand Down Expand Up @@ -346,7 +344,7 @@ function getValueIfComboboxOrListbox(
* otherwise.
*/
function rule2E(node: Node, context = getDefaultContext()): string|null {
if (!(node instanceof HTMLElement)) {
if (!isHTMLElement(node)) {
return null;
}

Expand Down Expand Up @@ -387,7 +385,7 @@ function rule2E(node: Node, context = getDefaultContext()): string|null {
* null otherwise.
*/
function rule2F(node: Node, context = getDefaultContext()): string|null {
if (!(node instanceof HTMLElement)) {
if (!isHTMLElement(node)) {
return null;
}

Expand Down
56 changes: 37 additions & 19 deletions src/lib/compute_text_alternative_test.ts
@@ -1,7 +1,12 @@
import {html, render} from 'lit-html';
import {customMatchers} from '../testing/custom_matchers';
import {computeTextAlternative, Rule} from './compute_text_alternative';

describe('The computeTextAlternative function', () => {
beforeAll(() => {
jasmine.addMatchers(customMatchers);
});

let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
Expand All @@ -25,7 +30,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('foo');
expect(computeTextAlternative(elem!).name).toBe('Hello world');
expect(elem!).toHaveTextAlernative('Hello world');
});

it('uses aria-labelledby references when computing \'name from content\' nodes',
Expand All @@ -39,7 +44,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('foo');
expect(computeTextAlternative(elem!).name).toBe('Hello world');
expect(elem!).toHaveTextAlernative('Hello world');
});

it('prefers input value to aria-label for embedded controls', () => {
Expand All @@ -53,7 +58,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('foo');
expect(computeTextAlternative(elem!).name).toBe('Say hello 5 times');
expect(elem!).toHaveTextAlernative('Say hello 5 times');
});

it('returns correct nodesUsed and rulesApplied sets for simple button input',
Expand Down Expand Up @@ -89,7 +94,7 @@ describe('The computeTextAlternative function', () => {
html` <input type="checkbox" title="Hello world" id="foo" /> `,
container);
const elem = document.getElementById('foo')!;
expect(computeTextAlternative(elem).name).toEqual('Hello world');
expect(elem).toHaveTextAlernative('Hello world');
});

it('check title attribute for name when subtree is hidden', () => {
Expand All @@ -101,7 +106,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('foo')!;
expect(computeTextAlternative(elem).name).toEqual('Hello world');
expect(elem).toHaveTextAlernative('Hello world');
});

it('allows name from content through elements with 0 height and width',
Expand All @@ -113,7 +118,7 @@ describe('The computeTextAlternative function', () => {
</div>`,
container);
const elem = document.getElementById('foo')!;
expect(computeTextAlternative(elem).name).toBe('Hello world');
expect(elem).toHaveTextAlernative('Hello world');
});

// http://wpt.live/accname/name_file-label-owned-combobox-manual.html
Expand All @@ -137,7 +142,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name).toBe('Flash the screen 1 times.');
expect(elem).toHaveTextAlernative('Flash the screen 1 times.');
});

// http://wpt.live/accname/name_file-label-owned-combobox-owned-listbox-manual.html
Expand All @@ -164,8 +169,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name)
.toBe('Flash the screen 2 times.');
expect(elem).toHaveTextAlernative('Flash the screen 2 times.');
});

// http://wpt.live/accname/name_checkbox-label-embedded-menu-manual.html
Expand All @@ -186,8 +190,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name)
.toBe('Flash the screen times.');
expect(elem).toHaveTextAlernative('Flash the screen times.');
});

// http://wpt.live/accname/name_test_case_733-manual.html
Expand All @@ -205,7 +208,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name).toBe('crazy');
expect(elem).toHaveTextAlernative('crazy');
});

// http://wpt.live/accname/name_from_content-manual.html
Expand Down Expand Up @@ -245,8 +248,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name)
.toBe('My name is Eli the weird. (QED) Where are my marbles?');
expect(elem).toHaveTextAlernative('My name is Eli the weird. (QED) Where are my marbles?');
});

// http://wpt.live/accname/name_file-label-inline-block-elements-manual.html
Expand All @@ -259,15 +261,15 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name).toBe('What is your name?');
expect(elem).toHaveTextAlernative('What is your name?');
});

it('doesnt add whitespace between inline elements (span in this case)',
() => {
render(
html` <h1 id="test"><span>E</span><span>E</span></h1> `, container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name).toBe('EE');
expect(elem).toHaveTextAlernative('EE');
});

it('does add whitespace if inline elements are on different lines', () => {
Expand All @@ -280,7 +282,7 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name).toBe('E E');
expect(elem).toHaveTextAlernative('E E');
});

// http://wpt.live/accname/name_test_case_608-manual.html
Expand All @@ -289,7 +291,7 @@ describe('The computeTextAlternative function', () => {
render(
html` <a href="test.html" id="test" title="Tag"></a> `, container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name).toBe('Tag');
expect(elem).toHaveTextAlernative('Tag');
});

// http://wpt.live/accname/name_test_case_619-manual.html
Expand All @@ -301,6 +303,22 @@ describe('The computeTextAlternative function', () => {
`,
container);
const elem = document.getElementById('test')!;
expect(computeTextAlternative(elem).name).toBe('foo bar baz');
expect(elem).toHaveTextAlernative('foo bar baz');
});

it('can handle elements in iframes', async () => {
render(html`<iframe srcdoc="<body></body>"></iframe>`, container);
const iframe = container.querySelector('iframe')!;
await iframeLoadedPromise(iframe);
const iframeDocument = iframe.contentWindow!.document;
render(html`<button>Inside iframe</button>`, iframeDocument.body);
const button = iframeDocument.querySelector('button')!;
expect(button).toHaveTextAlernative('Inside iframe');
});
});

async function iframeLoadedPromise(iframe: HTMLIFrameElement) {
return new Promise((resolve) => {
iframe.addEventListener('load', resolve, {once: true});
});
}
6 changes: 3 additions & 3 deletions src/lib/rule2A.ts
Expand Up @@ -6,7 +6,7 @@

import {Context, getDefaultContext} from './context';
import {closest} from './polyfill';
import {isFocusable} from './util';
import {isFocusable, isHTMLElement, hasTagName} from './util';

/**
* Looks at a variety of characteristics (CSS, size on screen, attributes)
Expand All @@ -16,12 +16,12 @@ import {isFocusable} from './util';
*/
// #SPEC_ASSUMPTION (A.2) : definition of 'hidden'
function isHidden(node: Node, context: Context): boolean {
if (!(node instanceof HTMLElement)) {
if (!isHTMLElement(node)) {
return false;
}

// #SPEC_ASSUMPTION (A.3) : options shouldn't be hidden
if (node instanceof HTMLOptionElement && closest(node, 'select') !== null &&
if (hasTagName(node, 'option') && closest(node, 'select') !== null &&
context.inherited.partOfName) {
return false;
}
Expand Down
14 changes: 8 additions & 6 deletions src/lib/rule2E.ts
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {hasTagName} from './util';

// Input types that imply role 'textbox' if list attribute is not present,
// and imply role 'combobox' if list attribute is present.
export const TEXT_INPUT_TYPES = ['email', 'tel', 'text', 'url', 'search'];
Expand All @@ -25,12 +27,12 @@ export function getValueIfTextbox(node: HTMLElement): string|null {
}

// type <textarea> implies role='textbox'
if (node instanceof HTMLTextAreaElement) {
if (hasTagName(node, 'textarea')) {
return node.value;
}

// <input> with certain type values & no list attribute implies role='textbox'
if (node instanceof HTMLInputElement &&
if (hasTagName(node, 'input') &&
TEXT_INPUT_TYPES.includes(node.type) && !node.hasAttribute('list')) {
return node.value;
}
Expand Down Expand Up @@ -61,9 +63,9 @@ export function getValueIfRange(node: HTMLElement): string|null {
return null;
}

const isImplicitRange = (node instanceof HTMLInputElement &&
const isImplicitRange = (hasTagName(node, 'input') &&
RANGE_INPUT_TYPES.includes(node.type)) ||
node instanceof HTMLProgressElement;
hasTagName(node, 'progress');

if (isExplicitRange || isImplicitRange) {
if (node.hasAttribute('aria-valuetext')) {
Expand All @@ -72,10 +74,10 @@ export function getValueIfRange(node: HTMLElement): string|null {
if (node.hasAttribute('aria-valuenow')) {
return node.getAttribute('aria-valuenow');
}
if (node instanceof HTMLInputElement) {
if (hasTagName(node, 'input')) {
return node.value;
}
if (node instanceof HTMLProgressElement) {
if (hasTagName(node, 'progress')) {
return node.value.toString();
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/lib/rule2I.ts
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { isHTMLElement } from './util';

// Input types for whom placeholders should be considered when computing
// a text alternative. See
// https://www.w3.org/TR/html-aam-1.0/#input-type-text-input-type-password-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-element-accessible-name-computation
Expand All @@ -22,7 +24,7 @@ const TEXTUAL_INPUT_TYPES = [
* @return - text alternative if rule 2I applies to node, null otherwise.
*/
export function rule2I(node: Node): string|null {
if (!(node instanceof HTMLElement)) {
if (!isHTMLElement(node)) {
return null;
}

Expand Down

0 comments on commit 466d7e9

Please sign in to comment.