Skip to content

Instantly share code, notes, and snippets.

@d-asensio
Created November 25, 2020 12:59
Show Gist options
  • Save d-asensio/a6923f6a175d0a008a141643ac9fe25f to your computer and use it in GitHub Desktop.
Save d-asensio/a6923f6a175d0a008a141643ac9fe25f to your computer and use it in GitHub Desktop.
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