diff --git a/packages/react-core/src/components/Wizard/examples/Wizard.md b/packages/react-core/src/components/Wizard/examples/Wizard.md index 027987b3e34..377c4969894 100644 --- a/packages/react-core/src/components/Wizard/examples/Wizard.md +++ b/packages/react-core/src/components/Wizard/examples/Wizard.md @@ -133,8 +133,10 @@ class IncrementallyEnabledStepsWizard extends React.Component { stepIdReached: 1 }; this.onNext = ({ id }) => { + const [, orderIndex] = id.split('-'); + this.setState({ - stepIdReached: this.state.stepIdReached < id ? id : this.state.stepIdReached + stepIdReached: this.state.stepIdReached < orderIndex ? orderIndex : this.state.stepIdReached }); }; this.closeWizard = () => { @@ -146,27 +148,27 @@ class IncrementallyEnabledStepsWizard extends React.Component { const { stepIdReached } = this.state; const steps = [ - { id: 'incrementally-enabled-1', name: 'First step', component:

Step 1 content

}, + { id: 'incrementallyEnabled-1', name: 'First step', component:

Step 1 content

}, { - id: 'incrementally-enabled-2', + id: 'incrementallyEnabled-2', name: 'Second step', component:

Step 2 content

, canJumpTo: stepIdReached >= 2 }, { - id: 'incrementally-enabled-3', + id: 'incrementallyEnabled-3', name: 'Third step', component:

Step 3 content

, canJumpTo: stepIdReached >= 3 }, { - id: 'incrementally-enabled-4', + id: 'incrementallyEnabled-4', name: 'Fourth step', component:

Step 4 content

, canJumpTo: stepIdReached >= 4 }, { - id: 'incrementally-enabled-5', + id: 'incrementallyEnabled-5', name: 'Review', component:

Review step content

, nextButtonText: 'Finish', @@ -311,8 +313,10 @@ class ValidationWizard extends React.Component { this.onNext = ({ id, name }, { prevId, prevName }) => { console.log(`current id: ${id}, current name: ${name}, previous id: ${prevId}, previous name: ${prevName}`); + const [, orderIndex] = id.split('-'); + this.setState({ - stepIdReached: this.state.stepIdReached < id ? id : this.state.stepIdReached + stepIdReached: this.state.stepIdReached < orderIndex ? orderIndex : this.state.stepIdReached }); this.areAllStepsValid(); }; @@ -812,10 +816,10 @@ class GetCurrentStepWizard extends React.Component { step: 1 }; this.onCurrentStepChanged = ({ id }) => { - this.setState({ - step: id - }); - } + this.setState({ + step: id + }); + }; this.closeWizard = () => { console.log('close wizard'); }; diff --git a/packages/react-core/src/next/components/Wizard/Wizard.tsx b/packages/react-core/src/next/components/Wizard/Wizard.tsx index 8034ab498bb..337ec12b536 100644 --- a/packages/react-core/src/next/components/Wizard/Wizard.tsx +++ b/packages/react-core/src/next/components/Wizard/Wizard.tsx @@ -42,8 +42,6 @@ export interface WizardProps extends React.HTMLProps { 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 */ @@ -66,7 +64,6 @@ export const Wizard = ({ nav, startIndex = 1, isStepVisitRequired = false, - hasUnmountedSteps = true, onNavByIndex, onNext, onBack, @@ -74,42 +71,37 @@ export const Wizard = ({ 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) => { @@ -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 ( {header} - + ); }; -const WizardInternal = ({ - nav, - hasUnmountedSteps, - isStepVisitRequired -}: Pick) => { - const { currentStep, steps, footer, goToStepByIndex } = useWizardContext(); +const WizardInternal = ({ nav, isStepVisitRequired }: Pick) => { + 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 ; - }, [currentStep, isStepVisitRequired, goToStepByIndex, isNavExpanded, nav, steps]); + }, [activeStep, isStepVisitRequired, goToStepByIndex, isNavExpanded, nav, steps]); return ( setIsNavExpanded(prevIsExpanded => !prevIsExpanded)} - hasUnmountedSteps={hasUnmountedSteps} /> ); }; diff --git a/packages/react-core/src/next/components/Wizard/WizardBody.tsx b/packages/react-core/src/next/components/Wizard/WizardBody.tsx index 872851d1c46..ab0362bb02b 100644 --- a/packages/react-core/src/next/components/Wizard/WizardBody.tsx +++ b/packages/react-core/src/next/components/Wizard/WizardBody.tsx @@ -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; /** An aria-label to use for the wrapper element */ 'aria-label'?: string; /** Sets the aria-labelledby attribute for the wrapper element */ @@ -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) => ( -
{children}
+
{children}
); diff --git a/packages/react-core/src/next/components/Wizard/WizardContext.tsx b/packages/react-core/src/next/components/Wizard/WizardContext.tsx index 8e9c25de173..c3f7cd88dc5 100644 --- a/packages/react-core/src/next/components/Wizard/WizardContext.tsx +++ b/packages/react-core/src/next/components/Wizard/WizardContext.tsx @@ -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 */ @@ -31,17 +29,14 @@ export interface WizardContextProps { getStep: (stepId: number | string) => WizardControlStep; /** Set step by ID */ setStep: (step: Pick & Partial) => 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; @@ -54,8 +49,7 @@ export interface WizardContextProviderProps { export const WizardContextProvider: React.FunctionComponent = ({ steps: initialSteps, footer: initialFooter, - currentStepIndex, - isStepVisitRequired, + activeStepIndex, children, onNext, onBack, @@ -64,43 +58,58 @@ export const WizardContextProvider: React.FunctionComponent { - const [steps, setSteps] = React.useState(initialSteps); + const [currentSteps, setCurrentSteps] = React.useState(initialSteps); const [currentFooter, setCurrentFooter] = React.useState( typeof initialFooter !== 'function' ? initialFooter : undefined ); - const currentStep = getCurrentStep(steps, currentStepIndex); + + // Combined initial and current state steps + const steps = React.useMemo( + () => + currentSteps.map((currentStepProps, index) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { isVisited, ...initialStepProps } = initialSteps[index]; + + return { + ...currentStepProps, + ...initialStepProps + }; + }), + [initialSteps, currentSteps] + ); + const activeStep = getActiveStep(steps, activeStepIndex); const goToNextStep = React.useCallback(() => onNext(steps), [onNext, steps]); const goToPrevStep = React.useCallback(() => onBack(steps), [onBack, steps]); 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 ( ); - }, [currentFooter, initialFooter, currentStep, goToNextStep, goToPrevStep, onClose, steps]); + }, [currentFooter, initialFooter, activeStep, goToNextStep, goToPrevStep, onClose, steps]); const getStep = React.useCallback((stepId: string | number) => steps.find(step => step.id === stepId), [steps]); const setStep = React.useCallback( (step: Pick & Partial) => - setSteps(prevSteps => + setCurrentSteps(prevSteps => prevSteps.map(prevStep => { if (prevStep.id === step.id) { return { ...prevStep, ...step }; @@ -112,56 +121,15 @@ export const WizardContextProvider: React.FunctionComponent - 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 ( WizardNavStepFunction | void; /** Back button callback */ @@ -43,9 +43,9 @@ export const WizardFooterWrapper = ({ children }: WizardFooterWrapperProps) => (
{children}
); -export const WizardFooter = ({ currentStep, ...internalProps }: WizardFooterProps) => { - const currentStepFooter = !isCustomWizardFooter(currentStep.footer) && currentStep.footer; - return ; +export const WizardFooter = ({ activeStep, ...internalProps }: WizardFooterProps) => { + const activeStepFooter = !isCustomWizardFooter(activeStep.footer) && activeStep.footer; + return ; }; const InternalWizardFooter = ({ @@ -59,7 +59,7 @@ const InternalWizardFooter = ({ nextButtonText = 'Next', backButtonText = 'Back', cancelButtonText = 'Cancel' -}: Omit) => ( +}: Omit) => ( - - - - ), - [goToNextStep, isLoading, onBack, onClose] - ); - return ( - - Step 3 content w/ custom async footer - + + + + + ); }; -const StepWithContextActions = (props: WizardStepProps) => { +const StepContentWithActions = () => { const { isToggleStepChecked, errorMessage, setIsToggleStepChecked, setErrorMessage } = React.useContext(SomeContext); - const navItem: WizardNavItemType = React.useCallback( - (step, currentStep, steps, goToStepByIndex) => ( - - ), - [errorMessage] - ); - return ( - + <> { id="toggle-hide-step-checkbox" name="Toggle Hide Step Checkbox" /> - + ); }; -const StepToHide = (props: WizardStepProps) => { - const { isToggleStepChecked } = React.useContext(SomeContext); - return ; -}; - export const WizardKitchenSink: React.FunctionComponent = () => { const onNext: WizardNavStepFunction = (_currentStep: WizardNavStepData, _previousStep: WizardNavStepData) => {}; return ( - } - footer={} - isStepVisitRequired - onNext={onNext} - hasUnmountedSteps={false} - > - - - - , - - Substep 2 content - - ]} - /> - Custom item }} - /> - - Review step content - - + {({ isToggleStepChecked, errorMessage }) => ( + } + footer={} + onNext={onNext} + isStepVisitRequired + > + + + + + + , + + Substep 2 content + + ]} + /> + Custom item + }} + footer={} + > + Step 3 content w/ custom async footer + + + Review step content + + + )} ); }; diff --git a/packages/react-core/src/next/components/Wizard/hooks/__tests__/useWizardFooter.test.tsx b/packages/react-core/src/next/components/Wizard/hooks/__tests__/useWizardFooter.test.tsx index cba661c66d9..f8dde2dae05 100644 --- a/packages/react-core/src/next/components/Wizard/hooks/__tests__/useWizardFooter.test.tsx +++ b/packages/react-core/src/next/components/Wizard/hooks/__tests__/useWizardFooter.test.tsx @@ -16,15 +16,15 @@ test('sets the footer when one is provided without a stepId', () => { expect(setFooter).toHaveBeenCalledWith(customFooter); }); -test(`sets the footer when the provided stepId matches the currentStep's id`, () => { - useWizardContextSpy.mockReturnValueOnce({ setFooter, currentStep: { id: 'curr-step-id' } } as any); +test(`sets the footer when the provided stepId matches the activeStep's id`, () => { + useWizardContextSpy.mockReturnValueOnce({ setFooter, activeStep: { id: 'curr-step-id' } } as any); renderHook(() => useWizardFooter(customFooter, 'curr-step-id')); expect(setFooter).toHaveBeenCalledWith(customFooter); }); -test(`does not set the footer when the provided stepId does not match the currentStep's id`, () => { - useWizardContextSpy.mockReturnValueOnce({ setFooter, currentStep: { id: 'curr-step-id' } } as any); +test(`does not set the footer when the provided stepId does not match the activeStep's id`, () => { + useWizardContextSpy.mockReturnValueOnce({ setFooter, activeStep: { id: 'curr-step-id' } } as any); renderHook(() => useWizardFooter(customFooter, 'some-other-step-id')); expect(setFooter).not.toHaveBeenCalled(); diff --git a/packages/react-core/src/next/components/Wizard/hooks/useWizardFooter.tsx b/packages/react-core/src/next/components/Wizard/hooks/useWizardFooter.tsx index 56a49afd9ea..f41b4e0d82b 100644 --- a/packages/react-core/src/next/components/Wizard/hooks/useWizardFooter.tsx +++ b/packages/react-core/src/next/components/Wizard/hooks/useWizardFooter.tsx @@ -9,10 +9,10 @@ import { WizardFooterProps } from '../WizardFooter'; * @param stepId */ export const useWizardFooter = (footer: React.ReactElement | Partial, stepId?: string | number) => { - const { currentStep, setFooter } = useWizardContext(); + const { activeStep, setFooter } = useWizardContext(); React.useEffect(() => { - if (footer && (!stepId || currentStep?.id === stepId)) { + if (footer && (!stepId || activeStep?.id === stepId)) { setFooter(footer); // Reset the footer on unmount. @@ -20,5 +20,5 @@ export const useWizardFooter = (footer: React.ReactElement | Partial | CustomWizardNavIte export type CustomWizardNavFunction = ( isExpanded: boolean, steps: WizardControlStep[], - currentStep: WizardControlStep, + activeStep: WizardControlStep, goToStepByIndex: (index: number) => void ) => React.ReactElement; /** Callback for the Wizard's 'navItem' property. Returns element which replaces the WizardStep's default navigation item. */ export type CustomWizardNavItemFunction = ( step: WizardControlStep, - currentStep: WizardControlStep, + activeStep: WizardControlStep, steps: WizardControlStep[], goToStepByIndex: (index: number) => void ) => React.ReactElement; /** Callback for the Wizard's 'footer' property. Returns element which replaces the Wizard's default footer. */ export type CustomWizardFooterFunction = ( - currentStep: WizardControlStep, + activeStep: WizardControlStep, onNext: () => void, onBack: () => void, onClose: () => void diff --git a/packages/react-core/src/next/components/Wizard/utils.ts b/packages/react-core/src/next/components/Wizard/utils.ts index ede2574412c..109523349f3 100644 --- a/packages/react-core/src/next/components/Wizard/utils.ts +++ b/packages/react-core/src/next/components/Wizard/utils.ts @@ -9,27 +9,37 @@ import { WizardStep, WizardStepProps } from './WizardStep'; * @returns WizardControlStep[] */ export const buildSteps = (children: React.ReactElement | React.ReactElement[]) => - React.Children.toArray(children).reduce((acc: WizardControlStep[], child: React.ReactChild | React.ReactFragment) => { + React.Children.toArray(children).reduce((acc: WizardControlStep[], child: React.ReactNode) => { if (isWizardStep(child)) { - const { steps: subSteps, id } = child.props; - - acc.push({ - component: child, - ...normalizeStep(child.props), - ...(subSteps && { - subStepIds: subSteps?.map(subStep => { - acc.push({ - ...normalizeStep(subStep.props), - component: subStep, - parentId: id - }); - - return subStep.props.id; - }) - }) - }); + const { steps: subSteps, id, isHidden, isDisabled } = child.props; + const subControlledSteps: WizardControlStep[] = []; + const stepIndex = acc.length + 1; + + acc.push( + { + index: stepIndex, + component: child, + ...(stepIndex === 1 && { isVisited: true }), + ...(subSteps && { + subStepIds: subSteps?.map((subStep, subStepIndex) => { + subControlledSteps.push({ + isHidden, + isDisabled, + component: subStep, + parentId: id, + index: stepIndex + subStepIndex + 1, + ...normalizeStep(subStep.props) + }); + + return subStep.props.id; + }) + }), + ...normalizeStep(child.props) + }, + ...subControlledSteps + ); } else { - throw new Error('Wizard only accepts children of type WizardStep'); + throw new Error('Wizard only accepts children with required WizardStepProps.'); } return acc; @@ -45,14 +55,14 @@ export function isWizardStep( } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const normalizeStep = ({ children, steps, body, ...controlStep }: WizardStepProps): WizardControlStep => +export const normalizeStep = ({ children, steps, ...controlStep }: WizardStepProps): Omit => controlStep; -export const normalizeNavStep = (navStep: WizardControlStep, steps: WizardControlStep[]): WizardNavStepData => ({ +export const normalizeNavStep = (navStep: WizardControlStep): WizardNavStepData => ({ id: navStep.id, - name: navStep.name.toString(), - index: steps.indexOf(navStep) + 1 + index: navStep.index, + name: navStep.name.toString() }); -export const getCurrentStep = (steps: WizardControlStep[], currentStepIndex: number) => - steps.find((_, index) => index + 1 === currentStepIndex); +export const getActiveStep = (steps: WizardControlStep[], activeStepIndex: number) => + steps.find(step => step.index === activeStepIndex);