semantic-ui-react
Version:
The official Semantic-UI-React integration.
322 lines (259 loc) • 8.22 kB
JavaScript
import cx from 'classnames'
import _ from 'lodash'
import PropTypes from 'prop-types'
import React from 'react'
import {
AutoControlledComponent as Component,
customPropTypes,
getElementType,
getUnhandledProps,
isBrowser,
makeDebugger,
META,
useKeyOnly,
} from '../../lib'
import Icon from '../../elements/Icon'
import Portal from '../../addons/Portal'
import ModalHeader from './ModalHeader'
import ModalContent from './ModalContent'
import ModalActions from './ModalActions'
import ModalDescription from './ModalDescription'
const debug = makeDebugger('modal')
/**
* A modal displays content that temporarily blocks interactions with the main view of a site.
* @see Confirm
* @see Portal
*/
class Modal extends Component {
static propTypes = {
/** An element type to render as (string or function). */
as: customPropTypes.as,
/** A modal can reduce its complexity */
basic: PropTypes.bool,
/** Primary content. */
children: PropTypes.node,
/** Additional classes. */
className: PropTypes.string,
/** Icon. */
closeIcon: PropTypes.oneOfType([
PropTypes.node,
PropTypes.object,
PropTypes.bool,
]),
/** Whether or not the Modal should close when the dimmer is clicked. */
closeOnDimmerClick: PropTypes.bool,
/** Whether or not the Modal should close when the document is clicked. */
closeOnDocumentClick: PropTypes.bool,
/** Initial value of open. */
defaultOpen: PropTypes.bool,
/** A modal can appear in a dimmer. */
dimmer: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.oneOf(['inverted', 'blurring']),
]),
/** The node where the modal should mount. Defaults to document.body. */
mountNode: PropTypes.any,
/**
* Called when a close event happens.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onClose: PropTypes.func,
/**
* Called when the portal is mounted on the DOM.
*
* @param {null}
* @param {object} data - All props.
*/
onMount: PropTypes.func,
/**
* Called when an open event happens.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onOpen: PropTypes.func,
/**
* Called when the portal is unmounted from the DOM.
*
* @param {null}
* @param {object} data - All props.
*/
onUnmount: PropTypes.func,
/** Controls whether or not the Modal is displayed. */
open: PropTypes.bool,
/** A modal can vary in size */
size: PropTypes.oneOf(['fullscreen', 'large', 'small']),
/** Custom styles. */
style: PropTypes.object,
/**
* NOTE: Any unhandled props that are defined in Portal are passed-through
* to the wrapping Portal.
*/
}
static defaultProps = {
dimmer: true,
closeOnDimmerClick: true,
closeOnDocumentClick: false,
}
static autoControlledProps = [
'open',
]
static _meta = {
name: 'Modal',
type: META.TYPES.MODULE,
}
static Header = ModalHeader
static Content = ModalContent
static Description = ModalDescription
static Actions = ModalActions
state = {}
componentWillUnmount() {
debug('componentWillUnmount()')
this.handlePortalUnmount()
}
// Do not access document when server side rendering
getMountNode = () => isBrowser ? this.props.mountNode || document.body : null
handleClose = (e) => {
debug('close()')
const { onClose } = this.props
if (onClose) onClose(e, this.props)
this.trySetState({ open: false })
}
handleIconOverrides = predefinedProps => ({
onClick: e => {
_.invoke(predefinedProps, 'onClick', e)
this.handleClose(e)
},
})
handleOpen = (e) => {
debug('open()')
const { onOpen } = this.props
if (onOpen) onOpen(e, this.props)
this.trySetState({ open: true })
}
handlePortalMount = (e) => {
debug('handlePortalMount()')
const { dimmer } = this.props
const mountNode = this.getMountNode()
if (dimmer) {
debug('adding dimmer')
mountNode.classList.add('dimmable', 'dimmed')
if (dimmer === 'blurring') {
debug('adding blurred dimmer')
mountNode.classList.add('blurring')
}
}
this.setPosition()
const { onMount } = this.props
if (onMount) onMount(e, this.props)
}
handlePortalUnmount = (e) => {
debug('handlePortalUnmount()')
// Always remove all dimmer classes.
// If the dimmer value changes while the modal is open, then removing its
// current value could leave cruft classes previously added.
const mountNode = this.getMountNode()
mountNode.classList.remove('blurring', 'dimmable', 'dimmed', 'scrollable')
cancelAnimationFrame(this.animationRequestId)
const { onUnmount } = this.props
if (onUnmount) onUnmount(e, this.props)
}
handleRef = c => (this.ref = c)
setPosition = () => {
if (this.ref) {
const mountNode = this.getMountNode()
const { height } = this.ref.getBoundingClientRect()
const marginTop = -Math.round(height / 2)
const scrolling = height >= window.innerHeight
const newState = {}
if (this.state.marginTop !== marginTop) {
newState.marginTop = marginTop
}
if (this.state.scrolling !== scrolling) {
newState.scrolling = scrolling
if (scrolling) {
mountNode.classList.add('scrolling')
} else {
mountNode.classList.remove('scrolling')
}
}
if (Object.keys(newState).length > 0) this.setState(newState)
}
this.animationRequestId = requestAnimationFrame(this.setPosition)
}
render() {
const { open } = this.state
const {
basic,
children,
className,
closeIcon,
closeOnDimmerClick,
closeOnDocumentClick,
dimmer,
size,
style,
} = this.props
const mountNode = this.getMountNode()
// Short circuit when server side rendering
if (!isBrowser) return null
const { marginTop, scrolling } = this.state
const classes = cx(
'ui',
size,
useKeyOnly(basic, 'basic'),
useKeyOnly(scrolling, 'scrolling'),
'modal transition visible active',
className,
)
const unhandled = getUnhandledProps(Modal, this.props)
const portalPropNames = Portal.handledProps
const rest = _.omit(unhandled, portalPropNames)
const portalProps = _.pick(unhandled, portalPropNames)
const ElementType = getElementType(Modal, this.props)
const closeIconName = closeIcon === true ? 'close' : closeIcon
const modalJSX = (
<ElementType {...rest} className={classes} style={{ marginTop, ...style }} ref={this.handleRef}>
{Icon.create(closeIconName, { overrideProps: this.handleIconOverrides })}
{children}
</ElementType>
)
// wrap dimmer modals
const dimmerClasses = !dimmer
? null
: cx(
'ui',
dimmer === 'inverted' && 'inverted',
'page modals dimmer transition visible active',
)
// Heads up!
//
// The SUI CSS selector to prevent the modal itself from blurring requires an immediate .dimmer child:
// .blurring.dimmed.dimmable>:not(.dimmer) { ... }
//
// The .blurring.dimmed.dimmable is the body, so that all body content inside is blurred.
// We need the immediate child to be the dimmer to :not() blur the modal itself!
// Otherwise, the portal div is also blurred, blurring the modal.
//
// We cannot them wrap the modalJSX in an actual <Dimmer /> instead, we apply the dimmer classes to the <Portal />.
return (
<Portal
closeOnRootNodeClick={closeOnDimmerClick}
closeOnDocumentClick={closeOnDocumentClick}
{...portalProps}
className={dimmerClasses}
mountNode={mountNode}
onClose={this.handleClose}
onMount={this.handlePortalMount}
onOpen={this.handleOpen}
onUnmount={this.handlePortalUnmount}
open={open}
>
{modalJSX}
</Portal>
)
}
}
export default Modal