Skip to content

Instantly share code, notes, and snippets.

@oscarmarina
Last active May 24, 2024 09:28
Show Gist options
  • Save oscarmarina/9ce95f491a4c53ed01d989de4a87c0c9 to your computer and use it in GitHub Desktop.
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);
/**
* 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