Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 35 additions & 54 deletions packages/react-core/src/next/components/Wizard/Wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
height?: number | string;
/** Disables navigation items that haven't been visited. Defaults to false */
isStepVisitRequired?: boolean;
/** Flag to unmount inactive steps instead of hiding. Defaults to true */
hasUnmountedSteps?: boolean;
/** Callback function when a step in the navigation is clicked */
onNavByIndex?: WizardNavStepFunction;
/** Callback function after next button is clicked */
Expand All @@ -66,50 +64,44 @@ export const Wizard = ({
nav,
startIndex = 1,
isStepVisitRequired = false,
hasUnmountedSteps = true,
onNavByIndex,
onNext,
onBack,
onSave,
onClose,
...wrapperProps
}: WizardProps) => {
const [currentStepIndex, setCurrentStepIndex] = React.useState(startIndex);
const [activeStepIndex, setActiveStepIndex] = React.useState(startIndex);
const initialSteps = buildSteps(children);

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

if (currentStepIndex >= steps.length || !newStepIndex) {
if (activeStepIndex >= steps.length || !newStepIndex) {
return onSave ? onSave() : onClose?.();
}

const currStep = isWizardParentStep(steps[currentStepIndex])
? steps[currentStepIndex + 1]
: steps[currentStepIndex];
const prevStep = steps[currentStepIndex - 1];

setCurrentStepIndex(newStepIndex);
const currStep = isWizardParentStep(steps[activeStepIndex]) ? steps[activeStepIndex + 1] : steps[activeStepIndex];
const prevStep = steps[activeStepIndex - 1];

return onNext?.(normalizeNavStep(currStep, steps), normalizeNavStep(prevStep, steps));
setActiveStepIndex(newStepIndex);
return onNext?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
};

const goToPrevStep = (steps: WizardControlStep[] = initialSteps) => {
const newStepIndex =
findLastIndex(
steps,
(step: WizardControlStep, index: number) =>
index + 1 < currentStepIndex && !step.isHidden && !isWizardParentStep(step)
(step: WizardControlStep) => step.index < activeStepIndex && !step.isHidden && !isWizardParentStep(step)
) + 1;
const currStep = isWizardParentStep(steps[currentStepIndex - 2])
? steps[currentStepIndex - 3]
: steps[currentStepIndex - 2];
const prevStep = steps[currentStepIndex - 1];

setCurrentStepIndex(newStepIndex);
const currStep = isWizardParentStep(steps[activeStepIndex - 2])
? steps[activeStepIndex - 3]
: steps[activeStepIndex - 2];
const prevStep = steps[activeStepIndex - 1];

return onBack?.(normalizeNavStep(currStep, steps), normalizeNavStep(prevStep, steps));
setActiveStepIndex(newStepIndex);
return onBack?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
};

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

const currStep = steps[index - 1];
const prevStep = steps[currentStepIndex - 1];
setCurrentStepIndex(index);
const prevStep = steps[activeStepIndex - 1];

return onNavByIndex?.(normalizeNavStep(currStep, steps), normalizeNavStep(prevStep, steps));
setActiveStepIndex(index);
return onNavByIndex?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
};

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

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

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

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

