@clayui/shared
Version:
ClayShared component
206 lines (205 loc) • 6.67 kB
JavaScript
import React from "react";
const HostComponent = 5;
let minimalTabIndex = 0;
function isFocusable({
contentEditable,
disabled,
href,
offsetParent,
rel,
tabIndex,
tagName,
type
}) {
tagName = tagName?.toLowerCase();
if (!offsetParent) {
return false;
}
if (disabled) {
return false;
}
if (tabIndex !== null && tabIndex !== void 0 && tabIndex < minimalTabIndex) {
return false;
}
if (tabIndex !== null && tabIndex !== void 0 && tabIndex >= minimalTabIndex || contentEditable === true || contentEditable === "true") {
return true;
}
if (tagName === "a" || tagName === "area") {
return !!href && rel !== "ignore";
}
if (tagName === "input") {
return type !== "file" && type !== "hidden";
}
return tagName === "button" || tagName === "embed" || tagName === "iframe" || tagName === "object" || tagName === "select" || tagName === "textarea";
}
const FOCUS_SCOPE_MARKERS = [
'span[data-focus-scope-end="true"]',
'span[data-focus-scope-start="true"]'
];
const FOCUSABLE_ELEMENTS = [
"a[href]",
"[contenteditable]",
'[tabindex]:not([tabindex^="-"])',
"area[href]",
"button:not([disabled])",
"embed",
"iframe",
'input:not([disabled]):not([type="hidden"])',
"object",
"select:not([disabled]):not([aria-hidden])",
"textarea:not([disabled]):not([aria-hidden])"
];
let hasSibling = false;
function collectDocumentFocusTargets() {
const focusTargets = [...FOCUSABLE_ELEMENTS, ...FOCUS_SCOPE_MARKERS];
return Array.from(
document.querySelectorAll(focusTargets.join(","))
).filter((element) => {
const isFocusScopeMarker = element.dataset["focusScopeEnd"] || element.dataset["focusScopeStart"];
if (isFocusable(element) || isFocusScopeMarker) {
return window.getComputedStyle(element).visibility !== "hidden";
}
return false;
});
}
function isFiberFocusable(fiber) {
const { memoizedProps, stateNode, type } = fiber;
if (memoizedProps === null) {
return false;
}
return isFocusable({
contentEditable: memoizedProps.contentEditable,
disabled: memoizedProps.disabled,
href: memoizedProps.href,
offsetParent: stateNode.offsetParent,
rel: memoizedProps.rel,
tabIndex: memoizedProps.tabIndex,
tagName: type,
type: memoizedProps.type
});
}
function isFiberFocusScopeMarker(fiber) {
return fiber.stateNode.dataset["focusScopeEnd"] || fiber.stateNode.dataset["focusScopeStart"];
}
function collectFocusTargets(node, focusTargets) {
const isFiberFocusTarget = node.tag === HostComponent && (isFiberFocusable(node) || isFiberFocusScopeMarker(node));
if (isFiberFocusTarget) {
focusTargets.push(node.stateNode);
}
const child = node.child;
if (child !== null) {
collectFocusTargets(child, focusTargets);
}
const sibling = node.sibling;
if (sibling) {
hasSibling = true;
collectFocusTargets(sibling, focusTargets);
}
}
function getFiber(scope) {
if (!scope.current) {
return null;
}
const internalKey = Object.keys(scope.current).find(
(key) => key.indexOf("__reactInternalInstance") === 0 || key.indexOf("__reactFiber") === 0
);
if (internalKey) {
return scope.current[internalKey];
}
return null;
}
function getFocusTargetsInScope(fiberNode) {
const focusTargets = [];
const { child } = fiberNode;
if (child !== null) {
collectFocusTargets(child, focusTargets);
}
return focusTargets;
}
function useFocusManagement(scope) {
const nextFocusInDocRef = React.useRef(null);
const prevFocusInDocRef = React.useRef(null);
const moveFocusInScope = (scope2, backwards = false, persistOnScope = false) => {
let fiberFocusTargets = getFocusTargetsInScope(
scope2.alternate ?? scope2
);
if (!hasSibling) {
fiberFocusTargets = getFocusTargetsInScope(scope2);
} else {
hasSibling = false;
}
if (!fiberFocusTargets.length) {
return null;
}
const activeElement = document.activeElement;
if (!activeElement) {
return;
}
const docFocusTargets = collectDocumentFocusTargets();
const docPosition = docFocusTargets.indexOf(activeElement);
const reactFiberPosition = fiberFocusTargets.indexOf(activeElement);
const startFocusTrap = fiberFocusTargets.find(
(element) => element.getAttribute("data-focus-scope-start") === "true"
);
const endFocusTrap = fiberFocusTargets.find(
(element) => element.getAttribute("data-focus-scope-end") === "true"
);
const nextFocusInDoc = docFocusTargets[docPosition + 1];
const prevFocusInDoc = docFocusTargets[docPosition - 1];
if (reactFiberPosition < 0 && !prevFocusInDocRef.current && !nextFocusInDocRef.current && nextFocusInDoc !== endFocusTrap && prevFocusInDoc !== startFocusTrap) {
return null;
}
let nextFocusInFiber = fiberFocusTargets[reactFiberPosition + 1];
let prevFocusInFiber = fiberFocusTargets[reactFiberPosition - 1];
if (startFocusTrap && endFocusTrap && startFocusTrap !== prevFocusInDoc && endFocusTrap !== nextFocusInDoc) {
return null;
}
if (endFocusTrap && endFocusTrap === nextFocusInDoc) {
nextFocusInFiber = docFocusTargets.find(
(_, index, array) => array[index - 1] === startFocusTrap
);
}
if (startFocusTrap && startFocusTrap === prevFocusInDoc) {
prevFocusInFiber = docFocusTargets.find(
(_, index, array) => array[index + 1] === endFocusTrap
);
}
if (persistOnScope && (!nextFocusInFiber || backwards && !prevFocusInFiber)) {
return null;
}
if (nextFocusInFiber !== nextFocusInDoc) {
nextFocusInDocRef.current = nextFocusInDoc;
}
if (prevFocusInFiber !== prevFocusInDoc) {
prevFocusInDocRef.current = prevFocusInDoc;
}
let nextActive = backwards ? prevFocusInFiber : nextFocusInFiber;
if (!nextActive) {
nextActive = backwards ? prevFocusInDocRef.current : nextFocusInDocRef.current;
}
if (nextActive) {
nextActive.focus();
if (nextActive === prevFocusInDocRef.current || nextActive === nextFocusInDocRef.current) {
nextFocusInDocRef.current = null;
prevFocusInDocRef.current = null;
}
return nextActive;
}
return null;
};
return {
focusFirst: () => {
minimalTabIndex = -1;
const next = moveFocusInScope(getFiber(scope), false, true);
minimalTabIndex = 0;
return next;
},
focusNext: (persistOnScope) => moveFocusInScope(getFiber(scope), false, persistOnScope),
focusPrevious: (persistOnScope) => moveFocusInScope(getFiber(scope), true, persistOnScope)
};
}
export {
FOCUSABLE_ELEMENTS,
isFocusable,
useFocusManagement
};