-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b95b21a
commit 7d23cfe
Showing
6 changed files
with
273 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
65
src/components/AppLayout/Submenu /__tests__/Submenu.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
), | ||
}; |