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;