Skip to content

Commit 34658c1

Browse files
committed
fix(Wizard): onStepChange - skip isDisabled & isHidden
1 parent bf81164 commit 34658c1

8 files changed

Lines changed: 242 additions & 47 deletions

File tree

packages/react-core/src/components/Wizard/Wizard.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
WizardNavType,
1212
WizardStepChangeScope
1313
} from './types';
14-
import { buildSteps } from './utils';
14+
import { buildSteps, isStepEnabled } from './utils';
1515
import { useWizardContext, WizardContextProvider } from './WizardContext';
1616
import { WizardToggle } from './WizardToggle';
1717
import { WizardNavInternal } from './WizardNavInternal';
@@ -23,7 +23,7 @@ import { WizardNavInternal } from './WizardNavInternal';
2323

2424
export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
2525
/** Step components */
26-
children: React.ReactNode | React.ReactNode[];
26+
children: React.ReactNode;
2727
/** Wizard header */
2828
header?: React.ReactNode;
2929
/** Wizard footer */
@@ -86,35 +86,23 @@ export const Wizard = ({
8686
}, [startIndex]);
8787

8888
const goToNextStep = (event: React.MouseEvent<HTMLButtonElement>, steps: WizardStepType[] = initialSteps) => {
89-
const newStep = steps.find(
90-
(step) => step.index > activeStepIndex && !step.isHidden && !step.isDisabled && !isWizardParentStep(step)
91-
);
89+
const newStep = steps.find((step) => step.index > activeStepIndex && isStepEnabled(steps, step));
9290

9391
if (activeStepIndex >= steps.length || !newStep?.index) {
9492
return onSave ? onSave(event) : onClose?.(event);
9593
}
9694

97-
const currStep = isWizardParentStep(steps[activeStepIndex]) ? steps[activeStepIndex + 1] : steps[activeStepIndex];
98-
const prevStep = steps[activeStepIndex - 1];
99-
10095
setActiveStepIndex(newStep?.index);
101-
onStepChange?.(event, currStep, prevStep, WizardStepChangeScope.Next);
96+
onStepChange?.(event, newStep, steps[activeStepIndex - 1], WizardStepChangeScope.Next);
10297
};
10398

10499
const goToPrevStep = (event: React.MouseEvent<HTMLButtonElement>, steps: WizardStepType[] = initialSteps) => {
105100
const newStep = [...steps]
106101
.reverse()
107-
.find(
108-
(step: WizardStepType) =>
109-
step.index < activeStepIndex && !step.isHidden && !step.isDisabled && !isWizardParentStep(step)
110-
);
111-
const currStep = isWizardParentStep(steps[activeStepIndex - 2])
112-
? steps[activeStepIndex - 3]
113-
: steps[activeStepIndex - 2];
114-
const prevStep = steps[activeStepIndex - 1];
102+
.find((step: WizardStepType) => step.index < activeStepIndex && isStepEnabled(steps, step));
115103

116104
setActiveStepIndex(newStep?.index);
117-
onStepChange?.(event, currStep, prevStep, WizardStepChangeScope.Back);
105+
onStepChange?.(event, newStep, steps[activeStepIndex - 1], WizardStepChangeScope.Back);
118106
};
119107

