securityos/components/system/Menu/MenuItemEntry.tsx

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;