Skip to content

Commit ea9dd19

Browse files
committed
add the Steps widget component
1 parent ee28601 commit ea9dd19

4 files changed

Lines changed: 193 additions & 1 deletion

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { createElement, useState } from 'react'
2+
import './steps.css'
3+
4+
type StepStatus = {
5+
[stepIndex: number]: 'current' | 'completed' | undefined | null
6+
}
7+
8+
export type Step<T> = {
9+
title: string
10+
status?: 'current' | 'completed' | undefined | null
11+
element: React.ComponentType<StepProps<T>>
12+
}
13+
14+
export type StepProps<T> = {
15+
showBackButton?: boolean
16+
moveToNext?: () => void
17+
moveToPrevious?: () => void
18+
tabsData: T
19+
setTabsData: (data: T) => void
20+
}
21+
22+
type StepsProps<T> = {
23+
steps: Array<Step<T>>
24+
skipSteps?: boolean
25+
onFinish: (tabsData: T | undefined) => void
26+
onSkip: () => void
27+
}
28+
29+
export const Steps = <T extends any>({
30+
onFinish,
31+
onSkip,
32+
skipSteps = false,
33+
steps,
34+
}: StepsProps<T>) => {
35+
const [currentStep, setCurrentStep] = useState<number>(0)
36+
const [tabsData, setTabsData] = useState<T>()
37+
const [stepsStatuses, setStepsStatuses] = useState<StepStatus>(
38+
steps.reduce(
39+
(obj: StepStatus, step, index: number) => {
40+
if (step.status) {
41+
obj[index] = step.status
42+
}
43+
44+
return obj
45+
},
46+
{ 0: 'current' }
47+
)
48+
)
49+
50+
const moveToNext = () => {
51+
if (currentStep < steps.length - 1) {
52+
setStepsStatuses((prev) => {
53+
prev[currentStep] = 'completed'
54+
prev[currentStep + 1] = 'current'
55+
return prev
56+
})
57+
setCurrentStep((prev) => prev + 1)
58+
} else {
59+
onFinish(tabsData)
60+
}
61+
}
62+
63+
const moveToPrevious = () => {
64+
if (currentStep > 0) {
65+
setCurrentStep((prev) => prev - 1)
66+
} else if (currentStep === 0) {
67+
onSkip()
68+
}
69+
}
70+
71+
const onStepClicked = (e: React.MouseEvent, index: number) => {
72+
e.preventDefault()
73+
setCurrentStep(index)
74+
}
75+
76+
const renderStep = () => {
77+
const props = {
78+
moveToNext,
79+
moveToPrevious,
80+
tabsData,
81+
setTabsData,
82+
} as unknown as StepProps<T>
83+
84+
const element = createElement(steps[currentStep].element, props)
85+
86+
return element
87+
}
88+
return (
89+
<div>
90+
<nav className="my-8" aria-label="Progress">
91+
<ol role="presentation" className="steps">
92+
{steps.map((_, index) => {
93+
return (
94+
<li key={index} className="wrapper">
95+
{stepsStatuses[index] === 'completed' || stepsStatuses[index] === 'current' ? (
96+
<button onClick={(e) => onStepClicked(e, index)} className="step active">
97+
<span className="stepBadge">{index + 1}</span>
98+
99+
<div className="stepLine">
100+
{index !== steps.length - 1 && <div className="progressLine" />}
101+
</div>
102+
</button>
103+
) : (
104+
<button
105+
onClick={(e) => {
106+
e.preventDefault()
107+
if (skipSteps) {
108+
onStepClicked(e, index)
109+
}
110+
}}
111+
className="step">
112+
<span className="stepBadge">{index + 1}</span>
113+
<div className="stepLine">
114+
{index !== steps.length - 1 && <div className="progressLine" />}
115+
</div>
116+
</button>
117+
)}
118+
</li>
119+
)
120+
})}
121+
</ol>
122+
</nav>
123+
<div className="bg-white shadow overflow-hidden sm:rounded-lg px-8 py-4">{renderStep()}</div>
124+
</div>
125+
)
126+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./Steps"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
.steps {
2+
display: flex;
3+
align-items: center;
4+
justify-content: center;
5+
flex-direction: row;
6+
list-style-type: none;
7+
padding: 0;
8+
margin: 0;
9+
}
10+
.steps .wrapper {
11+
position: relative;
12+
}
13+
.step {
14+
position: relative;
15+
display: flex;
16+
flex-direction: row;
17+
align-items: center;
18+
justify-content: center;
19+
border: none;
20+
background: none;
21+
}
22+
23+
.step .stepBadge {
24+
width: 38px;
25+
height: 38px;
26+
background-color: var(--step-normal-background-color);
27+
border-radius: 100%;
28+
display: flex;
29+
justify-content: center;
30+
align-items: center;
31+
color: var(--step-normal-text-color);
32+
font-size: 16px;
33+
border: 1px solid var(--step-normal-border-color);
34+
font-weight: bold;
35+
}
36+
.step .stepBadge:hover {
37+
opacity: 0.9;
38+
cursor: pointer;
39+
}
40+
41+
.step.active .stepBadge {
42+
background-color: var(--step-active-background-color);
43+
color: var(--step-active-text-color);
44+
border: 1px solid var(--step-active-border-color);
45+
}
46+
47+
.step .stepLine {
48+
display: flex;
49+
flex-direction: row;
50+
align-items: center;
51+
}
52+
53+
.step .stepLine .progressLine {
54+
position: relative;
55+
height: 2px;
56+
width: 80px;
57+
background-color: var(--step-normal-border-color);
58+
margin-left: 12px;
59+
margin-right: 2px;
60+
border-radius: 50px;
61+
}
62+
.step.active .stepLine .progressLine {
63+
background-color: var(--step-active-background-color);
64+
}

src/components/Elements/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export * from "./CardWithActions"
88
export * from "./ClickableItem"
99
export * from "./FloatingFilter"
1010
export * from "./InlineTextFilter"
11-
export * from "./ChipsSet"
11+
export * from "./ChipsSet"
12+
export * from "./Steps"

0 commit comments

Comments
 (0)