return (
<WizardContextProvider
steps={initialSteps}
currentStepIndex={currentStepIndex}
activeStepIndex={activeStepIndex}
footer={footer}
isStepVisitRequired={isStepVisitRequired}
onNext={goToNextStep}
onBack={goToPrevStep}
onClose={onClose}
Expand All @@ -176,37 +162,32 @@ export const Wizard = ({
{...wrapperProps}
>
{header}
<WizardInternal nav={nav} hasUnmountedSteps={hasUnmountedSteps} isStepVisitRequired={isStepVisitRequired} />
<WizardInternal nav={nav} isStepVisitRequired={isStepVisitRequired} />
</div>
</WizardContextProvider>
);
};

const WizardInternal = ({
nav,
hasUnmountedSteps,
isStepVisitRequired
}: Pick<WizardProps, 'nav' | 'hasUnmountedSteps' | 'isStepVisitRequired'>) => {
const { currentStep, steps, footer, goToStepByIndex } = useWizardContext();
const WizardInternal = ({ nav, isStepVisitRequired }: Pick<WizardProps, 'nav' | 'isStepVisitRequired'>) => {
const { activeStep, steps, footer, goToStepByIndex } = useWizardContext();
const [isNavExpanded, setIsNavExpanded] = React.useState(false);

const wizardNav = React.useMemo(() => {
if (isCustomWizardNav(nav)) {
return typeof nav === 'function' ? nav(isNavExpanded, steps, currentStep, goToStepByIndex) : nav;
return typeof nav === 'function' ? nav(isNavExpanded, steps, activeStep, goToStepByIndex) : nav;
}

return <WizardNavInternal nav={nav} isNavExpanded={isNavExpanded} isStepVisitRequired={isStepVisitRequired} />;
}, [currentStep, isStepVisitRequired, goToStepByIndex, isNavExpanded, nav, steps]);
}, [activeStep, isStepVisitRequired, goToStepByIndex, isNavExpanded, nav, steps]);

return (
<WizardToggle
nav={wizardNav}
footer={footer}
steps={steps}
currentStep={currentStep}
activeStep={activeStep}
isNavExpanded={isNavExpanded}
toggleNavExpanded={() => setIsNavExpanded(prevIsExpanded => !prevIsExpanded)}
hasUnmountedSteps={hasUnmountedSteps}
/>
);
};
Expand Down
6 changes: 3 additions & 3 deletions packages/react-core/src/next/components/Wizard/WizardBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { css } from '@patternfly/react-styles';
export interface WizardBodyProps {
children: React.ReactNode | React.ReactNode[];
/** Set to true to remove the default body padding */
hasNoBodyPadding?: boolean;
hasNoPadding?: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

/** An aria-label to use for the wrapper element */
'aria-label'?: string;
/** Sets the aria-labelledby attribute for the wrapper element */
Expand All @@ -21,13 +21,13 @@ export interface WizardBodyProps {

export const WizardBody = ({
children,
hasNoBodyPadding = false,
hasNoPadding = false,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
component: WrapperComponent = 'div'
}: WizardBodyProps) => (
<WrapperComponent aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} className={css(styles.wizardMain)}>
<div className={css(styles.wizardMainBody, hasNoBodyPadding && styles.modifiers.noPadding)}>{children}</div>
<div className={css(styles.wizardMainBody, hasNoPadding && styles.modifiers.noPadding)}>{children}</div>
</WrapperComponent>
);

Expand Down
102 changes: 36 additions & 66 deletions packages/react-core/src/next/components/Wizard/WizardContext.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import React from 'react';

import { isCustomWizardFooter, isWizardParentStep, WizardControlStep, WizardFooterType } from './types';
import { getCurrentStep } from './utils';
import { isCustomWizardFooter, WizardControlStep, WizardFooterType } from './types';
import { getActiveStep } from './utils';
import { WizardFooter, WizardFooterProps } from './WizardFooter';

export interface WizardContextProps {
/** List of steps */
steps: WizardControlStep[];
/** Current step */
currentStep: WizardControlStep;
/** Current step index */
currentStepIndex: number;
activeStep: WizardControlStep;
/** Footer element */
footer: React.ReactElement;
/** Navigate to the next step */
Expand All @@ -31,17 +29,14 @@ export interface WizardContextProps {
getStep: (stepId: number | string) => WizardControlStep;
/** Set step by ID */
setStep: (step: Pick<WizardControlStep, 'id'> & Partial<WizardControlStep>) => void;
/** Toggle step visibility by ID */
toggleStep: (stepId: number | string, isHidden: boolean) => void;
}

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

export interface WizardContextProviderProps {
steps: WizardControlStep[];
currentStepIndex: number;
activeStepIndex: number;
footer: WizardFooterType;
isStepVisitRequired: boolean;
children: React.ReactElement;
onNext(steps: WizardControlStep[]): void;
onBack(steps: WizardControlStep[]): void;
Expand All @@ -54,8 +49,7 @@ export interface WizardContextProviderProps {
export const WizardContextProvider: React.FunctionComponent<WizardContextProviderProps> = ({
steps: initialSteps,
footer: initialFooter,
currentStepIndex,
isStepVisitRequired,
activeStepIndex,
children,
onNext,
onBack,
Expand All @@ -68,35 +62,52 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
const [currentFooter, setCurrentFooter] = React.useState(
typeof initialFooter !== 'function' ? initialFooter : undefined
);
const currentStep = getCurrentStep(steps, currentStepIndex);

const goToNextStep = React.useCallback(() => onNext(steps), [onNext, steps]);
const goToPrevStep = React.useCallback(() => onBack(steps), [onBack, steps]);
// Combined initial and current state steps
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool application of useMemo here!

If this is just an array of all the steps, I'm wondering if allSteps or just steps might be a better name - it looks like this was just called steps initially? I wouldn't block over this, but it might help to have some more context here!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good suggestion. I agree, 'merged' doesn't tell much of a story. I'll update it to just be steps.

const mergedSteps = React.useMemo(
() =>
steps.map((currentStepProps, index) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { isVisited, ...initialStepProps } = initialSteps[index];

return {
...currentStepProps,
...initialStepProps
};
}),
[initialSteps, steps]
);
const activeStep = getActiveStep(mergedSteps, activeStepIndex);

const goToNextStep = React.useCallback(() => onNext(mergedSteps), [onNext, mergedSteps]);
const goToPrevStep = React.useCallback(() => onBack(mergedSteps), [onBack, mergedSteps]);

const footer = React.useMemo(() => {
const wizardFooter = currentFooter || initialFooter;
const wizardFooter = activeStep?.footer || currentFooter || initialFooter;

if (isCustomWizardFooter(wizardFooter)) {
const customFooter = wizardFooter;

return typeof customFooter === 'function'
? customFooter(currentStep, goToNextStep, goToPrevStep, onClose)
? customFooter(activeStep, goToNextStep, goToPrevStep, onClose)
: customFooter;
}

return (
<WizardFooter
currentStep={currentStep}
activeStep={activeStep}
onNext={goToNextStep}
onBack={goToPrevStep}
onClose={onClose}
isBackDisabled={currentStep?.id === steps[0]?.id}
isBackDisabled={activeStep?.id === mergedSteps[0]?.id}
{...wizardFooter}
/>
);
}, [currentFooter, initialFooter, currentStep, goToNextStep, goToPrevStep, onClose, steps]);
}, [currentFooter, initialFooter, activeStep, goToNextStep, goToPrevStep, onClose, mergedSteps]);

const getStep = React.useCallback((stepId: string | number) => steps.find(step => step.id === stepId), [steps]);
const getStep = React.useCallback((stepId: string | number) => mergedSteps.find(step => step.id === stepId), [
mergedSteps
]);

const setStep = React.useCallback(
(step: Pick<WizardControlStep, 'id'> & Partial<WizardControlStep>) =>
Expand All @@ -112,62 +123,21 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
[]
);

const toggleStep = React.useCallback(
(stepId: string | number, isHidden: boolean) =>
setSteps(prevSteps => {
let stepToHide: WizardControlStep;

return prevSteps.map(prevStep => {
if (prevStep.id === stepId) {
// Don't hide the currently active step or its parent (if a sub-step).
if (
isHidden &&
(currentStep.id === prevStep.id ||
(isWizardParentStep(prevStep) && prevStep.subStepIds.includes(currentStep.id)))
) {
// eslint-disable-next-line no-console
console.error('Wizard: Unable to hide the current step or its parent.');
return prevStep;
}

stepToHide = { ...prevStep, isHidden };
return stepToHide;
}

// When isStepVisitRequired is enabled, if the step was previously hidden and not visited yet,
// when it is shown, all steps beyond it should be disabled to ensure it is visited.
if (
isStepVisitRequired &&
stepToHide?.isHidden === false &&
!stepToHide?.isVisited &&
prevSteps.indexOf(stepToHide) < prevSteps.indexOf(prevStep)
) {
return { ...prevStep, isVisited: false };
}

return prevStep;
});
}),
[currentStep.id, isStepVisitRequired]
);

return (
<WizardContext.Provider
value={{
steps,
currentStep,
currentStepIndex,
steps: mergedSteps,
activeStep,
footer,
onClose,
getStep,
setStep,
toggleStep,
setFooter: setCurrentFooter,
onNext: goToNextStep,
onBack: goToPrevStep,
goToStepById: React.useCallback(id => goToStepById(steps, id), [goToStepById, steps]),
goToStepByName: React.useCallback(name => goToStepByName(steps, name), [goToStepByName, steps]),
goToStepByIndex: React.useCallback(index => goToStepByIndex(steps, index), [goToStepByIndex, steps])
goToStepById: React.useCallback(id => goToStepById(mergedSteps, id), [goToStepById, mergedSteps]),
goToStepByName: React.useCallback(name => goToStepByName(mergedSteps, name), [goToStepByName, mergedSteps]),
goToStepByIndex: React.useCallback(index => goToStepByIndex(mergedSteps, index), [goToStepByIndex, mergedSteps])
}}
>
{children}
Expand Down
Loading