Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Wizard - next): supporting component unit tests #7731

Merged
merged 2 commits into from
Sep 19, 2022
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
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