Skip to content

Commit

Permalink
feat: submenu component
Browse files Browse the repository at this point in the history
  • Loading branch information
niloofar-deriv committed Jun 7, 2024
1 parent b95b21a commit 7d23cfe
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 0 deletions.
45 changes: 45 additions & 0 deletions src/components/AppLayout/Submenu /Submenu.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@keyframes openSubmenu {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0);
}
}

@keyframes closeSubmenu {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}

.submenu {
background-color: #fff;
position: absolute;
inset: 0;
will-change: transform;
transform: translateX(-100%);
animation-name: openSubmenu;
animation-duration: 0.4s;
animation-fill-mode: forwards;
overflow: hidden;

&_button {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}

&_wrapper {
display: flex;
align-items: center;
}

&_exit {
animation: closeDrawer 0.3s;
}
}
65 changes: 65 additions & 0 deletions src/components/AppLayout/Submenu /__tests__/Submenu.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Submenu } from "..";

describe("Submenu Component", () => {
it("renders correctly with required props", () => {
render(
<Submenu
icon={<span>Icon</span>}
label="Account settings"
submenuContent={<button>Click me</button>}
/>,
);
expect(screen.getByText("Account settings")).toBeInTheDocument();
expect(screen.getByText("Icon")).toBeInTheDocument();
});

it("toggles submenu on button click", async () => {
render(
<Submenu
icon={<span>Icon</span>}
label="Account settings"
submenuContent={<span>Close</span>}
/>,
);

expect(screen.queryByText("Close")).not.toBeInTheDocument();
// Open the submenu
await userEvent.click(screen.getByRole("button"));
expect(screen.getByText("Close")).toBeInTheDocument();
});

it("applies custom class names", async () => {
const mockClassName = "test-class";

render(
<Submenu
icon={<span>Icon</span>}
label="Account settings"
submenuClassName={mockClassName}
submenuContent={<span>Submenu Content</span>}
/>,
);

await userEvent.click(screen.getByRole("button"));
expect(
screen.getByRole("button", { name: "Submenu Content" }).parentNode,
).toHaveClass(mockClassName);
});

it("renders the submenu children properly", async () => {
render(
<Submenu
icon={<span>Icon</span>}
label="Account settings"
submenuContent={<span>Submenu Content</span>}
>
<span>submenu children</span>
</Submenu>,
);

await userEvent.click(screen.getByRole("button"));
expect(screen.getByText("submenu children")).toBeInTheDocument();
});
});
84 changes: 84 additions & 0 deletions src/components/AppLayout/Submenu /index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ReactNode, useState, ComponentProps, PropsWithChildren } from "react";
import clsx from "clsx";
import { LegacyChevronRight1pxIcon } from "@deriv/quill-icons";
import { Text } from "../../Text";
import "./Submenu.scss";

type TSubmenu = {
icon: ReactNode;
label: string;
labelSize?: ComponentProps<typeof Text>["size"];
submenuContent: ReactNode;
submenuClassName?: ComponentProps<"div">["className"];
className?: ComponentProps<"button">["className"];
};

/**
* Represents a submenu component with expandable/collapsible functionality.
* This component displays a button that, when clicked, toggles the visibility
* of a submenu panel. The submenu can contain any ReactNode elements provided
* through props and has customizable text and icon components.
*
* @component
* @param {ReactNode} props.icon - The icon displayed in the button that toggles the submenu.
* @param {string} props.label - The label text displayed next to the icon in the toggle button.
* @param {string} [props.labelSize="md"] - The size of the label text, defaults to "md".
* @param {ReactNode} props.submenuContent - The content displayed inside the submenu when it is open.
* @param {string} [props.submenuClassName] - Optional custom class name for styling the submenu container.
* @param {string} [props.className] - Optional custom class name for styling the toggle button.
* @param {ReactNode} props.children - The children nodes provided to the submenu panel, which are displayed below the submenuContent.
* @returns {JSX.Element} The rendered Submenu component with toggle functionality.
*/
export const Submenu = ({
icon,
label,
labelSize = "md",
className,
children,
submenuContent,
submenuClassName,
}: PropsWithChildren<TSubmenu>) => {
const [submenuOpen, SetSubmenuOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);

const onCloseSubmenu = () => {
setIsClosing(true);

setTimeout(() => {
SetSubmenuOpen(false);
setIsClosing(false);
}, 500);
};

const onOpenSubmenu = () => SetSubmenuOpen(true);

return (
<>
<button
className={clsx("submenu_button", className)}
onClick={onOpenSubmenu}
>
<span className="submenu_wrapper">
{icon}
<Text size={labelSize}>{label}</Text>
</span>
<LegacyChevronRight1pxIcon iconSize="xs" />
</button>

{submenuOpen && (
<div
className={clsx(
"submenu",
{ submenu_exit: isClosing },
submenuClassName,
)}
>
<button onClick={onCloseSubmenu}>{submenuContent}</button>
{children}
</div>
)}
</>
);
};

Submenu.displayName = "Submenu";
1 change: 1 addition & 0 deletions src/components/AppLayout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { PlatformSwitcherItem } from "./PlatformSwitcher/PlatformSwitcherItem";
export { DesktopLanguagesModal } from "./LanguagesSwitcher/DesktopLanguagesModal";
export { MobileLanguagesDrawer } from "./LanguagesSwitcher/MobileLanguagesDrawer";
export { TooltipMenuIcon } from "./TooltipMenuIcon";
export { Submenu } from "./Submenu ";
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ export {
MobileLanguagesDrawer,
Notifications,
TooltipMenuIcon,
Submenu,
} from "./components/AppLayout";
export { ContextMenu } from "./components/ContextMenu";
77 changes: 77 additions & 0 deletions stories/Submenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Submenu } from "../src/main";
import { LegacyAdsIcon } from "@deriv/quill-icons";

const meta = {
title: "Components/Submenu",
component: Submenu,
args: {
children: <span>Test Children</span>,
icon: <LegacyAdsIcon iconSize="xs" />,
label: "test settings",
className: "",
labelSize: "md",
submenuContent: <span>Close</span>,
submenuClassName: "",
},
argTypes: {
icon: {
control: false,
description:
"The icon displayed in the button that toggles the submenu.",
},
label: {
control: false,
description:
"The label text displayed next to the icon in the toggle button.",
},
labelSize: {
control: false,
description: 'The size of the label text, defaults to "md".',
},
submenuContent: {
control: false,
description:
"The content displayed inside the submenu when it is open.",
},
submenuClassName: {
control: false,
description:
"Optional custom class name for styling the submenu container.",
},
className: {
control: false,
description:
"Optional custom class name for styling the toggle button.",
},
children: {
control: false,
description:
"The children nodes provided to the submenu panel, which are displayed below the submenuContent.",
},
},
parameters: { layout: "centered" },
tags: ["autodocs"],
} satisfies Meta<typeof Submenu>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
name: "Submenu",
render: (args) => (
<div
style={{
height: "400px",
width: "300px",
backgroundColor: "orange",
position: "relative",
padding: "20px",
}}
>
<Submenu {...args}>
<div>Test children</div>
</Submenu>
</div>
),
};

0 comments on commit 7d23cfe

Please sign in to comment.