You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

608 lines
18 KiB

/**
* 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
* @returns
*/
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,
_options$hidden = options.hidden,
hidden = _options$hidden === void 0 ? false : _options$hidden; // 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.trim();
}
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";
}
if (hasAnyConcreteRoles(node, ["button"])) {
// https://www.w3.org/TR/html-aam-1.0/#button-element
var nameFromSubTree = computeMiscTextAlternative(node, {
isEmbeddedInLabel: false,
isReferenced: false
});
if (nameFromSubTree !== "") {
return nameFromSubTree;
}
return useAttribute(node, "title");
}
return useAttribute(node, "title");
}
function computeTextAlternative(current, context) {
if (consultedNodes.has(current)) {
return "";
} // 2A
if (!hidden && 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;
}
}
} // special casing, cheating to make tests pass
// https://github.com/w3c/accname/issues/67
if (hasAnyConcreteRoles(current, ["menu"])) {
consultedNodes.add(current);
return "";
} // 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