Skip to content

Commit

Permalink
fix: review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
niloofar-deriv committed Jun 7, 2024
1 parent 7d23cfe commit bf244b4
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 167 deletions.
12 changes: 0 additions & 12 deletions src/components/AppLayout/Submenu /Submenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,6 @@
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;
}
Expand Down
69 changes: 22 additions & 47 deletions src/components/AppLayout/Submenu /__tests__/Submenu.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,40 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Submenu } from "..";

const mockText = "Submenu test content";

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

it("toggles submenu on button click", async () => {
it("does not render when isOpen is false", () => {
render(
<Submenu
icon={<span>Icon</span>}
label="Account settings"
submenuContent={<span>Close</span>}
/>,
<Submenu isOpen={false}>
<p>{mockText}</p>
</Submenu>,
);

expect(screen.queryByText("Close")).not.toBeInTheDocument();
// Open the submenu
await userEvent.click(screen.getByRole("button"));
expect(screen.getByText("Close")).toBeInTheDocument();
expect(screen.queryByText(mockText)).not.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>}
/>,
it("applies exit animation class when isOpen changes to false", () => {
const { rerender, container } = render(
<Submenu isOpen>
<p>{mockText}</p>
</Submenu>,
);
expect(container.firstChild).toHaveClass("submenu");

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>
rerender(
<Submenu isOpen={false}>
<p>{mockText}</p>
</Submenu>,
);

await userEvent.click(screen.getByRole("button"));
expect(screen.getByText("submenu children")).toBeInTheDocument();
expect(container.firstChild).toHaveClass("submenu_exit");
});
});
115 changes: 53 additions & 62 deletions src/components/AppLayout/Submenu /index.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,74 @@
import { ReactNode, useState, ComponentProps, PropsWithChildren } from "react";
import {
useState,
ComponentProps,
PropsWithChildren,
useRef,
useEffect,
} 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"];
type TSubmenu = ComponentProps<"div"> & {
isOpen: boolean;
};

/**
* 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.
* `Submenu` is a component that renders a collapsible/expandable menu which supports an exit animation when closed.
* The component will remain in the DOM just long enough to perform the exit animation before it is unmounted,
* ensuring a smooth user experience. It utilizes the CSS class `submenu_exit` to apply styles for the exit animation.
*
* @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.
* @param {React.ReactNode} [props.children] - The content to be rendered inside the submenu. This is optional and can be any React node.
* @param {string} [props.className] - Optional CSS class to be applied to the submenu container for additional styling.
* @param {boolean} props.isOpen - A boolean flag to control the visibility of the submenu. When set to true, the submenu is open or visible. When set to false, the submenu will start the exit animation and then unmount.
*
* @returns {JSX.Element|null} The JSX code for the submenu if it is mounted; otherwise, null if it is not mounted.
*
* @example
* // To use the Submenu component:
* <Submenu isOpen={true} className="custom-submenu">
* <p>Menu Content Here</p>
* </Submenu>
*
* @example
* // To trigger an exit animation, change isOpen to false:
* <Submenu isOpen={false} className="custom-submenu">
* <p>Menu Content Here</p>
* </Submenu>
*/
export const Submenu = ({
icon,
label,
labelSize = "md",
className,
children,
submenuContent,
submenuClassName,
className,
isOpen,
}: PropsWithChildren<TSubmenu>) => {
const [submenuOpen, SetSubmenuOpen] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);

Check failure on line 44 in src/components/AppLayout/Submenu /index.tsx

View workflow job for this annotation

GitHub Actions / Reporter

Cannot find namespace 'NodeJS'.

Check failure on line 44 in src/components/AppLayout/Submenu /index.tsx

View workflow job for this annotation

GitHub Actions / Build and test

Cannot find namespace 'NodeJS'.
const [isMounted, setIsMounted] = useState(isOpen);
const [isClosing, setIsClosing] = useState(false);

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

setTimeout(() => {
SetSubmenuOpen(false);
useEffect(() => {
if (isOpen) {
setIsMounted(true);
setIsClosing(false);
}, 500);
};
} else {
setIsClosing(true);
timerRef.current = setTimeout(() => {
setIsMounted(false);
setIsClosing(false);
}, 400);
}

const onOpenSubmenu = () => SetSubmenuOpen(true);
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [isOpen]);

if (!isMounted) return null;
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>
)}
</>
<div
className={clsx("submenu", { submenu_exit: isClosing }, className)}
>
{children}
</div>
);
};

Expand Down
74 changes: 28 additions & 46 deletions stories/Submenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,32 @@
import { useState } from "react";
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: "",
isOpen: false,
},
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: {
children: {
control: false,
description:
"The content displayed inside the submenu when it is open.",
"The children nodes provided to the submenu panel, which are displayed below the submenuContent.",
},
submenuClassName: {
control: false,
isOpen: {
description:
"Optional custom class name for styling the submenu container.",
"A boolean flag to control the visibility of the submenu. When set to true, the submenu is open or visible. When set to false, the submenu will start the exit animation and then unmount.",
},
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" },
parameters: { layout: "left" },
tags: ["autodocs"],
} satisfies Meta<typeof Submenu>;

Expand All @@ -59,19 +35,25 @@ 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>
),
render: () => {
const [isOpen, setIsOpen] = useState(false);

return (
<div
style={{
height: "400px",
width: "300px",
backgroundColor: "yellowgreen",
position: "relative",
padding: "20px",
}}
>
<button onClick={() => setIsOpen(true)}>Click me</button>
<Submenu isOpen={isOpen}>
<div>Test children</div>
<button onClick={() => setIsOpen(false)}>Close me</button>
</Submenu>
</div>
);
},
};

0 comments on commit bf244b4

Please sign in to comment.