Source

components/Modal.jsx

import React from "react"
import PropTypes from "prop-types"
import "./Modal.css"

/**
 * Used to define the parameters of the form
 * associated with the confirmation button, modify
 * the value of its name attribute and replace its
 * text with another text, an element or a component.
 *
 * @typedef {Object} ConfirmButtonOptions
 *
 * @property {String} name
 * [See the MDN reference of the \<button\> element `name` attribute]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name}
 *
 * @property {Boolean} formNoValidate
 * [See the MDN reference of the \<button\> element `formNoValidate` attribute]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formnovalidate}
 *
 * @property {String} formAction
 * [See the MDN reference of the \<button\> element `formAction` attribute]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formaction}
 *
 * @property {String} formEncType
 * [See the MDN reference of the  \<button\> element `formEncType` attribute]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formenctype}
 *
 * @property {String} formMethod
 * [See the MDN reference of the  \<button\> element `formMethod` attribute]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formmethod}
 *
 * @property {String} formTarget
 * [See the MDN reference of the \<button\> element `formTarget` attribute]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formtarget}
 *
 * @property {String | React.ReactNode} content
 * Allows you to replace the default button text
 * with an element (e.g. an SVG icon), a component
 * or a string.
 *
 */

/**
 * Allows you to modify the style of the different
 * elements of the modal by adding additional CSS
 * classes to them.
 *
 * @typedef {Object} ModalStyleModifier
 *
 * @property {String} backdrop
 *
 * @property {String} contentContainer
 *
 * @property {String} header
 *
 * @property {String} title
 *
 * @property {String} mainContent
 *
 * @property {String} footer
 *
 * @property {String} confirmButton
 *
 * @property {String} cancelButton
 *
 * @property {String} closeButton
 *
 * @property {String} closeButtonIcon
 */

/**
 * @typedef {Object} ModalProps
 *
 * @property {Boolean} isOpen
 *
 * @property {React.ReactNode} children
 * Represents the main content of the modal that
 * will be displayed between the header and the
 * footer.
 *
 * @property {VoidFunction} handleClickOutside
 * Manages the closing of the modal when the user
 * clicks on the latter's backdrop.
 *
 * [See the type definition of this callback in the useModal module.]{@link module:useModal~clickOutsideCallback}
 *
 * @property {VoidFunction} handleClickOnCloseButton
 *
 * @property {Function} [handleConfirm=() => {}]
 *
 * @property {Function} [handleCancel=() => {}]
 *
 * @property {String} [buttonsForm=""]
 * [See the MDN reference of the \<button\> element `form` attribute]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-form}
 *
 * @property {String} [title] The title displayed in
 * the modal's header. If this property is not
 * provided then no header will be rendered.
 *
 * @property {Boolean} [animationEnabled=false]
 *
 * @property {Boolean} [hideTitle=true]
 *
 * @property {Boolean} [hideCancelButton=true]
 *
 * @property {Boolean} [hideConfirmButton=true]
 *
 * @property {ConfirmButtonOptions} [confirmButtonOptions]
 *
 * @property {ModalStyleModifier} [styleModifier]
 *
 * @property {React.ReactNode} [closeButtonIcon]
 *
 * @property {React.ReactNode} [subHeaderContent] The additional
 * content that displays after the title in the header.
 *
 * @property {React.ReactNode} [footerContent]
 *
 * @property {String | React.ReactNode} [cancelButtonContent]
 * Allows you to replace the default cancel button
 * text with an element (e.g. an SVG icon), a
 * component or a string.
 */

/**
 * Provides the structure of a modal with:
 *
 *  - A header with the title of the latter,
 *  - An empty body which can be filled with the `children` prop,
 *  - A footer with a confirmation button and a button
 *    cancellation belonging to the form whose `id` corresponds
 *    to the value of the `buttonsForm` prop.
 *
 * @function
 *
 * @param {ModalProps} ModalProps
 *
 * @returns {JSX.Element} The modal component
 *
 * @example
 *
 * const SignupModal = ({
 *  isOpen,
 *  handleClickOutside,
 *  handleClickOnCloseButton,
 * }) => {
 *  return (
 *    <Modal
 *      isOpen={isOpen}
 *      handleClickOutside={handleClickOutside}
 *      handleClickOnCloseButton={
 *        handleClickOnCloseButton
 *      }
 *      buttonsForm="signup-form"
 *      title="Inscription"
 *      subHeaderContent={
 *        <p className="signup-modal__header__subtitle">
 *            Création d'un compte utilisateur
 *        </p>
 *      }
 *      hideCancelButton
 *      confirmButtonOptions={{
 *        name: "signup-form-submit-button",
 *        content: "S'inscrire",
 *      }}
 *      styleModifier={{
 *        backdrop: "signup-modal",
 *        header: "signup-modal__header",
 *        confirmButton: "signup-modal__footer__confirm-button",
 *      }}
 *    >
 *      <form
 *        id="signup-form"
 *        action="#"
 *        className="signup-form"
 *      >
 *        <fieldset>
 *          <legend className="signup-form__fieldset__legend">
 *            Identity
 *          </legend>
 *
 *          <div className="signup-form__fieldset__field">
 *            <label htmlFor="first-name">
 *              First name:
 *            </label>
 *
 *            <input
 *              type="text"
 *              name="first-name"
 *              id="first-name"
 *            />
 *          </div>
 *
 *          <div className="signup-form__fieldset__field">
 *            <label htmlFor="last-name">
 *              Last name:
 *            </label>
 *
 *            <input
 *              type="text"
 *              name="last-name"
 *              id="last-name"
 *            />
 *          </div>
 *        </fieldset>
 *
 *        <fieldset>
 *          <legend className="signup-form__fieldset__legend">
 *            Credentials
 *          </legend>
 *
 *          <div className="signup-form__fieldset__field">
 *            <label htmlFor="email">Email:</label>
 *
 *            <input
 *              type="text"
 *              name="email"
 *              id="email"
 *            />
 *          </div>
 *
 *          <div className="signup-form__fieldset__field">
 *            <label htmlFor="password">
 *              Password:
 *            </label>
 *
 *            <input
 *              type="text"
 *              name="password"
 *              id="password"
 *            />
 *          </div>
 *        </fieldset>
 *      </form>
 *    </Modal>
 *  )
 * }
 */
