@base-ui/react
Version:
Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.
211 lines (210 loc) • 7.72 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.disableFocusInside = disableFocusInside;
exports.enableFocusInside = enableFocusInside;
exports.focusable = focusable;
exports.getNextTabbable = getNextTabbable;
exports.getPreviousTabbable = getPreviousTabbable;
exports.getTabbableAfterElement = getTabbableAfterElement;
exports.getTabbableBeforeElement = getTabbableBeforeElement;
exports.isOutsideEvent = isOutsideEvent;
exports.isTabbable = isTabbable;
exports.tabbable = tabbable;
var _dom = require("@floating-ui/utils/dom");
var _owner = require("@base-ui/utils/owner");
var _element = require("./element");
var _composite = require("./composite");
const CANDIDATE_SELECTOR = 'a[href],button,input,select,textarea,summary,details,iframe,object,embed,[tabindex],[contenteditable]:not([contenteditable="false"]),audio[controls],video[controls]';
function getParentElement(element) {
const assignedSlot = element.assignedSlot;
if (assignedSlot) {
return assignedSlot;
}
if (element.parentElement) {
return element.parentElement;
}
const rootNode = element.getRootNode();
return (0, _dom.isShadowRoot)(rootNode) ? rootNode.host : null;
}
function getDetailsSummary(details) {
for (const child of Array.from(details.children)) {
if ((0, _dom.getNodeName)(child) === 'summary') {
return child;
}
}
return null;
}
function isWithinOpenDetailsSummary(element, details) {
const summary = getDetailsSummary(details);
return !!summary && (element === summary || (0, _element.contains)(summary, element));
}
function isFocusableCandidate(element) {
const nodeName = element ? (0, _dom.getNodeName)(element) : '';
return element != null && element.matches(CANDIDATE_SELECTOR) && (nodeName !== 'summary' || element.parentElement != null && (0, _dom.getNodeName)(element.parentElement) === 'details' && getDetailsSummary(element.parentElement) === element) && (nodeName !== 'details' || getDetailsSummary(element) == null) && (nodeName !== 'input' || element.type !== 'hidden');
}
function isFocusableElement(element) {
if (!isFocusableCandidate(element) || !element.isConnected || element.matches(':disabled')) {
return false;
}
for (let current = element; current; current = getParentElement(current)) {
const isAncestor = current !== element;
const isSlot = (0, _dom.getNodeName)(current) === 'slot';
if (current.hasAttribute('inert')) {
return false;
}
if (isAncestor && (0, _dom.getNodeName)(current) === 'details' && !current.open && !isWithinOpenDetailsSummary(element, current) || current.hasAttribute('hidden') || !isSlot && !isVisibleInTabbableTree(current, isAncestor)) {
return false;
}
}
return true;
}
function isVisibleInTabbableTree(element, isAncestor) {
const styles = (0, _dom.getComputedStyle)(element);
if (!isAncestor) {
return (0, _composite.isElementVisible)(element, styles);
}
return styles.display !== 'none';
}
function getTabIndex(element) {
const tabIndex = element.tabIndex;
if (tabIndex < 0) {
const nodeName = (0, _dom.getNodeName)(element);
if (nodeName === 'details' || nodeName === 'audio' || nodeName === 'video' || (0, _dom.isHTMLElement)(element) && element.isContentEditable) {
return 0;
}
}
return tabIndex;
}
function getNamedRadioInput(element) {
if ((0, _dom.getNodeName)(element) !== 'input') {
return null;
}
const input = element;
return input.type === 'radio' && input.name !== '' ? input : null;
}
function isTabbableRadio(element, candidates) {
const input = getNamedRadioInput(element);
if (!input) {
return true;
}
const checkedRadio = candidates.find(candidate => {
const radio = getNamedRadioInput(candidate);
return radio?.name === input.name && radio.form === input.form && radio.checked;
});
if (checkedRadio) {
return checkedRadio === input;
}
return candidates.find(candidate => {
const radio = getNamedRadioInput(candidate);
return radio?.name === input.name && radio.form === input.form;
}) === input;
}
function getComposedChildren(container) {
if ((0, _dom.isHTMLElement)(container) && (0, _dom.getNodeName)(container) === 'slot') {
const assignedElements = container.assignedElements({
flatten: true
});
if (assignedElements.length > 0) {
return assignedElements;
}
}
if ((0, _dom.isHTMLElement)(container) && container.shadowRoot) {
return Array.from(container.shadowRoot.children);
}
return Array.from(container.children);
}
function appendCandidates(container, list) {
getComposedChildren(container).forEach(child => {
if (isFocusableCandidate(child)) {
list.push(child);
}
appendCandidates(child, list);
});
}
function appendMatchingElements(container, selector, list) {
getComposedChildren(container).forEach(child => {
if ((0, _dom.isHTMLElement)(child) && child.matches(selector)) {
list.push(child);
}
appendMatchingElements(child, selector, list);
});
}
function isTabbable(element) {
return isFocusableElement(element) && getTabIndex(element) >= 0;
}
function focusable(container) {
const candidates = [];
appendCandidates(container, candidates);
return candidates.filter(isFocusableElement);
}
function tabbable(container) {
const candidates = focusable(container);
return candidates.filter(element => getTabIndex(element) >= 0 && isTabbableRadio(element, candidates));
}
function getTabbableIn(container, dir) {
const list = tabbable(container);
const len = list.length;
if (len === 0) {
return undefined;
}
const active = (0, _element.activeElement)((0, _owner.ownerDocument)(container));
const index = list.indexOf(active);
// eslint-disable-next-line no-nested-ternary
const nextIndex = index === -1 ? dir === 1 ? 0 : len - 1 : index + dir;
return list[nextIndex];
}
function getNextTabbable(referenceElement) {
return getTabbableIn((0, _owner.ownerDocument)(referenceElement).body, 1) || referenceElement;
}
function getPreviousTabbable(referenceElement) {
return getTabbableIn((0, _owner.ownerDocument)(referenceElement).body, -1) || referenceElement;
}
function getTabbableNearElement(referenceElement, dir) {
if (!referenceElement) {
return null;
}
const list = tabbable((0, _owner.ownerDocument)(referenceElement).body);
const elementCount = list.length;
if (elementCount === 0) {
return null;
}
const index = list.indexOf(referenceElement);
if (index === -1) {
return null;
}
const nextIndex = (index + dir + elementCount) % elementCount;
return list[nextIndex];
}
function getTabbableAfterElement(referenceElement) {
return getTabbableNearElement(referenceElement, 1);
}
function getTabbableBeforeElement(referenceElement) {
return getTabbableNearElement(referenceElement, -1);
}
function isOutsideEvent(event, container) {
const containerElement = container || event.currentTarget;
const relatedTarget = event.relatedTarget;
return !relatedTarget || !(0, _element.contains)(containerElement, relatedTarget);
}
function disableFocusInside(container) {
const tabbableElements = tabbable(container);
tabbableElements.forEach(element => {
element.dataset.tabindex = element.getAttribute('tabindex') || '';
element.setAttribute('tabindex', '-1');
});
}
function enableFocusInside(container) {
const elements = [];
appendMatchingElements(container, '[data-tabindex]', elements);
elements.forEach(element => {
const tabindex = element.dataset.tabindex;
delete element.dataset.tabindex;
if (tabindex) {
element.setAttribute('tabindex', tabindex);
} else {
element.removeAttribute('tabindex');
}
});
}