@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,572 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { StepperItem } from "./StepperItem";
|
|
4
|
+
import { StepperItemProps } from "./stepperItem.types";
|
|
5
|
+
import { StepperContext } from "../StepperContext";
|
|
6
|
+
|
|
7
|
+
const makeSut = (
|
|
8
|
+
props: StepperItemProps,
|
|
9
|
+
totalSteps?: number,
|
|
10
|
+
activeStep?: number,
|
|
11
|
+
selectedStep?: number,
|
|
12
|
+
onSelect?: (step: number) => void
|
|
13
|
+
) => {
|
|
14
|
+
const defaultTotalSteps = totalSteps ?? 3;
|
|
15
|
+
const defaultActiveStep = activeStep ?? 0;
|
|
16
|
+
const defaultSelectedStep = selectedStep ?? 0;
|
|
17
|
+
render(
|
|
18
|
+
<StepperContext.Provider
|
|
19
|
+
value={{ totalSteps: defaultTotalSteps, activeStep: defaultActiveStep, selectedStep: defaultSelectedStep, onSelect }}
|
|
20
|
+
>
|
|
21
|
+
<StepperItem {...props} data-testid="stepper-item" />
|
|
22
|
+
</StepperContext.Provider>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("GIVEN <Stepper.Item />", () => {
|
|
27
|
+
describe("WHEN rendered", () => {
|
|
28
|
+
it("THEN should render the step label", () => {
|
|
29
|
+
makeSut({
|
|
30
|
+
step: 1,
|
|
31
|
+
label: "Test step",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(screen.getByText("Test step")).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("THEN should display step number for started state", () => {
|
|
38
|
+
makeSut(
|
|
39
|
+
{
|
|
40
|
+
step: 2,
|
|
41
|
+
label: "Started step",
|
|
42
|
+
},
|
|
43
|
+
3,
|
|
44
|
+
2
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText("3")).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("THEN should display step number for pending state", () => {
|
|
51
|
+
makeSut(
|
|
52
|
+
{
|
|
53
|
+
step: 3,
|
|
54
|
+
label: "Pending step",
|
|
55
|
+
},
|
|
56
|
+
3,
|
|
57
|
+
1
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText("4")).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("THEN should display check icon for completed state", () => {
|
|
64
|
+
makeSut(
|
|
65
|
+
{
|
|
66
|
+
step: 1,
|
|
67
|
+
label: "Completed step",
|
|
68
|
+
},
|
|
69
|
+
3,
|
|
70
|
+
2,
|
|
71
|
+
2
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Check that step number is not displayed when completed
|
|
75
|
+
expect(screen.queryByText("1")).toBeNull();
|
|
76
|
+
// Check icon should be present (testing by checking for icon presence)
|
|
77
|
+
const iconContainer = screen
|
|
78
|
+
.getByTestId("stepper-item")
|
|
79
|
+
.querySelector('[class*="item__icon"]');
|
|
80
|
+
expect(iconContainer).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("THEN should display step number for selected state", () => {
|
|
84
|
+
makeSut(
|
|
85
|
+
{
|
|
86
|
+
step: 4,
|
|
87
|
+
label: "Selected step",
|
|
88
|
+
},
|
|
89
|
+
5,
|
|
90
|
+
1,
|
|
91
|
+
4
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(screen.getByText("5")).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("THEN should render without label", () => {
|
|
98
|
+
makeSut(
|
|
99
|
+
{
|
|
100
|
+
step: 0,
|
|
101
|
+
},
|
|
102
|
+
3,
|
|
103
|
+
0
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
107
|
+
expect(stepperItem).toBeDefined();
|
|
108
|
+
expect(screen.getByText("1")).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("THEN should render with empty label", () => {
|
|
112
|
+
makeSut(
|
|
113
|
+
{
|
|
114
|
+
step: 0,
|
|
115
|
+
label: "",
|
|
116
|
+
},
|
|
117
|
+
3,
|
|
118
|
+
0
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(screen.getByTestId("stepper-item")).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("THEN should apply custom props", () => {
|
|
125
|
+
makeSut({
|
|
126
|
+
step: 0,
|
|
127
|
+
label: "Test",
|
|
128
|
+
"aria-label": "Custom aria label",
|
|
129
|
+
className: "custom-class",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
133
|
+
expect(stepperItem).toHaveAttribute("aria-label", "Custom aria label");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("WHEN step is not the last step", () => {
|
|
138
|
+
it("THEN should render connecting line", () => {
|
|
139
|
+
makeSut(
|
|
140
|
+
{
|
|
141
|
+
step: 0,
|
|
142
|
+
label: "First step",
|
|
143
|
+
},
|
|
144
|
+
3
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
148
|
+
const line = stepperItem.querySelector('[class*="item__line"]');
|
|
149
|
+
expect(line).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("WHEN step is the last step", () => {
|
|
154
|
+
it("THEN should not render connecting line", () => {
|
|
155
|
+
makeSut(
|
|
156
|
+
{
|
|
157
|
+
step: 2,
|
|
158
|
+
label: "Last step",
|
|
159
|
+
},
|
|
160
|
+
3
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
164
|
+
const line = stepperItem.querySelector('[class*="item__line"]');
|
|
165
|
+
expect(line).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("THEN should not render line when step equals totalSteps minus 1", () => {
|
|
169
|
+
makeSut(
|
|
170
|
+
{
|
|
171
|
+
step: 2,
|
|
172
|
+
label: "Last step",
|
|
173
|
+
},
|
|
174
|
+
3
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const container = screen.getByTestId("stepper-item").parentElement;
|
|
178
|
+
const line = container?.querySelector('[class*="item__line"]');
|
|
179
|
+
expect(line).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("WHEN step is clickable", () => {
|
|
184
|
+
it("THEN should call onSelect when clicked", () => {
|
|
185
|
+
const onSelectMock = jest.fn();
|
|
186
|
+
makeSut(
|
|
187
|
+
{
|
|
188
|
+
step: 0,
|
|
189
|
+
label: "Clickable step",
|
|
190
|
+
},
|
|
191
|
+
3,
|
|
192
|
+
2,
|
|
193
|
+
2,
|
|
194
|
+
onSelectMock
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
198
|
+
fireEvent.click(stepperItem);
|
|
199
|
+
|
|
200
|
+
expect(onSelectMock).toHaveBeenCalledWith(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("THEN should call onSelect when Enter key is pressed", () => {
|
|
204
|
+
const onSelectMock = jest.fn();
|
|
205
|
+
makeSut(
|
|
206
|
+
{
|
|
207
|
+
step: 0,
|
|
208
|
+
label: "Clickable step",
|
|
209
|
+
},
|
|
210
|
+
3,
|
|
211
|
+
2,
|
|
212
|
+
2,
|
|
213
|
+
onSelectMock
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
217
|
+
fireEvent.keyDown(stepperItem, { key: "Enter" });
|
|
218
|
+
|
|
219
|
+
expect(onSelectMock).toHaveBeenCalledWith(0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("THEN should call onSelect when Space key is pressed", () => {
|
|
223
|
+
const onSelectMock = jest.fn();
|
|
224
|
+
makeSut(
|
|
225
|
+
{
|
|
226
|
+
step: 0,
|
|
227
|
+
label: "Clickable step",
|
|
228
|
+
},
|
|
229
|
+
3,
|
|
230
|
+
2,
|
|
231
|
+
2,
|
|
232
|
+
onSelectMock
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
236
|
+
fireEvent.keyDown(stepperItem, { key: " " });
|
|
237
|
+
|
|
238
|
+
expect(onSelectMock).toHaveBeenCalledWith(0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("THEN should set proper accessibility attributes", () => {
|
|
242
|
+
makeSut(
|
|
243
|
+
{
|
|
244
|
+
step: 0,
|
|
245
|
+
label: "Clickable step",
|
|
246
|
+
},
|
|
247
|
+
3,
|
|
248
|
+
2,
|
|
249
|
+
2,
|
|
250
|
+
jest.fn()
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
254
|
+
expect(stepperItem.getAttribute("role")).toBe("button");
|
|
255
|
+
expect(stepperItem.getAttribute("tabIndex")).toBe("0");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("WHEN step is not clickable", () => {
|
|
262
|
+
it("THEN should not call onSelect when clicked", () => {
|
|
263
|
+
const onSelectMock = jest.fn();
|
|
264
|
+
makeSut(
|
|
265
|
+
{
|
|
266
|
+
step: 3,
|
|
267
|
+
label: "Non-clickable step",
|
|
268
|
+
},
|
|
269
|
+
3,
|
|
270
|
+
1,
|
|
271
|
+
1,
|
|
272
|
+
onSelectMock
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
276
|
+
fireEvent.click(stepperItem);
|
|
277
|
+
|
|
278
|
+
expect(onSelectMock).not.toHaveBeenCalled();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("THEN should have tabIndex -1 when not clickable", () => {
|
|
282
|
+
makeSut(
|
|
283
|
+
{
|
|
284
|
+
step: 3,
|
|
285
|
+
label: "Non-clickable step",
|
|
286
|
+
},
|
|
287
|
+
3,
|
|
288
|
+
1,
|
|
289
|
+
1
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
293
|
+
expect(stepperItem.getAttribute("tabIndex")).toBe("-1");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("THEN should not respond to keyboard when pending", () => {
|
|
297
|
+
const onSelectMock = jest.fn();
|
|
298
|
+
makeSut(
|
|
299
|
+
{
|
|
300
|
+
step: 2,
|
|
301
|
+
label: "Pending step",
|
|
302
|
+
},
|
|
303
|
+
3,
|
|
304
|
+
1,
|
|
305
|
+
1,
|
|
306
|
+
onSelectMock
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
310
|
+
fireEvent.keyDown(stepperItem, { key: "Enter" });
|
|
311
|
+
fireEvent.keyDown(stepperItem, { key: " " });
|
|
312
|
+
|
|
313
|
+
expect(onSelectMock).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("THEN should not respond to keyboard when selected", () => {
|
|
317
|
+
const onSelectMock = jest.fn();
|
|
318
|
+
makeSut(
|
|
319
|
+
{
|
|
320
|
+
step: 1,
|
|
321
|
+
label: "Selected step",
|
|
322
|
+
},
|
|
323
|
+
3,
|
|
324
|
+
2,
|
|
325
|
+
1,
|
|
326
|
+
onSelectMock
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
330
|
+
fireEvent.keyDown(stepperItem, { key: "Enter" });
|
|
331
|
+
fireEvent.keyDown(stepperItem, { key: " " });
|
|
332
|
+
|
|
333
|
+
expect(onSelectMock).not.toHaveBeenCalled();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("THEN should have tabIndex -1 when no onSelect handler", () => {
|
|
337
|
+
makeSut(
|
|
338
|
+
{
|
|
339
|
+
step: 0,
|
|
340
|
+
label: "Step without handler",
|
|
341
|
+
},
|
|
342
|
+
3,
|
|
343
|
+
1,
|
|
344
|
+
1
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
348
|
+
expect(stepperItem.getAttribute("tabIndex")).toBe("-1");
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("WHEN keyboard navigation is used", () => {
|
|
353
|
+
it("THEN should ignore other keyboard keys", () => {
|
|
354
|
+
const onSelectMock = jest.fn();
|
|
355
|
+
makeSut(
|
|
356
|
+
{
|
|
357
|
+
step: 0,
|
|
358
|
+
label: "Completed step",
|
|
359
|
+
},
|
|
360
|
+
3,
|
|
361
|
+
2,
|
|
362
|
+
2,
|
|
363
|
+
onSelectMock
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
367
|
+
fireEvent.keyDown(stepperItem, { key: "Tab" });
|
|
368
|
+
fireEvent.keyDown(stepperItem, { key: "Escape" });
|
|
369
|
+
fireEvent.keyDown(stepperItem, { key: "ArrowRight" });
|
|
370
|
+
|
|
371
|
+
expect(onSelectMock).not.toHaveBeenCalled();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("THEN should handle keyboard event without preventDefault", () => {
|
|
375
|
+
const onSelectMock = jest.fn();
|
|
376
|
+
makeSut(
|
|
377
|
+
{
|
|
378
|
+
step: 0,
|
|
379
|
+
label: "Completed step",
|
|
380
|
+
},
|
|
381
|
+
3,
|
|
382
|
+
2,
|
|
383
|
+
2,
|
|
384
|
+
onSelectMock
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
388
|
+
fireEvent.keyDown(stepperItem, { key: "Tab" });
|
|
389
|
+
|
|
390
|
+
expect(onSelectMock).not.toHaveBeenCalled();
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe("WHEN step states are determined", () => {
|
|
395
|
+
it("THEN should correctly identify selected state", () => {
|
|
396
|
+
makeSut(
|
|
397
|
+
{
|
|
398
|
+
step: 1,
|
|
399
|
+
label: "Selected step",
|
|
400
|
+
},
|
|
401
|
+
3,
|
|
402
|
+
2,
|
|
403
|
+
1
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
407
|
+
const icon = stepperItem.querySelector('[class*="item__icon_selected"]');
|
|
408
|
+
expect(icon).toBeDefined();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("THEN should correctly identify started state", () => {
|
|
412
|
+
makeSut(
|
|
413
|
+
{
|
|
414
|
+
step: 1,
|
|
415
|
+
label: "Started step",
|
|
416
|
+
},
|
|
417
|
+
3,
|
|
418
|
+
1,
|
|
419
|
+
0
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
423
|
+
const icon = stepperItem.querySelector('[class*="item__icon_started"]');
|
|
424
|
+
expect(icon).toBeDefined();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("THEN should correctly identify completed state", () => {
|
|
428
|
+
makeSut(
|
|
429
|
+
{
|
|
430
|
+
step: 0,
|
|
431
|
+
label: "Completed step",
|
|
432
|
+
},
|
|
433
|
+
3,
|
|
434
|
+
2,
|
|
435
|
+
2
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
439
|
+
const icon = stepperItem.querySelector('[class*="item__icon_completed"]');
|
|
440
|
+
expect(icon).toBeDefined();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("THEN should correctly identify pending state", () => {
|
|
444
|
+
makeSut(
|
|
445
|
+
{
|
|
446
|
+
step: 2,
|
|
447
|
+
label: "Pending step",
|
|
448
|
+
},
|
|
449
|
+
3,
|
|
450
|
+
1,
|
|
451
|
+
1
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
455
|
+
const icon = stepperItem.querySelector('[class*="item__icon_pending"]');
|
|
456
|
+
expect(icon).toBeDefined();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("THEN should apply disabled class for pending steps", () => {
|
|
460
|
+
makeSut(
|
|
461
|
+
{
|
|
462
|
+
step: 2,
|
|
463
|
+
label: "Pending step",
|
|
464
|
+
},
|
|
465
|
+
3,
|
|
466
|
+
1,
|
|
467
|
+
1
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
471
|
+
expect(stepperItem.className).toContain("item__disabled");
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("THEN should apply correct label classes for each state", () => {
|
|
475
|
+
render(
|
|
476
|
+
<StepperContext.Provider
|
|
477
|
+
value={{
|
|
478
|
+
totalSteps: 4,
|
|
479
|
+
activeStep: 2,
|
|
480
|
+
selectedStep: 1,
|
|
481
|
+
onSelect: jest.fn(),
|
|
482
|
+
}}
|
|
483
|
+
>
|
|
484
|
+
<StepperItem
|
|
485
|
+
step={0}
|
|
486
|
+
label="Completed"
|
|
487
|
+
data-testid="completed-item"
|
|
488
|
+
/>
|
|
489
|
+
<StepperItem step={1} label="Selected" data-testid="selected-item" />
|
|
490
|
+
<StepperItem step={2} label="Started" data-testid="started-item" />
|
|
491
|
+
<StepperItem step={3} label="Pending" data-testid="pending-item" />
|
|
492
|
+
</StepperContext.Provider>
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
expect(
|
|
496
|
+
screen
|
|
497
|
+
.getByTestId("completed-item")
|
|
498
|
+
.querySelector('[class*="item__label_completed"]')
|
|
499
|
+
).toBeDefined();
|
|
500
|
+
expect(
|
|
501
|
+
screen
|
|
502
|
+
.getByTestId("selected-item")
|
|
503
|
+
.querySelector('[class*="item__label_selected"]')
|
|
504
|
+
).toBeDefined();
|
|
505
|
+
expect(
|
|
506
|
+
screen
|
|
507
|
+
.getByTestId("started-item")
|
|
508
|
+
.querySelector('[class*="item__label_started"]')
|
|
509
|
+
).toBeDefined();
|
|
510
|
+
expect(
|
|
511
|
+
screen
|
|
512
|
+
.getByTestId("pending-item")
|
|
513
|
+
.querySelector('[class*="item__label_pending"]')
|
|
514
|
+
).toBeDefined();
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe("WHEN rendered with different total steps", () => {
|
|
519
|
+
it("THEN should handle single step scenario", () => {
|
|
520
|
+
makeSut(
|
|
521
|
+
{
|
|
522
|
+
step: 0,
|
|
523
|
+
label: "Only step",
|
|
524
|
+
},
|
|
525
|
+
1,
|
|
526
|
+
0,
|
|
527
|
+
0
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const container = screen.getByTestId("stepper-item").parentElement;
|
|
531
|
+
const line = container?.querySelector('[class*="item__line"]');
|
|
532
|
+
expect(line).toBeNull();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("THEN should handle zero-based step numbering", () => {
|
|
536
|
+
makeSut(
|
|
537
|
+
{
|
|
538
|
+
step: 0,
|
|
539
|
+
label: "First step",
|
|
540
|
+
},
|
|
541
|
+
3,
|
|
542
|
+
0,
|
|
543
|
+
0
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
expect(screen.getByText("1")).toBeDefined();
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe("WHEN StepperItem has displayName", () => {
|
|
551
|
+
it("THEN should have correct displayName", () => {
|
|
552
|
+
expect(StepperItem.displayName).toBe("Stepper.Item");
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
describe("WHEN interaction happens without event", () => {
|
|
557
|
+
it("THEN should handle click without onSelect", () => {
|
|
558
|
+
makeSut(
|
|
559
|
+
{
|
|
560
|
+
step: 0,
|
|
561
|
+
label: "Step without handler",
|
|
562
|
+
},
|
|
563
|
+
3,
|
|
564
|
+
1,
|
|
565
|
+
1
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
const stepperItem = screen.getByTestId("stepper-item");
|
|
569
|
+
expect(() => fireEvent.click(stepperItem)).not.toThrow();
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Stepper } from "../../Stepper";
|
|
4
|
+
|
|
5
|
+
const currentStep = 0;
|
|
6
|
+
|
|
7
|
+
const meta: Meta<
|
|
8
|
+
typeof Stepper.Item & { currentStep: number; totalSteps: number }
|
|
9
|
+
> = {
|
|
10
|
+
title: "Composite/Stepper/Stepper.Item",
|
|
11
|
+
component: Stepper.Item,
|
|
12
|
+
render: (args) => (
|
|
13
|
+
<Stepper activeStep={currentStep}>
|
|
14
|
+
<Stepper.Item {...args} />
|
|
15
|
+
</Stepper>
|
|
16
|
+
),
|
|
17
|
+
argTypes: {
|
|
18
|
+
label: {
|
|
19
|
+
control: "text",
|
|
20
|
+
description: "The label text for the step",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
tags: ["autodocs"],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof Stepper.Item>;
|
|
28
|
+
|
|
29
|
+
export const Selected: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
label: "Selected step",
|
|
32
|
+
},
|
|
33
|
+
render: (args) => (
|
|
34
|
+
<Stepper activeStep={0}>
|
|
35
|
+
<Stepper.Item {...args} />
|
|
36
|
+
</Stepper>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Completed: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
label: "Completed step",
|
|
43
|
+
},
|
|
44
|
+
render: (args) => (
|
|
45
|
+
<Stepper activeStep={1} onSelectStep={() => undefined} selectedStep={1}>
|
|
46
|
+
<Stepper.Item {...args} />
|
|
47
|
+
</Stepper>
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const Pending: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
label: "Pending step",
|
|
54
|
+
},
|
|
55
|
+
render: (args) => (
|
|
56
|
+
<Stepper activeStep={-1}>
|
|
57
|
+
<Stepper.Item {...args} />
|
|
58
|
+
</Stepper>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents the visual state of a step
|
|
5
|
+
*/
|
|
6
|
+
export type StepState = "completed" | "started" | "pending";
|
|
7
|
+
|
|
8
|
+
export interface StepperItemProperties {
|
|
9
|
+
/**
|
|
10
|
+
* The step number (0-based index) for this item.
|
|
11
|
+
* This is automatically assigned by the parent Stepper component.
|
|
12
|
+
*/
|
|
13
|
+
step: number;
|
|
14
|
+
/**
|
|
15
|
+
* The label text to display for this step
|
|
16
|
+
*/
|
|
17
|
+
label?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type StepperItemProps = StepperItemProperties &
|
|
21
|
+
HTMLAttributes<HTMLDivElement>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { StepperItem } from "./StepperItem";
|
|
2
|
+
export { StepperCard } from "./StepperCard";
|
|
3
|
+
export { StepperContext } from "./StepperContext";
|
|
4
|
+
export type { StepperItemProps, StepperItemProperties, StepState } from "./StepperItem";
|
|
5
|
+
export type { StepperCardProps } from "./StepperCard";
|
|
6
|
+
export type { StepperContextValue } from "./StepperContext";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { Stepper } from "./Stepper";
|
|
2
|
+
export type {
|
|
3
|
+
StepperProps,
|
|
4
|
+
StepperItemProps,
|
|
5
|
+
StepState,
|
|
6
|
+
StepperProperties,
|
|
7
|
+
StepperComponents,
|
|
8
|
+
BaseStepperProperties,
|
|
9
|
+
ControlledStepperProperties
|
|
10
|
+
} from "./stepper.types";
|
|
11
|
+
export type { StepperContextValue } from "./components/StepperContext";
|
|
12
|
+
export { isControlled } from "./stepper.definitions";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ControlledStepperProperties } from "./stepper.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the stepper is in controlled mode.
|
|
5
|
+
* @param props - The props of the stepper
|
|
6
|
+
* @returns True if the stepper is controlled, false otherwise
|
|
7
|
+
*/
|
|
8
|
+
export const isControlled = (
|
|
9
|
+
props: any
|
|
10
|
+
): props is ControlledStepperProperties =>
|
|
11
|
+
"selectedStep" in props && "onSelectStep" in props;
|