diff options
Diffstat (limited to 'FrontEnd/src/components/menu')
-rw-r--r-- | FrontEnd/src/components/menu/Menu.css | 36 | ||||
-rw-r--r-- | FrontEnd/src/components/menu/Menu.tsx | 62 | ||||
-rw-r--r-- | FrontEnd/src/components/menu/PopupMenu.css | 7 | ||||
-rw-r--r-- | FrontEnd/src/components/menu/PopupMenu.tsx | 72 |
4 files changed, 177 insertions, 0 deletions
diff --git a/FrontEnd/src/components/menu/Menu.css b/FrontEnd/src/components/menu/Menu.css new file mode 100644 index 00000000..75734533 --- /dev/null +++ b/FrontEnd/src/components/menu/Menu.css @@ -0,0 +1,36 @@ +.cru-menu {
+ min-width: 200px;
+}
+
+.cru-menu-item {
+ display: block;
+ font-size: 1em;
+ width: 100%;
+ padding: 0.5em 1.5em;
+ transition: all 0.5s;
+ color: var(--cru-clickable-normal-color);
+ background-color: var(--cru-clickable-grayscale-normal-color);
+ border: none;
+ cursor: pointer;
+}
+
+.cru-menu-item:hover {
+ background-color: var(--cru-clickable-grayscale-hover-color);
+}
+
+.cru-menu-item:focus {
+ background-color: var(--cru-clickable-grayscale-focus-color);
+}
+
+.cru-menu-item:active {
+ background-color: var(--cru-clickable-grayscale-active-color);
+}
+
+.cru-menu-item-icon {
+ margin-right: 1em;
+}
+
+.cru-menu-divider {
+ border-width: 0;
+ border-top: 1px solid var(--cru-primary-color);
+}
\ No newline at end of file diff --git a/FrontEnd/src/components/menu/Menu.tsx b/FrontEnd/src/components/menu/Menu.tsx new file mode 100644 index 00000000..1a196a69 --- /dev/null +++ b/FrontEnd/src/components/menu/Menu.tsx @@ -0,0 +1,62 @@ +import { MouseEvent, CSSProperties } from "react"; +import classNames from "classnames"; + +import { useC, Text, ThemeColor } from "../common"; +import Icon from "../Icon"; + +import "./Menu.css"; + +export type MenuItem = + | { + type: "divider"; + } + | { + type: "button"; + text: Text; + icon?: string; + color?: ThemeColor; + onClick?: (e: MouseEvent<HTMLButtonElement>) => void; + }; + +export type MenuItems = MenuItem[]; + +export type MenuProps = { + items: MenuItems; + onItemClick?: (e: MouseEvent<HTMLButtonElement>) => void; + className?: string; + style?: CSSProperties; +}; + +export default function Menu({ + items, + onItemClick, + className, + style, +}: MenuProps) { + const c = useC(); + + return ( + <div className={classNames("cru-menu", className)} style={style}> + {items.map((item, index) => { + if (item.type === "divider") { + return <hr key={index} className="cru-menu-divider" />; + } else { + const { text, color, icon, onClick } = item; + return ( + <button + key={index} + className={`cru-menu-item cru-clickable-${color ?? "primary"}`} + onClick={(e) => { + onClick?.(e); + onItemClick?.(e); + }} + > + {icon != null && <Icon color={color} icon={icon} />} + {c(text)} + </button> + ); + } + })} + </div> + ); +} diff --git a/FrontEnd/src/components/menu/PopupMenu.css b/FrontEnd/src/components/menu/PopupMenu.css new file mode 100644 index 00000000..149e0699 --- /dev/null +++ b/FrontEnd/src/components/menu/PopupMenu.css @@ -0,0 +1,7 @@ +.cru-popup-menu-menu-container {
+ z-index: 1040;
+ border-radius: 3px;
+ border: var(--cru-clickable-normal-color) 1.5px solid;
+ background-color: var(--cru-background-color);
+ overflow: hidden;
+}
diff --git a/FrontEnd/src/components/menu/PopupMenu.tsx b/FrontEnd/src/components/menu/PopupMenu.tsx new file mode 100644 index 00000000..7ac2abfe --- /dev/null +++ b/FrontEnd/src/components/menu/PopupMenu.tsx @@ -0,0 +1,72 @@ +import { useState, CSSProperties, ReactNode } from "react"; +import classNames from "classnames"; +import { createPortal } from "react-dom"; +import { usePopper } from "react-popper"; + +import { ThemeColor } from "../common"; +import { useClickOutside } from "../hooks"; +import Menu, { MenuItems } from "./Menu"; + +import "./PopupMenu.css"; + +export interface PopupMenuProps { + color?: ThemeColor; + items: MenuItems; + children?: ReactNode; + containerClassName?: string; + containerStyle?: CSSProperties; +} + +export default function PopupMenu({ + color, + items, + children, + containerClassName, + containerStyle, +}: PopupMenuProps) { + const [show, setShow] = useState<boolean>(false); + + const [referenceElement, setReferenceElement] = + useState<HTMLDivElement | null>(null); + const [popperElement, setPopperElement] = useState<HTMLDivElement | null>( + null, + ); + const { styles, attributes } = usePopper(referenceElement, popperElement); + + useClickOutside(popperElement, () => setShow(false), true); + + return ( + <div + ref={setReferenceElement} + className={classNames( + "cru-popup-menu-trigger-container", + containerClassName, + )} + style={containerStyle} + onClick={() => setShow(true)} + > + {children} + {show && + createPortal( + <div + ref={setPopperElement} + className={`cru-popup-menu-menu-container cru-clickable-${ + color ?? "primary" + }`} + style={styles.popper} + {...attributes.popper} + > + <Menu + items={items} + onItemClick={(e) => { + setShow(false); + e.stopPropagation(); + }} + /> + </div>, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + document.getElementById("portal")!, + )} + </div> + ); +} |