diff options
Diffstat (limited to 'FrontEnd/src/views/common/menu')
-rw-r--r-- | FrontEnd/src/views/common/menu/Menu.css | 24 | ||||
-rw-r--r-- | FrontEnd/src/views/common/menu/Menu.tsx | 72 | ||||
-rw-r--r-- | FrontEnd/src/views/common/menu/PopupMenu.css | 6 | ||||
-rw-r--r-- | FrontEnd/src/views/common/menu/PopupMenu.tsx | 84 |
4 files changed, 186 insertions, 0 deletions
diff --git a/FrontEnd/src/views/common/menu/Menu.css b/FrontEnd/src/views/common/menu/Menu.css new file mode 100644 index 00000000..c3fa82c4 --- /dev/null +++ b/FrontEnd/src/views/common/menu/Menu.css @@ -0,0 +1,24 @@ +.cru-menu {
+ min-width: 200px;
+}
+
+.cru-menu-item {
+ font-size: 1em;
+ padding: 0.5em 1.5em;
+ cursor: pointer;
+ transition: all 0.5s;
+ color: var(--cru-theme-color);
+}
+
+.cru-menu-item:hover {
+ color: var(--cru-theme-t-color);
+ background-color: var(--cru-theme-color);
+}
+
+.cru-menu-item-icon {
+ margin-right: 1em;
+}
+
+.cru-menu-divider {
+ border-top: 1px solid #e9ecef;
+}
diff --git a/FrontEnd/src/views/common/menu/Menu.tsx b/FrontEnd/src/views/common/menu/Menu.tsx new file mode 100644 index 00000000..d2f65391 --- /dev/null +++ b/FrontEnd/src/views/common/menu/Menu.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import classnames from "classnames"; +import { useTranslation } from "react-i18next"; + +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; + +import "./Menu.css"; + +export type MenuItem = + | { + type: "divider"; + } + | { + type: "button"; + text: I18nText; + iconClassName?: string; + color?: PaletteColorType; + onClick: () => void; + }; + +export type MenuItems = MenuItem[]; + +export type MenuProps = { + items: MenuItems; + onItemClicked?: () => void; + className?: string; + style?: React.CSSProperties; +}; + +export default function _Menu({ + items, + onItemClicked, + className, + style, +}: MenuProps): React.ReactElement | null { + const { t } = useTranslation(); + + return ( + <div className={classnames("cru-menu", className)} style={style}> + {items.map((item, index) => { + if (item.type === "divider") { + return <div key={index} className="cru-menu-divider" />; + } else { + return ( + <div + key={index} + className={classnames( + "cru-menu-item", + `cru-${item.color ?? "primary"}` + )} + onClick={() => { + item.onClick(); + onItemClicked?.(); + }} + > + {item.iconClassName != null ? ( + <i + className={classnames( + item.iconClassName, + "cru-menu-item-icon" + )} + /> + ) : null} + {convertI18nText(item.text, t)} + </div> + ); + } + })} + </div> + ); +} diff --git a/FrontEnd/src/views/common/menu/PopupMenu.css b/FrontEnd/src/views/common/menu/PopupMenu.css new file mode 100644 index 00000000..f6654f68 --- /dev/null +++ b/FrontEnd/src/views/common/menu/PopupMenu.css @@ -0,0 +1,6 @@ +.cru-popup-menu-menu-container {
+ z-index: 1040;
+ border-radius: 5px;
+ border: var(--cru-primary-color) 1px solid;
+ background-color: white;
+}
diff --git a/FrontEnd/src/views/common/menu/PopupMenu.tsx b/FrontEnd/src/views/common/menu/PopupMenu.tsx new file mode 100644 index 00000000..d7b81f49 --- /dev/null +++ b/FrontEnd/src/views/common/menu/PopupMenu.tsx @@ -0,0 +1,84 @@ +import classNames from "classnames"; +import React from "react"; +import { createPortal } from "react-dom"; +import { usePopper } from "react-popper"; + +import Menu, { MenuItems } from "./Menu"; + +import "./PopupMenu.css"; + +export interface PopupMenuProps { + items: MenuItems; + children?: React.ReactNode; + containerClassName?: string; + containerStyle?: React.CSSProperties; +} + +const PopupMenu: React.FC<PopupMenuProps> = ({ + items, + children, + containerClassName, + containerStyle, +}) => { + const [show, setShow] = React.useState<boolean>(false); + + const [referenceElement, setReferenceElement] = + React.useState<HTMLDivElement | null>(null); + const [popperElement, setPopperElement] = + React.useState<HTMLDivElement | null>(null); + const { styles, attributes } = usePopper(referenceElement, popperElement); + + React.useEffect(() => { + const handler = (event: MouseEvent): void => { + let element: HTMLElement | null = event.target as HTMLElement; + while (element) { + if (element == referenceElement || element == popperElement) { + return; + } + element = element.parentElement; + } + setShow(false); + }; + document.addEventListener("click", handler); + return () => { + document.removeEventListener("click", handler); + }; + }, [referenceElement, popperElement]); + + return ( + <> + <div + ref={setReferenceElement} + className={classNames( + "cru-popup-menu-trigger-container", + containerClassName + )} + style={containerStyle} + onClick={() => setShow(true)} + > + {children} + </div> + {show + ? createPortal( + <div + ref={setPopperElement} + className="cru-popup-menu-menu-container" + style={styles.popper} + {...attributes.popper} + > + <Menu + items={items} + onItemClicked={() => { + setShow(false); + }} + /> + </div>, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + document.getElementById("portal")! + ) + : null} + </> + ); +}; + +export default PopupMenu; |