Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created a light-weight configurable modal #107

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"dependencies": {
"next": "12.1.6",
"react": "18.2.0",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"react-icons": "^4.8.0",
"yarn": "^1.22.19"
},
"devDependencies": {
"@types/jest": "28.1.3",
Expand Down
169 changes: 169 additions & 0 deletions frontend/src/Components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useCallback, useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { AiOutlineClose } from 'react-icons/ai';

/** AreYouSureModal | A lightweight modal thats just want you to be sure before you kill it. */

type ModalProps = {
children: React.ReactNode;
closeOverride?: () => void;
isOpen: boolean;
overlayDismissed?: boolean;
setOpen: (newOpen: boolean) => void;
size?: string;
title?: string;
};

const AreYouSureModal = ({
isOpen = false,
setOpen,
closeOverride,
children,
size = 'md',
title = '',
overlayDismissed = false,
}: ModalProps) => {
const [modalElement, setModalElement] = useState<HTMLDivElement | null>(null);

const close = useCallback(() => {
document.body.classList.remove('modal-open');
setOpen(false);
}, [setOpen]);

const onClose = useCallback(() => {
if (closeOverride && typeof closeOverride === 'function') {
closeOverride();
} else {
close();
}
}, [closeOverride, close]);

useEffect(() => {
if (!modalElement) {
const element = document.createElement('div');
setModalElement(element);
document.body.appendChild(element);
}

return () => {
if (modalElement) {
document.body.removeChild(modalElement);
}
};
}, [modalElement]);

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.keyCode === 27) {
onClose();
} else if (event.keyCode === 9) {
const focusableElements = modalElement?.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'
);
if (
focusableElements &&
focusableElements.length > 0 &&
event.target === focusableElements[focusableElements.length - 1]
) {
focusableElements[0]?.focus();

event.preventDefault();
}
}
};

if (isOpen) {
// Background scroll-locking
document.body.classList.add('modal-open');
const focusableElements = modalElement?.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'
);
if (focusableElements && focusableElements.length > 0) {
focusableElements[0]?.focus();
}

document.addEventListener('keydown', handleKeyDown);
}

return () => {
document.body.classList.remove('modal-open');
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, modalElement, onClose]);

return isOpen
? ReactDOM.createPortal(
<div>
{overlayDismissed ? (
<div className="modal-wrapper" onClick={onClose}>
<div
className={`modal-container ${size}`}
onClick={(e) => e.stopPropagation()}
>
<div className="modal-header">
<div style={{ flexGrow: '1' }}>
{title && (
<div className="modal-header title-container">
<h1 className="modal-title">{title}</h1>
</div>
)}
</div>
<div>
<div className="btn-close-wrapper">
<button
className="btn-close"
onClick={onClose}
style={{ fontSize: '2rem' }}
type="submit"
>
<AiOutlineClose />
</button>
</div>
</div>
</div>
<hr className="mt-1" />
<div className="modal-body">
<div onKeyDown={(e) => e.stopPropagation()}>
<div>{children}</div>
</div>
</div>
</div>
</div>
) : (
<div className="modal-wrapper">
<div
className={`modal-container ${size}`}
onClick={(e) => e.stopPropagation()}
>
<div className="modal-header">
<div style={{ flexGrow: '1' }}>
{title && (
<div className="modal-header title-container">
<h1 className="modal-title">{title}</h1>
</div>
)}
</div>
<div>
<div className="btn-close-wrapper">
<button className="btn-close" onClick={onClose}>
<AiOutlineClose />
</button>
</div>
</div>
</div>
<hr className="mt-1" />
<div className="modal-body">
<div onKeyDown={(e) => e.stopPropagation()}>
<div>{children}</div>
</div>
</div>
</div>
</div>
)}
</div>,
modalElement as HTMLDivElement
)
: null;
};

export default AreYouSureModal;
52 changes: 52 additions & 0 deletions frontend/src/fixtures/tableData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export default [
{
default: '',
description: 'A single child content element.',
id: 1,
name: 'children*',
type: 'element',
},
{
default: 'false',
description: 'If true, the component is shown.',
id: 2,
name: 'isOpen',
type: 'boolean',
},
{
default: '',
description: 'setter for isOpen',
id: 3,
name: 'setIsOpen',
type: 'function',
},
{
default: '',
description:
'Callback fired when the component requests to be closed. The reason parameter can optionally be used to control the response to onClose.',
id: 3,
name: 'closeOverride',
type: 'function',
},
{
default: "'md'",
description: "Options are 'xs', 'sm', 'md' & 'lg'.",
id: 4,
name: 'size',
type: 'string',
},
{
default: '',
description: "It's the title of the modal.",
id: 5,
name: 'title',
type: 'string',
},
{
default: 'false',
description: 'It handle overlay dismissed functionality',
id: 6,
name: 'overlayDismissed',
type: 'boolean',
},
];
1 change: 1 addition & 0 deletions frontend/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable canonical/filename-match-exported */
import { type AppProps } from 'next/app';
import '../../styles.css';

const App = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
Expand Down
121 changes: 120 additions & 1 deletion frontend/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,127 @@
/* eslint-disable canonical/filename-match-exported */
import { type NextPage } from 'next';
import { useEffect, useState } from 'react';
import AreYouSureModal from '../Components/Modal';
import tableData from 'fixtures/tableData';


const Index: NextPage = () => {
return <h1>Welcome to Contra!</h1>;
const [showModal, setShowModal] = useState<boolean>(false);

const handleShowModal = (): void => {
setShowModal(true);
};

const handleCloseModal = (): void => {
// Here we handle anything before the modal close. e.g if we are using form,
// we can guide user that your form values are not save, please save it before modal close.
if (window.confirm('Are you sure you want to kill it?')) {
setShowModal(false);
}
};

const [showChildModal, setShowChildModal] = useState<boolean>(false);
const handleShowChildModal = (): void => {
setShowChildModal(true);
};

const handleCloseChildModal = (): void => {
setShowChildModal(false);
};

useEffect(() => {
// Background scroll-locking
if (showModal || showChildModal) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}

return () => {
document.body.style.overflow = 'unset';
};
}, [showModal, showChildModal]);

return (
<div className="page-main-container">
<AreYouSureModal
closeOverride={handleCloseModal}
isOpen={showModal}
overlayDismissed
setOpen={setShowModal}
size="sm"
title="Are You Sure? <modal>"
>
<div>
<br />
<p className="paragraph-style">
A simple, lightweight and innocent modal that just wants you to be sure before you <b>KILL</b> it.
<br />
</p>


<button className="child-modal-btn" onClick={handleShowChildModal}>
Open Child Modal
</button>
<br />
<p className="paragraph-style">
This innocent modal can be configured in following ways:
<br />
</p>
<br />
<div className="table-wrapper">
<table
className="w-100 table-border"
style={{ color: 'black', border: 'solid 1px black' }}
>
<thead>
<tr>
<th className="table-border">Name</th>
<th className="table-border">Type</th>
<th className="table-border">Default</th>
<th className="table-border">Description</th>
</tr>
</thead>
<tbody>
{tableData.map((data) => (
<tr key={data.id}>
<td className="table-border">{data.name}</td>
<td className="table-border">{data.type}</td>
<td className="table-border">{data.default}</td>
<td className="table-border">{data.description}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>

<AreYouSureModal
closeOverride={handleCloseChildModal}
isOpen={showChildModal}
overlayDismissed={false}
setOpen={setShowChildModal}
size="xs"
title="Child Modal"
>
<div>
<div className="btn-container">
<button
className="child-modal-btn"
onClick={handleCloseChildModal}
>
Close Child Modal
</button>
</div>
</div>
</AreYouSureModal>
</AreYouSureModal>
{/* Button to open the modal */}
<button className="page-btn-modal-open" onClick={handleShowModal}>
Open Modal
</button>
</div>
);
};

export default Index;