Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
171 changes: 107 additions & 64 deletions packages/react-core/src/next/components/Wizard/Wizard.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import React from 'react';
import findLastIndex from 'lodash/findLastIndex';

import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/Wizard/wizard';

import {
DefaultWizardFooterProps,
DefaultWizardNavProps,
isWizardParentStep,
WizardNavStepFunction,
CustomWizardNavFunction
WizardControlStep,
isCustomWizardNav,
WizardFooterType,
WizardNavType
} from './types';
import { buildSteps, normalizeNavStep } from './utils';
import { WizardContextProvider } from './WizardContext';
import { useWizardContext, WizardContextProvider } from './WizardContext';
import { WizardStepProps } from './WizardStep';
import { WizardToggleInternal } from './WizardToggle';
import { WizardToggle } from './WizardToggle';
import { WizardNavInternal } from './WizardNavInternal';

/**
* Wrapper for all steps and hosts state, including navigation helpers, within context.
Expand All @@ -26,9 +29,9 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
/** Wizard header */
header?: React.ReactNode;
/** Wizard footer */
footer?: DefaultWizardFooterProps | React.ReactElement;
/** Wizard nav */
nav?: DefaultWizardNavProps | CustomWizardNavFunction;
footer?: WizardFooterType;
/** Wizard navigation */
nav?: WizardNavType;
/** The initial index the wizard is to start on (1 or higher). Defaults to 1. */
startIndex?: number;
/** Additional classes spread to the wizard */
Expand All @@ -37,9 +40,11 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
width?: number | string;
/** Custom height of the wizard */
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 */
unmountInactiveSteps?: boolean;
/** Callback function when a step in the nav is clicked */
hasUnmountedSteps?: boolean;
/** Callback function when a step in the navigation is clicked */
onNavByIndex?: WizardNavStepFunction;
/** Callback function after next button is clicked */
onNext?: WizardNavStepFunction;
Expand All @@ -52,15 +57,16 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
}

export const Wizard = ({
startIndex = 1,
children,
footer,
height,
width,
className,
header,
nav,
unmountInactiveSteps,
startIndex = 1,
isStepVisitRequired = false,
hasUnmountedSteps = true,
onNavByIndex,
onNext,
onBack,
Expand All @@ -69,83 +75,91 @@ export const Wizard = ({
...wrapperProps
}: WizardProps) => {
const [currentStepIndex, setCurrentStepIndex] = React.useState(startIndex);
const steps = buildSteps(children);
const initialSteps = buildSteps(children);

const goToStepByIndex = (index: number) => {
const lastStepIndex = steps.length;
const goToNextStep = (steps: WizardControlStep[] = initialSteps) => {
const newStepIndex =
steps.findIndex((step, index) => index + 1 > currentStepIndex && !step.isHidden && !isWizardParentStep(step)) + 1;

if (index < 1) {
index = 1;
} else if (index > lastStepIndex) {
index = lastStepIndex;
if (currentStepIndex >= steps.length || !newStepIndex) {
return onSave ? onSave() : onClose?.();
}

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

return onNavByIndex?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
setCurrentStepIndex(newStepIndex);

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

const goToNextStep = () => {
// Save when on the last step, otherwise close
if (currentStepIndex >= steps.length) {
if (onSave) {
return onSave();
}
const goToPrevStep = (steps: WizardControlStep[] = initialSteps) => {
const newStepIndex =
findLastIndex(
steps,
(step: WizardControlStep, index: number) =>
index + 1 < currentStepIndex && !step.isHidden && !isWizardParentStep(step)
) + 1;
const currStep = isWizardParentStep(steps[currentStepIndex - 2])
? steps[currentStepIndex - 3]
: steps[currentStepIndex - 2];
const prevStep = steps[currentStepIndex - 1];

return onClose?.();
}
setCurrentStepIndex(newStepIndex);

let currStep = steps[currentStepIndex];
let newStepIndex = currentStepIndex + 1;
const prevStep = steps[currentStepIndex - 1];
return onBack?.(normalizeNavStep(currStep, steps), normalizeNavStep(prevStep, steps));
};

const goToStepByIndex = (steps: WizardControlStep[] = initialSteps, index: number) => {
const lastStepIndex = steps.length + 1;

// Skip parent step and focus on the first sub-step if they exist
if (isWizardParentStep(currStep)) {
newStepIndex += 1;
currStep = steps[currentStepIndex + 1];
// Handle index when out of bounds or hidden
if (index < 1) {
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.');
}

setCurrentStepIndex(newStepIndex);
return onNext?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
const currStep = steps[index - 1];
const prevStep = steps[currentStepIndex - 1];
setCurrentStepIndex(index);

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

const goToPrevStep = () => {
if (steps.length < currentStepIndex) {
// Previous step was removed, just update the currentStep state
setCurrentStepIndex(steps.length);
const goToStepById = (steps: WizardControlStep[] = initialSteps, id: number | string) => {
const stepIndex = steps.findIndex(step => step.id === id) + 1;

if (stepIndex > 0 && stepIndex < steps.length + 1 && !steps[stepIndex].isHidden) {
setCurrentStepIndex(stepIndex);
} else {
let currStep = steps[currentStepIndex - 2];
let newStepIndex = currentStepIndex - 1;
const prevStep = steps[currentStepIndex - 1];

// // Skip parent step and focus on the step prior
if (isWizardParentStep(currStep)) {
newStepIndex -= 1;
currStep = steps[currentStepIndex - 3];
}

setCurrentStepIndex(newStepIndex);
return onBack?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
// eslint-disable-next-line no-console
console.error(`Wizard: Unable to navigate to step with id: ${id}.`);
}
};

const goToStepById = (id: number | string) => {
const stepIndex = steps.findIndex(step => step.id === id) + 1;
stepIndex > 0 && setCurrentStepIndex(stepIndex);
};
const goToStepByName = (steps: WizardControlStep[] = initialSteps, name: string) => {
const stepIndex = initialSteps.findIndex(step => step.name === name) + 1;

const goToStepByName = (name: string) => {
const stepIndex = steps.findIndex(step => step.name === name) + 1;
stepIndex > 0 && setCurrentStepIndex(stepIndex);
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}.`);
}
};

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

const WizardInternal = ({
nav,
hasUnmountedSteps,
isStepVisitRequired
}: Pick<WizardProps, 'nav' | 'hasUnmountedSteps' | 'isStepVisitRequired'>) => {
const { currentStep, 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 <WizardNavInternal nav={nav} isNavExpanded={isNavExpanded} isStepVisitRequired={isStepVisitRequired} />;
}, [currentStep, isStepVisitRequired, goToStepByIndex, isNavExpanded, nav, steps]);

return (
<WizardToggle
nav={wizardNav}
footer={footer}
steps={steps}
currentStep={currentStep}
isNavExpanded={isNavExpanded}
toggleNavExpanded={() => setIsNavExpanded(prevIsExpanded => !prevIsExpanded)}
hasUnmountedSteps={hasUnmountedSteps}
/>
);
};

Wizard.displayName = 'Wizard';
8 changes: 4 additions & 4 deletions packages/react-core/src/next/components/Wizard/WizardBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ export interface WizardBodyProps {
/** Sets the aria-labelledby attribute for the wrapper element */
'aria-labelledby'?: string;
/** Component used as the wrapping content container */
wrapperElement?: React.ElementType;
component?: React.ElementType;
}

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

WizardBody.displayName = 'WizardBody';
Loading