120108
const goToStepByIndex = (

packages/react-core/src/components/Wizard/WizardBody.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { getResizeObserver } from '../../helpers/resizeObserver';
1212

1313
export interface WizardBodyProps {
1414
/** Anything that can be rendered in the Wizard body */
15-
children: React.ReactNode | React.ReactNode[];
15+
children: React.ReactNode;
1616
/** Flag to remove the default body padding */
1717
hasNoPadding?: boolean;
1818
/** Adds an accessible name to the wrapper element when the content overflows and renders
@@ -67,7 +67,7 @@ export const WizardBody = ({
6767
return () => {
6868
observer();
6969
};
70-
}, []);
70+
}, [previousWidth]);
7171

7272
return (
7373
<WrapperComponent

packages/react-core/src/components/Wizard/WizardNavInternal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const WizardNavInternal = ({
8585
id={subStep.id}
8686
content={subStep.name}
8787
isCurrent={activeStep?.id === subStep.id}
88-
isDisabled={isSubStepDisabled}
88+
isDisabled={isSubStepDisabled || isStepDisabled}
8989
isVisited={subStep.isVisited}
9090
stepIndex={subStep.index}
9191
onClick={() => goToStepByIndex(subStep.index)}
@@ -109,7 +109,7 @@ export const WizardNavInternal = ({
109109
content={step.name}
110110
isExpandable={step.isExpandable}
111111
isCurrent={hasActiveChild}
112-
isDisabled={!hasEnabledChildren}
112+
isDisabled={!hasEnabledChildren || isStepDisabled}
113113
isVisited={step.isVisited}
114114
stepIndex={firstSubStepIndex}
115115
onClick={() => goToStepByIndex(firstSubStepIndex)}

packages/react-core/src/components/Wizard/WizardToggle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const WizardToggle = ({
7171

7272
return (
7373
<React.Fragment key={step.id}>
74-
{activeStep?.name === step.name &&
74+
{activeStep?.id === step.id &&
7575
(body || body === undefined ? <WizardBody {...body}>{children}</WizardBody> : children)}
7676

7777
<div key={step.id} style={{ display: 'none' }}>

packages/react-core/src/components/Wizard/__tests__/Wizard.test.tsx

Lines changed: 167 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import { render, screen } from '@testing-library/react';
44
import userEvent from '@testing-library/user-event';
55

6-
import { Wizard, WizardFooterProps, WizardStep, WizardNavProps } from '../';
6+
import { Wizard, WizardFooterProps, WizardStep, WizardNavProps, WizardStepChangeScope } from '../';
77

88
test('renders step when child is of type WizardStep', () => {
99
render(
@@ -100,7 +100,6 @@ test('renders default nav with custom props', () => {
100100
});
101101

102102
test('renders nav aria label', () => {
103-
104103
render(
105104
<Wizard navAriaLabel="custom nav aria-label">
106105
<WizardStep id="test-step" name="Test step" />
@@ -174,45 +173,55 @@ test(`can customize the wizard's height and width`, () => {
174173
expect(wizard).toHaveStyle('width: 500px');
175174
});
176175

177-
test('calls onNavByIndex on nav item click', async () => {
176+
test('calls onStepChange on nav item click', async () => {
178177
const user = userEvent.setup();
179-
const onNavByIndex = jest.fn();
178+
const onStepChange = jest.fn();
180179

181180
render(
182-
<Wizard onStepChange={onNavByIndex}>
181+
<Wizard onStepChange={onStepChange}>
183182
<WizardStep id="step-1" name="Test step 1" />
184183
<WizardStep id="step-2" name="Test step 2" />
185184
</Wizard>
186185
);
187186

188187
await user.click(screen.getByRole('button', { name: 'Test step 2' }));
189-
expect(onNavByIndex).toHaveBeenCalled();
188+
expect(onStepChange).toHaveBeenCalledWith(
189+
null,
190+
expect.objectContaining({ id: 'step-2' }),
191+
expect.objectContaining({ id: 'step-1' }),
192+
WizardStepChangeScope.Nav
193+
);
190194
});
191195

192-
test('calls onNext and not onSave on next button click when not on the last step', async () => {
196+
test('calls onStepChange and not onSave on next button click when not on the last step', async () => {
193197
const user = userEvent.setup();
194-
const onNext = jest.fn();
198+
const onStepChange = jest.fn();
195199
const onSave = jest.fn();
196200

197201
render(
198-
<Wizard onStepChange={onNext} onSave={onSave}>
202+
<Wizard onStepChange={onStepChange} onSave={onSave}>
199203
<WizardStep id="step-1" name="Test step 1" />
200204
<WizardStep id="step-2" name="Test step 2" />
201205
</Wizard>
202206
);
203207

204208
await user.click(screen.getByRole('button', { name: 'Next' }));
205209

206-
expect(onNext).toHaveBeenCalled();
210+
expect(onStepChange).toHaveBeenCalledWith(
211+
null,
212+
expect.objectContaining({ id: 'step-2' }),
213+
expect.objectContaining({ id: 'step-1' }),
214+
WizardStepChangeScope.Next
215+
);
207216
expect(onSave).not.toHaveBeenCalled();
208217
});
209218

210-
test('calls onBack on back button click', async () => {
219+
test('calls onStepChange on back button click', async () => {
211220
const user = userEvent.setup();
212-
const onBack = jest.fn();
221+
const onStepChange = jest.fn();
213222

214223
render(
215-
<Wizard onStepChange={onBack}>
224+
<Wizard onStepChange={onStepChange}>
216225
<WizardStep id="step-1" name="Test step 1" />
217226
<WizardStep id="step-2" name="Test step 2" />
218227
</Wizard>
@@ -221,7 +230,12 @@ test('calls onBack on back button click', async () => {
221230
await user.click(screen.getByRole('button', { name: 'Test step 2' }));
222231
await user.click(screen.getByRole('button', { name: 'Back' }));
223232

224-
expect(onBack).toHaveBeenCalled();
233+
expect(onStepChange).toHaveBeenCalledWith(
234+
null,
235+
expect.objectContaining({ id: 'step-1' }),
236+
expect.objectContaining({ id: 'step-2' }),
237+
WizardStepChangeScope.Back
238+
);
225239
});
226240

227241
test('calls onSave and not onClose on next button click when on the last step', async () => {
@@ -445,17 +459,153 @@ test('incrementally shows/hides steps based on the activeStep when isProgressive
445459
).toBeNull();
446460
});
447461

448-
test('parent step can be non-collapsible by setting isCollapsible to false', () => {
462+
test('parent step can be expandable by setting isExpandable to true', () => {
449463
render(
450464
<Wizard>
451465
<WizardStep
452466
id="step-1"
453467
name="Test step 1"
454-
isCollapsible={false}
468+
isExpandable
455469
steps={[<WizardStep id="sub-step-1" name="Sub step 1" />]}
456470
/>
457471
</Wizard>
458472
);
459473

460-
expect(screen.queryByLabelText('step icon', { exact: false })).toBeNull();
474+
expect(screen.getByLabelText('step icon', { exact: false })).toBeVisible();
475+
});
476+
477+
test('child steps are disabled when parent is disabled', () => {
478+
render(
479+
<Wizard>
480+
<WizardStep
481+
id="step-1"
482+
name="Test step 1"
483+
isDisabled
484+
steps={[<WizardStep id="sub-step-1" name="Sub step 1" />]}
485+
/>
486+
</Wizard>
487+
);
488+
489+
expect(
490+
screen.getByRole('button', {
491+
name: 'Test step 1'
492+
})
493+
).toBeDisabled();
494+
expect(
495+
screen.getByRole('button', {
496+
name: 'Sub step 1'
497+
})
498+
).toBeDisabled();
499+
});
500+
501+
test('child steps are hidden when parent is hidden', () => {
502+
render(
503+
<Wizard>
504+
<WizardStep id="step-1" name="Test step 1" isHidden steps={[<WizardStep id="sub-step-1" name="Sub step 1" />]} />
505+
</Wizard>
506+
);
507+
508+
expect(
509+
screen.queryByRole('button', {
510+
name: 'Test step 1'
511+
})
512+
).toBeNull();
513+
expect(
514+
screen.queryByRole('button', {
515+
name: 'Sub step 1'
516+
})
517+
).toBeNull();
518+
});
519+
520+
test('onStepChange skips over disabled or hidden steps and substeps', async () => {
521+
const user = userEvent.setup();
522+
const onStepChange = jest.fn();
523+
524+
render(
525+
<Wizard onStepChange={onStepChange}>
526+
<WizardStep id="step-1" name="Test step 1" />
527+
<WizardStep id="step-2" name="Test step 2" steps={[<WizardStep id="step2-sub1" name="Test Substep 1" />]} />
528+
<WizardStep id="step-3" name="Test step 3" isDisabled />
529+
<WizardStep
530+
id="step-4"
531+
name="Test step 4"
532+
isDisabled
533+
steps={[<WizardStep id="step4-sub1" name="Test Substep 1" />]}
534+
/>
535+
<WizardStep
536+
id="step-5"
537+
name="Test step 4"
538+
steps={[
539+
<WizardStep id="step5-sub1" name="Test Substep 1" isDisabled />,
540+
<WizardStep id="step5-sub2" name="Test Substep 2" />
541+
]}
542+
/>
543+
<WizardStep id="step-6" name="Test step 6" isHidden />
544+
<WizardStep
545+
id="step-7"
546+
name="Test step 7"
547+
isHidden
548+
steps={[<WizardStep id="step7-sub1" name="Test Substep 1" />]}
549+
/>
550+
<WizardStep
551+
id="step-8"
552+
name="Test step 8"
553+
steps={[
554+
<WizardStep id="step8-sub1" name="Test Substep 1" isHidden />,
555+
<WizardStep id="step8-sub2" name="Test Substep 2" />
556+
]}
557+
/>
558+
</Wizard>
559+
);
560+
561+
const nextButton = screen.getByRole('button', { name: 'Next' });
562+
const backButton = screen.getByRole('button', { name: 'Back' });
563+
564+
await user.click(nextButton);
565+
expect(onStepChange).toHaveBeenCalledWith(
566+
null,
567+
expect.objectContaining({ id: 'step2-sub1' }),
568+
expect.objectContaining({ id: 'step-1' }),
569+
WizardStepChangeScope.Next
570+
);
571+
572+
await user.click(nextButton);
573+
expect(onStepChange).toHaveBeenCalledWith(
574+
null,
575+
expect.objectContaining({ id: 'step5-sub2' }),
576+
expect.objectContaining({ id: 'step2-sub1' }),
577+
WizardStepChangeScope.Next
578+
);
579+
580+
await user.click(nextButton);
581+
expect(onStepChange).toHaveBeenCalledWith(
582+
null,
583+
expect.objectContaining({ id: 'step8-sub2' }),
584+
expect.objectContaining({ id: 'step5-sub2' }),
585+
WizardStepChangeScope.Next
586+
);
587+
588+
await user.click(backButton);
589+
expect(onStepChange).toHaveBeenCalledWith(
590+
null,
591+
expect.objectContaining({ id: 'step5-sub2' }),
592+
expect.objectContaining({ id: 'step8-sub2' }),
593+
WizardStepChangeScope.Back
594+
);
595+
596+
await user.click(backButton);
597+
expect(onStepChange).toHaveBeenCalledWith(
598+
null,
599+
expect.objectContaining({ id: 'step2-sub1' }),
600+
expect.objectContaining({ id: 'step5-sub2' }),
601+
WizardStepChangeScope.Back
602+
);
603+
604+
await user.click(backButton);
605+
expect(onStepChange).toHaveBeenCalledWith(
606+
null,
607+
expect.objectContaining({ id: 'step-1' }),
608+
expect.objectContaining({ id: 'step2-sub1' }),
609+
WizardStepChangeScope.Back
610+
);
461611
});

packages/react-core/src/components/Wizard/__tests__/WizardBody.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ test('renders children without additional props', () => {
1010
expect(container).not.toHaveAttribute('aria-labelledby');
1111
});
1212

13-
test('has no padding className when hasNoBodyPadding is not specified', () => {
13+
test('has no padding className when hasNoPadding is not specified', () => {
1414
render(<WizardBody>content</WizardBody>);
1515
expect(screen.getByText('content')).not.toHaveClass('pf-m-no-padding');
1616
});

0 commit comments

Comments
 (0)