diff --git a/package-lock.json b/package-lock.json
index b57bd6f9..154a8350 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "@deriv-com/ui",
"version": "0.0.0-development",
"dependencies": {
+ "@deriv/quill-icons": "^1.22.5",
"@types/react-modal": "^3.16.3"
},
"devDependencies": {
@@ -2174,6 +2175,15 @@
"node": ">=0.1.90"
}
},
+ "node_modules/@deriv/quill-icons": {
+ "version": "1.22.5",
+ "resolved": "https://registry.npmjs.org/@deriv/quill-icons/-/quill-icons-1.22.5.tgz",
+ "integrity": "sha512-HsuM6yh/D3l+fQe7YSwUm25E9zoP7gP/S1/u524wRDFxUfNrq5N4VTpfy2Brl3fvRq0ModHjKaVcrB00Ebef1w==",
+ "peerDependencies": {
+ "react": ">= 16",
+ "react-dom": ">= 16"
+ }
+ },
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
@@ -15322,8 +15332,7 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "3.14.1",
@@ -15916,7 +15925,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -20447,7 +20455,6 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
- "dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -20505,7 +20512,6 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
- "dev": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
@@ -21180,7 +21186,6 @@
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
- "dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
diff --git a/package.json b/package.json
index 45bf7bb5..265775e7 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"test:report": "jest --collectCoverage"
},
"dependencies": {
+ "@deriv/quill-icons": "^1.22.5",
"@types/react-modal": "^3.16.3"
},
"devDependencies": {
diff --git a/playground/index.tsx b/playground/index.tsx
index 12b41819..8f67a326 100644
--- a/playground/index.tsx
+++ b/playground/index.tsx
@@ -1,9 +1,34 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { Drawer, Button } from "../src/main";
+const App = () => {
+ const [isOpen, setIsOpen] = React.useState(false);
-ReactDOM.createRoot(document.getElementById('root')!).render(
-
- playground app
- ,
-)
+ const handleCloseDrawer = () => {
+ setIsOpen(false);
+ };
+
+ return (
+
+
+
+ Menu
+
+ this is the ss
+ Footer
+
+
+
+ );
+};
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/src/components/AppLayout/Drawer/Drawer.scss b/src/components/AppLayout/Drawer/Drawer.scss
new file mode 100644
index 00000000..f1f8647f
--- /dev/null
+++ b/src/components/AppLayout/Drawer/Drawer.scss
@@ -0,0 +1,73 @@
+@keyframes openDrawer {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 60%,
+ 100% {
+ transform: translateX(0);
+ }
+ 80% {
+ transform: translateX(-5%);
+ }
+}
+
+@keyframes closeDrawer {
+ 0% {
+ transform: translateX(0);
+ }
+ 100% {
+ transform: translateX(-100%);
+ }
+}
+
+@keyframes disappear {
+ 0%{
+ opacity: 1;
+ }
+ 100%{
+ opacity: 0;
+ }
+}
+
+.deriv-drawer__container {
+ background-color: white;
+ position: absolute;
+ width: 60%;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ box-shadow:
+ 0 16px 16px 0 rgba(0, 0, 0, 0.16),
+ 0 0 16px 0 rgba(0, 0, 0, 0.16);
+ z-index: 10000;
+ display: flex;
+ flex-direction: column;
+ will-change: transform;
+ transform: translateX(-100%);
+ animation-name: openDrawer;
+ animation-duration: 0.4s;
+ animation-fill-mode: forwards;
+ overflow: hidden;
+
+ &.exit {
+ animation: closeDrawer 0.3s;
+ }
+}
+
+.deriv-drawer__overlay {
+ background-color: rgba(0, 0, 0, 0.5);
+ position: absolute;
+ inset: 0;
+ margin: 0;
+ overflow-y: hidden;
+ z-index: 9999;
+ will-change: transform;
+ transition-delay: 0.4s;
+
+ &.exit {
+ animation: disappear 0.3s;
+ opacity: 0;
+ }
+}
+
+
diff --git a/src/components/AppLayout/Drawer/DrawerContent/DrawerContent.scss b/src/components/AppLayout/Drawer/DrawerContent/DrawerContent.scss
new file mode 100644
index 00000000..a4430c95
--- /dev/null
+++ b/src/components/AppLayout/Drawer/DrawerContent/DrawerContent.scss
@@ -0,0 +1,4 @@
+.deriv-drawer__content {
+ flex: 1;
+ overflow-y: auto;
+}
\ No newline at end of file
diff --git a/src/components/AppLayout/Drawer/DrawerContent/__test__/DrawerContent.spec.tsx b/src/components/AppLayout/Drawer/DrawerContent/__test__/DrawerContent.spec.tsx
new file mode 100644
index 00000000..26796398
--- /dev/null
+++ b/src/components/AppLayout/Drawer/DrawerContent/__test__/DrawerContent.spec.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { DrawerContent } from "../";
+
+describe("DrawerContent Component", () => {
+ it("renders the component correctly", () => {
+ render(
+
+ some test content
+ ,
+ );
+
+ const contentElement = screen.getByText("some test content");
+ expect(contentElement).toBeInTheDocument();
+ });
+
+ it("applies className correctly", () => {
+ render(
+
+ some test content
+ ,
+ );
+
+ const contentElement = screen.getByText("some test content");
+ expect(contentElement).toBeInTheDocument();
+ expect(contentElement).toHaveClass("test-class");
+ });
+});
diff --git a/src/components/AppLayout/Drawer/DrawerContent/index.tsx b/src/components/AppLayout/Drawer/DrawerContent/index.tsx
new file mode 100644
index 00000000..31a94eca
--- /dev/null
+++ b/src/components/AppLayout/Drawer/DrawerContent/index.tsx
@@ -0,0 +1,20 @@
+import { ComponentProps, PropsWithChildren } from "react";
+
+import "./DrawerContent.scss";
+import clsx from "clsx";
+
+type DrawerContentProps = ComponentProps<"div">;
+export const DrawerContent = ({
+ children,
+ className,
+ ...rest
+}: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ );
+};
+
+DrawerContent.displayName = "DrawerContent";
+
diff --git a/src/components/AppLayout/Drawer/DrawerFooter/DrawerFooter.scss b/src/components/AppLayout/Drawer/DrawerFooter/DrawerFooter.scss
new file mode 100644
index 00000000..8edbad8f
--- /dev/null
+++ b/src/components/AppLayout/Drawer/DrawerFooter/DrawerFooter.scss
@@ -0,0 +1,6 @@
+.deriv-drawer__footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-top: 1px solid var(--du-border-divider, #f2f3f4);
+}
\ No newline at end of file
diff --git a/src/components/AppLayout/Drawer/DrawerFooter/__test__/DrawerFooter.spec.tsx b/src/components/AppLayout/Drawer/DrawerFooter/__test__/DrawerFooter.spec.tsx
new file mode 100644
index 00000000..838073c5
--- /dev/null
+++ b/src/components/AppLayout/Drawer/DrawerFooter/__test__/DrawerFooter.spec.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { DrawerFooter } from "../";
+
+describe("DrawerFooter Component", () => {
+ it("renders the component correctly", () => {
+ render(
+
+ footer content
+ ,
+ );
+
+ const contentElement = screen.getByText("footer content");
+ expect(contentElement).toBeInTheDocument();
+ });
+
+ it("applies className correctly", () => {
+ render(
+
+ some test content
+ ,
+ );
+
+ const contentElement = screen.getByText("some test content");
+ screen.debug();
+ expect(contentElement).toBeInTheDocument();
+ expect(contentElement).toHaveClass("deriv-drawer__footer test-class");
+ });
+});
diff --git a/src/components/AppLayout/Drawer/DrawerFooter/index.tsx b/src/components/AppLayout/Drawer/DrawerFooter/index.tsx
new file mode 100644
index 00000000..61093daa
--- /dev/null
+++ b/src/components/AppLayout/Drawer/DrawerFooter/index.tsx
@@ -0,0 +1,19 @@
+import { ComponentProps, PropsWithChildren } from "react";
+
+import "./DrawerFooter.scss";
+import clsx from "clsx";
+
+type DrawerFooterProps = ComponentProps<"div">;
+export const DrawerFooter = ({
+ children,
+ className,
+ ...rest
+}: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ );
+};
+
+DrawerFooter.displayName = "DrawerFooter";
diff --git a/src/components/AppLayout/Drawer/DrawerHeader/DrawerHeader.scss b/src/components/AppLayout/Drawer/DrawerHeader/DrawerHeader.scss
new file mode 100644
index 00000000..41add271
--- /dev/null
+++ b/src/components/AppLayout/Drawer/DrawerHeader/DrawerHeader.scss
@@ -0,0 +1,23 @@
+.deriv-drawer__header {
+ display: flex;
+ height: 50px;
+ z-index: 1001;
+ border-bottom: 1px solid var(--du-border-divider, #f2f3f4);
+
+
+ &__close-btn{
+ display: flex;
+ cursor: pointer;
+ padding: 12px;
+ border-right: 1px solid var(--du-border-divider, #f2f3f4);
+ align-items: center;
+ margin: 5px 12px 5px 0;
+ }
+
+ &__content {
+ display: flex;
+ flex: 1;
+ overflow-y: auto;
+ align-items: center;
+ }
+}
\ No newline at end of file
diff --git a/src/components/AppLayout/Drawer/DrawerHeader/__test__/DrawerHeader.spec.tsx b/src/components/AppLayout/Drawer/DrawerHeader/__test__/DrawerHeader.spec.tsx
new file mode 100644
index 00000000..cf89df4f
--- /dev/null
+++ b/src/components/AppLayout/Drawer/DrawerHeader/__test__/DrawerHeader.spec.tsx
@@ -0,0 +1,75 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { DrawerHeader } from "..";
+
+describe("DrawerHeader Component", () => {
+ const onCloseDrawerMock = jest.fn();
+
+ beforeEach(() => {
+ onCloseDrawerMock.mockClear();
+ });
+
+ it("renders Drawer correctly", () => {
+ render(
+
+ Test Header Content
+ ,
+ );
+
+ const headerContentElement = screen.getByText("Test Header Content");
+ expect(headerContentElement).toBeInTheDocument();
+ });
+
+ it("applies wrapper class correctly", () => {
+ const wrapperClassName = "test-class";
+ render(
+
+ Test Header Content
+ ,
+ );
+
+ const headerContentElement = screen.getByText("Test Header Content");
+ expect(headerContentElement).toBeInTheDocument();
+ expect(headerContentElement.parentElement).toHaveClass(
+ "deriv-drawer__header",
+ wrapperClassName,
+ );
+ });
+
+ it("applies wrapper class correctly", () => {
+ const className = "test-class";
+ render(
+
+ Test Header Content
+ ,
+ );
+
+ const headerContentElement = screen.getByText("Test Header Content");
+ expect(headerContentElement).toBeInTheDocument();
+ expect(headerContentElement).toHaveClass(
+ "deriv-drawer__header__content",
+ className,
+ );
+ });
+
+ it("calls onCloseDrawer when close button is clicked", async () => {
+ render(
+
+ Test Header Content
+ ,
+ );
+
+ const closeButton = screen.getByRole("drawer-close-button");
+ expect(closeButton).toBeInTheDocument();
+
+ await fireEvent.click(closeButton);
+
+ expect(onCloseDrawerMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/AppLayout/Drawer/DrawerHeader/index.tsx b/src/components/AppLayout/Drawer/DrawerHeader/index.tsx
new file mode 100644
index 00000000..fa907a4d
--- /dev/null
+++ b/src/components/AppLayout/Drawer/DrawerHeader/index.tsx
@@ -0,0 +1,47 @@
+import { ComponentProps, PropsWithChildren } from "react";
+import { LegacyClose2pxIcon } from "@deriv/quill-icons";
+
+import "./DrawerHeader.scss";
+import clsx from "clsx";
+
+type DrawerHeaderProps = ComponentProps<"div"> & {
+ onCloseDrawer: () => void;
+ wrapperClassName?: string;
+};
+
+/**
+ * DrawerHeader component.
+ * @param {VoidFunction} onCloseDrawer
+ * @param {React.ReactNode} children - The children nodes to be rendered inside the header.
+ * @param {string} className - Applies on the right side content wrapper of the Drawerheader.
+ * @param {string} WrapperClassName - Applies on the header wrapper
+ *
+ * @returns {JSX.Element} - Returns the header element.
+ */
+export const DrawerHeader = ({
+ onCloseDrawer,
+ children,
+ className,
+ wrapperClassName,
+ ...rest
+}: PropsWithChildren) => {
+ return (
+
+ );
+};
+
+DrawerHeader.displayName = "DrawerHeader";
diff --git a/src/components/AppLayout/Drawer/__test__/Drawer.spec.tsx b/src/components/AppLayout/Drawer/__test__/Drawer.spec.tsx
new file mode 100644
index 00000000..26913638
--- /dev/null
+++ b/src/components/AppLayout/Drawer/__test__/Drawer.spec.tsx
@@ -0,0 +1,59 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { Drawer } from "..";
+
+describe("Drawer Component", () => {
+ const onCloseDrawerMock = jest.fn();
+
+ beforeEach(() => {
+ onCloseDrawerMock.mockClear();
+ });
+
+ test("renders children when isOpen is true", () => {
+ render(
+
+ Test Drawer Content
+ ,
+ );
+
+ const drawerContentElement = screen.getByText("Test Drawer Content");
+ expect(drawerContentElement).toBeInTheDocument();
+ });
+
+ test("does not render children when isOpen is false", () => {
+ const testText = "Test Drawer Content";
+ render(
+
+ {testText}
+ ,
+ );
+
+ const drawerContentElement = screen.queryByText(testText);
+ expect(drawerContentElement).not.toBeInTheDocument();
+ });
+
+ test("calls onCloseDrawer when overlay is clicked", async () => {
+ render(
+
+ Test Drawer Content
+
+ );
+
+ const overlayElement = screen.getByTestId("drawer-overlay");
+ await fireEvent.click(overlayElement);
+
+ expect(onCloseDrawerMock).toHaveBeenCalledTimes(1);
+ });
+
+ test("apply the width to the drawer", async () => {
+ render(
+
+ Test Drawer Content
+
+ );
+
+ const overlayElement = screen.getByText("Test Drawer Content");
+ expect(overlayElement).toHaveStyle("width: 400px");
+
+ });
+});
diff --git a/src/components/AppLayout/Drawer/index.tsx b/src/components/AppLayout/Drawer/index.tsx
new file mode 100644
index 00000000..49a6bcfb
--- /dev/null
+++ b/src/components/AppLayout/Drawer/index.tsx
@@ -0,0 +1,76 @@
+import React, { ComponentProps, useEffect, useRef, useState } from "react";
+import { DrawerHeader } from "./DrawerHeader";
+import { DrawerContent } from "./DrawerContent";
+import { DrawerFooter } from "./DrawerFooter";
+
+import "./Drawer.scss";
+import clsx from "clsx";
+
+type DrawerProps = ComponentProps<"div"> & {
+ isOpen: boolean;
+ onCloseDrawer: () => void;
+ width?: string;
+ overlayClassName?: string;
+};
+
+export const Drawer = ({
+ isOpen,
+ onCloseDrawer,
+ width = "auto",
+ children,
+ className,
+ overlayClassName,
+ ...rest
+}: React.PropsWithChildren) => {
+ const [hide, setHide] = useState(true);
+ const timerRef = useRef | null>(null);
+
+ useEffect(() => {
+ if (isOpen) setHide(false);
+ else {
+ timerRef.current = setTimeout(() => {
+ setHide(!isOpen);
+ }, 400);
+ }
+ return () => {
+ timerRef.current && clearTimeout(timerRef.current);
+ };
+ }, [isOpen]);
+
+ if (hide) return null;
+
+ return (
+ <>
+ {
+ e.stopPropagation();
+ onCloseDrawer();
+ }}
+ />
+
+ {children}
+
+ >
+ );
+};
+
+Drawer.Header = DrawerHeader;
+Drawer.Content = DrawerContent;
+Drawer.Footer = DrawerFooter;
+
+Drawer.displayName = "Drawer";
+
diff --git a/src/components/AppLayout/SideBarMenu/index.tsx b/src/components/AppLayout/SideBarMenu/index.tsx
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/components/AppLayout/index.ts b/src/components/AppLayout/index.ts
index 3189715c..b01cea9d 100644
--- a/src/components/AppLayout/index.ts
+++ b/src/components/AppLayout/index.ts
@@ -1,3 +1,4 @@
-export { Header } from "./Header";
+export { Drawer } from "./Drawer";
export { Footer } from "./Footer";
+export { Header } from "./Header";
export { Wrapper } from "./Wrapper";
diff --git a/src/main.ts b/src/main.ts
index dfe40a8b..624794bd 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -23,4 +23,4 @@ export { ToggleSwitch } from "./components/ToggleSwitch";
export { Tooltip } from "./components/Tooltip";
export { useDevice } from "./hooks";
export { VerticalTab, VerticalTabItems } from "./components/VerticalTab";
-export { Header, Footer, Wrapper } from "./components/AppLayout";
+export { Header, Footer, Wrapper, Drawer } from "./components/AppLayout";
diff --git a/stories/Drawer.stories.tsx b/stories/Drawer.stories.tsx
new file mode 100644
index 00000000..66524ac8
--- /dev/null
+++ b/stories/Drawer.stories.tsx
@@ -0,0 +1,73 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { Drawer, Button } from "../src/main";
+import { useEffect, useState } from "react";
+
+const meta = {
+ title: "Components/Drawer",
+ component: Drawer,
+ parameters: {
+ layout: "centered",
+ },
+ tags: ["autodocs"],
+ args: {
+ children:
,
+ isOpen: false,
+ onCloseDrawer: () => {},
+ },
+ argTypes: {
+ children: {
+ description:
+ "The content of the Drawer.you can use the `Drawer.Header`, `Drawer.Body`, and `Drawer.Footer` components to structure the content of the Drawer.",
+ control: false,
+ },
+ isOpen: {
+ description: "controls the visibility of the Drawer",
+ control: {
+ type: "boolean",
+ },
+ },
+ onCloseDrawer: {
+ description: "Callback function to close the Drawer",
+ control: false,
+ },
+ },
+} satisfies Meta
;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ name: "Default Drawer",
+ args: {
+ isOpen: false,
+ },
+ render: (args) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleCloseDrawer = () => {
+ setIsOpen(false);
+ };
+ useEffect(() => {
+ setIsOpen(args.isOpen);
+ }, [args.isOpen]);
+ return (
+
+
+
+ Menu
+
+ this is the content
+ Footer
+
+
+
+ );
+ },
+};