Skip to content

Instantly share code, notes, and snippets.

@betalantz
Created May 12, 2019 01:22
Show Gist options
  • Save betalantz/8d2e29287d942df17d31e8d10c988c08 to your computer and use it in GitHub Desktop.
Save betalantz/8d2e29287d942df17d31e8d10c988c08 to your computer and use it in GitHub Desktop.
React modal animation
const { map, is, contains, curry, __, prop, equals, pipe, find, ifElse, F, identity } = R
const cloneChildren = (children, props) => React.Children.map(children, child => <child.type {...child.props} {...props} />)
const Head = ({ children, ...props }) => cloneChildren(children, props)
const Content = ({ children, ...props }) => cloneChildren(children, props)
const pickFromRect = rect => {
const { width, height, top, left } = rect
return { width, height, x: left, y: top }
}
const _findChildren = curry((component, children) => find(pipe(prop('type'), equals(component)))(children))
const findChildrenOr = curry((val, component, children) => ifElse(is(Array), _findChildren(component), val)(children))
const findChildren = findChildrenOr(F)
const findChildrenOrIdentity = findChildrenOr(identity)
const getLastPositionStyles = ({ maxWidth, maxHeight }) => {
const mW = maxWidth > window.innerWidth ? window.innerWidth : maxWidth
const mH = maxHeight > window.innerHeight ? window.innerHeight : maxHeight
return {
width: mW,
height: mH,
x: window.innerWidth / 2 - mW / 2,
y: window.innerHeight / 2 - mH / 2
}
}
const states = {
IDLE: 'IDLE',
OPEN: 'OPEN',
OPENED: 'OPENED',
CLOSE: 'CLOSE',
IMMEDIATELY_CLOSE: 'IMMEDIATELY_CLOSE'
}
const openState = [ states.OPEN, states.OPENED, states.CLOSE, states.IMMEDIATELY_CLOSE ]
const afterOpenState = [ states.OPENED, states.CLOSE ]
const closingState = [ states.CLOSE, states.IMMEDIATELY_CLOSE ]
const isActiveState = contains(__, openState)
const isAfterOpenState = contains(__, afterOpenState)
const isClosingState = contains(__, closingState)
const isOpenedState = equals(states.OPENED)
class Modal extends React.Component {
static Head = Head
static Content = Content
state = {
styles: {},
state: states.IDLE
}
constructor(props) {
super(props)
this.open = this.open.bind(this)
this.close = this.close.bind(this)
this.createProps = this.createProps.bind(this)
this.closeDoneCallback = this.closeDoneCallback.bind(this)
this.openDoneCallback = this.openDoneCallback.bind(this)
this.clone = React.createRef()
this.content = React.createRef()
}
openDoneCallback() {
const to = getLastPositionStyles(this.props)
this.setState({
state: states.OPENED,
styles: {
maxWidth: to.width,
height: to.height,
left: '50%',
top: '50%',
transform: 'translate3d(-50%, -50%, 0)'
}
})
}
closeDoneCallback() {
this.setState({
state: states.IDLE,
styles: {},
bodyStyles: {}
})
}
setStartData(state, cb) {
const cloneRect = pickFromRect(this.clone.current.getBoundingClientRect())
const rect = pickFromRect(this.content.current.getBoundingClientRect())
const styles = {
maxWidth: rect.width,
height: rect.height,
top: 0,
left: 0,
transform: `translate3d(${rect.x}px, ${rect.y}px, 0)`
}
this.setState({ ...state, cloneRect, rect, styles }, cb)
}
openAnimation = () => {
const { rect, state } = this.state
if (state !== states.OPEN) return
const to = getLastPositionStyles(this.props)
anime({
targets: this.content.current,
maxWidth: to.width,
height: to.height,
translateX: [ rect.x, to.x ],
translateY: [ rect.y, to.y ],
duration: this.props.ms,
baseFrequency: 0,
complete: this.openDoneCallback,
easing: 'easeInQuad'
});
}
open() {
if (this.state.state !== states.IDLE) return
this.setStartData({ state: states.OPEN }, this.openAnimation)
}
closeAnimation = () => {
const { cloneRect: to, rect } = this.state
anime({
targets: this.content.current,
maxWidth: to.width,
height: to.height,
translateX: [ rect.x, to.x ],
translateY: [ rect.y, to.y ],
duration: this.props.ms,
baseFrequency: 0,
complete: this.closeDoneCallback,
easing: 'easeOutQuad'
})
}
close() {
if (this.state.state !== states.OPENED) return
this.setStartData(undefined, this.closeAnimation)
}
createProps(Component, props) {
return {
...Component.props,
modal: {
...props,
isOpen: isActiveState(this.state.state),
close: this.close
}
}
}
renderClone() {
const Head = findChildrenOrIdentity(Modal.Head, this.props.children)
return Head && isActiveState(this.state.state)
? <Head.type {...this.createProps(Head)}/>
: null
}
renderHead() {
const Head = findChildrenOrIdentity(Modal.Head, this.props.children)
return Head
? <Head.type {...this.createProps(Head, { original: true })}/>
: null
}
renderContent() {
const Content = findChildren(Modal.Content, this.props.children)
return Content && isOpenedState(this.state.state)
? <Content.type {...this.createProps(Content)}/>
: null
}
getBackgroundStyle() {
return {
transition: `opacity ${this.props.ms / 4}ms ease-in-out`
}
}
getContentStyle() {
return {
...this.state.styles,
transition: `box-shadow ${this.props.ms}ms ease-in-out`
}
}
getContaninerClassNames() {
const { state } = this.state
return [
'transform-modal__container',
isActiveState(state) ? 'transform-modal__container--open' : '',
isClosingState(state) ? 'transform-modal__container--closing' : ''
].join(' ')
}
render() {
return (
<div className='transform-modal' {...this.props}>
<div ref={this.clone}>
{this.renderClone()}
</div>
<div className={this.getContaninerClassNames()}>
<div
className='transform-modal__background'
onClick={this.close}
style={this.getBackgroundStyle()}
/>
<div
className='transform-modal__content'
style={this.getContentStyle()}
onClick={this.open}
ref={this.content}
>
{this.renderHead()}
{this.renderContent()}
</div>
</div>
</div>
)
}
}
const images = [
{
bg: 'https://images.unsplash.com/photo-1465765407776-61e63b99d10c?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=bc1d692696d6dc9f422fd89b2aaa1cd1',
title: 'Winner #1',
text: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna.'
},
{
bg: 'https://images.unsplash.com/photo-1485286162995-aa63d31c06cb?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=edb0ee9e83e720444637907175b1b521',
title: 'Winner #2',
text: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna.'
},
{
bg: 'https://images.unsplash.com/photo-1473346782721-d6cef5897f07?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=254be18c40b7520249b2b29f85e05fa4',
title: 'Winner #3',
text: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna.'
},
{
bg: 'https://images.unsplash.com/photo-1516737347189-ed6ee46a4027?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=bcf69dbf1cf03001f85e570f09237baa',
title: 'Winner #4',
text: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna.'
},
{
bg: 'https://images.unsplash.com/photo-1516419591857-14c5e8ce3a6e?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=a7dc46f376e7dd7650884f1314712c5c',
title: 'Winner #5',
text: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna.'
},
{
bg: 'https://images.unsplash.com/photo-1520405231068-ff009f727fe6?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=4dea33bfbf12e317d9966179cdc14188',
title: 'Winner #6',
text: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna.'
}
]
const ImageHead = ({ title, bg, modal: { isOpen, original, close } }) => (
<div className={`image ${isOpen & original ? 'image--active' : ''}`} style={{ }}>
<div className='image__bg' style={{ backgroundImage: `url(${bg})` }}/>
<div className='image__content'>
{
isOpen & original
? <button className='image__close' onClick={close}>&#10006;</button>
: null
}
<h2>{title}</h2>
</div>
</div>
)
const ImageContent = ({ title, text }) => (
<div className='image-content'>
<h3>{title}</h3>
<p>{text}</p>
</div>
)
const ImageModal = item => (
<div className='grid__item'>
<Modal
maxWidth={700}
maxHeight={500}
ms={300}
>
<Modal.Head>
<ImageHead {...item}/>
</Modal.Head>
<Modal.Content>
<ImageContent {...item}/>
</Modal.Content>
</Modal>
</div>
)
const ImageModalList = ({ images }) => map(ImageModal, images)
const Layout = ({ children }) => (
<React.Fragment>
<div class="title">
<h1>React modal animation</h1>
<ul class="socials">
<li>
<a href="https://odintsov.me" target="_blank">
<img src="https://maxcdn.icons8.com/Android_L/PNG/24/Messaging/link-24.png" alt="Twitter icon"/>
</a>
</li>
<li>
<a href="https://twitter.com/odintsov_design" target="_blank">
<img src="https://img.icons8.com/color/48/000000/twitter.png" alt="Twitter icon"/>
</a>
</li>
</ul>
</div>
{children}
<div class="credits">Created with <span class="love"></span> by <a href="https://codepen.io/ivanodintsov">Ivan Odintsov</a></div>
</React.Fragment>
)
const App = () => (
<Layout>
<div className='grid'>
<ImageModalList images={images}/>
</div>
</Layout>
)
const root = document.querySelector('#root')
ReactDOM.render(<App/>, root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/2.2.0/anime.min.js"></script>
// App styles
.grid
max-width: 900px
margin: 40px auto
padding: 0 15px
display: grid
grid-template-columns: 1fr
grid-gap: 15px
@media screen and (min-width: 500px)
grid-template-columns: 2fr 4fr
@media screen and (min-width: 900px)
grid-template-columns: 2fr 4fr 3fr
&__item
height: 300px
width: 100%
.image
box-shadow: #adb5bd 0px 10px 20px -10px
border-radius: 8px
color: #f8f9fa
min-height: 300px
height: 300px
overflow: hidden
transition: box-shadow .2s ease-in-out
position: relative
will-change: box-shadow
width: 100%
&__bg
background-size: cover
background-position: center
width: 100%
height: 100%
position: absolute
left: 0
top: 0
z-index: 0
&:before
background: linear-gradient(rgba(0, 0, 0, .5), transparent)
border-radius: 8px
content: ''
display: block
position: absolute
top: 0
left: 0
bottom: 0
right: 0
opacity: 0
transition: opacity .3s ease-in-out
will-change: opacity
z-index: 1
&:not(.image--active):not(:active)
cursor: pointer
&:hover
box-shadow: #adb5bd 0px 10px 30px -2px
// transform: translateY(-10px)
&--active
box-shadow: none
&:before
opacity: 1
&__content
overflow: auto
padding: 10px 24px
position: relative
z-index: 2
&-content
color: #343a40
padding: 10px 24px
p
line-height: 1.7
&__close
background: none
outline: none
border: none
color: #fff
cursor: pointer
padding: 0
position: absolute
right: 24px
top: 24px
transition: transform .25s ease-in-out
&:hover
transform: rotate(90deg)
// Modal styles
.transform-modal
-webkit-tap-highlight-color: rgba(0, 0, 0, 0)
-webkit-touch-callout: none
.transform-modal__background
background-color: rgba(0, 0, 0, .2)
content: ''
display: block
opacity: 0
.transform-modal__container--open
position: fixed
top: 0
left: 0
right: 0
bottom: 0
z-index: 1115
.transform-modal__background
position: fixed
top: 0
left: 0
right: 0
bottom: 0
opacity: 1
.transform-modal__content
box-shadow: rgba(0, 0, 0, .1) 0px 10px 30px
border-radius: 8px
background-color: #f1f3f5
position: fixed
width: 100%
max-height: 100%
overflow: auto
-ms-overflow-style: -ms-autohiding-scrollbar
.transform-modal__container--closing
.transform-modal__content
box-shadow: rgba(0, 0, 0, .1) 0 0 0 0
overflow: hidden
.transform-modal__background
opacity: 0
.transform-modal__content
box-shadow: none
will-change: max-width, height, transform
::-webkit-scrollbar
display: none
// Template styles
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,600)
a
font-weight: 600
color: #91a7ff
text-decoration: none
&:hover
color: #5c7cfa
text-decoration: underline
html,
body
font-family: 'Open Sans'
body
background-color: #f8f9fa
color: #495057
.title
text-align: center
h1
font-size: 2.4em
margin: 100px 0 24px 0
.credits
font-size: .8em
text-align: center
margin-bottom: 40px
.love
background: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/42764/heart-smil.svg)
display: inline-block
height: 16px
vertical-align: middle
width: 16px
.socials
display: block
font-size: 14px
margin: 0
padding: 0
li
display: inline
&:not(:last-child)
margin-right: .75em
a
vertical-align: middle
&:hover
img
animation: link .5s
img
width: 1.3em
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment