'use client' import { forwardRef, useCallback, useMemo, useState } from 'react' import * as React from 'react' import { CancelIcon } from '@channel.io/bezier-icons' import classNames from 'classnames' import useMergeRefs from '~/src/hooks/useMergeRefs' import { getZIndexClassName } from '~/src/types/props-helpers' import { createContext } from '~/src/utils/react' import { cssDimension } from '~/src/utils/style' import { isNil, isNumber } from '~/src/utils/type' import { AlphaDialogPrimitive, AlphaDialogPrimitiveClose, AlphaDialogPrimitiveContent, AlphaDialogPrimitiveDescription, AlphaDialogPrimitiveOverlay, AlphaDialogPrimitivePortal, type AlphaDialogPrimitiveProps, AlphaDialogPrimitiveTitle, AlphaDialogPrimitiveTrigger, } from '~/src/components/AlphaDialogPrimitive' import { Button } from '~/src/components/Button' import { Text } from '~/src/components/Text' import { ThemeProvider, useThemeName } from '~/src/components/ThemeProvider' import { VisuallyHidden } from '~/src/components/VisuallyHidden' import { useRootElement } from '~/src/components/WindowProvider' import { type ModalBodyProps, type ModalCloseProps, type ModalContentProps, type ModalContentPropsContextValue, type ModalFooterProps, type ModalHeaderProps, type ModalProps, type ModalTitleSize, type ModalTriggerProps, } from './Modal.types' import styles from './Modal.module.scss' const [ModalContainerContextProvider, useModalContainerContext] = createContext< HTMLElement | undefined >(undefined) export { useModalContainerContext } const [ModalContentPropsContextProvider, useModalContentPropsContext] = createContext({ showCloseIcon: false, }) /** * `Modal` is a dialog that appears on top of the page. * * `Modal` is a context of the Modal-related components. It doesn't render any DOM node. * It controls the visibility of the entire component and provides * handlers and accessibility properties to Modal-related components. * @example * * ```tsx * // Anatomy of the Modal * * * * * * * * * ``` */ export function Modal({ children, show, defaultShow, onShow, onHide, }: ModalProps) { const onOpenChange = useCallback< NonNullable >( (open) => { const callback = open ? onShow : onHide callback?.() }, [onShow, onHide] ) return ( {children} ) } /** * `ModalContent` is a container of the modal content. * It creates a portal to render the modal content outside of the DOM tree * and renders overlay behind the modal content too. */ export const ModalContent = forwardRef( function ModalContent( { children, style, className, container: givenContainer, showCloseIcon = false, preventHideOnOutsideClick = false, width = 'max-content', height = 'fit-content', zIndex = 'modal', collisionPadding = { top: 40, bottom: 40 }, ...rest }, forwardedRef ) { const rootElement = useRootElement() const container = givenContainer ?? rootElement const [contentContainer, setContentContainer] = useState() const contentRef = useMergeRefs( forwardedRef, useCallback((node: HTMLElement | null) => { setContentContainer(node ?? undefined) }, []) ) const overlayStyle = (() => { const padding = (() => { if (isNumber(collisionPadding)) { return `${collisionPadding}px` } const { top, right, bottom, left } = { top: 0, right: 0, bottom: 0, left: 0, ...collisionPadding, } return `${top}px ${right}px ${bottom}px ${left}px` })() return { '--b-modal-collision-padding': padding, } as React.CSSProperties })() const propsContextValue = useMemo( (): ModalContentPropsContextValue => ({ showCloseIcon, }), [showCloseIcon] ) return ( { if (preventHideOnOutsideClick) { e.preventDefault() } }} onInteractOutside={(e) => { if (preventHideOnOutsideClick) { e.preventDefault() } }} >
{children} {/* NOTE: To prevent focusing first on the close button when opening the modal, place the close button behind. */} {showCloseIcon && ( // eslint-disable-next-line @typescript-eslint/no-use-before-define
) } ) function getTitleTypo(size: ModalTitleSize) { return ( { l: '24', m: '16', } as const )[size] } function ModalHeaderTitle({ children, size, subtitle, }: React.PropsWithChildren< Pick & { size: NonNullable } >) { const Title = ( {children} ) return ( {!isNil(subtitle) ? (
{Title} {subtitle}
) : ( Title )}
) } /** * `ModalHeader` is a header of the modal content. * It renders the accessible title and description of the modal. * If you want to hide the title and description, use `hidden` prop. */ export const ModalHeader = forwardRef( function ModalHeader( { className, title, subtitle, description, titleSize = 'l', hidden = false, ...rest }, forwardedRef ) { const { showCloseIcon } = useModalContentPropsContext() const hasTitleArea = title || showCloseIcon const Hidden = hidden ? VisuallyHidden : React.Fragment return ( ) } ) /** * `ModalBody` is a simple wrapper of the main modal content. */ export const ModalBody = forwardRef(function ModalBody( { children, className, ...rest }: ModalBodyProps, forwardedRef: React.Ref ) { return (
{children}
) }) /** * `ModalFooter` is a simple wrapper of the footer of the modal content. * Usually, it contains the action buttons of the modal. */ export const ModalFooter = forwardRef( function ModalFooter( { className, leftContent, rightContent, ...rest }, forwardedRef ) { return (
{leftContent && (
{leftContent}
)} {rightContent && (
{rightContent}
)}
) } ) /** * `ModalTrigger` is a button that opens the modal. **It doesn't render any DOM node.** * It passes the handler that opens the modal and accessibility properties to the children. * * It **must** be placed outside of the `ModalContent`. */ export function ModalTrigger({ children }: ModalTriggerProps) { return ( {children} ) } /** * `ModalClose` is a button that closes the modal. **It doesn't render any DOM node.** * It passes the handler that closes the modal to the children. */ export function ModalClose({ children }: ModalCloseProps) { return ( {children} ) }