From 17886e13aa9c4065b174dbc9d218653d3a065ba0 Mon Sep 17 00:00:00 2001 From: Saad Asad Khokhar Date: Fri, 5 May 2023 17:53:50 +0500 Subject: [PATCH] Created a lightwight configurable modal --- frontend/package.json | 4 +- frontend/src/Components/Modal/index.tsx | 169 ++++++++++++++ frontend/src/fixtures/tableData.js | 52 +++++ frontend/src/pages/_app.tsx | 1 + frontend/src/pages/index.tsx | 121 +++++++++- frontend/styles.css | 286 ++++++++++++++++++++++++ frontend/yarn.lock | 10 + 7 files changed, 641 insertions(+), 2 deletions(-) create mode 100644 frontend/src/Components/Modal/index.tsx create mode 100644 frontend/src/fixtures/tableData.js create mode 100644 frontend/styles.css diff --git a/frontend/package.json b/frontend/package.json index bba4dfee..7d807b7e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/Components/Modal/index.tsx b/frontend/src/Components/Modal/index.tsx new file mode 100644 index 00000000..ba17e1b9 --- /dev/null +++ b/frontend/src/Components/Modal/index.tsx @@ -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(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( +
+ {overlayDismissed ? ( +
+
e.stopPropagation()} + > +
+
+ {title && ( +
+

{title}

+
+ )} +
+
+
+ +
+
+
+
+
+
e.stopPropagation()}> +
{children}
+
+
+
+
+ ) : ( +
+
e.stopPropagation()} + > +
+
+ {title && ( +
+

{title}

+
+ )} +
+
+
+ +
+
+
+
+
+
e.stopPropagation()}> +
{children}
+
+
+
+
+ )} +
, + modalElement as HTMLDivElement + ) + : null; +}; + +export default AreYouSureModal; diff --git a/frontend/src/fixtures/tableData.js b/frontend/src/fixtures/tableData.js new file mode 100644 index 00000000..bf2c367c --- /dev/null +++ b/frontend/src/fixtures/tableData.js @@ -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', + }, + ]; \ No newline at end of file diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 3bee965f..4696693e 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -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 ; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index a36c83eb..7995ccd0 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -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

Welcome to Contra!

; + const [showModal, setShowModal] = useState(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(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 ( +
+ +
+
+

+ A simple, lightweight and innocent modal that just wants you to be sure before you KILL it. +
+

+ + + +
+

+ This innocent modal can be configured in following ways: +
+

+
+
+ + + + + + + + + + + {tableData.map((data) => ( + + + + + + + ))} + +
NameTypeDefaultDescription
{data.name}{data.type}{data.default}{data.description}
+
+
+ + +
+
+ +
+
+
+
+ {/* Button to open the modal */} + +
+ ); }; export default Index; diff --git a/frontend/styles.css b/frontend/styles.css new file mode 100644 index 00000000..6427fac9 --- /dev/null +++ b/frontend/styles.css @@ -0,0 +1,286 @@ +/* + ================== + Global CSS + ================== +*/ + +/* Font size of 1rem is equivalent to 10px */ +html { + font-size: 62.5%; +} + +/* Apply box-sizing border-box to all elements */ +*, +*::after, +*::before { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* + ================== + Layout CSS + ================== +*/ + +/* Margin */ +.m-3 { + margin: 2rem; +} + +.mt-1 { + margin-top: 1rem; +} + +/* Width */ +.xs { + width: 40%; +} +.sm { + width: 60%; +} + +.md { + width: 70%; +} + +.lg { + width: 80%; +} + +.w-100 { + width: 100%; +} + +/* Background color */ +.bg-danger { + background-color: #ec2f4b; +} + +.bg-warning { + background-color: yellow; +} + +.bg-success { + background-color: greenyellow; +} + +/* Flexbox */ +.d-flex { + display: flex; +} + +/* Links */ +a, +a:link { + font-family: inherit; + text-decoration: none; +} + +/* Headings */ +h4 { + color: #b993d6; +} + +/* Overflow hidden */ +.modal-open { + overflow: hidden; +} + +/* Body */ +body { + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Open Sans', + sans-serif; + min-height: 100vh; +} + +/* + ================== + Main Page CSS + ================== +*/ + +.page-main-container { + display: flex; + width: 100vw; + height: 100vh; + background: linear-gradient(to right, #8ca6db, #b993d6); +} + +.page-btn-modal-open { + margin: auto; + display: inline-block; + font-weight: 400; + color: #fff; + text-align: center; + border: 1px solid #fff; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + background-color: transparent; +} + +/* + ================== + Modal Section + ================== +*/ + +/* Wrapper */ +.modal-wrapper { + border-radius: 0; + color: #fff; + background-color: rgba(0, 0, 0, 0.75); + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + z-index: 1122; + padding-inline: 20px; +} + +/* Container */ +.modal-container { + /* background-color: white; */ + background: linear-gradient(to right, #8ca6db, #b993d6); + border-radius: 1rem; + min-height: 40rem; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); + position: relative; + z-index: 1125; + max-height: calc(100vh - 100px); +} + +/* Header */ +.modal-header { + display: flex; + padding-inline: 20px; + padding-block: 5px 0; +} + +/* Styles for Modal Components */ +.table-wrapper { + max-width: 1000px; + overflow-x: auto; +} + +.table-border { + border: solid 1px black; + padding: 5px; + font-size: 1.4rem; +} + +.btn-close-wrapper { + width: 100%; + display: flex; + justify-content: flex-end; + border-radius: 2rem; +} + +.btn-close { + background-color: transparent; + border: none; + border-radius: 50%; + margin-top: 1rem; + cursor: pointer; + font-size: 2rem; +} + +.modal-header { + display: flex; + justify-content: flex-end; +} + +.modal-body { + padding-inline: 20px; + overflow-y: auto; + max-height: calc(100vh - 200px); + padding-bottom: 15px; +} + +.title-container { + display: flex; + justify-content: center; +} + +.modal-title { + color: black; + font-size: 3rem; +} + +/* Styles for Child Modal Buttons */ +.btn-container { + display: flex; + align-items: center; + justify-content: center; + height: 200px; +} +.child-modal-btn { + background-color: #ea4c89; + border-radius: 8px; + border-style: none; + box-sizing: border-box; + color: #ffffff; + cursor: pointer; + display: inline-block; + font-family: 'Haas Grot Text R Web', 'Helvetica Neue', Helvetica, Arial, + sans-serif; + font-size: 14px; + font-weight: 500; + height: 40px; + line-height: 20px; + list-style: none; + margin: 0; + outline: none; + padding: 10px 16px; + position: relative; + text-align: center; + text-decoration: none; + transition: color 100ms; + vertical-align: baseline; + user-select: none; + -webkit-user-select: none; + touch-action: manipulation; + margin: 12px auto; +} + +.child-modal-btn:hover, +.child-modal-btn:focus { + background-color: #f082ac; +} + +.paragraph-style { + font-size: 1.6rem; + color: black; + padding: 0.3rem; +} + +/* Styles for Responsive Design */ +@media only screen and (max-width: 992px) { + .modal-container { + width: 100%; + } + .form-wrapper-element { + width: 100%; + } +} + +@media only screen and (max-width: 540px) { + .col-half { + width: 100%; + padding-right: 0; + } +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e8d32dba..ebf5319d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4151,6 +4151,11 @@ react-dom@18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-icons@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445" + integrity sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg== + react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -4973,6 +4978,11 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.0.0" +yarn@^1.22.19: + version "1.22.19" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.19.tgz#4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" + integrity sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ== + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"