|
|
"use strict";
exports.__esModule = true; exports.computeTextAlternative = computeTextAlternative;
var _array = _interopRequireDefault(require("./polyfills/array.from"));
var _SetLike = _interopRequireDefault(require("./polyfills/SetLike"));
var _util = require("./util");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/** * implements https://w3c.github.io/accname/
*/
/** * * @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 (!(0, _util.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 (0, _util.hasAnyConcreteRoles)(node, ["button", "combobox", "listbox", "textbox"]) || hasAbstractRole(node, "range"); }
function hasAbstractRole(node, role) { if (!(0, _util.isElement)(node)) { return false; }
switch (role) { case "range": return (0, _util.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 = (0, _array.default)(element.querySelectorAll(selectors)); (0, _util.queryIdRefs)(element, "aria-owns").forEach(function (root) { // babel transpiles this assuming an iterator
elements.push.apply(elements, (0, _array.default)(root.querySelectorAll(selectors))); }); return elements; }
function querySelectedOptions(listbox) { if ((0, _util.isHTMLSelectElement)(listbox)) { // IE11 polyfill
return listbox.selectedOptions || querySelectorAllSubtree(listbox, "[selected]"); }
return querySelectorAllSubtree(listbox, '[aria-selected="true"]'); }
function isMarkedPresentational(node) { return (0, _util.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 (0, _util.isHTMLTableCaptionElement)(node); } /** * https://w3c.github.io/aria/#namefromcontent
*/
function allowsNameFromContent(node) { 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"]); } /** * 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 ((0, _util.isHTMLInputElement)(element) || (0, _util.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 = (0, _util.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 && (0, _util.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 (0, _array.default)(labelsProperty); }
if (!isLabelableElement(element)) { return null; }
var document = element.ownerDocument; return (0, _array.default)(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 (0, _array.default)(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` */
function computeTextAlternative(root) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var consultedNodes = new _SetLike.default(); var window = (0, _util.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 ((0, _util.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 = (0, _util.isHTMLSlotElement)(node) ? getSlotContents(node) : (0, _array.default)(node.childNodes).concat((0, _util.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 = (0, _util.isElement)(child) ? getComputedStyle(child).getPropertyValue("display") : "inline"; var separator = display !== "inline" ? " " : ""; // trailing separator for wpt tests
accumulatedText += "".concat(separator).concat(result).concat(separator); });
if ((0, _util.isElement)(node) && computedStyleSupportsPseudoElements) { var pseudoAfter = getComputedStyle(node, "::after"); var afterContent = getTextualContent(pseudoAfter); accumulatedText = "".concat(accumulatedText, " ").concat(afterContent); }
return accumulatedText; }
function computeElementTextAlternative(node) { if (!(0, _util.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 ((0, _util.isHTMLFieldSetElement)(node)) { consultedNodes.add(node); var children = (0, _array.default)(node.childNodes);
for (var i = 0; i < children.length; i += 1) { var child = children[i];
if ((0, _util.isHTMLLegendElement)(child)) { return computeTextAlternative(child, { isEmbeddedInLabel: false, isReferenced: false, recursion: false }); } } } else if ((0, _util.isHTMLTableElement)(node)) { // https://w3c.github.io/html-aam/#table-element
consultedNodes.add(node);
var _children = (0, _array.default)(node.childNodes);
for (var _i = 0; _i < _children.length; _i += 1) { var _child = _children[_i];
if ((0, _util.isHTMLTableCaptionElement)(_child)) { return computeTextAlternative(_child, { isEmbeddedInLabel: false, isReferenced: false, recursion: false }); } } } else if ((0, _util.isSVGSVGElement)(node)) { // https://www.w3.org/TR/svg-aam-1.0/
consultedNodes.add(node);
var _children2 = (0, _array.default)(node.childNodes);
for (var _i2 = 0; _i2 < _children2.length; _i2 += 1) { var _child2 = _children2[_i2];
if ((0, _util.isSVGTitleElement)(_child2)) { return _child2.textContent; } }
return null; } else if ((0, _util.getLocalName)(node) === "img" || (0, _util.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; } }
if ((0, _util.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"; } }
if ((0, _util.isHTMLInputElement)(node) || (0, _util.isHTMLSelectElement)(node) || (0, _util.isHTMLTextAreaElement)(node)) { var input = node; var labels = getLabels(input);
if (labels !== null && labels.length !== 0) { consultedNodes.add(input); return (0, _array.default)(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 ((0, _util.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 ((0, _util.hasAnyConcreteRoles)(current, ["menu"])) { consultedNodes.add(current); return ""; } // 2A
if (isHidden(current, getComputedStyle) && !context.isReferenced) { consultedNodes.add(current); return ""; } // 2B
var labelElements = (0, _util.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 = ((0, _util.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 ((0, _util.hasAnyConcreteRoles)(current, ["combobox", "listbox"])) { consultedNodes.add(current); var selectedOptions = querySelectedOptions(current);
if (selectedOptions.length === 0) { // defined per test `name_heading_combobox`
return (0, _util.isHTMLInputElement)(current) ? current.value : ""; }
return (0, _array.default)(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 ((0, _util.hasAnyConcreteRoles)(current, ["textbox"])) { consultedNodes.add(current); return getValueOfTextbox(current); } } // 2F: https://w3c.github.io/accname/#step2F
if (allowsNameFromContent(current) || (0, _util.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.js.map
|