112 lines
3 KiB
JavaScript
112 lines
3 KiB
JavaScript
/**
|
|
* Implementation based on: https://github.com/PolymerElements/iron-overlay-behavior/blob/master/iron-focusables-helper.html
|
|
* The original implementation does not work for non-Polymer web components,
|
|
* and contains several bugs on IE11.
|
|
*/
|
|
|
|
import { isVisible } from './is-visible.js';
|
|
import { sortByTabIndex } from './sort-by-tabindex.js';
|
|
|
|
// IE11 supports matches as 'msMatchesSelector'
|
|
const matchesFunc = 'matches' in Element.prototype ? 'matches' : 'msMatchesSelector';
|
|
|
|
/**
|
|
* @param {HTMLElement} element
|
|
* @returns {boolean} Whether the element matches
|
|
*/
|
|
function isFocusable(element) {
|
|
// Elements that cannot be focused if they have [disabled] attribute.
|
|
if (element[matchesFunc]('input, select, textarea, button, object')) {
|
|
return element[matchesFunc](':not([disabled])');
|
|
}
|
|
|
|
// Elements that can be focused even if they have [disabled] attribute.
|
|
return element[matchesFunc]('a[href], area[href], iframe, [tabindex], [contentEditable]');
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} element
|
|
* @returns {Number}
|
|
*/
|
|
function getTabindex(element) {
|
|
if (isFocusable(element)) {
|
|
return Number(element.getAttribute('tabindex') || 0);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} element
|
|
*/
|
|
function getChildNodes(element) {
|
|
if (element.localName === 'slot') {
|
|
/** @type {HTMLSlotElement} */
|
|
const slot = element;
|
|
return slot.assignedNodes({ flatten: true });
|
|
}
|
|
|
|
const { children } = element.shadowRoot || element;
|
|
// On IE11, SVGElement.prototype.children is undefined
|
|
return children || [];
|
|
}
|
|
|
|
/**
|
|
* @param {Node} node
|
|
* @returns {boolean}
|
|
*/
|
|
function isVisibleElement(node) {
|
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
return false;
|
|
}
|
|
|
|
// A slot is not visible, but it's children might so we need
|
|
// to treat is as such.
|
|
if (node.localName === 'slot') {
|
|
return true;
|
|
}
|
|
|
|
return isVisible(/** @type {HTMLElement} */ (node));
|
|
}
|
|
|
|
/**
|
|
* Recursive function that traverses the children of the target node and finds
|
|
* elements that can receive focus. Mutates the nodes property for performance.
|
|
*
|
|
* @param {Node} node
|
|
* @param {HTMLElement[]} nodes
|
|
* @returns {boolean} whether the returned node list should be sorted. This happens when
|
|
* there is an element with tabindex > 0
|
|
*/
|
|
function collectFocusableElements(node, nodes) {
|
|
// If not an element or not visible, no need to explore children.
|
|
if (!isVisibleElement(node)) {
|
|
return false;
|
|
}
|
|
|
|
/** @type {HTMLElement} */
|
|
const element = node;
|
|
|
|
const tabIndex = getTabindex(element);
|
|
let needsSort = tabIndex > 0;
|
|
if (tabIndex >= 0) {
|
|
nodes.push(element);
|
|
}
|
|
|
|
const childNodes = getChildNodes(element);
|
|
for (let i = 0; i < childNodes.length; i += 1) {
|
|
needsSort = collectFocusableElements(childNodes[i], nodes) || needsSort;
|
|
}
|
|
return needsSort;
|
|
}
|
|
|
|
/**
|
|
* @param {Node} node
|
|
* @returns {HTMLElement[]}
|
|
*/
|
|
export function getFocusableElements(node) {
|
|
/** @type {HTMLElement[]} */
|
|
const nodes = [];
|
|
|
|
const needsSort = collectFocusableElements(node, nodes);
|
|
return needsSort ? sortByTabIndex(nodes) : nodes;
|
|
}
|