Gaëtan Renaudeau
6 years ago
committed by
GitHub
41 changed files with 1121 additions and 924 deletions
@ -1,95 +1,102 @@ |
|||
// @flow
|
|||
|
|||
import React, { PureComponent } from 'react' |
|||
import styled, { keyframes } from 'styled-components' |
|||
import React, { PureComponent, Fragment } from 'react' |
|||
import Animated from 'animated/lib/targets/react-dom' |
|||
import { findDOMNode } from 'react-dom' |
|||
|
|||
import Box from 'components/base/Box' |
|||
import IconCross from 'icons/Cross' |
|||
import ModalContent from './ModalContent' |
|||
import ModalHeader from './ModalHeader' |
|||
import ModalFooter from './ModalFooter' |
|||
|
|||
export const Container = styled(Box).attrs({ |
|||
px: 5, |
|||
pb: 5, |
|||
})`` |
|||
import type { RenderProps } from './index' |
|||
|
|||
type Props = { |
|||
deferHeight?: number, |
|||
onClose?: Function, |
|||
children: any, |
|||
title: string, |
|||
onBack?: void => void, |
|||
onClose?: void => void, |
|||
render?: (?RenderProps) => any, |
|||
renderFooter?: (?RenderProps) => any, |
|||
renderProps?: RenderProps, |
|||
noScroll?: boolean, |
|||
refocusWhenChange?: any, |
|||
} |
|||
|
|||
type State = { |
|||
isHidden: boolean, |
|||
animGradient: Animated.Value, |
|||
} |
|||
|
|||
class ModalBody extends PureComponent<Props, State> { |
|||
static defaultProps = { |
|||
onClose: undefined, |
|||
state = { |
|||
animGradient: new Animated.Value(0), |
|||
} |
|||
|
|||
state = { |
|||
isHidden: true, |
|||
componentDidUpdate(prevProps: Props) { |
|||
const shouldFocus = prevProps.refocusWhenChange !== this.props.refocusWhenChange |
|||
if (shouldFocus) { |
|||
if (this._content) { |
|||
const node = findDOMNode(this._content) // eslint-disable-line react/no-find-dom-node
|
|||
if (node) { |
|||
// $FlowFixMe
|
|||
node.focus() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
componentDidMount() { |
|||
setTimeout(() => { |
|||
window.requestAnimationFrame(() => { |
|||
this.setState({ isHidden: false }) |
|||
}) |
|||
}, 150) |
|||
_content = null |
|||
|
|||
animateGradient = (isScrollable: boolean) => { |
|||
const anim = { |
|||
duration: 150, |
|||
toValue: isScrollable ? 1 : 0, |
|||
} |
|||
Animated.timing(this.state.animGradient, anim).start() |
|||
} |
|||
|
|||
render() { |
|||
const { children, onClose, deferHeight, ...props } = this.props |
|||
const { isHidden } = this.state |
|||
const { onBack, onClose, title, render, renderFooter, renderProps, noScroll } = this.props |
|||
const { animGradient } = this.state |
|||
|
|||
const gradientStyle = { |
|||
...GRADIENT_STYLE, |
|||
opacity: animGradient, |
|||
} |
|||
|
|||
return ( |
|||
<Body |
|||
style={{ height: isHidden && deferHeight ? deferHeight : undefined }} |
|||
data-e2e="modalBody" |
|||
> |
|||
{onClose && ( |
|||
<CloseContainer onClick={onClose}> |
|||
<IconCross size={16} /> |
|||
</CloseContainer> |
|||
)} |
|||
{(!isHidden || !deferHeight) && <Inner {...props}>{children}</Inner>} |
|||
</Body> |
|||
<Fragment> |
|||
<ModalHeader onBack={onBack} onClose={onClose}> |
|||
{title} |
|||
</ModalHeader> |
|||
<ModalContent |
|||
tabIndex={0} |
|||
ref={n => (this._content = n)} |
|||
onIsScrollableChange={this.animateGradient} |
|||
noScroll={noScroll} |
|||
> |
|||
{render && render(renderProps)} |
|||
</ModalContent> |
|||
<div style={GRADIENT_WRAPPER_STYLE}> |
|||
<Animated.div style={gradientStyle} /> |
|||
</div> |
|||
{renderFooter && <ModalFooter>{renderFooter(renderProps)}</ModalFooter>} |
|||
</Fragment> |
|||
) |
|||
} |
|||
} |
|||
|
|||
const CloseContainer = styled(Box).attrs({ |
|||
p: 4, |
|||
color: 'fog', |
|||
})` |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
z-index: 1; |
|||
|
|||
&:hover { |
|||
color: ${p => p.theme.colors.grey}; |
|||
} |
|||
|
|||
&:active { |
|||
color: ${p => p.theme.colors.dark}; |
|||
} |
|||
` |
|||
|
|||
const Body = styled(Box).attrs({ |
|||
bg: p => p.theme.colors.white, |
|||
relative: true, |
|||
borderRadius: 1, |
|||
})` |
|||
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2); |
|||
` |
|||
|
|||
const appear = keyframes` |
|||
from { opacity: 0; } |
|||
to { opacity: 1; } |
|||
` |
|||
const GRADIENT_STYLE = { |
|||
background: 'linear-gradient(rgba(255, 255, 255, 0), #ffffff)', |
|||
height: 40, |
|||
position: 'absolute', |
|||
bottom: 0, |
|||
left: 0, |
|||
right: 20, |
|||
} |
|||
|
|||
const Inner = styled(Box)` |
|||
animation: ${appear} 80ms linear; |
|||
` |
|||
const GRADIENT_WRAPPER_STYLE = { |
|||
height: 0, |
|||
position: 'relative', |
|||
pointerEvents: 'none', |
|||
} |
|||
|
|||
export default ModalBody |
|||
|
@ -0,0 +1,59 @@ |
|||
// @flow
|
|||
|
|||
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */ |
|||
|
|||
import React, { PureComponent } from 'react' |
|||
|
|||
class ModalContent extends PureComponent<{ |
|||
children: any, |
|||
onIsScrollableChange: boolean => void, |
|||
noScroll?: boolean, |
|||
}> { |
|||
componentDidMount() { |
|||
window.requestAnimationFrame(() => { |
|||
if (this._isUnmounted) return |
|||
this.showHideGradient() |
|||
if (this._outer) { |
|||
const ro = new ResizeObserver(this.showHideGradient) |
|||
ro.observe(this._outer) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
this._isUnmounted = true |
|||
} |
|||
|
|||
_outer = null |
|||
_isUnmounted = false |
|||
|
|||
showHideGradient = () => { |
|||
if (!this._outer) return |
|||
const { onIsScrollableChange } = this.props |
|||
const isScrollable = this._outer.scrollHeight > this._outer.clientHeight |
|||
onIsScrollableChange(isScrollable) |
|||
} |
|||
|
|||
render() { |
|||
const { children, noScroll } = this.props |
|||
|
|||
const contentStyle = { |
|||
...CONTENT_STYLE, |
|||
overflow: noScroll ? 'visible' : 'auto', |
|||
} |
|||
|
|||
return ( |
|||
<div style={contentStyle} ref={n => (this._outer = n)} tabIndex={0}> |
|||
{children} |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
const CONTENT_STYLE = { |
|||
flexShrink: 1, |
|||
padding: 20, |
|||
paddingBottom: 40, |
|||
} |
|||
|
|||
export default ModalContent |
@ -0,0 +1,18 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
|
|||
import { colors } from 'styles/theme' |
|||
|
|||
const MODAL_FOOTER_STYLE = { |
|||
display: 'flex', |
|||
justifyContent: 'flex-end', |
|||
borderTop: `2px solid ${colors.lightGrey}`, |
|||
padding: 20, |
|||
} |
|||
|
|||
const ModalFooter = ({ children }: { children: any }) => ( |
|||
<div style={MODAL_FOOTER_STYLE}>{children}</div> |
|||
) |
|||
|
|||
export default ModalFooter |
@ -0,0 +1,93 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
import styled from 'styled-components' |
|||
import { translate } from 'react-i18next' |
|||
|
|||
import type { T } from 'types/common' |
|||
|
|||
import Box from 'components/base/Box' |
|||
|
|||
import IconAngleLeft from 'icons/AngleLeft' |
|||
import IconCross from 'icons/Cross' |
|||
|
|||
const MODAL_HEADER_STYLE = { |
|||
position: 'relative', |
|||
display: 'flex', |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
padding: 20, |
|||
} |
|||
|
|||
const ModalTitle = styled(Box).attrs({ |
|||
color: 'dark', |
|||
ff: 'Museo Sans|Regular', |
|||
fontSize: 6, |
|||
grow: true, |
|||
shrink: true, |
|||
})` |
|||
text-align: center; |
|||
line-height: 1; |
|||
` |
|||
|
|||
const iconAngleLeft = <IconAngleLeft size={16} /> |
|||
const iconCross = <IconCross size={16} /> |
|||
|
|||
const ModalHeaderAction = styled(Box).attrs({ |
|||
horizontal: true, |
|||
align: 'center', |
|||
fontSize: 3, |
|||
p: 4, |
|||
color: 'grey', |
|||
})` |
|||
position: absolute; |
|||
top: 0; |
|||
left: ${p => (p.right ? 'auto' : 0)}; |
|||
right: ${p => (p.right ? 0 : 'auto')}; |
|||
line-height: 0; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
color: ${p => p.theme.colors.graphite}; |
|||
} |
|||
|
|||
&:active { |
|||
color: ${p => p.theme.colors.dark}; |
|||
} |
|||
|
|||
span { |
|||
border-bottom: 1px dashed transparent; |
|||
} |
|||
&:focus span { |
|||
border-bottom-color: inherit; |
|||
} |
|||
` |
|||
|
|||
const ModalHeader = ({ |
|||
children, |
|||
onBack, |
|||
onClose, |
|||
t, |
|||
}: { |
|||
children: any, |
|||
onBack: void => void, |
|||
onClose: void => void, |
|||
t: T, |
|||
}) => ( |
|||
<div style={MODAL_HEADER_STYLE}> |
|||
{onBack && ( |
|||
<ModalHeaderAction onClick={onBack}> |
|||
{iconAngleLeft} |
|||
<span>{t('common.back')}</span> |
|||
</ModalHeaderAction> |
|||
)} |
|||
<ModalTitle>{children}</ModalTitle> |
|||
{onClose && ( |
|||
<ModalHeaderAction right color="fog" onClick={onClose}> |
|||
{iconCross} |
|||
</ModalHeaderAction> |
|||
)} |
|||
</div> |
|||
) |
|||
|
|||
export default translate()(ModalHeader) |
@ -1,75 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
import styled from 'styled-components' |
|||
import { translate } from 'react-i18next' |
|||
|
|||
import type { T } from 'types/common' |
|||
|
|||
import Box from 'components/base/Box' |
|||
import IconAngleLeft from 'icons/AngleLeft' |
|||
|
|||
const Container = styled(Box).attrs({ |
|||
alignItems: 'center', |
|||
color: 'dark', |
|||
ff: 'Museo Sans|Regular', |
|||
fontSize: 6, |
|||
justifyContent: 'center', |
|||
p: 5, |
|||
relative: true, |
|||
})`` |
|||
|
|||
const Back = styled(Box).attrs({ |
|||
unstyled: true, |
|||
horizontal: true, |
|||
align: 'center', |
|||
color: 'grey', |
|||
ff: 'Open Sans', |
|||
fontSize: 3, |
|||
p: 4, |
|||
})` |
|||
position: absolute; |
|||
line-height: 1; |
|||
top: 0; |
|||
left: 0; |
|||
|
|||
&:hover { |
|||
color: ${p => p.theme.colors.graphite}; |
|||
} |
|||
|
|||
&:active { |
|||
color: ${p => p.theme.colors.dark}; |
|||
} |
|||
|
|||
span { |
|||
border-bottom: 1px dashed transparent; |
|||
} |
|||
&:focus span { |
|||
border-bottom-color: inherit; |
|||
} |
|||
` |
|||
|
|||
function ModalTitle({ |
|||
t, |
|||
onBack, |
|||
children, |
|||
...props |
|||
}: { |
|||
t: T, |
|||
onBack: any => void, |
|||
children: any, |
|||
}) { |
|||
return ( |
|||
<Container {...props} data-e2e="modal_title"> |
|||
{onBack && ( |
|||
<Back onClick={onBack}> |
|||
<IconAngleLeft size={16} /> |
|||
<span>{t('common.back')}</span> |
|||
</Back> |
|||
)} |
|||
{children} |
|||
</Container> |
|||
) |
|||
} |
|||
|
|||
export default translate()(ModalTitle) |
Loading…
Reference in new issue