Skip to content

Commit 0ffa3fd

Browse files
committed
next(Wizard): Allow for WizardStep to better control state from props entrypoint, include index
1 parent 9d771b7 commit 0ffa3fd

21 files changed

+351
-439
lines changed

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

Lines changed: 35 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
4242
height?: number | string;
4343
/** Disables navigation items that haven't been visited. Defaults to false */
4444
isStepVisitRequired?: boolean;
45-
/** Flag to unmount inactive steps instead of hiding. Defaults to true */
46-
hasUnmountedSteps?: boolean;
4745
/** Callback function when a step in the navigation is clicked */
4846
onNavByIndex?: WizardNavStepFunction;
4947
/** Callback function after next button is clicked */
@@ -66,50 +64,44 @@ export const Wizard = ({
6664
nav,
6765
startIndex = 1,
6866
isStepVisitRequired = false,
69-
hasUnmountedSteps = true,
7067
onNavByIndex,
7168
onNext,
7269
onBack,
7370
onSave,
7471
onClose,
7572
...wrapperProps
7673
}: WizardProps) => {
77-
const [currentStepIndex, setCurrentStepIndex] = React.useState(startIndex);
74+
const [activeStepIndex, setActiveStepIndex] = React.useState(startIndex);
7875
const initialSteps = buildSteps(children);
7976

8077
const goToNextStep = (steps: WizardControlStep[] = initialSteps) => {
81-
const newStepIndex =
82-
steps.findIndex((step, index) => index + 1 > currentStepIndex && !step.isHidden && !isWizardParentStep(step)) + 1;
78+
const newStepIndex = steps.find(step => step.index > activeStepIndex && !step.isHidden && !isWizardParentStep(step))
79+
?.index;
8380

84-
if (currentStepIndex >= steps.length || !newStepIndex) {
81+
if (activeStepIndex >= steps.length || !newStepIndex) {
8582
return onSave ? onSave() : onClose?.();
8683
}
8784

88-
const currStep = isWizardParentStep(steps[currentStepIndex])
89-
? steps[currentStepIndex + 1]
90-
: steps[currentStepIndex];
91-
const prevStep = steps[currentStepIndex - 1];
92-
93-
setCurrentStepIndex(newStepIndex);
85+
const currStep = isWizardParentStep(steps[activeStepIndex]) ? steps[activeStepIndex + 1] : steps[activeStepIndex];
86+
const prevStep = steps[activeStepIndex - 1];
9487

95-
return onNext?.(normalizeNavStep(currStep, steps), normalizeNavStep(prevStep, steps));
88+
setActiveStepIndex(newStepIndex);
89+
return onNext?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
9690
};
9791

9892
const goToPrevStep = (steps: WizardControlStep[] = initialSteps) => {
9993
const newStepIndex =
10094
findLastIndex(
10195
steps,
102-
(step: WizardControlStep, index: number) =>
103-
index + 1 < currentStepIndex && !step.isHidden && !isWizardParentStep(step)
96+
(step: WizardControlStep) => step.index < activeStepIndex && !step.isHidden && !isWizardParentStep(step)
10497
) + 1;
105-
const currStep = isWizardParentStep(steps[currentStepIndex - 2])
106-
? steps[currentStepIndex - 3]
107-
: steps[currentStepIndex - 2];
108-
const prevStep = steps[currentStepIndex - 1];
109-
110-
setCurrentStepIndex(newStepIndex);
98+
const currStep = isWizardParentStep(steps[activeStepIndex - 2])
99+
? steps[activeStepIndex - 3]
100+
: steps[activeStepIndex - 2];
101+
const prevStep = steps[activeStepIndex - 1];
111102

112-
return onBack?.(normalizeNavStep(currStep, steps), normalizeNavStep(prevStep, steps));
103+
setActiveStepIndex(newStepIndex);
104+
return onBack?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
113105
};
114106

115107
const goToStepByIndex = (steps: WizardControlStep[] = initialSteps, index: number) => {
@@ -120,46 +112,40 @@ export const Wizard = ({
120112
index = 1;
121113
} else if (index > lastStepIndex) {
122114
index = lastStepIndex;
123-
} else if (steps[index - 1].isHidden) {
124-
// eslint-disable-next-line no-console
125-
console.error('Wizard: Unable to navigate to hidden step.');
126115
}
127116

128117
const currStep = steps[index - 1];
129-
const prevStep = steps[currentStepIndex - 1];
130-
setCurrentStepIndex(index);
118+
const prevStep = steps[activeStepIndex - 1];
131119

132-
return onNavByIndex?.(normalizeNavStep(currStep, steps), normalizeNavStep(prevStep, steps));
120+
setActiveStepIndex(index);
121+
return onNavByIndex?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
133122
};
134123

135124
const goToStepById = (steps: WizardControlStep[] = initialSteps, id: number | string) => {
136-
const stepIndex = steps.findIndex(step => step.id === id) + 1;
125+
const step = steps.find(step => step.id === id);
126+
const stepIndex = step?.index;
127+
const lastStepIndex = steps.length + 1;
137128

138-
if (stepIndex > 0 && stepIndex < steps.length + 1 && !steps[stepIndex].isHidden) {
139-
setCurrentStepIndex(stepIndex);
140-
} else {
141-
// eslint-disable-next-line no-console
142-
console.error(`Wizard: Unable to navigate to step with id: ${id}.`);
129+
if (stepIndex > 0 && stepIndex < lastStepIndex && !step.isHidden) {
130+
setActiveStepIndex(stepIndex);
143131
}
144132
};
145133

146134
const goToStepByName = (steps: WizardControlStep[] = initialSteps, name: string) => {
147-
const stepIndex = initialSteps.findIndex(step => step.name === name) + 1;
135+
const step = steps.find(step => step.name === name);
136+
const stepIndex = step?.index;
137+
const lastStepIndex = steps.length + 1;
148138

149-
if (stepIndex > 0 && stepIndex < steps.length + 1 && !steps[stepIndex].isHidden) {
150-
setCurrentStepIndex(stepIndex);
151-
} else {
152-
// eslint-disable-next-line no-console
153-
console.error(`Wizard: Unable to navigate to step with name: ${name}.`);
139+
if (stepIndex > 0 && stepIndex < lastStepIndex && !step.isHidden) {
140+
setActiveStepIndex(stepIndex);
154141
}
155142
};
156143

157144
return (
158145
<WizardContextProvider
159146
steps={initialSteps}
160-
currentStepIndex={currentStepIndex}
147+
activeStepIndex={activeStepIndex}
161148
footer={footer}
162-
isStepVisitRequired={isStepVisitRequired}
163149
onNext={goToNextStep}
164150
onBack={goToPrevStep}
165151
onClose={onClose}
@@ -176,37 +162,32 @@ export const Wizard = ({
176162
{...wrapperProps}
177163
>
178164
{header}
179-
<WizardInternal nav={nav} hasUnmountedSteps={hasUnmountedSteps} isStepVisitRequired={isStepVisitRequired} />
165+
<WizardInternal nav={nav} isStepVisitRequired={isStepVisitRequired} />
180166
</div>
181167
</WizardContextProvider>
182168
);
183169
};
184170

185-
const WizardInternal = ({
186-
nav,
187-
hasUnmountedSteps,
188-
isStepVisitRequired
189-
}: Pick<WizardProps, 'nav' | 'hasUnmountedSteps' | 'isStepVisitRequired'>) => {
190-
const { currentStep, steps, footer, goToStepByIndex } = useWizardContext();
171+
const WizardInternal = ({ nav, isStepVisitRequired }: Pick<WizardProps, 'nav' | 'isStepVisitRequired'>) => {
172+
const { activeStep, steps, footer, goToStepByIndex } = useWizardContext();
191173
const [isNavExpanded, setIsNavExpanded] = React.useState(false);
192174

193175
const wizardNav = React.useMemo(() => {
194176
if (isCustomWizardNav(nav)) {
195-
return typeof nav === 'function' ? nav(isNavExpanded, steps, currentStep, goToStepByIndex) : nav;
177+
return typeof nav === 'function' ? nav(isNavExpanded, steps, activeStep, goToStepByIndex) : nav;
196178
}
197179

198180
return <WizardNavInternal nav={nav} isNavExpanded={isNavExpanded} isStepVisitRequired={isStepVisitRequired} />;
199-
}, [currentStep, isStepVisitRequired, goToStepByIndex, isNavExpanded, nav, steps]);
181+
}, [activeStep, isStepVisitRequired, goToStepByIndex, isNavExpanded, nav, steps]);
200182

201183
return (
202184
<WizardToggle
203185
nav={wizardNav}
204186
footer={footer}
205187
steps={steps}
206-
currentStep={currentStep}
188+
activeStep={activeStep}
207189
isNavExpanded={isNavExpanded}
208190
toggleNavExpanded={() => setIsNavExpanded(prevIsExpanded => !prevIsExpanded)}
209-
hasUnmountedSteps={hasUnmountedSteps}
210191
/>
211192
);
212193
};

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { css } from '@patternfly/react-styles';
1010
export interface WizardBodyProps {
1111
children: React.ReactNode | React.ReactNode[];
1212
/** Set to true to remove the default body padding */
13-
hasNoBodyPadding?: boolean;
13+
hasNoPadding?: boolean;
1414
/** An aria-label to use for the wrapper element */
1515
'aria-label'?: string;
1616
/** Sets the aria-labelledby attribute for the wrapper element */
@@ -21,13 +21,13 @@ export interface WizardBodyProps {
2121

2222
export const WizardBody = ({
2323
children,
24-
hasNoBodyPadding = false,
24+
hasNoPadding = false,
2525
'aria-label': ariaLabel,
2626
'aria-labelledby': ariaLabelledBy,
2727
component: WrapperComponent = 'div'
2828
}: WizardBodyProps) => (
2929
<WrapperComponent aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} className={css(styles.wizardMain)}>
30-
<div className={css(styles.wizardMainBody, hasNoBodyPadding && styles.modifiers.noPadding)}>{children}</div>
30+
<div className={css(styles.wizardMainBody, hasNoPadding && styles.modifiers.noPadding)}>{children}</div>
3131
</WrapperComponent>
3232
);
3333

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

Lines changed: 26 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import React from 'react';
22

3-
import { isCustomWizardFooter, isWizardParentStep, WizardControlStep, WizardFooterType } from './types';
4-
import { getCurrentStep } from './utils';
3+
import { isCustomWizardFooter, WizardControlStep, WizardFooterType } from './types';
4+
import { getActiveStep } from './utils';
5+
import { useGetMergedSteps } from './hooks/useGetMergedSteps';
56
import { WizardFooter, WizardFooterProps } from './WizardFooter';
67

78
export interface WizardContextProps {
89
/** List of steps */
910
steps: WizardControlStep[];
1011
/** Current step */
11-
currentStep: WizardControlStep;
12-
/** Current step index */
13-
currentStepIndex: number;
12+
activeStep: WizardControlStep;
1413
/** Footer element */
1514
footer: React.ReactElement;
1615
/** Navigate to the next step */
@@ -31,17 +30,16 @@ export interface WizardContextProps {
3130
getStep: (stepId: number | string) => WizardControlStep;
3231
/** Set step by ID */
3332
setStep: (step: Pick<WizardControlStep, 'id'> & Partial<WizardControlStep>) => void;
34-
/** Toggle step visibility by ID */
35-
toggleStep: (stepId: number | string, isHidden: boolean) => void;
33+
/** Set multiple steps */
34+
setSteps: React.Dispatch<React.SetStateAction<WizardControlStep[]>>;
3635
}
3736

3837
export const WizardContext = React.createContext({} as WizardContextProps);
3938

4039
export interface WizardContextProviderProps {
4140
steps: WizardControlStep[];
42-
currentStepIndex: number;
41+
activeStepIndex: number;
4342
footer: WizardFooterType;
44-
isStepVisitRequired: boolean;
4543
children: React.ReactElement;
4644
onNext(steps: WizardControlStep[]): void;
4745
onBack(steps: WizardControlStep[]): void;
@@ -54,8 +52,7 @@ export interface WizardContextProviderProps {
5452
export const WizardContextProvider: React.FunctionComponent<WizardContextProviderProps> = ({
5553
steps: initialSteps,
5654
footer: initialFooter,
57-
currentStepIndex,
58-
isStepVisitRequired,
55+
activeStepIndex,
5956
children,
6057
onNext,
6158
onBack,
@@ -68,35 +65,38 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
6865
const [currentFooter, setCurrentFooter] = React.useState(
6966
typeof initialFooter !== 'function' ? initialFooter : undefined
7067
);
71-
const currentStep = getCurrentStep(steps, currentStepIndex);
68+
const mergedSteps = useGetMergedSteps(initialSteps, steps);
69+
const activeStep = getActiveStep(mergedSteps, activeStepIndex);
7270

73-
const goToNextStep = React.useCallback(() => onNext(steps), [onNext, steps]);
74-
const goToPrevStep = React.useCallback(() => onBack(steps), [onBack, steps]);
71+
const goToNextStep = React.useCallback(() => onNext(mergedSteps), [onNext, mergedSteps]);
72+
const goToPrevStep = React.useCallback(() => onBack(mergedSteps), [onBack, mergedSteps]);
7573

7674
const footer = React.useMemo(() => {
77-
const wizardFooter = currentFooter || initialFooter;
75+
const wizardFooter = activeStep?.footer || currentFooter || initialFooter;
7876

7977
if (isCustomWizardFooter(wizardFooter)) {
8078
const customFooter = wizardFooter;
8179

8280
return typeof customFooter === 'function'
83-
? customFooter(currentStep, goToNextStep, goToPrevStep, onClose)
81+
? customFooter(activeStep, goToNextStep, goToPrevStep, onClose)
8482
: customFooter;
8583
}
8684

8785
return (
8886
<WizardFooter
89-
currentStep={currentStep}
87+
activeStep={activeStep}
9088
onNext={goToNextStep}
9189
onBack={goToPrevStep}
9290
onClose={onClose}
93-
isBackDisabled={currentStep?.id === steps[0]?.id}
91+
isBackDisabled={activeStep?.id === mergedSteps[0]?.id}
9492
{...wizardFooter}
9593
/>
9694
);
97-
}, [currentFooter, initialFooter, currentStep, goToNextStep, goToPrevStep, onClose, steps]);
95+
}, [currentFooter, initialFooter, activeStep, goToNextStep, goToPrevStep, onClose, mergedSteps]);
9896

99-
const getStep = React.useCallback((stepId: string | number) => steps.find(step => step.id === stepId), [steps]);
97+
const getStep = React.useCallback((stepId: string | number) => mergedSteps.find(step => step.id === stepId), [
98+
mergedSteps
99+
]);
100100

101101
const setStep = React.useCallback(
102102
(step: Pick<WizardControlStep, 'id'> & Partial<WizardControlStep>) =>
@@ -112,62 +112,22 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
112112
[]
113113
);
114114

115-
const toggleStep = React.useCallback(
116-
(stepId: string | number, isHidden: boolean) =>
117-
setSteps(prevSteps => {
118-
let stepToHide: WizardControlStep;
119-
120-
return prevSteps.map(prevStep => {
121-
if (prevStep.id === stepId) {
122-
// Don't hide the currently active step or its parent (if a sub-step).
123-
if (
124-
isHidden &&
125-
(currentStep.id === prevStep.id ||
126-
(isWizardParentStep(prevStep) && prevStep.subStepIds.includes(currentStep.id)))
127-
) {
128-
// eslint-disable-next-line no-console
129-
console.error('Wizard: Unable to hide the current step or its parent.');
130-
return prevStep;
131-
}
132-
133-
stepToHide = { ...prevStep, isHidden };
134-
return stepToHide;
135-
}
136-
137-
// When isStepVisitRequired is enabled, if the step was previously hidden and not visited yet,
138-
// when it is shown, all steps beyond it should be disabled to ensure it is visited.
139-
if (
140-
isStepVisitRequired &&
141-
stepToHide?.isHidden === false &&
142-
!stepToHide?.isVisited &&
143-
prevSteps.indexOf(stepToHide) < prevSteps.indexOf(prevStep)
144-
) {
145-
return { ...prevStep, isVisited: false };
146-
}
147-
148-
return prevStep;
149-
});
150-
}),
151-
[currentStep.id, isStepVisitRequired]
152-
);
153-
154115
return (
155116
<WizardContext.Provider
156117
value={{
157-
steps,
158-
currentStep,
159-
currentStepIndex,
118+
steps: mergedSteps,
119+
activeStep,
160120
footer,
161121
onClose,
162122
getStep,
163123
setStep,
164-
toggleStep,
124+
setSteps,
165125
setFooter: setCurrentFooter,
166126
onNext: goToNextStep,
167127
onBack: goToPrevStep,
168-
goToStepById: React.useCallback(id => goToStepById(steps, id), [goToStepById, steps]),
169-
goToStepByName: React.useCallback(name => goToStepByName(steps, name), [goToStepByName, steps]),
170-
goToStepByIndex: React.useCallback(index => goToStepByIndex(steps, index), [goToStepByIndex, steps])
128+
goToStepById: React.useCallback(id => goToStepById(mergedSteps, id), [goToStepById, mergedSteps]),
129+
goToStepByName: React.useCallback(name => goToStepByName(mergedSteps, name), [goToStepByName, mergedSteps]),
130+
goToStepByIndex: React.useCallback(index => goToStepByIndex(mergedSteps, index), [goToStepByIndex, mergedSteps])
171131
}}
172132
>
173133
{children}

0 commit comments

Comments
 (0)