@indico-data/design-system 2.58.1 → 2.59.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.
Files changed (53) hide show
  1. package/lib/components/index.d.ts +1 -0
  2. package/lib/components/stepper/Stepper.d.ts +2 -0
  3. package/lib/components/stepper/Stepper.stories.d.ts +9 -0
  4. package/lib/components/stepper/__tests__/Stepper.tests.d.ts +1 -0
  5. package/lib/components/stepper/components/BackNavigation.d.ts +6 -0
  6. package/lib/components/stepper/components/Legend.d.ts +2 -0
  7. package/lib/components/stepper/components/NextNavigation.d.ts +8 -0
  8. package/lib/components/stepper/examples/MixedExample.d.ts +1 -0
  9. package/lib/components/stepper/examples/OptionalStepsExample.d.ts +1 -0
  10. package/lib/components/stepper/examples/RequiredStepsExample.d.ts +1 -0
  11. package/lib/components/stepper/examples/commonExample/CommonExample.d.ts +1 -0
  12. package/lib/components/stepper/examples/commonExample/steps/StepOne.d.ts +3 -0
  13. package/lib/components/stepper/examples/commonExample/steps/StepThree.d.ts +3 -0
  14. package/lib/components/stepper/examples/commonExample/steps/StepTwo.d.ts +3 -0
  15. package/lib/components/stepper/examples/constants.d.ts +61 -0
  16. package/lib/components/stepper/index.d.ts +1 -0
  17. package/lib/components/stepper/types.d.ts +24 -0
  18. package/lib/index.css +100 -2
  19. package/lib/index.d.ts +23 -2
  20. package/lib/index.esm.css +100 -2
  21. package/lib/index.esm.js +145 -50
  22. package/lib/index.esm.js.map +1 -1
  23. package/lib/index.js +144 -48
  24. package/lib/index.js.map +1 -1
  25. package/package.json +1 -2
  26. package/src/components/index.ts +1 -0
  27. package/src/components/stepper/Stepper.mdx +140 -0
  28. package/src/components/stepper/Stepper.stories.tsx +196 -0
  29. package/src/components/stepper/Stepper.tsx +85 -0
  30. package/src/components/stepper/__tests__/Stepper.tests.tsx +213 -0
  31. package/src/components/stepper/components/BackNavigation.tsx +22 -0
  32. package/src/components/stepper/components/Legend.tsx +66 -0
  33. package/src/components/stepper/components/NextNavigation.tsx +38 -0
  34. package/src/components/stepper/examples/MixedExample.tsx +140 -0
  35. package/src/components/stepper/examples/OptionalStepsExample.tsx +139 -0
  36. package/src/components/stepper/examples/RequiredStepsExample.tsx +158 -0
  37. package/src/components/stepper/examples/commonExample/CommonExample.tsx +115 -0
  38. package/src/components/stepper/examples/commonExample/steps/StepOne.tsx +57 -0
  39. package/src/components/stepper/examples/commonExample/steps/StepThree.tsx +56 -0
  40. package/src/components/stepper/examples/commonExample/steps/StepTwo.tsx +52 -0
  41. package/src/components/stepper/examples/constants.ts +168 -0
  42. package/src/components/stepper/index.ts +1 -0
  43. package/src/components/stepper/styles/Stepper.scss +131 -0
  44. package/src/components/stepper/types.ts +27 -0
  45. package/src/components/tanstackTable/components/TableBody/TableBody.tsx +2 -1
  46. package/src/components/tanstackTable/styles/table.scss +2 -2
  47. package/src/components/toast/Toast.mdx +1 -1
  48. package/src/components/toast/Toast.stories.tsx +1 -1
  49. package/src/components/truncate/Truncate.stories.tsx +1 -1
  50. package/src/components/truncate/Truncate.tsx +2 -3
  51. package/src/components/truncate/__tests__/Truncate.test.tsx +3 -3
  52. package/src/index.ts +4 -2
  53. package/src/styles/index.scss +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indico-data/design-system",
3
- "version": "2.58.1",
3
+ "version": "2.59.0",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "main": "lib/index.js",
@@ -45,7 +45,6 @@
45
45
  "date-fns": "^3.6.0",
46
46
  "focus-trap-react": "^10.2.3",
