UNPKG

@react-marking-menu/core

Version:

Headless React primitives for marking menus with gestural interaction

1,105 lines (1,087 loc) 34.6 kB
"use strict"; 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); // src/index.ts var index_exports = {}; __export(index_exports, { KeyboardIndicator: () => KeyboardIndicator, LiveRegion: () => LiveRegion, MarkingMenu: () => MarkingMenu, MarkingMenuContent: () => MarkingMenuContent, MarkingMenuItem: () => MarkingMenuItem, MarkingMenuTrigger: () => MarkingMenuTrigger, createKeyboardState: () => createKeyboardState, directionToAngle: () => directionToAngle, getAvailableDirections: () => getAvailableDirections, getDirectionFromKeys: () => getDirectionFromKeys, getDirectionFromPosition: () => getDirectionFromPosition, getDirectionFromSingleKey: () => getDirectionFromSingleKey, getDirectionFromTwoKeys: () => getDirectionFromTwoKeys, getDistance: () => getDistance, handleKeyDown: () => handleKeyDown, handleKeyUp: () => handleKeyUp, isArrowKey: () => isArrowKey, resetKeyboardState: () => resetKeyboardState, useFocusManagement: () => useFocusManagement, useMarkingMenuContext: () => useMarkingMenuContext, useMarkingMenuGesture: () => useMarkingMenuGesture, useMarkingMenuStateMachine: () => useMarkingMenuStateMachine, useReducedMotion: () => useReducedMotion }); module.exports = __toCommonJS(index_exports); // src/hooks/useMarkingMenuStateMachine.ts var import_react = require("react"); function useMarkingMenuStateMachine() { const [state, setState] = (0, import_react.useState)("idle"); const [origin, setOrigin] = (0, import_react.useState)(null); const [currentDirection, setCurrentDirection] = (0, import_react.useState)(null); const [selectedItem, setSelectedItem] = (0, import_react.useState)(null); const pressTimerRef = (0, import_react.useRef)(null); const startPress = (0, import_react.useCallback)((position, delay) => { setState("pressed"); setOrigin(position); setCurrentDirection(null); setSelectedItem(null); pressTimerRef.current = setTimeout(() => { setState("active"); }, delay); }, []); const startImmediate = (0, import_react.useCallback)((position) => { setState("active"); setOrigin(position); setCurrentDirection(null); setSelectedItem(null); }, []); const updatePosition = (0, import_react.useCallback)((direction) => { setState((currentState) => { if (currentState === "active" || currentState === "selecting") { setCurrentDirection(direction); if (direction !== null && currentState === "active") { return "selecting"; } } return currentState; }); }, []); const endPress = (0, import_react.useCallback)((itemId) => { if (pressTimerRef.current) { clearTimeout(pressTimerRef.current); pressTimerRef.current = null; } setSelectedItem(itemId); setState("idle"); setOrigin(null); setCurrentDirection(null); return itemId; }, []); const cancel = (0, import_react.useCallback)(() => { if (pressTimerRef.current) { clearTimeout(pressTimerRef.current); pressTimerRef.current = null; } setState("idle"); setOrigin(null); setCurrentDirection(null); setSelectedItem(null); }, []); return { state, origin, currentDirection, selectedItem, startPress, startImmediate, updatePosition, endPress, cancel }; } // src/hooks/useMarkingMenuGesture.ts var import_react2 = require("react"); // src/utils/directions.ts function getDirectionFromPosition(x, y, originX, originY, directions = 8) { const dx = x - originX; const dy = y - originY; let angle = Math.atan2(dy, dx) * (180 / Math.PI); angle = (angle + 360) % 360; if (directions === 8) { return getDirection8(angle); } else { return getDirection4(angle); } } function getDirection8(angle) { const normalized = (angle + 22.5) % 360; if (normalized < 45) return "E"; if (normalized < 90) return "SE"; if (normalized < 135) return "S"; if (normalized < 180) return "SW"; if (normalized < 225) return "W"; if (normalized < 270) return "NW"; if (normalized < 315) return "N"; return "NE"; } function getDirection4(angle) { const normalized = (angle + 45) % 360; if (normalized < 90) return "E"; if (normalized < 180) return "S"; if (normalized < 270) return "W"; return "N"; } function directionToAngle(direction) { const angles = { E: 0, SE: 45, S: 90, SW: 135, W: 180, NW: 225, N: 270, NE: 315 }; return angles[direction] ?? 0; } function getDistance(x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy); } function getAvailableDirections(directions) { if (directions === 4) { return ["N", "E", "S", "W"]; } return ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; } // src/utils/keyboard.ts var ARROW_KEYS = [ "ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft" ]; function isArrowKey(key) { return ARROW_KEYS.includes(key); } function getDirectionFromSingleKey(key) { const keyMap = { ArrowUp: "N", ArrowRight: "E", ArrowDown: "S", ArrowLeft: "W" }; return keyMap[key]; } function areOppositeKeys(key1, key2) { return key1 === "ArrowUp" && key2 === "ArrowDown" || key1 === "ArrowDown" && key2 === "ArrowUp" || key1 === "ArrowLeft" && key2 === "ArrowRight" || key1 === "ArrowRight" && key2 === "ArrowLeft"; } function getDirectionFromTwoKeys(key1, key2) { if (key1 === key2) { return getDirectionFromSingleKey(key1); } if (areOppositeKeys(key1, key2)) { return getDirectionFromSingleKey(key2); } const diagonalMap = { "ArrowUp+ArrowRight": "NE", "ArrowRight+ArrowUp": "NE", "ArrowDown+ArrowRight": "SE", "ArrowRight+ArrowDown": "SE", "ArrowDown+ArrowLeft": "SW", "ArrowLeft+ArrowDown": "SW", "ArrowUp+ArrowLeft": "NW", "ArrowLeft+ArrowUp": "NW" }; const combo = `${key1}+${key2}`; const direction = diagonalMap[combo]; if (direction) { return direction; } return getDirectionFromSingleKey(key2); } function getDirectionFromKeys(pressedKeys) { if (pressedKeys.length === 0) { return null; } if (pressedKeys.length === 1) { return getDirectionFromSingleKey(pressedKeys[0]); } const last2Keys = pressedKeys.slice(-2); return getDirectionFromTwoKeys(last2Keys[0], last2Keys[1]); } function createKeyboardState() { return { pressedKeys: [], isReleasing: false, latchedDirection: null }; } function handleKeyDown(state, key) { if (!isArrowKey(key)) { return { state, direction: getDirectionFromKeys(state.pressedKeys) }; } if (state.pressedKeys.includes(key)) { return { state, direction: getDirectionFromKeys(state.pressedKeys) }; } const newPressedKeys = [...state.pressedKeys, key]; const direction = getDirectionFromKeys(newPressedKeys); return { state: { ...state, pressedKeys: newPressedKeys }, direction }; } function handleKeyUp(state, key) { if (!isArrowKey(key)) { return { state, direction: state.isReleasing ? state.latchedDirection : getDirectionFromKeys(state.pressedKeys), allKeysReleased: false }; } const newPressedKeys = state.pressedKeys.filter((k) => k !== key); let isReleasing = state.isReleasing; let latchedDirection = state.latchedDirection; if (!state.isReleasing && state.pressedKeys.length >= 2 && newPressedKeys.length < 2) { isReleasing = true; latchedDirection = getDirectionFromKeys(state.pressedKeys); } if (newPressedKeys.length === 0) { return { state: createKeyboardState(), // Reset state direction: latchedDirection || getDirectionFromKeys(state.pressedKeys), allKeysReleased: true }; } const direction = isReleasing ? latchedDirection : getDirectionFromKeys(newPressedKeys); return { state: { pressedKeys: newPressedKeys, isReleasing, latchedDirection }, direction, allKeysReleased: false }; } function resetKeyboardState() { return createKeyboardState(); } // src/hooks/useMarkingMenuGesture.ts function useMarkingMenuGesture({ config = {}, items, onSelect, onCancel, enabled = true }) { const { pressThreshold = 150, minDistance = 50, directions = 8, preventContextMenu = true, originMode = "element" } = config; const stateMachine = useMarkingMenuStateMachine(); const { state, origin, currentDirection, selectedItem, startPress, startImmediate, updatePosition, endPress, cancel } = stateMachine; const keyboardStateRef = (0, import_react2.useRef)(createKeyboardState()); const pointerIdRef = (0, import_react2.useRef)(null); const triggerElementRef = (0, import_react2.useRef)(null); const pointerDownTimerRef = (0, import_react2.useRef)(null); const isPointerGestureActiveRef = (0, import_react2.useRef)(false); const calculateOrigin = (0, import_react2.useCallback)( (event, element) => { switch (originMode) { case "cursor": if (event && "clientX" in event) { return { x: event.clientX, y: event.clientY }; } return calculateElementCenter(element); case "viewport": return { x: window.innerWidth / 2, y: window.innerHeight / 2 }; case "element": default: return calculateElementCenter(element); } }, [originMode] ); const calculateElementCenter = (element) => { if (element) { const rect = element.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; } return { x: window.innerWidth / 2, y: window.innerHeight / 2 }; }; const findItemByDirection = (0, import_react2.useCallback)( (direction) => { return items.find((item) => item.direction === direction && !item.disabled) || null; }, [items] ); const executeSelection = (0, import_react2.useCallback)( (direction) => { if (!direction) { endPress(null); return; } const item = findItemByDirection(direction); if (item) { endPress(item.id); onSelect?.(item.id); item.onSelect?.(); } else { endPress(null); } }, [endPress, findItemByDirection, onSelect] ); const handlePointerDown = (0, import_react2.useCallback)( (e) => { if (!enabled) return; if (e.button !== 0) return; if (pointerIdRef.current !== null) return; e.preventDefault(); triggerElementRef.current = e.currentTarget; pointerIdRef.current = e.pointerId; e.target.setPointerCapture?.(e.pointerId); const originPos = calculateOrigin(e, triggerElementRef.current); isPointerGestureActiveRef.current = false; pointerDownTimerRef.current = setTimeout(() => { isPointerGestureActiveRef.current = true; startPress(originPos, pressThreshold); keyboardStateRef.current = createKeyboardState(); }, pressThreshold); }, [enabled, startPress, pressThreshold, calculateOrigin] ); const handlePointerMove = (0, import_react2.useCallback)( (e) => { if (!enabled) return; if (pointerIdRef.current !== e.pointerId) return; if (state !== "active" && state !== "selecting") return; if (!origin) return; e.preventDefault(); const distance = getDistance(origin.x, origin.y, e.clientX, e.clientY); if (distance < minDistance) { updatePosition(null); } else { const direction = getDirectionFromPosition( e.clientX, e.clientY, origin.x, origin.y, directions ); updatePosition(direction); } }, [enabled, state, origin, minDistance, directions, updatePosition] ); const handlePointerUp = (0, import_react2.useCallback)( (e) => { if (!enabled) return; if (pointerIdRef.current !== e.pointerId) return; if (pointerDownTimerRef.current) { clearTimeout(pointerDownTimerRef.current); pointerDownTimerRef.current = null; } ; e.target.releasePointerCapture?.(e.pointerId); pointerIdRef.current = null; if (isPointerGestureActiveRef.current) { executeSelection(currentDirection); isPointerGestureActiveRef.current = false; } else { cancel(); } }, [enabled, executeSelection, currentDirection, cancel] ); const handlePointerCancel = (0, import_react2.useCallback)( (e) => { if (!enabled) return; if (pointerIdRef.current !== e.pointerId) return; if (pointerDownTimerRef.current) { clearTimeout(pointerDownTimerRef.current); pointerDownTimerRef.current = null; } ; e.target.releasePointerCapture?.(e.pointerId); pointerIdRef.current = null; isPointerGestureActiveRef.current = false; cancel(); onCancel?.(); }, [enabled, cancel, onCancel] ); const handleKeyDown2 = (0, import_react2.useCallback)( (e) => { if (!enabled) return; if (!triggerElementRef.current) { triggerElementRef.current = e.currentTarget; } if (e.key === "Escape") { e.preventDefault(); cancel(); keyboardStateRef.current = createKeyboardState(); onCancel?.(); return; } if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) { return; } if (!isArrowKey(e.key)) return; e.preventDefault(); const result = handleKeyDown(keyboardStateRef.current, e.key); keyboardStateRef.current = result.state; if (result.direction) { if (state === "idle") { const originPos = calculateOrigin(e, triggerElementRef.current); startImmediate(originPos); } updatePosition(result.direction); } }, [enabled, state, cancel, startImmediate, updatePosition, calculateOrigin] ); const handleKeyUp2 = (0, import_react2.useCallback)( (e) => { if (!enabled) return; if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) { return; } if (!isArrowKey(e.key)) return; e.preventDefault(); const result = handleKeyUp(keyboardStateRef.current, e.key); keyboardStateRef.current = result.state; if (result.allKeysReleased) { executeSelection(result.direction); } else if (result.direction) { updatePosition(result.direction); } }, [enabled, updatePosition, executeSelection] ); const handleContextMenu = (0, import_react2.useCallback)( (e) => { if (preventContextMenu) { e.preventDefault(); } }, [preventContextMenu] ); const getTriggerProps = (0, import_react2.useCallback)(() => { const baseProps = { onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, onKeyDown: handleKeyDown2, onKeyUp: handleKeyUp2, tabIndex: 0 }; if (preventContextMenu) { return { ...baseProps, onContextMenu: handleContextMenu }; } return baseProps; }, [ handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel, handleKeyDown2, handleKeyUp2, handleContextMenu, preventContextMenu ]); (0, import_react2.useEffect)(() => { return () => { if (pointerDownTimerRef.current) { clearTimeout(pointerDownTimerRef.current); pointerDownTimerRef.current = null; } keyboardStateRef.current = createKeyboardState(); }; }, []); return { state, origin, currentDirection, selectedItem, getTriggerProps, keyboardState: keyboardStateRef.current }; } // src/hooks/useMarkingMenuContext.ts var import_react5 = require("react"); // src/components/MarkingMenu.tsx var import_react4 = require("react"); // src/components/LiveRegion.tsx var import_react3 = __toESM(require("react")); var import_jsx_runtime = require("react/jsx-runtime"); function LiveRegion({ message, politeness = "polite", clearOnAnnounce = true, clearDelay = 1e3 }) { const messageRef = (0, import_react3.useRef)(""); const timeoutRef = (0, import_react3.useRef)(null); const [currentMessage, setCurrentMessage] = import_react3.default.useState(""); (0, import_react3.useEffect)(() => { if (message && message !== messageRef.current) { messageRef.current = message; setCurrentMessage(message); if (clearOnAnnounce) { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { setCurrentMessage(""); messageRef.current = ""; }, clearDelay); } } return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [message, clearOnAnnounce, clearDelay]); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { role: "status", "aria-live": politeness, "aria-atomic": "true", style: { position: "absolute", width: "1px", height: "1px", padding: 0, margin: "-1px", overflow: "hidden", clip: "rect(0, 0, 0, 0)", whiteSpace: "nowrap", border: 0 }, children: currentMessage } ); } LiveRegion.displayName = "LiveRegion"; // src/components/MarkingMenu.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); var MarkingMenuContext = (0, import_react4.createContext)(null); function MarkingMenu({ children, config, a11y, onSelect, onCancel, disabled = false }) { const [items, setItems] = (0, import_react4.useState)([]); const [announcement, setAnnouncement] = (0, import_react4.useState)(""); const defaultConfig = { pressThreshold: config?.pressThreshold ?? 150, minDistance: config?.minDistance ?? 50, directions: config?.directions ?? 8, preventContextMenu: config?.preventContextMenu ?? true, originMode: config?.originMode ?? "element" }; const defaultA11y = { announcements: a11y?.announcements ?? true, messages: { menuOpened: a11y?.messages?.menuOpened ?? "Marking menu opened", directionChanged: a11y?.messages?.directionChanged ?? ((direction) => `${direction} direction selected`), itemSelected: a11y?.messages?.itemSelected ?? ((label) => `${label} selected`), menuCancelled: a11y?.messages?.menuCancelled ?? "Menu cancelled" }, label: a11y?.label ?? "Marking menu", description: a11y?.description ?? "Press and hold, then drag or use arrow keys to select an action" }; const registerItem = (0, import_react4.useCallback)((item) => { setItems((prev) => { const existingIndex = prev.findIndex((i) => i.id === item.id); if (existingIndex >= 0) { const newItems = [...prev]; newItems[existingIndex] = item; return newItems; } return [...prev, item]; }); }, []); const unregisterItem = (0, import_react4.useCallback)((id) => { setItems((prev) => prev.filter((item) => item.id !== id)); }, []); const gesture = useMarkingMenuGesture({ config: defaultConfig, items, onSelect, onCancel, enabled: !disabled }); const contextValue = (0, import_react4.useMemo)( () => ({ state: gesture.state, origin: gesture.origin, currentDirection: gesture.currentDirection, selectedItem: gesture.selectedItem, config: defaultConfig, a11y: defaultA11y, items, registerItem, unregisterItem, getTriggerProps: gesture.getTriggerProps, keyboardState: gesture.keyboardState }), [ gesture.state, gesture.origin, gesture.currentDirection, gesture.selectedItem, gesture.getTriggerProps, gesture.keyboardState, defaultConfig.pressThreshold, defaultConfig.minDistance, defaultConfig.directions, defaultConfig.preventContextMenu, defaultConfig.originMode, defaultA11y.announcements, defaultA11y.label, defaultA11y.description, items, registerItem, unregisterItem ] ); (0, import_react4.useEffect)(() => { if (!defaultA11y.announcements) return; if (gesture.state === "active" && defaultA11y.messages.menuOpened) { setAnnouncement(defaultA11y.messages.menuOpened); } }, [gesture.state, defaultA11y]); (0, import_react4.useEffect)(() => { if (!defaultA11y.announcements) return; if (gesture.currentDirection && gesture.state === "selecting" && defaultA11y.messages.directionChanged) { const item = items.find((i) => i.direction === gesture.currentDirection); if (item) { setAnnouncement(defaultA11y.messages.directionChanged(gesture.currentDirection)); } } }, [gesture.currentDirection, gesture.state, items, defaultA11y]); return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(MarkingMenuContext.Provider, { value: contextValue, children: [ !disabled && children, defaultA11y.announcements && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LiveRegion, { message: announcement }) ] }); } MarkingMenu.displayName = "MarkingMenu"; // src/hooks/useMarkingMenuContext.ts function useMarkingMenuContext() { const context = (0, import_react5.useContext)(MarkingMenuContext); if (!context) { throw new Error( "useMarkingMenuContext must be used within a MarkingMenu component. Make sure your component is wrapped in <MarkingMenu>...</MarkingMenu>." ); } return context; } // src/hooks/useFocusManagement.ts var import_react6 = require("react"); function useFocusManagement({ isActive, restoreFocus = true, containerRef }) { const previousActiveElementRef = (0, import_react6.useRef)(null); (0, import_react6.useEffect)(() => { if (isActive && restoreFocus) { previousActiveElementRef.current = document.activeElement; } return () => { if (!isActive && restoreFocus && previousActiveElementRef.current && document.body.contains(previousActiveElementRef.current)) { setTimeout(() => { previousActiveElementRef.current?.focus(); }, 0); } }; }, [isActive, restoreFocus]); (0, import_react6.useEffect)(() => { if (!isActive || !containerRef?.current) return; const container = containerRef.current; const handleKeyDown2 = (e) => { if (e.key !== "Tab") return; const focusableElements = container.querySelectorAll( 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])' ); if (focusableElements.length === 0) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } }; container.addEventListener("keydown", handleKeyDown2); return () => { container.removeEventListener("keydown", handleKeyDown2); }; }, [isActive, containerRef]); } // src/hooks/useReducedMotion.ts var import_react7 = require("react"); function useReducedMotion() { const [prefersReducedMotion, setPrefersReducedMotion] = (0, import_react7.useState)(() => { if (typeof window === "undefined" || !window.matchMedia) return false; const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); return mediaQuery.matches; }); (0, import_react7.useEffect)(() => { if (typeof window === "undefined" || !window.matchMedia) return; const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const handleChange = (event) => { setPrefersReducedMotion(event.matches); }; handleChange(mediaQuery); if (mediaQuery.addEventListener) { mediaQuery.addEventListener("change", handleChange); } else { mediaQuery.addListener(handleChange); } return () => { if (mediaQuery.removeEventListener) { mediaQuery.removeEventListener("change", handleChange); } else { mediaQuery.removeListener(handleChange); } }; }, []); return prefersReducedMotion; } // src/components/MarkingMenuTrigger.tsx var import_react8 = __toESM(require("react")); var import_jsx_runtime3 = require("react/jsx-runtime"); function MarkingMenuTrigger({ children, asChild = false, className, style }) { const { getTriggerProps, a11y, state } = useMarkingMenuContext(); const triggerProps = getTriggerProps(); const ariaProps = { "aria-label": a11y.label, "aria-expanded": state === "active" || state === "selecting", "aria-haspopup": "menu", "aria-describedby": a11y.description ? "marking-menu-description" : void 0 }; const dataProps = { "data-state": state, "data-idle": state === "idle" ? "" : void 0, "data-pressed": state === "pressed" ? "" : void 0, "data-active": state === "active" ? "" : void 0, "data-selecting": state === "selecting" ? "" : void 0 }; if (asChild) { const child = import_react8.default.Children.only(children); return import_react8.default.cloneElement(child, { ...triggerProps, ...ariaProps, ...dataProps, ...child.props, className: className ? `${child.props.className || ""} ${className}`.trim() : child.props.className, style: style ? { ...child.props.style, ...style } : child.props.style }); } return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( "button", { ...triggerProps, ...ariaProps, ...dataProps, className, style, type: "button", children } ), a11y.description && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( "div", { id: "marking-menu-description", style: { position: "absolute", width: "1px", height: "1px", padding: 0, margin: "-1px", overflow: "hidden", clip: "rect(0, 0, 0, 0)", whiteSpace: "nowrap", border: 0 }, children: a11y.description } ) ] }); } MarkingMenuTrigger.displayName = "MarkingMenuTrigger"; // src/components/MarkingMenuContent.tsx var import_react9 = require("react"); var import_jsx_runtime4 = require("react/jsx-runtime"); function MarkingMenuContent({ children, render, className, style, forceMount = false }) { const { state, origin, currentDirection, a11y } = useMarkingMenuContext(); const shouldRender = forceMount || state === "active" || state === "selecting"; const positionStyles = (0, import_react9.useMemo)(() => { if (!origin) return {}; return { position: "fixed", left: origin.x, top: origin.y, pointerEvents: "none", // Prevent interfering with pointer events zIndex: 9999 // Ensure menu appears above other content }; }, [origin]); if (!shouldRender) { return null; } const mergedStyle = { ...positionStyles, ...style }; const ariaProps = { role: "menu", "aria-label": a11y.label }; if (render) { return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ...ariaProps, className, style: mergedStyle, children: render({ state, origin, currentDirection }) }); } return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ...ariaProps, className, style: mergedStyle, children }); } MarkingMenuContent.displayName = "MarkingMenuContent"; // src/components/MarkingMenuItem.tsx var import_react10 = require("react"); var import_jsx_runtime5 = require("react/jsx-runtime"); function MarkingMenuItem({ id, direction, label, icon, onSelect, disabled = false, children, className, style }) { const { registerItem, unregisterItem, currentDirection, selectedItem, state } = useMarkingMenuContext(); (0, import_react10.useEffect)(() => { registerItem({ id, direction, label: label || "", icon, onSelect, disabled }); return () => { unregisterItem(id); }; }, [id, direction, label, icon, onSelect, disabled, registerItem, unregisterItem]); const isHighlighted = currentDirection === direction; const isSelected = selectedItem === id; const renderProps = (0, import_react10.useMemo)( () => ({ isHighlighted, isSelected, isDisabled: disabled, direction, state }), [isHighlighted, isSelected, disabled, direction, state] ); const content = typeof children === "function" ? children(renderProps) : children ? children : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [ icon && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: icon }), label && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: label }) ] }); const mergedClassName = className ? `${className} ${isHighlighted ? "marking-menu-item-highlighted" : ""} ${isSelected ? "marking-menu-item-selected" : ""} ${disabled ? "marking-menu-item-disabled" : ""}` : `${isHighlighted ? "marking-menu-item-highlighted" : ""} ${isSelected ? "marking-menu-item-selected" : ""} ${disabled ? "marking-menu-item-disabled" : ""}`; const ariaProps = { role: "menuitem", "aria-label": label || `${direction} direction`, "aria-disabled": disabled, "aria-current": isHighlighted ? "true" : void 0 }; return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( "div", { ...ariaProps, className: mergedClassName.trim(), style, "data-direction": direction, "data-item-id": id, "data-highlighted": isHighlighted, "data-selected": isSelected, "data-disabled": disabled, children: content } ); } MarkingMenuItem.displayName = "MarkingMenuItem"; // src/components/KeyboardIndicator.tsx var import_react11 = require("react"); var import_jsx_runtime6 = require("react/jsx-runtime"); function KeyboardIndicator({ render, className, style, show = true }) { const { keyboardState, currentDirection } = useMarkingMenuContext(); const formatKey = (key) => { return key.replace("Arrow", ""); }; const defaultRender = (0, import_react11.useMemo)(() => { if (!keyboardState.pressedKeys.length) { return null; } const keys = keyboardState.pressedKeys.map(formatKey).join(" + "); const status = keyboardState.isReleasing ? " (releasing)" : ""; return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)( "div", { style: { display: "flex", flexDirection: "column", gap: "0.25rem", padding: "0.5rem", background: "rgba(0, 0, 0, 0.8)", color: "white", borderRadius: "4px", fontSize: "0.875rem", fontFamily: "monospace", ...style }, className, children: [ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: "Keys:" }), " ", keys, status ] }), currentDirection && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: "Direction:" }), " ", currentDirection ] }), keyboardState.latchedDirection && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: "Latched:" }), " ", keyboardState.latchedDirection ] }) ] } ); }, [keyboardState, currentDirection, className, style]); if (!show || !keyboardState.pressedKeys.length) { return null; } if (render) { return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className, style, children: render({ pressedKeys: keyboardState.pressedKeys, isReleasing: keyboardState.isReleasing, latchedDirection: keyboardState.latchedDirection }) }); } return defaultRender; } KeyboardIndicator.displayName = "KeyboardIndicator"; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { KeyboardIndicator, LiveRegion, MarkingMenu, MarkingMenuContent, MarkingMenuItem, MarkingMenuTrigger, createKeyboardState, directionToAngle, getAvailableDirections, getDirectionFromKeys, getDirectionFromPosition, getDirectionFromSingleKey, getDirectionFromTwoKeys, getDistance, handleKeyDown, handleKeyUp, isArrowKey, resetKeyboardState, useFocusManagement, useMarkingMenuContext, useMarkingMenuGesture, useMarkingMenuStateMachine, useReducedMotion });