Skip to content

Instantly share code, notes, and snippets.

@rmdort
Created April 17, 2018 14:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rmdort/6e0784ff5c3fb81612485ceed11895a5 to your computer and use it in GitHub Desktop.
Save rmdort/6e0784ff5c3fb81612485ceed11895a5 to your computer and use it in GitHub Desktop.
Simple react modal using portals
import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import cx from 'classnames'
const TAB_KEY = 9
const ESC_KEY = 27
const FOCUSABLE_ELEMENTS = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA', 'A']
class ModalPortal extends React.Component {
constructor (props) {
super(props)
this.shouldClose = null
}
handleContentOnClick = () => {
this.shouldClose = false
}
handleContentOnMouseDown = () => {
this.shouldClose = false
}
handleContentOnMouseUp = () => {
this.shouldClose = false
}
handleOverlayOnClick = (event) => {
if (this.shouldClose === null) {
this.shouldClose = true
}
if (this.shouldClose) {
this.requestClose(event)
}
this.shouldClose = null
}
componentDidUpdate (prevProps, prevState) {
if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen) {
this.focusContent()
}
}
focusContent = () => {
this.content && this.content.focus()
}
requestClose = (event) => {
this.props.onRequestClose && this.props.onRequestClose(event)
}
setContentRef = (ref) => {
this.content = ref
}
handleKeyDown = (event) => {
if (document.activeElement && FOCUSABLE_ELEMENTS.indexOf(document.activeElement.nodeName) !== -1) return
if (event.keyCode === ESC_KEY) {
event.stopPropagation()
this.requestClose(event)
}
}
render () {
const { isOpen, inline } = this.props
if (!isOpen) return null
const contentClass = cx('ola-modal-content', this.props.contentClassName)
const overlayClass = cx('ola-modal-overlay', {
'ola-modal-inline': inline
})
return (
<div
className={overlayClass}
aria-modal='true'
onClick={this.handleOverlayOnClick}
>
<div
className={contentClass}
onClick={this.handleContentOnClick}
ref={this.setContentRef}
onKeyDown={this.handleKeyDown}
onMouseDown={this.handleContentOnMouseDown}
onMouseUp={this.handleContentOnMouseUp}
tabIndex='-1'
>
<button className='ola-modal-close' onClick={this.requestClose}>
Close
</button>
{inline ? (
this.props.children
) : (
<div className='ola-modal-body'>{this.props.children}</div>
)}
</div>
</div>
)
}
}
class Portal extends React.Component {
constructor (props) {
super(props)
this.el = document.createElement('div')
this.el.className = props.className
}
static defaultProps = {
isOpen: false,
className: 'ola-modal-portal',
inline: false,
contentClassName: 'ola-modal-content-small'
}
componentDidMount () {
document.body.appendChild(this.el)
}
componentWillUnmount () {
document.body.removeChild(this.el)
}
render () {
return ReactDOM.createPortal(<ModalPortal {...this.props} />, this.el)
}
}
Portal.propTypes = {
children: PropTypes.node.isRequired,
onRequestClose: PropTypes.func,
contentClassName: PropTypes.string
}
export default Portal
@rmdort
Copy link
Author

rmdort commented Apr 17, 2018

Usage

import Portal from './Portal'
class App extends React.Component {
  constructor (props) {
    super (props)
    this.state = {
      isOpen: false
    }
  }
  render () {
    return (
      <div>
        <button onClick={() => this.setState({ isOpen: true })}>Open modal</button>
        <Portal
          isOpen={this.state.isOpen}
          onRequestClose={() => this.setState({ isOpen: false })}
        >Content</Portal
      </div>
    )
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment