| 1 | /**
|
|---|
| 2 | * @flow
|
|---|
| 3 | */
|
|---|
| 4 |
|
|---|
| 5 | import { dom, roles } from 'aria-query';
|
|---|
| 6 | import includes from 'array-includes';
|
|---|
| 7 | import fromEntries from 'object.fromentries';
|
|---|
| 8 |
|
|---|
| 9 | import JSXAttributeMock from './JSXAttributeMock';
|
|---|
| 10 | import JSXElementMock from './JSXElementMock';
|
|---|
| 11 |
|
|---|
| 12 | import type { JSXAttributeMockType } from './JSXAttributeMock';
|
|---|
| 13 | import type { JSXElementMockType } from './JSXElementMock';
|
|---|
| 14 |
|
|---|
| 15 | const domElements = dom.keys();
|
|---|
| 16 | const roleNames = roles.keys();
|
|---|
| 17 |
|
|---|
| 18 | const interactiveElementsMap = {
|
|---|
| 19 | a: [{ prop: 'href', value: '#' }],
|
|---|
| 20 | area: [{ prop: 'href', value: '#' }],
|
|---|
| 21 | audio: [],
|
|---|
| 22 | button: [],
|
|---|
| 23 | canvas: [],
|
|---|
| 24 | datalist: [],
|
|---|
| 25 | embed: [],
|
|---|
| 26 | input: [],
|
|---|
| 27 | 'input[type="button"]': [{ prop: 'type', value: 'button' }],
|
|---|
| 28 | 'input[type="checkbox"]': [{ prop: 'type', value: 'checkbox' }],
|
|---|
| 29 | 'input[type="color"]': [{ prop: 'type', value: 'color' }],
|
|---|
| 30 | 'input[type="date"]': [{ prop: 'type', value: 'date' }],
|
|---|
| 31 | 'input[type="datetime"]': [{ prop: 'type', value: 'datetime' }],
|
|---|
| 32 | 'input[type="email"]': [{ prop: 'type', value: 'email' }],
|
|---|
| 33 | 'input[type="file"]': [{ prop: 'type', value: 'file' }],
|
|---|
| 34 | 'input[type="image"]': [{ prop: 'type', value: 'image' }],
|
|---|
| 35 | 'input[type="month"]': [{ prop: 'type', value: 'month' }],
|
|---|
| 36 | 'input[type="number"]': [{ prop: 'type', value: 'number' }],
|
|---|
| 37 | 'input[type="password"]': [{ prop: 'type', value: 'password' }],
|
|---|
| 38 | 'input[type="radio"]': [{ prop: 'type', value: 'radio' }],
|
|---|
| 39 | 'input[type="range"]': [{ prop: 'type', value: 'range' }],
|
|---|
| 40 | 'input[type="reset"]': [{ prop: 'type', value: 'reset' }],
|
|---|
| 41 | 'input[type="search"]': [{ prop: 'type', value: 'search' }],
|
|---|
| 42 | 'input[type="submit"]': [{ prop: 'type', value: 'submit' }],
|
|---|
| 43 | 'input[type="tel"]': [{ prop: 'type', value: 'tel' }],
|
|---|
| 44 | 'input[type="text"]': [{ prop: 'type', value: 'text' }],
|
|---|
| 45 | 'input[type="time"]': [{ prop: 'type', value: 'time' }],
|
|---|
| 46 | 'input[type="url"]': [{ prop: 'type', value: 'url' }],
|
|---|
| 47 | 'input[type="week"]': [{ prop: 'type', value: 'week' }],
|
|---|
| 48 | menuitem: [],
|
|---|
| 49 | option: [],
|
|---|
| 50 | select: [],
|
|---|
| 51 | summary: [],
|
|---|
| 52 | // Whereas ARIA makes a distinction between cell and gridcell, the AXObject
|
|---|
| 53 | // treats them both as CellRole and since gridcell is interactive, we consider
|
|---|
| 54 | // cell interactive as well.
|
|---|
| 55 | td: [],
|
|---|
| 56 | th: [],
|
|---|
| 57 | tr: [],
|
|---|
| 58 | textarea: [],
|
|---|
| 59 | video: [],
|
|---|
| 60 | };
|
|---|
| 61 |
|
|---|
| 62 | const nonInteractiveElementsMap: {[string]: Array<{[string]: string}>} = {
|
|---|
| 63 | abbr: [],
|
|---|
| 64 | address: [],
|
|---|
| 65 | article: [],
|
|---|
| 66 | aside: [],
|
|---|
| 67 | blockquote: [],
|
|---|
| 68 | br: [],
|
|---|
| 69 | caption: [],
|
|---|
| 70 | code: [],
|
|---|
| 71 | dd: [],
|
|---|
| 72 | del: [],
|
|---|
| 73 | details: [],
|
|---|
| 74 | dfn: [],
|
|---|
| 75 | dialog: [],
|
|---|
| 76 | dir: [],
|
|---|
| 77 | dl: [],
|
|---|
| 78 | dt: [],
|
|---|
| 79 | em: [],
|
|---|
| 80 | fieldset: [],
|
|---|
| 81 | figcaption: [],
|
|---|
| 82 | figure: [],
|
|---|
| 83 | footer: [],
|
|---|
| 84 | form: [],
|
|---|
| 85 | h1: [],
|
|---|
| 86 | h2: [],
|
|---|
| 87 | h3: [],
|
|---|
| 88 | h4: [],
|
|---|
| 89 | h5: [],
|
|---|
| 90 | h6: [],
|
|---|
| 91 | hr: [],
|
|---|
| 92 | html: [],
|
|---|
| 93 | iframe: [],
|
|---|
| 94 | img: [],
|
|---|
| 95 | ins: [],
|
|---|
| 96 | label: [],
|
|---|
| 97 | legend: [],
|
|---|
| 98 | li: [],
|
|---|
| 99 | main: [],
|
|---|
| 100 | mark: [],
|
|---|
| 101 | marquee: [],
|
|---|
| 102 | menu: [],
|
|---|
| 103 | meter: [],
|
|---|
| 104 | nav: [],
|
|---|
| 105 | ol: [],
|
|---|
| 106 | optgroup: [],
|
|---|
| 107 | output: [],
|
|---|
| 108 | p: [],
|
|---|
| 109 | pre: [],
|
|---|
| 110 | progress: [],
|
|---|
| 111 | ruby: [],
|
|---|
| 112 | 'section[aria-label]': [{ prop: 'aria-label' }],
|
|---|
| 113 | 'section[aria-labelledby]': [{ prop: 'aria-labelledby' }],
|
|---|
| 114 | strong: [],
|
|---|
| 115 | sub: [],
|
|---|
| 116 | sup: [],
|
|---|
| 117 | table: [],
|
|---|
| 118 | tbody: [],
|
|---|
| 119 | tfoot: [],
|
|---|
| 120 | thead: [],
|
|---|
| 121 | time: [],
|
|---|
| 122 | ul: [],
|
|---|
| 123 | };
|
|---|
| 124 |
|
|---|
| 125 | const indeterminantInteractiveElementsMap: { [key: string]: Array<any> } = fromEntries(domElements.map((name) => [name, []]));
|
|---|
| 126 |
|
|---|
| 127 | Object.keys(interactiveElementsMap)
|
|---|
| 128 | .concat(Object.keys(nonInteractiveElementsMap))
|
|---|
| 129 | .forEach((name) => delete indeterminantInteractiveElementsMap[name]);
|
|---|
| 130 |
|
|---|
| 131 | const abstractRoles = roleNames.filter((role) => roles.get(role).abstract);
|
|---|
| 132 |
|
|---|
| 133 | const nonAbstractRoles = roleNames.filter((role) => !roles.get(role).abstract);
|
|---|
| 134 |
|
|---|
| 135 | const interactiveRoles = []
|
|---|
| 136 | .concat(
|
|---|
| 137 | roleNames,
|
|---|
| 138 | // 'toolbar' does not descend from widget, but it does support
|
|---|
| 139 | // aria-activedescendant, thus in practice we treat it as a widget.
|
|---|
| 140 | 'toolbar',
|
|---|
| 141 | )
|
|---|
| 142 | .filter((role) => (
|
|---|
| 143 | !roles.get(role).abstract
|
|---|
| 144 | && roles.get(role).superClass.some((klasses) => includes(klasses, 'widget'))
|
|---|
| 145 | ));
|
|---|
| 146 |
|
|---|
| 147 | const nonInteractiveRoles = roleNames
|
|---|
| 148 | .filter((role) => (
|
|---|
| 149 | !roles.get(role).abstract
|
|---|
| 150 | && !roles.get(role).superClass.some((klasses) => includes(klasses, 'widget'))
|
|---|
| 151 |
|
|---|
| 152 | // 'toolbar' does not descend from widget, but it does support
|
|---|
| 153 | // aria-activedescendant, thus in practice we treat it as a widget.
|
|---|
| 154 | && !includes(['toolbar'], role)
|
|---|
| 155 | ));
|
|---|
| 156 |
|
|---|
| 157 | export function genElementSymbol(openingElement: Object): string {
|
|---|
| 158 | return (
|
|---|
| 159 | openingElement.name.name + (openingElement.attributes.length > 0
|
|---|
| 160 | ? `${openingElement.attributes.map((attr) => `[${attr.name.name}="${attr.value.value}"]`).join('')}`
|
|---|
| 161 | : ''
|
|---|
| 162 | )
|
|---|
| 163 | );
|
|---|
| 164 | }
|
|---|
| 165 |
|
|---|
| 166 | export function genInteractiveElements(): Array<JSXElementMockType> {
|
|---|
| 167 | return Object.keys(interactiveElementsMap).map((elementSymbol: string): JSXElementMockType => {
|
|---|
| 168 | const bracketIndex = elementSymbol.indexOf('[');
|
|---|
| 169 | let name = elementSymbol;
|
|---|
| 170 | if (bracketIndex > -1) {
|
|---|
| 171 | name = elementSymbol.slice(0, bracketIndex);
|
|---|
| 172 | }
|
|---|
| 173 | const attributes = interactiveElementsMap[elementSymbol].map(({ prop, value }) => JSXAttributeMock(prop, value));
|
|---|
| 174 | return JSXElementMock(name, attributes);
|
|---|
| 175 | });
|
|---|
| 176 | }
|
|---|
| 177 |
|
|---|
| 178 | export function genInteractiveRoleElements(): Array<JSXElementMockType> {
|
|---|
| 179 | return interactiveRoles.concat('button article', 'fakerole button article').map((value): JSXElementMockType => JSXElementMock(
|
|---|
| 180 | 'div',
|
|---|
| 181 | [JSXAttributeMock('role', value)],
|
|---|
| 182 | ));
|
|---|
| 183 | }
|
|---|
| 184 |
|
|---|
| 185 | export function genNonInteractiveElements(): Array<JSXElementMockType> {
|
|---|
| 186 | return Object.keys(nonInteractiveElementsMap).map((elementSymbol): JSXElementMockType => {
|
|---|
| 187 | const bracketIndex = elementSymbol.indexOf('[');
|
|---|
| 188 | let name = elementSymbol;
|
|---|
| 189 | if (bracketIndex > -1) {
|
|---|
| 190 | name = elementSymbol.slice(0, bracketIndex);
|
|---|
| 191 | }
|
|---|
| 192 | const attributes = nonInteractiveElementsMap[elementSymbol].map(({ prop, value }) => JSXAttributeMock(prop, value));
|
|---|
| 193 | return JSXElementMock(name, attributes);
|
|---|
| 194 | });
|
|---|
| 195 | }
|
|---|
| 196 |
|
|---|
| 197 | export function genNonInteractiveRoleElements(): Array<JSXElementMockType> {
|
|---|
| 198 | return [
|
|---|
| 199 | ...nonInteractiveRoles,
|
|---|
| 200 | 'article button',
|
|---|
| 201 | 'fakerole article button',
|
|---|
| 202 | ].map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
|
|---|
| 203 | }
|
|---|
| 204 |
|
|---|
| 205 | export function genAbstractRoleElements(): Array<JSXElementMockType> {
|
|---|
| 206 | return abstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
|
|---|
| 207 | }
|
|---|
| 208 |
|
|---|
| 209 | export function genNonAbstractRoleElements(): Array<JSXElementMockType> {
|
|---|
| 210 | return nonAbstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
|
|---|
| 211 | }
|
|---|
| 212 |
|
|---|
| 213 | export function genIndeterminantInteractiveElements(): Array<JSXElementMockType> {
|
|---|
| 214 | return Object.keys(indeterminantInteractiveElementsMap).map((name) => {
|
|---|
| 215 | const attributes = indeterminantInteractiveElementsMap[name].map(({ prop, value }): JSXAttributeMockType => JSXAttributeMock(prop, value));
|
|---|
| 216 | return JSXElementMock(name, attributes);
|
|---|
| 217 | });
|
|---|
| 218 | }
|
|---|