lion/packages/overlays/src/utils/get-focusable-elements.js

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;
}