Skip to content

Commit 85e18ca

Browse files
order tracker
1 parent 3802d26 commit 85e18ca

File tree

2 files changed

+189
-100
lines changed

2 files changed

+189
-100
lines changed
Lines changed: 121 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,133 @@
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'
310

4-
const steps = [
5-
"Ordered",
6-
"Dispatched",
7-
"Out for delivery",
8-
"Delivered"
9-
];
11+
const steps = ['Ordered', 'Dispatched', 'Out for delivery', 'Delivered']
1012

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
1921

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
2324

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
2730

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)
3033

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}
4548
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
7073
}
71-
return shapes;
72-
};
7374

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
98111
}
99-
return labels;
100-
};
101112

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+
}
111132

112-
export default ProgressTracker;
133+
export default ProgressTracker
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 {screen} from '@testing-library/react'
9+
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
10+
import ProgressTracker from '.'
11+
12+
describe('ProgressTracker', () => {
13+
beforeEach(() => jest.clearAllMocks())
14+
15+
test('renders all step labels', () => {
16+
renderWithProviders(<ProgressTracker />)
17+
18+
expect(screen.getByText('Ordered')).toBeInTheDocument()
19+
expect(screen.getByText('Dispatched')).toBeInTheDocument()
20+
expect(screen.getByText('Out for delivery')).toBeInTheDocument()
21+
expect(screen.getByText('Delivered')).toBeInTheDocument()
22+
})
23+
24+
test('renders step labels with responsive font sizes', () => {
25+
renderWithProviders(<ProgressTracker />)
26+
27+
const labels = screen.getAllByText(/Ordered|Dispatched|Out for delivery|Delivered/)
28+
labels.forEach((label) => {
29+
// Check that fontSize is an array (responsive values)
30+
const computedStyle = window.getComputedStyle(label)
31+
// Note: In test environment, we can't easily test responsive values
32+
// but we can verify the component renders without errors
33+
expect(label).toBeInTheDocument()
34+
})
35+
})
36+
37+
test('renders SVG element', () => {
38+
renderWithProviders(<ProgressTracker />)
39+
40+
const svg = document.querySelector('svg')
41+
expect(svg).toBeInTheDocument()
42+
expect(svg).toHaveAttribute('viewBox', '0 0 1080 50')
43+
expect(svg).toHaveAttribute('width', '100%')
44+
expect(svg).toHaveAttribute('preserveAspectRatio', 'none')
45+
})
46+
47+
test('renders all step paths in SVG', () => {
48+
renderWithProviders(<ProgressTracker />)
49+
50+
const svg = document.querySelector('svg')
51+
const paths = svg.querySelectorAll('path')
52+
expect(paths).toHaveLength(4) // Should have 4 paths for 4 steps
53+
})
54+
55+
test('renders with different currentStepLabel props', () => {
56+
// Test with valid step label
57+
const {rerender} = renderWithProviders(<ProgressTracker currentStepLabel="Dispatched" />)
58+
expect(screen.getByText('Dispatched')).toBeInTheDocument()
59+
60+
// Test with invalid step label (should default to first step)
61+
rerender(<ProgressTracker currentStepLabel="Invalid Step" />)
62+
expect(screen.getByText('Ordered')).toBeInTheDocument()
63+
64+
// Test with empty string
65+
rerender(<ProgressTracker currentStepLabel="" />)
66+
expect(screen.getByText('Ordered')).toBeInTheDocument()
67+
})
68+
})

0 commit comments

Comments
 (0)