ariakit-utils
Version:
Ariakit utils
282 lines (261 loc) • 8.88 kB
JavaScript
'use strict';
/**
* It's `true` if it is running in a browser environment or `false` if it is not
* (SSR).
* @example
* const title = canUseDOM ? document.title : "";
*/
const canUseDOM = checkIsBrowser();
// Check if we can use the DOM. Useful for SSR purposes
function checkIsBrowser() {
return typeof window !== "undefined" && !!window.document?.createElement;
}
/**
* Returns `element.ownerDocument || document`.
*/
function getDocument(node) {
return node ? node.ownerDocument || node : document;
}
/**
* Returns `element.ownerDocument.defaultView || window`.
*/
function getWindow(node) {
return getDocument(node).defaultView || window;
}
/**
* Returns `element.ownerDocument.activeElement`.
*/
function getActiveElement(node, activeDescendant) {
if (activeDescendant === void 0) {
activeDescendant = false;
}
const {
activeElement
} = getDocument(node);
if (!activeElement?.nodeName) {
// In IE11, activeElement might be an empty object if we're interacting
// with elements inside of an iframe.
return null;
}
if (isFrame(activeElement) && activeElement.contentDocument) {
return getActiveElement(activeElement.contentDocument.body, activeDescendant);
}
if (activeDescendant) {
const id = activeElement.getAttribute("aria-activedescendant");
if (id) {
const element = getDocument(activeElement).getElementById(id);
if (element) {
return element;
}
}
}
return activeElement;
}
/**
* Similar to `Element.prototype.contains`, but a little bit faster when
* `element` is the same as `child`.
* @example
* contains(
* document.getElementById("parent"),
* document.getElementById("child")
* );
*/
function contains(parent, child) {
return parent === child || parent.contains(child);
}
/**
* Checks whether `element` is a frame element.
*/
function isFrame(element) {
return element.tagName === "IFRAME";
}
/**
* Checks whether `element` is a native HTML button element.
* @example
* isButton(document.querySelector("button")); // true
* isButton(document.querySelector("input[type='button']")); // true
* isButton(document.querySelector("div")); // false
* isButton(document.querySelector("input[type='text']")); // false
* isButton(document.querySelector("div[role='button']")); // false
*/
function isButton(element) {
const tagName = element.tagName.toLowerCase();
if (tagName === "button") return true;
if (tagName === "input" && element.type) {
return buttonInputTypes.indexOf(element.type) !== -1;
}
return false;
}
const buttonInputTypes = ["button", "color", "file", "image", "reset", "submit"];
/**
* Ponyfill for `Element.prototype.matches`
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
*/
function matches(element, selectors) {
if ("matches" in element) {
return element.matches(selectors);
}
if ("msMatchesSelector" in element) {
return element.msMatchesSelector(selectors);
}
return element.webkitMatchesSelector(selectors);
}
/**
* Checks if the element is visible or not.
*/
function isVisible(element) {
const htmlElement = element;
return htmlElement.offsetWidth > 0 || htmlElement.offsetHeight > 0 || element.getClientRects().length > 0;
}
/**
* Ponyfill for `Element.prototype.closest`
* @example
* closest(document.getElementById("id"), "div");
* // same as
* document.getElementById("id").closest("div");
*/
function closest(element, selectors) {
if ("closest" in element) return element.closest(selectors);
do {
if (matches(element, selectors)) return element;
element = element.parentElement || element.parentNode;
} while (element !== null && element.nodeType === 1);
return null;
}
/**
* Check whether the given element is a text field, where text field is defined
* by the ability to select within the input.
* @example
* isTextField(document.querySelector("div")); // false
* isTextField(document.querySelector("input")); // true
* isTextField(document.querySelector("input[type='button']")); // false
* isTextField(document.querySelector("textarea")); // true
*/
function isTextField(element) {
try {
const isTextInput = element instanceof HTMLInputElement && element.selectionStart !== null;
const isTextArea = element.tagName === "TEXTAREA";
return isTextInput || isTextArea || false;
} catch (error) {
// Safari throws an exception when trying to get `selectionStart` on
// non-text <input> elements (which, understandably, don't have the text
// selection API). We catch this via a try/catch block, as opposed to a more
// explicit check of the element's input types, because of Safari's
// non-standard behavior. This also means we don't have to worry about the
// list of input types that support `selectionStart` changing as the HTML
// spec evolves over time.
return false;
}
}
/**
* Returns the element's role attribute, if it has one.
*/
function getPopupRole(element, fallback) {
const allowedPopupRoles = ["dialog", "menu", "listbox", "tree", "grid"];
const role = element?.getAttribute("role");
if (role && allowedPopupRoles.indexOf(role) !== -1) {
return role;
}
return fallback;
}
/**
* Returns the item role attribute based on the popup's role.
*/
function getPopupItemRole(element, fallback) {
const itemRoleByPopupRole = {
menu: "menuitem",
listbox: "option",
tree: "treeitem",
grid: "gridcell"
};
const popupRole = getPopupRole(element);
if (!popupRole) return fallback;
const key = popupRole;
return itemRoleByPopupRole[key] ?? fallback;
}
/**
* Returns the start and end offsets of the selection in the element.
*/
function getTextboxSelection(element) {
let start = 0;
let end = 0;
if (isTextField(element)) {
start = element.selectionStart || 0;
end = element.selectionEnd || 0;
} else if (element.isContentEditable) {
const selection = getDocument(element).getSelection();
if (selection?.rangeCount && selection.anchorNode && contains(element, selection.anchorNode) && selection.focusNode && contains(element, selection.focusNode)) {
const range = selection.getRangeAt(0);
const nextRange = range.cloneRange();
nextRange.selectNodeContents(element);
nextRange.setEnd(range.startContainer, range.startOffset);
start = nextRange.toString().length;
nextRange.setEnd(range.endContainer, range.endOffset);
end = nextRange.toString().length;
}
}
return {
start,
end
};
}
/**
* Calls `element.scrollIntoView()` if the element is hidden or partly hidden in
* the viewport.
*/
function scrollIntoViewIfNeeded(element, arg) {
if (isPartiallyHidden(element) && "scrollIntoView" in element) {
element.scrollIntoView(arg);
}
}
/**
* Returns the scrolling container element of a given element.
*/
function getScrollingElement(element) {
if (!element) return null;
if (element.clientHeight && element.scrollHeight > element.clientHeight) {
const {
overflowY
} = getComputedStyle(element);
const isScrollable = overflowY !== "visible" && overflowY !== "hidden";
if (isScrollable) return element;
}
return getScrollingElement(element.parentElement) || document.scrollingElement || document.body;
}
/**
* Determines whether an element is hidden or partially hidden in the viewport.
*/
function isPartiallyHidden(element) {
const elementRect = element.getBoundingClientRect();
const scroller = getScrollingElement(element);
if (!scroller) return false;
const scrollerRect = scroller.getBoundingClientRect();
const isHTML = scroller.tagName === "HTML";
const scrollerTop = isHTML ? scrollerRect.top + scroller.scrollTop : scrollerRect.top;
const scrollerBottom = isHTML ? scroller.clientHeight : scrollerRect.bottom;
const scrollerLeft = isHTML ? scrollerRect.left + scroller.scrollLeft : scrollerRect.left;
const scrollerRight = isHTML ? scroller.clientWidth : scrollerRect.right;
const top = elementRect.top < scrollerTop;
const left = elementRect.left < scrollerLeft;
const bottom = elementRect.bottom > scrollerBottom;
const right = elementRect.right > scrollerRight;
return top || left || bottom || right;
}
exports.canUseDOM = canUseDOM;
exports.closest = closest;
exports.contains = contains;
exports.getActiveElement = getActiveElement;
exports.getDocument = getDocument;
exports.getPopupItemRole = getPopupItemRole;
exports.getPopupRole = getPopupRole;
exports.getScrollingElement = getScrollingElement;
exports.getTextboxSelection = getTextboxSelection;
exports.getWindow = getWindow;
exports.isButton = isButton;
exports.isFrame = isFrame;
exports.isPartiallyHidden = isPartiallyHidden;
exports.isTextField = isTextField;
exports.isVisible = isVisible;
exports.matches = matches;
exports.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded;