UNPKG

@navinc/base-react-components

Version:
117 lines (110 loc) 5.87 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useState } from 'react'; import { styled, css, keyframes } from 'styled-components'; import { Copy } from './copy.js'; import { Icon } from './icon.js'; import { useIsLargerThanPhone } from './use-media-query.js'; const setIconStateFill = (iconState, theme) => { return iconState && ['active', 'complete'].includes(iconState) ? theme.navPrimary : theme.navNeutral400; }; 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.withConfig({ displayName: "brc-sc-IconBase", componentId: "brc-sc-10vlzx7" }) ` fill: ${({ theme }) => theme.navNeutral100}; stroke: ${({ theme }) => theme.navNeutral300}; stroke-width: ${({ strokeWidth }) => strokeWidth}; ${({ $iconState }) => $iconState === 'complete' && popAnimation} `; const ActiveStatus = styled.circle.withConfig({ displayName: "brc-sc-ActiveStatus", componentId: "brc-sc-1o674q7" }) ` stroke-linecap: round; stroke-width: ${({ strokeWidth }) => strokeWidth}; transform: rotate(-90deg); transform-origin: center; stroke-dasharray: ${({ lineLength }) => lineLength}; stroke-dashoffset: ${({ lineLength }) => lineLength}; stroke: ${({ theme }) => theme.navPrimary400}; animation: ${dash} 1s forwards; fill: none; `; const StyledIcon = styled(Icon).withConfig({ displayName: "brc-sc-StyledIcon", componentId: "brc-sc-m5vqcv" }) ` 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; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- not sure if it is safe to remove the optional chaining const pathLength = (node) => { if (node !== null) { setLineLength(node.getTotalLength()); } }; return (_jsxs("svg", { viewBox: `0 0 ${circleContainer} ${circleContainer}`, children: [_jsx(IconBase, { strokeWidth: strokeWidth, cx: centerPoint, cy: centerPoint, r: circleRadius, "$iconState": currentState }), currentState && ['active', 'complete'].includes(currentState) && (_jsx(ActiveStatus, { strokeWidth: strokeWidth, cx: centerPoint, cy: centerPoint, r: circleRadius, ref: pathLength, lineLength: lineLength })), _jsx(StyledIcon, { name: currentState !== 'complete' ? icon : 'actions/check-circle', x: iconCenter, y: iconCenter, "$iconState": currentState })] })); }; const IconStepContainer = styled.div.withConfig({ displayName: "brc-sc-IconStepContainer", componentId: "brc-sc-6ckew8" }) ` display: flex; flex-direction: column; align-items: center; text-align: center; max-width: 80px; z-index: 1; `; const iconWidth = 80; const mobileIconWidth = 40; const StyledStatusBar = styled.div.withConfig({ displayName: "brc-sc-StyledStatusBar", componentId: "brc-sc-1hgkrg" }) ` width: 100%; position: relative; display: grid; grid-template-columns: repeat(auto-fit, ${mobileIconWidth}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 }) => theme.navNeutral300}; top: ${mobileIconWidth / 2}px; } @media (${({ theme }) => theme.forLargerThanPhone}) { grid-template-columns: repeat(auto-fit, ${iconWidth}px); &::before { top: ${iconWidth / 2}px; } } `; /** The FlowSteps component receives a flow of routes with corresponding icons and names, as well as a reference to the current route within the component of which it is a child. The component accepts a `flow` of steps along with a reference to the `currentPath` from an external router to keep the component aligned as the user moves from one step to the next. When the index of the current route exceeds the indices of previous routes in the flow, the icons representing these routes will turn gray and the icon SVG itself will become a check mark to indicate completion. If an `endLocation` is passed, the flow stepper will render this as a last "dummy" step though it is the responsibility of the consuming component to push there and unmount the flow steps, as the endLocation will never be shown as `active`. ``` <FlowSteps flow={flowSteps} currentPath={location.pathname} endLocation={endLocation} /> ``` */ export const FlowSteps = ({ currentPath, flow = [], endLocation, 'data-testid': dataTestid, className, }) => { const step = flow.findIndex((path) => path.route === currentPath); const isDesktop = useIsLargerThanPhone(); return (_jsxs(StyledStatusBar, { iconCount: flow.length, "data-testid": dataTestid, className: className, children: [flow.map((item, i) => (_jsxs(IconStepContainer, { children: [_jsx(FlowIcon, { icon: item.icon, // eslint-disable-next-line no-nested-ternary currentState: item.route === currentPath ? 'active' : i >= step ? 'incomplete' : 'complete' }), _jsx(Copy, { size: isDesktop ? 'sm' : 'xs', children: item.name })] }, i))), endLocation && (_jsxs(IconStepContainer, { children: [_jsx(FlowIcon, { icon: endLocation.icon }), _jsx(Copy, { size: isDesktop ? 'sm' : 'xs', children: endLocation.name })] }))] })); }; //# sourceMappingURL=flow-steps.js.map