UNPKG

@crossed/primitive

Version:

A universal & performant styling library for React Native, Next.js & React

274 lines (273 loc) 10.8 kB
"use strict"; "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