import React, {
forwardRef,
HTMLAttributes,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { Transition } from 'react-transition-group'
import { CBackdrop } from '../backdrop'
import { CConditionalPortal } from '../conditional-portal'
import { CFocusTrap } from '../focus-trap'
import { CModalContent } from './CModalContent'
import { CModalContext } from './CModalContext'
import { CModalDialog } from './CModalDialog'
import { useForkedRef } from '../../hooks'
export interface CModalProps extends HTMLAttributes {
/**
* Align the modal in the center or top of the screen.
*/
alignment?: 'top' | 'center'
/**
* Apply a backdrop on body while modal is open.
*/
backdrop?: boolean | 'static'
/**
* A string of all className you want applied to the base component.
*/
className?: string
/**
* Appends the react modal to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`.
*
* @since 5.3.0
*/
container?: DocumentFragment | Element | (() => DocumentFragment | Element | null) | null
/**
* @ignore
*/
duration?: number
/**
* Puts the focus on the modal when shown.
*
* @since 4.10.0
*/
focus?: boolean
/**
* Set modal to covers the entire user viewport.
*/
fullscreen?: boolean | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
/**
* Closes the modal when escape key is pressed.
*/
keyboard?: boolean
/**
* Callback fired when the component requests to be closed.
*/
onClose?: () => void
/**
* Callback fired when the component requests to be closed.
*/
onClosePrevented?: () => void
/**
* Callback fired when the modal is shown, its backdrop is static and a click outside the modal or an escape key press is performed with the keyboard option set to false.
*/
onShow?: () => void
/**
* Generates modal using createPortal.
*/
portal?: boolean
/**
* Create a scrollable modal that allows scrolling the modal body.
*/
scrollable?: boolean
/**
* Size the component small, large, or extra large.
*/
size?: 'sm' | 'lg' | 'xl'
/**
* Remove animation to create modal that simply appear rather than fade in to view.
*/
transition?: boolean
/**
* By default the component is unmounted after close animation, if you want to keep the component mounted set this property to false.
*/
unmountOnClose?: boolean
/**
* Toggle the visibility of modal component.
*/
visible?: boolean
}
export const CModal = forwardRef(
(
{
children,
alignment,
backdrop = true,
className,
container,
duration = 150,
focus = true,
fullscreen,
keyboard = true,
onClose,
onClosePrevented,
onShow,
portal = true,
scrollable,
size,
transition = true,
unmountOnClose = true,
visible,
...rest
},
ref
) => {
const modalRef = useRef(null)
const forkedRef = useForkedRef(ref, modalRef)
const [_visible, setVisible] = useState(visible)
const [staticBackdrop, setStaticBackdrop] = useState(false)
const contextValues = {
visible: _visible,
setVisible,
}
useEffect(() => {
setVisible(visible)
}, [visible])
useEffect(() => {
if (_visible) {
document.addEventListener('mouseup', handleClickOutside)
document.addEventListener('keydown', handleKeyDown)
}
return () => {
document.removeEventListener('mouseup', handleClickOutside)
document.removeEventListener('keydown', handleKeyDown)
}
}, [_visible])
const handleDismiss = () => {
if (backdrop === 'static') {
return setStaticBackdrop(true)
}
setVisible(false)
}
useLayoutEffect(() => {
onClosePrevented?.()
setTimeout(() => setStaticBackdrop(false), duration)
}, [staticBackdrop])
// Set focus to modal after open
useLayoutEffect(() => {
if (_visible) {
document.body.classList.add('modal-open')
if (backdrop) {
document.body.style.overflow = 'hidden'
document.body.style.paddingRight = '0px'
}
} else {
document.body.classList.remove('modal-open')
if (backdrop) {
document.body.style.removeProperty('overflow')
document.body.style.removeProperty('padding-right')
}
}
return () => {
document.body.classList.remove('modal-open')
if (backdrop) {
document.body.style.removeProperty('overflow')
document.body.style.removeProperty('padding-right')
}
}
}, [_visible])
const handleClickOutside = (event: Event) => {
if (modalRef.current && modalRef.current == event.target) {
handleDismiss()
}
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && keyboard) {
handleDismiss()
}
}
return (
<>
{(state) => (
{children}
)}
{backdrop && (
)}
>
)
}
)
CModal.propTypes = {
alignment: PropTypes.oneOf(['top', 'center']),
backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf<'static'>(['static'])]),
children: PropTypes.node,
className: PropTypes.string,
container: PropTypes.any, // HTMLElement
duration: PropTypes.number,
focus: PropTypes.bool,
fullscreen: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.oneOf<'sm' | 'md' | 'lg' | 'xl' | 'xxl'>(['sm', 'md', 'lg', 'xl', 'xxl']),
]),
keyboard: PropTypes.bool,
onClose: PropTypes.func,
onClosePrevented: PropTypes.func,
onShow: PropTypes.func,
portal: PropTypes.bool,
scrollable: PropTypes.bool,
size: PropTypes.oneOf(['sm', 'lg', 'xl']),
transition: PropTypes.bool,
unmountOnClose: PropTypes.bool,
visible: PropTypes.bool,
}
CModal.displayName = 'CModal'