Last active
May 24, 2024 09:28
-
-
Save oscarmarina/9ce95f491a4c53ed01d989de4a87c0c9 to your computer and use it in GitHub Desktop.
Checks if an element is focusable - treewalker = walkComposedTree(this, NodeFilter.SHOW_ELEMENT, isFocusable);
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Checks if an element should be ignored. | |
* @param {Element} element - The DOM element to check. | |
* @param {Array} [exceptions=['dialog', '[popover]']] - Array of Elements to ignore when checking the element. | |
* @returns {boolean} True if the element should be ignored by a screen reader, false otherwise. | |
*/ | |
const isElementInvisible = (element, exceptions = ['dialog', '[popover]']) => { | |
if (!element || !(element instanceof HTMLElement)) { | |
return false; | |
} | |
if (element.matches(exceptions.join(','))) { | |
return false; | |
} | |
const computedStyle = window.getComputedStyle(element); | |
const isStyleHidden = computedStyle.display === 'none' || computedStyle.visibility === 'hidden'; | |
const isAttributeHidden = element.matches('[disabled], [hidden], [inert], [aria-hidden="true"]'); | |
return isStyleHidden || isAttributeHidden; | |
}; | |
/** | |
* Checks if an element is focusable. An element is considered focusable if it matches | |
* standard focusable elements criteria (such as buttons, inputs, etc., that are not disabled | |
* and do not have a negative tabindex) or is a custom element with a shadow root that delegates focus. | |
* | |
* @param {Element} element - The DOM element to check for focusability. | |
* @returns {boolean} True if the element is focusable, false otherwise. | |
*/ | |
const isFocusable = element => { | |
if (!(element instanceof HTMLElement)) { | |
return false; | |
} | |
// https://stackoverflow.com/a/30753870/76472 | |
const knownFocusableElements = | |
'a[href],area[href],button:not([disabled]),details,iframe,object,input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[contentEditable="true"],[tabindex]:not([tabindex^="-"])'; | |
if (element.matches(knownFocusableElements)) { | |
return true; | |
} | |
const isDisabledCustomElement = | |
element.localName.includes('-') && element.matches('[disabled], [aria-disabled="true"]'); | |
if (isDisabledCustomElement) { | |
return false; | |
} | |
return element.shadowRoot?.delegatesFocus ?? false; | |
}; | |
/** | |
* Retrieves the first and last focusable children of a node using a TreeWalker. | |
* | |
* @param {IterableIterator<HTMLElement>} walker - The TreeWalker object used to traverse the node's children. | |
* @returns {[first: HTMLElement|null, last: HTMLElement|null]} An object containing the first and last focusable children. If no focusable children are found, `null` is returned for both. | |
*/ | |
const getFirstAndLastFocusableChildren = walker => { | |
let firstFocusableChild = null; | |
let lastFocusableChild = null; | |
for (const currentNode of walker) { | |
if (!firstFocusableChild) { | |
firstFocusableChild = currentNode; | |
} | |
lastFocusableChild = currentNode; | |
} | |
return [firstFocusableChild, lastFocusableChild]; | |
}; | |
/** | |
* Traverse the composed tree from the root, selecting elements that meet the provided filter criteria. | |
* You can pass [NodeFilter](https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter) or 0 to retrieve all nodes. | |
* @author Jan Miksovsky | |
* @see https://github.com/JanMiksovsky/elix/blob/main/src/core/dom.js#L320 | |
* @param {Node} node - The root node for traversal. | |
* @param {number} [whatToShow=0] - NodeFilter code for node types to include. | |
* @param {function} [filter=(n: Node) => true] - Filters nodes. Child nodes are considered even if parent does not satisfy the filter. | |
* @param {function} [skipNode=(n: Node) => false] - Determines whether to skip a node and its children. | |
* @returns {IterableIterator<Node>} An iterator yielding nodes meeting the filter criteria. | |
*/ | |
function* walkComposedTree(node, whatToShow = 0, filter = () => true, skipNode = () => false) { | |
if ((whatToShow && node.nodeType !== whatToShow) || skipNode(node)) { | |
return; | |
} | |
if (filter(node)) { | |
yield node; | |
} | |
const children = | |
// eslint-disable-next-line no-nested-ternary | |
node instanceof HTMLElement && node.shadowRoot | |
? node.shadowRoot.children | |
: node instanceof HTMLSlotElement | |
? node.assignedNodes({ flatten: true }) | |
: node.childNodes; | |
for (const child of children) { | |
yield* walkComposedTree(child, whatToShow, filter, skipNode); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment