UNPKG

@navinc/base-react-components

Version:
223 lines (202 loc) 6.13 kB
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)``