2024-02-16 16:19:03 +01:00
|
|
|
import React, { PropsWithChildren, ReactNode, useEffect } from "react";
|
2024-01-18 14:38:48 +01:00
|
|
|
import classNames from "classnames";
|
2024-04-04 15:01:29 +02:00
|
|
|
import ReactModal from "react-modal";
|
2024-01-18 14:38:48 +01:00
|
|
|
import { Button } from ":/components/Button";
|
2024-04-04 16:59:12 +02:00
|
|
|
import { NOSCROLL_CLASS, useModals } from ":/components/Modal/ModalProvider";
|
2024-01-18 14:38:48 +01:00
|
|
|
|
|
|
|
|
export type ModalHandle = {};
|
|
|
|
|
|
2024-04-04 15:01:29 +02:00
|
|
|
export const MODAL_CLASS = "c__modal";
|
|
|
|
|
|
2024-01-18 14:38:48 +01:00
|
|
|
export enum ModalSize {
|
|
|
|
|
SMALL = "small",
|
|
|
|
|
MEDIUM = "medium",
|
|
|
|
|
LARGE = "large",
|
2024-04-03 15:56:07 +02:00
|
|
|
EXTRA_LARGE = "extra-large",
|
2024-01-18 14:38:48 +01:00
|
|
|
FULL = "full",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const useModal = ({
|
|
|
|
|
isOpenDefault,
|
|
|
|
|
}: { isOpenDefault?: boolean } = {}) => {
|
|
|
|
|
const [isOpen, setIsOpen] = React.useState(!!isOpenDefault);
|
|
|
|
|
const onClose = () => {
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const open = () => {
|
|
|
|
|
setIsOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const close = () => {
|
|
|
|
|
onClose();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
isOpen,
|
|
|
|
|
onClose,
|
|
|
|
|
open,
|
|
|
|
|
close,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-14 17:16:03 +01:00
|
|
|
export type ModalProps = PropsWithChildren & {
|
|
|
|
|
size: ModalSize;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
leftActions?: React.ReactNode;
|
|
|
|
|
rightActions?: React.ReactNode;
|
|
|
|
|
actions?: React.ReactNode;
|
|
|
|
|
title?: ReactNode;
|
|
|
|
|
titleIcon?: React.ReactNode;
|
|
|
|
|
hideCloseButton?: boolean;
|
|
|
|
|
closeOnClickOutside?: boolean;
|
|
|
|
|
preventClose?: boolean;
|
|
|
|
|
};
|
2024-01-18 14:38:48 +01:00
|
|
|
|
|
|
|
|
export const Modal = (props: ModalProps) => {
|
2024-02-16 17:18:32 +01:00
|
|
|
/**
|
|
|
|
|
* This is a workaround to prevent the modal from rendering on the first render because if the modal is open on the
|
|
|
|
|
* first render, it will not be able to resolve document.getElementById("c__modals-portal") which is not rendered yet.
|
|
|
|
|
*/
|
|
|
|
|
const [firstRender, setFirstRender] = React.useState(true);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setFirstRender(false);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
if (firstRender) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <ModalInner {...props} />;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const ModalInner = (props: ModalProps) => {
|
2024-04-04 15:01:29 +02:00
|
|
|
const { modalParentSelector } = useModals();
|
2024-01-18 14:38:48 +01:00
|
|
|
|
|
|
|
|
if (!props.isOpen) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2024-04-04 15:01:29 +02:00
|
|
|
<ReactModal
|
|
|
|
|
isOpen={props.isOpen}
|
|
|
|
|
onRequestClose={() => {
|
|
|
|
|
if (!props.preventClose) {
|
|
|
|
|
props.onClose();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
parentSelector={modalParentSelector}
|
|
|
|
|
overlayClassName="c__modal__backdrop"
|
|
|
|
|
className={classNames(MODAL_CLASS, `${MODAL_CLASS}--${props.size}`)}
|
|
|
|
|
shouldCloseOnOverlayClick={!!props.closeOnClickOutside}
|
|
|
|
|
bodyOpenClassName={classNames("c__modals--opened", NOSCROLL_CLASS)}
|
|
|
|
|
>
|
|
|
|
|
{!props.hideCloseButton && !props.preventClose && (
|
|
|
|
|
<div className="c__modal__close">
|
|
|
|
|
<Button
|
|
|
|
|
icon={<span className="material-icons">close</span>}
|
|
|
|
|
color="tertiary-text"
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={() => props.onClose()}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{props.titleIcon && (
|
|
|
|
|
<div className="c__modal__title-icon">{props.titleIcon}</div>
|
2024-01-18 14:38:48 +01:00
|
|
|
)}
|
2024-04-04 15:01:29 +02:00
|
|
|
{props.title && <div className="c__modal__title">{props.title}</div>}
|
|
|
|
|
|
|
|
|
|
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
|
|
|
|
<div className="c__modal__content" tabIndex={0}>
|
|
|
|
|
{props.children}
|
|
|
|
|
</div>
|
|
|
|
|
<ModalFooter {...props} />
|
|
|
|
|
</ReactModal>
|
2024-01-18 14:38:48 +01:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-14 17:16:03 +01:00
|
|
|
const ModalFooter = ({
|
|
|
|
|
leftActions,
|
|
|
|
|
rightActions,
|
|
|
|
|
actions,
|
|
|
|
|
}: Pick<ModalProps, "leftActions" | "rightActions" | "actions">) => {
|
2024-01-18 14:38:48 +01:00
|
|
|
if ((leftActions || rightActions) && actions) {
|
|
|
|
|
throw new Error("Cannot use leftActions or rightActions with actions");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!leftActions && !rightActions && !actions) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={classNames("c__modal__footer", {
|
|
|
|
|
"c__modal__footer--sided": leftActions || rightActions,
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
{actions || (
|
|
|
|
|
<>
|
|
|
|
|
<div className="c__modal__footer__left">{leftActions}</div>
|
|
|
|
|
<div className="c__modal__footer__right">{rightActions}</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|