From d27ef442f425714c34dae6b67c24d312e5d96702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 24 Mar 2022 10:39:18 +0100 Subject: [PATCH] feat(dashboard-v2): add Modal component --- packages/dashboard-v2/gatsby-browser.js | 8 +++- packages/dashboard-v2/gatsby-ssr.js | 8 +++- .../src/components/Modal/Modal.js | 37 ++++++++++++++++ .../src/components/Modal/ModalPortal.js | 16 +++++++ .../src/components/Modal/Overlay.js | 42 +++++++++++++++++++ .../src/components/Modal/index.js | 1 + 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 packages/dashboard-v2/src/components/Modal/Modal.js create mode 100644 packages/dashboard-v2/src/components/Modal/ModalPortal.js create mode 100644 packages/dashboard-v2/src/components/Modal/Overlay.js create mode 100644 packages/dashboard-v2/src/components/Modal/index.js diff --git a/packages/dashboard-v2/gatsby-browser.js b/packages/dashboard-v2/gatsby-browser.js index a71e49c3..a39bdb48 100644 --- a/packages/dashboard-v2/gatsby-browser.js +++ b/packages/dashboard-v2/gatsby-browser.js @@ -6,8 +6,14 @@ import "@fontsource/sora/600.css"; // semibold import "@fontsource/source-sans-pro/400.css"; // normal import "@fontsource/source-sans-pro/600.css"; // semibold import "./src/styles/global.css"; +import { MODAL_ROOT_ID } from "./src/components/Modal"; export function wrapPageElement({ element, props }) { const Layout = element.type.Layout ?? React.Fragment; - return {element}; + return ( + + {element} +
+ + ); } diff --git a/packages/dashboard-v2/gatsby-ssr.js b/packages/dashboard-v2/gatsby-ssr.js index a71e49c3..a39bdb48 100644 --- a/packages/dashboard-v2/gatsby-ssr.js +++ b/packages/dashboard-v2/gatsby-ssr.js @@ -6,8 +6,14 @@ import "@fontsource/sora/600.css"; // semibold import "@fontsource/source-sans-pro/400.css"; // normal import "@fontsource/source-sans-pro/600.css"; // semibold import "./src/styles/global.css"; +import { MODAL_ROOT_ID } from "./src/components/Modal"; export function wrapPageElement({ element, props }) { const Layout = element.type.Layout ?? React.Fragment; - return {element}; + return ( + + {element} +
+ + ); } diff --git a/packages/dashboard-v2/src/components/Modal/Modal.js b/packages/dashboard-v2/src/components/Modal/Modal.js new file mode 100644 index 00000000..6fd8e00a --- /dev/null +++ b/packages/dashboard-v2/src/components/Modal/Modal.js @@ -0,0 +1,37 @@ +import cn from "classnames"; +import PropTypes from "prop-types"; + +import { PlusIcon } from "../Icons"; +import { Panel } from "../Panel"; + +import { ModalPortal } from "./ModalPortal"; +import { Overlay } from "./Overlay"; + +export const Modal = ({ children, className, onClose }) => ( + + +
+ + {children} +
+
+
+); + +Modal.propTypes = { + /** + * Modal's body. + */ + children: PropTypes.node.isRequired, + /** + * Handler function to be called when user clicks on the "X" icon, + * or outside of the modal. + */ + onClose: PropTypes.func.isRequired, + /** + * Additional CSS classes to be applied to modal's body. + */ + className: PropTypes.string, +}; diff --git a/packages/dashboard-v2/src/components/Modal/ModalPortal.js b/packages/dashboard-v2/src/components/Modal/ModalPortal.js new file mode 100644 index 00000000..ef18e612 --- /dev/null +++ b/packages/dashboard-v2/src/components/Modal/ModalPortal.js @@ -0,0 +1,16 @@ +import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +export const MODAL_ROOT_ID = "__modal-root"; + +export const ModalPortal = ({ children }) => { + const ref = useRef(); + const [isClientSide, setIsClientSide] = useState(false); + + useEffect(() => { + ref.current = document.querySelector(MODAL_ROOT_ID) || document.body; + setIsClientSide(true); + }, []); + + return isClientSide ? createPortal(children, ref.current) : null; +}; diff --git a/packages/dashboard-v2/src/components/Modal/Overlay.js b/packages/dashboard-v2/src/components/Modal/Overlay.js new file mode 100644 index 00000000..7f1cbb35 --- /dev/null +++ b/packages/dashboard-v2/src/components/Modal/Overlay.js @@ -0,0 +1,42 @@ +import { useRef } from "react"; +import { useLockBodyScroll } from "react-use"; +import PropTypes from "prop-types"; + +export const Overlay = ({ children, onClick }) => { + const overlayRef = useRef(null); + + useLockBodyScroll(true); + + const handleClick = (event) => { + if (event.target !== overlayRef.current) { + return; + } + + event.nativeEvent.stopImmediatePropagation(); + + onClick?.(event); + }; + + return ( +
+ {children} +
+ ); +}; + +Overlay.propTypes = { + /** + * Overlay's body. + */ + children: PropTypes.node.isRequired, + /** + * Handler function to be called when user clicks on the overlay + * (but not the overlay's content). + */ + onClick: PropTypes.func, +}; diff --git a/packages/dashboard-v2/src/components/Modal/index.js b/packages/dashboard-v2/src/components/Modal/index.js new file mode 100644 index 00000000..28d34710 --- /dev/null +++ b/packages/dashboard-v2/src/components/Modal/index.js @@ -0,0 +1 @@ +export * from "./ModalPortal";