You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

223 lines
5.2 KiB

// @flow
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable react/no-multi-comp */
import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
import { connect } from 'react-redux'
import Mortal from 'react-mortal'
import styled from 'styled-components'
import noop from 'lodash/noop'
import { rgba } from 'styles/helpers'
import { radii } from 'styles/theme'
import { closeModal, isModalOpened, getModalData } from 'reducers/modals'
import Box, { Tabbable } from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import Defer from 'components/base/Defer'
export { default as ModalBody } from './ModalBody'
const springConfig = {
stiffness: 320,
}
const mapStateToProps: Function = (
state,
{ name, isOpened, onBeforeOpen }: { name: string, isOpened?: boolean, onBeforeOpen: Function },
): * => {
const data = getModalData(state, name)
const modalOpened = isOpened || (name && isModalOpened(state, name))
if (onBeforeOpen) {
onBeforeOpen({ data, isOpened: modalOpened })
}
return {
isOpened: modalOpened,
data,
}
}
const mapDispatchToProps: Function = (dispatch, { name, onClose = noop }): * => ({
onClose: name
? () => {
dispatch(closeModal(name))
onClose()
}
: onClose,
})
const Container = styled(Box).attrs({
color: 'grey',
alignItems: 'center',
justifyContent: 'flex-start',
sticky: true,
style: p => ({
pointerEvents: p.isVisible ? 'auto' : 'none',
}),
})`
position: fixed;
z-index: 20;
`
const Backdrop = styled(Box).attrs({
bg: p => rgba(p.theme.colors.black, 0.4),
sticky: true,
style: p => ({
opacity: p.op,
}),
})`
position: fixed;
`
const Wrapper = styled(Tabbable).attrs({
bg: 'transparent',
flow: 4,
mt: 100,
mb: 100,
style: p => ({
opacity: p.op,
transform: `scale3d(${p.scale}, ${p.scale}, ${p.scale})`,
}),
})`
outline: none;
width: 500px;
z-index: 2;
`
class Pure extends Component<any> {
shouldComponentUpdate(nextProps) {
if (nextProps.isAnimated) {
return false
}
return true
}
render() {
const { data, onClose, render } = this.props
return <Defer>{render({ data, onClose })}</Defer>
}
}
type Props = {
data?: any,
isOpened?: boolean,
onClose: Function,
onHide?: Function,
preventBackdropClick?: boolean,
render: Function,
}
export class Modal extends Component<Props> {
static defaultProps = {
data: undefined,
isOpened: false,
onClose: noop,
onHide: noop,
preventBackdropClick: false,
}
shouldComponentUpdate(nextProps: Props) {
if (this.props.isOpened || nextProps.isOpened) {
return true
}
return false
}
componentDidUpdate(prevProps: Props) {
const didOpened = this.props.isOpened && !prevProps.isOpened
const didClose = !this.props.isOpened && prevProps.isOpened
if (didOpened) {
// Store a reference to the last active element, to restore it after
// modal close
this._lastFocusedElement = document.activeElement
// Forced to use findDOMNode here, because innerRef is giving a proxied component
const domWrapper = findDOMNode(this._wrapper) // eslint-disable-line react/no-find-dom-node
if (domWrapper instanceof HTMLDivElement) {
domWrapper.focus()
}
}
if (didClose) {
if (this._lastFocusedElement) {
this._lastFocusedElement.focus()
}
}
}
_wrapper = null
_lastFocusedElement = null
render() {
const { preventBackdropClick, isOpened, onHide, render, data, onClose } = this.props
return (
<Mortal
isOpened={isOpened}
onClose={onClose}
onHide={onHide}
motionStyle={(spring, isVisible) => ({
opacity: spring(isVisible ? 1 : 0, springConfig),
scale: spring(isVisible ? 1 : 0.95, springConfig),
})}
>
{(m, isVisible, isAnimated) => (
<Container isVisible={isVisible} onClick={preventBackdropClick ? undefined : onClose}>
<Backdrop op={m.opacity} />
<GrowScroll
alignItems="center"
full
justifyContent="flex-start"
style={{ height: '100%' }}
>
<Wrapper
op={m.opacity}
scale={m.scale}
innerRef={n => (this._wrapper = n)}
onClick={e => e.stopPropagation()}
>
<Pure isAnimated={isAnimated} render={render} data={data} onClose={onClose} />
</Wrapper>
</GrowScroll>
</Container>
)}
</Mortal>
)
}
}
export const ModalTitle = styled(Box).attrs({
alignItems: 'center',
color: 'dark',
ff: 'Museo Sans|Regular',
fontSize: 6,
justifyContent: 'center',
p: 5,
relative: true,
})``
export const ModalFooter = styled(Box).attrs({
px: 5,
py: 3,
})`
border-top: 2px solid ${p => p.theme.colors.lightGrey};
border-bottom-left-radius: ${radii[1]}px;
border-bottom-right-radius: ${radii[1]}px;
`
export const ModalContent = styled(Box).attrs({
px: 5,
pb: 5,
})``
export default connect(mapStateToProps, mapDispatchToProps)(Modal)