@nimbus-ds/stepper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +24 -0
- package/CHANGELOG.md +3 -0
- package/README.md +125 -0
- package/dist/CHANGELOG.md +3 -0
- package/dist/README.md +125 -0
- package/dist/index.d.ts +850 -0
- package/dist/index.js +1 -0
- package/package.json +48 -0
- package/src/Stepper.tsx +114 -0
- package/src/components/StepperCard/StepperCard.tsx +15 -0
- package/src/components/StepperCard/index.ts +2 -0
- package/src/components/StepperCard/stepperCard.spec.tsx +47 -0
- package/src/components/StepperCard/stepperCard.stories.tsx +39 -0
- package/src/components/StepperCard/stepperCard.types.ts +11 -0
- package/src/components/StepperContext/StepperContext.tsx +9 -0
- package/src/components/StepperContext/index.ts +2 -0
- package/src/components/StepperContext/stepperContext.spec.tsx +54 -0
- package/src/components/StepperContext/stepperContext.types.ts +21 -0
- package/src/components/StepperItem/StepperItem.definitions.ts +11 -0
- package/src/components/StepperItem/StepperItem.tsx +111 -0
- package/src/components/StepperItem/index.ts +2 -0
- package/src/components/StepperItem/stepperItem.spec.tsx +572 -0
- package/src/components/StepperItem/stepperItem.stories.tsx +60 -0
- package/src/components/StepperItem/stepperItem.types.ts +21 -0
- package/src/components/index.ts +6 -0
- package/src/index.ts +12 -0
- package/src/stepper.definitions.ts +11 -0
- package/src/stepper.docs.json +68 -0
- package/src/stepper.spec.tsx +344 -0
- package/src/stepper.stories.tsx +145 -0
- package/src/stepper.types.ts +48 -0
- package/tsconfig.json +4 -0
- package/webpack.config.ts +11 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "stepper",
|
|
3
|
+
"name": "Stepper",
|
|
4
|
+
"totalProps": 4,
|
|
5
|
+
"packageName": "@nimbus-ds/stepper",
|
|
6
|
+
"version": "0.0.0",
|
|
7
|
+
"docLink": "https://nimbus.nuvemshop.com.br/documentation/composite-components/stepper",
|
|
8
|
+
"props": [
|
|
9
|
+
{
|
|
10
|
+
"description": "The currently active step (0-based index).\nSteps before this will be marked as completed.",
|
|
11
|
+
"type": "number",
|
|
12
|
+
"title": "activeStep",
|
|
13
|
+
"required": true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"description": "The content of the stepper (StepperItem components).\nTotal steps will be calculated automatically based on the number of children.",
|
|
17
|
+
"type": "React.ReactNode",
|
|
18
|
+
"title": "children",
|
|
19
|
+
"required": true
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"description": "The currently selected step (0-based index).\nThis step will be visually highlighted to show user selection.",
|
|
23
|
+
"type": "number",
|
|
24
|
+
"title": "selectedStep",
|
|
25
|
+
"required": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"description": "Callback called when a step is selected.\nReceives the step number (0-based index) as parameter.",
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"propertyOrder": [],
|
|
32
|
+
"title": "onSelectStep",
|
|
33
|
+
"required": true
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"subComponents": [
|
|
37
|
+
{
|
|
38
|
+
"name": "Stepper.Item",
|
|
39
|
+
"totalProps": 2,
|
|
40
|
+
"props": [
|
|
41
|
+
{
|
|
42
|
+
"description": "The step number (0-based index) for this item.\nThis is automatically assigned by the parent Stepper component.",
|
|
43
|
+
"type": "number",
|
|
44
|
+
"title": "step",
|
|
45
|
+
"required": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"description": "The label text to display for this step",
|
|
49
|
+
"type": "string",
|
|
50
|
+
"title": "label",
|
|
51
|
+
"required": false
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "Stepper.Card",
|
|
57
|
+
"totalProps": 1,
|
|
58
|
+
"props": [
|
|
59
|
+
{
|
|
60
|
+
"description": "The content to be rendered inside the card container",
|
|
61
|
+
"type": "React.ReactNode",
|
|
62
|
+
"title": "children",
|
|
63
|
+
"required": true
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { Stepper } from "./Stepper";
|
|
5
|
+
import {
|
|
6
|
+
BaseStepperProperties,
|
|
7
|
+
ControlledStepperProperties,
|
|
8
|
+
} from "./stepper.types";
|
|
9
|
+
|
|
10
|
+
const makeUncontrolledSut = (props: BaseStepperProperties) => {
|
|
11
|
+
render(<Stepper {...props} data-testid="stepper-element" />);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const makeControlledSut = (props: ControlledStepperProperties) => {
|
|
15
|
+
render(<Stepper {...props} data-testid="stepper-element" />);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe("GIVEN <Stepper />", () => {
|
|
19
|
+
describe("WHEN rendered in uncontrolled mode", () => {
|
|
20
|
+
it("THEN should correctly render the stepper container", () => {
|
|
21
|
+
makeUncontrolledSut({
|
|
22
|
+
activeStep: 2,
|
|
23
|
+
children: [
|
|
24
|
+
<Stepper.Item key="1" label="First step" />,
|
|
25
|
+
<Stepper.Item key="2" label="Second step" />,
|
|
26
|
+
<Stepper.Item key="3" label="Third step" />,
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(screen.getByTestId("stepper-element")).toBeDefined();
|
|
31
|
+
expect(screen.getByText("First step")).toBeDefined();
|
|
32
|
+
expect(screen.getByText("Second step")).toBeDefined();
|
|
33
|
+
expect(screen.getByText("Third step")).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("THEN should correctly set aria attributes", () => {
|
|
37
|
+
makeUncontrolledSut({
|
|
38
|
+
activeStep: 1,
|
|
39
|
+
children: [
|
|
40
|
+
<Stepper.Item key="1" label="Step 1" />,
|
|
41
|
+
<Stepper.Item key="2" label="Step 2" />,
|
|
42
|
+
<Stepper.Item key="3" label="Step 3" />,
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const stepper = screen.getByTestId("stepper-element");
|
|
47
|
+
expect(stepper.getAttribute("role")).toBe("progressbar");
|
|
48
|
+
expect(stepper.getAttribute("aria-valuenow")).toBe("2");
|
|
49
|
+
expect(stepper.getAttribute("aria-valuemin")).toBe("1");
|
|
50
|
+
expect(stepper.getAttribute("aria-valuemax")).toBe("3");
|
|
51
|
+
expect(stepper.getAttribute("aria-label")).toBe("Multi-step process: Step 2 of 3");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("THEN should render with single step", () => {
|
|
55
|
+
makeUncontrolledSut({
|
|
56
|
+
activeStep: 0,
|
|
57
|
+
children: [<Stepper.Item key="1" label="Only step" />],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText("Only step")).toBeDefined();
|
|
61
|
+
const stepper = screen.getByTestId("stepper-element");
|
|
62
|
+
expect(stepper.getAttribute("aria-valuemax")).toBe("1");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("THEN should handle activeStep 0", () => {
|
|
66
|
+
makeUncontrolledSut({
|
|
67
|
+
activeStep: 0,
|
|
68
|
+
children: [
|
|
69
|
+
<Stepper.Item key="1" label="Started step" />,
|
|
70
|
+
<Stepper.Item key="2" label="Pending step" />,
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const stepper = screen.getByTestId("stepper-element");
|
|
75
|
+
expect(stepper.getAttribute("aria-valuenow")).toBe("1");
|
|
76
|
+
expect(stepper.getAttribute("aria-label")).toBe("Multi-step process: Step 1 of 2");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("THEN should handle maximum activeStep", () => {
|
|
80
|
+
makeUncontrolledSut({
|
|
81
|
+
activeStep: 2,
|
|
82
|
+
children: [
|
|
83
|
+
<Stepper.Item key="1" label="Completed" />,
|
|
84
|
+
<Stepper.Item key="2" label="Completed" />,
|
|
85
|
+
<Stepper.Item key="3" label="Started" />,
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const stepper = screen.getByTestId("stepper-element");
|
|
90
|
+
expect(stepper.getAttribute("aria-valuenow")).toBe("3");
|
|
91
|
+
expect(stepper.getAttribute("aria-valuemax")).toBe("3");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("THEN should apply additional props to container", () => {
|
|
95
|
+
makeUncontrolledSut({
|
|
96
|
+
activeStep: 0,
|
|
97
|
+
children: [<Stepper.Item key="1" label="Step" />],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const stepper = screen.getByTestId("stepper-element");
|
|
101
|
+
expect(stepper).toHaveAttribute("role", "progressbar");
|
|
102
|
+
expect(stepper).toHaveAttribute("aria-valuenow", "1");
|
|
103
|
+
expect(stepper).toHaveAttribute("aria-valuemax", "1");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("THEN should update internal state when activeStep changes", () => {
|
|
107
|
+
const { rerender } = render(
|
|
108
|
+
<Stepper activeStep={0} data-testid="stepper-element">
|
|
109
|
+
<Stepper.Item key="1" label="Step 1" />
|
|
110
|
+
<Stepper.Item key="2" label="Step 2" />
|
|
111
|
+
</Stepper>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
let stepper = screen.getByTestId("stepper-element");
|
|
115
|
+
expect(stepper.getAttribute("aria-valuenow")).toBe("1");
|
|
116
|
+
|
|
117
|
+
rerender(
|
|
118
|
+
<Stepper activeStep={1} data-testid="stepper-element">
|
|
119
|
+
<Stepper.Item key="1" label="Step 1" />
|
|
120
|
+
<Stepper.Item key="2" label="Step 2" />
|
|
121
|
+
</Stepper>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
stepper = screen.getByTestId("stepper-element");
|
|
125
|
+
expect(stepper.getAttribute("aria-valuenow")).toBe("2");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("THEN should handle non-React element children gracefully", () => {
|
|
129
|
+
makeUncontrolledSut({
|
|
130
|
+
activeStep: 0,
|
|
131
|
+
children: [
|
|
132
|
+
"text node",
|
|
133
|
+
<Stepper.Item key="1" label="Valid step" />,
|
|
134
|
+
null,
|
|
135
|
+
<Stepper.Item key="2" label="Another step" />,
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(screen.getByText("Valid step")).toBeDefined();
|
|
140
|
+
expect(screen.getByText("Another step")).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("WHEN rendered in controlled mode", () => {
|
|
145
|
+
const mockOnSelectStep = jest.fn();
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
mockOnSelectStep.mockClear();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("THEN should correctly render with controlled state", () => {
|
|
152
|
+
makeControlledSut({
|
|
153
|
+
activeStep: 2,
|
|
154
|
+
selectedStep: 1,
|
|
155
|
+
onSelectStep: mockOnSelectStep,
|
|
156
|
+
children: [
|
|
157
|
+
<Stepper.Item key="1" label="First step" />,
|
|
158
|
+
<Stepper.Item key="2" label="Second step" />,
|
|
159
|
+
<Stepper.Item key="3" label="Third step" />,
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(screen.getByTestId("stepper-element")).toBeDefined();
|
|
164
|
+
expect(screen.getByText("First step")).toBeDefined();
|
|
165
|
+
expect(screen.getByText("Second step")).toBeDefined();
|
|
166
|
+
expect(screen.getByText("Third step")).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("THEN should call onSelectStep when a step is clicked", () => {
|
|
170
|
+
makeControlledSut({
|
|
171
|
+
activeStep: 2,
|
|
172
|
+
selectedStep: 1,
|
|
173
|
+
onSelectStep: mockOnSelectStep,
|
|
174
|
+
children: [
|
|
175
|
+
<Stepper.Item key="1" label="Completed step" />,
|
|
176
|
+
<Stepper.Item key="2" label="Another completed step" />,
|
|
177
|
+
<Stepper.Item key="3" label="Started step" />,
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const stepperItems = screen.getAllByRole("button");
|
|
182
|
+
fireEvent.click(stepperItems[0]);
|
|
183
|
+
|
|
184
|
+
expect(mockOnSelectStep).toHaveBeenCalledWith(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("THEN should not update internal state when activeStep changes", () => {
|
|
188
|
+
const { rerender } = render(
|
|
189
|
+
<Stepper
|
|
190
|
+
activeStep={0}
|
|
191
|
+
selectedStep={0}
|
|
192
|
+
onSelectStep={mockOnSelectStep}
|
|
193
|
+
data-testid="stepper-element"
|
|
194
|
+
>
|
|
195
|
+
<Stepper.Item key="1" label="Step 1" />
|
|
196
|
+
<Stepper.Item key="2" label="Step 2" />
|
|
197
|
+
</Stepper>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
rerender(
|
|
201
|
+
<Stepper
|
|
202
|
+
activeStep={1}
|
|
203
|
+
selectedStep={0}
|
|
204
|
+
onSelectStep={mockOnSelectStep}
|
|
205
|
+
data-testid="stepper-element"
|
|
206
|
+
>
|
|
207
|
+
<Stepper.Item key="1" label="Step 1" />
|
|
208
|
+
<Stepper.Item key="2" label="Step 2" />
|
|
209
|
+
</Stepper>
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(mockOnSelectStep).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("THEN should filter controlled props from container", () => {
|
|
216
|
+
makeControlledSut({
|
|
217
|
+
activeStep: 0,
|
|
218
|
+
selectedStep: 0,
|
|
219
|
+
onSelectStep: mockOnSelectStep,
|
|
220
|
+
children: [<Stepper.Item key="1" label="Step" />],
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const stepper = screen.getByTestId("stepper-element");
|
|
224
|
+
expect(stepper).not.toHaveAttribute("selectedStep");
|
|
225
|
+
expect(stepper).not.toHaveAttribute("onSelectStep");
|
|
226
|
+
expect(stepper).toHaveAttribute("role", "progressbar");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("THEN should handle selectedStep equal to activeStep", () => {
|
|
230
|
+
makeControlledSut({
|
|
231
|
+
activeStep: 1,
|
|
232
|
+
selectedStep: 1,
|
|
233
|
+
onSelectStep: mockOnSelectStep,
|
|
234
|
+
children: [
|
|
235
|
+
<Stepper.Item key="1" label="Completed" />,
|
|
236
|
+
<Stepper.Item key="2" label="Selected started" />,
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const stepperItems = screen.getAllByRole("button");
|
|
241
|
+
fireEvent.click(stepperItems[1]);
|
|
242
|
+
|
|
243
|
+
expect(mockOnSelectStep).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("THEN should handle selectedStep beyond activeStep", () => {
|
|
247
|
+
makeControlledSut({
|
|
248
|
+
activeStep: 1,
|
|
249
|
+
selectedStep: 2,
|
|
250
|
+
onSelectStep: mockOnSelectStep,
|
|
251
|
+
children: [
|
|
252
|
+
<Stepper.Item key="1" label="Completed" />,
|
|
253
|
+
<Stepper.Item key="2" label="Started" />,
|
|
254
|
+
<Stepper.Item key="3" label="Selected pending" />,
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const stepperItems = screen.getAllByRole("button");
|
|
259
|
+
expect(stepperItems).toHaveLength(3);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("WHEN step states are rendered", () => {
|
|
264
|
+
it("THEN should show step numbers for started and pending steps", () => {
|
|
265
|
+
makeUncontrolledSut({
|
|
266
|
+
activeStep: 1,
|
|
267
|
+
children: [
|
|
268
|
+
<Stepper.Item key="1" label="Completed step" />,
|
|
269
|
+
<Stepper.Item key="2" label="Current step" />,
|
|
270
|
+
<Stepper.Item key="3" label="Pending step" />,
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(screen.getByText("2")).toBeDefined();
|
|
275
|
+
expect(screen.getByText("3")).toBeDefined();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("THEN should show completed steps with check icons", () => {
|
|
279
|
+
makeUncontrolledSut({
|
|
280
|
+
activeStep: 2,
|
|
281
|
+
children: [
|
|
282
|
+
<Stepper.Item key="1" label="First completed" />,
|
|
283
|
+
<Stepper.Item key="2" label="Second completed" />,
|
|
284
|
+
<Stepper.Item key="3" label="Current step" />,
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(screen.queryByText("1")).toBeNull();
|
|
289
|
+
expect(screen.queryByText("2")).toBeNull();
|
|
290
|
+
expect(screen.getByText("3")).toBeDefined();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("WHEN Stepper has static properties", () => {
|
|
295
|
+
it("THEN should have correct displayName", () => {
|
|
296
|
+
expect(Stepper.displayName).toBe("Stepper");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("THEN should have Item component with displayName", () => {
|
|
300
|
+
expect(Stepper.Item).toBeDefined();
|
|
301
|
+
expect(Stepper.Item.displayName).toBe("Stepper.Item");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("THEN should have Card component with displayName", () => {
|
|
305
|
+
expect(Stepper.Card).toBeDefined();
|
|
306
|
+
expect(Stepper.Card.displayName).toBe("Stepper.Card");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("WHEN rendered with empty children", () => {
|
|
311
|
+
it("THEN should handle empty children array", () => {
|
|
312
|
+
makeUncontrolledSut({
|
|
313
|
+
activeStep: 0,
|
|
314
|
+
children: [],
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const stepper = screen.getByTestId("stepper-element");
|
|
318
|
+
expect(stepper.getAttribute("aria-valuemax")).toBe("0");
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("WHEN steps are automatically numbered", () => {
|
|
323
|
+
it("THEN should assign correct step indices to children", () => {
|
|
324
|
+
const mockOnSelect = jest.fn();
|
|
325
|
+
render(
|
|
326
|
+
<Stepper
|
|
327
|
+
activeStep={3}
|
|
328
|
+
selectedStep={0}
|
|
329
|
+
onSelectStep={mockOnSelect}
|
|
330
|
+
data-testid="stepper-element"
|
|
331
|
+
>
|
|
332
|
+
<Stepper.Item key="1" label="Step 1" />
|
|
333
|
+
<Stepper.Item key="2" label="Step 2" />
|
|
334
|
+
<Stepper.Item key="3" label="Step 3" />
|
|
335
|
+
</Stepper>
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const buttons = screen.getAllByRole("button");
|
|
339
|
+
fireEvent.click(buttons[1]);
|
|
340
|
+
|
|
341
|
+
expect(mockOnSelect).toHaveBeenCalledWith(1);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { Button } from "@nimbus-ds/button";
|
|
5
|
+
import { Box } from "@nimbus-ds/box";
|
|
6
|
+
import { Text } from "@nimbus-ds/text";
|
|
7
|
+
import { Stepper } from "./Stepper";
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof Stepper> = {
|
|
10
|
+
title: "Composite/Stepper",
|
|
11
|
+
component: Stepper,
|
|
12
|
+
parameters: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"A stepper component for guiding users through multi-step processes.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
argTypes: {
|
|
21
|
+
activeStep: {
|
|
22
|
+
control: { type: "number", min: 0, max: 4 },
|
|
23
|
+
description: "The currently active step (0-based index)",
|
|
24
|
+
},
|
|
25
|
+
selectedStep: {
|
|
26
|
+
control: { type: "number", min: 0, max: 4 },
|
|
27
|
+
description: "The currently selected step (0-based index)",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
tags: ["autodocs"],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
|
|
35
|
+
type Story = StoryObj<typeof Stepper>;
|
|
36
|
+
|
|
37
|
+
const labels = [
|
|
38
|
+
"Select audience",
|
|
39
|
+
"Create content",
|
|
40
|
+
"Define budget",
|
|
41
|
+
"Review",
|
|
42
|
+
"Publish",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const Controlled: Story = {
|
|
46
|
+
render: () => {
|
|
47
|
+
const [selected, setSelected] = useState(0);
|
|
48
|
+
const [activeStep, setActiveStep] = useState(0);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Box display="flex" flexDirection="column" gap="3">
|
|
52
|
+
<Text>Selected step: {selected + 1}</Text>
|
|
53
|
+
<Stepper
|
|
54
|
+
activeStep={activeStep}
|
|
55
|
+
selectedStep={selected}
|
|
56
|
+
onSelectStep={(step: number) => setSelected(step)}
|
|
57
|
+
justifyContent="flex-start"
|
|
58
|
+
>
|
|
59
|
+
{labels.map((label) => (
|
|
60
|
+
<Stepper.Item key={label} label={label} />
|
|
61
|
+
))}
|
|
62
|
+
</Stepper>
|
|
63
|
+
<Button
|
|
64
|
+
onClick={() => {
|
|
65
|
+
setActiveStep(activeStep + 1);
|
|
66
|
+
setSelected(activeStep + 1);
|
|
67
|
+
}}
|
|
68
|
+
disabled={activeStep === labels.length - 1}
|
|
69
|
+
>
|
|
70
|
+
Next
|
|
71
|
+
</Button>
|
|
72
|
+
</Box>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Uncontrolled: Story = {
|
|
78
|
+
render: () => {
|
|
79
|
+
const [activeStep, setActiveStep] = useState(0);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Box display="flex" flexDirection="column" gap="3">
|
|
83
|
+
<Stepper activeStep={activeStep} justifyContent="flex-start">
|
|
84
|
+
{labels.map((label) => (
|
|
85
|
+
<Stepper.Item key={label} label={label} />
|
|
86
|
+
))}
|
|
87
|
+
</Stepper>
|
|
88
|
+
<Button
|
|
89
|
+
onClick={() => setActiveStep(activeStep + 1)}
|
|
90
|
+
disabled={activeStep === labels.length - 1}
|
|
91
|
+
>
|
|
92
|
+
Next
|
|
93
|
+
</Button>
|
|
94
|
+
</Box>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const WithCard: Story = {
|
|
100
|
+
render: () => (
|
|
101
|
+
<Stepper.Card>
|
|
102
|
+
<Stepper activeStep={2}>
|
|
103
|
+
<Stepper.Item label="Setup" />
|
|
104
|
+
<Stepper.Item label="Configuration" />
|
|
105
|
+
<Stepper.Item label="Review" />
|
|
106
|
+
<Stepper.Item label="Deploy" />
|
|
107
|
+
</Stepper>
|
|
108
|
+
</Stepper.Card>
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* For mobile, we don't display the labels, only the icons
|
|
114
|
+
*/
|
|
115
|
+
export const Mobile: Story = {
|
|
116
|
+
render: () => {
|
|
117
|
+
const [activeStep, setActiveStep] = useState(0);
|
|
118
|
+
const [selected, setSelected] = useState(0);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Box display="flex" flexDirection="column" gap="3">
|
|
122
|
+
<Text>Step {selected + 1}: Description</Text>
|
|
123
|
+
<Stepper
|
|
124
|
+
activeStep={activeStep}
|
|
125
|
+
selectedStep={selected}
|
|
126
|
+
onSelectStep={setSelected}
|
|
127
|
+
justifyContent="flex-start"
|
|
128
|
+
>
|
|
129
|
+
{labels.map((label) => (
|
|
130
|
+
<Stepper.Item key={label} />
|
|
131
|
+
))}
|
|
132
|
+
</Stepper>
|
|
133
|
+
<Button
|
|
134
|
+
onClick={() => {
|
|
135
|
+
setActiveStep(activeStep + 1);
|
|
136
|
+
setSelected(activeStep + 1);
|
|
137
|
+
}}
|
|
138
|
+
disabled={activeStep === labels.length - 1}
|
|
139
|
+
>
|
|
140
|
+
Next
|
|
141
|
+
</Button>
|
|
142
|
+
</Box>
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import { BoxProps } from "@nimbus-ds/box";
|
|
3
|
+
import { StepperItemProps } from "./components/StepperItem";
|
|
4
|
+
import { StepperCardProps } from "./components/StepperCard";
|
|
5
|
+
|
|
6
|
+
export interface StepperComponents {
|
|
7
|
+
Item: React.FC<Omit<StepperItemProps, "step">>;
|
|
8
|
+
Card: React.FC<StepperCardProps>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BaseStepperProperties {
|
|
12
|
+
/**
|
|
13
|
+
* The currently active step (0-based index).
|
|
14
|
+
* Steps before this will be marked as completed.
|
|
15
|
+
*/
|
|
16
|
+
activeStep: number;
|
|
17
|
+
/**
|
|
18
|
+
* The content of the stepper (StepperItem components).
|
|
19
|
+
* Total steps will be calculated automatically based on the number of children.
|
|
20
|
+
* @TJS-type React.ReactNode
|
|
21
|
+
*/
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ControlledStepperProperties extends BaseStepperProperties {
|
|
26
|
+
/**
|
|
27
|
+
* The currently selected step (0-based index).
|
|
28
|
+
* This step will be visually highlighted to show user selection.
|
|
29
|
+
*/
|
|
30
|
+
selectedStep: number;
|
|
31
|
+
/**
|
|
32
|
+
* Callback called when a step is selected.
|
|
33
|
+
* Receives the step number (0-based index) as parameter.
|
|
34
|
+
*/
|
|
35
|
+
onSelectStep: (step: number) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Props for the Stepper component, supporting both controlled and uncontrolled modes
|
|
40
|
+
*/
|
|
41
|
+
export type StepperProps = (BaseStepperProperties | ControlledStepperProperties) & Omit<BoxProps, "display" | "flexWrap" | "gap">;
|
|
42
|
+
|
|
43
|
+
// For docs purposes, we need to merge the two types
|
|
44
|
+
export type StepperProperties = BaseStepperProperties & ControlledStepperProperties;
|
|
45
|
+
|
|
46
|
+
// Re-export types from components for convenience
|
|
47
|
+
export type { StepperItemProps, StepState } from "./components/StepperItem";
|
|
48
|
+
export type { StepperCardProps } from "./components/StepperCard";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { configuration } from "@nimbus-ds/webpack/src";
|
|
3
|
+
|
|
4
|
+
const config = {
|
|
5
|
+
output: {
|
|
6
|
+
path: path.resolve(__dirname, "dist"),
|
|
7
|
+
library: "@nimbus-ds/stepper",
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default () => configuration.getConfiguration(config);
|