Files
cunningham/packages/react/src/components/Modal/index.tsx

154 lines
3.9 KiB
TypeScript
Raw Normal View History

import React, { PropsWithChildren, ReactNode, useEffect } from "react";
import classNames from "classnames";
import ReactModal from "react-modal";
import { Button } from ":/components/Button";
import { NOSCROLL_CLASS, useModals } from ":/components/Modal/ModalProvider";
export type ModalHandle = {};
export const MODAL_CLASS = "c__modal";
export enum ModalSize {
SMALL = "small",
MEDIUM = "medium",
LARGE = "large",
EXTRA_LARGE = "extra-large",
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,
};
};
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;
closeOnEsc?: boolean;
preventClose?: boolean;
"aria-label"?: string;
};
export const Modal = (props: ModalProps) => {
/**
* 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(<MODAL_PARENT_ID>) which is not rendered yet.
*/
const [firstRender, setFirstRender] = React.useState(true);
useEffect(() => {
setFirstRender(false);
}, []);
if (firstRender) {
return null;
}
return <ModalInner {...props} />;
};
export const ModalInner = ({ closeOnEsc = true, ...props }: ModalProps) => {
const { modalParentSelector } = useModals();
if (!props.isOpen) {
return null;
}
return (
<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}
shouldCloseOnEsc={closeOnEsc}
bodyOpenClassName={classNames("c__modals--opened", NOSCROLL_CLASS)}
contentLabel={props["aria-label"] || props.title?.toString()}
>
<div className="c__modal__scroller">
{!props.hideCloseButton && !props.preventClose && (
<div className="c__modal__close">
<Button
icon={<span className="material-icons">close</span>}
variant="tertiary"
color="neutral"
size="small"
onClick={props.onClose}
/>
</div>
)}
{props.titleIcon && (
<div className="c__modal__title-icon">{props.titleIcon}</div>
)}
{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} />
</div>
</ReactModal>
);
};
const ModalFooter = ({
leftActions,
rightActions,
actions,
}: Pick<ModalProps, "leftActions" | "rightActions" | "actions">) => {
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>
);
};