|
1 | | -import React from "react"; |
2 | | -import { Box, useTheme, Text } from "@chakra-ui/react"; |
| 1 | +/* |
| 2 | + * Copyright (c) 2025, Salesforce, Inc. |
| 3 | + * All rights reserved. |
| 4 | + * SPDX-License-Identifier: BSD-3-Clause |
| 5 | + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause |
| 6 | + */ |
| 7 | +import React from 'react' |
| 8 | +import {Box, useTheme, Text} from '@chakra-ui/react' |
| 9 | +import PropTypes from 'prop-types' |
3 | 10 |
|
4 | | -const steps = [ |
5 | | - "Ordered", |
6 | | - "Dispatched", |
7 | | - "Out for delivery", |
8 | | - "Delivered" |
9 | | -]; |
| 11 | +const steps = ['Ordered', 'Dispatched', 'Out for delivery', 'Delivered'] |
10 | 12 |
|
11 | | -const ProgressTracker = ({ currentStepLabel }) => { |
12 | | - const theme = useTheme(); |
13 | | - // Layout constants |
14 | | - const n = steps.length; |
15 | | - const width = 320; |
16 | | - const height = 64; |
17 | | - const chevronWidth = 24; |
18 | | - const radius = 32; |
| 13 | +const ProgressTracker = ({currentStepLabel}) => { |
| 14 | + const theme = useTheme() |
| 15 | + // Layout constants |
| 16 | + const n = steps.length |
| 17 | + const svgWidth = 1080 |
| 18 | + const svgHeight = 50 |
| 19 | + const chevronWidth = 24 |
| 20 | + const radius = 25 |
19 | 21 |
|
20 | | - // Find the index of the current step label (case-insensitive, trim whitespace) |
21 | | - let currentStep = steps.indexOf(currentStepLabel); |
22 | | - if (currentStep === -1) currentStep = 0; |
| 22 | + // Dynamically calculate step width so all steps + chevrons fit in svgWidth |
| 23 | + const stepWidth = (svgWidth + (n - 1) * chevronWidth) / n |
23 | 24 |
|
24 | | - // Calculate total SVG width (overlap chevrons) |
25 | | - const svgWidth = width + (n - 1) * (width - chevronWidth); |
26 | | - const svgHeight = height; |
| 25 | + // Find the index of the current step label (case-insensitive, trim whitespace) |
| 26 | + let currentStep = steps.findIndex( |
| 27 | + (step) => step.trim().toLowerCase() === (currentStepLabel || '').trim().toLowerCase() |
| 28 | + ) |
| 29 | + if (currentStep === -1) currentStep = 0 |
27 | 30 |
|
28 | | - // Helper to get the x offset for each step (overlap chevrons) |
29 | | - const getStepOffset = (i) => i * (width - chevronWidth); |
| 31 | + // Helper to get the x offset for each step (overlap chevrons) |
| 32 | + const getStepOffset = (i) => i * (stepWidth - chevronWidth) |
30 | 33 |
|
31 | | - // Generate polygons/paths for each step |
32 | | - const renderStepShapes = () => { |
33 | | - const shapes = []; |
34 | | - for (let i = 0; i < n; i++) { |
35 | | - const x = getStepOffset(i); |
36 | | - let path, fill; |
37 | | - if (i === 0) { |
38 | | - // First: rounded left, chevron right (chevron tip overlaps next step) |
39 | | - path = `M ${x + radius},0 |
40 | | - L ${x + width - chevronWidth},0 |
41 | | - L ${x + width},${height / 2} |
42 | | - L ${x + width - chevronWidth},${height} |
43 | | - L ${x + radius},${height} |
44 | | - A ${radius},${radius} 0 0 1 ${x},${height / 2} |
| 34 | + // Generate polygons/paths for each step |
| 35 | + const renderStepShapes = () => { |
| 36 | + const shapes = [] |
| 37 | + for (let i = 0; i < n; i++) { |
| 38 | + const x = getStepOffset(i) |
| 39 | + let path, fill |
| 40 | + if (i === 0) { |
| 41 | + // First: rounded left, chevron right (chevron tip overlaps next step) |
| 42 | + path = `M ${x + radius},0 |
| 43 | + L ${x + stepWidth - chevronWidth},0 |
| 44 | + L ${x + stepWidth},${svgHeight / 2} |
| 45 | + L ${x + stepWidth - chevronWidth},${svgHeight} |
| 46 | + L ${x + radius},${svgHeight} |
| 47 | + A ${radius},${radius} 0 0 1 ${x},${svgHeight / 2} |
45 | 48 | A ${radius},${radius} 0 0 1 ${x + radius},0 |
46 | | - Z`; |
47 | | - } else if (i === n - 1) { |
48 | | - // Last: chevron left, rounded right (no chevron tip on right) |
49 | | - path = `M ${x},0 |
50 | | - L ${x + width - radius},0 |
51 | | - A ${radius},${radius} 0 0 1 ${x + width},${height / 2} |
52 | | - A ${radius},${radius} 0 0 1 ${x + width - radius},${height} |
53 | | - L ${x},${height} |
54 | | - L ${x + chevronWidth},${height / 2} |
55 | | - Z`; |
56 | | - } else { |
57 | | - // Middle: chevron left, chevron right (chevron tip overlaps next step) |
58 | | - path = `M ${x},0 |
59 | | - L ${x + width - chevronWidth},0 |
60 | | - L ${x + width},${height / 2} |
61 | | - L ${x + width - chevronWidth},${height} |
62 | | - L ${x},${height} |
63 | | - L ${x + chevronWidth},${height / 2} |
64 | | - Z`; |
65 | | - } |
66 | | - fill = i <= currentStep ? theme.colors.blue[900] : theme.colors.gray[200]; |
67 | | - shapes.push( |
68 | | - <path key={i} d={path} fill={fill} stroke="white" strokeWidth="2" /> |
69 | | - ); |
| 49 | + Z` |
| 50 | + } else if (i === n - 1) { |
| 51 | + // Last: chevron left, rounded right (no chevron tip on right) |
| 52 | + path = `M ${x},0 |
| 53 | + L ${x + stepWidth - radius},0 |
| 54 | + A ${radius},${radius} 0 0 1 ${x + stepWidth},${svgHeight / 2} |
| 55 | + A ${radius},${radius} 0 0 1 ${x + stepWidth - radius},${svgHeight} |
| 56 | + L ${x},${svgHeight} |
| 57 | + L ${x + chevronWidth},${svgHeight / 2} |
| 58 | + Z` |
| 59 | + } else { |
| 60 | + // Middle: chevron left, chevron right (chevron tip overlaps next step) |
| 61 | + path = `M ${x},0 |
| 62 | + L ${x + stepWidth - chevronWidth},0 |
| 63 | + L ${x + stepWidth},${svgHeight / 2} |
| 64 | + L ${x + stepWidth - chevronWidth},${svgHeight} |
| 65 | + L ${x},${svgHeight} |
| 66 | + L ${x + chevronWidth},${svgHeight / 2} |
| 67 | + Z` |
| 68 | + } |
| 69 | + fill = i <= currentStep ? theme.colors.blue[900] : theme.colors.gray[200] |
| 70 | + shapes.push(<path key={i} d={path} fill={fill} stroke="white" strokeWidth="2" />) |
| 71 | + } |
| 72 | + return shapes |
70 | 73 | } |
71 | | - return shapes; |
72 | | - }; |
73 | 74 |
|
74 | | - // Overlay text for each step (shift overlays for overlap) |
75 | | - const renderStepLabels = () => { |
76 | | - const labels = []; |
77 | | - for (let i = 0; i < n; i++) { |
78 | | - const x = getStepOffset(i); |
79 | | - const labelColor = i <= currentStep ? "white" : theme.colors.blue[900]; |
80 | | - labels.push( |
81 | | - <Box |
82 | | - key={i} |
83 | | - position="absolute" |
84 | | - top={0} |
85 | | - left={`${x}px`} |
86 | | - width={`${width}px`} |
87 | | - height={`${height}px`} |
88 | | - display="flex" |
89 | | - alignItems="center" |
90 | | - justifyContent="center" |
91 | | - pointerEvents="none" |
92 | | - > |
93 | | - <Text color={labelColor} fontWeight="medium" fontSize="xl"> |
94 | | - {steps[i]} |
95 | | - </Text> |
96 | | - </Box> |
97 | | - ); |
| 75 | + // Overlay text for each step (shift overlays for overlap) |
| 76 | + const renderStepLabels = () => { |
| 77 | + const labels = [] |
| 78 | + for (let i = 0; i < n; i++) { |
| 79 | + const x = getStepOffset(i) |
| 80 | + const labelColor = i <= currentStep ? 'white' : theme.colors.blue[900] |
| 81 | + labels.push( |
| 82 | + <Box |
| 83 | + key={i} |
| 84 | + position="absolute" |
| 85 | + top={0} |
| 86 | + left={`calc(${(x / svgWidth) * 100}% )`} |
| 87 | + width={`calc(${(stepWidth / svgWidth) * 100}% )`} |
| 88 | + height="100%" |
| 89 | + display="flex" |
| 90 | + alignItems="center" |
| 91 | + justifyContent="center" |
| 92 | + pointerEvents="none" |
| 93 | + px={[1, 2]} // Add horizontal padding for text |
| 94 | + > |
| 95 | + <Text |
| 96 | + color={labelColor} |
| 97 | + fontWeight="medium" |
| 98 | + fontSize={['xs', 'sm', 'md', 'lg']} |
| 99 | + textAlign="center" |
| 100 | + lineHeight="1.2" |
| 101 | + wordBreak="break-word" |
| 102 | + hyphens="auto" |
| 103 | + maxW="100%" |
| 104 | + > |
| 105 | + {steps[i]} |
| 106 | + </Text> |
| 107 | + </Box> |
| 108 | + ) |
| 109 | + } |
| 110 | + return labels |
98 | 111 | } |
99 | | - return labels; |
100 | | - }; |
101 | 112 |
|
102 | | - return ( |
103 | | - <Box position="relative" width={`${svgWidth}px`} height={`${svgHeight}px`}> |
104 | | - <svg width={svgWidth} height={svgHeight} style={{ display: "block" }}> |
105 | | - {renderStepShapes()} |
106 | | - </svg> |
107 | | - {renderStepLabels()} |
108 | | - </Box> |
109 | | - ); |
110 | | -}; |
| 113 | + return ( |
| 114 | + <Box position="relative" width="100%" maxWidth="1080px" height="50px"> |
| 115 | + <svg |
| 116 | + width="100%" |
| 117 | + height="auto" |
| 118 | + viewBox={`0 0 ${svgWidth} ${svgHeight}`} |
| 119 | + style={{display: 'block'}} |
| 120 | + preserveAspectRatio="none" |
| 121 | + > |
| 122 | + {renderStepShapes()} |
| 123 | + </svg> |
| 124 | + {renderStepLabels()} |
| 125 | + </Box> |
| 126 | + ) |
| 127 | +} |
| 128 | + |
| 129 | +ProgressTracker.propTypes = { |
| 130 | + currentStepLabel: PropTypes.string |
| 131 | +} |
111 | 132 |
|
112 | | -export default ProgressTracker; |
| 133 | +export default ProgressTracker |
0 commit comments