import React, { useRef } from "react"; import classNames from "classnames"; import FocusLock from "react-focus-lock"; import { RemoveScroll } from "react-remove-scroll"; import { Box, BoxProps } from "../Box"; import { Fixed } from "../Fixed"; import { Flex } from "../Flex"; import { Portal } from "../Portal"; import { Description } from "../Description"; import { CloseButton } from "../CloseButton"; import { Stack, StackProps } from "../Stack"; import { KEY_CODE } from "../../types"; import { useKeyPress, useOutsideClick } from "../../hooks"; import { bem } from "../../utilities/bem"; import { Tab } from "../Tab"; import { Heading } from "../Heading"; const cn = "Modal"; export enum MODAL_SIZE { DEFAULT = "default", LARGE = "large", FULL = "full", } export interface ModalProps extends BoxProps { /** * The title of the modal is placed in the modal header. */ title?: string; /** * The secondary title is the same font size and weight of the title, * but a subdued gray and it's placed on the right side if the modal * header opposite of the title. * * When using the secondary title, `hideCloseButton` should be `true` * and then the modal should be closed another way. The use case is * using the modal to build a set up wizard. */ secondaryTitle?: string; /** * The description is smaller text placed under the title in the modal * header. * * Deprecated; this prop will be removed in the next major release. * Place any supporting description copy within the modal body. */ description?: string; /** * A boolean to control whether or not the modal is open. This is * useful if the modal needs to be open when the page loads. * * Generally, this value is set via the `useDisclosure` hook. */ isOpen?: boolean; /** * A method to close the modal window. Generally, this function is set * via the `useDisclosure` hook. */ onClose?: () => void; /** * The modal component uses react-focus-lock to trap the keyboard focus within * the modal when it's opened. This is useful so that elements under the modal * cannot be focused with the keyboard. This is considered good practice for * usability and accessibility. * * By default, FocusLock gives focus to the first focusable element, which in * many cases is the close button. This isn't always ideal, so the default * value for autoFocus is false. Enable autoFocus when the modal has no other * focusable elements. * * If autoFocus is not set on the modal, then you'll need to manually set * focus to another element. This can be done by putting `autoFocus` on the * element. */ autoFocus?: boolean; /** * Clicking outside of the modal (on the overlay) dismisses it. */ disableOutsideClick?: boolean; /** * Pressing the escape key on the keyboard dismisses the modal. This * is the default behavior, but it can be disabled by setting this * prop to `true`. */ disableEscClose?: boolean; /** * If the close button in the modal header needs to be hidden, it can * be done so by setting this prop to `true`. */ hideCloseButton?: boolean; /** * Size controls the width of the modal. * * `MODAL_SIZE` enum can be one of `DEFAULT` | `LARGE` | `FULL` */ size?: MODAL_SIZE; /** * Whether or not to center the modal vertically within the viewport. */ center?: boolean; } export interface ModalBodyProps extends BoxProps { /** * Adds padding around the modal body content. */ padded?: boolean; } export interface ModalFooterMultiStepProps extends BoxProps { /** * The total number of steps within the multi-step flow; this will be used to * create the correct number of dots in the progress indicator. */ steps: number; /** * The number of the current step within the multi-step flow; this will be * used to mark the dot representing the current step active in the progress * indicator. */ currentStep: number; /** * Text label to describe the left-hand side button, or the previous button. */ leftButtonLabel?: string; /** * Text label to describe the right-hand side button, or the next button. */ rightButtonLabel?: string; } const ModalHeader = (props: BoxProps) => { const { className, ...rest } = props; return ( ); }; export const ModalBody = (props: ModalBodyProps) => { const { padded = true, className, ...rest } = props; return ( ); }; export const ModalTabs = (props: StackProps) => { const { className, ...rest } = props; return ( ); }; export const ModalFooter = (props: BoxProps) => { const { className, ...rest } = props; return ( ); }; export const ModalFooterMultiStep = ({ steps, currentStep, leftButtonLabel, rightButtonLabel, children, className, ...rest }: ModalFooterMultiStepProps) => { return ( {leftButtonLabel} {rightButtonLabel} {children} {[...new Array(steps).keys()].map((dot, index) => ( ))} ); }; /** * Modals are overlays that prevent users from interacting with the rest * of the application until a specific action is taken. * * The modal contains a portal by default, so there is no need to wrap * it in one in the application. * * [Skip to examples](#stories) */ export const Modal = (props: ModalProps) => { const { title, secondaryTitle, description, children, isOpen, onClose, autoFocus = false, disableOutsideClick = true, // @TODO [breaking change] change to enableOutsideClick = false disableEscClose = false, hideCloseButton = false, size = MODAL_SIZE.DEFAULT, center = false, className, ...rest } = props; const modalElement = useRef(null); const showHeader = title || secondaryTitle || !hideCloseButton; useKeyPress(KEY_CODE.ESC, () => { if (!disableEscClose && onClose) { onClose(); } }); useOutsideClick(modalElement, () => { if (!disableOutsideClick && onClose) { onClose(); } }); return isOpen ? ( {showHeader && ( {title && {title}} {secondaryTitle && ( {secondaryTitle} )} {!hideCloseButton && ( )} )} {description && ( {description} )} {children} ) : null; }; Modal.Tabs = ModalTabs; Modal.Body = ModalBody; Modal.Footer = ModalFooter; Modal.FooterMultiStep = ModalFooterMultiStep;