diff --git a/packages/react-core/src/components/Menu/MenuGroup.tsx b/packages/react-core/src/components/Menu/MenuGroup.tsx index bc2e244f6e9..3cbd0f17adc 100644 --- a/packages/react-core/src/components/Menu/MenuGroup.tsx +++ b/packages/react-core/src/components/Menu/MenuGroup.tsx @@ -8,7 +8,7 @@ export interface MenuGroupProps extends Omit, '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 */ @@ -32,7 +32,7 @@ const MenuGroupBase: React.FunctionComponent = ({ <> {['function', 'string'].includes(typeof label) ? ( - {label as React.ReactNode} + {label} ) : ( label diff --git a/packages/react-core/src/next/components/Wizard/Wizard.tsx b/packages/react-core/src/next/components/Wizard/Wizard.tsx index cd1f9ccb841..8499788fb93 100644 --- a/packages/react-core/src/next/components/Wizard/Wizard.tsx +++ b/packages/react-core/src/next/components/Wizard/Wizard.tsx @@ -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'; @@ -25,7 +23,7 @@ import { WizardNavInternal } from './WizardNavInternal'; export interface WizardProps extends React.HTMLProps { /** Step components */ - children: React.ReactElement | React.ReactElement[]; + children: React.ReactNode | React.ReactNode[]; /** Wizard header */ header?: React.ReactNode; /** Wizard footer */ @@ -40,18 +38,21 @@ export interface WizardProps extends React.HTMLProps { 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?: ( + event: React.MouseEvent, + currentStep: WizardStepType, + prevStep: WizardStepType, + scope: WizardStepChangeScope + ) => void | Promise; /** Callback function to save at the end of the wizard, if not specified uses onClose */ - onSave?: () => void | Promise; + onSave?: (event: React.MouseEvent) => void | Promise; /** Callback function to close the wizard */ - onClose?: () => void; + onClose?: (event: React.MouseEvent) => void; } export const Wizard = ({ @@ -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, 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, 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, + steps: WizardStepType[] = initialSteps, + index: number + ) => { const lastStepIndex = steps.length + 1; // Handle index when out of bounds or hidden @@ -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); } }; @@ -162,13 +176,17 @@ export const Wizard = ({ {...wrapperProps} > {header} - + ); }; -const WizardInternal = ({ nav, isStepVisitRequired }: Pick) => { +const WizardInternal = ({ + nav, + isVisitRequired, + isProgressive +}: Pick) => { const { activeStep, steps, footer, goToStepByIndex } = useWizardContext(); const [isNavExpanded, setIsNavExpanded] = React.useState(false); @@ -177,8 +195,15 @@ const WizardInternal = ({ nav, isStepVisitRequired }: Pick; - }, [activeStep, isStepVisitRequired, goToStepByIndex, isNavExpanded, nav, steps]); + return ( + + ); + }, [activeStep, isVisitRequired, isProgressive, goToStepByIndex, isNavExpanded, nav, steps]); return ( void; /** Navigate to the next step */ - onNext: () => void | Promise; + goToNextStep: () => void | Promise; /** Navigate to the previous step */ - onBack: () => void | Promise; - /** Close the wizard */ - onClose: () => void; + goToPrevStep: () => void | Promise; /** Navigate to step by ID */ goToStepById: (id: number | string) => void; /** Navigate to step by name */ @@ -26,24 +27,28 @@ export interface WizardContextProps { /** Update the footer with any react element */ setFooter: (footer: React.ReactElement | Partial) => void; /** Get step by ID */ - getStep: (stepId: number | string) => WizardControlStep; + getStep: (stepId: number | string) => WizardStepType; /** Set step by ID */ - setStep: (step: Pick & Partial) => void; + setStep: (step: Pick & Partial) => 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, steps: WizardStepType[]): void; + onBack(event: React.MouseEvent, steps: WizardStepType[]): void; + onClose(event: React.MouseEvent): void; + goToStepById(steps: WizardStepType[], id: number | string): void; + goToStepByName(steps: WizardStepType[], name: string): void; + goToStepByIndex( + event: React.MouseEvent | React.MouseEvent, + steps: WizardStepType[], + index: number + ): void; } export const WizardContextProvider: React.FunctionComponent = ({ @@ -62,25 +67,12 @@ export const WizardContextProvider: React.FunctionComponent 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; @@ -89,7 +81,7 @@ export const WizardContextProvider: React.FunctionComponent ); - }, [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 & Partial) => + (step: Pick & Partial) => setCurrentSteps(prevSteps => prevSteps.map(prevStep => { if (prevStep.id === step.id) { @@ -127,15 +119,18 @@ export const WizardContextProvider: React.FunctionComponent 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} diff --git a/packages/react-core/src/next/components/Wizard/WizardFooter.tsx b/packages/react-core/src/next/components/Wizard/WizardFooter.tsx index 0b200ef1086..dba701662b8 100644 --- a/packages/react-core/src/next/components/Wizard/WizardFooter.tsx +++ b/packages/react-core/src/next/components/Wizard/WizardFooter.tsx @@ -4,7 +4,7 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/Wizard/wizard'; import { Button, ButtonVariant } from '../../../components/Button'; -import { isCustomWizardFooter, WizardControlStep, WizardNavStepFunction } from './types'; +import { isCustomWizardFooter, WizardStepType } 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. @@ -12,13 +12,13 @@ import { isCustomWizardFooter, WizardControlStep, WizardNavStepFunction } from ' export interface WizardFooterProps { /** The active step */ - activeStep: WizardControlStep; + activeStep: WizardStepType; /** Next button callback */ - onNext: () => WizardNavStepFunction | void | Promise; + onNext: (event: React.MouseEvent) => void | Promise; /** Back button callback */ - onBack: () => WizardNavStepFunction | void | Promise; + onBack: (event: React.MouseEvent) => void | Promise; /** Cancel link callback */ - onClose: () => void; + onClose: (event: React.MouseEvent) => void; /** Custom text for the Next button. The current step's nextButtonText takes precedence. */ nextButtonText?: React.ReactNode; /** Custom text for the Back button */ @@ -35,6 +35,10 @@ export interface WizardFooterProps { isCancelHidden?: boolean; } +/** + * Applies default wizard footer styling any number of child elements. + */ + interface WizardFooterWrapperProps { children: React.ReactNode; } @@ -44,7 +48,7 @@ export const WizardFooterWrapper = ({ children }: WizardFooterWrapperProps) => ( ); export const WizardFooter = ({ activeStep, ...internalProps }: WizardFooterProps) => { - const activeStepFooter = !isCustomWizardFooter(activeStep.footer) && activeStep.footer; + const activeStepFooter = !isCustomWizardFooter(activeStep?.footer) && activeStep?.footer; return ; }; diff --git a/packages/react-core/src/next/components/Wizard/WizardHeader.tsx b/packages/react-core/src/next/components/Wizard/WizardHeader.tsx index c459efc2b53..82dbb2f0258 100644 --- a/packages/react-core/src/next/components/Wizard/WizardHeader.tsx +++ b/packages/react-core/src/next/components/Wizard/WizardHeader.tsx @@ -7,7 +7,7 @@ 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; + onClose?: (event: React.MouseEvent) => void; /** Title of the wizard */ title: string; /** Description of the wizard */ diff --git a/packages/react-core/src/next/components/Wizard/WizardNavInternal.tsx b/packages/react-core/src/next/components/Wizard/WizardNavInternal.tsx index 5f45258ebda..30fb194545c 100644 --- a/packages/react-core/src/next/components/Wizard/WizardNavInternal.tsx +++ b/packages/react-core/src/next/components/Wizard/WizardNavInternal.tsx @@ -11,12 +11,12 @@ import { WizardNavItem } from './WizardNavItem'; * This component is not exposed to consumers. */ -interface WizardNavInternalProps extends Pick { +interface WizardNavInternalProps extends Pick { nav: Partial; isNavExpanded: boolean; } -export const WizardNavInternal = ({ nav, isStepVisitRequired, isNavExpanded }: WizardNavInternalProps) => { +export const WizardNavInternal = ({ nav, isVisitRequired, isProgressive, isNavExpanded }: WizardNavInternalProps) => { const { activeStep, steps, goToStepByIndex } = useWizardContext(); const wizardNavProps: WizardNavProps = { @@ -31,7 +31,7 @@ export const WizardNavInternal = ({ nav, isStepVisitRequired, isNavExpanded }: W {steps.map((step, stepIndex) => { const hasVisitedNextStep = steps.some(step => step.index > stepIndex + 1 && step.isVisited); - const isStepDisabled = step.isDisabled || (isStepVisitRequired && !step.isVisited && !hasVisitedNextStep); + const isStepDisabled = step.isDisabled || (isVisitRequired && !step.isVisited && !hasVisitedNextStep); const customStepNavItem = isCustomWizardNavItem(step.navItem) && ( {typeof step.navItem === 'function' ? step.navItem(step, activeStep, steps, goToStepByIndex) : step.navItem} @@ -46,7 +46,7 @@ export const WizardNavInternal = ({ nav, isStepVisitRequired, isNavExpanded }: W const subStep = steps.find(step => step.id === subStepId); const hasVisitedNextStep = steps.some(step => step.index > subStep.index && step.isVisited); const isSubStepDisabled = - subStep.isDisabled || (isStepVisitRequired && !subStep.isVisited && !hasVisitedNextStep); + subStep.isDisabled || (isVisitRequired && !subStep.isVisited && !hasVisitedNextStep); const customSubStepNavItem = isCustomWizardNavItem(subStep.navItem) && ( {typeof subStep.navItem === 'function' @@ -70,51 +70,59 @@ export const WizardNavInternal = ({ nav, isStepVisitRequired, isNavExpanded }: W hasActiveChild = true; } - return ( - customSubStepNavItem || ( - - ) - ); + if (!isProgressive || (isProgressive && subStep.index <= activeStep.index)) { + return ( + customSubStepNavItem || ( + goToStepByIndex(subStep.index)} + status={subStep.status} + {...subStep.navItem} + /> + ) + ); + } }); const hasEnabledChildren = React.Children.toArray(subNavItems).some( child => React.isValidElement(child) && !child.props.isDisabled ); - return ( - customStepNavItem || ( - - - {subNavItems} - - - ) - ); + if (!isProgressive || (isProgressive && step.index <= activeStep.index)) { + return ( + customStepNavItem || ( + goToStepByIndex(firstSubStepIndex)} + status={step.status} + {...step.navItem} + > + + {subNavItems} + + + ) + ); + } } - if (isWizardBasicStep(step) && !step.isHidden) { + if ( + isWizardBasicStep(step) && + !step.isHidden && + (!isProgressive || (isProgressive && step.index <= activeStep.index)) + ) { return ( customStepNavItem || ( goToStepByIndex(step.index)} status={step.status} {...step.navItem} /> diff --git a/packages/react-core/src/next/components/Wizard/WizardNavItem.tsx b/packages/react-core/src/next/components/Wizard/WizardNavItem.tsx index a0bd4e9d085..cd3f9e11661 100644 --- a/packages/react-core/src/next/components/Wizard/WizardNavItem.tsx +++ b/packages/react-core/src/next/components/Wizard/WizardNavItem.tsx @@ -5,9 +5,10 @@ import styles from '@patternfly/react-styles/css/components/Wizard/wizard'; import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; +import { OUIAProps, useOUIAProps } from '../../../helpers'; import { WizardNavItemStatus } from './types'; -export interface WizardNavItemProps { +export interface WizardNavItemProps extends OUIAProps { /** Can nest a WizardNav component for substeps */ children?: React.ReactNode; /** The content to display in the navigation item */ @@ -21,11 +22,13 @@ export interface WizardNavItemProps { /** The step index passed into the onNavItemClick callback */ stepIndex: number; /** Callback for when the navigation item is clicked */ - onNavItemClick?: (stepIndex: number) => any; + onClick?: (event: React.MouseEvent | React.MouseEvent, index: number) => any; /** Component used to render WizardNavItem */ - navItemComponent?: 'button' | 'a'; + component?: 'button' | 'a'; /** An optional url to use for when using an anchor component */ href?: string; + /** Where to display the linked URL when using an anchor component */ + target?: React.HTMLAttributeAnchorTarget; /** Flag indicating that this NavItem has child steps and is expandable */ isExpandable?: boolean; /** The id for the navigation item */ @@ -34,29 +37,31 @@ export interface WizardNavItemProps { status?: 'default' | 'error'; } -export const WizardNavItem: React.FunctionComponent = ({ +export const WizardNavItem = ({ children = null, content = '', isCurrent = false, isDisabled = false, isVisited = false, stepIndex, - onNavItemClick = () => undefined, - navItemComponent = 'button', - href = null, + onClick, + component: NavItemComponent = 'button', + href, isExpandable = false, id, status = 'default', - ...rest + target, + ouiaId, + ouiaSafe = true }: WizardNavItemProps) => { - const NavItemComponent = navItemComponent; const [isExpanded, setIsExpanded] = React.useState(false); + const ouiaProps = useOUIAProps(WizardNavItem.displayName, ouiaId, ouiaSafe); React.useEffect(() => { setIsExpanded(isCurrent); }, [isCurrent]); - if (navItemComponent === 'a' && !href && process.env.NODE_ENV !== 'production') { + if (NavItemComponent === 'a' && !href && process.env.NODE_ENV !== 'production') { // eslint-disable-next-line no-console console.error('WizardNavItem: When using an anchor, please provide an href'); } @@ -87,12 +92,13 @@ export const WizardNavItem: React.FunctionComponent = ({ )} > { - e.preventDefault(); - isExpandable ? setIsExpanded(!isExpanded || isCurrent) : onNavItemClick(stepIndex); + e.stopPropagation(); + isExpandable ? setIsExpanded(!isExpanded || isCurrent) : onClick?.(e, stepIndex); }} className={css( styles.wizardNavLink, @@ -103,6 +109,7 @@ export const WizardNavItem: React.FunctionComponent = ({ aria-current={isCurrent && !children ? 'step' : false} {...(isExpandable && { 'aria-expanded': isExpanded })} {...(ariaLabel && { 'aria-label': ariaLabel })} + {...ouiaProps} > {isExpandable ? ( <> diff --git a/packages/react-core/src/next/components/Wizard/WizardStep.tsx b/packages/react-core/src/next/components/Wizard/WizardStep.tsx index d59cc7ebbf9..20bc4673f0f 100644 --- a/packages/react-core/src/next/components/Wizard/WizardStep.tsx +++ b/packages/react-core/src/next/components/Wizard/WizardStep.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { WizardNavItemType } from './types'; +import { isWizardParentStep, WizardNavItemType } from './types'; import { WizardBodyProps } from './WizardBody'; import { useWizardContext } from './WizardContext'; import { WizardFooterProps } from './WizardFooter'; @@ -15,7 +15,7 @@ export interface WizardStepProps { /** Unique identifier */ id: string | number; /** Optional for when the step is used as a parent to sub-steps */ - children?: React.ReactNode; + children?: React.ReactNode | undefined; /** Props for WizardBody that wraps content by default. Can be set to null for exclusion of WizardBody. */ body?: Omit, 'children'> | null; /** Optional list of sub-steps */ @@ -30,8 +30,8 @@ export interface WizardStepProps { footer?: React.ReactElement | Partial; /** Used to determine icon next to the step's navigation item */ status?: 'default' | 'error'; - /** Flag to determine whether sub-steps can collapse or not */ - isCollapsible?: boolean; + /** Flag to determine whether parent steps can expand or not. Defaults to false. */ + isExpandable?: boolean; } export const WizardStep = ({ children, steps: _subSteps, ...props }: WizardStepProps) => { @@ -40,6 +40,8 @@ export const WizardStep = ({ children, steps: _subSteps, ...props }: WizardStepP // Update step in context when props change or when the step is active has yet to be marked as visited. React.useEffect(() => { + const shouldUpdateIsVisited = !isWizardParentStep(activeStep) && id === activeStep?.id && !activeStep?.isVisited; + setStep({ id, name, @@ -49,9 +51,9 @@ export const WizardStep = ({ children, steps: _subSteps, ...props }: WizardStepP ...(navItem && { navItem }), ...(footer && { footer }), ...(status && { status }), - ...(id === activeStep?.id && !activeStep?.isVisited && { isVisited: true }) + ...(shouldUpdateIsVisited && { isVisited: true }) }); - }, [body, footer, id, isDisabled, isHidden, name, navItem, status, activeStep?.id, activeStep?.isVisited, setStep]); + }, [body, footer, id, isDisabled, isHidden, name, navItem, status, activeStep, setStep]); return <>{children}; }; diff --git a/packages/react-core/src/next/components/Wizard/WizardToggle.tsx b/packages/react-core/src/next/components/Wizard/WizardToggle.tsx index 7a535b4813c..caa888f0e34 100644 --- a/packages/react-core/src/next/components/Wizard/WizardToggle.tsx +++ b/packages/react-core/src/next/components/Wizard/WizardToggle.tsx @@ -7,7 +7,7 @@ import CaretDownIcon from '@patternfly/react-icons/dist/esm/icons/caret-down-ico import { KeyTypes } from '../../../helpers/constants'; import { WizardNavProps, WizardBody, WizardStep, WizardStepProps } from '../Wizard'; -import { WizardControlStep, isWizardSubStep } from './types'; +import { WizardStepType, isWizardSubStep } from './types'; /** * Used to toggle between step content, including the body and footer. This is also where the navigation and its expandability is controlled. @@ -15,9 +15,9 @@ import { WizardControlStep, isWizardSubStep } from './types'; export interface WizardToggleProps { /** List of steps and/or sub-steps */ - steps: WizardControlStep[]; + steps: WizardStepType[]; /** The current step */ - activeStep: WizardControlStep; + activeStep: WizardStepType; /** Wizard footer */ footer: React.ReactElement; /** Wizard navigation */ @@ -27,7 +27,7 @@ export interface WizardToggleProps { /** Flag to determine whether the dropdown navigation is expanded */ isNavExpanded?: boolean; /** Callback to expand or collapse the dropdown navigation */ - toggleNavExpanded?: () => void; + toggleNavExpanded?: (event: React.MouseEvent | KeyboardEvent) => void; } export const WizardToggle = ({ @@ -49,7 +49,7 @@ export const WizardToggle = ({ const handleKeyClicks = React.useCallback( (event: KeyboardEvent): void => { if (isNavExpanded && event.key === KeyTypes.Escape) { - toggleNavExpanded?.(); + toggleNavExpanded?.(event); } }, [isNavExpanded, toggleNavExpanded] diff --git a/packages/react-core/src/next/components/Wizard/__tests__/Wizard.test.tsx b/packages/react-core/src/next/components/Wizard/__tests__/Wizard.test.tsx index 779ed5c1be8..c1b2f7d9cd3 100644 --- a/packages/react-core/src/next/components/Wizard/__tests__/Wizard.test.tsx +++ b/packages/react-core/src/next/components/Wizard/__tests__/Wizard.test.tsx @@ -18,21 +18,6 @@ test('renders step when child is of type WizardStep', () => { expect(screen.getByText('Step content')).toBeVisible(); }); -test('renders step when child has required props; name, id, children', () => { - const CustomStep = props =>
; - - render( - - - Custom step content - - - ); - - expect(screen.getByRole('button', { name: 'Test step' })).toBeVisible(); - expect(screen.getByText('Custom step content')).toBeVisible(); -}); - test('renders a header when specified', () => { render( @@ -101,7 +86,7 @@ test('renders default nav with custom props', () => { }; render( - + ]} /> @@ -111,7 +96,6 @@ test('renders default nav with custom props', () => { expect(navElement).toBeVisible(); expect(navElement).toHaveAttribute('aria-labelledby', 'wizard-id'); - expect(screen.getByRole('button', { name: 'Test step 1, visited' }).parentElement).toHaveClass('pf-m-expandable'); expect(screen.getByRole('button', { name: 'Test step 2' })).toHaveAttribute('disabled'); }); @@ -183,7 +167,7 @@ test('calls onNavByIndex on nav item click', async () => { const onNavByIndex = jest.fn(); render( - + @@ -199,7 +183,7 @@ test('calls onNext and not onSave on next button click when not on the last step const onSave = jest.fn(); render( - + @@ -216,7 +200,7 @@ test('calls onBack on back button click', async () => { const onBack = jest.fn(); render( - + @@ -298,13 +282,18 @@ test('does not render inactive step content', async () => { expect(screen.getByText('Step 2 content')).toBeVisible(); }); -test('parent steps have collapsed sub-steps by default unless the step is active', async () => { +test('parent steps have collapsed sub-steps when isExpandable is true unless the step is active', async () => { const user = userEvent.setup(); render( - + - ]} /> + ]} + /> ); @@ -315,17 +304,131 @@ test('parent steps have collapsed sub-steps by default unless the step is active expect(screen.getByLabelText('Collapse step icon')).toBeVisible(); }); -test('parent step can be non-collapsible by setting isCollapsible to false', () => { +test('parent step can be expandable by setting isExpandable to true', () => { render( ]} /> ); - expect(screen.queryByLabelText('step icon', { exact: false })).toBeNull(); + expect(screen.queryByLabelText('step icon', { exact: false })).toBeVisible(); +}); + +test('incrementally shows/hides steps based on the activeStep when isProgressive is enabled', async () => { + const user = userEvent.setup(); + + render( + + + Step 1 content + + + Step 2 content + + + Step 3 content + + + ); + + const nextButton = screen.getByRole('button', { + name: 'Next' + }); + const backButton = screen.getByRole('button', { + name: 'Back' + }); + + // Initially only the first nav item will be visible + expect( + screen.getByRole('button', { + name: 'Test step 1' + }) + ).toBeVisible(); + expect( + screen.queryByRole('button', { + name: 'Test step 2' + }) + ).toBeNull(); + expect( + screen.queryByRole('button', { + name: 'Test step 3' + }) + ).toBeNull(); + + // Progressing to the next step will show steps 1 & 2 + await user.click(nextButton); + expect( + screen.getByRole('button', { + name: 'Test step 1, visited' + }) + ).toBeVisible(); + expect( + screen.getByRole('button', { + name: 'Test step 2' + }) + ).toBeVisible(); + expect( + screen.queryByRole('button', { + name: 'Test step 3' + }) + ).toBeNull(); + + // Progressing to the next step will show all steps + await user.click(nextButton); + expect( + screen.getByRole('button', { + name: 'Test step 1, visited' + }) + ).toBeVisible(); + expect( + screen.getByRole('button', { + name: 'Test step 2, visited' + }) + ).toBeVisible(); + expect( + screen.getByRole('button', { + name: 'Test step 3' + }) + ).toBeVisible(); + + // Going back a step will hide step 3 + await user.click(backButton); + expect( + screen.getByRole('button', { + name: 'Test step 1, visited' + }) + ).toBeVisible(); + expect( + screen.getByRole('button', { + name: 'Test step 2' + }) + ).toBeVisible(); + expect( + screen.queryByRole('button', { + name: 'Test step 3' + }) + ).toBeNull(); + + // Going back a step will hide step 2 & 3 + await user.click(backButton); + expect( + screen.getByRole('button', { + name: 'Test step 1' + }) + ).toBeVisible(); + expect( + screen.queryByRole('button', { + name: 'Test step 2' + }) + ).toBeNull(); + expect( + screen.queryByRole('button', { + name: 'Test step 3' + }) + ).toBeNull(); }); diff --git a/packages/react-core/src/next/components/Wizard/__tests__/WizardStep.test.tsx b/packages/react-core/src/next/components/Wizard/__tests__/WizardStep.test.tsx index f48ee373b83..43ed3486f48 100644 --- a/packages/react-core/src/next/components/Wizard/__tests__/WizardStep.test.tsx +++ b/packages/react-core/src/next/components/Wizard/__tests__/WizardStep.test.tsx @@ -4,7 +4,7 @@ import { render, screen } from '@testing-library/react'; import { WizardStep, WizardStepProps } from '../WizardStep'; import * as WizardContext from '../WizardContext'; -import { WizardControlStep } from '..'; +import { WizardStepType } from '..'; const testStepProps: WizardStepProps = { id: 'test-step', @@ -12,7 +12,7 @@ const testStepProps: WizardStepProps = { footer: <>Step footer }; -const testStep: WizardControlStep = { +const testStep: WizardStepType = { ...testStepProps, index: 1 }; @@ -24,9 +24,9 @@ const wizardContext: WizardContext.WizardContextProps = { steps: [testStep], activeStep: testStep, footer: <>Wizard footer, - onNext: jest.fn(), - onBack: jest.fn(), - onClose: jest.fn(), + goToNextStep: jest.fn(), + goToPrevStep: jest.fn(), + close: jest.fn(), goToStepById: jest.fn(), goToStepByName: jest.fn(), goToStepByIndex: jest.fn(), diff --git a/packages/react-core/src/next/components/Wizard/__tests__/WizardToggle.test.tsx b/packages/react-core/src/next/components/Wizard/__tests__/WizardToggle.test.tsx index 970904ec8d8..99efefe596e 100644 --- a/packages/react-core/src/next/components/Wizard/__tests__/WizardToggle.test.tsx +++ b/packages/react-core/src/next/components/Wizard/__tests__/WizardToggle.test.tsx @@ -5,9 +5,9 @@ import userEvent from '@testing-library/user-event'; import { KeyTypes } from '../../../../helpers'; import { WizardToggle, WizardToggleProps } from '../WizardToggle'; -import { WizardControlStep } from '../types'; +import { WizardStepType } from '../types'; -const steps: WizardControlStep[] = [ +const steps: WizardStepType[] = [ { id: 'id-1', name: 'First step', index: 1, component: <>First step content }, { id: 'id-2', name: 'Second step', index: 2, component: <>Second step content } ]; diff --git a/packages/react-core/src/next/components/Wizard/__tests__/utils.test.tsx b/packages/react-core/src/next/components/Wizard/__tests__/utils.test.tsx index d211c681808..5e4732c29b4 100644 --- a/packages/react-core/src/next/components/Wizard/__tests__/utils.test.tsx +++ b/packages/react-core/src/next/components/Wizard/__tests__/utils.test.tsx @@ -5,11 +5,11 @@ import { buildSteps } from '../utils'; import { WizardStep } from '../WizardStep'; describe('buildSteps', () => { - test('throws error if child does not have required props or type of WizardStep', () => { + test('throws error if child is not of type WizardStep', () => { try { buildSteps(
); } catch (error) { - expect(error.message).toEqual('Wizard only accepts children with required WizardStepProps.'); + expect(error.message).toEqual('Wizard only accepts children of type WizardStep.'); } }); @@ -17,7 +17,7 @@ describe('buildSteps', () => { try { buildSteps('test' as any); } catch (error) { - expect(error.message).toEqual('Wizard only accepts children with required WizardStepProps.'); + expect(error.message).toEqual('Wizard only accepts children of type WizardStep.'); } }); @@ -30,20 +30,6 @@ describe('buildSteps', () => { expect(step.component?.props).toEqual(component.props); }); - test('returns array of steps if children have required props', () => { - const CustomStep = props =>
; - const component = ( - - Content - - ); - const [step] = buildSteps(component); - - expect(step.id).toEqual('step-1'); - expect(step.name).toEqual('Step 1'); - expect(step.component?.props).toEqual(component.props); - }); - test('returns flattened array of steps and sub-steps when sub-steps exist', () => { const CustomSubStep = props =>
; diff --git a/packages/react-core/src/next/components/Wizard/examples/Wizard.md b/packages/react-core/src/next/components/Wizard/examples/Wizard.md index f94cc88bfd2..1ef28d1d16f 100644 --- a/packages/react-core/src/next/components/Wizard/examples/Wizard.md +++ b/packages/react-core/src/next/components/Wizard/examples/Wizard.md @@ -6,6 +6,7 @@ propComponents: [ 'Wizard', 'WizardFooter', + 'WizardFooterWrapper', 'WizardToggle', 'WizardStep', 'WizardBody', @@ -16,7 +17,6 @@ propComponents: 'WizardBasicStep', 'WizardParentStep', 'WizardSubStep', - 'WizardNavStepData', ] beta: true --- @@ -49,6 +49,9 @@ WizardNavItem, WizardNav, WizardHeader } from '@patternfly/react-core/next'; +import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; +import SlackHashIcon from '@patternfly/react-icons/dist/esm/icons/slack-hash-icon'; +import CogsIcon from '@patternfly/react-icons/dist/esm/icons/cogs-icon'; PatternFly has two implementations of a `Wizard`. This newer `Wizard` takes a more explicit and declarative approach compared to the older implementation, which can be found under the [React](/components/wizard/react) tab. @@ -59,64 +62,89 @@ PatternFly has two implementations of a `Wizard`. This newer `Wizard` takes a mo ```ts file="./WizardBasic.tsx" ``` -### Custom navigation +### Basic with disabled steps -The `Wizard`'s `nav` property can be used to build your own navigation. +```ts file="./WizardBasicDisabledSteps.tsx" +``` -```noLive -/** Callback for the Wizard's 'nav' property. Returns element which replaces the Wizard's default navigation. */ -export type CustomWizardNavFunction = ( - isExpanded: boolean, - steps: WizardControlStep[], - activeStep: WizardControlStep, - goToStepByIndex: (index: number) => void -) => React.ReactElement; +### Anchors for nav items -/** Encompasses all step type variants that are internally controlled by the Wizard */ -type WizardControlStep = WizardBasicStep | WizardParentStep | WizardSubStep; +```ts file="./WizardWithNavAnchors.tsx" ``` -```ts file="./WizardCustomNav.tsx" +### Incrementally enabled steps + +```ts file="./WizardStepVisitRequired.tsx" ``` -### Kitchen sink +### Expandable steps -Includes the following: +```ts file="./WizardExpandableSteps.tsx" +``` -- Header -- Custom footer -- Sub-steps -- Step content with a drawer -- Custom navigation item -- Disabled navigation items until visited -- Action to toggle visibility of a step -- Action to toggle navigation item error status +### Progress after submission -Custom operations when navigating between steps can be achieved by utilizing `onNext`, `onBack`, or `onNavByIndex` properties whose callback functions return the 'id' and 'name' of the currently focused step (currentStep), and the previously focused step (previousStep). +```ts file="./WizardWithSubmitProgress.tsx" +``` -```noLive -/** Callback for the Wizard's 'onNext', 'onBack', and 'onNavByIndex' properties */ -type WizardNavStepFunction = (currentStep: WizardNavStepData, previousStep: WizardNavStepData) => void | Promise; +### Enabled on form validation -/** Data returned for either parameter of WizardNavStepFunction */ -type WizardNavStepData = Pick; +```ts file="./WizardEnabledOnFormValidation.tsx" ``` -# +### Validate on button press -The `WizardStep`'s `navItem` property can be used to build your own nav item for that step. +```ts file="./WizardValidateOnButtonPress.tsx" +``` -```noLive -/** Callback for the Wizard's 'navItem' property. Returns element which replaces the WizardStep's default navItem. */ -export type CustomWizardNavItemFunction = ( - step: WizardControlStep, - activeStep: WizardControlStep, - steps: WizardControlStep[], - goToStepByIndex: (index: number) => void -) => React.ReactElement; +### Progressive steps + +```ts file="./WizardProgressiveSteps.tsx" +``` + +### Get current step + +```ts file="./WizardGetCurrentStep.tsx" +``` + +### Within modal + +```ts file="./WizardWithinModal.tsx" +``` + +### Step drawer content + +```ts file="./WizardStepDrawerContent.tsx" +``` + +### Custom navigation + +```ts file="./WizardWithCustomNav.tsx" +``` + +### Header + +```ts file="./WizardWithHeader.tsx" +``` + +### Custom footer + +```ts file="./WizardWithCustomFooter.tsx" +``` + +### Custom navigation item + +```ts file="./WizardWithCustomNavItem.tsx" ``` -```ts file="./WizardKitchenSink.tsx" +### Toggle step visibility + +```ts file="./WizardToggleStepVisibility.tsx" +``` + +### Step error status + +```ts file="./WizardStepErrorStatus.tsx" ``` ## Hooks diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardBasic.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardBasic.tsx index 4d1823dbfae..d9cf3055a15 100644 --- a/packages/react-core/src/next/components/Wizard/examples/WizardBasic.tsx +++ b/packages/react-core/src/next/components/Wizard/examples/WizardBasic.tsx @@ -2,21 +2,15 @@ import React from 'react'; import { Wizard, WizardStep } from '@patternfly/react-core/next'; export const WizardBasic: React.FunctionComponent = () => ( - - -

Step 1 content

+ + + Step 1 content - -

Step 2 content

-
- -

Step 3 content

-
- -

Step 4 content

+ + Step 2 content -

Review step content

+ Review step content
); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardBasicDisabledSteps.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardBasicDisabledSteps.tsx new file mode 100644 index 00000000000..b3ce4d5e76f --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardBasicDisabledSteps.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; + +export const WizardBasicDisabledSteps: React.FunctionComponent = () => ( + + + Step 1 content + + + Step 2 content + + + Step 3 content + + + Step 4 content + + + Review step content + + +); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardEnabledOnFormValidation.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardEnabledOnFormValidation.tsx new file mode 100644 index 00000000000..2a8044dbff1 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardEnabledOnFormValidation.tsx @@ -0,0 +1,84 @@ +import React from 'react'; + +import { Form, FormGroup, TextInput } from '@patternfly/react-core'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; + +interface SampleFormProps { + value: string; + isValid: boolean; + setValue: (value: string) => void; + setIsValid: (isValid: boolean) => void; +} + +const SampleForm: React.FunctionComponent = ({ value, isValid, setValue, setIsValid }) => { + const validated = isValid ? 'default' : 'error'; + + const handleTextInputChange = (value: string) => { + const isValid = /^\d+$/.test(value); + + setValue(value); + setIsValid(isValid); + }; + + return ( +
+ + + +
+ ); +}; + +export const WizardEnabledOnFormValidation: React.FunctionComponent = () => { + const [ageValue, setAgeValue] = React.useState('Thirty'); + const [isSubAFormValid, setIsSubAFormValid] = React.useState(false); + + const onSave = () => alert(`Wow, you look a lot younger than ${ageValue}.`); + + return ( + + + Information content + + + + , + + Substep B content + + ]} + /> + + Additional step content + + + + ); +}; diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardExpandableSteps.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardExpandableSteps.tsx new file mode 100644 index 00000000000..971a79514c9 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardExpandableSteps.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; + +export const WizardExpandableSteps: React.FunctionComponent = () => ( + + + Substep A content + , + + Substep B content + + ]} + /> + + Step 2 content + + + Substep C content + , + + Substep D content + + ]} + /> + + Step 4 content + + + Review step content + + +); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardGetCurrentStep.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardGetCurrentStep.tsx new file mode 100644 index 00000000000..3c10cb10bb3 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardGetCurrentStep.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { + DescriptionList, + DescriptionListGroup, + DescriptionListTerm, + DescriptionListDescription +} from '@patternfly/react-core'; +import { Wizard, WizardStep, WizardStepType } from '@patternfly/react-core/next'; + +const CurrentStepDescriptionList = ({ currentStep }: { currentStep: WizardStepType | undefined }) => ( + + + Index + {currentStep?.index} + + + + ID + {currentStep?.id} + + + + Name + {currentStep?.name} + + + + Visited + {currentStep?.isVisited ? 'true' : 'false'} + + +); + +export const GetCurrentStepWizard: React.FunctionComponent = () => { + const [currentStep, setCurrentStep] = React.useState(); + + const onStepChange = (event: React.MouseEvent, currentStep: WizardStepType) => + setCurrentStep(currentStep); + + return ( + + + {currentStep ? : 'Step 1 content'} + + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardKitchenSink.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardKitchenSink.tsx deleted file mode 100644 index 124f928558a..00000000000 --- a/packages/react-core/src/next/components/Wizard/examples/WizardKitchenSink.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import React from 'react'; - -import { - FormGroup, - TextInput, - Drawer, - DrawerContent, - Button, - Flex, - DrawerPanelContent, - DrawerColorVariant, - DrawerHead, - DrawerActions, - DrawerCloseButton, - Checkbox -} from '@patternfly/react-core'; -import { - Wizard, - WizardStep, - WizardBody, - WizardFooter, - useWizardContext, - WizardHeader, - WizardFooterWrapper, - WizardNavStepFunction, - WizardNavStepData -} from '@patternfly/react-core/next'; - -const StepId = { - StepOne: 'sink-step-1', - StepTwo: 'sink-step-2', - StepTwoSubOne: 'sink-step-2-1', - StepTwoSubTwo: 'sink-step-2-2', - StepThree: 'sink-step-3', - StepFour: 'sink-step-4', - ReviewStep: 'sink-review-step' -}; - -interface SomeContextProps { - isToggleStepChecked: boolean; - errorMessage: string | undefined; - setIsToggleStepChecked(isHidden: boolean): void; - setErrorMessage(error: string | undefined): void; -} -type SomeContextRenderProps = Pick; -interface SomeContextProviderProps { - children: (context: SomeContextRenderProps) => React.ReactElement; -} - -const SomeContext = React.createContext({} as SomeContextProps); - -const SomeContextProvider = ({ children }: SomeContextProviderProps) => { - const [isToggleStepChecked, setIsToggleStepChecked] = React.useState(false); - const [errorMessage, setErrorMessage] = React.useState(); - - return ( - - {children({ isToggleStepChecked, errorMessage })} - - ); -}; - -const CustomWizardFooter = () => { - const { activeStep, onNext, onBack, onClose } = useWizardContext(); - - return ( - - ); -}; - -const StepContentWithDrawer = () => { - const [isDrawerExpanded, setIsDrawerExpanded] = React.useState(false); - const drawerRef = React.useRef(null); - - const onWizardExpand = () => { - drawerRef.current && drawerRef.current.focus(); - }; - - return ( - - - - - drawer content - - - setIsDrawerExpanded(false)} /> - - - - } - > - - - {!isDrawerExpanded && ( - - )} - - - - - - - - - ); -}; - -const CustomStepThreeFooter = () => { - const { onNext: goToNextStep, onBack, onClose } = useWizardContext(); - const [isLoading, setIsLoading] = React.useState(false); - - async function onNext(goToStep: () => void) { - setIsLoading(true); - await new Promise(resolve => setTimeout(resolve, 2000)); - setIsLoading(false); - - goToStep(); - } - - return ( - - - - - - ); -}; - -const StepContentWithActions = () => { - const { isToggleStepChecked, errorMessage, setIsToggleStepChecked, setErrorMessage } = React.useContext(SomeContext); - - return ( - <> - setErrorMessage(checked ? 'Some error message' : undefined)} - id="toggle-error-checkbox" - name="Toggle Error Checkbox" - /> - setIsToggleStepChecked(checked)} - id="toggle-hide-step-checkbox" - name="Toggle Hide Step Checkbox" - /> - - ); -}; - -export const WizardKitchenSink: React.FunctionComponent = () => { - const onNext: WizardNavStepFunction = (_currentStep: WizardNavStepData, _previousStep: WizardNavStepData) => {}; - const [isSubmitting, setIsSubmitting] = React.useState(false); - - async function onSubmit(): Promise { - setIsSubmitting(true); - - await new Promise(resolve => setTimeout(resolve, 5000)); - - setIsSubmitting(false); - alert('50 points to Gryffindor!'); - } - - return ( - - {({ isToggleStepChecked, errorMessage }) => ( - } - footer={} - onNext={onNext} - isStepVisitRequired - > - - - - - - , - - Substep 2 content - - ]} - /> - Custom item - }} - footer={} - > - Step 3 content w/ custom async footer - - - {isSubmitting ? 'Calculating wizard score...' : 'Review step content'} - - - )} - - ); -}; diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardProgressiveSteps.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardProgressiveSteps.tsx new file mode 100644 index 00000000000..abd08e5563e --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardProgressiveSteps.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; + +export const WizardProgressiveSteps: React.FunctionComponent = () => ( + + + Get started content + + + Update options content + + + Substep 1 content + , + + Substep 2 content + , + + Substep 3 content + + ]} + /> + + Review content + + +); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardStepDrawerContent.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardStepDrawerContent.tsx new file mode 100644 index 00000000000..5a5d001e3dd --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardStepDrawerContent.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +import { + Drawer, + DrawerContent, + DrawerPanelContent, + DrawerColorVariant, + DrawerHead, + DrawerActions, + DrawerCloseButton, + Flex, + Button +} from '@patternfly/react-core'; +import { useWizardContext, Wizard, WizardStep } from '@patternfly/react-core/next'; + +const StepContentWithDrawer: React.FunctionComponent = () => { + const [isDrawerExpanded, setIsDrawerExpanded] = React.useState(false); + const { activeStep } = useWizardContext(); + const drawerRef = React.useRef(null); + + const onWizardExpand = () => drawerRef.current && drawerRef.current.focus(); + + return ( + + + + + Drawer content: {activeStep?.name} + + + setIsDrawerExpanded(false)} /> + + + + } + > + + {!isDrawerExpanded && ( + + )} +
{activeStep?.name} content
+
+
+
+ ); +}; + +export const WizardStepDrawerContent: React.FunctionComponent = () => ( + + + + + + + , + + + + ]} + > + +
+ + Review step content + +
+); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardStepErrorStatus.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardStepErrorStatus.tsx new file mode 100644 index 00000000000..9f535a0e7a7 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardStepErrorStatus.tsx @@ -0,0 +1,55 @@ +import React from 'react'; + +import { Checkbox } from '@patternfly/react-core'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; + +interface SomeContextProps { + errorMessage: string | undefined; + setErrorMessage(error: string | undefined): void; +} +type SomeContextRenderProps = Pick; +interface SomeContextProviderProps { + children: (context: SomeContextRenderProps) => React.ReactElement; +} + +const SomeContext: React.Context = React.createContext({} as SomeContextProps); + +const SomeContextProvider = ({ children }: SomeContextProviderProps) => { + const [errorMessage, setErrorMessage] = React.useState(); + + return ( + {children({ errorMessage })} + ); +}; + +const StepContentWithAction = () => { + const { errorMessage, setErrorMessage } = React.useContext(SomeContext); + + return ( + setErrorMessage(checked ? 'Some error message' : undefined)} + id="toggle-error-checkbox" + name="Toggle Error Checkbox" + /> + ); +}; + +export const WizardStepErrorStatus: React.FunctionComponent = () => ( + + {({ errorMessage }) => ( + + + Step 1 content + + + + + + Review step content + + + )} + +); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardStepVisitRequired.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardStepVisitRequired.tsx new file mode 100644 index 00000000000..8eb8e011c53 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardStepVisitRequired.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; + +export const WizardStepVisitRequired: React.FunctionComponent = () => ( + + + Step 1 content + + + Step 2 content + + + Step 3 content + + + Step 4 content + + + Review step content + + +); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardToggleStepVisibility.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardToggleStepVisibility.tsx new file mode 100644 index 00000000000..ade517a8c20 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardToggleStepVisibility.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { Checkbox } from '@patternfly/react-core'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; + +interface SomeContextProps { + isToggleStepChecked: boolean; + setIsToggleStepChecked(isHidden: boolean): void; +} +type SomeContextRenderProps = Pick; +interface SomeContextProviderProps { + children: (context: SomeContextRenderProps) => React.ReactElement; +} + +const SomeContext: React.Context = React.createContext({} as SomeContextProps); + +const SomeContextProvider: React.FunctionComponent = ({ children }) => { + const [isToggleStepChecked, setIsToggleStepChecked] = React.useState(false); + + return ( + + {children({ isToggleStepChecked })} + + ); +}; + +const StepContentWithAction: React.FunctionComponent = () => { + const { isToggleStepChecked, setIsToggleStepChecked } = React.useContext(SomeContext); + + return ( + setIsToggleStepChecked(checked)} + id="toggle-hide-step-checkbox" + name="Toggle Hide Step Checkbox" + /> + ); +}; + +export const WizardToggleStepVisibility: React.FunctionComponent = () => ( + + {({ isToggleStepChecked }) => ( + + + + + + Step 2 content + + + Review step content + + + )} + +); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardValidateOnButtonPress.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardValidateOnButtonPress.tsx new file mode 100644 index 00000000000..b43782815f5 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardValidateOnButtonPress.tsx @@ -0,0 +1,183 @@ +import React from 'react'; + +import { + Button, + Alert, + EmptyState, + EmptyStateIcon, + Title, + EmptyStateBody, + Progress, + EmptyStateSecondaryActions, + Form, + FormGroup, + TextInput +} from '@patternfly/react-core'; +import { Wizard, WizardStep, WizardFooterWrapper, useWizardContext } from '@patternfly/react-core/next'; +import CogsIcon from '@patternfly/react-icons/dist/esm/icons/cogs-icon'; + +interface ValidationProgressProps { + onClose(): void; +} + +const ValidationProgress: React.FunctionComponent = ({ onClose }) => { + const [percentValidated, setPercentValidated] = React.useState(0); + + const tick = React.useCallback(() => { + if (percentValidated < 100) { + setPercentValidated(prevValue => prevValue + 20); + } + }, [percentValidated]); + + React.useEffect(() => { + const interval = setInterval(() => tick(), 1000); + + return () => { + clearInterval(interval); + }; + }, [tick]); + + return ( +
+ + + + {percentValidated === 100 ? 'Validation complete' : 'Validating credentials'} + + + + + + Description can be used to further elaborate on the validation step, or give the user a better idea of how + long the process will take. + + + + + +
+ ); +}; + +interface LastStepFooterProps { + isValid: boolean; + setIsSubmitted(isSubmitted: boolean): void; + setHasErrorOnSubmit(isSubmitted: boolean): void; +} + +const LastStepFooter: React.FunctionComponent = ({ + isValid, + setIsSubmitted, + setHasErrorOnSubmit +}) => { + const { goToNextStep, goToPrevStep } = useWizardContext(); + + const onValidate = () => { + setIsSubmitted(true); + + if (!isValid) { + setIsSubmitted(false); + setHasErrorOnSubmit(true); + } else { + goToNextStep(); + } + }; + + return ( + + + + + ); +}; + +interface SampleFormProps { + value: string; + isValid: boolean; + setValue: (value: string) => void; + setIsValid: (isValid: boolean) => void; +} + +const SampleForm: React.FunctionComponent = ({ value, isValid, setValue, setIsValid }) => { + const validated = isValid ? 'default' : 'error'; + + const handleTextInputChange = (value: string) => { + const isValid = /^\d+$/.test(value); + + setValue(value); + setIsValid(isValid); + }; + + return ( +
+ + + +
+ ); +}; + +export const WizardValidateOnButtonPress: React.FunctionComponent = () => { + const [ageValue, setAgeValue] = React.useState('Thirty'); + const [isSubmitted, setIsSubmitted] = React.useState(false); + const [isFirstStepValid, setIsFirstStepValid] = React.useState(false); + const [hasErrorOnSubmit, setHasErrorOnSubmit] = React.useState(false); + + // eslint-disable-next-line no-console + const onClose = () => console.log('Some close action occurs here.'); + + if (isSubmitted && isFirstStepValid) { + return ; + } + + return ( + + + Step 1 content + + + Step 2 content + + + } + > + {hasErrorOnSubmit && ( +
+ +
+ )} + setAgeValue(value)} + isValid={!hasErrorOnSubmit || isFirstStepValid} + setIsValid={setIsFirstStepValid} + /> +
+
+ ); +}; diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardWithCustomFooter.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardWithCustomFooter.tsx new file mode 100644 index 00000000000..aff46356d29 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardWithCustomFooter.tsx @@ -0,0 +1,107 @@ +import React from 'react'; + +import { Button, Flex, FlexItem, Spinner } from '@patternfly/react-core'; +import { useWizardContext, Wizard, WizardFooter, WizardFooterWrapper, WizardStep } from '@patternfly/react-core/next'; + +const CustomWizardFooter = () => { + const { activeStep, goToNextStep, goToPrevStep, close } = useWizardContext(); + + return ( + + ); +}; + +const CustomStepTwoFooter = () => { + const { goToNextStep, goToPrevStep, close } = useWizardContext(); + const [isLoading, setIsLoading] = React.useState(false); + + async function onNext() { + setIsLoading(true); + await new Promise(resolve => setTimeout(resolve, 2000)); + setIsLoading(false); + + goToNextStep(); + } + + return ( + + + + + + ); +}; + +interface ReviewStepContentProps { + isSubmitting: boolean | undefined; +} + +const ReviewStepContent: React.FunctionComponent = ({ isSubmitting }) => { + if (isSubmitting === undefined) { + return Review step content; + } + + if (isSubmitting) { + return ( + <> +
Calculating wizard score...
+ + + ); + } + + return <>50 points to Gryffindor!; +}; + +export const WizardWithCustomFooter: React.FunctionComponent = () => { + const [isSubmitting, setIsSubmitting] = React.useState(); + + async function onSubmit(): Promise { + setIsSubmitting(true); + + await new Promise(resolve => setTimeout(resolve, 5000)); + + setIsSubmitting(false); + } + + return ( + }> + + Step 1 content + + }> + Step 2 content with a custom async footer + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardCustomNav.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardWithCustomNav.tsx similarity index 58% rename from packages/react-core/src/next/components/Wizard/examples/WizardCustomNav.tsx rename to packages/react-core/src/next/components/Wizard/examples/WizardWithCustomNav.tsx index d6e96379c8f..3b120321bd7 100644 --- a/packages/react-core/src/next/components/Wizard/examples/WizardCustomNav.tsx +++ b/packages/react-core/src/next/components/Wizard/examples/WizardWithCustomNav.tsx @@ -2,17 +2,17 @@ import React from 'react'; import { Wizard, WizardStep, - WizardControlStep, + WizardStepType, CustomWizardNavFunction, WizardNav, WizardNavItem } from '@patternfly/react-core/next'; -export const WizardCustomNav: React.FunctionComponent = () => { +export const WizardWithCustomNav: React.FunctionComponent = () => { const nav: CustomWizardNavFunction = ( isExpanded: boolean, - steps: WizardControlStep[], - activeStep: WizardControlStep, + steps: WizardStepType[], + activeStep: WizardStepType, goToStepByIndex: (index: number) => void ) => ( @@ -24,7 +24,7 @@ export const WizardCustomNav: React.FunctionComponent = () => { isCurrent={activeStep.id === step.id} isDisabled={step.isDisabled} stepIndex={step.index} - onNavItemClick={goToStepByIndex} + onClick={() => goToStepByIndex(step.index)} /> ))} @@ -32,14 +32,14 @@ export const WizardCustomNav: React.FunctionComponent = () => { return ( - -

Did you say...custom nav?

+ + Did you say...custom nav? - -

Step 2 content

+ + Oh, you didn't? - -

Review step content

+ + Well, this is awkward.
); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardWithCustomNavItem.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardWithCustomNavItem.tsx new file mode 100644 index 00000000000..6d4291be7a7 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardWithCustomNavItem.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; + +export const WizardWithCustomNavItem: React.FunctionComponent = () => ( + + + Step 1 content + + Custom item + }} + > + Step 2 content with a custom navigation item + + + Review step content + + +); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardWithHeader.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardWithHeader.tsx new file mode 100644 index 00000000000..a334d0fb26b --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardWithHeader.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Wizard, WizardHeader, WizardStep } from '@patternfly/react-core/next'; + +export const WizardWithHeader: React.FunctionComponent = () => ( + + } + > + + Step 1 content + + + Step 2 content + + + Review step content + + +); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardWithNavAnchors.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardWithNavAnchors.tsx new file mode 100644 index 00000000000..67e842ce3d9 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardWithNavAnchors.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { Button } from '@patternfly/react-core'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; +import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; +import SlackHashIcon from '@patternfly/react-icons/dist/esm/icons/slack-hash-icon'; + +export const WizardWithNavAnchors: React.FunctionComponent = () => ( + + + PF3 + + } + id="navanchors-pf3-step" + navItem={{ component: 'a', href: 'https://www.patternfly.org/v3', target: '_blank' }} + > + Step 1: Read about PF3 + + + PF4 + + } + id="navanchors-pf4-step" + navItem={{ component: 'a', href: 'https://www.patternfly.org/v4', target: '_blank' }} + > + Step 2: Read about PF4 + + + Join us on slack + + } + id="navanchors-slack-step" + navItem={{ component: 'a', href: 'https://patternfly.slack.com', target: '_blank' }} + > + + + +); diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardWithSubmitProgress.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardWithSubmitProgress.tsx new file mode 100644 index 00000000000..dc90412de50 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardWithSubmitProgress.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { + EmptyState, + EmptyStateIcon, + EmptyStateBody, + EmptyStateSecondaryActions, + Title, + Progress, + Button +} from '@patternfly/react-core'; +import { Wizard, WizardStep } from '@patternfly/react-core/next'; +import CogsIcon from '@patternfly/react-icons/dist/esm/icons/cogs-icon'; + +interface ValidationProgressProps { + onClose(): void; +} + +const ValidationProgress: React.FunctionComponent = ({ onClose }) => { + const [percentValidated, setPercentValidated] = React.useState(0); + + const tick = React.useCallback(() => { + if (percentValidated < 100) { + setPercentValidated(prevValue => prevValue + 20); + } + }, [percentValidated]); + + React.useEffect(() => { + const interval = setInterval(() => tick(), 1000); + + return () => { + clearInterval(interval); + }; + }, [tick]); + + return ( +
+ + + + {percentValidated === 100 ? 'Validation complete' : 'Validating credentials'} + + + + + + Description can be used to further elaborate on the validation step, or give the user a better idea of how + long the process will take. + + + + + +
+ ); +}; + +export const WizardWithSubmitProgress: React.FunctionComponent = () => { + const [isSubmitted, setIsSubmitted] = React.useState(false); + + // eslint-disable-next-line no-console + const onClose = () => console.log('Some close action occurs here.'); + + if (isSubmitted) { + return ; + } + + return ( + + + Step 1 content + + + Step 2 content + + setIsSubmitted(true) }} + > + Review step content + + + ); +}; diff --git a/packages/react-core/src/next/components/Wizard/examples/WizardWithinModal.tsx b/packages/react-core/src/next/components/Wizard/examples/WizardWithinModal.tsx new file mode 100644 index 00000000000..9d46b9f3a02 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/examples/WizardWithinModal.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Button, Modal, ModalVariant } from '@patternfly/react-core'; +import { Wizard, WizardStep, WizardHeader } from '@patternfly/react-core/next'; + +export const WizardWithinModal = () => { + const [isModelOpen, setIsModalOpen] = React.useState(false); + + return ( + <> + + + + } + > + + Step 1 content + + + Step 2 content + + setIsModalOpen(false) }} + > + Review step content + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Wizard/hooks/index.ts b/packages/react-core/src/next/components/Wizard/hooks/index.ts index c0abee9f15b..5b343ba2ed6 100644 --- a/packages/react-core/src/next/components/Wizard/hooks/index.ts +++ b/packages/react-core/src/next/components/Wizard/hooks/index.ts @@ -1 +1,2 @@ export { useWizardFooter } from './useWizardFooter'; +export { useGetMergedSteps } from './useGetMergedSteps'; diff --git a/packages/react-core/src/next/components/Wizard/hooks/useGetMergedSteps.tsx b/packages/react-core/src/next/components/Wizard/hooks/useGetMergedSteps.tsx new file mode 100644 index 00000000000..0e61cf41198 --- /dev/null +++ b/packages/react-core/src/next/components/Wizard/hooks/useGetMergedSteps.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import isEqual from 'fast-deep-equal'; +import { WizardStepType } from '../types'; + +/** + * Get stable ref of combined initial (prop provided) and current (context stored) steps. + * @param initialSteps + * @param currentSteps + * @returns WizardStepType[] + */ +export const useGetMergedSteps = (initialSteps: WizardStepType[], currentSteps: WizardStepType[]): WizardStepType[] => { + const initialStepsRef = React.useRef(initialSteps); + const currentStepsRef = React.useRef(currentSteps); + + // Check each value within the array of initialSteps (props) and currentSteps (context). + // If any value changes for either of these, update the reference. + if (!isEqual(initialStepsRef.current, initialSteps)) { + initialStepsRef.current = initialSteps; + } + if (!isEqual(currentStepsRef.current, currentSteps)) { + currentStepsRef.current = currentSteps; + } + + // Combine both initial and current steps, where prop provided values + // take precedence over what's stored in context. + const mergedSteps = React.useMemo( + () => + initialSteps.reduce((acc, initialStepProps, index) => { + const currentStepProps = currentSteps[index]; + + if (initialStepProps.id === currentStepProps.id) { + acc.push({ + ...currentStepProps, + ...initialStepProps + }); + } + + return acc; + }, []), + // eslint-disable-next-line react-hooks/exhaustive-deps + [initialStepsRef.current, currentStepsRef.current] + ); + + return mergedSteps; +}; diff --git a/packages/react-core/src/next/components/Wizard/index.ts b/packages/react-core/src/next/components/Wizard/index.ts index 88def117af3..6d29b46b423 100644 --- a/packages/react-core/src/next/components/Wizard/index.ts +++ b/packages/react-core/src/next/components/Wizard/index.ts @@ -6,6 +6,6 @@ export * from './WizardStep'; export * from './WizardNav'; export * from './WizardNavItem'; export * from './WizardHeader'; -export * from './hooks'; export * from './types'; -export { useWizardContext } from './WizardContext'; +export { useWizardFooter } from './hooks'; +export { useWizardContext, WizardContext } from './WizardContext'; diff --git a/packages/react-core/src/next/components/Wizard/types.tsx b/packages/react-core/src/next/components/Wizard/types.tsx index 80f0d2ad8c4..2226471616e 100644 --- a/packages/react-core/src/next/components/Wizard/types.tsx +++ b/packages/react-core/src/next/components/Wizard/types.tsx @@ -34,8 +34,8 @@ export enum WizardNavItemStatus { export interface WizardParentStep extends WizardBasicStep { /** Nested step IDs */ subStepIds: (string | number)[]; - /** Flag to determine whether sub-steps can collapse or not */ - isCollapsible?: boolean; + /** Flag to determine whether the step can expand or not. */ + isExpandable?: boolean; } /** Type used to define sub-steps. */ @@ -45,22 +45,12 @@ export interface WizardSubStep extends WizardBasicStep { } /** Encompasses all step type variants that are internally controlled by the Wizard. */ -export type WizardControlStep = WizardBasicStep | WizardParentStep | WizardSubStep; +export type WizardStepType = WizardBasicStep | WizardParentStep | WizardSubStep; -/** Callback for the Wizard's 'onNext', 'onBack', and 'onNavByIndex' properties. */ -export type WizardNavStepFunction = ( - currentStep: WizardNavStepData, - previousStep: WizardNavStepData -) => void | Promise; - -/** Data returned for either parameter of WizardNavStepFunction. */ -export interface WizardNavStepData { - /** Unique identifier */ - id: string | number; - /** Name of the step */ - name: string; - /** Index of the step (starts at 1) */ - index: number; +export enum WizardStepChangeScope { + Next = 'next', + Back = 'back', + Nav = 'nav' } export type WizardFooterType = Partial | CustomWizardFooterFunction | React.ReactElement; @@ -70,25 +60,25 @@ export type WizardNavItemType = Partial | CustomWizardNavIte /** Callback for the Wizard's 'nav' property. Returns element which replaces the Wizard's default navigation. */ export type CustomWizardNavFunction = ( isExpanded: boolean, - steps: WizardControlStep[], - activeStep: WizardControlStep, + steps: WizardStepType[], + activeStep: WizardStepType, 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, - activeStep: WizardControlStep, - steps: WizardControlStep[], + step: WizardStepType, + activeStep: WizardStepType, + steps: WizardStepType[], 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 = ( - activeStep: WizardControlStep, - onNext: () => void | Promise, - onBack: () => void | Promise, - onClose: () => void | Promise + activeStep: WizardStepType, + onNext: (event: React.MouseEvent) => void | Promise, + onBack: (event: React.MouseEvent) => void | Promise, + onClose: (event: React.MouseEvent) => void | Promise ) => React.ReactElement; export function isCustomWizardNav(nav: WizardNavType): nav is CustomWizardNavFunction | React.ReactElement { @@ -107,14 +97,14 @@ export function isCustomWizardFooter( return typeof footer === 'function' || React.isValidElement(footer); } -export function isWizardBasicStep(step: WizardControlStep): step is WizardBasicStep { +export function isWizardBasicStep(step: WizardStepType): step is WizardBasicStep { return (step as WizardParentStep)?.subStepIds === undefined && !isWizardSubStep(step); } -export function isWizardSubStep(step: WizardControlStep): step is WizardSubStep { +export function isWizardSubStep(step: WizardStepType): step is WizardSubStep { return (step as WizardSubStep)?.parentId !== undefined; } -export function isWizardParentStep(step: WizardControlStep): step is WizardParentStep { +export function isWizardParentStep(step: WizardStepType): step is WizardParentStep { return (step as WizardParentStep)?.subStepIds !== undefined; } diff --git a/packages/react-core/src/next/components/Wizard/utils.ts b/packages/react-core/src/next/components/Wizard/utils.ts index 109523349f3..accd1db2fc2 100644 --- a/packages/react-core/src/next/components/Wizard/utils.ts +++ b/packages/react-core/src/next/components/Wizard/utils.ts @@ -1,45 +1,45 @@ import React from 'react'; -import { WizardControlStep, WizardNavStepData } from './types'; +import { WizardStepType } from './types'; import { WizardStep, WizardStepProps } from './WizardStep'; /** * Accumulate list of step & sub-step props pulled from child components * @param children - * @returns WizardControlStep[] + * @returns WizardStepType[] */ -export const buildSteps = (children: React.ReactElement | React.ReactElement[]) => - React.Children.toArray(children).reduce((acc: WizardControlStep[], child: React.ReactNode) => { +export const buildSteps = (children: React.ReactNode | React.ReactNode[]) => + React.Children.toArray(children).reduce((acc: WizardStepType[], child: React.ReactNode, index: number) => { if (isWizardStep(child)) { - const { steps: subSteps, id, isHidden, isDisabled } = child.props; - const subControlledSteps: WizardControlStep[] = []; + const { props: childProps } = child; + const { steps: childStepComponents, id } = childProps; const stepIndex = acc.length + 1; + const subSteps: WizardStepType[] = []; 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 === 0 && !childStepComponents?.length && { isVisited: true }), + ...(childStepComponents && { + subStepIds: childStepComponents?.map((childStepComponent, subStepIndex) => { + subSteps.push({ index: stepIndex + subStepIndex + 1, - ...normalizeStep(subStep.props) + component: childStepComponent, + parentId: id, + ...(index === 0 && subStepIndex === 0 && { isVisited: true }), + ...normalizeStepProps(childStepComponent.props) }); - return subStep.props.id; + return childStepComponent.props.id; }) }), - ...normalizeStep(child.props) + ...normalizeStepProps(childProps) }, - ...subControlledSteps + ...subSteps ); } else { - throw new Error('Wizard only accepts children with required WizardStepProps.'); + throw new Error('Wizard only accepts children of type WizardStep.'); } return acc; @@ -48,21 +48,15 @@ export const buildSteps = (children: React.ReactElement | React export function isWizardStep( child: any | React.ReactElement ): child is React.ReactElement { - return ( - (React.isValidElement(child) && (child as React.ReactElement).type === WizardStep) || - (child.props?.name !== undefined && child.props?.id !== undefined) - ); + return React.isValidElement(child) && (child as React.ReactElement).type === WizardStep; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const normalizeStep = ({ children, steps, ...controlStep }: WizardStepProps): Omit => - controlStep; - -export const normalizeNavStep = (navStep: WizardControlStep): WizardNavStepData => ({ - id: navStep.id, - index: navStep.index, - name: navStep.name.toString() -}); +// Omit "children" and "steps" when building steps for the Wizard's context +export const normalizeStepProps = ({ + children: _children, + steps: _steps, + ...controlStep +}: WizardStepProps): Omit => controlStep; -export const getActiveStep = (steps: WizardControlStep[], activeStepIndex: number) => +export const getActiveStep = (steps: WizardStepType[], activeStepIndex: number) => steps.find(step => step.index === activeStepIndex); diff --git a/packages/react-integration/demo-app-ts/src/components/demos/MenuDemo/MenuDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/MenuDemo/MenuDemo.tsx index f4717213631..2a2428c044a 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/MenuDemo/MenuDemo.tsx +++ b/packages/react-integration/demo-app-ts/src/components/demos/MenuDemo/MenuDemo.tsx @@ -321,9 +321,9 @@ export class MenuDemo extends Component { renderMenuWithTitledGroups() { const { activeItem } = this.state; - const GroupMenuExampleCmp: React.FC<{ className: string }> = ({ className }) => ( + const GroupMenuExampleCmp: React.FunctionComponent<{ className: string }> = ({ className }) => (
-

Group 4

+

Group 4

); @@ -372,7 +372,7 @@ export class MenuDemo extends Component { Link 1 - + }> Link 1