accessible-name-and-description.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. "use strict";
  2. exports.__esModule = true;
  3. exports.computeTextAlternative = computeTextAlternative;
  4. var _array = _interopRequireDefault(require("./polyfills/array.from"));
  5. var _SetLike = _interopRequireDefault(require("./polyfills/SetLike"));
  6. var _util = require("./util");
  7. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  8. /**
  9. * implements https://w3c.github.io/accname/
  10. */
  11. /**
  12. *
  13. * @param {string} string -
  14. * @returns {FlatString} -
  15. */
  16. function asFlatString(s) {
  17. return s.trim().replace(/\s\s+/g, " ");
  18. }
  19. /**
  20. *
  21. * @param node -
  22. * @param options - These are not optional to prevent accidentally calling it without options in `computeAccessibleName`
  23. * @returns {boolean} -
  24. */
  25. function isHidden(node, getComputedStyleImplementation) {
  26. if (!(0, _util.isElement)(node)) {
  27. return false;
  28. }
  29. if (node.hasAttribute("hidden") || node.getAttribute("aria-hidden") === "true") {
  30. return true;
  31. }
  32. var style = getComputedStyleImplementation(node);
  33. return style.getPropertyValue("display") === "none" || style.getPropertyValue("visibility") === "hidden";
  34. }
  35. /**
  36. * @param {Node} node -
  37. * @returns {boolean} - As defined in step 2E of https://w3c.github.io/accname/#mapping_additional_nd_te
  38. */
  39. function isControl(node) {
  40. return (0, _util.hasAnyConcreteRoles)(node, ["button", "combobox", "listbox", "textbox"]) || hasAbstractRole(node, "range");
  41. }
  42. function hasAbstractRole(node, role) {
  43. if (!(0, _util.isElement)(node)) {
  44. return false;
  45. }
  46. switch (role) {
  47. case "range":
  48. return (0, _util.hasAnyConcreteRoles)(node, ["meter", "progressbar", "scrollbar", "slider", "spinbutton"]);
  49. default:
  50. throw new TypeError("No knowledge about abstract role '".concat(role, "'. This is likely a bug :("));
  51. }
  52. }
  53. /**
  54. * element.querySelectorAll but also considers owned tree
  55. * @param element
  56. * @param selectors
  57. */
  58. function querySelectorAllSubtree(element, selectors) {
  59. var elements = (0, _array.default)(element.querySelectorAll(selectors));
  60. (0, _util.queryIdRefs)(element, "aria-owns").forEach(function (root) {
  61. // babel transpiles this assuming an iterator
  62. elements.push.apply(elements, (0, _array.default)(root.querySelectorAll(selectors)));
  63. });
  64. return elements;
  65. }
  66. function querySelectedOptions(listbox) {
  67. if ((0, _util.isHTMLSelectElement)(listbox)) {
  68. // IE11 polyfill
  69. return listbox.selectedOptions || querySelectorAllSubtree(listbox, "[selected]");
  70. }
  71. return querySelectorAllSubtree(listbox, '[aria-selected="true"]');
  72. }
  73. function isMarkedPresentational(node) {
  74. return (0, _util.hasAnyConcreteRoles)(node, ["none", "presentation"]);
  75. }
  76. /**
  77. * Elements specifically listed in html-aam
  78. *
  79. * We don't need this for `label` or `legend` elements.
  80. * Their implicit roles already allow "naming from content".
  81. *
  82. * sources:
  83. *
  84. * - https://w3c.github.io/html-aam/#table-element
  85. */
  86. function isNativeHostLanguageTextAlternativeElement(node) {
  87. return (0, _util.isHTMLTableCaptionElement)(node);
  88. }
  89. /**
  90. * https://w3c.github.io/aria/#namefromcontent
  91. */
  92. function allowsNameFromContent(node) {
  93. return (0, _util.hasAnyConcreteRoles)(node, ["button", "cell", "checkbox", "columnheader", "gridcell", "heading", "label", "legend", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "row", "rowheader", "switch", "tab", "tooltip", "treeitem"]);
  94. }
  95. /**
  96. * TODO https://github.com/eps1lon/dom-accessibility-api/issues/100
  97. */
  98. function isDescendantOfNativeHostLanguageTextAlternativeElement( // eslint-disable-next-line @typescript-eslint/no-unused-vars -- not implemented yet
  99. node) {
  100. return false;
  101. }
  102. /**
  103. * TODO https://github.com/eps1lon/dom-accessibility-api/issues/101
  104. */
  105. // eslint-disable-next-line @typescript-eslint/no-unused-vars -- not implemented yet
  106. function computeTooltipAttributeValue(node) {
  107. return null;
  108. }
  109. function getValueOfTextbox(element) {
  110. if ((0, _util.isHTMLInputElement)(element) || (0, _util.isHTMLTextAreaElement)(element)) {
  111. return element.value;
  112. } // https://github.com/eps1lon/dom-accessibility-api/issues/4
  113. return element.textContent || "";
  114. }
  115. function getTextualContent(declaration) {
  116. var content = declaration.getPropertyValue("content");
  117. if (/^["'].*["']$/.test(content)) {
  118. return content.slice(1, -1);
  119. }
  120. return "";
  121. }
  122. /**
  123. * https://html.spec.whatwg.org/multipage/forms.html#category-label
  124. * TODO: form-associated custom elements
  125. * @param element
  126. */
  127. function isLabelableElement(element) {
  128. var localName = (0, _util.getLocalName)(element);
  129. return localName === "button" || localName === "input" && element.getAttribute("type") !== "hidden" || localName === "meter" || localName === "output" || localName === "progress" || localName === "select" || localName === "textarea";
  130. }
  131. /**
  132. * > [...], then the first such descendant in tree order is the label element's labeled control.
  133. * -- https://html.spec.whatwg.org/multipage/forms.html#labeled-control
  134. * @param element
  135. */
  136. function findLabelableElement(element) {
  137. if (isLabelableElement(element)) {
  138. return element;
  139. }
  140. var labelableElement = null;
  141. element.childNodes.forEach(function (childNode) {
  142. if (labelableElement === null && (0, _util.isElement)(childNode)) {
  143. var descendantLabelableElement = findLabelableElement(childNode);
  144. if (descendantLabelableElement !== null) {
  145. labelableElement = descendantLabelableElement;
  146. }
  147. }
  148. });
  149. return labelableElement;
  150. }
  151. /**
  152. * Polyfill of HTMLLabelElement.control
  153. * https://html.spec.whatwg.org/multipage/forms.html#labeled-control
  154. * @param label
  155. */
  156. function getControlOfLabel(label) {
  157. if (label.control !== undefined) {
  158. return label.control;
  159. }
  160. var htmlFor = label.getAttribute("for");
  161. if (htmlFor !== null) {
  162. return label.ownerDocument.getElementById(htmlFor);
  163. }
  164. return findLabelableElement(label);
  165. }
  166. /**
  167. * Polyfill of HTMLInputElement.labels
  168. * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/labels
  169. * @param element
  170. */
  171. function getLabels(element) {
  172. var labelsProperty = element.labels;
  173. if (labelsProperty === null) {
  174. return labelsProperty;
  175. }
  176. if (labelsProperty !== undefined) {
  177. return (0, _array.default)(labelsProperty);
  178. } // polyfill
  179. if (!isLabelableElement(element)) {
  180. return null;
  181. }
  182. var document = element.ownerDocument;
  183. return (0, _array.default)(document.querySelectorAll("label")).filter(function (label) {
  184. return getControlOfLabel(label) === element;
  185. });
  186. }
  187. /**
  188. * Gets the contents of a slot used for computing the accname
  189. * @param slot
  190. */
  191. function getSlotContents(slot) {
  192. // Computing the accessible name for elements containing slots is not
  193. // currently defined in the spec. This implementation reflects the
  194. // behavior of NVDA 2020.2/Firefox 81 and iOS VoiceOver/Safari 13.6.
  195. var assignedNodes = slot.assignedNodes();
  196. if (assignedNodes.length === 0) {
  197. // if no nodes are assigned to the slot, it displays the default content
  198. return (0, _array.default)(slot.childNodes);
  199. }
  200. return assignedNodes;
  201. }
  202. /**
  203. * implements https://w3c.github.io/accname/#mapping_additional_nd_te
  204. * @param root
  205. * @param options
  206. * @returns
  207. */
  208. function computeTextAlternative(root) {
  209. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  210. var consultedNodes = new _SetLike.default();
  211. var window = (0, _util.safeWindow)(root);
  212. var _options$compute = options.compute,
  213. compute = _options$compute === void 0 ? "name" : _options$compute,
  214. _options$computedStyl = options.computedStyleSupportsPseudoElements,
  215. computedStyleSupportsPseudoElements = _options$computedStyl === void 0 ? options.getComputedStyle !== undefined : _options$computedStyl,
  216. _options$getComputedS = options.getComputedStyle,
  217. getComputedStyle = _options$getComputedS === void 0 ? window.getComputedStyle.bind(window) : _options$getComputedS,
  218. _options$hidden = options.hidden,
  219. hidden = _options$hidden === void 0 ? false : _options$hidden; // 2F.i
  220. function computeMiscTextAlternative(node, context) {
  221. var accumulatedText = "";
  222. if ((0, _util.isElement)(node) && computedStyleSupportsPseudoElements) {
  223. var pseudoBefore = getComputedStyle(node, "::before");
  224. var beforeContent = getTextualContent(pseudoBefore);
  225. accumulatedText = "".concat(beforeContent, " ").concat(accumulatedText);
  226. } // FIXME: Including aria-owns is not defined in the spec
  227. // But it is required in the web-platform-test
  228. var childNodes = (0, _util.isHTMLSlotElement)(node) ? getSlotContents(node) : (0, _array.default)(node.childNodes).concat((0, _util.queryIdRefs)(node, "aria-owns"));
  229. childNodes.forEach(function (child) {
  230. var result = computeTextAlternative(child, {
  231. isEmbeddedInLabel: context.isEmbeddedInLabel,
  232. isReferenced: false,
  233. recursion: true
  234. }); // TODO: Unclear why display affects delimiter
  235. // see https://github.com/w3c/accname/issues/3
  236. var display = (0, _util.isElement)(child) ? getComputedStyle(child).getPropertyValue("display") : "inline";
  237. var separator = display !== "inline" ? " " : ""; // trailing separator for wpt tests
  238. accumulatedText += "".concat(separator).concat(result).concat(separator);
  239. });
  240. if ((0, _util.isElement)(node) && computedStyleSupportsPseudoElements) {
  241. var pseudoAfter = getComputedStyle(node, "::after");
  242. var afterContent = getTextualContent(pseudoAfter);
  243. accumulatedText = "".concat(accumulatedText, " ").concat(afterContent);
  244. }
  245. return accumulatedText;
  246. }
  247. function computeElementTextAlternative(node) {
  248. if (!(0, _util.isElement)(node)) {
  249. return null;
  250. }
  251. /**
  252. *
  253. * @param element
  254. * @param attributeName
  255. * @returns A string non-empty string or `null`
  256. */
  257. function useAttribute(element, attributeName) {
  258. var attribute = element.getAttributeNode(attributeName);
  259. if (attribute !== null && !consultedNodes.has(attribute) && attribute.value.trim() !== "") {
  260. consultedNodes.add(attribute);
  261. return attribute.value;
  262. }
  263. return null;
  264. } // https://w3c.github.io/html-aam/#fieldset-and-legend-elements
  265. if ((0, _util.isHTMLFieldSetElement)(node)) {
  266. consultedNodes.add(node);
  267. var children = (0, _array.default)(node.childNodes);
  268. for (var i = 0; i < children.length; i += 1) {
  269. var child = children[i];
  270. if ((0, _util.isHTMLLegendElement)(child)) {
  271. return computeTextAlternative(child, {
  272. isEmbeddedInLabel: false,
  273. isReferenced: false,
  274. recursion: false
  275. });
  276. }
  277. }
  278. } else if ((0, _util.isHTMLTableElement)(node)) {
  279. // https://w3c.github.io/html-aam/#table-element
  280. consultedNodes.add(node);
  281. var _children = (0, _array.default)(node.childNodes);
  282. for (var _i = 0; _i < _children.length; _i += 1) {
  283. var _child = _children[_i];
  284. if ((0, _util.isHTMLTableCaptionElement)(_child)) {
  285. return computeTextAlternative(_child, {
  286. isEmbeddedInLabel: false,
  287. isReferenced: false,
  288. recursion: false
  289. });
  290. }
  291. }
  292. } else if ((0, _util.isSVGSVGElement)(node)) {
  293. // https://www.w3.org/TR/svg-aam-1.0/
  294. consultedNodes.add(node);
  295. var _children2 = (0, _array.default)(node.childNodes);
  296. for (var _i2 = 0; _i2 < _children2.length; _i2 += 1) {
  297. var _child2 = _children2[_i2];
  298. if ((0, _util.isSVGTitleElement)(_child2)) {
  299. return _child2.textContent;
  300. }
  301. }
  302. return null;
  303. } else if ((0, _util.getLocalName)(node) === "img" || (0, _util.getLocalName)(node) === "area") {
  304. // https://w3c.github.io/html-aam/#area-element
  305. // https://w3c.github.io/html-aam/#img-element
  306. var nameFromAlt = useAttribute(node, "alt");
  307. if (nameFromAlt !== null) {
  308. return nameFromAlt;
  309. }
  310. } else if ((0, _util.isHTMLOptGroupElement)(node)) {
  311. var nameFromLabel = useAttribute(node, "label");
  312. if (nameFromLabel !== null) {
  313. return nameFromLabel;
  314. }
  315. }
  316. if ((0, _util.isHTMLInputElement)(node) && (node.type === "button" || node.type === "submit" || node.type === "reset")) {
  317. // https://w3c.github.io/html-aam/#input-type-text-input-type-password-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-element-accessible-description-computation
  318. var nameFromValue = useAttribute(node, "value");
  319. if (nameFromValue !== null) {
  320. return nameFromValue;
  321. } // TODO: l10n
  322. if (node.type === "submit") {
  323. return "Submit";
  324. } // TODO: l10n
  325. if (node.type === "reset") {
  326. return "Reset";
  327. }
  328. }
  329. var labels = getLabels(node);
  330. if (labels !== null && labels.length !== 0) {
  331. consultedNodes.add(node);
  332. return (0, _array.default)(labels).map(function (element) {
  333. return computeTextAlternative(element, {
  334. isEmbeddedInLabel: true,
  335. isReferenced: false,
  336. recursion: true
  337. });
  338. }).filter(function (label) {
  339. return label.length > 0;
  340. }).join(" ");
  341. } // https://w3c.github.io/html-aam/#input-type-image-accessible-name-computation
  342. // TODO: wpt test consider label elements but html-aam does not mention them
  343. // We follow existing implementations over spec
  344. if ((0, _util.isHTMLInputElement)(node) && node.type === "image") {
  345. var _nameFromAlt = useAttribute(node, "alt");
  346. if (_nameFromAlt !== null) {
  347. return _nameFromAlt;
  348. }
  349. var nameFromTitle = useAttribute(node, "title");
  350. if (nameFromTitle !== null) {
  351. return nameFromTitle;
  352. } // TODO: l10n
  353. return "Submit Query";
  354. }
  355. return useAttribute(node, "title");
  356. }
  357. function computeTextAlternative(current, context) {
  358. if (consultedNodes.has(current)) {
  359. return "";
  360. } // 2A
  361. if (!hidden && isHidden(current, getComputedStyle) && !context.isReferenced) {
  362. consultedNodes.add(current);
  363. return "";
  364. } // 2B
  365. var labelElements = (0, _util.queryIdRefs)(current, "aria-labelledby");
  366. if (compute === "name" && !context.isReferenced && labelElements.length > 0) {
  367. return labelElements.map(function (element) {
  368. return computeTextAlternative(element, {
  369. isEmbeddedInLabel: context.isEmbeddedInLabel,
  370. isReferenced: true,
  371. // thais isn't recursion as specified, otherwise we would skip
  372. // `aria-label` in
  373. // <input id="myself" aria-label="foo" aria-labelledby="myself"
  374. recursion: false
  375. });
  376. }).join(" ");
  377. } // 2C
  378. // Changed from the spec in anticipation of https://github.com/w3c/accname/issues/64
  379. // spec says we should only consider skipping if we have a non-empty label
  380. var skipToStep2E = context.recursion && isControl(current) && compute === "name";
  381. if (!skipToStep2E) {
  382. var ariaLabel = ((0, _util.isElement)(current) && current.getAttribute("aria-label") || "").trim();
  383. if (ariaLabel !== "" && compute === "name") {
  384. consultedNodes.add(current);
  385. return ariaLabel;
  386. } // 2D
  387. if (!isMarkedPresentational(current)) {
  388. var elementTextAlternative = computeElementTextAlternative(current);
  389. if (elementTextAlternative !== null) {
  390. consultedNodes.add(current);
  391. return elementTextAlternative;
  392. }
  393. }
  394. } // special casing, cheating to make tests pass
  395. // https://github.com/w3c/accname/issues/67
  396. if ((0, _util.hasAnyConcreteRoles)(current, ["menu"])) {
  397. consultedNodes.add(current);
  398. return "";
  399. } // 2E
  400. if (skipToStep2E || context.isEmbeddedInLabel || context.isReferenced) {
  401. if ((0, _util.hasAnyConcreteRoles)(current, ["combobox", "listbox"])) {
  402. consultedNodes.add(current);
  403. var selectedOptions = querySelectedOptions(current);
  404. if (selectedOptions.length === 0) {
  405. // defined per test `name_heading_combobox`
  406. return (0, _util.isHTMLInputElement)(current) ? current.value : "";
  407. }
  408. return (0, _array.default)(selectedOptions).map(function (selectedOption) {
  409. return computeTextAlternative(selectedOption, {
  410. isEmbeddedInLabel: context.isEmbeddedInLabel,
  411. isReferenced: false,
  412. recursion: true
  413. });
  414. }).join(" ");
  415. }
  416. if (hasAbstractRole(current, "range")) {
  417. consultedNodes.add(current);
  418. if (current.hasAttribute("aria-valuetext")) {
  419. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- safe due to hasAttribute guard
  420. return current.getAttribute("aria-valuetext");
  421. }
  422. if (current.hasAttribute("aria-valuenow")) {
  423. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- safe due to hasAttribute guard
  424. return current.getAttribute("aria-valuenow");
  425. } // Otherwise, use the value as specified by a host language attribute.
  426. return current.getAttribute("value") || "";
  427. }
  428. if ((0, _util.hasAnyConcreteRoles)(current, ["textbox"])) {
  429. consultedNodes.add(current);
  430. return getValueOfTextbox(current);
  431. }
  432. } // 2F: https://w3c.github.io/accname/#step2F
  433. if (allowsNameFromContent(current) || (0, _util.isElement)(current) && context.isReferenced || isNativeHostLanguageTextAlternativeElement(current) || isDescendantOfNativeHostLanguageTextAlternativeElement(current)) {
  434. consultedNodes.add(current);
  435. return computeMiscTextAlternative(current, {
  436. isEmbeddedInLabel: context.isEmbeddedInLabel,
  437. isReferenced: false
  438. });
  439. }
  440. if (current.nodeType === current.TEXT_NODE) {
  441. consultedNodes.add(current);
  442. return current.textContent || "";
  443. }
  444. if (context.recursion) {
  445. consultedNodes.add(current);
  446. return computeMiscTextAlternative(current, {
  447. isEmbeddedInLabel: context.isEmbeddedInLabel,
  448. isReferenced: false
  449. });
  450. }
  451. var tooltipAttributeValue = computeTooltipAttributeValue(current);
  452. if (tooltipAttributeValue !== null) {
  453. consultedNodes.add(current);
  454. return tooltipAttributeValue;
  455. } // TODO should this be reachable?
  456. consultedNodes.add(current);
  457. return "";
  458. }
  459. return asFlatString(computeTextAlternative(root, {
  460. isEmbeddedInLabel: false,
  461. // by spec computeAccessibleDescription starts with the referenced elements as roots
  462. isReferenced: compute === "description",
  463. recursion: false
  464. }));
  465. }
  466. //# sourceMappingURL=accessible-name-and-description.js.map