@crossed/primitive
Version:
A universal & performant styling library for React Native, Next.js & React
288 lines (287 loc) • 9.79 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var FocusScope_exports = {};
__export(FocusScope_exports, {
FocusScope: () => FocusScope,
useFocusScope: () => useFocusScope
});
module.exports = __toCommonJS(FocusScope_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var React = __toESM(require("react"));
var import_core = require("@crossed/core");
var import_useEvent = require("../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__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.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 = (0, import_useEvent.useEvent)(onMountAutoFocusProp);
const onUnmountAutoFocus = (0, import_useEvent.useEvent)(onUnmountAutoFocusProp);
const lastFocusedElementRef = React.useRef(null);
const composedRefs = (0, import_core.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");
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
FocusScope,
useFocusScope
});
//# sourceMappingURL=FocusScope.js.map