Skip to content

Commit

Permalink
Merge pull request #18 from UofA-Blueprint/ASC-76-add-member-modal
Browse files Browse the repository at this point in the history
✨ Implements generic modal
  • Loading branch information
PaulHo0501 authored Apr 1, 2024
2 parents 5661b94 + 3de1b78 commit e3257fa
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 2 deletions.
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand Down Expand Up @@ -64,7 +64,6 @@ const router = createBrowserRouter([
path: "/color-picker-test",
element: <ColorPickerTestRoute />,
},

{
path: "/member-header-test",
element: <MemberHeaderTestRoute />,
Expand All @@ -85,6 +84,10 @@ const router = createBrowserRouter([
path: "/input-field-test",
element: <InputFieldTestRoute />,
},
{
path: "/modal-test",
element: <ModalTestRoute />,
},
]);

function App() {
Expand Down
132 changes: 132 additions & 0 deletions src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<div
ref={modalRef}
className={clsx(body)}
>
<div className={clsx(header)}>
<div className={clsx(headerTitle)}>
<i className={clsx(headerTitleIcon)}>{icon}</i>
<h3>{title}</h3>
</div>
<div className={clsx(closeButton)}>
<button onClick={onClose}>
<X size={32} />
</button>
</div>
</div>
<div className={clsx(form)}>
{content}
{actions}
</div>
</div>
);
}

export default Modal;
67 changes: 67 additions & 0 deletions src/routes/ModalTestRoute.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="fixed inset-0 flex flex-col gap-6 items-center justify-center bg-pink-300">
<button
className="bg-blue-500 rounded"
onClick={toggleModal}
>
Open Modal
</button>
<Modal
isOpen={isModalOpen}
onClose={toggleModal}
title="Modal Title"
icon={<Truck size={24} />}
content={
<div className="flex flex-col gap-6">
<InputField
type="text"
error={false}
label="Name"
required={false}
/>
<InputField
type="text"
error={false}
required={false}
/>
<InputField
type="text"
error={false}
label="Phone"
required={false}
/>
</div>
}
actions={
<div className="flex flex-row gap-6 h-4">
<Button
text="Cancel"
fill={false}
onClick={toggleModal}
/>
<Button
text="Submit"
fill={true}
onClick={toggleModal}
/>
</div>
}
></Modal>
</div>
);
}

export default ModalTestRoute;

0 comments on commit e3257fa

Please sign in to comment.