@crossed/primitive
Version:
A universal & performant styling library for React Native, Next.js & React
274 lines (273 loc) • 10.8 kB
JavaScript
;
"use client";
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 RovingFocus_exports = {};
__export(RovingFocus_exports, {
Item: () => Item,
Root: () => Root,
RovingFocus: () => RovingFocus,
RovingFocusGroup: () => RovingFocusGroup,
RovingFocusGroupItem: () => RovingFocusGroupItem,
RovingFocusProvider: () => RovingFocusProvider,
useRovingFocusContext: () => useRovingFocusContext
});
module.exports = __toCommonJS(RovingFocus_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var React = __toESM(require("react"));
var import_Collections = require("./Collections");
var import_core = require("@crossed/core");
var import_Primitive = require("./Primitive");
var import_Slot = require("./Slot");
const ENTRY_FOCUS = "rovingFocusGroup.onEntryFocus";
const EVENT_OPTIONS = { bubbles: false, cancelable: true };
const GROUP_NAME = "RovingFocusGroup";
const [Collection, useCollection] = (0, import_Collections.createCollection)(
GROUP_NAME
);
const [RovingFocusProvider, useRovingFocusContext] = (0, import_core.createScope)({});
const RovingFocusGroup = React.forwardRef((props, forwardedRef) => {
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Collection.Provider, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Collection.Slot, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RovingFocusGroupImpl, { ...props, ref: forwardedRef }) }) });
});
RovingFocusGroup.displayName = GROUP_NAME;
const RovingFocusGroupImpl = React.forwardRef((props, forwardedRef) => {
const {
orientation,
loop = false,
dir,
currentTabStopId: currentTabStopIdProp,
defaultCurrentTabStopId,
onCurrentTabStopIdChange,
onEntryFocus,
...groupProps
} = props;
const ref = React.useRef(null);
const composedRefs = (0, import_core.useComposedRefs)(forwardedRef, ref);
const direction = (0, import_core.useDirection)(dir);
const [currentTabStopId = null, setCurrentTabStopId] = (0, import_core.useUncontrolled)({
value: currentTabStopIdProp,
defaultValue: defaultCurrentTabStopId,
onChange: onCurrentTabStopIdChange
});
const [isTabbingBackOut, setIsTabbingBackOut] = React.useState(false);
const handleEntryFocus = (0, import_core.useCallbackRef)(onEntryFocus);
const getItems = useCollection();
const isClickFocusRef = React.useRef(false);
const [focusableItemsCount, setFocusableItemsCount] = React.useState(0);
React.useEffect(() => {
const node = ref.current;
if (node) {
node.addEventListener(ENTRY_FOCUS, handleEntryFocus);
return () => node.removeEventListener(ENTRY_FOCUS, handleEntryFocus);
}
return () => {
};
}, [handleEntryFocus]);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
RovingFocusProvider,
{
orientation,
dir: direction,
loop,
currentTabStopId,
onItemFocus: React.useCallback(
(tabStopId) => setCurrentTabStopId(tabStopId),
[setCurrentTabStopId]
),
onItemShiftTab: React.useCallback(() => setIsTabbingBackOut(true), []),
onFocusableItemAdd: React.useCallback(
() => setFocusableItemsCount((prevCount) => prevCount + 1),
[]
),
onFocusableItemRemove: React.useCallback(
() => setFocusableItemsCount((prevCount) => prevCount - 1),
[]
),
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_Primitive.Primitive.div,
{
tabIndex: isTabbingBackOut || focusableItemsCount === 0 ? -1 : 0,
"data-orientation": orientation,
...groupProps,
ref: composedRefs,
style: { outline: "none", ...props.style },
onMouseDown: (0, import_core.composeEventHandlers)(props.onMouseDown, () => {
isClickFocusRef.current = true;
}),
onFocus: (0, import_core.composeEventHandlers)(props.onFocus, (event) => {
const isKeyboardFocus = !isClickFocusRef.current;
if (event.target === event.currentTarget && isKeyboardFocus && !isTabbingBackOut) {
const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);
event.currentTarget.dispatchEvent(entryFocusEvent);
if (!entryFocusEvent.defaultPrevented) {
const items = getItems().filter((item) => item.focusable);
const activeItem = items.find((item) => item.active);
const currentItem = items.find(
(item) => item.id === currentTabStopId
);
const candidateItems = [activeItem, currentItem, ...items].filter(
Boolean
);
const candidateNodes = candidateItems.map(
(item) => item.ref.current
);
focusFirst(candidateNodes);
}
}
isClickFocusRef.current = false;
}),
onBlur: (0, import_core.composeEventHandlers)(
props.onBlur,
() => setIsTabbingBackOut(false)
)
}
)
}
);
});
const ITEM_NAME = "RovingFocusGroupItem";
const RovingFocusGroupItem = React.forwardRef((props, forwardedRef) => {
const { focusable = true, active = false, tabStopId, ...itemProps } = props;
const autoId = React.useId();
const id = tabStopId || autoId;
const context = useRovingFocusContext();
const isCurrentTabStop = context.currentTabStopId === id;
const getItems = useCollection();
const { onFocusableItemAdd, onFocusableItemRemove } = context;
React.useEffect(() => {
if (focusable) {
onFocusableItemAdd == null ? void 0 : onFocusableItemAdd();
return () => onFocusableItemRemove == null ? void 0 : onFocusableItemRemove();
}
return () => {
};
}, [focusable, onFocusableItemAdd, onFocusableItemRemove]);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Collection.ItemSlot, { id, focusable, active, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_Slot.Slot,
{
tabIndex: isCurrentTabStop ? 0 : -1,
"data-orientation": context.orientation,
...itemProps,
ref: forwardedRef,
onMouseDown: (0, import_core.composeEventHandlers)(props.onMouseDown, (event) => {
var _a;
if (!focusable)
event.preventDefault();
else
(_a = context.onItemFocus) == null ? void 0 : _a.call(context, id);
}),
onFocus: (0, import_core.composeEventHandlers)(
props.onFocus,
() => {
var _a;
return (_a = context.onItemFocus) == null ? void 0 : _a.call(context, id);
}
),
onKeyDown: (0, import_core.composeEventHandlers)(props.onKeyDown, (event) => {
var _a;
if (event.key === "Tab" && event.shiftKey) {
(_a = context.onItemShiftTab) == null ? void 0 : _a.call(context);
return;
}
if (event.target !== event.currentTarget)
return;
const focusIntent = getFocusIntent(
event,
context.orientation,
context.dir
);
if (focusIntent !== void 0) {
event.preventDefault();
const items = getItems().filter((item) => item.focusable);
let candidateNodes = items.map((item) => item.ref.current);
if (focusIntent === "last")
candidateNodes.reverse();
else if (focusIntent === "prev" || focusIntent === "next") {
if (focusIntent === "prev")
candidateNodes.reverse();
const currentIndex = candidateNodes.indexOf(event.currentTarget);
candidateNodes = context.loop ? wrapArray(candidateNodes, currentIndex + 1) : candidateNodes.slice(currentIndex + 1);
}
setTimeout(() => focusFirst(candidateNodes));
}
})
}
) });
});
RovingFocusGroupItem.displayName = ITEM_NAME;
const MAP_KEY_TO_FOCUS_INTENT = {
ArrowLeft: "prev",
ArrowUp: "prev",
ArrowRight: "next",
ArrowDown: "next",
PageUp: "first",
Home: "first",
PageDown: "last",
End: "last"
};
function getDirectionAwareKey(key, dir) {
if (dir !== "rtl")
return key;
return key === "ArrowLeft" ? "ArrowRight" : key === "ArrowRight" ? "ArrowLeft" : key;
}
function getFocusIntent(event, orientation, dir) {
const key = getDirectionAwareKey(event.key, dir);
if (orientation === "vertical" && ["ArrowLeft", "ArrowRight"].includes(key))
return void 0;
if (orientation === "horizontal" && ["ArrowUp", "ArrowDown"].includes(key))
return void 0;
return MAP_KEY_TO_FOCUS_INTENT[key];
}
function focusFirst(candidates) {
var _a;
const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;
for (const candidate of candidates) {
if (candidate === PREVIOUSLY_FOCUSED_ELEMENT)
return;
(_a = candidate.focus) == null ? void 0 : _a.call(candidate);
if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT)
return;
}
}
function wrapArray(array, startIndex) {
return array.map((_, index) => array[(startIndex + index) % array.length]);
}
const Root = RovingFocusGroup;
const Item = RovingFocusGroupItem;
const RovingFocus = (0, import_core.withStaticProperties)(Root, { Item });
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Item,
Root,
RovingFocus,
RovingFocusGroup,
RovingFocusGroupItem,
RovingFocusProvider,
useRovingFocusContext
});
//# sourceMappingURL=RovingFocus.js.map