@navinc/base-react-components
Version:
Nav's Pattern Library
138 lines (123 loc) • 4.18 kB
JavaScript
import React, { useState } from 'react'
import styled, { css, keyframes } from 'styled-components'
import Copy from './copy.js'
import Icon from './icon.js'
import isRebrand from './is-rebrand.js'
const setIconStateFill = (iconState, theme) => {
if (isRebrand(theme)) {
return ['active', 'complete'].includes(iconState) ? theme.navPrimary : theme.navNeutral400
}
return ['active', 'complete'].includes(iconState) ? theme.bubbleBlue500 : theme.neutral300
}
const pop = keyframes`
0% { transform: scale(0.9); }
70% { transform: scale(1.1); }
100% { transform: scale(1); }
`
const popAnimation = () => css`
animation: ${pop} 0.3s ${({ theme }) => theme.materialTransitionTiming};
transform-origin: center;
`
const dash = keyframes`
to { stroke-dashoffset: 0; }
`
const IconBase = styled.circle`
fill: ${({ theme }) => (isRebrand(theme) ? theme.navNeutral100 : theme.neutral100)};
stroke: ${({ theme }) => (isRebrand(theme) ? theme.navNeutral300 : theme.neutral200)};
stroke-width: ${({ strokeWidth }) => strokeWidth};
${({ iconState }) => iconState === 'complete' && popAnimation}
`
const ActiveStatus = styled.circle`
stroke-linecap: round;
stroke-width: ${({ strokeWidth }) => strokeWidth};
transform: rotate(-90deg);
transform-origin: center;
stroke-dasharray: ${({ lineLength }) => lineLength};
stroke-dashoffset: ${({ lineLength }) => lineLength};
stroke: ${({ theme }) => (isRebrand(theme) ? theme.navPrimary400 : theme.bubbleBlue500)};
animation: ${dash} 1s forwards;
fill: none;
`
const StyledIcon = styled(Icon)`
fill: ${({ iconState, theme }) => setIconStateFill(iconState, theme)};
`
const FlowIcon = ({ icon, currentState }) => {
const [lineLength, setLineLength] = useState(0)
const strokeWidth = 3
const circleContainer = 80
const circleDiameter = circleContainer * 0.75
const circleRadius = circleDiameter / 2 - strokeWidth / 2
const centerPoint = circleContainer / 2
const iconCenter = centerPoint - 12
const pathLength = (node) => node !== null && setLineLength(node?.getTotalLength?.() ?? 0)
return (
<svg viewBox={`0 0 ${circleContainer} ${circleContainer}`}>
<IconBase strokeWidth={strokeWidth} cx={centerPoint} cy={centerPoint} r={circleRadius} iconState={currentState} />
{['active', 'complete'].includes(currentState) && (
<ActiveStatus
strokeWidth={strokeWidth}
cx={centerPoint}
cy={centerPoint}
r={circleRadius}
ref={pathLength}
lineLength={lineLength}
/>
)}
<StyledIcon
name={currentState !== 'complete' ? icon : 'actions/check-circle'}
x={iconCenter}
y={iconCenter}
iconState={currentState}
/>
</svg>
)
}
const IconStepContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
max-width: 80px;
z-index: 1;
`
const iconWidth = 80
const StyledStatusBar = styled.div`
width: 100%;
position: relative;
display: grid;
grid-template-columns: repeat(auto-fit, ${iconWidth}px);
justify-items: stretch;
justify-content: space-between;
&::before {
content: '';
display: ${(props) => props.iconCount <= 1 && 'none'};
position: absolute;
width: calc(100% - 40px);
left: 20px;
height: 2px;
background: ${({ theme }) => (isRebrand(theme) ? theme.navNeutral300 : theme.neutral200)};
top: ${iconWidth / 2}px;
}
`
export default ({ currentPath, flow = [], endLocation }) => {
const step = flow.findIndex((path) => path.route === currentPath)
return (
<StyledStatusBar iconCount={flow.length}>
{flow.map((item, i) => (
<IconStepContainer key={i}>
<FlowIcon
icon={item.icon}
currentState={item.route === currentPath ? 'active' : i >= step ? 'incomplete' : 'complete'}
/>
<Copy size="sm">{item.name}</Copy>
</IconStepContainer>
))}
{endLocation && (
<IconStepContainer>
<FlowIcon icon={endLocation.icon} />
<Copy size="sm">{endLocation.name}</Copy>
</IconStepContainer>
)}
</StyledStatusBar>
)
}