Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
92 changes: 26 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,15 @@
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 { useGetMergedSteps } from './hooks/useGetMergedSteps';
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 +30,16 @@ 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;
/** Set multiple steps */
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this prop used for? Could you expand on the purpose/use case for setSteps?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's currently used in the WizardStep to update the state of multiple steps at once. In that particular case, if there is a step that was previously hidden and then shown, the steps beyond that newly shown step, if they were already visited, are set to not-visited. The purpose being when isStepVisitRequired is enabled, we want to make sure the newly shown step is not skipped over.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After thinking about this a bit more, I think the better default behavior is to not disable the newly shown step if a step proceeding it was already visited. In terms of automatic behavior, this makes more sense to me at least.

As a result of this, I removed this prop as it's not needed for now at least as a part of this change.

setSteps: React.Dispatch<React.SetStateAction<WizardControlStep[]>>;
}

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 +52,7 @@ export interface WizardContextProviderProps {
export const WizardContextProvider: React.FunctionComponent<WizardContextProviderProps> = ({
steps: initialSteps,
footer: initialFooter,
currentStepIndex,
isStepVisitRequired,
activeStepIndex,
children,
onNext,
onBack,
Expand All @@ -68,35 +65,38 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
const [currentFooter, setCurrentFooter] = React.useState(
typeof initialFooter !== 'function' ? initialFooter : undefined
);
const currentStep = getCurrentStep(steps, currentStepIndex);
const mergedSteps = useGetMergedSteps(initialSteps, steps);
const activeStep = getActiveStep(mergedSteps, activeStepIndex);

const goToNextStep = React.useCallback(() => onNext(steps), [onNext, steps]);
const goToPrevStep = React.useCallback(() => onBack(steps), [onBack, steps]);
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 +112,22 @@ 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,
setSteps,
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