@helpwave/hightide
Version:
helpwave's component and theming library
245 lines (240 loc) • 6.59 kB
JavaScript
// 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