@navinc/base-react-components
Version:
Nav's Pattern Library
223 lines (202 loc) • 6.13 kB
JavaScript
import React, { createContext, useContext, useState, useRef, useEffect, useCallback } from 'react'
import styled, { keyframes } from 'styled-components'
import { useDebouncedCallback } from 'use-debounce'
import Header from './header.js'
import Icon from './icon.js'
const moveOffScreen = keyframes`
from { right: 0%; }
to { right: -100%; }
`
const moveOnScreen = keyframes`
from { right: -100%; }
to { right: 0%; }
`
const suddenIn = keyframes`
from { width: 100%; opacity: 0; }
to { width: 100%; opacity: 1; }
`
const suddenOut = keyframes`
0% { width: 100%; opacity: 1; }
99.9% { width: 100%; }
100% { width: 0%; opacity: 0; }
`
const Backdrop = styled.div`
align-items: flex-start;
animation: 0.2s ${({ theme }) => theme.materialTransitionTiming} both ${({ isOpen }) =>
isOpen ? suddenIn : suddenOut} ;
background-color: hsla(0, 0%, 0%, .2)};
box-sizing: border-box;
display: flex;
height: 100%;
justify-content: flex-end;
overflow: hidden;
pointer-events: ${({ isOpen }) => (isOpen ? 'initial' : 'none')};
position: fixed;
right: 0;
& * {
box-sizing: border-box;
}
`
const drawerHeightVariations = {
dynamic: `
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
height: auto;
margin-top: 100px;
max-height: calc(100vh - 100px);
min-height: 80px;
`,
full: `
align-content: flex-start;
height: 100vh;
`,
}
const contentHeightVariations = {
dynamic: 'max-height: calc(100vh - 160px);',
full: 'max-height: calc(100vh - 65px);',
}
const CloseButton = styled(Icon).attrs(() => ({ name: 'actions/close' }))`
cursor: pointer;
`
const Content = styled.div`
overflow: scroll;
${({ heightVariation }) => contentHeightVariations[heightVariation] ?? contentHeightVariations.dynamic}
`
const Drawer = styled.div`
animation: 0.2s ${({ theme }) => theme.materialTransitionTiming} both
${({ isOpen }) => (isOpen ? moveOnScreen : moveOffScreen)};
background-color: ${({ variation = 'default', theme }) => {
const colorVariation = {
blue: theme.pastelBlue100,
brown: theme.timberwolf100,
default: theme.white,
}
return colorVariation[variation] ?? colorVariation.default
}};
display: grid;
grid-template-columns: 1fr min-content;
gap: 16px;
border-right: solid 1px ${({ theme }) => theme.border};
max-width: calc(100vw - 55px);
overflow: hidden;
padding: 16px;
transform: translateX(
${({ isOpen, moveStartX, translateX }) => (isOpen ? Math.max(translateX - moveStartX, 0) : 485)}px
);
transition: transform 0.2s ${({ theme }) => theme.materialTransitionTiming};
${({ heightVariation }) => drawerHeightVariations[heightVariation] ?? drawerHeightVariations.dynamic}
width: 485px;
$ > ${Content} {
grid-columns: 1 / -1;
}
@media (${({ theme }) => theme.forLargerThanPhone}) {
padding: 24px 24px 32px;
}
`
const InfoDrawerContext = createContext({
content: null,
title: null,
setInfoDrawer: () => {},
closeHandlersRef: { current: [] },
})
export const InfoDrawerProvider = ({ children }) => {
const [content, setContent] = useState(null)
const [title, setTitle] = useState(null)
const [isOpen, setIsOpen] = useState(false)
const closeHandlersRef = useRef([])
const close = useCallback(() => {
setIsOpen(false)
closeHandlersRef.current.forEach((closeHandler) => closeHandler())
}, [])
const setInfoDrawer = useCallback(
(config = {}) => {
const cfg = {
title,
content,
isOpen,
...config,
}
if (isOpen && !config.isOpen) {
close()
} else {
setIsOpen(cfg.isOpen)
}
setTitle(cfg.title)
setContent(cfg.content)
},
[close, content, isOpen, title]
)
return (
<InfoDrawerContext.Provider value={{ content, isOpen, setIsOpen, title, closeHandlersRef, close, setInfoDrawer }}>
{children}
</InfoDrawerContext.Provider>
)
}
export const useInfoDrawer = ({ onClose } = {}) => {
const { content, title, setInfoDrawer, closeHandlersRef, isOpen } = useContext(InfoDrawerContext)
if (onClose && !closeHandlersRef.current.includes(onClose)) {
closeHandlersRef.current = closeHandlersRef.current.concat(onClose)
}
// When the component unmounts, we want to clean up our onClose handler.
useEffect(
() => () => {
closeHandlersRef.current = closeHandlersRef.current.filter((closeHandler) => closeHandlersRef !== onClose)
},
[closeHandlersRef, onClose]
)
return {
content,
title,
isOpen,
setInfoDrawer,
}
}
export const InfoDrawer = ({ variation, heightVariation = 'dynamic', className }) => {
const { content, title, isOpen, setInfoDrawer } = useInfoDrawer()
const [moveStartX, setMoveStartX] = useState(0)
const [translateX, setTranslateX] = useState(0)
const onTouchMove = useDebouncedCallback(
(e) => {
onTouchMove.cancel()
e.persist()
setTranslateX(e.touches[0].clientX)
},
2,
{ maxWait: 5 }
)
return (
<Backdrop
isOpen={isOpen}
className={className}
data-testid="info-drawer:backdrop"
onTouchStart={(e) => setMoveStartX(e.touches[0].clientX)}
onTouchMove={onTouchMove}
onTouchEnd={() => {
onTouchMove.cancel()
translateX - moveStartX > 180 && setInfoDrawer({ isOpen: false })
setMoveStartX(0)
setTranslateX(0)
}}
onClick={(e) => {
e.persist()
if (e.target.dataset.testid === 'info-drawer:backdrop') {
setInfoDrawer({ isOpen: false })
}
}}
>
<Drawer
moveStartX={moveStartX}
translateX={translateX}
isOpen={isOpen}
variation={variation}
heightVariation={heightVariation}
data-testid="info-drawer:drawer"
>
<Header size="sm">{title}</Header>
<CloseButton onClick={() => setInfoDrawer({ isOpen: false })} data-testid="info-drawer:close-button" />
<Content heightVariation={heightVariation}>{content}</Content>
</Drawer>
</Backdrop>
)
}
export default styled(InfoDrawer)``