@axeptio/design-system
Version:
Design System for Axeptio
198 lines (176 loc) • 5.66 kB
JSX
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;