UNPKG

@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
"use strict"; 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'); } }); }