genInteractives.js 6.5 KB

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