From 3de1b780063adbe1763abc7859d2da0330aa93b8 Mon Sep 17 00:00:00 2001 From: Alastair Cottier Date: Wed, 20 Mar 2024 17:38:55 -0600 Subject: [PATCH] :sparkles: Implements generic modal --- src/App.tsx | 7 +- src/components/Modal.tsx | 132 ++++++++++++++++++++++++++++++++++ src/routes/ModalTestRoute.tsx | 67 +++++++++++++++++ 3 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 src/components/Modal.tsx create mode 100644 src/routes/ModalTestRoute.tsx diff --git a/src/App.tsx b/src/App.tsx index 5caee5b..877cc58 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,9 +15,9 @@ import ToastTestRoute from "@/routes/ToastTestRoute"; import MediaUploadZoneTestRoute from "@/routes/MediaUploadZoneTestRoute"; import ColorPickerTestRoute from "@/routes/ColorPickerTestRoute"; import InputFieldTestRoute from "./routes/InputFieldTestRoute"; - import MemberHeaderTestRoute from "@/routes/MemberHeaderTestRoute"; import { MemberInformation } from "./components/MemberInformation"; +import ModalTestRoute from "@/routes/ModalTestRoute"; const router = createBrowserRouter([ { @@ -64,7 +64,6 @@ const router = createBrowserRouter([ path: "/color-picker-test", element: , }, - { path: "/member-header-test", element: , @@ -85,6 +84,10 @@ const router = createBrowserRouter([ path: "/input-field-test", element: , }, + { + path: "/modal-test", + element: , + }, ]); function App() { diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 0000000..3f3affa --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,132 @@ +import clsx from "clsx"; +import { useEffect, useRef, useCallback } from "react"; +import { X } from "@phosphor-icons/react"; + +interface ModalProps { + /* Toggles the modal's visibility */ + isOpen: boolean; + + /* Callback to close the modal */ + onClose: () => void; + + /* Width of the modal (rem) */ + width?: number; + + /* Title of the modal */ + title: string; + + /* 32x32 Phosphor icon for the title */ + icon?: React.ReactNode; + + /* Disables the modal from closing on escape key press */ + disableCloseOnEscape?: boolean; + + /* Disables the modal from closing on click outside */ + disableCloseOnClickOutside?: boolean; + + /* Content of the modal */ + content?: React.ReactNode; + + /* Actions of the modal */ + actions?: React.ReactNode; +} +function Modal({ + isOpen = false, + onClose, + width = 35, + title, + icon = null, + disableCloseOnEscape = false, + disableCloseOnClickOutside = false, + content, + actions, +}: ModalProps) { + // Body style + const body = `flex flex-col w-[${width}rem] p-8 gap-4 bg-neutrals-light-100 rounded-lg`; + + // Header style + const header = "flex flex-row w-full justify-between items-center"; + + // Header title style + const headerTitle = "flex flex-row gap-1 text-h3 text-neutrals-dark-500"; + + // Header title icon style + const headerTitleIcon = "flex items-center justify-center pb-1"; + + // Close button style + const closeButton = + "flex items-center justify-center w-8 h-8 text-neutrals-dark-200"; + + // Form styles + const form = "flex flex-col gap-8"; + + // Encapsulates the visibility of the modal in a prop + if (!isOpen) return null; + + const modalRef = useRef(null); + + // Memoize the handleClickOutside function + const handleClickOutside = useCallback( + (event: MouseEvent) => { + if ( + !disableCloseOnClickOutside && + modalRef.current && + !modalRef.current.contains(event.target as Node) + ) { + onClose(); + } + }, + [disableCloseOnClickOutside, onClose], + ); + + // Close modal on click outside + useEffect(() => { + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen, handleClickOutside]); + + // Close modal on escape keypress + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (!disableCloseOnEscape && event.key === "Escape") { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener("keydown", handleKeyDown); + } + + return () => document.removeEventListener("keydown", handleKeyDown); + }, [isOpen, disableCloseOnEscape, onClose]); + + return ( +
+
+
+ {icon} +

{title}

+
+
+ +
+
+
+ {content} + {actions} +
+
+ ); +} + +export default Modal; diff --git a/src/routes/ModalTestRoute.tsx b/src/routes/ModalTestRoute.tsx new file mode 100644 index 0000000..c6c0371 --- /dev/null +++ b/src/routes/ModalTestRoute.tsx @@ -0,0 +1,67 @@ +import Modal from "@/components/Modal"; +import Button from "@/components/Button"; +import InputField from "@/components/InputField"; +import { Truck } from "@phosphor-icons/react"; +import { useState } from "react"; + +function ModalTestRoute() { + // State to toggle the modal + const [isModalOpen, setIsModalOpen] = useState(false); + + // Function to toggle the modal + const toggleModal = () => setIsModalOpen(!isModalOpen); + + return ( +
+ + } + content={ +
+ + + +
+ } + actions={ +
+
+ } + >
+
+ ); +} + +export default ModalTestRoute;