UNPKG

@axeptio/design-system

Version:
198 lines (176 loc) 5.66 kB
import React, { Children, createRef, forwardRef } from 'react'; import PropTypes from 'prop-types'; import styled, { keyframes } from 'styled-components'; import { Flipped } from 'react-flip-toolkit'; const getFadeContainerKeyFrame = ({ animatingOut, direction }) => { if (!direction) { return; } return keyframes` to { transform: translateX(0px); opacity: ${animatingOut ? 0 : 1}; } `; }; const FadeContainer = styled.div` will-change: transform; animation-name: ${getFadeContainerKeyFrame}; animation-duration: ${props => props.duration}ms; animation-fill-mode: forwards; opacity: ${props => (props.direction && !props.animatingOut ? 0 : 1)}; top: 0; left: 0; `; const FadeContents = forwardRef(({ children, duration, animatingOut, direction }, ref) => ( <FadeContainer // prevent screen readers from reading out hidden content aria-hidden={animatingOut} animatingOut={animatingOut} direction={direction} duration={duration} ref={ref} > {children} </FadeContainer> )); FadeContents.displayName = 'FadeContents'; FadeContents.propTypes = { duration: PropTypes.number, direction: PropTypes.oneOf(['right', 'left']), animatingOut: PropTypes.bool, children: PropTypes.node }; const getDropdownRootKeyFrame = ({ animatingOut, direction }) => { if (!animatingOut && direction) { return null; } return keyframes` from { transform: ${animatingOut ? 'rotateX(0)' : 'rotateX(-15deg)'}; opacity: ${animatingOut ? 1 : 0}; } to { transform: ${animatingOut ? 'rotateX(-15deg)' : 'rotateX(0)'}; opacity: ${animatingOut ? 0 : 1}; } `; }; const DropdownRoot = styled.div` transform-origin: 0 0; will-change: transform; animation-name: ${getDropdownRootKeyFrame}; animation-duration: ${props => props.duration}ms; /* use 'forwards' to prevent flicker on leave animation */ animation-fill-mode: forwards; display: flex; flex-direction: column; align-items: center; position: relative; top: 10px; `; const DropdownBackground = styled.div` transform-origin: 0 0; background-color: white; border-radius: 12px; overflow: hidden; position: relative; box-shadow: 0 5.5px 2.2px rgba(0, 0, 0, 0.028), 0 18.3px 13.9px rgba(0, 0, 0, 0.042), 0 82px 80px rgba(0, 0, 0, 0.07); will-change: transform; border-radius: 16px; `; const AltBackground = styled.div` background-color: white; width: 300%; height: 100%; position: absolute; top: 0; left: -100%; transform-origin: 0 0; z-index: 0; transition: transform ${props => props.duration}ms; `; const InvertedDiv = styled.div` will-change: transform; position: ${props => (props.absolute ? 'absolute' : 'relative')}; top: 0; left: 0; &:first-of-type { z-index: 1; } &:not(:first-of-type) { z-index: -1; } `; const getFirstDropdownSectionHeight = el => { if (!el || !el.querySelector || !el.querySelector('*[data-first-dropdown-section]')) return 0; return el.querySelector('*[data-first-dropdown-section]').offsetHeight; }; const updateAltBackground = ({ altBackground, prevDropdown, currentDropdown }) => { const prevHeight = getFirstDropdownSectionHeight(prevDropdown); const currentHeight = getFirstDropdownSectionHeight(currentDropdown); const immediateSetTranslateY = (el, translateY) => { el.style.transform = `translateY(${translateY}px)`; el.style.transition = 'transform 0s'; requestAnimationFrame(() => (el.style.transitionDuration = '')); }; if (prevHeight) { // transition the grey ("alt") background from its previous height to its current height immediateSetTranslateY(altBackground, prevHeight); requestAnimationFrame(() => { altBackground.style.transform = `translateY(${currentHeight}px)`; }); } else { // just immediately set the background to the appropriate height // since we don't have a stored value immediateSetTranslateY(altBackground, currentHeight); } }; const DropdownContainer = ({ children, direction, animatingOut, duration }) => { let altBackgroundEl; const currentDropdownEl = createRef(); const prevDropdownEl = createRef(); React.useEffect(() => { updateAltBackground({ altBackground: altBackgroundEl, prevDropdown: prevDropdownEl.current, currentDropdown: currentDropdownEl.current }); }, []); const [currentDropdown, prevDropdown] = Children.toArray(children); return ( <DropdownRoot direction={direction} animatingOut={animatingOut} duration={duration}> <Flipped flipId="dropdown"> <DropdownBackground> <Flipped inverseFlipId="dropdown"> <InvertedDiv> <AltBackground ref={el => (altBackgroundEl = el)} duration={duration} /> <FadeContents direction={direction} duration={duration} ref={currentDropdownEl}> {currentDropdown} </FadeContents> </InvertedDiv> </Flipped> <Flipped inverseFlipId="dropdown" scale> <InvertedDiv absolute> {prevDropdown && ( <FadeContents animatingOut direction={direction} duration={duration} ref={prevDropdownEl}> {prevDropdown} </FadeContents> )} </InvertedDiv> </Flipped> </DropdownBackground> </Flipped> </DropdownRoot> ); }; DropdownContainer.propTypes = { duration: PropTypes.number, direction: PropTypes.oneOf(['right', 'left']), animatingOut: PropTypes.bool, children: PropTypes.node }; export default DropdownContainer;