@blockquote/dev-utilities
v1.0.1
Published
Developer utilities for handling DOM, events, and other web development tasks
Downloads
44
Maintainers
Readme
Developer utilities for handling DOM, events, and other web development tasks
This repository provides a set of utility functions designed to facilitate DOM manipulation, event handling, and other common tasks in web development. These functions address various needs such as re-dispatching events, checking element visibility, and traversing the DOM tree.
Table of Contents
- Re-dispatch Event Functions
- Element Visibility and Focus Functions
- DOM Traversal Functions
- Event Handling Functions
- Miscellaneous Functions
Re-dispatch Event Functions
redispatchEventFromEvent
Re-dispatches an event from the provided element. This function stops the propagation of bubbling events and dispatches a copy of the event from the specified element.
/**
* Re-dispatches an event from the provided element.
*
* @param {Element} element - The element to dispatch the event from.
* @param {Event} ev - The event to re-dispatch.
* @param {Object} [options={}] - An object with properties to override in the new event.
* @returns {boolean} - Whether or not the event was dispatched (if cancelable).
*/
const redispatchEventFromEvent = (element, ev, options = {}) => {
if (ev.bubbles && (!element.shadowRoot || ev.composed)) {
ev.stopPropagation();
}
const copy = Reflect.construct(ev.constructor, [ev.type, { ...ev, ...options }]);
const dispatched = element.dispatchEvent(copy);
if (!dispatched) {
ev.preventDefault();
}
return dispatched;
};
redispatchEvent
This function simplifies re-dispatching an event. If the event parameter is a string, it creates a new CustomEvent
and dispatches it.
/**
* Re-dispatches an event from the provided element.
*
* @param {Element} element - The element to dispatch the event from.
* @param {Event|string} ev - The event to re-dispatch. If it's a string, a new Event is created.
* @param {Object} [options={}] - An object with properties to override in the new event.
* @returns {boolean} - Whether or not the event was dispatched (if cancelable).
*/
export const redispatchEvent = (element, ev, options = {}) => {
if (typeof ev === 'string') {
const eventType = ev;
const newEvent = new CustomEvent(eventType);
return redispatchEventFromEvent(element, newEvent, options);
}
return redispatchEventFromEvent(element, ev, options);
};
Element Visibility and Focus Functions
isElementInvisible
Checks if an element should be ignored based on its visibility or specific attributes.
/**
* 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.
*/
export 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;
};
isFocusable
Determines if an element is focusable based on standard criteria or custom element properties.
/**
* Checks if an element is focusable.
*
* @param {Element} element - The DOM element to check for focusability.
* @returns {boolean} True if the element is focusable, false otherwise.
*/
export const isFocusable = element => {
if (!(element instanceof HTMLElement)) {
return false;
}
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^="-"]),audio[controls],video[controls]`;
if (element.matches(knownFocusableElements)) {
return true;
}
const isDisabledCustomElement =
element.localName.includes('-') && element.matches('[disabled], [aria-disabled="true"]');
if (isDisabledCustomElement) {
return false;
}
return /** @type {ShadowRoot | *} */ (element.shadowRoot)?.delegatesFocus ?? false;
};
getFirstAndLastFocusableChildren
Retrieves the first and last focusable children of a node using a TreeWalker.
/**
* Retrieves the first and last focusable children of a node.
*
* @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.
*/
export const getFirstAndLastFocusableChildren = walker => {
let firstFocusableChild = null;
let lastFocusableChild = null;
for (const currentNode of walker) {
if (!firstFocusableChild) {
firstFocusableChild = currentNode;
}
lastFocusableChild = currentNode;
}
return [firstFocusableChild, lastFocusableChild];
};
DOM Traversal Functions
walkComposedTree
Traverse the composed tree from the root, selecting elements that meet the provided filter criteria.
/**
* Traverse the composed tree from the root, selecting elements that meet the provided filter criteria.
*
* @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.
*/
export function* walkComposedTree(
node,
whatToShow = 0,
filter = () => true,
skipNode = () => false,
) {
if ((whatToShow && node.nodeType !== whatToShow) || skipNode(node)) {
return;
}
if (filter(node)) {
yield node;
}
const children =
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);
}
}
getDeepActiveElement
Returns the deepest active element considering Shadow DOM subtrees.
/**
* Returns the deepest active element, considering Shadow DOM subtrees.
*
* @param {Document | ShadowRoot} root - The root element to start the search from.
* @returns {Element} The deepest active element or body element if no active element is found.
*/
export const getDeepActiveElement = (root = document) => {
const activeEl = root?.activeElement;
if (activeEl) {
if (activeEl.shadowRoot) {
return getDeepActiveElement(activeEl.shadowRoot) ?? activeEl;
}
return activeEl;
}
return document.body;
};
deepContains
Checks if a container node contains the target node, considering Shadow DOM boundaries.
/**
* Returns true if the first node contains the second, even if the second node is in a shadow tree.
*
* @param {Node} container - The container to search within.
* @param {Node} target - The node that may be inside the container.
* @returns {boolean} - True if the container contains the target node.
*/
export const deepContains = (container, target) => {
let current = target;
while (current) {
const parent = current.assignedSlot || current.parentNode || current.host;
if (parent === container) {
return true;
}
current = parent;
}
return false;
};
composedAncestors
Yields the ancestors of the given node in the composed tree, considering Shadow DOM
.
/**
* Return the ancestors of the given node in the composed tree.
*
* @param {Node} node - The node to find ancestors for.
* @returns {Iterable<Node>} - The ancestors in the composed tree.
*/
export function* composedAncestors(node) {
for (let current = node; current; ) {
const next =
current instanceof HTMLElement && current.assignedSlot
? current.assignedSlot
: current instanceof ShadowRoot
? current.host
: current.parentNode;
if (next) {
yield next;
}
current = next;
}
}
Event Handling Functions
isClickInsideRect
Checks if a click event occurred inside a given bounding rectangle.
/**
* Checks if a click event occurred inside a given bounding rectangle.
*
* @param {DOMRect} rect - The bounding rectangle.
* @param {PointerEvent} ev - The click event.
* @returns {boolean} True if the click occurred inside the rectangle, false otherwise.
*/
export const isClickInsideRect = (rect, ev) => {
const { top, left, height, width } = rect;
const { clientX, clientY } = ev;
return clientY >= top && clientY <= top + height && clientX >= left && clientX <= left + width;
};
Miscellaneous Functions
randomID
Generates a random alphanumeric string of a specified length.
/**
* Generates a random alphanumeric string of a specified length.
*
* @param {number} [length=10] - The length of the random string to generate. Default is 10.
* @returns {string} A random alphanumeric string of the specified length.
*/
export const randomID = (length = 10) => Math.random().toString(36).substring(2, length);
urlToPlainObject
Converts a URL object to a plain object with its properties as key-value pairs.
/**
* Converts a URL object to a plain object.
*
* @param {URL|string} url - The URL object to parse.
* @returns {Object} An object representing the parsed URL.
*/
export function urlToPlainObject(url) {
const urlObject = typeof url === 'string' ? new URL(url) : url;
const plainObject = {};
// eslint-disable-next-line no-restricted-syntax
for (const key in urlObject) {
if (typeof urlObject[key] === 'string') {
plainObject[key] = urlObject[key];
}
}
return plainObject;
}
Author Information
- Original Authors: @material/web, Jan Miksovsky, Cory LaViska