genInteractives.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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. summary: [],
  47. // Whereas ARIA makes a distinction between cell and gridcell, the AXObject
  48. // treats them both as CellRole and since gridcell is interactive, we consider
  49. // cell interactive as well.
  50. td: [],
  51. th: [],
  52. tr: [],
  53. textarea: [],
  54. video: [],
  55. };
  56. const nonInteractiveElementsMap: {[string]: Array<{[string]: string}>} = {
  57. abbr: [],
  58. address: [],
  59. article: [],
  60. aside: [],
  61. blockquote: [],
  62. br: [],
  63. caption: [],
  64. code: [],
  65. dd: [],
  66. del: [],
  67. details: [],
  68. dfn: [],
  69. dialog: [],
  70. dir: [],
  71. dl: [],
  72. dt: [],
  73. em: [],
  74. fieldset: [],
  75. figcaption: [],
  76. figure: [],
  77. footer: [],
  78. form: [],
  79. h1: [],
  80. h2: [],
  81. h3: [],
  82. h4: [],
  83. h5: [],
  84. h6: [],
  85. hr: [],
  86. html: [],
  87. iframe: [],
  88. img: [],
  89. ins: [],
  90. label: [],
  91. legend: [],
  92. li: [],
  93. main: [],
  94. mark: [],
  95. marquee: [],
  96. menu: [],
  97. meter: [],
  98. nav: [],
  99. ol: [],
  100. optgroup: [],
  101. output: [],
  102. p: [],
  103. pre: [],
  104. progress: [],
  105. ruby: [],
  106. 'section[aria-label]': [{ prop: 'aria-label' }],
  107. 'section[aria-labelledby]': [{ prop: 'aria-labelledby' }],
  108. strong: [],
  109. sub: [],
  110. sup: [],
  111. table: [],
  112. tbody: [],
  113. tfoot: [],
  114. thead: [],
  115. time: [],
  116. ul: [],
  117. };
  118. const indeterminantInteractiveElementsMap: { [key: string]: Array<any> } = fromEntries(domElements.map((name) => [name, []]));
  119. Object.keys(interactiveElementsMap)
  120. .concat(Object.keys(nonInteractiveElementsMap))
  121. .forEach((name) => delete indeterminantInteractiveElementsMap[name]);
  122. const abstractRoles = roleNames.filter((role) => roles.get(role).abstract);
  123. const nonAbstractRoles = roleNames.filter((role) => !roles.get(role).abstract);
  124. const interactiveRoles = []
  125. .concat(
  126. roleNames,
  127. // 'toolbar' does not descend from widget, but it does support
  128. // aria-activedescendant, thus in practice we treat it as a widget.
  129. 'toolbar',
  130. )
  131. .filter((role) => (
  132. !roles.get(role).abstract
  133. && roles.get(role).superClass.some((klasses) => includes(klasses, 'widget'))
  134. ));
  135. const nonInteractiveRoles = roleNames
  136. .filter((role) => (
  137. !roles.get(role).abstract
  138. && !roles.get(role).superClass.some((klasses) => includes(klasses, 'widget'))
  139. // 'toolbar' does not descend from widget, but it does support
  140. // aria-activedescendant, thus in practice we treat it as a widget.
  141. && !includes(['toolbar'], role)
  142. ));
  143. export function genElementSymbol(openingElement: Object): string {
  144. return (
  145. openingElement.name.name + (openingElement.attributes.length > 0
  146. ? `${openingElement.attributes.map((attr) => `[${attr.name.name}="${attr.value.value}"]`).join('')}`
  147. : ''
  148. )
  149. );
  150. }
  151. export function genInteractiveElements(): Array<JSXElementMockType> {
  152. return Object.keys(interactiveElementsMap).map((elementSymbol: string): JSXElementMockType => {
  153. const bracketIndex = elementSymbol.indexOf('[');
  154. let name = elementSymbol;
  155. if (bracketIndex > -1) {
  156. name = elementSymbol.slice(0, bracketIndex);
  157. }
  158. const attributes = interactiveElementsMap[elementSymbol].map(({ prop, value }) => JSXAttributeMock(prop, value));
  159. return JSXElementMock(name, attributes);
  160. });
  161. }
  162. export function genInteractiveRoleElements(): Array<JSXElementMockType> {
  163. return interactiveRoles.concat('button article', 'fakerole button article').map((value): JSXElementMockType => JSXElementMock(
  164. 'div',
  165. [JSXAttributeMock('role', value)],
  166. ));
  167. }
  168. export function genNonInteractiveElements(): Array<JSXElementMockType> {
  169. return Object.keys(nonInteractiveElementsMap).map((elementSymbol): JSXElementMockType => {
  170. const bracketIndex = elementSymbol.indexOf('[');
  171. let name = elementSymbol;
  172. if (bracketIndex > -1) {
  173. name = elementSymbol.slice(0, bracketIndex);
  174. }
  175. const attributes = nonInteractiveElementsMap[elementSymbol].map(({ prop, value }) => JSXAttributeMock(prop, value));
  176. return JSXElementMock(name, attributes);
  177. });
  178. }
  179. export function genNonInteractiveRoleElements(): Array<JSXElementMockType> {
  180. return [
  181. ...nonInteractiveRoles,
  182. 'article button',
  183. 'fakerole article button',
  184. ].map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
  185. }
  186. export function genAbstractRoleElements(): Array<JSXElementMockType> {
  187. return abstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
  188. }
  189. export function genNonAbstractRoleElements(): Array<JSXElementMockType> {
  190. return nonAbstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
  191. }
  192. export function genIndeterminantInteractiveElements(): Array<JSXElementMockType> {
  193. return Object.keys(indeterminantInteractiveElementsMap).map((name) => {
  194. const attributes = indeterminantInteractiveElementsMap[name].map(({ prop, value }): JSXAttributeMockType => JSXAttributeMock(prop, value));
  195. return JSXElementMock(name, attributes);
  196. });
  197. }