Created
November 25, 2020 12:59
-
-
Save d-asensio/a6923f6a175d0a008a141643ac9fe25f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useState, useRef } from 'react' | |
import PropTypes from 'prop-types' | |
import styled, { css } from 'styled-components' | |
import get from 'lodash.get' | |
import { useLockBodyScroll } from 'react-use' | |
import { Portal } from '../Portal' | |
import { Backdrop } from '../Backdrop' | |
// #region Constants | |
export const MODAL_ALIGNMENTS = { | |
center: () => _AlingmentCenter(), | |
right: () => _AlingmentRight(), | |
bottom: () => _AlingmentBottom(), | |
left: () => _AlingmentLeft(), | |
top: () => _AlingmentTop() | |
} | |
export const MODAL_TRANSITIONS = { | |
reveal: ms => _TransitionReveal(ms), | |
slide: ms => _TransitionSlide(ms) | |
} | |
// #region | |
// #region Definition | |
/** | |
* - {0} This is here to avoid z-index issues with elements that could be fixed | |
* outside the context of the modal. | |
* | |
* - {1} Setting pointer-events to `none` ensures that the wrapper won't | |
* interfere with other elements. | |
*/ | |
const Wrapper = styled.div` | |
position: fixed; /*{0}*/ | |
z-index: 1; | |
pointer-events: none; /*{1}*/ | |
top: 0; | |
left: 0; | |
width: 100vw; | |
height: 100vh; | |
display: flex; | |
align-items: center; | |
&[open] { | |
${() => _Open()} | |
} | |
${() => _BackgroundColor('neutral.n100')} | |
` | |
const Content = styled.div` | |
z-index: 1; | |
overflow: scroll; | |
-webkit-overflow-scrolling: touch; | |
opacity: 0; | |
box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; | |
` | |
// #endregion | |
// #region Style Modifiers | |
/** | |
* - {0} Increase the specificity of the modifier. Since this modifier is | |
* responsible of opening the modal, we have to ensure that it overrides any | |
* other modifier, so we increase its specificity to reach that goal. | |
* | |
* Warning: The specificity could be overridden from outside the component with | |
* the same technique, but we assume that such scenario is the consequence of an | |
* antipattern/bad practice since we don't encourage more than 2 levels of | |
* nested selectors. | |
*/ | |
const _Open = () => css` | |
&& { /*{0}*/ | |
pointer-events: auto; | |
${Content} { /*{0}*/ | |
transform: none; | |
opacity: 1; | |
} | |
${Backdrop} { | |
${Backdrop.mods.Show()} | |
} | |
} | |
` | |
const _AlingmentLeft = () => css` | |
flex-direction: row; | |
justify-content: flex-start; | |
${Content} { | |
transform: translateX(-100%); | |
} | |
` | |
const _AlingmentCenter = () => css` | |
flex-direction: row; | |
justify-content: center; | |
${Content} { | |
transform: translateY(-100vh); | |
} | |
` | |
const _AlingmentRight = () => css` | |
flex-direction: row; | |
justify-content: flex-end; | |
${Content} { | |
transform: translateX(100%); | |
} | |
` | |
const _AlingmentTop = () => css` | |
flex-direction: column; | |
justify-content: flex-start; | |
${Content} { | |
transform: translateY(-100%); | |
} | |
` | |
const _AlingmentBottom = () => css` | |
flex-direction: column; | |
justify-content: flex-end; | |
${Content} { | |
transform: translateY(100%); | |
} | |
` | |
const Alignment = alignment => css` | |
${MODAL_ALIGNMENTS[alignment]()} | |
` | |
const _TransitionReveal = durationMs => css` | |
${Content} { | |
transition-property: opacity, transform; | |
transition-duration: ${durationMs}ms, 0ms; | |
transition-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); | |
transition-delay: 0ms, ${durationMs}ms; | |
} | |
&[open] ${Content} { | |
transition-property: opacity; | |
transition-duration: ${durationMs}ms; | |
transition-delay: 0ms; | |
} | |
` | |
const _TransitionSlide = durationMs => css` | |
${Content}, | |
&[open] ${Content} { | |
transition-property: opacity, transform; | |
transition-duration: ${durationMs}ms; | |
transition-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); | |
transition-delay: 0ms; | |
} | |
` | |
const Transition = (transition, durationMs = 400) => css` | |
${MODAL_TRANSITIONS[transition](durationMs)} | |
` | |
const Width = width => css` | |
${Content} { | |
width: ${width}; | |
} | |
` | |
const Height = height => css` | |
${Content} { | |
height: ${height}; | |
} | |
` | |
const _BackgroundColor = color => css` | |
${Content} { | |
${({ theme }) => css` | |
background-color: ${get(theme.palette, color, color)}; | |
`} | |
} | |
` | |
// #endregion | |
// #region Composition | |
function Modal ({ | |
children, | |
onOutsideClick, | |
...rest | |
}) { | |
const { open } = rest | |
useLockBodyScroll(!!open) | |
const [transitioning, setTransitioning] = useState(false) | |
const contentRef = useRef(null) | |
useEffect(() => { | |
setTransitioning(true) | |
}, [open]) | |
const handleTransitionEnd = e => { | |
if (e.target.isSameNode(contentRef.current)) { | |
setTransitioning(false) | |
} | |
} | |
return ( | |
<Portal> | |
<Wrapper {...rest}> | |
<Backdrop | |
onClick={onOutsideClick} | |
/> | |
<Content | |
onTransitionEnd={handleTransitionEnd} | |
ref={contentRef} | |
> | |
{(open || transitioning) && children} | |
</Content> | |
</Wrapper> | |
</Portal> | |
) | |
} | |
// #endregion | |
// #region Export | |
Modal.mods = { | |
Alignment, | |
Transition, | |
Width, | |
Height | |
} | |
Modal.propTypes = { | |
children: PropTypes.node, | |
open: PropTypes.bool, | |
onOutsideClick: PropTypes.func, | |
onTransitionEnd: PropTypes.func | |
} | |
export default styled(Modal)`` | |
// #endregion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment