import { useContext, forwardRef, memo } from 'react' import * as React from 'react' import { Overlay } from '@react-aria/overlays' import type { AriaDialogProps } from '@react-types/dialog' import { animated, useTransition, easings } from '@react-spring/web' import Button, { ButtonProps } from '../Button' import IconButton, { IconButtonProps } from '../IconButton' import { useObjectRef } from '@react-aria/utils' import { Dialog } from './Dialog' import { ModalBackgroundContext } from './ModalBackgroundContext' import { CharcoalModalOverlayProps, useCharcoalModalOverlay, useWindowWidth, } from './useCustomModalOverlay' import './index.css' export type BottomSheet = boolean | 'full' export type Size = 'S' | 'M' | 'L' export type ModalProps = CharcoalModalOverlayProps & AriaDialogProps & { children: React.ReactNode zIndex?: number title: string size?: Size bottomSheet?: BottomSheet isOpen: boolean onClose: () => void className?: string closeButtonAriaLabel?: string /** * https://github.com/adobe/react-spectrum/issues/3787 * Next.jsで使用する際に発生するエラーの一時的な回避策でdocument.bodyを指定する必要がある */ portalContainer?: HTMLElement } const DEFAULT_Z_INDEX = 10 /** * モーダルコンポーネント。 * * @example アプリケーションルートで `` ないし `` で囲った上で利用する * ```tsx * import { * OverlayProvider, * Modal, * ModalHeader, * ModalBody, * ModalButtons * } from '@charcoal-ui/react' * * * * state.close()} isDismissable> * * * ... * ... * * * * * ``` */ const Modal = forwardRef(function ModalInner( { children, zIndex = DEFAULT_Z_INDEX, portalContainer, ...props }, external, ) { const { title, size = 'M', bottomSheet = false, isDismissable, onClose, className, isOpen = false, closeButtonAriaLabel = 'Close', } = props const ref = useObjectRef(external) const { modalProps, underlayProps } = useCharcoalModalOverlay( { ...props, isKeyboardDismissDisabled: isDismissable === undefined || isDismissable === false, }, { onClose, isOpen, }, ref, ) const isMobile = (useWindowWidth() ?? Infinity) < 744 const transitionEnabled = isMobile && bottomSheet !== false const showDismiss = !isMobile || bottomSheet !== true const transition = useTransition(isOpen, { from: { transform: 'translateY(100%)', backgroundColor: 'rgba(0, 0, 0, 0)', overflow: 'hidden', }, enter: { transform: 'translateY(0%)', backgroundColor: 'rgba(0, 0, 0, 0.4)', }, update: { overflow: 'auto', }, leave: { transform: 'translateY(100%)', backgroundColor: 'rgba(0, 0, 0, 0)', overflow: 'hidden', }, config: transitionEnabled ? { duration: 400, easing: easings.easeOutQuart } : { duration: 0 }, }) const bgRef = React.useRef(null) const handleClick = React.useCallback( (e: React.MouseEvent) => { if (e.currentTarget === e.target) { onClose() } }, [onClose], ) return transition( ({ backgroundColor, overflow, transform }, item) => item && ( {/* https://github.com/pmndrs/react-spring/issues/1515 */} {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} {/* @ts-ignore */} {/* https://github.com/pmndrs/react-spring/issues/1515 */} {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} {/* @ts-ignore */} {children} {isDismissable === true && ( )} ), ) }) const AnimatedDialog = animated(Dialog) export default memo(Modal) export const ModalContext = React.createContext<{ /** * @deprecated */ titleProps: React.HTMLAttributes title: string close?: () => void showDismiss: boolean bottomSheet: BottomSheet }>({ titleProps: {}, title: '', close: undefined, showDismiss: true, bottomSheet: false, }) export function ModalCloseButton(props: Omit) { return ( ) } export function ModalDismissButton({ children, ...props }: ButtonProps) { const { close, showDismiss } = useContext(ModalContext) if (!showDismiss) { return null } return ( ) }