genInteractives.js 6.6 KB

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