const Modal = ({
  isOpen,
  children,
  handleClickOutside,
  handleClickOnCloseButton,
  handleConfirm,
  handleCancel,
  buttonsForm,
  title,
  animationEnabled,
  hideCancelButton,
  hideConfirmButton,
  confirmButtonOptions,
  styleModifier,
  closeButtonIcon,
  subHeaderContent,
  footerContent,
  cancelButtonContent,
}) => {
  return (
    <div
      id="react-modal-component-root"
      className={`react-modal-component${
        isOpen ? "" : " react-modal-component--close"
      }`}
      onClick={handleClickOutside}
    >
      <div
        id="react-modal-component-backdrop"
        className={`react-modal-component-backdrop${
          styleModifier?.backdrop ? ` ${styleModifier?.backdrop}` : ""
        }${
          animationEnabled
            ? " react-modal-component__backdrop--animation-enabled"
            : ""
        }`}
      >
        {isOpen && (
          <div
            className={`react-modal-component__content${
              styleModifier?.contentContainer
                ? ` ${styleModifier.contentContainer}`
                : ""
            }`}
          >
            <button
              type="reset"
              autoFocus
              form={buttonsForm}
              onClick={handleClickOnCloseButton}
              className={`react-modal-component__button react-modal-component__content__close-button${
                styleModifier?.closeButton
                  ? ` ${styleModifier?.closeButton}`
                  : ""
              }${
                animationEnabled
                  ? " react-modal-component__button--transition-enabled"
                  : ""
              }`}
            >
              {closeButtonIcon ? (
                closeButtonIcon
              ) : (
                <svg
                  width={32}
                  height={32}
                  viewBox="0 0 320 512"
                  className={`react-modal-component__content__close-button__icon${
                    styleModifier?.closeButtonIcon
                      ? ` ${styleModifier?.closeButtonIcon}`
                      : ""
                  }`}
                >
                  {/* <!--! Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> */}
                  <path
                    d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"
                    fill="currentColor"
                  />
                </svg>
              )}
            </button>

            {title && (
              <header
                className={`react-modal-component__content__header${
                  styleModifier?.header
                    ? ` ${styleModifier?.header}`
                    : ""
                }`}
              >
                <h2
                  className={`react-modal-component__content__header__title${
                    styleModifier && styleModifier.title
                      ? ` ${styleModifier.title}`
                      : ""
                  }`}
                >
                  {title}
                </h2>

                {subHeaderContent && subHeaderContent}
              </header>
            )}

            <main
              className={`react-modal-component__content__main${
                styleModifier?.mainContent
                  ? ` ${styleModifier?.mainContent}`
                  : ""
              }`}
            >
              {children}
            </main>

            <footer
              className={`react-modal-component__content__footer${
                styleModifier?.footer
                  ? ` ${styleModifier?.footer}`
                  : ""
              }`}
            >
              {!hideConfirmButton && (
                <button
                  type="submit"
                  name={
                    confirmButtonOptions?.name
                      ? confirmButtonOptions?.name
                      : "react-modal-component-confirm-button"
                  }
                  onClick={handleConfirm}
                  form={buttonsForm}
                  formNoValidate={
                    confirmButtonOptions?.formNoValidate
                  }
                  formAction={confirmButtonOptions?.formAction}
                  formEncType={confirmButtonOptions?.formEncType}
                  formMethod={confirmButtonOptions?.formMethod}
                  formTarget={confirmButtonOptions?.formTarget}
                  className={`react-modal-component__button${
                    styleModifier?.confirmButton
                      ? ` ${styleModifier.confirmButton}`
                      : ""
                  }`}
                >
                  {confirmButtonOptions?.content
                    ? confirmButtonOptions?.content
                    : "Confirmer"}
                </button>
              )}

              {!hideCancelButton && (
                <button
                  type="reset"
                  onClick={handleCancel}
                  form={buttonsForm}
                  className={`react-modal-component__button${
                    styleModifier?.cancelButton
                      ? ` ${styleModifier?.cancelButton}`
                      : ""
                  }`}
                >
                  {cancelButtonContent
                    ? cancelButtonContent
                    : "Annuler"}
                </button>
              )}

              {footerContent && footerContent}
            </footer>
          </div>
        )}
      </div>
    </div>
  )
}

Modal.propTypes = {
  isOpen: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  handleClickOutside: PropTypes.func.isRequired,
  handleClickOnCloseButton: PropTypes.func.isRequired,
  handleCancel: PropTypes.func.isRequired,
  handleConfirm: PropTypes.func.isRequired,
  buttonsForm: PropTypes.string.isRequired,
  animationEnabled: PropTypes.bool.isRequired,
  title: PropTypes.string,
  hideTitle: PropTypes.bool,
  hideCancelButton: PropTypes.bool,
  hideConfirmButton: PropTypes.bool,

  confirmButtonOptions: PropTypes.exact({
    name: PropTypes.string,
    formNoValidate: PropTypes.bool,
    formAction: PropTypes.string,
    formEncType: PropTypes.string,
    formMethod: PropTypes.string,
    formTarget: PropTypes.string,
    content: PropTypes.node,
  }),

  styleModifier: PropTypes.exact({
    backdrop: PropTypes.string,
    header: PropTypes.string,
    contentContainer: PropTypes.string,
    title: PropTypes.string,
    mainContent: PropTypes.string,
    footer: PropTypes.string,
    cancelButton: PropTypes.string,
    confirmButton: PropTypes.string,
    closeButton: PropTypes.string,
    closeButtonIcon: PropTypes.string,
  }),

  closeButtonIcon: PropTypes.node,
  subHeaderContent: PropTypes.node,
  footerContent: PropTypes.node,
  cancelButtonContent: PropTypes.node,
}

Modal.defaultProps = {
  handleCancel: () => {},
  handleConfirm: () => {},
  buttonsForm: "",
  animationEnabled: false,
  hideTitle: false,
  hideCancelButton: false,
  hideConfirmButton: false,
  styleModifier: {},
}

export { Modal }