Skip to content

Commit

Permalink
feat(Wizard - next): supporting component unit tests (#7731)
Browse files Browse the repository at this point in the history
* feat(Wizard - next): supporting component unit tests

* refactor WizardToggle a bit more and address feedback
  • Loading branch information
jpuzz0 authored Sep 19, 2022
1 parent 2703bc1 commit 2e4ec26
Show file tree
Hide file tree
Showing 22 changed files with 919 additions and 449 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.9.0",
"@octokit/rest": "^16.43.2",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "14.4.3",
Expand Down
81 changes: 34 additions & 47 deletions packages/react-core/src/next/components/Wizard/Wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ 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 { WizardContextProvider } from './WizardContext';
import { WizardStepProps } from './WizardStep';
import { WizardFooter } from './WizardFooter';
import { WizardToggle } from './WizardToggle';
import { WizardToggleInternal } from './WizardToggle';

/**
* Wrapper for all steps and hosts state, including navigation helpers, within context.
Expand All @@ -29,7 +27,7 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
header?: React.ReactNode;
/** Wizard footer */
footer?: DefaultWizardFooterProps | React.ReactElement;
/** Default wizard nav props or a custom WizardNav (with callback) */
/** Wizard nav */
nav?: DefaultWizardNavProps | CustomWizardNavFunction;
/** The initial index the wizard is to start on (1 or higher). Defaults to 1. */
startIndex?: number;
Expand All @@ -39,6 +37,8 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
width?: number | string;
/** Custom height of the wizard */
height?: number | string;
/** Flag to unmount inactive steps instead of hiding. Defaults to true */
unmountInactiveSteps?: boolean;
/** Callback function when a step in the nav is clicked */
onNavByIndex?: WizardNavStepFunction;
/** Callback function after next button is clicked */
Expand All @@ -51,8 +51,23 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
onClose?: () => void;
}

export const Wizard = (props: WizardProps) => {
const { startIndex = 1, children, footer, onNavByIndex, onNext, onBack, onSave, onClose, ...internalProps } = props;
export const Wizard = ({
startIndex = 1,
children,
footer,
height,
width,
className,
header,
nav,
unmountInactiveSteps,
onNavByIndex,
onNext,
onBack,
onSave,
onClose,
...wrapperProps
}: WizardProps) => {
const [currentStepIndex, setCurrentStepIndex] = React.useState(startIndex);
const steps = buildSteps(children);

Expand Down Expand Up @@ -130,55 +145,27 @@ export const Wizard = (props: WizardProps) => {
<WizardContextProvider
steps={steps}
currentStepIndex={currentStepIndex}
footer={isCustomWizardFooter(footer) && footer}
footer={footer}
onNext={goToNextStep}
onBack={goToPrevStep}
onClose={onClose}
goToStepById={goToStepById}
goToStepByName={goToStepByName}
goToStepByIndex={goToStepByIndex}
>
<WizardInternal {...internalProps} footer={footer}>
{children}
</WizardInternal>
<div
className={css(styles.wizard, className)}
style={{
...(height ? { height } : {}),
...(width ? { width } : {})
}}
{...wrapperProps}
>
{header}
<WizardToggleInternal nav={nav} unmountInactiveSteps={unmountInactiveSteps} />
</div>
</WizardContextProvider>
);
};

// 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 || (
<WizardFooter
activeStep={activeStep}
onNext={onNext}
onBack={onBack}
onClose={onClose}
disableBackButton={activeStep?.id === steps[0]?.id}
{...footer}
/>
);

return (
<div
className={css(styles.wizard, className)}
style={{
...(height ? { height } : {}),
...(width ? { width } : {})
}}
{...divProps}
>
{header}
<WizardToggle
steps={steps}
activeStep={activeStep}
footer={wizardFooter}
nav={nav}
goToStepByIndex={goToStepByIndex}
/>
</div>
);
};

Wizard.displayName = 'Wizard';
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { css } from '@patternfly/react-styles';
*/

