@crossed/primitive
Version:
A universal & performant styling library for React Native, Next.js & React
253 lines (252 loc) • 8.02 kB
JavaScript
import { Fragment, jsx } from "react/jsx-runtime";
import * as React from "react";
import { useComposedRefs } from "@crossed/core";
import { useEvent } from "../useEvent";
const AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount";
const AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount";
const EVENT_OPTIONS = { bubbles: false, cancelable: true };
const FocusScope = React.forwardRef(
function FocusScope2(props, forwardedRef) {
const childProps = useFocusScope(props, forwardedRef);
if (typeof props.children === "function") {
return /* @__PURE__ */ jsx(Fragment, { children: props.children(childProps) });
}
return React.cloneElement(
React.Children.only(props.children),
childProps
);
}
);
function useFocusScope(props, forwardedRef) {
const {
loop = false,
enabled = true,
trapped = false,
onMountAutoFocus: onMountAutoFocusProp,
onUnmountAutoFocus: onUnmountAutoFocusProp,
forceUnmount,
children: _children,
...scopeProps
} = props;
const [container, setContainer] = React.useState(null);
const onMountAutoFocus = useEvent(onMountAutoFocusProp);
const onUnmountAutoFocus = useEvent(onUnmountAutoFocusProp);
const lastFocusedElementRef = React.useRef(null);
const composedRefs = useComposedRefs(
forwardedRef,
(node) => setContainer(node)
);
const focusScope = React.useRef({
paused: false,
pause() {
this.paused = true;
},
resume() {
this.paused = false;
}
}).current;
React.useEffect(() => {
if (!enabled)
return;
if (!trapped)
return;
function handleFocusIn(event) {
if (focusScope.paused || !container)
return;
const target = event.target;
if (container.contains(target)) {
lastFocusedElementRef.current = target;
} else {
focus(lastFocusedElementRef.current, { select: true });
}
}
function handleFocusOut(event) {
if (focusScope.paused || !container)
return;
if (!container.contains(event.relatedTarget)) {
focus(lastFocusedElementRef.current, { select: true });
}
}
document.addEventListener("focusin", handleFocusIn);
document.addEventListener("focusout", handleFocusOut);
return () => {
document.removeEventListener("focusin", handleFocusIn);
document.removeEventListener("focusout", handleFocusOut);
};
}, [trapped, forceUnmount, container, focusScope.paused]);
React.useEffect(() => {
if (!enabled)
return;
if (!container)
return;
if (forceUnmount)
return;
focusScopesStack.add(focusScope);
const previouslyFocusedElement = document.activeElement;
const hasFocusedCandidate = container.contains(previouslyFocusedElement);
if (!hasFocusedCandidate) {
const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);
container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);
container.dispatchEvent(mountEvent);
if (!mountEvent.defaultPrevented) {
focusFirst(removeLinks(getTabbableCandidates(container)), {
select: true
});
if (document.activeElement === previouslyFocusedElement) {
focus(container);
}
}
}
return () => {
container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);
const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS);
container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);
container.dispatchEvent(unmountEvent);
if (!unmountEvent.defaultPrevented) {
focus(previouslyFocusedElement ?? document.body, { select: true });
}
container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);
focusScopesStack.remove(focusScope);
};
}, [
enabled,
container,
forceUnmount,
onMountAutoFocus,
onUnmountAutoFocus,
focusScope
]);
const handleKeyDown = React.useCallback(
(event) => {
if (!trapped)
return;
if (!loop)
return;
if (focusScope.paused)
return;
const isTabKey = event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey;
const focusedElement = document.activeElement;
if (isTabKey && focusedElement) {
const container2 = event.currentTarget;
const [first, last] = getTabbableEdges(container2);
const hasTabbableElementsInside = first && last;
if (!hasTabbableElementsInside) {
if (focusedElement === container2)
event.preventDefault();
} else {
if (!event.shiftKey && focusedElement === last) {
event.preventDefault();
if (loop)
focus(first, { select: true });
} else if (event.shiftKey && focusedElement === first) {
event.preventDefault();
if (loop)
focus(last, { select: true });
}
}
}
},
[loop, trapped, focusScope.paused]
);
return {
tabIndex: -1,
...scopeProps,
ref: composedRefs,
onKeyDown: handleKeyDown
};
}
function focusFirst(candidates, { select = false } = {}) {
const previouslyFocusedElement = document.activeElement;
for (const candidate of candidates) {
focus(candidate, { select });
if (document.activeElement !== previouslyFocusedElement)
return;
}
}
function getTabbableEdges(container) {
const candidates = getTabbableCandidates(container);
const first = findVisible(candidates, container);
const last = findVisible(candidates.reverse(), container);
return [first, last];
}
function getTabbableCandidates(container) {
const nodes = [];
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
if (node.disabled || node.hidden || isHiddenInput)
return NodeFilter.FILTER_SKIP;
return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
while (walker.nextNode())
nodes.push(walker.currentNode);
return nodes;
}
function findVisible(elements, container) {
for (const element of elements) {
if (!isHidden(element, { upTo: container }))
return element;
}
return;
}
function isHidden(node, { upTo }) {
if (getComputedStyle(node).visibility === "hidden")
return true;
while (node) {
if (upTo !== void 0 && node === upTo)
return false;
if (getComputedStyle(node).display === "none")
return true;
node = node.parentElement;
}
return false;
}
function isSelectableInput(element) {
return element instanceof HTMLInputElement && "select" in element;
}
function focus(element, { select = false } = {}) {
setTimeout(() => {
if (element == null ? void 0 : element.focus) {
const previouslyFocusedElement = document.activeElement;
element.focus({ preventScroll: true });
if (element !== previouslyFocusedElement && isSelectableInput(element) && select)
element.select();
}
});
}
const focusScopesStack = createFocusScopesStack();
function createFocusScopesStack() {
let stack = [];
return {
add(focusScope) {
const activeFocusScope = stack[0];
if (focusScope !== activeFocusScope) {
activeFocusScope == null ? void 0 : activeFocusScope.pause();
}
stack = arrayRemove(stack, focusScope);
stack.unshift(focusScope);
},
remove(focusScope) {
var _a;
stack = arrayRemove(stack, focusScope);
(_a = stack[0]) == null ? void 0 : _a.resume();
}
};
}
function arrayRemove(array, item) {
const updatedArray = [...array];
const index = updatedArray.indexOf(item);
if (index !== -1) {
updatedArray.splice(index, 1);
}
return updatedArray;
}
function removeLinks(items) {
return items.filter((item) => item.tagName !== "A");
}
export {
FocusScope,
useFocusScope
};
//# sourceMappingURL=FocusScope.js.map