genInteractives.js 6.7 KB

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