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
4 changes: 2 additions & 2 deletions packages/react-core/src/components/Menu/MenuGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface MenuGroupProps extends Omit<React.HTMLProps<HTMLElement>, 'labe
/** Additional classes added to the MenuGroup */
className?: string;
/** Group label */
label?: React.ReactNode | React.FC;
label?: React.ReactNode;
/** ID for title label */
titleId?: string;
/** Forwarded ref */
Expand All @@ -32,7 +32,7 @@ const MenuGroupBase: React.FunctionComponent<MenuGroupProps> = ({
<>
{['function', 'string'].includes(typeof label) ? (
<Wrapper className={css(styles.menuGroupTitle)} id={titleId}>
{label as React.ReactNode}
{label}
</Wrapper>
) : (
label
Expand Down
117 changes: 71 additions & 46 deletions packages/react-core/src/next/components/Wizard/Wizard.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
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 {
isWizardParentStep,
WizardNavStepFunction,
WizardControlStep,
WizardStepType,
isCustomWizardNav,
WizardFooterType,
WizardNavType
WizardNavType,
WizardStepChangeScope
} from './types';
import { buildSteps, normalizeNavStep } from './utils';
import { buildSteps } from './utils';
import { useWizardContext, WizardContextProvider } from './WizardContext';
import { WizardStepProps } from './WizardStep';
import { WizardToggle } from './WizardToggle';
import { WizardNavInternal } from './WizardNavInternal';

Expand All @@ -25,7 +23,7 @@ import { WizardNavInternal } from './WizardNavInternal';

export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
/** Step components */
children: React.ReactElement<WizardStepProps> | React.ReactElement<WizardStepProps>[];
children: React.ReactNode | React.ReactNode[];
/** Wizard header */
header?: React.ReactNode;
/** Wizard footer */
Expand All @@ -40,18 +38,21 @@ 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;
/** Callback function when a step in the navigation is clicked */
onNavByIndex?: WizardNavStepFunction;
/** Callback function after next button is clicked */
onNext?: WizardNavStepFunction;
/** Callback function after back button is clicked */
onBack?: WizardNavStepFunction;
/** Disables steps that haven't been visited. Defaults to false. */
isVisitRequired?: boolean;
/** Progressively shows steps, where all steps following the active step are hidden. Defaults to false. */
isProgressive?: boolean;
/** Callback function when navigating between steps */
onStepChange?: (
Copy link
Contributor

Choose a reason for hiding this comment

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

@tlabaj can correct me if I'm off, but I think we are trying to standardize all callback prop types exposed to the consumers across the library so that they all pass the event back as the first parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would certainly complicate things if that were true for this case. This function is a consolidation of 3 that were already defined as a type without an event handler bound as the first parameter, so I wonder if this could be handled separately as a part of the broader effort to make this change in PF?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @jeffpuzzo we are trying to standardize your callback signatures so that an event is always passed and it is the first parameter. This was an ask from consumers. Some consumers have to wrap PF components in order to use 3rd party libs like Formik due to our inconsistent/non standard following callbacks.

I am ok with opening a separate issue to tackle that, but we would want to get that in for our major release.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tlabaj @nicolethoen I ended up just trying to add those event props as a part of this PR. Please have a look when you have a moment.

event: React.MouseEvent<HTMLButtonElement>,
currentStep: WizardStepType,
prevStep: WizardStepType,
scope: WizardStepChangeScope
) => void | Promise<void>;
/** Callback function to save at the end of the wizard, if not specified uses onClose */
onSave?: () => void | Promise<void>;
onSave?: (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
/** Callback function to close the wizard */
onClose?: () => void;
onClose?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}

export const Wizard = ({
Expand All @@ -63,48 +64,61 @@ export const Wizard = ({
header,
nav,
startIndex = 1,
isStepVisitRequired = false,
onNavByIndex,
onNext,
onBack,
isVisitRequired = false,
isProgressive = false,
onStepChange,
onSave,
onClose,
...wrapperProps
}: WizardProps) => {
const [activeStepIndex, setActiveStepIndex] = React.useState(startIndex);
const initialSteps = buildSteps(children);
const firstStepRef = React.useRef(initialSteps[startIndex - 1]);

const goToNextStep = (steps: WizardControlStep[] = initialSteps) => {
const newStepIndex = steps.find(step => step.index > activeStepIndex && !step.isHidden && !isWizardParentStep(step))
?.index;
// When the startIndex maps to a parent step, focus on the first sub-step
React.useEffect(() => {
if (isWizardParentStep(firstStepRef.current)) {
setActiveStepIndex(startIndex + 1);
}
}, [startIndex]);

const goToNextStep = (event: React.MouseEvent<HTMLButtonElement>, steps: WizardStepType[] = initialSteps) => {
const newStep = steps.find(
step => step.index > activeStepIndex && !step.isHidden && !step.isDisabled && !isWizardParentStep(step)
);

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

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

setActiveStepIndex(newStepIndex);
return onNext?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
setActiveStepIndex(newStep?.index);
onStepChange?.(event, currStep, prevStep, WizardStepChangeScope.Next);
};

const goToPrevStep = (steps: WizardControlStep[] = initialSteps) => {
const newStepIndex =
findLastIndex(
steps,
(step: WizardControlStep) => step.index < activeStepIndex && !step.isHidden && !isWizardParentStep(step)
) + 1;
const goToPrevStep = (event: React.MouseEvent<HTMLButtonElement>, steps: WizardStepType[] = initialSteps) => {
const newStep = [...steps]
.reverse()
.find(
(step: WizardStepType) =>
step.index < activeStepIndex && !step.isHidden && !step.isDisabled && !isWizardParentStep(step)
);
const currStep = isWizardParentStep(steps[activeStepIndex - 2])
? steps[activeStepIndex - 3]
: steps[activeStepIndex - 2];
const prevStep = steps[activeStepIndex - 1];

setActiveStepIndex(newStepIndex);
return onBack?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
setActiveStepIndex(newStep?.index);
onStepChange?.(event, currStep, prevStep, WizardStepChangeScope.Back);
};

const goToStepByIndex = (steps: WizardControlStep[] = initialSteps, index: number) => {
const goToStepByIndex = (
event: React.MouseEvent<HTMLButtonElement>,
steps: WizardStepType[] = initialSteps,
index: number
) => {
const lastStepIndex = steps.length + 1;

// Handle index when out of bounds or hidden
Expand All @@ -118,25 +132,25 @@ export const Wizard = ({
const prevStep = steps[activeStepIndex - 1];

setActiveStepIndex(index);
return onNavByIndex?.(normalizeNavStep(currStep), normalizeNavStep(prevStep));
onStepChange?.(event, currStep, prevStep, WizardStepChangeScope.Nav);
};

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

if (stepIndex > 0 && stepIndex < lastStepIndex && !step.isHidden) {
if (stepIndex > 0 && stepIndex < lastStepIndex && !step.isDisabled && !step.isHidden) {
setActiveStepIndex(stepIndex);
}
};

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

if (stepIndex > 0 && stepIndex < lastStepIndex && !step.isHidden) {
if (stepIndex > 0 && stepIndex < lastStepIndex && !step.isDisabled && !step.isHidden) {
setActiveStepIndex(stepIndex);
}
};
Expand All @@ -162,13 +176,17 @@ export const Wizard = ({
{...wrapperProps}
>
{header}
<WizardInternal nav={nav} isStepVisitRequired={isStepVisitRequired} />
<WizardInternal nav={nav} isVisitRequired={isVisitRequired} isProgressive={isProgressive} />
</div>
</WizardContextProvider>
);
};

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

Expand All @@ -177,8 +195,15 @@ const WizardInternal = ({ nav, isStepVisitRequired }: Pick<WizardProps, 'nav' |
return typeof nav === 'function' ? nav(isNavExpanded, steps, activeStep, goToStepByIndex) : nav;
}

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

return (
<WizardToggle
Expand Down
81 changes: 38 additions & 43 deletions packages/react-core/src/next/components/Wizard/WizardContext.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import React from 'react';

import { isCustomWizardFooter, WizardControlStep, WizardFooterType } from './types';
import { isCustomWizardFooter, isWizardSubStep, WizardStepType, WizardFooterType } from './types';
import { getActiveStep } from './utils';
import { useGetMergedSteps } from './hooks';
import { WizardFooter, WizardFooterProps } from './WizardFooter';

export interface WizardContextProps {
/** List of steps */
steps: WizardControlStep[];
steps: WizardStepType[];
/** Current step */
activeStep: WizardControlStep;
activeStep: WizardStepType;
/** Footer element */
footer: React.ReactElement;
/** Close the wizard */
close: () => void;
/** Navigate to the next step */
onNext: () => void | Promise<void>;
goToNextStep: () => void | Promise<void>;
/** Navigate to the previous step */
onBack: () => void | Promise<void>;
/** Close the wizard */
onClose: () => void;
goToPrevStep: () => void | Promise<void>;
/** Navigate to step by ID */
goToStepById: (id: number | string) => void;
/** Navigate to step by name */
Expand All @@ -26,24 +27,28 @@ export interface WizardContextProps {
/** Update the footer with any react element */
setFooter: (footer: React.ReactElement | Partial<WizardFooterProps>) => void;
/** Get step by ID */
getStep: (stepId: number | string) => WizardControlStep;
getStep: (stepId: number | string) => WizardStepType;
/** Set step by ID */
setStep: (step: Pick<WizardControlStep, 'id'> & Partial<WizardControlStep>) => void;
setStep: (step: Pick<WizardStepType, 'id'> & Partial<WizardStepType>) => void;
}

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

export interface WizardContextProviderProps {
steps: WizardControlStep[];
steps: WizardStepType[];
activeStepIndex: number;
footer: WizardFooterType;
children: React.ReactElement;
onNext(steps: WizardControlStep[]): void;
onBack(steps: WizardControlStep[]): void;
onClose(): void;
goToStepById(steps: WizardControlStep[], id: number | string): void;
goToStepByName(steps: WizardControlStep[], name: string): void;
goToStepByIndex(steps: WizardControlStep[], index: number): void;
onNext(event: React.MouseEvent<HTMLButtonElement>, steps: WizardStepType[]): void;
onBack(event: React.MouseEvent<HTMLButtonElement>, steps: WizardStepType[]): void;
onClose(event: React.MouseEvent<HTMLButtonElement>): void;
goToStepById(steps: WizardStepType[], id: number | string): void;
goToStepByName(steps: WizardStepType[], name: string): void;
goToStepByIndex(
event: React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLAnchorElement>,
steps: WizardStepType[],
index: number
): void;
}

export const WizardContextProvider: React.FunctionComponent<WizardContextProviderProps> = ({
Expand All @@ -62,25 +67,12 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
const [currentFooter, setCurrentFooter] = React.useState(
typeof initialFooter !== 'function' ? initialFooter : undefined
);
const steps = useGetMergedSteps(initialSteps, currentSteps);
const activeStep = React.useMemo(() => getActiveStep(steps, activeStepIndex), [activeStepIndex, steps]);

// 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 close = React.useCallback(() => onClose(null), [onClose]);
const goToNextStep = React.useCallback(() => onNext(null, steps), [onNext, steps]);
const goToPrevStep = React.useCallback(() => onBack(null, steps), [onBack, steps]);

const footer = React.useMemo(() => {
const wizardFooter = activeStep?.footer || currentFooter || initialFooter;
Expand All @@ -89,7 +81,7 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
const customFooter = wizardFooter;

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

Expand All @@ -98,17 +90,17 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
activeStep={activeStep}
onNext={goToNextStep}
onBack={goToPrevStep}
onClose={onClose}
isBackDisabled={activeStep?.id === steps[0]?.id}
onClose={close}
isBackDisabled={activeStep?.index === 1 || (isWizardSubStep(activeStep) && activeStep?.index === 2)}
{...wizardFooter}
/>
);
}, [currentFooter, initialFooter, activeStep, goToNextStep, goToPrevStep, onClose, steps]);
}, [currentFooter, initialFooter, activeStep, goToNextStep, goToPrevStep, close]);

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

const setStep = React.useCallback(
(step: Pick<WizardControlStep, 'id'> & Partial<WizardControlStep>) =>
(step: Pick<WizardStepType, 'id'> & Partial<WizardStepType>) =>
setCurrentSteps(prevSteps =>
prevSteps.map(prevStep => {
if (prevStep.id === step.id) {
Expand All @@ -127,15 +119,18 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
steps,
activeStep,
footer,
onClose,
close,
getStep,
setStep,
goToNextStep,
goToPrevStep,
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])
goToStepByIndex: React.useCallback((index: number) => goToStepByIndex(null, steps, index), [
goToStepByIndex,
steps
])
}}
>
{children}
Expand Down
Loading