role.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.queryByRole = exports.queryAllByRole = exports.getByRole = exports.getAllByRole = exports.findByRole = exports.findAllByRole = void 0;
  6. var _domAccessibilityApi = require("dom-accessibility-api");
  7. var _ariaQuery = require("aria-query");
  8. var _roleHelpers = require("../role-helpers");
  9. var _queryHelpers = require("../query-helpers");
  10. var _helpers = require("../helpers");
  11. var _allUtils = require("./all-utils");
  12. function queryAllByRole(container, role, {
  13. exact = true,
  14. collapseWhitespace,
  15. hidden = (0, _allUtils.getConfig)().defaultHidden,
  16. name,
  17. description,
  18. trim,
  19. normalizer,
  20. queryFallbacks = false,
  21. selected,
  22. checked,
  23. pressed,
  24. current,
  25. level,
  26. expanded
  27. } = {}) {
  28. (0, _helpers.checkContainerType)(container);
  29. const matcher = exact ? _allUtils.matches : _allUtils.fuzzyMatches;
  30. const matchNormalizer = (0, _allUtils.makeNormalizer)({
  31. collapseWhitespace,
  32. trim,
  33. normalizer
  34. });
  35. if (selected !== undefined) {
  36. var _allRoles$get;
  37. // guard against unknown roles
  38. if (((_allRoles$get = _ariaQuery.roles.get(role)) == null ? void 0 : _allRoles$get.props['aria-selected']) === undefined) {
  39. throw new Error(`"aria-selected" is not supported on role "${role}".`);
  40. }
  41. }
  42. if (checked !== undefined) {
  43. var _allRoles$get2;
  44. // guard against unknown roles
  45. if (((_allRoles$get2 = _ariaQuery.roles.get(role)) == null ? void 0 : _allRoles$get2.props['aria-checked']) === undefined) {
  46. throw new Error(`"aria-checked" is not supported on role "${role}".`);
  47. }
  48. }
  49. if (pressed !== undefined) {
  50. var _allRoles$get3;
  51. // guard against unknown roles
  52. if (((_allRoles$get3 = _ariaQuery.roles.get(role)) == null ? void 0 : _allRoles$get3.props['aria-pressed']) === undefined) {
  53. throw new Error(`"aria-pressed" is not supported on role "${role}".`);
  54. }
  55. }
  56. if (current !== undefined) {
  57. var _allRoles$get4;
  58. /* istanbul ignore next */
  59. // guard against unknown roles
  60. // All currently released ARIA versions support `aria-current` on all roles.
  61. // Leaving this for symetry and forward compatibility
  62. if (((_allRoles$get4 = _ariaQuery.roles.get(role)) == null ? void 0 : _allRoles$get4.props['aria-current']) === undefined) {
  63. throw new Error(`"aria-current" is not supported on role "${role}".`);
  64. }
  65. }
  66. if (level !== undefined) {
  67. // guard against using `level` option with any role other than `heading`
  68. if (role !== 'heading') {
  69. throw new Error(`Role "${role}" cannot have "level" property.`);
  70. }
  71. }
  72. if (expanded !== undefined) {
  73. var _allRoles$get5;
  74. // guard against unknown roles
  75. if (((_allRoles$get5 = _ariaQuery.roles.get(role)) == null ? void 0 : _allRoles$get5.props['aria-expanded']) === undefined) {
  76. throw new Error(`"aria-expanded" is not supported on role "${role}".`);
  77. }
  78. }
  79. const subtreeIsInaccessibleCache = new WeakMap();
  80. function cachedIsSubtreeInaccessible(element) {
  81. if (!subtreeIsInaccessibleCache.has(element)) {
  82. subtreeIsInaccessibleCache.set(element, (0, _roleHelpers.isSubtreeInaccessible)(element));
  83. }
  84. return subtreeIsInaccessibleCache.get(element);
  85. }
  86. return Array.from(container.querySelectorAll(
  87. // Only query elements that can be matched by the following filters
  88. makeRoleSelector(role, exact, normalizer ? matchNormalizer : undefined))).filter(node => {
  89. const isRoleSpecifiedExplicitly = node.hasAttribute('role');
  90. if (isRoleSpecifiedExplicitly) {
  91. const roleValue = node.getAttribute('role');
  92. if (queryFallbacks) {
  93. return roleValue.split(' ').filter(Boolean).some(text => matcher(text, node, role, matchNormalizer));
  94. }
  95. // if a custom normalizer is passed then let normalizer handle the role value
  96. if (normalizer) {
  97. return matcher(roleValue, node, role, matchNormalizer);
  98. }
  99. // other wise only send the first word to match
  100. const [firstWord] = roleValue.split(' ');
  101. return matcher(firstWord, node, role, matchNormalizer);
  102. }
  103. const implicitRoles = (0, _roleHelpers.getImplicitAriaRoles)(node);
  104. return implicitRoles.some(implicitRole => matcher(implicitRole, node, role, matchNormalizer));
  105. }).filter(element => {
  106. if (selected !== undefined) {
  107. return selected === (0, _roleHelpers.computeAriaSelected)(element);
  108. }
  109. if (checked !== undefined) {
  110. return checked === (0, _roleHelpers.computeAriaChecked)(element);
  111. }
  112. if (pressed !== undefined) {
  113. return pressed === (0, _roleHelpers.computeAriaPressed)(element);
  114. }
  115. if (current !== undefined) {
  116. return current === (0, _roleHelpers.computeAriaCurrent)(element);
  117. }
  118. if (expanded !== undefined) {
  119. return expanded === (0, _roleHelpers.computeAriaExpanded)(element);
  120. }
  121. if (level !== undefined) {
  122. return level === (0, _roleHelpers.computeHeadingLevel)(element);
  123. }
  124. // don't care if aria attributes are unspecified
  125. return true;
  126. }).filter(element => {
  127. if (name === undefined) {
  128. // Don't care
  129. return true;
  130. }
  131. return (0, _allUtils.matches)((0, _domAccessibilityApi.computeAccessibleName)(element, {
  132. computedStyleSupportsPseudoElements: (0, _allUtils.getConfig)().computedStyleSupportsPseudoElements
  133. }), element, name, text => text);
  134. }).filter(element => {
  135. if (description === undefined) {
  136. // Don't care
  137. return true;
  138. }
  139. return (0, _allUtils.matches)((0, _domAccessibilityApi.computeAccessibleDescription)(element, {
  140. computedStyleSupportsPseudoElements: (0, _allUtils.getConfig)().computedStyleSupportsPseudoElements
  141. }), element, description, text => text);
  142. }).filter(element => {
  143. return hidden === false ? (0, _roleHelpers.isInaccessible)(element, {
  144. isSubtreeInaccessible: cachedIsSubtreeInaccessible
  145. }) === false : true;
  146. });
  147. }
  148. function makeRoleSelector(role, exact, customNormalizer) {
  149. var _roleElements$get;
  150. if (typeof role !== 'string') {
  151. // For non-string role parameters we can not determine the implicitRoleSelectors.
  152. return '*';
  153. }
  154. const explicitRoleSelector = exact && !customNormalizer ? `*[role~="${role}"]` : '*[role]';
  155. const roleRelations = (_roleElements$get = _ariaQuery.roleElements.get(role)) != null ? _roleElements$get : new Set();
  156. const implicitRoleSelectors = new Set(Array.from(roleRelations).map(({
  157. name
  158. }) => name));
  159. // Current transpilation config sometimes assumes `...` is always applied to arrays.
  160. // `...` is equivalent to `Array.prototype.concat` for arrays.
  161. // If you replace this code with `[explicitRoleSelector, ...implicitRoleSelectors]`, make sure every transpilation target retains the `...` in favor of `Array.prototype.concat`.
  162. return [explicitRoleSelector].concat(Array.from(implicitRoleSelectors)).join(',');
  163. }
  164. const getNameHint = name => {
  165. let nameHint = '';
  166. if (name === undefined) {
  167. nameHint = '';
  168. } else if (typeof name === 'string') {
  169. nameHint = ` and name "${name}"`;
  170. } else {
  171. nameHint = ` and name \`${name}\``;
  172. }
  173. return nameHint;
  174. };
  175. const getMultipleError = (c, role, {
  176. name
  177. } = {}) => {
  178. return `Found multiple elements with the role "${role}"${getNameHint(name)}`;
  179. };
  180. const getMissingError = (container, role, {
  181. hidden = (0, _allUtils.getConfig)().defaultHidden,
  182. name,
  183. description
  184. } = {}) => {
  185. if ((0, _allUtils.getConfig)()._disableExpensiveErrorDiagnostics) {
  186. return `Unable to find role="${role}"${getNameHint(name)}`;
  187. }
  188. let roles = '';
  189. Array.from(container.children).forEach(childElement => {
  190. roles += (0, _roleHelpers.prettyRoles)(childElement, {
  191. hidden,
  192. includeDescription: description !== undefined
  193. });
  194. });
  195. let roleMessage;
  196. if (roles.length === 0) {
  197. if (hidden === false) {
  198. roleMessage = 'There are no accessible roles. But there might be some inaccessible roles. ' + 'If you wish to access them, then set the `hidden` option to `true`. ' + 'Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole';
  199. } else {
  200. roleMessage = 'There are no available roles.';
  201. }
  202. } else {
  203. roleMessage = `
  204. Here are the ${hidden === false ? 'accessible' : 'available'} roles:
  205. ${roles.replace(/\n/g, '\n ').replace(/\n\s\s\n/g, '\n\n')}
  206. `.trim();
  207. }
  208. let nameHint = '';
  209. if (name === undefined) {
  210. nameHint = '';
  211. } else if (typeof name === 'string') {
  212. nameHint = ` and name "${name}"`;
  213. } else {
  214. nameHint = ` and name \`${name}\``;
  215. }
  216. let descriptionHint = '';
  217. if (description === undefined) {
  218. descriptionHint = '';
  219. } else if (typeof description === 'string') {
  220. descriptionHint = ` and description "${description}"`;
  221. } else {
  222. descriptionHint = ` and description \`${description}\``;
  223. }
  224. return `
  225. Unable to find an ${hidden === false ? 'accessible ' : ''}element with the role "${role}"${nameHint}${descriptionHint}
  226. ${roleMessage}`.trim();
  227. };
  228. const queryAllByRoleWithSuggestions = (0, _queryHelpers.wrapAllByQueryWithSuggestion)(queryAllByRole, queryAllByRole.name, 'queryAll');
  229. exports.queryAllByRole = queryAllByRoleWithSuggestions;
  230. const [queryByRole, getAllByRole, getByRole, findAllByRole, findByRole] = (0, _allUtils.buildQueries)(queryAllByRole, getMultipleError, getMissingError);
  231. exports.findByRole = findByRole;
  232. exports.findAllByRole = findAllByRole;
  233. exports.getByRole = getByRole;
  234. exports.getAllByRole = getAllByRole;
  235. exports.queryByRole = queryByRole;