✨ Component submenu working
This commit is contained in:
parent
a7779f82b0
commit
5e980501fb
@ -149,6 +149,10 @@ export enum ETypoColor {
|
|||||||
INPUT_ERROR = "--input-error",
|
INPUT_ERROR = "--input-error",
|
||||||
|
|
||||||
TEXT_ACCENT = "--text-accent",
|
TEXT_ACCENT = "--text-accent",
|
||||||
|
|
||||||
|
CONTRAST_DEFAULT = "--contrast-default",
|
||||||
|
CONTRAST_HOVERED = "--contrast-hovered",
|
||||||
|
ERROR_WEAK_CONTRAST = "--error-weak-contrast",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Typography(props: IProps) {
|
export default function Typography(props: IProps) {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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<HTMLDivElement>) => {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={classes["menu-item-wrapper"]}
|
||||||
|
onClick={element.onClick ?? handleClickElement}
|
||||||
|
data-link={element.link}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}>
|
||||||
|
<div className={classes["menu-item"]}>
|
||||||
|
{React.cloneElement(element.icon, { color: `var(${getColor()})` })}
|
||||||
|
<Typography typo={ETypo.TEXT_LG_REGULAR} color={getColor()}>
|
||||||
|
{element.text}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
{element.hasSeparator && <div className={classes["separator"]} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
69
src/front/Components/Elements/ButtonWithSubMenu/index.tsx
Normal file
69
src/front/Components/Elements/ButtonWithSubMenu/index.tsx
Normal file
@ -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<HTMLDivElement>(null);
|
||||||
|
const iconRef = useRef<HTMLDivElement>(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 (
|
||||||
|
<div className={classes["root"]}>
|
||||||
|
<div className={classes["main"]} onClick={handleClick} ref={iconRef}>
|
||||||
|
<IconButton icon={props.icon} variant={EIconButtonVariant.NEUTRAL} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isSubMenuOpened && (
|
||||||
|
<div className={classes["sub-menu"]} ref={subMenuRef}>
|
||||||
|
{props.subElements.map((element, index) => {
|
||||||
|
return <SubMenuItem element={element} key={index} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -9,15 +9,24 @@ import Modal from "@Front/Components/DesignSystem/Modal";
|
|||||||
import Newsletter from "@Front/Components/DesignSystem/Newsletter";
|
import Newsletter from "@Front/Components/DesignSystem/Newsletter";
|
||||||
import Table from "@Front/Components/DesignSystem/Table";
|
import Table from "@Front/Components/DesignSystem/Table";
|
||||||
import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag";
|
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 NumberPicker from "@Front/Components/Elements/NumberPicker";
|
||||||
import Tabs from "@Front/Components/Elements/Tabs";
|
import Tabs from "@Front/Components/Elements/Tabs";
|
||||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||||
import useOpenable from "@Front/Hooks/useOpenable";
|
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 { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
import classes from "./classes.module.scss";
|
import classes from "./classes.module.scss";
|
||||||
|
import ButtonWithSubMenu from "@Front/Components/Elements/ButtonWithSubMenu";
|
||||||
|
|
||||||
export default function DesignSystem() {
|
export default function DesignSystem() {
|
||||||
const { isOpen, open, close } = useOpenable();
|
const { isOpen, open, close } = useOpenable();
|
||||||
@ -69,6 +78,30 @@ export default function DesignSystem() {
|
|||||||
<Newsletter isOpen />
|
<Newsletter isOpen />
|
||||||
<div className={classes["root"]}>
|
<div className={classes["root"]}>
|
||||||
<div className={classes["components"]}>
|
<div className={classes["components"]}>
|
||||||
|
<Typography typo={ETypo.TEXT_LG_BOLD}>Button icon with menu</Typography>
|
||||||
|
<ButtonWithSubMenu
|
||||||
|
icon={<EllipsisHorizontalIcon />}
|
||||||
|
subElements={[
|
||||||
|
{
|
||||||
|
icon: <UsersIcon />,
|
||||||
|
text: "Modifier les collaborateurs",
|
||||||
|
onClick: () => alert("yo"),
|
||||||
|
hasSeparator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PencilSquareIcon />,
|
||||||
|
text: "Modifier les informations du dossier",
|
||||||
|
link: "/",
|
||||||
|
hasSeparator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ArchiveBoxIcon />,
|
||||||
|
text: "Archiver le dossier",
|
||||||
|
link: "/",
|
||||||
|
color: ETypoColor.ERROR_WEAK_CONTRAST,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<Typography typo={ETypo.TEXT_LG_BOLD}>Inputs</Typography>
|
<Typography typo={ETypo.TEXT_LG_BOLD}>Inputs</Typography>
|
||||||
<Typography typo={ETypo.TEXT_SM_REGULAR}>Number picker avec min à 1 et max à 10</Typography>
|
<Typography typo={ETypo.TEXT_SM_REGULAR}>Number picker avec min à 1 et max à 10</Typography>
|
||||||
<NumberPicker defaultValue={1} onChange={() => {}} min={1} max={10} />
|
<NumberPicker defaultValue={1} onChange={() => {}} min={1} max={10} />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user