accessibility-developer-tools
Version:
This is a library of accessibility-related testing and utility code.
213 lines (190 loc) • 7.65 kB
JavaScript
// Copyright 2015 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('axs.dom');
/**
* Returns the nearest ancestor which is an Element.
* @param {Node} node
* @return {?Element}
*/
axs.dom.parentElement = function(node) {
if (!node)
return null;
var parentNode = axs.dom.composedParentNode(node);
if (!parentNode)
return null;
switch (parentNode.nodeType) {
case Node.ELEMENT_NODE:
return /** @type {Element} */ (parentNode);
default:
return axs.dom.parentElement(parentNode);
}
};
/**
* Returns the shadow host of a document fragment if it is a Shadow DOM fragment
* otherwise returns `null`.
* @param {DocumentFragment} fragment
* @return {?Element}
*/
axs.dom.shadowHost = function(fragment) {
// If host exists, this is a Shadow DOM fragment.
if ('host' in fragment)
return fragment.host;
else
return null;
};
/**
* Returns the given Node's parent in the composed tree.
* @param {Node} node
* @return {?Node}
*/
axs.dom.composedParentNode = function(node) {
if (!node)
return null;
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE)
return axs.dom.shadowHost(/** @type {DocumentFragment} */ (node));
var parentNode = node.parentNode;
if (!parentNode)
return null;
if (parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE)
return axs.dom.shadowHost(/** @type {DocumentFragment} */ (parentNode));
if (!parentNode.shadowRoot)
return parentNode;
// Shadow DOM v1
if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
var assignedSlot = node.assignedSlot;
if (HTMLSlotElement && assignedSlot instanceof HTMLSlotElement)
return axs.dom.composedParentNode(assignedSlot);
}
// Shadow DOM v0
if (typeof node.getDestinationInsertionPoints === 'function') {
var insertionPoints = node.getDestinationInsertionPoints();
if (insertionPoints.length > 0)
return axs.dom.composedParentNode(insertionPoints[insertionPoints.length - 1]);
}
return null;
};
/**
* Return the corresponding element for the given node.
* @param {Node} node
* @return {Element}
* @suppress {checkTypes}
*/
axs.dom.asElement = function(node) {
/** @type {Element} */ var element;
switch (node.nodeType) {
case Node.COMMENT_NODE:
return null; // Skip comments
case Node.ELEMENT_NODE:
element = /** (@type {Element}) */ node;
if (element.localName == 'script' ||
element.localName == 'template')
return null; // Skip script-supporting elements
return element;
case Node.DOCUMENT_FRAGMENT_NODE:
return node.host;
case Node.TEXT_NODE:
return axs.dom.parentElement(node);
default:
console.warn('Unhandled node type: ', node.nodeType);
}
return null;
};
/**
* Recursively walk the composed tree from |node|, aborting if |end| is encountered.
* @param {Node} node
* @param {?Node} end
* @param {{preorder: (function (Node, Object):boolean|undefined),
* postorder: (function (Node, Object)|undefined)}} callbacks
* Callbacks to be called for each element traversed, excluding
* |end|. Possible callbacks are |preorder|, called before descending into
* child nodes, and |postorder| called after all child nodes have been
* traversed. If |preorder| returns false, its child nodes will not be
* traversed.
* @param {Object} parentFlags
* @param {ShadowRoot=} opt_shadowRoot The nearest ShadowRoot ancestor, if any.
* @return {boolean} Whether |end| was found, if provided.
*/
axs.dom.composedTreeSearch = function(node, end, callbacks, parentFlags, opt_shadowRoot) {
if (node === end)
return true;
if (node.nodeType == Node.ELEMENT_NODE)
var element = /** @type {Element} */ (node);
var found = false;
var flags = Object.create(parentFlags);
// Descend into node:
// If it has a ShadowRoot, ignore all child elements - these will be picked
// up by the <content> or <shadow> elements. Descend straight into the
// ShadowRoot.
if (element) {
var localName = element.localName;
if (flags.collectIdRefs) {
flags.idrefs = axs.utils.getReferencedIds(element);
}
if (!flags.disabled || (localName === 'legend') && axs.browserUtils.matchSelector(element, 'fieldset>legend:first-of-type')) {
flags.disabled = axs.utils.isElementDisabled(element, true);
}
if (!flags.hidden) {
flags.hidden = axs.utils.isElementHidden(element);
}
if (callbacks.preorder) {
if (!callbacks.preorder(element, flags))
return found;
}
// NOTE: grunt qunit DOES NOT support Shadow DOM, so if changing this
// code, be sure to run the tests in the browser before committing.
var shadowRoot = element.shadowRoot || element.webkitShadowRoot;
if (shadowRoot) {
flags.level++;
found = axs.dom.composedTreeSearch(shadowRoot,
end,
callbacks,
flags,
shadowRoot);
if (element && callbacks.postorder && !found)
callbacks.postorder(element, flags);
return found;
}
// If it is a <content> element, descend into distributed elements - these
// are elements from outside the shadow root which are rendered inside the
// shadow DOM.
if (localName == 'content' && typeof element.getDistributedNodes === 'function') {
var content = /** @type {HTMLContentElement} */ (element);
var distributedNodes = content.getDistributedNodes();
for (var i = 0; i < distributedNodes.length && !found; i++) {
found = axs.dom.composedTreeSearch(distributedNodes[i],
end,
callbacks,
flags,
opt_shadowRoot);
}
if (callbacks.postorder && !found)
callbacks.postorder.call(null, element, flags);
return found;
}
}
// If it is neither the parent of a ShadowRoot, a <content> element, nor
// a <shadow> element recurse normally.
var child = node.firstChild;
while (child != null && !found) {
found = axs.dom.composedTreeSearch(child,
end,
callbacks,
flags,
opt_shadowRoot);
child = child.nextSibling;
}
if (element && callbacks.postorder && !found)
callbacks.postorder.call(null, element, flags);
return found;
};