blob: 9ddf50784c839d1ad3c4fe3c257a2e883d549547 [file] [log] [blame]
/**
* @flow
*/
import { dom, roles } from 'aria-query';
import includes from 'array-includes';
import JSXAttributeMock from './JSXAttributeMock';
import JSXElementMock from './JSXElementMock';
import type { TJSXElementMock } from './JSXElementMock';
const domElements = [...dom.keys()];
const roleNames = [...roles.keys()];
const interactiveElementsMap = {
a: [{ prop: 'href', value: '#' }],
area: [{ prop: 'href', value: '#' }],
audio: [],
button: [],
canvas: [],
embed: [],
label: [],
link: [],
input: [],
'input[type="button"]': [{ prop: 'type', value: 'button' }],
'input[type="checkbox"]': [{ prop: 'type', value: 'checkbox' }],
'input[type="color"]': [{ prop: 'type', value: 'color' }],
'input[type="date"]': [{ prop: 'type', value: 'date' }],
'input[type="datetime"]': [{ prop: 'type', value: 'datetime' }],
'input[type="email"]': [{ prop: 'type', value: 'email' }],
'input[type="file"]': [{ prop: 'type', value: 'file' }],
'input[type="image"]': [{ prop: 'type', value: 'image' }],
'input[type="month"]': [{ prop: 'type', value: 'month' }],
'input[type="number"]': [{ prop: 'type', value: 'number' }],
'input[type="password"]': [{ prop: 'type', value: 'password' }],
'input[type="radio"]': [{ prop: 'type', value: 'radio' }],
'input[type="range"]': [{ prop: 'type', value: 'range' }],
'input[type="reset"]': [{ prop: 'type', value: 'reset' }],
'input[type="search"]': [{ prop: 'type', value: 'search' }],
'input[type="submit"]': [{ prop: 'type', value: 'submit' }],
'input[type="tel"]': [{ prop: 'type', value: 'tel' }],
'input[type="text"]': [{ prop: 'type', value: 'text' }],
'input[type="time"]': [{ prop: 'type', value: 'time' }],
'input[type="url"]': [{ prop: 'type', value: 'url' }],
'input[type="week"]': [{ prop: 'type', value: 'week' }],
menuitem: [],
option: [],
select: [],
// Whereas ARIA makes a distinction between cell and gridcell, the AXObject
// treats them both as CellRole and since gridcell is interactive, we consider
// cell interactive as well.
// td: [],
th: [],
tr: [],
textarea: [],
video: [],
};
const nonInteractiveElementsMap = {
abbr: [],
article: [],
blockquote: [],
br: [],
caption: [],
dd: [],
details: [],
dfn: [],
dialog: [],
dir: [],
dl: [],
dt: [],
fieldset: [],
figcaption: [],
figure: [],
footer: [],
form: [],
frame: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
hr: [],
iframe: [],
img: [],
legend: [],
li: [],
main: [],
mark: [],
marquee: [],
menu: [],
meter: [],
nav: [],
ol: [],
p: [],
pre: [],
progress: [],
ruby: [],
section: [],
table: [],
tbody: [],
td: [],
tfoot: [],
thead: [],
time: [],
ul: [],
};
const indeterminantInteractiveElementsMap = domElements.reduce(
(accumulator: { [key: string]: Array<any> }, name: string): { [key: string]: Array<any> } => ({
...accumulator,
[name]: [],
}),
{},
);
Object.keys(interactiveElementsMap)
.concat(Object.keys(nonInteractiveElementsMap))
.forEach((name: string) => delete indeterminantInteractiveElementsMap[name]);
const abstractRoles = roleNames.filter(role => roles.get(role).abstract);
const nonAbstractRoles = roleNames.filter(role => !roles.get(role).abstract);
const interactiveRoles = []
.concat(
roleNames,
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
'toolbar',
)
.filter(role => !roles.get(role).abstract)
.filter(role => roles.get(role).superClass.some(klasses => includes(klasses, 'widget')));
const nonInteractiveRoles = roleNames
.filter(role => !roles.get(role).abstract)
.filter(role => !roles.get(role).superClass.some(klasses => includes(klasses, 'widget')))
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
.filter(role => !includes(['toolbar'], role));
export function genElementSymbol(openingElement: Object) {
return (
openingElement.name.name + (openingElement.attributes.length > 0
? `${openingElement.attributes
.map(attr => `[${attr.name.name}="${attr.value.value}"]`)
.join('')}`
: ''
)
);
}
export function genInteractiveElements(): Array<TJSXElementMock> {
return Object.keys(interactiveElementsMap).map((elementSymbol: string): TJSXElementMock => {
const bracketIndex = elementSymbol.indexOf('[');
let name = elementSymbol;
if (bracketIndex > -1) {
name = elementSymbol.slice(0, bracketIndex);
}
const attributes = interactiveElementsMap[elementSymbol].map(({ prop, value }) => JSXAttributeMock(prop, value));
return JSXElementMock(name, attributes);
});
}
export function genInteractiveRoleElements(): Array<TJSXElementMock> {
return [...interactiveRoles, 'button article', 'fakerole button article'].map((value): TJSXElementMock => JSXElementMock(
'div',
[JSXAttributeMock('role', value)],
));
}
export function genNonInteractiveElements(): Array<TJSXElementMock> {
return Object.keys(nonInteractiveElementsMap).map((elementSymbol): TJSXElementMock => {
const bracketIndex = elementSymbol.indexOf('[');
let name = elementSymbol;
if (bracketIndex > -1) {
name = elementSymbol.slice(0, bracketIndex);
}
const attributes = nonInteractiveElementsMap[elementSymbol].map(({ prop, value }) => JSXAttributeMock(prop, value));
return JSXElementMock(name, attributes);
});
}
export function genNonInteractiveRoleElements() {
return [
...nonInteractiveRoles,
'article button',
'fakerole article button',
].map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}
export function genAbstractRoleElements() {
return abstractRoles.map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}
export function genNonAbstractRoleElements() {
return nonAbstractRoles.map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}
export function genIndeterminantInteractiveElements(): Array<TJSXElementMock> {
return Object.keys(indeterminantInteractiveElementsMap).map((name) => {
const attributes = indeterminantInteractiveElementsMap[name].map(({ prop, value }): TJSXElementMock => JSXAttributeMock(prop, value));
return JSXElementMock(name, attributes);
});
}