141 lines
3.9 KiB
TypeScript
141 lines
3.9 KiB
TypeScript
import Menu, { topLeftPosition } from "components/system/Menu";
|
|
import type { MenuItem } from "contexts/menu/useMenuContextState";
|
|
import dynamic from "next/dynamic";
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import type { Position } from "react-rnd";
|
|
import { useTheme } from "styled-components";
|
|
import Button from "styles/common/Button";
|
|
import Icon from "styles/common/Icon";
|
|
import { FOCUSABLE_ELEMENT, PREVENT_SCROLL } from "utils/constants";
|
|
import { haltEvent } from "utils/functions";
|
|
|
|
type MenuItemEntryProps = MenuItem & {
|
|
isSubMenu: boolean;
|
|
resetMenu: () => void;
|
|
};
|
|
|
|
const Checkmark = dynamic(() =>
|
|
import("components/system/Menu/MenuIcons").then((mod) => mod.Checkmark)
|
|
);
|
|
|
|
const ChevronRight = dynamic(() =>
|
|
import("components/system/Menu/MenuIcons").then((mod) => mod.ChevronRight)
|
|
);
|
|
|
|
const Circle = dynamic(() =>
|
|
import("components/system/Menu/MenuIcons").then((mod) => mod.Circle)
|
|
);
|
|
|
|
const MenuItemEntry: FC<MenuItemEntryProps> = ({
|
|
action,
|
|
checked,
|
|
disabled,
|
|
icon,
|
|
isSubMenu,
|
|
label,
|
|
menu,
|
|
primary,
|
|
resetMenu,
|
|
seperator,
|
|
toggle,
|
|
}) => {
|
|
const entryRef = useRef<HTMLLIElement | null>(null);
|
|
const [subMenuOffset, setSubMenuOffset] = useState<Position>(topLeftPosition);
|
|
const [showSubMenu, setShowSubMenu] = useState(false);
|
|
const { sizes } = useTheme();
|
|
const onMouseEnter: React.MouseEventHandler = () => setShowSubMenu(true);
|
|
const onMouseLeave: React.MouseEventHandler = ({ relatedTarget }) => {
|
|
if (
|
|
!(relatedTarget instanceof HTMLElement) ||
|
|
!entryRef.current?.contains(relatedTarget)
|
|
) {
|
|
setShowSubMenu(false);
|
|
}
|
|
};
|
|
const subMenuEvents = menu
|
|
? {
|
|
onBlur: onMouseLeave as unknown as React.FocusEventHandler,
|
|
onMouseEnter,
|
|
onMouseLeave,
|
|
}
|
|
: {};
|
|
const triggerAction = useCallback<React.MouseEventHandler>(
|
|
(event) => {
|
|
haltEvent(event);
|
|
|
|
if (!menu) {
|
|
action?.();
|
|
resetMenu();
|
|
}
|
|
},
|
|
[action, menu, resetMenu]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const menuEntryElement = entryRef.current;
|
|
const showBaseMenu = !isSubMenu && menu && !showSubMenu;
|
|
const touchListener = (event: TouchEvent): void => {
|
|
if (showBaseMenu) {
|
|
haltEvent(event);
|
|
menuEntryElement?.focus(PREVENT_SCROLL);
|
|
}
|
|
setShowSubMenu(true);
|
|
};
|
|
|
|
menuEntryElement?.addEventListener("touchstart", touchListener, {
|
|
passive: !showBaseMenu,
|
|
});
|
|
|
|
return () =>
|
|
menuEntryElement?.removeEventListener("touchstart", touchListener);
|
|
}, [isSubMenu, menu, showSubMenu]);
|
|
|
|
useLayoutEffect(() => {
|
|
if (menu && entryRef.current) {
|
|
const { height, width } = entryRef.current.getBoundingClientRect();
|
|
|
|
setSubMenuOffset({
|
|
x: width - sizes.contextMenu.subMenuOffset,
|
|
y: 0 - height - sizes.contextMenu.subMenuOffset,
|
|
});
|
|
}
|
|
}, [menu, sizes.contextMenu.subMenuOffset]);
|
|
|
|
return (
|
|
<li
|
|
ref={entryRef}
|
|
className={disabled ? "disabled" : undefined}
|
|
{...FOCUSABLE_ELEMENT}
|
|
{...(menu && subMenuEvents)}
|
|
>
|
|
{seperator ? (
|
|
<hr />
|
|
) : (
|
|
<Button
|
|
as="figure"
|
|
className={showSubMenu ? "active" : undefined}
|
|
onClick={triggerAction}
|
|
onMouseUp={triggerAction}
|
|
>
|
|
{icon && <Icon alt={label} imgSize={16} src={icon} />}
|
|
{checked && <Checkmark className="left" />}
|
|
{toggle && <Circle className="left" />}
|
|
<figcaption className={primary ? "primary" : undefined}>
|
|
{label}
|
|
</figcaption>
|
|
{menu && <ChevronRight className="right" />}
|
|
</Button>
|
|
)}
|
|
{showSubMenu && <Menu subMenu={{ items: menu, ...subMenuOffset }} />}
|
|
</li>
|
|
);
|
|
};
|
|
|
|
export default MenuItemEntry;
|