47
47
  "html-webpack-plugin": "^5.6.0",
48
- "nanoid": "^5.1.5",
49
48
  "prop-types": "^15.8.1",
50
49
  "react-aria-components": "^1.2.1",
51
50
  "react-data-table-component": "^7.6.2",
@@ -27,3 +27,4 @@ export { BarSpinner } from './loading-indicators/BarSpinner/BarSpinner';
27
27
  export { CirclePulse } from './loading-indicators/CirclePulse/CirclePulse';
28
28
  export { Truncate } from './truncate';
29
29
  export { toast, ToastContainer } from 'react-toastify';
30
+ export { Stepper } from './stepper';
@@ -0,0 +1,140 @@
1
+ import { Canvas, Meta, Controls } from '@storybook/blocks';
2
+ import * as Stepper from './Stepper.stories';
3
+
4
+ <Meta title="Components/Stepper" name="Stepper" />
5
+
6
+ # Stepper
7
+ The Stepper component is used to guide users through a series of steps. It is managed via a steps array of objects and individual steps inside the component.
8
+
9
+ <Canvas of={Stepper.CommonExampleStory} />
10
+
11
+ <Controls of={Stepper.CommonExampleStory} />
12
+
13
+ ## Usage
14
+ In order to use the stepper, you have various options. You could have a stepper with all steps required, meaning that you must complete the current step before you can proceed to the next step. You also have the option to mix and match other steps between required and optional. You can also make all steps optional as well. The other examples demonstrate those settings. For the sake of time, we currently only have all required as a documented example below.
15
+
16
+ ### All Steps Required
17
+ #### Data
18
+ Copy the data below to recreate the example displayed on this page.
19
+ ```ts
20
+ const exampleSteps = [
21
+ {
22
+ id: 'step1',
23
+ label: 'Application Information',
24
+ isCompleted: false,
25
+ isOptional: false,
26
+ isSidebarEnabled: true,
27
+ isNextDisabled: false,
28
+ },
29
+ {
30
+ id: 'step2',
31
+ label: 'Agent Information',
32
+ isCompleted: false,
33
+ isOptional: false,
34
+ isSidebarEnabled: false,
35
+ isNextDisabled: true,
36
+ },
37
+ {
38
+ id: 'step3',
39
+ label: 'User Information',
40
+ isCompleted: false,
41
+ isOptional: false,
42
+ isSidebarEnabled: false,
43
+ isNextDisabled: true,
44
+ },
45
+ ];
46
+ ```
47
+
48
+ #### Functions And Components
49
+ Copy the functions and components below to recreate the example displayed on this page.
50
+ ```tsx
51
+
52
+ // This sets up the data for the stepper on a parent component. We want to follow this process to maintain state outside of the stepper component.
53
+ const [currentStep, setCurrentStep] = useState(0); // This handles the current step that is being viewed.
54
+ const [steps, setSteps] = useState(exampleSteps); // This is the list of all the steps, matching up in index order with the children of the stepper.
55
+
56
+ // This handles what happens when the back button is clicked on the stepper. The current step is decremented by 1.
57
+ const handleBackClick = () => {
58
+ setCurrentStep(currentStep - 1);
59
+ };
60
+
61
+ // Handles next button click - only proceeds if current step is completed. We set the sidebar for the next step to enabled and we enable the next step. Then, we increment the current step by 1.
62
+ const handleNextClick = () => {
63
+ const currentStepData = steps[currentStep];
64
+ if (currentStepData && currentStepData.isCompleted) {
65
+ setSteps(
66
+ steps.map((step, index) =>
67
+ index === currentStep + 1
68
+ ? { ...step, isSidebarEnabled: true, isNextDisabled: false }
69
+ : step,
70
+ ),
71
+ );
72
+ setCurrentStep(currentStep + 1);
73
+ }
74
+ };
75
+
76
+ // This handles what happens when a step is clicked on the sidebar. We only allow clicking on enabled steps in the sidebar. Then, we set the current step to the step that was clicked.
77
+ const handleSidebarNavigationItemClick = (step: number) => {
78
+ if (steps[step].isSidebarEnabled) {
79
+ setCurrentStep(step);
80
+ }
81
+ };
82
+
83
+ // This handles what happens when a step is completed. We set the current step as completed and we enable the next step in the sidebar.
84
+ const handleOnStepCompletion = (step: number) => {
85
+ setSteps(
86
+ steps.map((currentStep, index) => {
87
+ if (index === step) {
88
+ // Mark current step as completed
89
+ return { ...currentStep, isCompleted: true, isNextDisabled: false };
90
+ } else if (index === step + 1) {
91
+ // Enable the next step in the sidebar
92
+ return { ...currentStep, isSidebarEnabled: true };
93
+ } else {
94
+ return currentStep;
95
+ }
96
+ }),
97
+ );
98
+ };
99
+
100
+ // This handles what happens when the finish button is clicked. We check if all steps are completed. If they are, we show a success message. If they are not, we show an error message.
101
+ const handleCompleteStepperClick = () => {
102
+ // if all steps are completed, then show a success message
103
+ if (steps.every((step) => step.isCompleted)) {
104
+ alert('Successfully completed all required steps!');
105
+ } else {
106
+ alert('You must complete all steps before finishing.');
107
+ }
108
+ };
109
+
110
+ return (
111
+ <Stepper
112
+ steps={steps}
113
+ currentStep={currentStep}
114
+ legendHeader={
115
+ <div>
116
+ <h1>Publish Agent To Gallery</h1>
117
+ <p className="subtitle-2 color-tertiary-200 mt-2 mb-6">
118
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
119
+ incididunt ut labore et dolore.
120
+ </p>
121
+ </div>
122
+ }
123
+ legendFooter={
124
+ <p className="subtitle-2 color-tertiary-200 mt-6">Example of a legend footer</p>
125
+ }
126
+ onBackClick={handleBackClick}
127
+ onNextClick={handleNextClick}
128
+ onFinishClick={handleCompleteStepperClick}
129
+ onStepClick={handleSidebarNavigationItemClick}
130
+ >
131
+ <StepOne onCompletion={() => handleOnStepCompletion(0)} /> // index 0 of the steps array
132
+ <StepTwo onCompletion={() => handleOnStepCompletion(1)} /> // index 1 of the steps array
133
+ <StepThree onCompletion={() => handleOnStepCompletion(2)} /> // index 2 of the steps array
134
+ </Stepper>
135
+ );
136
+ };
137
+ ```
138
+
139
+ ## TODO
140
+ -- Document the other examples
@@ -0,0 +1,196 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { Stepper } from './Stepper';
3
+ import { indiconDefinitions } from '@/components/icons/indicons';
4
+ import { registerFontAwesomeIcons } from '@/setup/setupIcons';
5
+ registerFontAwesomeIcons(...Object.values(fas), ...indiconDefinitions);
6
+
7
+ // Import all example components
8
+ import { MixedExample } from './examples/MixedExample';
9
+ import { RequiredStepsExample } from './examples/RequiredStepsExample';
10
+ import { OptionalStepsExample } from './examples/OptionalStepsExample';
11
+ import { CommonExample } from './examples/commonExample/CommonExample';
12
+ import { fas } from '@fortawesome/free-solid-svg-icons';
13
+
14
+ const meta: Meta = {
15
+ title: 'Components/Stepper',
16
+ component: Stepper,
17
+ argTypes: {
18
+ legendHeader: {
19
+ control: false,
20
+ description: 'The header content for the legend.',
21
+ table: {
22
+ category: 'Props',
23
+ type: {
24
+ summary: 'ReactNode',
25
+ },
26
+ },
27
+ },
28
+ legendFooter: {
29
+ control: false,
30
+ description: 'The footer content for the legend.',
31
+ table: {
32
+ category: 'Props',
33
+ type: {
34
+ summary: 'ReactNode',
35
+ },
36
+ },
37
+ },
38
+ steps: {
39
+ control: false,
40
+ description: 'An array of step objects that define the stepper navigation.',
41
+ table: {
42
+ category: 'Props',
43
+ type: {
44
+ summary: 'Step[]',
45
+ },
46
+ },
47
+ },
48
+ label: {
49
+ description: 'Display text for the step in the sidebar/legend',
50
+ table: {
51
+ category: 'Step Properties',
52
+ type: {
53
+ summary: 'string',
54
+ },
55
+ },
56
+ control: false,
57
+ },
58
+ isCompleted: {
59
+ description:
60
+ 'Indicates whether this step has been completed. Used to track progress and determine whether to enable subsequent steps.',
61
+ table: {
62
+ category: 'Step Properties',
63
+ type: {
64
+ summary: 'boolean',
65
+ },
66
+ defaultValue: { summary: 'false' },
67
+ },
68
+ control: false,
69
+ },
70
+ isNextDisabled: {
71
+ description:
72
+ 'When true, the Next button will be disabled for this step. Set to true for steps that require completion before proceeding.',
73
+ table: {
74
+ category: 'Step Properties',
75
+ type: {
76
+ summary: 'boolean',
77
+ },
78
+ defaultValue: { summary: 'false' },
79
+ },
80
+ control: false,
81
+ },
82
+ isOptional: {
83
+ description:
84
+ 'When true, the step can be skipped (the Next button remains enabled). Used for optional information or steps that are not required to complete the flow.',
85
+ table: {
86
+ category: 'Step Properties',
87
+ type: {
88
+ summary: 'boolean',
89
+ },
90
+ defaultValue: { summary: 'false' },
91
+ },
92
+ control: false,
93
+ },
94
+ isSidebarEnabled: {
95
+ description:
96
+ 'When true, the step is clickable in the sidebar. Used for restricting navigation until previous steps are completed.',
97
+ table: {
98
+ category: 'Step Properties',
99
+ type: {
100
+ summary: 'boolean',
101
+ },
102
+ defaultValue: { summary: 'true' },
103
+ },
104
+ control: false,
105
+ },
106
+ currentStep: {
107
+ control: false,
108
+ description:
109
+ 'The current step of the stepper. This value should be an index of the steps array.',
110
+ table: {
111
+ category: 'Props',
112
+ type: {
113
+ summary: 'number',
114
+ },
115
+ },
116
+ defaultValue: 0,
117
+ },
118
+ onBackClick: {
119
+ control: false,
120
+ description: 'The function to call when the back button is clicked.',
121
+ table: {
122
+ category: 'Callbacks',
123
+ type: {
124
+ summary: '() => void',
125
+ },
126
+ },
127
+ },
128
+ onFinishClick: {
129
+ control: false,
130
+ description: 'The function to call when the finish button is clicked.',
131
+ table: {
132
+ category: 'Callbacks',
133
+ type: {
134
+ summary: '() => void',
135
+ },
136
+ },
137
+ },
138
+ onStepClick: {
139
+ control: false,
140
+ description: 'The function to call when a step is clicked ont he legend.',
141
+ table: {
142
+ category: 'Callbacks',
143
+ type: {
144
+ summary: '(step: number) => void',
145
+ },
146
+ },
147
+ },
148
+ onNextClick: {
149
+ control: false,
150
+ description: 'The function to call when the next button is clicked.',
151
+ table: {
152
+ category: 'Callbacks',
153
+ type: {
154
+ summary: '() => void',
155
+ },
156
+ },
157
+ },
158
+ children: {
159
+ control: false,
160
+ description: 'Any item passed to this component as a child will be rendered as a step.',
161
+ table: {
162
+ category: 'Props',
163
+ type: {
164
+ summary: 'ReactNode',
165
+ },
166
+ },
167
+ },
168
+ },
169
+ };
170
+
171
+ export default meta;
172
+
173
+ type Story = StoryObj<typeof Stepper>;
174
+
175
+ export const CommonExampleStory: Story = {
176
+ name: 'Common Example',
177
+ render: () => <CommonExample />,
178
+ };
179
+
180
+ export const MixedExampleStory: Story = {
181
+ args: {},
182
+ name: 'Mixed Example',
183
+ render: () => <MixedExample />,
184
+ };
185
+
186
+ export const RequiredStepsStory: Story = {
187
+ args: {},
188
+ name: 'Required Steps',
189
+ render: () => <RequiredStepsExample />,
190
+ };
191
+
192
+ export const OptionalStepsStory: Story = {
193
+ args: {},
194
+ name: 'Optional Steps',
195
+ render: () => <OptionalStepsExample />,
196
+ };
@@ -0,0 +1,85 @@
1
+ import { useState, Children } from 'react';
2
+ import { Legend } from './components/Legend';
3
+ import { BackNavigation } from './components/BackNavigation';
4
+ import { NextNavigation } from './components/NextNavigation';
5
+ import { StepperProps } from './types';
6
+
7
+ export const Stepper = ({
8
+ currentStep: externalCurrentStep,
9
+ legendHeader,
10
+ legendFooter,
11
+ steps,
12
+ onBackClick,
13
+ onNextClick,
14
+ onFinishClick,
15
+ children,
16
+ onStepClick,
17
+ }: StepperProps) => {
18
+ const [internalCurrentStep, setInternalCurrentStep] = useState(0);
19
+
20
+ const currentStep = externalCurrentStep !== undefined ? externalCurrentStep : internalCurrentStep;
21
+
22
+ // Convert children to array to access by index
23
+ const childrenArray = Children.toArray(children);
24
+ const totalSteps = childrenArray.length;
25
+
26
+ const isFirstStep = currentStep === 0;
27
+ const isLastStep = currentStep === totalSteps - 1;
28
+
29
+ const handleBackClick = () => {
30
+ if (onBackClick) {
31
+ onBackClick();
32
+ } else if (!isFirstStep) {
33
+ setInternalCurrentStep(currentStep - 1);
34
+ }
35
+ };
36
+
37
+ const handleNextClick = () => {
38
+ if (onNextClick) {
39
+ onNextClick();
40
+ } else if (!isLastStep) {
41
+ setInternalCurrentStep(currentStep + 1);
42
+ }
43
+ };
44
+
45
+ const handleFinishClick = () => {
46
+ if (onFinishClick) {
47
+ onFinishClick();
48
+ }
49
+ };
50
+
51
+ const handleStepClick = (step: number) => {
52
+ onStepClick?.(step);
53
+ };
54
+
55
+ // If step is optional, and its not the last step, then enable the next button
56
+ const disableNextButton = (step: number) => {
57
+ return steps[step].isOptional || !steps[step].isCompleted;
58
+ };
59
+
60
+ return (
61
+ <div className="stepper">
62
+ <div className="legend">
63
+ <div className="legend-header">{legendHeader}</div>
64
+ <div className="legend-body">
65
+ <Legend currentStep={currentStep} steps={steps} onStepClick={handleStepClick} />
66
+ </div>
67
+ <div className="legend-footer">{legendFooter}</div>
68
+ </div>
69
+ <div className="stepper-body">
70
+ <div className="stepper-content">
71
+ <div className="stepper-steps">{childrenArray[currentStep]}</div>
72
+ </div>
73
+ <div className="stepper-actions">
74
+ <BackNavigation isDisabled={isFirstStep} onBackClick={handleBackClick} />
75
+ <NextNavigation
76
+ isLastStep={isLastStep}
77
+ onNextClick={handleNextClick}
78
+ onFinishClick={handleFinishClick}
79
+ isDisabled={disableNextButton(currentStep)}
80
+ />
81
+ </div>
82
+ </div>
83
+ </div>
84
+ );
85
+ };
@@ -0,0 +1,213 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { Stepper } from '../Stepper';
3
+ import { StepOne } from '../examples/commonExample/steps/StepOne';
4
+ import { StepTwo } from '../examples/commonExample/steps/StepTwo';
5
+ import { StepThree } from '../examples/commonExample/steps/StepThree';
6
+ import userEvent from '@testing-library/user-event';
7
+ import React from 'react';
8
+
9
+ // NOTES ================================================
10
+ // The core functionality of the stepper is mainly various callbacks. As a result, we are indirectly testing them through the testing conditions created in this file. For example, to check if a step is complete, we have a callback function that fires when that step is complete. You will need to test your own logic in your implementation as the conditions tested here are for the purposes of the stepper itself, not the handlers we created.
11
+
12
+ const exampleSteps = [
13
+ {
14
+ id: 'step1',
15
+ label: 'Application Name',
16
+ isCompleted: false,
17
+ isOptional: false,
18
+ isSidebarEnabled: true,
19
+ isNextDisabled: false,
20
+ },
21
+ {
22
+ id: 'step2',
23
+ label: 'Agent Information',
24
+ isCompleted: false,
25
+ isOptional: false,
26
+ isSidebarEnabled: false,
27
+ isNextDisabled: true,
28
+ },
29
+ {
30
+ id: 'step3',
31
+ label: 'User Information',
32
+ isCompleted: false,
33
+ isOptional: false,
34
+ isSidebarEnabled: false,
35
+ isNextDisabled: true,
36
+ },
37
+ ];
38
+
39
+ // Test wrapper component to handle state
40
+ const TestStepperWrapper = ({ initialStep = 0 }) => {
41
+ const [currentStep, setCurrentStep] = React.useState(initialStep);
42
+ const [steps, setSteps] = React.useState(exampleSteps);
43
+
44
+ const handleBackClick = () => {
45
+ setCurrentStep(Math.max(0, currentStep - 1));
46
+ };
47
+
48
+ const handleNextClick = () => {
49
+ const currentStepData = steps[currentStep];
50
+ if (currentStepData && currentStepData.isCompleted) {
51
+ setSteps(
52
+ steps.map((step, index) =>
53
+ index === currentStep + 1
54
+ ? { ...step, isSidebarEnabled: true, isNextDisabled: false }
55
+ : step,
56
+ ),
57
+ );
58
+ setCurrentStep(Math.min(steps.length - 1, currentStep + 1));
59
+ }
60
+ };
61
+
62
+ const handleSidebarNavigationItemClick = (step: number) => {
63
+ if (steps[step].isSidebarEnabled) {
64
+ setCurrentStep(step);
65
+ }
66
+ };
67
+
68
+ const handleOnStepCompletion = (step: number) => {
69
+ setSteps(
70
+ steps.map((currentStep, index) => {
71
+ if (index === step) {
72
+ return { ...currentStep, isCompleted: true, isNextDisabled: false };
73
+ } else if (index === step + 1) {
74
+ return { ...currentStep, isSidebarEnabled: true };
75
+ } else {
76
+ return currentStep;
77
+ }
78
+ }),
79
+ );
80
+ };
81
+
82
+ const handleCompleteStepperClick = () => {
83
+ // Mock alert for testing
84
+ if (steps.every((step) => step.isCompleted)) {
85
+ // all completed
86
+ } else {
87
+ // not all completed
88
+ }
89
+ };
90
+
91
+ return (
92
+ <Stepper
93
+ steps={steps}
94
+ currentStep={currentStep}
95
+ onBackClick={handleBackClick}
96
+ onNextClick={handleNextClick}
97
+ onFinishClick={handleCompleteStepperClick}
98
+ onStepClick={handleSidebarNavigationItemClick}
99
+ >
100
+ <StepOne onCompletion={() => handleOnStepCompletion(0)} />
101
+ <StepTwo onCompletion={() => handleOnStepCompletion(1)} />
102
+ <StepThree onCompletion={() => handleOnStepCompletion(2)} />
103
+ </Stepper>
104
+ );
105
+ };
106
+
107
+ describe('Stepper', () => {
108
+ it('It loads the stepper first step as the current step', () => {
109
+ render(<TestStepperWrapper initialStep={0} />);
110
+ const stepOneTitle = screen.getByTestId('step-1-title');
111
+ expect(stepOneTitle).toBeInTheDocument();
112
+ });
113
+
114
+ it('It loads the stepper second step as the current step', () => {
115
+ render(<TestStepperWrapper initialStep={1} />);
116
+ const stepTwoTitle = screen.getByTestId('step-2-title');
117
+ expect(stepTwoTitle).toBeInTheDocument();
118
+ });
119
+
120
+ it('It loads the stepper third step as the current step', () => {
121
+ render(<TestStepperWrapper initialStep={2} />);
122
+ const stepThreeTitle = screen.getByTestId('step-3-title');
123
+ expect(stepThreeTitle).toBeInTheDocument();
124
+ });
125
+
126
+ describe('Testing individual steps', () => {
127
+ it('Step one calls onCompletion when fields are filled', async () => {
128
+ const mockOnCompletion = jest.fn();
129
+ render(<StepOne onCompletion={mockOnCompletion} />);
130
+
131
+ // Fill required fields
132
+ const inputs = screen.getAllByRole('textbox');
133
+ await userEvent.type(inputs[0], 'Test App Name');
134
+ await userEvent.type(inputs[1], '1.0.0');
135
+
136
+ // Verify completion was called
137
+ expect(mockOnCompletion).toHaveBeenCalled();
138
+ });
139
+ });
140
+
141
+ describe('Stepper Navigation', () => {
142
+ it('prevents navigation to a step that is not completed', async () => {
143
+ render(<TestStepperWrapper initialStep={1} />);
144
+ const nextButton = screen.getByTestId('stepper-next-button');
145
+ await userEvent.click(nextButton);
146
+ expect(nextButton).toBeDisabled();
147
+ });
148
+ it('prevents back navigation on the first step', async () => {
149
+ render(<TestStepperWrapper initialStep={0} />);
150
+ const backButton = screen.getByTestId('stepper-back-button');
151
+ await userEvent.click(backButton);
152
+ expect(backButton).toBeDisabled();
153
+ });
154
+ it('prevents legend navigation when the selected step is not optional', async () => {
155
+ render(<TestStepperWrapper initialStep={1} />);
156
+ const legendNavigation = screen.getByTestId('stepper-legend-step-1');
157
+ await userEvent.click(legendNavigation);
158
+ expect(legendNavigation).toBeDisabled();
159
+ });
160
+ it('disables the finish button when not all steps are completed', async () => {
161
+ render(<TestStepperWrapper initialStep={2} />);
162
+ const finishButton = screen.getByTestId('stepper-finish-button');
163
+ await userEvent.click(finishButton);
164
+ expect(finishButton).toBeDisabled();
165
+ });
166
+ it('enables the finish button when all steps are completed', async () => {
167
+ render(<TestStepperWrapper initialStep={2} />);
168
+ const finishButton = screen.getByTestId('stepper-finish-button');
169
+ const inputs = screen.getAllByRole('textbox');
170
+ await userEvent.type(inputs[0], 'Test App Name');
171
+ await userEvent.type(inputs[1], '1.0.0');
172
+ await userEvent.click(finishButton);
173
+ expect(finishButton).toBeEnabled();
174
+ });
175
+ it('shows the finish button when on the last step', async () => {
176
+ render(<TestStepperWrapper initialStep={2} />);
177
+ const finishButton = screen.getByTestId('stepper-finish-button');
178
+ expect(finishButton).toBeInTheDocument();
179
+ });
180
+
181
+ it('It navigates to the next step when Next is clicked', async () => {
182
+ // Mock the necessary handler for testing
183
+ const handleNextClick = jest.fn();
184
+
185
+ render(
186
+ <Stepper
187
+ steps={exampleSteps.map((step) => ({
188
+ ...step,
189
+ isCompleted: true,
190
+ isNextDisabled: false,
191
+ }))}
192
+ currentStep={0}
193
+ onBackClick={() => {}}
194
+ onNextClick={handleNextClick}
195
+ onFinishClick={() => {}}
196
+ onStepClick={() => {}}
197
+ >
198
+ <StepOne onCompletion={() => {}} />
199
+ <StepTwo onCompletion={() => {}} />
200
+ <StepThree onCompletion={() => {}} />
201
+ </Stepper>,
202
+ );
203
+
204
+ expect(screen.getByTestId('step-1-title')).toBeInTheDocument();
205
+
206
+ const nextButton = screen.getByTestId('stepper-next-button');
207
+ await userEvent.click(nextButton);
208
+
209
+ // Verify next handler was called
210
+ expect(handleNextClick).toHaveBeenCalled();
211
+ });
212
+ });
213
+ });
@@ -0,0 +1,22 @@
1
+ import { Button } from '../../button';
2
+ type Props = {
3
+ isDisabled: boolean;
4
+ onBackClick: () => void;
5
+ };
6
+
7
+ export const BackNavigation = ({ isDisabled, onBackClick }: Props) => {
8
+ return (
9
+ <div className="stepper-navigation-back">
10
+ <Button
11
+ data-testid="stepper-back-button"
12
+ ariaLabel="Previous Step"
13
+ iconLeft="fa-arrow-left"
14
+ onClick={onBackClick}
15
+ variant="outline"
16
+ isDisabled={isDisabled}
17
+ >
18
+ Previous Step
19
+ </Button>
20
+ </div>
21
+ );
22
+ };