export interface WizardBodyProps {
children?: React.ReactNode | React.ReactNode[];
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 */
Expand Down
34 changes: 29 additions & 5 deletions packages/react-core/src/next/components/Wizard/WizardContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import React from 'react';
import { WizardControlStep } from './types';

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

import { DefaultWizardFooterProps, isCustomWizardFooter, WizardControlStep } from './types';
import { getActiveStep } from './utils';
import { WizardFooter } from './WizardFooter';

export interface WizardContextProps {
/** List of steps */
Expand All @@ -22,7 +27,7 @@ export interface WizardContextProps {
/** Navigate to step by index */
goToStepByIndex: (index: number) => void;
/** Update the footer with any react element */
setFooter: (footer: React.ReactElement) => void;
setFooter: (footer: DefaultWizardFooterProps | React.ReactElement) => void;
}

export const WizardContext = React.createContext({} as WizardContextProps);
Expand All @@ -39,7 +44,7 @@ interface WizardContextRenderProps {
export interface WizardContextProviderProps {
steps: WizardControlStep[];
currentStepIndex: number;
footer: React.ReactElement;
footer: DefaultWizardFooterProps | React.ReactElement;
children: React.ReactElement | ((props: WizardContextRenderProps) => React.ReactElement);
onNext(): void;
onBack(): void;
Expand All @@ -66,6 +71,23 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
const [footer, setFooter] = React.useState(initialFooter);
const activeStep = getActiveStep(steps, currentStepIndex);

const wizardFooter = React.useMemo(
() =>
isCustomWizardFooter(footer) ? (
<div className={css(styles.wizardFooter)}>{footer}</div>
) : (
<WizardFooter
activeStep={activeStep}
onNext={onNext}
onBack={onBack}
onClose={onClose}
disableBackButton={activeStep?.id === steps[0]?.id}
{...footer}
/>
),
[activeStep, footer, onBack, onClose, onNext, steps]
);

// 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) {
Expand All @@ -86,7 +108,7 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
value={{
steps,
activeStep,
footer,
footer: wizardFooter,
onNext,
onBack,
onClose,
Expand All @@ -96,7 +118,9 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
setFooter
}}
>
{typeof children === 'function' ? children({ activeStep, steps, footer, onNext, onBack, onClose }) : children}
{typeof children === 'function'
? children({ activeStep, steps, footer: wizardFooter, onNext, onBack, onClose })
: children}
</WizardContext.Provider>
);
};
Expand Down
6 changes: 3 additions & 3 deletions packages/react-core/src/next/components/Wizard/WizardNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface WizardNavProps {
/** Sets the aria-labelledby attribute on the nav element */
'aria-labelledby'?: string;
/** Whether the nav is expanded */
isOpen?: boolean;
isExpanded?: boolean;
/** True to return the inner list without the wrapping nav element */
returnList?: boolean;
}
Expand All @@ -19,7 +19,7 @@ export const WizardNav: React.FunctionComponent<WizardNavProps> = ({
children,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
isOpen = false,
isExpanded = false,
returnList = false
}: WizardNavProps) => {
const innerList = <ol className={css(styles.wizardNavList)}>{children}</ol>;
Expand All @@ -30,7 +30,7 @@ export const WizardNav: React.FunctionComponent<WizardNavProps> = ({

return (
<nav
className={css(styles.wizardNav, isOpen && styles.modifiers.expanded)}
className={css(styles.wizardNav, isExpanded && styles.modifiers.expanded)}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ export const WizardNavItem: React.FunctionComponent<WizardNavItemProps> = ({
{...rest}
{...(navItemComponent === 'a' ? { ...linkProps } : { ...btnProps })}
{...(id && { id: id.toString() })}
onClick={() => (isExpandable ? setIsExpanded(!isExpanded || isCurrent) : onNavItemClick(step))}
onClick={e => {
e.preventDefault();
isExpandable ? setIsExpanded(!isExpanded || isCurrent) : onNavItemClick(step);
}}
className={css(
styles.wizardNavLink,
isCurrent && styles.modifiers.current,
Expand Down
8 changes: 4 additions & 4 deletions packages/react-core/src/next/components/Wizard/WizardStep.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import React from 'react';

import { WizardControlStep } from './types';
import { WizardBasicStep } from './types';
import { WizardBody, WizardBodyProps } from './WizardBody';

/**
* Used as a passthrough of step properties for Wizard and all supporting child components.
* Also acts as a wrapper for content, with an optional inclusion of WizardBody.
*/

export interface WizardStepProps extends Omit<WizardControlStep, 'parentId' | 'subStepIds' | 'visited'> {
export interface WizardStepProps extends Omit<WizardBasicStep, 'visited'> {
/** Optional for when the step is used as a parent to sub-steps */
children?: React.ReactNode;
/** Props for WizardBody that wraps content by default. Can be set to null for exclusion of WizardBody. */
body?: WizardBodyProps | null;
body?: Omit<WizardBodyProps, 'children'> | null;
/** Optional list of sub-steps */
steps?: React.ReactElement<WizardStepProps>[];
}

export const WizardStep = ({ body, children }: WizardStepProps) =>
body === undefined ? <WizardBody {...body}>{children}</WizardBody> : <>{children}</>;
body || body === undefined ? <WizardBody {...body}>{children}</WizardBody> : <>{children}</>;

WizardStep.displayName = 'WizardStep';
Loading

0 comments on commit 2e4ec26

Please sign in to comment.