diff --git a/src/front/Components/DesignSystem/Typography/index.tsx b/src/front/Components/DesignSystem/Typography/index.tsx index 30a5551e..8c5e87b1 100644 --- a/src/front/Components/DesignSystem/Typography/index.tsx +++ b/src/front/Components/DesignSystem/Typography/index.tsx @@ -149,6 +149,10 @@ export enum ETypoColor { INPUT_ERROR = "--input-error", TEXT_ACCENT = "--text-accent", + + CONTRAST_DEFAULT = "--contrast-default", + CONTRAST_HOVERED = "--contrast-hovered", + ERROR_WEAK_CONTRAST = "--error-weak-contrast", } export default function Typography(props: IProps) { diff --git a/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss new file mode 100644 index 00000000..3231922d --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/classes.module.scss @@ -0,0 +1,23 @@ +.menu-item-wrapper { + width: 100%; + .menu-item { + display: flex; + padding: var(--spacing-md, 16px); + justify-content: flex-start; + align-items: center; + gap: var(--spacing-lg, 24px); + cursor: pointer; + + > svg { + width: 24px; + height: 24px; + transition: all ease-in-out 0.1s; + } + } + + .separator { + width: 100%; + height: 1px; + background-color: var(--separator-stroke-light, #d7dce0); + } +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx new file mode 100644 index 00000000..83ccdc75 --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/SubMenuItem/index.tsx @@ -0,0 +1,44 @@ +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import classes from "./classes.module.scss"; +import { useRouter } from "next/router"; +import { ISubElementWithLink, ISubElementWithOnClick } from ".."; +import React, { useCallback } from "react"; +import useHoverable from "@Front/Hooks/useHoverable"; + +type IProps = { + element: ISubElementWithLink | ISubElementWithOnClick; +}; +export default function SubMenuItem(props: IProps) { + const { element } = props; + const router = useRouter(); + const handleClickElement = (e: React.MouseEvent) => { + const link = e.currentTarget.getAttribute("data-link"); + if (link) router.push(link); + }; + + const { handleMouseEnter, handleMouseLeave, isHovered } = useHoverable(); + + // The element has a link or an onClick but not both, if it has a link, show it has a link, if it has an onClick, show it has an onClick + const getColor = useCallback(() => { + if (isHovered && element.color !== ETypoColor.ERROR_WEAK_CONTRAST) return ETypoColor.CONTRAST_HOVERED; + if (element.color) return element.color; + return ETypoColor.CONTRAST_DEFAULT; + }, [element.color, isHovered]); + + return ( +
+
+ {React.cloneElement(element.icon, { color: `var(${getColor()})` })} + + {element.text} + +
+ {element.hasSeparator &&
} +
+ ); +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss b/src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss new file mode 100644 index 00000000..bbd8ced7 --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/classes.module.scss @@ -0,0 +1,20 @@ +.root { + position: relative; + width: fit-content; + + .sub-menu { + position: absolute; + top: 48px; + left: 0px; + display: inline-flex; + padding: var(--spacing-05, 4px) var(--spacing-2, 16px); + flex-direction: column; + align-items: flex-start; + border-radius: var(--menu-radius, 0px); + border: 1px solid var(--menu-border, #d7dce0); + background: var(--color-generic-white, #fff); + text-wrap: nowrap; + /* shadow/sm */ + box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1); + } +} diff --git a/src/front/Components/Elements/ButtonWithSubMenu/index.tsx b/src/front/Components/Elements/ButtonWithSubMenu/index.tsx new file mode 100644 index 00000000..7d9bf5e3 --- /dev/null +++ b/src/front/Components/Elements/ButtonWithSubMenu/index.tsx @@ -0,0 +1,69 @@ +import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton"; +import classes from "./classes.module.scss"; +import { ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import React, { useEffect, useRef, useState } from "react"; +import SubMenuItem from "./SubMenuItem"; + +type ISubElementBase = { + icon: JSX.Element; + text: string; + hasSeparator?: boolean; + color?: ETypoColor; +}; + +export type ISubElementWithLink = ISubElementBase & { + link: string; + onClick?: never; +}; + +export type ISubElementWithOnClick = ISubElementBase & { + onClick: () => void; + link?: never; +}; + +type IProps = { + icon: React.ReactNode; + text?: string; + subElements: (ISubElementWithLink | ISubElementWithOnClick)[]; +}; + +export default function ButtonWithSubMenu(props: IProps) { + const [isSubMenuOpened, setIsSubMenuOpened] = useState(false); + + const subMenuRef = useRef(null); + const iconRef = useRef(null); + + const handleClick = () => { + setIsSubMenuOpened((prev) => !prev); + }; + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ( + subMenuRef.current && + !subMenuRef.current.contains(e.target as Node) && + iconRef.current && + !iconRef.current.contains(e.target as Node) + ) { + setIsSubMenuOpened(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + return ( +
+
+ +
+ + {isSubMenuOpened && ( +
+ {props.subElements.map((element, index) => { + return ; + })} +
+ )} +
+ ); +} diff --git a/src/front/Components/Layouts/DesignSystem/index.tsx b/src/front/Components/Layouts/DesignSystem/index.tsx index 0e475bdf..4152498b 100644 --- a/src/front/Components/Layouts/DesignSystem/index.tsx +++ b/src/front/Components/Layouts/DesignSystem/index.tsx @@ -9,15 +9,24 @@ import Modal from "@Front/Components/DesignSystem/Modal"; import Newsletter from "@Front/Components/DesignSystem/Newsletter"; import Table from "@Front/Components/DesignSystem/Table"; import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag"; -import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import NumberPicker from "@Front/Components/Elements/NumberPicker"; import Tabs from "@Front/Components/Elements/Tabs"; import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate"; import useOpenable from "@Front/Hooks/useOpenable"; -import { ArrowLongLeftIcon, ArrowLongRightIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { + ArchiveBoxIcon, + ArrowLongLeftIcon, + ArrowLongRightIcon, + EllipsisHorizontalIcon, + PencilSquareIcon, + UsersIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; import { useCallback, useMemo, useState } from "react"; import classes from "./classes.module.scss"; +import ButtonWithSubMenu from "@Front/Components/Elements/ButtonWithSubMenu"; export default function DesignSystem() { const { isOpen, open, close } = useOpenable(); @@ -69,6 +78,30 @@ export default function DesignSystem() {
+ Button icon with menu + } + subElements={[ + { + icon: , + text: "Modifier les collaborateurs", + onClick: () => alert("yo"), + hasSeparator: true, + }, + { + icon: , + text: "Modifier les informations du dossier", + link: "/", + hasSeparator: true, + }, + { + icon: , + text: "Archiver le dossier", + link: "/", + color: ETypoColor.ERROR_WEAK_CONTRAST, + }, + ]} + /> Inputs Number picker avec min à 1 et max à 10 {}} min={1} max={10} />