You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
6.7 KiB
214 lines
6.7 KiB
/** |
|
* @flow |
|
*/ |
|
|
|
import { dom, roles } from 'aria-query'; |
|
import includes from 'array-includes'; |
|
import JSXAttributeMock from './JSXAttributeMock'; |
|
import JSXElementMock from './JSXElementMock'; |
|
|
|
import type { JSXAttributeMockType } from './JSXAttributeMock'; |
|
import type { JSXElementMockType } from './JSXElementMock'; |
|
|
|
const domElements = [...dom.keys()]; |
|
const roleNames = [...roles.keys()]; |
|
|
|
const interactiveElementsMap = { |
|
a: [{ prop: 'href', value: '#' }], |
|
area: [{ prop: 'href', value: '#' }], |
|
audio: [], |
|
button: [], |
|
canvas: [], |
|
datalist: [], |
|
embed: [], |
|
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' }], |
|
link: [{ prop: 'href', value: '#' }], |
|
menuitem: [], |
|
option: [], |
|
select: [], |
|
summary: [], |
|
// 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: {[string]: Array<{[string]: string}>} = { |
|
abbr: [], |
|
aside: [], |
|
article: [], |
|
blockquote: [], |
|
body: [], |
|
br: [], |
|
caption: [], |
|
dd: [], |
|
details: [], |
|
dfn: [], |
|
dialog: [], |
|
dir: [], |
|
dl: [], |
|
dt: [], |
|
fieldset: [], |
|
figcaption: [], |
|
figure: [], |
|
footer: [], |
|
form: [], |
|
frame: [], |
|
h1: [], |
|
h2: [], |
|
h3: [], |
|
h4: [], |
|
h5: [], |
|
h6: [], |
|
hr: [], |
|
iframe: [], |
|
img: [], |
|
label: [], |
|
legend: [], |
|
li: [], |
|
main: [], |
|
mark: [], |
|
marquee: [], |
|
menu: [], |
|
meter: [], |
|
nav: [], |
|
ol: [], |
|
optgroup: [], |
|
output: [], |
|
p: [], |
|
pre: [], |
|
progress: [], |
|
ruby: [], |
|
'section[aria-label]': [{ prop: 'aria-label' }], |
|
'section[aria-labelledby]': [{ prop: 'aria-labelledby' }], |
|
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): string { |
|
return ( |
|
openingElement.name.name + (openingElement.attributes.length > 0 |
|
? `${openingElement.attributes |
|
.map((attr) => `[${attr.name.name}="${attr.value.value}"]`) |
|
.join('')}` |
|
: '' |
|
) |
|
); |
|
} |
|
|
|
export function genInteractiveElements(): Array<JSXElementMockType> { |
|
return Object.keys(interactiveElementsMap).map((elementSymbol: string): JSXElementMockType => { |
|
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<JSXElementMockType> { |
|
return [...interactiveRoles, 'button article', 'fakerole button article'].map((value): JSXElementMockType => JSXElementMock( |
|
'div', |
|
[JSXAttributeMock('role', value)], |
|
)); |
|
} |
|
|
|
export function genNonInteractiveElements(): Array<JSXElementMockType> { |
|
return Object.keys(nonInteractiveElementsMap).map((elementSymbol): JSXElementMockType => { |
|
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(): Array<JSXElementMockType> { |
|
return [ |
|
...nonInteractiveRoles, |
|
'article button', |
|
'fakerole article button', |
|
].map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)])); |
|
} |
|
|
|
export function genAbstractRoleElements(): Array<JSXElementMockType> { |
|
return abstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)])); |
|
} |
|
|
|
export function genNonAbstractRoleElements(): Array<JSXElementMockType> { |
|
return nonAbstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)])); |
|
} |
|
|
|
export function genIndeterminantInteractiveElements(): Array<JSXElementMockType> { |
|
return Object.keys(indeterminantInteractiveElementsMap).map((name) => { |
|
const attributes = indeterminantInteractiveElementsMap[name].map(({ prop, value }): JSXAttributeMockType => JSXAttributeMock(prop, value)); |
|
return JSXElementMock(name, attributes); |
|
}); |
|
}
|
|
|