/** * @flow */ import { dom, roles } from 'aria-query'; import includes from 'array-includes'; import fromEntries from 'object.fromentries'; 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: [], // 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: { [key: string]: Array } = fromEntries(domElements.map((name: string) => [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 && roles.get(role).superClass.some((klasses) => includes(klasses, 'widget')) )); const nonInteractiveRoles = roleNames .filter((role) => ( !roles.get(role).abstract && !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. && !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 { 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 { return interactiveRoles.concat('button article', 'fakerole button article').map((value): JSXElementMockType => JSXElementMock( 'div', [JSXAttributeMock('role', value)], )); } export function genNonInteractiveElements(): Array { 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 { return [ ...nonInteractiveRoles, 'article button', 'fakerole article button', ].map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)])); } export function genAbstractRoleElements(): Array { return abstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)])); } export function genNonAbstractRoleElements(): Array { return nonAbstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)])); } export function genIndeterminantInteractiveElements(): Array { return Object.keys(indeterminantInteractiveElementsMap).map((name) => { const attributes = indeterminantInteractiveElementsMap[name].map(({ prop, value }): JSXAttributeMockType => JSXAttributeMock(prop, value)); return JSXElementMock(name, attributes); }); }