UNPKG

@helpwave/hightide

Version:

helpwave's component and theming library

245 lines (240 loc) 6.59 kB
// src/components/user-action/Menu.tsx import { useEffect as useEffect3, useRef, useState as useState2 } from "react"; import clsx from "clsx"; // src/hooks/useOutsideClick.ts import { useEffect } from "react"; var useOutsideClick = (refs, handler) => { useEffect(() => { const listener = (event) => { if (event.target === null) return; if (refs.some((ref) => !ref.current || ref.current.contains(event.target))) { return; } handler(); }; document.addEventListener("mousedown", listener); document.addEventListener("touchstart", listener); return () => { document.removeEventListener("mousedown", listener); document.removeEventListener("touchstart", listener); }; }, [refs, handler]); }; // src/hooks/useHoverState.ts import { useEffect as useEffect2, useState } from "react"; var defaultUseHoverStateProps = { closingDelay: 200, isDisabled: false }; var useHoverState = (props = void 0) => { const { closingDelay, isDisabled } = { ...defaultUseHoverStateProps, ...props }; const [isHovered, setIsHovered] = useState(false); const [timer, setTimer] = useState(); const onMouseEnter = () => { if (isDisabled) { return; } clearTimeout(timer); setIsHovered(true); }; const onMouseLeave = () => { if (isDisabled) { return; } setTimer(setTimeout(() => { setIsHovered(false); }, closingDelay)); }; useEffect2(() => { if (timer) { return () => { clearTimeout(timer); }; } }); useEffect2(() => { if (timer) { clearTimeout(timer); } }, [isDisabled]); return { isHovered, setIsHovered, handlers: { onMouseEnter, onMouseLeave } }; }; // src/util/PropsWithFunctionChildren.ts var resolve = (children, bag) => { if (typeof children === "function") { return children(bag); } return children ?? void 0; }; var BagFunctionUtil = { resolve }; // src/hooks/usePopoverPosition.ts var defaultPopoverPositionOptions = { edgePadding: 16, outerGap: 4, horizontalAlignment: "leftInside", verticalAlignment: "bottomOutside", disabled: false }; var usePopoverPosition = (trigger, options) => { const { edgePadding, outerGap, verticalAlignment, horizontalAlignment, disabled } = { ...defaultPopoverPositionOptions, ...options }; if (disabled || !trigger) { return {}; } const left = { leftOutside: trigger.left - outerGap, leftInside: trigger.left, rightOutside: trigger.right + outerGap, rightInside: trigger.right, center: trigger.left + trigger.width / 2 }[horizontalAlignment]; const top = { topOutside: trigger.top - outerGap, topInside: trigger.top, bottomOutside: trigger.bottom + outerGap, bottomInside: trigger.bottom, center: trigger.top + trigger.height / 2 }[verticalAlignment]; const translateX = { leftOutside: "-100%", leftInside: void 0, rightOutside: void 0, rightInside: "-100%", center: "-50%" }[horizontalAlignment]; const translateY = { topOutside: "-100%", topInside: void 0, bottomOutside: void 0, bottomInside: "-100%", center: "-50%" }[verticalAlignment]; return { left: Math.max(left, edgePadding), top: Math.max(top, edgePadding), translate: [translateX ?? "0", translateY ?? "0"].join(" ") }; }; // src/components/user-action/Menu.tsx import { createPortal } from "react-dom"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; var MenuItem = ({ children, onClick, alignment = "left", isDisabled = false, className }) => /* @__PURE__ */ jsx( "div", { className: clsx("block px-3 py-1.5 first:rounded-t-md last:rounded-b-md text-sm font-semibold text-nowrap", { "text-right": alignment === "right", "text-left": alignment === "left", "text-disabled-text cursor-not-allowed": isDisabled, "text-menu-text hover:bg-primary/20": !isDisabled, "cursor-pointer": !!onClick }, className), onClick, children } ); function getScrollableParents(element) { const scrollables = []; let parent = element.parentElement; while (parent) { scrollables.push(parent); parent = parent.parentElement; } return scrollables; } var Menu = ({ trigger, children, alignmentHorizontal = "leftInside", alignmentVertical = "bottomOutside", showOnHover = false, disabled = false, menuClassName = "" }) => { const { isHovered: isOpen, setIsHovered: setIsOpen } = useHoverState({ isDisabled: !showOnHover || disabled }); const triggerRef = useRef(null); const menuRef = useRef(null); useOutsideClick([triggerRef, menuRef], () => setIsOpen(false)); const [isHidden, setIsHidden] = useState2(true); const bag = { isOpen, close: () => setIsOpen(false), toggleOpen: () => setIsOpen((prevState) => !prevState), disabled }; const menuPosition = usePopoverPosition( triggerRef.current?.getBoundingClientRect(), { verticalAlignment: alignmentVertical, horizontalAlignment: alignmentHorizontal, disabled } ); useEffect3(() => { if (!isOpen) return; const triggerEl = triggerRef.current; if (!triggerEl) return; const scrollableParents = getScrollableParents(triggerEl); const close = () => setIsOpen(false); scrollableParents.forEach((parent) => { parent.addEventListener("scroll", close); }); window.addEventListener("resize", close); return () => { scrollableParents.forEach((parent) => { parent.removeEventListener("scroll", close); }); window.removeEventListener("resize", close); }; }, [isOpen, setIsOpen]); useEffect3(() => { if (isOpen) { setIsHidden(false); } }, [isOpen]); return /* @__PURE__ */ jsxs(Fragment, { children: [ trigger(bag, triggerRef), createPortal(/* @__PURE__ */ jsx( "div", { ref: menuRef, onClick: (e) => e.stopPropagation(), className: clsx( "absolute rounded-md bg-menu-background text-menu-text shadow-around-lg shadow-strong z-[300]", { "animate-pop-in": isOpen, "animate-pop-out": !isOpen, "hidden": isHidden }, menuClassName ), onAnimationEnd: () => { if (!isOpen) { setIsHidden(true); } }, style: { ...menuPosition }, children: BagFunctionUtil.resolve(children, bag) } ), document.body) ] }); }; export { Menu, MenuItem }; //# sourceMappingURL=Menu.mjs.map