,
+ canJumpTo: stepIdReached >= 4
+ },
{
id: 'incrementally-enabled-5',
name: 'Review',
diff --git a/packages/react-core/src/next/components/Wizard/Wizard.tsx b/packages/react-core/src/next/components/Wizard/Wizard.tsx
new file mode 100644
index 00000000000..040a8061335
--- /dev/null
+++ b/packages/react-core/src/next/components/Wizard/Wizard.tsx
@@ -0,0 +1,184 @@
+import React from 'react';
+
+import { css } from '@patternfly/react-styles';
+import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
+
+import {
+ DefaultWizardFooterProps,
+ DefaultWizardNavProps,
+ isCustomWizardFooter,
+ isWizardParentStep,
+ WizardNavStepFunction,
+ CustomWizardNavFunction
+} from './types';
+import { buildSteps, normalizeNavStep } from './utils';
+import { useWizardContext, WizardContextProvider } from './WizardContext';
+import { WizardStepProps } from './WizardStep';
+import { WizardFooter } from './WizardFooter';
+import { WizardToggle } from './WizardToggle';
+
+/**
+ * Wrapper for all steps and hosts state, including navigation helpers, within context.
+ * The WizardContext provided by default gives any child of wizard access to those resources.
+ */
+
+export interface WizardProps extends React.HTMLProps {
+ /** Step components */
+ children: React.ReactElement | React.ReactElement[];
+ /** Wizard header */
+ header?: React.ReactNode;
+ /** Wizard footer */
+ footer?: DefaultWizardFooterProps | React.ReactElement;
+ /** Default wizard nav props or a custom WizardNav (with callback) */
+ nav?: DefaultWizardNavProps | CustomWizardNavFunction;
+ /** The initial index the wizard is to start on (1 or higher). Defaults to 1. */
+ startIndex?: number;
+ /** Additional classes spread to the wizard */
+ className?: string;
+ /** Custom width of the wizard */
+ width?: number | string;
+ /** Custom height of the wizard */
+ height?: number | string;
+ /** Callback function when a step in the nav is clicked */
+ onNavByIndex?: WizardNavStepFunction;
+ /** Callback function after next button is clicked */
+ onNext?: WizardNavStepFunction;
+ /** Callback function after back button is clicked */
+ onBack?: WizardNavStepFunction;
+ /** Callback function to save at the end of the wizard, if not specified uses onClose */
+ onSave?: () => void;
+ /** Callback function to close the wizard */
+ onClose?: () => void;
+}
+
+export const Wizard = (props: WizardProps) => {
+ const { startIndex = 1, children, footer, onNavByIndex, onNext, onBack, onSave, onClose, ...internalProps } = props;
+ const [currentStepIndex, setCurrentStepIndex] = React.useState(startIndex);
+ const steps = buildSteps(children);
+
+ const goToStepByIndex = (index: number) => {
+ const lastStepIndex = steps.length;
+
+ if (index < 1) {
+ index = 1;
+ } else if (index > lastStepIndex) {
+ index = lastStepIndex;
+ }
+
+ const currStep = steps[index - 1];
+ const prevStep = steps[currentStepIndex - 1];
+ setCurrentStepIndex(index);
+
+ return onNavByIndex?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
+ };
+
+ const goToNextStep = () => {
+ // Save when on the last step, otherwise close
+ if (currentStepIndex >= steps.length) {
+ if (onSave) {
+ return onSave();
+ }
+
+ return onClose?.();
+ }
+
+ let currStep = steps[currentStepIndex];
+ let newStepIndex = currentStepIndex + 1;
+ const prevStep = steps[currentStepIndex - 1];
+
+ // Skip parent step and focus on the first sub-step if they exist
+ if (isWizardParentStep(currStep)) {
+ newStepIndex += 1;
+ currStep = steps[currentStepIndex + 1];
+ }
+
+ setCurrentStepIndex(newStepIndex);
+ return onNext?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
+ };
+
+ const goToPrevStep = () => {
+ if (steps.length < currentStepIndex) {
+ // Previous step was removed, just update the currentStep state
+ setCurrentStepIndex(steps.length);
+ } 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));
+ }
+ };
+
+ const goToStepById = (id: number | string) => {
+ const stepIndex = steps.findIndex(step => step.id === id) + 1;
+ stepIndex > 0 && setCurrentStepIndex(stepIndex);
+ };
+
+ const goToStepByName = (name: string) => {
+ const stepIndex = steps.findIndex(step => step.name === name) + 1;
+ stepIndex > 0 && setCurrentStepIndex(stepIndex);
+ };
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+// eslint-disable-next-line patternfly-react/no-anonymous-functions
+const WizardInternal = ({ height, width, className, header, footer, nav, ...divProps }: WizardProps) => {
+ const { activeStep, steps, footer: customFooter, onNext, onBack, onClose, goToStepByIndex } = useWizardContext();
+
+ const wizardFooter = customFooter || (
+
+ );
+
+ return (
+
+ {header}
+
+
+ );
+};
+
+Wizard.displayName = 'Wizard';
diff --git a/packages/react-core/src/next/components/Wizard/WizardBody.tsx b/packages/react-core/src/next/components/Wizard/WizardBody.tsx
new file mode 100644
index 00000000000..51cd195facc
--- /dev/null
+++ b/packages/react-core/src/next/components/Wizard/WizardBody.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+
+import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
+import { css } from '@patternfly/react-styles';
+
+/**
+ * Used as a wrapper for WizardStep content, where the wrapping element is customizable.
+ */
+
+export interface WizardBodyProps {
+ children?: React.ReactNode | React.ReactNode[];
+ /** Set to true to remove the default body padding */
+ hasNoBodyPadding?: boolean;
+ /** An aria-label to use for the wrapper element */
+ 'aria-label'?: string;
+ /** Sets the aria-labelledby attribute for the wrapper element */
+ 'aria-labelledby'?: string;
+ /** Component used as the wrapping content container */
+ wrapperElement?: React.ElementType;
+}
+
+export const WizardBody = ({
+ children,
+ hasNoBodyPadding = false,
+ 'aria-label': ariaLabel,
+ 'aria-labelledby': ariaLabelledBy,
+ wrapperElement: Wrapper = 'div'
+}: WizardBodyProps) => (
+
+
{children}
+
+);
+
+WizardBody.displayName = 'WizardBody';
diff --git a/packages/react-core/src/next/components/Wizard/WizardContext.tsx b/packages/react-core/src/next/components/Wizard/WizardContext.tsx
new file mode 100644
index 00000000000..8859d1b5879
--- /dev/null
+++ b/packages/react-core/src/next/components/Wizard/WizardContext.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { WizardControlStep } from './types';
+import { getActiveStep } from './utils';
+
+export interface WizardContextProps {
+ /** List of steps */
+ steps: WizardControlStep[];
+ /** Active step */
+ activeStep: WizardControlStep;
+ /** Footer element */
+ footer: React.ReactElement;
+ /** Navigate to the next step */
+ onNext: () => void;
+ /** Navigate to the previous step */
+ onBack: () => void;
+ /** Close the wizard */
+ onClose: () => void;
+ /** Navigate to step by ID */
+ goToStepById: (id: number | string) => void;
+ /** Navigate to step by name */
+ goToStepByName: (name: string) => void;
+ /** Navigate to step by index */
+ goToStepByIndex: (index: number) => void;
+ /** Update the footer with any react element */
+ setFooter: (footer: React.ReactElement) => void;
+}
+
+export const WizardContext = React.createContext({} as WizardContextProps);
+
+interface WizardContextRenderProps {
+ steps: WizardControlStep[];
+ activeStep: WizardControlStep;
+ footer: React.ReactElement;
+ onNext(): void;
+ onBack(): void;
+ onClose(): void;
+}
+
+export interface WizardContextProviderProps {
+ steps: WizardControlStep[];
+ currentStepIndex: number;
+ footer: React.ReactElement;
+ children: React.ReactElement | ((props: WizardContextRenderProps) => React.ReactElement);
+ onNext(): void;
+ onBack(): void;
+ onClose(): void;
+ goToStepById(id: number | string): void;
+ goToStepByName(name: string): void;
+ goToStepByIndex(index: number): void;
+}
+
+// eslint-disable-next-line patternfly-react/no-anonymous-functions
+export const WizardContextProvider: React.FunctionComponent = ({
+ steps: initialSteps,
+ footer: initialFooter,
+ currentStepIndex,
+ children,
+ onNext,
+ onBack,
+ onClose,
+ goToStepById,
+ goToStepByName,
+ goToStepByIndex
+}) => {
+ const [steps, setSteps] = React.useState(initialSteps);
+ const [footer, setFooter] = React.useState(initialFooter);
+ const activeStep = getActiveStep(steps, currentStepIndex);
+
+ // When the active step changes and the newly active step isn't visited, set the visited flag to true.
+ React.useEffect(() => {
+ if (activeStep && !activeStep?.visited) {
+ setSteps(prevSteps =>
+ prevSteps.map(step => {
+ if (step.id === activeStep.id) {
+ return { ...step, visited: true };
+ }
+
+ return step;
+ })
+ );
+ }
+ }, [activeStep]);
+
+ return (
+
+ {typeof children === 'function' ? children({ activeStep, steps, footer, onNext, onBack, onClose }) : children}
+
+ );
+};
+
+export const useWizardContext = () => React.useContext(WizardContext);
diff --git a/packages/react-core/src/next/components/Wizard/WizardFooter.tsx b/packages/react-core/src/next/components/Wizard/WizardFooter.tsx
new file mode 100644
index 00000000000..e99371969d2
--- /dev/null
+++ b/packages/react-core/src/next/components/Wizard/WizardFooter.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+
+import { css } from '@patternfly/react-styles';
+import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
+
+import { Button, ButtonVariant } from '../../../components/Button';
+import { WizardControlStep, WizardNavStepFunction } from './types';
+
+/**
+ * Hosts the standard structure of a footer with ties to the active step so that text for buttons can vary from step to step.
+ */
+
+export interface WizardFooterProps {
+ /** The currently active WizardStep */
+ activeStep: WizardControlStep;
+ /** Next button callback */
+ onNext: () => WizardNavStepFunction | void;
+ /** Back button callback */
+ onBack: () => WizardNavStepFunction | void;
+ /** Cancel link callback */
+ onClose: () => void;
+ /** Custom text for the Next button. The activeStep's nextButtonText takes precedence. */
+ nextButtonText?: React.ReactNode;
+ /** Custom text for the Back button */
+ backButtonText?: React.ReactNode;
+ /** Custom text for the Cancel link */
+ cancelButtonText?: React.ReactNode;
+ /** Optional flag to disable the first step's back button */
+ disableBackButton?: boolean;
+}
+
+export const WizardFooter = ({
+ onNext,
+ onBack,
+ onClose,
+ activeStep,
+ disableBackButton,
+ nextButtonText = 'Next',
+ backButtonText = 'Back',
+ cancelButtonText = 'Cancel'
+}: WizardFooterProps) => (
+
+);
+
+WizardFooter.displayName = 'WizardFooter';
diff --git a/packages/react-core/src/next/components/Wizard/WizardHeader.tsx b/packages/react-core/src/next/components/Wizard/WizardHeader.tsx
new file mode 100644
index 00000000000..a41307fcb4f
--- /dev/null
+++ b/packages/react-core/src/next/components/Wizard/WizardHeader.tsx
@@ -0,0 +1,53 @@
+import * as React from 'react';
+import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
+import { css } from '@patternfly/react-styles';
+import { Button } from '../../../components/Button';
+import { Title } from '../../../components/Title';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
+
+export interface WizardHeaderProps {
+ /** Callback function called when the X (Close) button is clicked */
+ onClose?: () => void;
+ /** Title of the wizard */
+ title: string;
+ /** Description of the wizard */
+ description?: React.ReactNode;
+ /** Component type of the description */
+ descriptionComponent?: 'div' | 'p';
+ /** Flag indicating whether the close button should be in the header */
+ hideClose?: boolean;
+ /** Aria-label applied to the X (Close) button */
+ closeButtonAriaLabel?: string;
+ /** id for the title */
+ titleId?: string;
+ /** id for the description */
+ descriptionId?: string;
+}
+
+export const WizardHeader: React.FunctionComponent = ({
+ onClose = () => undefined,
+ title,
+ description,
+ hideClose,
+ closeButtonAriaLabel,
+ titleId,
+ descriptionComponent: Component = 'p',
+ descriptionId
+}: WizardHeaderProps) => (
+