123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- /**
- * implements https://w3c.github.io/accname/
- */
- import ArrayFrom from "./polyfills/array.from.mjs";
- import SetLike from "./polyfills/SetLike.mjs";
- import { hasAnyConcreteRoles, isElement, isHTMLTableCaptionElement, isHTMLInputElement, isHTMLSelectElement, isHTMLTextAreaElement, safeWindow, isHTMLFieldSetElement, isHTMLLegendElement, isHTMLOptGroupElement, isHTMLTableElement, isHTMLSlotElement, isSVGSVGElement, isSVGTitleElement, queryIdRefs, getLocalName } from "./util.mjs";
- /**
- * A string of characters where all carriage returns, newlines, tabs, and form-feeds are replaced with a single space, and multiple spaces are reduced to a single space. The string contains only character data; it does not contain any markup.
- */
- /**
- *
- * @param {string} string -
- * @returns {FlatString} -
- */
- function asFlatString(s) {
- return s.trim().replace(/\s\s+/g, " ");
- }
- /**
- *
- * @param node -
- * @param options - These are not optional to prevent accidentally calling it without options in `computeAccessibleName`
- * @returns {boolean} -
- */
- function isHidden(node, getComputedStyleImplementation) {
- if (!isElement(node)) {
- return false;
- }
- if (node.hasAttribute("hidden") || node.getAttribute("aria-hidden") === "true") {
- return true;
- }
- var style = getComputedStyleImplementation(node);
- return style.getPropertyValue("display") === "none" || style.getPropertyValue("visibility") === "hidden";
- }
- /**
- * @param {Node} node -
- * @returns {boolean} - As defined in step 2E of https://w3c.github.io/accname/#mapping_additional_nd_te
- */
- function isControl(node) {
- return hasAnyConcreteRoles(node, ["button", "combobox", "listbox", "textbox"]) || hasAbstractRole(node, "range");
- }
- function hasAbstractRole(node, role) {
- if (!isElement(node)) {
- return false;
- }
- switch (role) {
- case "range":
- return hasAnyConcreteRoles(node, ["meter", "progressbar", "scrollbar", "slider", "spinbutton"]);
- default:
- throw new TypeError("No knowledge about abstract role '".concat(role, "'. This is likely a bug :("));
- }
- }
- /**
- * element.querySelectorAll but also considers owned tree
- * @param element
- * @param selectors
- */
- function querySelectorAllSubtree(element, selectors) {
- var elements = ArrayFrom(element.querySelectorAll(selectors));
- queryIdRefs(element, "aria-owns").forEach(function (root) {
- // babel transpiles this assuming an iterator
- elements.push.apply(elements, ArrayFrom(root.querySelectorAll(selectors)));
- });
- return elements;
- }
- function querySelectedOptions(listbox) {
- if (isHTMLSelectElement(listbox)) {
- // IE11 polyfill
- return listbox.selectedOptions || querySelectorAllSubtree(listbox, "[selected]");
- }
- return querySelectorAllSubtree(listbox, '[aria-selected="true"]');
- }
- function isMarkedPresentational(node) {
- return hasAnyConcreteRoles(node, ["none", "presentation"]);
- }
- /**
- * Elements specifically listed in html-aam
- *
- * We don't need this for `label` or `legend` elements.
- * Their implicit roles already allow "naming from content".
- *
- * sources:
- *
- * - https://w3c.github.io/html-aam/#table-element
- */
- function isNativeHostLanguageTextAlternativeElement(node) {
- return isHTMLTableCaptionElement(node);
- }
- /**
- * https://w3c.github.io/aria/#namefromcontent
- */
- function allowsNameFromContent(node) {
- return hasAnyConcreteRoles(node, ["button", "cell", "checkbox", "columnheader", "gridcell", "heading", "label", "legend", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "row", "rowheader", "switch", "tab", "tooltip", "treeitem"]);
- }
- /**
- * TODO https://github.com/eps1lon/dom-accessibility-api/issues/100
- */
- function isDescendantOfNativeHostLanguageTextAlternativeElement( // eslint-disable-next-line @typescript-eslint/no-unused-vars -- not implemented yet
- node) {
- return false;
- }
- /**
- * TODO https://github.com/eps1lon/dom-accessibility-api/issues/101
- */
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- not implemented yet
- function computeTooltipAttributeValue(node) {
- return null;
- }
- function getValueOfTextbox(element) {
- if (isHTMLInputElement(element) || isHTMLTextAreaElement(element)) {
- return element.value;
- } // https://github.com/eps1lon/dom-accessibility-api/issues/4
- return element.textContent || "";
- }
- function getTextualContent(declaration) {
- var content = declaration.getPropertyValue("content");
- if (/^["'].*["']$/.test(content)) {
- return content.slice(1, -1);
- }
- return "";
- }
- /**
- * https://html.spec.whatwg.org/multipage/forms.html#category-label
- * TODO: form-associated custom elements
- * @param element
- */
- function isLabelableElement(element) {
- var localName = getLocalName(element);
- return localName === "button" || localName === "input" && element.getAttribute("type") !== "hidden" || localName === "meter" || localName === "output" || localName === "progress" || localName === "select" || localName === "textarea";
- }
- /**
- * > [...], then the first such descendant in tree order is the label element's labeled control.
- * -- https://html.spec.whatwg.org/multipage/forms.html#labeled-control
- * @param element
- */
- function findLabelableElement(element) {
- if (isLabelableElement(element)) {
- return element;
- }
- var labelableElement = null;
- element.childNodes.forEach(function (childNode) {
- if (labelableElement === null && isElement(childNode)) {
- var descendantLabelableElement = findLabelableElement(childNode);
- if (descendantLabelableElement !== null) {
- labelableElement = descendantLabelableElement;
- }
- }
- });
- return labelableElement;
- }
- /**
- * Polyfill of HTMLLabelElement.control
- * https://html.spec.whatwg.org/multipage/forms.html#labeled-control
- * @param label
- */
- function getControlOfLabel(label) {
- if (label.control !== undefined) {
- return label.control;
- }
- var htmlFor = label.getAttribute("for");
- if (htmlFor !== null) {
- return label.ownerDocument.getElementById(htmlFor);
- }
- return findLabelableElement(label);
- }
- /**
- * Polyfill of HTMLInputElement.labels
- * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/labels
- * @param element
- */
- function getLabels(element) {
- var labelsProperty = element.labels;
- if (labelsProperty === null) {
- return labelsProperty;
- }
- if (labelsProperty !== undefined) {
- return ArrayFrom(labelsProperty);
- } // polyfill
- if (!isLabelableElement(element)) {
- return null;
- }
- var document = element.ownerDocument;
- return ArrayFrom(document.querySelectorAll("label")).filter(function (label) {
- return getControlOfLabel(label) === element;
- });
- }
- /**
- * Gets the contents of a slot used for computing the accname
- * @param slot
- */
- function getSlotContents(slot) {
- // Computing the accessible name for elements containing slots is not
- // currently defined in the spec. This implementation reflects the
- // behavior of NVDA 2020.2/Firefox 81 and iOS VoiceOver/Safari 13.6.
- var assignedNodes = slot.assignedNodes();
- if (assignedNodes.length === 0) {
- // if no nodes are assigned to the slot, it displays the default content
- return ArrayFrom(slot.childNodes);
- }
- return assignedNodes;
- }
- /**
- * implements https://w3c.github.io/accname/#mapping_additional_nd_te
- * @param root
- * @param [options]
- * @param [options.getComputedStyle] - mock window.getComputedStyle. Needs `content`, `display` and `visibility`
- */
- export function computeTextAlternative(root) {
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
- var consultedNodes = new SetLike();
- var window = safeWindow(root);
- var _options$compute = options.compute,
- compute = _options$compute === void 0 ? "name" : _options$compute,
- _options$computedStyl = options.computedStyleSupportsPseudoElements,
- computedStyleSupportsPseudoElements = _options$computedStyl === void 0 ? options.getComputedStyle !== undefined : _options$computedStyl,
- _options$getComputedS = options.getComputedStyle,
- getComputedStyle = _options$getComputedS === void 0 ? window.getComputedStyle.bind(window) : _options$getComputedS; // 2F.i
- function computeMiscTextAlternative(node, context) {
- var accumulatedText = "";
- if (isElement(node) && computedStyleSupportsPseudoElements) {
- var pseudoBefore = getComputedStyle(node, "::before");
- var beforeContent = getTextualContent(pseudoBefore);
- accumulatedText = "".concat(beforeContent, " ").concat(accumulatedText);
- } // FIXME: Including aria-owns is not defined in the spec
- // But it is required in the web-platform-test
- var childNodes = isHTMLSlotElement(node) ? getSlotContents(node) : ArrayFrom(node.childNodes).concat(queryIdRefs(node, "aria-owns"));
- childNodes.forEach(function (child) {
- var result = computeTextAlternative(child, {
- isEmbeddedInLabel: context.isEmbeddedInLabel,
- isReferenced: false,
- recursion: true
- }); // TODO: Unclear why display affects delimiter
- // see https://github.com/w3c/accname/issues/3
- var display = isElement(child) ? getComputedStyle(child).getPropertyValue("display") : "inline";
- var separator = display !== "inline" ? " " : ""; // trailing separator for wpt tests
- accumulatedText += "".concat(separator).concat(result).concat(separator);
- });
- if (isElement(node) && computedStyleSupportsPseudoElements) {
- var pseudoAfter = getComputedStyle(node, "::after");
- var afterContent = getTextualContent(pseudoAfter);
- accumulatedText = "".concat(accumulatedText, " ").concat(afterContent);
- }
- return accumulatedText;
- }
- function computeElementTextAlternative(node) {
- if (!isElement(node)) {
- return null;
- }
- /**
- *
- * @param element
- * @param attributeName
- * @returns A string non-empty string or `null`
- */
- function useAttribute(element, attributeName) {
- var attribute = element.getAttributeNode(attributeName);
- if (attribute !== null && !consultedNodes.has(attribute) && attribute.value.trim() !== "") {
- consultedNodes.add(attribute);
- return attribute.value;
- }
- return null;
- } // https://w3c.github.io/html-aam/#fieldset-and-legend-elements
- if (isHTMLFieldSetElement(node)) {
- consultedNodes.add(node);
- var children = ArrayFrom(node.childNodes);
- for (var i = 0; i < children.length; i += 1) {
- var child = children[i];
- if (isHTMLLegendElement(child)) {
- return computeTextAlternative(child, {
- isEmbeddedInLabel: false,
- isReferenced: false,
- recursion: false
- });
- }
- }
- } else if (isHTMLTableElement(node)) {
- // https://w3c.github.io/html-aam/#table-element
- consultedNodes.add(node);
- var _children = ArrayFrom(node.childNodes);
- for (var _i = 0; _i < _children.length; _i += 1) {
- var _child = _children[_i];
- if (isHTMLTableCaptionElement(_child)) {
- return computeTextAlternative(_child, {
- isEmbeddedInLabel: false,
- isReferenced: false,
- recursion: false
- });
- }
- }
- } else if (isSVGSVGElement(node)) {
- // https://www.w3.org/TR/svg-aam-1.0/
- consultedNodes.add(node);
- var _children2 = ArrayFrom(node.childNodes);
- for (var _i2 = 0; _i2 < _children2.length; _i2 += 1) {
- var _child2 = _children2[_i2];
- if (isSVGTitleElement(_child2)) {
- return _child2.textContent;
- }
- }
- return null;
- } else if (getLocalName(node) === "img" || getLocalName(node) === "area") {
- // https://w3c.github.io/html-aam/#area-element
- // https://w3c.github.io/html-aam/#img-element
- var nameFromAlt = useAttribute(node, "alt");
- if (nameFromAlt !== null) {
- return nameFromAlt;
- }
- } else if (isHTMLOptGroupElement(node)) {
- var nameFromLabel = useAttribute(node, "label");
- if (nameFromLabel !== null) {
- return nameFromLabel;
- }
- }
- if (isHTMLInputElement(node) && (node.type === "button" || node.type === "submit" || node.type === "reset")) {
- // 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
- var nameFromValue = useAttribute(node, "value");
- if (nameFromValue !== null) {
- return nameFromValue;
- } // TODO: l10n
- if (node.type === "submit") {
- return "Submit";
- } // TODO: l10n
- if (node.type === "reset") {
- return "Reset";
- }
- }
- var labels = getLabels(node);
- if (labels !== null && labels.length !== 0) {
- consultedNodes.add(node);
- return ArrayFrom(labels).map(function (element) {
- return computeTextAlternative(element, {
- isEmbeddedInLabel: true,
- isReferenced: false,
- recursion: true
- });
- }).filter(function (label) {
- return label.length > 0;
- }).join(" ");
- } // https://w3c.github.io/html-aam/#input-type-image-accessible-name-computation
- // TODO: wpt test consider label elements but html-aam does not mention them
- // We follow existing implementations over spec
- if (isHTMLInputElement(node) && node.type === "image") {
- var _nameFromAlt = useAttribute(node, "alt");
- if (_nameFromAlt !== null) {
- return _nameFromAlt;
- }
- var nameFromTitle = useAttribute(node, "title");
- if (nameFromTitle !== null) {
- return nameFromTitle;
- } // TODO: l10n
- return "Submit Query";
- }
- return useAttribute(node, "title");
- }
- function computeTextAlternative(current, context) {
- if (consultedNodes.has(current)) {
- return "";
- } // special casing, cheating to make tests pass
- // https://github.com/w3c/accname/issues/67
- if (hasAnyConcreteRoles(current, ["menu"])) {
- consultedNodes.add(current);
- return "";
- } // 2A
- if (isHidden(current, getComputedStyle) && !context.isReferenced) {
- consultedNodes.add(current);
- return "";
- } // 2B
- var labelElements = queryIdRefs(current, "aria-labelledby");
- if (compute === "name" && !context.isReferenced && labelElements.length > 0) {
- return labelElements.map(function (element) {
- return computeTextAlternative(element, {
- isEmbeddedInLabel: context.isEmbeddedInLabel,
- isReferenced: true,
- // thais isn't recursion as specified, otherwise we would skip
- // `aria-label` in
- // <input id="myself" aria-label="foo" aria-labelledby="myself"
- recursion: false
- });
- }).join(" ");
- } // 2C
- // Changed from the spec in anticipation of https://github.com/w3c/accname/issues/64
- // spec says we should only consider skipping if we have a non-empty label
- var skipToStep2E = context.recursion && isControl(current) && compute === "name";
- if (!skipToStep2E) {
- var ariaLabel = (isElement(current) && current.getAttribute("aria-label") || "").trim();
- if (ariaLabel !== "" && compute === "name") {
- consultedNodes.add(current);
- return ariaLabel;
- } // 2D
- if (!isMarkedPresentational(current)) {
- var elementTextAlternative = computeElementTextAlternative(current);
- if (elementTextAlternative !== null) {
- consultedNodes.add(current);
- return elementTextAlternative;
- }
- }
- } // 2E
- if (skipToStep2E || context.isEmbeddedInLabel || context.isReferenced) {
- if (hasAnyConcreteRoles(current, ["combobox", "listbox"])) {
- consultedNodes.add(current);
- var selectedOptions = querySelectedOptions(current);
- if (selectedOptions.length === 0) {
- // defined per test `name_heading_combobox`
- return isHTMLInputElement(current) ? current.value : "";
- }
- return ArrayFrom(selectedOptions).map(function (selectedOption) {
- return computeTextAlternative(selectedOption, {
- isEmbeddedInLabel: context.isEmbeddedInLabel,
- isReferenced: false,
- recursion: true
- });
- }).join(" ");
- }
- if (hasAbstractRole(current, "range")) {
- consultedNodes.add(current);
- if (current.hasAttribute("aria-valuetext")) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- safe due to hasAttribute guard
- return current.getAttribute("aria-valuetext");
- }
- if (current.hasAttribute("aria-valuenow")) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- safe due to hasAttribute guard
- return current.getAttribute("aria-valuenow");
- } // Otherwise, use the value as specified by a host language attribute.
- return current.getAttribute("value") || "";
- }
- if (hasAnyConcreteRoles(current, ["textbox"])) {
- consultedNodes.add(current);
- return getValueOfTextbox(current);
- }
- } // 2F: https://w3c.github.io/accname/#step2F
- if (allowsNameFromContent(current) || isElement(current) && context.isReferenced || isNativeHostLanguageTextAlternativeElement(current) || isDescendantOfNativeHostLanguageTextAlternativeElement(current)) {
- consultedNodes.add(current);
- return computeMiscTextAlternative(current, {
- isEmbeddedInLabel: context.isEmbeddedInLabel,
- isReferenced: false
- });
- }
- if (current.nodeType === current.TEXT_NODE) {
- consultedNodes.add(current);
- return current.textContent || "";
- }
- if (context.recursion) {
- consultedNodes.add(current);
- return computeMiscTextAlternative(current, {
- isEmbeddedInLabel: context.isEmbeddedInLabel,
- isReferenced: false
- });
- }
- var tooltipAttributeValue = computeTooltipAttributeValue(current);
- if (tooltipAttributeValue !== null) {
- consultedNodes.add(current);
- return tooltipAttributeValue;
- } // TODO should this be reachable?
- consultedNodes.add(current);
- return "";
- }
- return asFlatString(computeTextAlternative(root, {
- isEmbeddedInLabel: false,
- // by spec computeAccessibleDescription starts with the referenced elements as roots
- isReferenced: compute === "description",
- recursion: false
- }));
- }
- //# sourceMappingURL=accessible-name-and-description.mjs.map
|