feat(dashboard-v2): add Modal component
This commit is contained in:
parent
340fe5f203
commit
d27ef442f4
|
@ -6,8 +6,14 @@ import "@fontsource/sora/600.css"; // semibold
|
||||||
import "@fontsource/source-sans-pro/400.css"; // normal
|
import "@fontsource/source-sans-pro/400.css"; // normal
|
||||||
import "@fontsource/source-sans-pro/600.css"; // semibold
|
import "@fontsource/source-sans-pro/600.css"; // semibold
|
||||||
import "./src/styles/global.css";
|
import "./src/styles/global.css";
|
||||||
|
import { MODAL_ROOT_ID } from "./src/components/Modal";
|
||||||
|
|
||||||
export function wrapPageElement({ element, props }) {
|
export function wrapPageElement({ element, props }) {
|
||||||
const Layout = element.type.Layout ?? React.Fragment;
|
const Layout = element.type.Layout ?? React.Fragment;
|
||||||
return <Layout {...props}>{element}</Layout>;
|
return (
|
||||||
|
<Layout {...props}>
|
||||||
|
{element}
|
||||||
|
<div id={MODAL_ROOT_ID} />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,14 @@ import "@fontsource/sora/600.css"; // semibold
|
||||||
import "@fontsource/source-sans-pro/400.css"; // normal
|
import "@fontsource/source-sans-pro/400.css"; // normal
|
||||||
import "@fontsource/source-sans-pro/600.css"; // semibold
|
import "@fontsource/source-sans-pro/600.css"; // semibold
|
||||||
import "./src/styles/global.css";
|
import "./src/styles/global.css";
|
||||||
|
import { MODAL_ROOT_ID } from "./src/components/Modal";
|
||||||
|
|
||||||
export function wrapPageElement({ element, props }) {
|
export function wrapPageElement({ element, props }) {
|
||||||
const Layout = element.type.Layout ?? React.Fragment;
|
const Layout = element.type.Layout ?? React.Fragment;
|
||||||
return <Layout {...props}>{element}</Layout>;
|
return (
|
||||||
|
<Layout {...props}>
|
||||||
|
{element}
|
||||||
|
<div id={MODAL_ROOT_ID} />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }) => (
|
||||||
|
<ModalPortal>
|
||||||
|
<Overlay onClick={onClose}>
|
||||||
|
<div className="relative">
|
||||||
|
<button onClick={onClose} className="absolute top-[20px] right-[20px]">
|
||||||
|
<PlusIcon size={14} className="rotate-45" />
|
||||||
|
</button>
|
||||||
|
<Panel className={cn("px-8 py-6 sm:px-12 sm:py-10", className)}>{children}</Panel>
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
</ModalPortal>
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
<div
|
||||||
|
ref={overlayRef}
|
||||||
|
role="presentation"
|
||||||
|
onClick={handleClick}
|
||||||
|
className="fixed inset-0 z-50 bg-palette-100/80 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./ModalPortal";
|
Reference in New Issue