-
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.
Merge pull request #214 from deriv-com/drawer-component
Niloofar/ Submenu component
- Loading branch information
Showing
6 changed files
with
205 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,33 @@ | ||
@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; | ||
|
||
&_exit { | ||
animation: closeDrawer 0.3s; | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
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,40 @@ | ||
import { render, screen } from "@testing-library/react"; | ||
import { Submenu } from ".."; | ||
|
||
const mockText = "Submenu test content"; | ||
|
||
describe("Submenu Component", () => { | ||
it("renders when isOpen is true", () => { | ||
render( | ||
<Submenu isOpen> | ||
<p>{mockText}</p> | ||
</Submenu>, | ||
); | ||
expect(screen.getByText(mockText)).toBeInTheDocument(); | ||
}); | ||
|
||
it("does not render when isOpen is false", () => { | ||
render( | ||
<Submenu isOpen={false}> | ||
<p>{mockText}</p> | ||
</Submenu>, | ||
); | ||
expect(screen.queryByText(mockText)).not.toBeInTheDocument(); | ||
}); | ||
|
||
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"); | ||
|
||
rerender( | ||
<Submenu isOpen={false}> | ||
<p>{mockText}</p> | ||
</Submenu>, | ||
); | ||
expect(container.firstChild).toHaveClass("submenu_exit"); | ||
}); | ||
}); |
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,71 @@ | ||
import { | ||
useState, | ||
ComponentProps, | ||
PropsWithChildren, | ||
useRef, | ||
useEffect, | ||
} from "react"; | ||
import clsx from "clsx"; | ||
import "./Submenu.scss"; | ||
|
||
type TSubmenu = ComponentProps<"div"> & { | ||
isOpen: boolean; | ||
}; | ||
|
||
/** | ||
* `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 {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 = ({ | ||
children, | ||
className, | ||
isOpen, | ||
...rest | ||
}: PropsWithChildren<TSubmenu>) => { | ||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null); | ||
const [isClosing, setIsClosing] = useState(true); | ||
|
||
useEffect(() => { | ||
if (isOpen) setIsClosing(false); | ||
else { | ||
timerRef.current = setTimeout(() => { | ||
setIsClosing(!isOpen); | ||
}, 400); | ||
} | ||
return () => { | ||
timerRef.current && clearTimeout(timerRef.current); | ||
}; | ||
}, [isOpen]); | ||
|
||
if (isClosing) return null; | ||
return ( | ||
<div | ||
className={clsx("submenu", { submenu_exit: !isOpen }, className)} | ||
{...rest} | ||
> | ||
{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,59 @@ | ||
import { useState } from "react"; | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import { Submenu } from "../src/main"; | ||
|
||
const meta = { | ||
title: "Components/Submenu", | ||
component: Submenu, | ||
args: { | ||
children: <span>Test Children</span>, | ||
className: "", | ||
isOpen: false, | ||
}, | ||
argTypes: { | ||
children: { | ||
control: false, | ||
description: | ||
"The children nodes provided to the submenu panel, which are displayed below the submenuContent.", | ||
}, | ||
isOpen: { | ||
description: | ||
"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.", | ||
}, | ||
}, | ||
parameters: { layout: "left" }, | ||
tags: ["autodocs"], | ||
} satisfies Meta<typeof Submenu>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = { | ||
name: "Submenu", | ||
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> | ||
); | ||
}, | ||
}; |