@patternfly/react-core 4.234.4 → 4.235.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/esm/next/components/Wizard/Wizard.d.ts +40 -0
  3. package/dist/esm/next/components/Wizard/Wizard.d.ts.map +1 -0
  4. package/dist/esm/next/components/Wizard/Wizard.js +86 -0
  5. package/dist/esm/next/components/Wizard/Wizard.js.map +1 -0
  6. package/dist/esm/next/components/Wizard/WizardBody.d.ts +20 -0
  7. package/dist/esm/next/components/Wizard/WizardBody.d.ts.map +1 -0
  8. package/dist/esm/next/components/Wizard/WizardBody.js +7 -0
  9. package/dist/esm/next/components/Wizard/WizardBody.js.map +1 -0
  10. package/dist/esm/next/components/Wizard/WizardContext.d.ts +49 -0
  11. package/dist/esm/next/components/Wizard/WizardContext.d.ts.map +1 -0
  12. package/dist/esm/next/components/Wizard/WizardContext.js +34 -0
  13. package/dist/esm/next/components/Wizard/WizardContext.js.map +1 -0
  14. package/dist/esm/next/components/Wizard/WizardFooter.d.ts +28 -0
  15. package/dist/esm/next/components/Wizard/WizardFooter.d.ts.map +1 -0
  16. package/dist/esm/next/components/Wizard/WizardFooter.js +11 -0
  17. package/dist/esm/next/components/Wizard/WizardFooter.js.map +1 -0
  18. package/dist/esm/next/components/Wizard/WizardHeader.d.ts +21 -0
  19. package/dist/esm/next/components/Wizard/WizardHeader.d.ts.map +1 -0
  20. package/dist/esm/next/components/Wizard/WizardHeader.js +13 -0
  21. package/dist/esm/next/components/Wizard/WizardHeader.js.map +1 -0
  22. package/dist/esm/next/components/Wizard/WizardNav.d.ts +15 -0
  23. package/dist/esm/next/components/Wizard/WizardNav.d.ts.map +1 -0
  24. package/dist/esm/next/components/Wizard/WizardNav.js +13 -0
  25. package/dist/esm/next/components/Wizard/WizardNav.js.map +1 -0
  26. package/dist/esm/next/components/Wizard/WizardNavItem.d.ts +25 -0
  27. package/dist/esm/next/components/Wizard/WizardNavItem.d.ts.map +1 -0
  28. package/dist/esm/next/components/Wizard/WizardNavItem.js +33 -0
  29. package/dist/esm/next/components/Wizard/WizardNavItem.js.map +1 -0
  30. package/dist/esm/next/components/Wizard/WizardStep.d.ts +20 -0
  31. package/dist/esm/next/components/Wizard/WizardStep.d.ts.map +1 -0
  32. package/dist/esm/next/components/Wizard/WizardStep.js +5 -0
  33. package/dist/esm/next/components/Wizard/WizardStep.js.map +1 -0
  34. package/dist/esm/next/components/Wizard/WizardToggle.d.ts +26 -0
  35. package/dist/esm/next/components/Wizard/WizardToggle.d.ts.map +1 -0
  36. package/dist/esm/next/components/Wizard/WizardToggle.js +81 -0
  37. package/dist/esm/next/components/Wizard/WizardToggle.js.map +1 -0
  38. package/dist/esm/next/components/Wizard/hooks/index.d.ts +2 -0
  39. package/dist/esm/next/components/Wizard/hooks/index.d.ts.map +1 -0
  40. package/dist/esm/next/components/Wizard/hooks/index.js +2 -0
  41. package/dist/esm/next/components/Wizard/hooks/index.js.map +1 -0
  42. package/dist/esm/next/components/Wizard/hooks/useWizardFooter.d.ts +8 -0
  43. package/dist/esm/next/components/Wizard/hooks/useWizardFooter.d.ts.map +1 -0
  44. package/dist/esm/next/components/Wizard/hooks/useWizardFooter.js +20 -0
  45. package/dist/esm/next/components/Wizard/hooks/useWizardFooter.js.map +1 -0
  46. package/dist/esm/next/components/Wizard/index.d.ts +12 -0
  47. package/dist/esm/next/components/Wizard/index.d.ts.map +1 -0
  48. package/dist/esm/next/components/Wizard/index.js +12 -0
  49. package/dist/esm/next/components/Wizard/index.js.map +1 -0
  50. package/dist/esm/next/components/Wizard/types.d.ts +69 -0
  51. package/dist/esm/next/components/Wizard/types.d.ts.map +1 -0
  52. package/dist/esm/next/components/Wizard/types.js +20 -0
  53. package/dist/esm/next/components/Wizard/types.js.map +1 -0
  54. package/dist/esm/next/components/Wizard/utils.d.ts +15 -0
  55. package/dist/esm/next/components/Wizard/utils.d.ts.map +1 -0
  56. package/dist/esm/next/components/Wizard/utils.js +35 -0
  57. package/dist/esm/next/components/Wizard/utils.js.map +1 -0
  58. package/dist/esm/next/components/index.d.ts +1 -1
  59. package/dist/esm/next/components/index.d.ts.map +1 -1
  60. package/dist/esm/next/components/index.js +1 -1
  61. package/dist/esm/next/components/index.js.map +1 -1
  62. package/dist/js/next/components/Wizard/Wizard.d.ts +40 -0
  63. package/dist/js/next/components/Wizard/Wizard.d.ts.map +1 -0
  64. package/dist/js/next/components/Wizard/Wizard.js +90 -0
  65. package/dist/js/next/components/Wizard/Wizard.js.map +1 -0
  66. package/dist/js/next/components/Wizard/WizardBody.d.ts +20 -0
  67. package/dist/js/next/components/Wizard/WizardBody.d.ts.map +1 -0
  68. package/dist/js/next/components/Wizard/WizardBody.js +12 -0
  69. package/dist/js/next/components/Wizard/WizardBody.js.map +1 -0
  70. package/dist/js/next/components/Wizard/WizardContext.d.ts +49 -0
  71. package/dist/js/next/components/Wizard/WizardContext.d.ts.map +1 -0
  72. package/dist/js/next/components/Wizard/WizardContext.js +40 -0
  73. package/dist/js/next/components/Wizard/WizardContext.js.map +1 -0
  74. package/dist/js/next/components/Wizard/WizardFooter.d.ts +28 -0
  75. package/dist/js/next/components/Wizard/WizardFooter.d.ts.map +1 -0
  76. package/dist/js/next/components/Wizard/WizardFooter.js +16 -0
  77. package/dist/js/next/components/Wizard/WizardFooter.js.map +1 -0
  78. package/dist/js/next/components/Wizard/WizardHeader.d.ts +21 -0
  79. package/dist/js/next/components/Wizard/WizardHeader.d.ts.map +1 -0
  80. package/dist/js/next/components/Wizard/WizardHeader.js +18 -0
  81. package/dist/js/next/components/Wizard/WizardHeader.js.map +1 -0
  82. package/dist/js/next/components/Wizard/WizardNav.d.ts +15 -0
  83. package/dist/js/next/components/Wizard/WizardNav.d.ts.map +1 -0
  84. package/dist/js/next/components/Wizard/WizardNav.js +18 -0
  85. package/dist/js/next/components/Wizard/WizardNav.js.map +1 -0
  86. package/dist/js/next/components/Wizard/WizardNavItem.d.ts +25 -0
  87. package/dist/js/next/components/Wizard/WizardNavItem.d.ts.map +1 -0
  88. package/dist/js/next/components/Wizard/WizardNavItem.js +37 -0
  89. package/dist/js/next/components/Wizard/WizardNavItem.js.map +1 -0
  90. package/dist/js/next/components/Wizard/WizardStep.d.ts +20 -0
  91. package/dist/js/next/components/Wizard/WizardStep.d.ts.map +1 -0
  92. package/dist/js/next/components/Wizard/WizardStep.js +10 -0
  93. package/dist/js/next/components/Wizard/WizardStep.js.map +1 -0
  94. package/dist/js/next/components/Wizard/WizardToggle.d.ts +26 -0
  95. package/dist/js/next/components/Wizard/WizardToggle.d.ts.map +1 -0
  96. package/dist/js/next/components/Wizard/WizardToggle.js +86 -0
  97. package/dist/js/next/components/Wizard/WizardToggle.js.map +1 -0
  98. package/dist/js/next/components/Wizard/hooks/index.d.ts +2 -0
  99. package/dist/js/next/components/Wizard/hooks/index.d.ts.map +1 -0
  100. package/dist/js/next/components/Wizard/hooks/index.js +6 -0
  101. package/dist/js/next/components/Wizard/hooks/index.js.map +1 -0
  102. package/dist/js/next/components/Wizard/hooks/useWizardFooter.d.ts +8 -0
  103. package/dist/js/next/components/Wizard/hooks/useWizardFooter.d.ts.map +1 -0
  104. package/dist/js/next/components/Wizard/hooks/useWizardFooter.js +25 -0
  105. package/dist/js/next/components/Wizard/hooks/useWizardFooter.js.map +1 -0
  106. package/dist/js/next/components/Wizard/index.d.ts +12 -0
  107. package/dist/js/next/components/Wizard/index.d.ts.map +1 -0
  108. package/dist/js/next/components/Wizard/index.js +17 -0
  109. package/dist/js/next/components/Wizard/index.js.map +1 -0
  110. package/dist/js/next/components/Wizard/types.d.ts +69 -0
  111. package/dist/js/next/components/Wizard/types.d.ts.map +1 -0
  112. package/dist/js/next/components/Wizard/types.js +29 -0
  113. package/dist/js/next/components/Wizard/types.js.map +1 -0
  114. package/dist/js/next/components/Wizard/utils.d.ts +15 -0
  115. package/dist/js/next/components/Wizard/utils.d.ts.map +1 -0
  116. package/dist/js/next/components/Wizard/utils.js +41 -0
  117. package/dist/js/next/components/Wizard/utils.js.map +1 -0
  118. package/dist/js/next/components/index.d.ts +1 -1
  119. package/dist/js/next/components/index.d.ts.map +1 -1
  120. package/dist/js/next/components/index.js +1 -1
  121. package/dist/js/next/components/index.js.map +1 -1
  122. package/dist/umd/react-core.css +1055 -1055
  123. package/dist/umd/react-core.min.css +70 -70
  124. package/package.json +5 -5
  125. package/src/components/Wizard/examples/Wizard.md +21 -4
  126. package/src/next/components/Wizard/Wizard.tsx +184 -0
  127. package/src/next/components/Wizard/WizardBody.tsx +34 -0
  128. package/src/next/components/Wizard/WizardContext.tsx +104 -0
  129. package/src/next/components/Wizard/WizardFooter.tsx +63 -0
  130. package/src/next/components/Wizard/WizardHeader.tsx +53 -0
  131. package/src/next/components/Wizard/WizardNav.tsx +41 -0
  132. package/src/next/components/Wizard/WizardNavItem.tsx +103 -0
  133. package/src/next/components/Wizard/WizardStep.tsx +23 -0
  134. package/src/next/components/Wizard/WizardToggle.tsx +209 -0
  135. package/src/next/components/Wizard/__tests__/Wizard.test.tsx +298 -0
  136. package/src/next/components/Wizard/examples/Wizard.md +125 -0
  137. package/src/next/components/Wizard/examples/WizardBasic.tsx +22 -0
  138. package/src/next/components/Wizard/examples/WizardCustomNav.tsx +46 -0
  139. package/src/next/components/Wizard/examples/WizardKitchenSink.tsx +170 -0
  140. package/src/next/components/Wizard/hooks/index.ts +1 -0
  141. package/src/next/components/Wizard/hooks/useWizardFooter.tsx +22 -0
  142. package/src/next/components/Wizard/index.ts +11 -0
  143. package/src/next/components/Wizard/types.tsx +101 -0
  144. package/src/next/components/Wizard/utils.ts +47 -0
  145. package/src/next/components/index.ts +1 -1
@@ -0,0 +1,41 @@
1
+ import * as React from 'react';
2
+ import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
3
+ import { css } from '@patternfly/react-styles';
4
+
5
+ export interface WizardNavProps {
6
+ /** children should be WizardNavItem components */
7
+ children?: any;
8
+ /** Aria-label applied to the nav element */
9
+ 'aria-label'?: string;
10
+ /** Sets the aria-labelledby attribute on the nav element */
11
+ 'aria-labelledby'?: string;
12
+ /** Whether the nav is expanded */
13
+ isOpen?: boolean;
14
+ /** True to return the inner list without the wrapping nav element */
15
+ returnList?: boolean;
16
+ }
17
+
18
+ export const WizardNav: React.FunctionComponent<WizardNavProps> = ({
19
+ children,
20
+ 'aria-label': ariaLabel,
21
+ 'aria-labelledby': ariaLabelledBy,
22
+ isOpen = false,
23
+ returnList = false
24
+ }: WizardNavProps) => {
25
+ const innerList = <ol className={css(styles.wizardNavList)}>{children}</ol>;
26
+
27
+ if (returnList) {
28
+ return innerList;
29
+ }
30
+
31
+ return (
32
+ <nav
33
+ className={css(styles.wizardNav, isOpen && styles.modifiers.expanded)}
34
+ aria-label={ariaLabel}
35
+ aria-labelledby={ariaLabelledBy}
36
+ >
37
+ <ol className={css(styles.wizardNavList)}>{children}</ol>
38
+ </nav>
39
+ );
40
+ };
41
+ WizardNav.displayName = 'WizardNav';
@@ -0,0 +1,103 @@
1
+ import * as React from 'react';
2
+ import { css } from '@patternfly/react-styles';
3
+ import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
4
+ import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
5
+
6
+ export interface WizardNavItemProps {
7
+ /** Can nest a WizardNav component for substeps */
8
+ children?: React.ReactNode;
9
+ /** The content to display in the nav item */
10
+ content?: React.ReactNode;
11
+ /** Whether the nav item is the currently active item */
12
+ isCurrent?: boolean;
13
+ /** Whether the nav item is disabled */
14
+ isDisabled?: boolean;
15
+ /** The step passed into the onNavItemClick callback */
16
+ step: number;
17
+ /** Callback for when the nav item is clicked */
18
+ onNavItemClick?: (step: number) => any;
19
+ /** Component used to render WizardNavItem */
20
+ navItemComponent?: 'button' | 'a';
21
+ /** An optional url to use for when using an anchor component */
22
+ href?: string;
23
+ /** Flag indicating that this NavItem has child steps and is expandable */
24
+ isExpandable?: boolean;
25
+ /** The id for the nav item */
26
+ id?: string | number;
27
+ }
28
+
29
+ export const WizardNavItem: React.FunctionComponent<WizardNavItemProps> = ({
30
+ children = null,
31
+ content = '',
32
+ isCurrent = false,
33
+ isDisabled = false,
34
+ step,
35
+ onNavItemClick = () => undefined,
36
+ navItemComponent = 'button',
37
+ href = null,
38
+ isExpandable = false,
39
+ id,
40
+ ...rest
41
+ }: WizardNavItemProps) => {
42
+ const NavItemComponent = navItemComponent;
43
+
44
+ const [isExpanded, setIsExpanded] = React.useState(false);
45
+
46
+ React.useEffect(() => {
47
+ setIsExpanded(isCurrent);
48
+ }, [isCurrent]);
49
+
50
+ if (navItemComponent === 'a' && !href && process.env.NODE_ENV !== 'production') {
51
+ // eslint-disable-next-line no-console
52
+ console.error('WizardNavItem: When using an anchor, please provide an href');
53
+ }
54
+
55
+ const btnProps = {
56
+ disabled: isDisabled
57
+ };
58
+
59
+ const linkProps = {
60
+ tabIndex: isDisabled ? -1 : undefined,
61
+ href
62
+ };
63
+
64
+ return (
65
+ <li
66
+ className={css(
67
+ styles.wizardNavItem,
68
+ isExpandable && styles.modifiers.expandable,
69
+ isExpandable && isExpanded && styles.modifiers.expanded
70
+ )}
71
+ >
72
+ <NavItemComponent
73
+ {...rest}
74
+ {...(navItemComponent === 'a' ? { ...linkProps } : { ...btnProps })}
75
+ {...(id && { id: id.toString() })}
76
+ onClick={() => (isExpandable ? setIsExpanded(!isExpanded || isCurrent) : onNavItemClick(step))}
77
+ className={css(
78
+ styles.wizardNavLink,
79
+ isCurrent && styles.modifiers.current,
80
+ isDisabled && styles.modifiers.disabled
81
+ )}
82
+ aria-disabled={isDisabled ? true : null}
83
+ aria-current={isCurrent && !children ? 'step' : false}
84
+ {...(isExpandable && { 'aria-expanded': isExpanded })}
85
+ >
86
+ {isExpandable ? (
87
+ <>
88
+ <span className={css(styles.wizardNavLinkText)}>{content}</span>
89
+ <span className={css(styles.wizardNavLinkToggle)}>
90
+ <span className={css(styles.wizardNavLinkToggleIcon)}>
91
+ <AngleRightIcon />
92
+ </span>
93
+ </span>
94
+ </>
95
+ ) : (
96
+ content
97
+ )}
98
+ </NavItemComponent>
99
+ {children}
100
+ </li>
101
+ );
102
+ };
103
+ WizardNavItem.displayName = 'WizardNavItem';
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+
3
+ import { WizardControlStep } from './types';
4
+ import { WizardBody, WizardBodyProps } from './WizardBody';
5
+
6
+ /**
7
+ * Used as a passthrough of step properties for Wizard and all supporting child components.
8
+ * Also acts as a wrapper for content, with an optional inclusion of WizardBody.
9
+ */
10
+
11
+ export interface WizardStepProps extends Omit<WizardControlStep, 'parentId' | 'subStepIds' | 'visited'> {
12
+ /** Optional for when the step is used as a parent to sub-steps */
13
+ children?: React.ReactNode;
14
+ /** Props for WizardBody that wraps content by default. Can be set to null for exclusion of WizardBody. */
15
+ body?: WizardBodyProps | null;
16
+ /** Optional list of sub-steps */
17
+ steps?: React.ReactElement<WizardStepProps>[];
18
+ }
19
+
20
+ export const WizardStep = ({ body, children }: WizardStepProps) =>
21
+ body === undefined ? <WizardBody {...body}>{children}</WizardBody> : <>{children}</>;
22
+
23
+ WizardStep.displayName = 'WizardStep';
@@ -0,0 +1,209 @@
1
+ import React from 'react';
2
+
3
+ import { css } from '@patternfly/react-styles';
4
+ import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
5
+ import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
6
+ import CaretDownIcon from '@patternfly/react-icons/dist/esm/icons/caret-down-icon';
7
+
8
+ import { KeyTypes } from '../../../helpers/constants';
9
+ import { WizardNav, WizardNavItem } from '../Wizard';
10
+ import {
11
+ WizardControlStep,
12
+ CustomWizardNavFunction,
13
+ DefaultWizardNavProps,
14
+ isWizardBasicStep,
15
+ isWizardParentStep,
16
+ isWizardSubStep,
17
+ isCustomWizardNav
18
+ } from './types';
19
+
20
+ /**
21
+ * Used to toggle between step content, including the body and footer. This is also where the nav and its expandability is controlled.
22
+ */
23
+
24
+ export interface WizardToggleProps {
25
+ /** List of steps and/or sub-steps */
26
+ steps: WizardControlStep[];
27
+ /** The currently active WizardStep */
28
+ activeStep: WizardControlStep;
29
+ /** The WizardFooter */
30
+ footer: React.ReactElement;
31
+ /** Custom WizardNav or callback used to create a default WizardNav */
32
+ nav: DefaultWizardNavProps | CustomWizardNavFunction;
33
+ /** Navigate using the step index */
34
+ goToStepByIndex: (index: number) => void;
35
+ /** The button's aria-label */
36
+ 'aria-label'?: string;
37
+ /** Flag to unmount inactive steps instead of hiding. Defaults to true */
38
+ unmountInactiveSteps?: boolean;
39
+ }
40
+
41
+ export const WizardToggle = ({
42
+ steps,
43
+ activeStep,
44
+ footer,
45
+ nav,
46
+ goToStepByIndex,
47
+ unmountInactiveSteps = true,
48
+ 'aria-label': ariaLabel = 'Wizard toggle'
49
+ }: WizardToggleProps) => {
50
+ const [isNavOpen, setIsNavOpen] = React.useState(false);
51
+ const isActiveSubStep = isWizardSubStep(activeStep);
52
+
53
+ const handleKeyClicks = React.useCallback(
54
+ (event: KeyboardEvent): void => {
55
+ if (isNavOpen && event.key === KeyTypes.Escape) {
56
+ setIsNavOpen(!isNavOpen);
57
+ }
58
+ },
59
+ [isNavOpen]
60
+ );
61
+
62
+ // Open/close collapsable nav on keydown event
63
+ React.useEffect(() => {
64
+ const target = typeof document !== 'undefined' ? document.body : null;
65
+ target?.addEventListener('keydown', handleKeyClicks, false);
66
+
67
+ return () => {
68
+ target?.removeEventListener('keydown', handleKeyClicks, false);
69
+ };
70
+ }, [handleKeyClicks]);
71
+
72
+ // Only render the active step when unmountInactiveSteps is true
73
+ const bodyContent = unmountInactiveSteps
74
+ ? activeStep?.component
75
+ : steps.map(step => {
76
+ if (activeStep?.name === step.name) {
77
+ return step.component;
78
+ }
79
+
80
+ return (
81
+ <div key={step.id} style={{ display: 'none' }}>
82
+ {step.component}
83
+ </div>
84
+ );
85
+ });
86
+
87
+ const wizardNav = isCustomWizardNav(nav)
88
+ ? nav(isNavOpen, steps, activeStep, goToStepByIndex)
89
+ : React.useMemo(() => {
90
+ const props = {
91
+ isOpen: isNavOpen,
92
+ 'aria-label': nav?.ariaLabel || 'Wizard nav',
93
+ ...(nav?.ariaLabelledBy && { 'aria-labelledby': nav?.ariaLabelledBy })
94
+ };
95
+
96
+ return (
97
+ <WizardNav {...props}>
98
+ {steps.map((step, index) => {
99
+ const stepIndex = index + 1;
100
+ const stepNavItem = step.navItem && <React.Fragment key={step.id}>{step.navItem}</React.Fragment>;
101
+
102
+ if (isWizardParentStep(step)) {
103
+ let firstSubStepIndex;
104
+ let hasActiveChild = false;
105
+
106
+ const subNavItems = step.subStepIds?.map((subStepId, index) => {
107
+ const subStep = steps.find(step => step.id === subStepId);
108
+ const subStepIndex = steps.indexOf(subStep) + 1;
109
+
110
+ if (index === 0) {
111
+ firstSubStepIndex = subStepIndex;
112
+ }
113
+
114
+ if (activeStep?.id === subStep.id) {
115
+ hasActiveChild = true;
116
+ }
117
+
118
+ return subStep.navItem ? (
119
+ <React.Fragment key={subStep.id}>{subStep.navItem}</React.Fragment>
120
+ ) : (
121
+ <WizardNavItem
122
+ key={subStep.id}
123
+ id={subStep.id}
124
+ content={subStep.name}
125
+ isCurrent={activeStep?.id === subStep.id}
126
+ isDisabled={subStep.isDisabled || (nav?.forceStepVisit && !subStep.visited)}
127
+ step={subStepIndex}
128
+ onNavItemClick={goToStepByIndex}
129
+ />
130
+ );
131
+ });
132
+
133
+ const hasEnabledChildren = React.Children.toArray(subNavItems).some(
134
+ child => React.isValidElement(child) && !child.props.isDisabled
135
+ );
136
+
137
+ return (
138
+ stepNavItem || (
139
+ <WizardNavItem
140
+ key={step.id}
141
+ id={step.id}
142
+ content={step.name}
143
+ isExpandable={nav?.isExpandable}
144
+ isCurrent={hasActiveChild}
145
+ isDisabled={!hasEnabledChildren}
146
+ step={firstSubStepIndex}
147
+ onNavItemClick={goToStepByIndex}
148
+ >
149
+ <WizardNav {...props} returnList>
150
+ {subNavItems}
151
+ </WizardNav>
152
+ </WizardNavItem>
153
+ )
154
+ );
155
+ }
156
+
157
+ if (isWizardBasicStep(step)) {
158
+ return (
159
+ stepNavItem || (
160
+ <WizardNavItem
161
+ key={step.id}
162
+ id={step.id}
163
+ content={step.name}
164
+ isCurrent={activeStep?.id === step.id}
165
+ isDisabled={step.isDisabled || (nav?.forceStepVisit && !step.visited)}
166
+ step={stepIndex}
167
+ onNavItemClick={goToStepByIndex}
168
+ />
169
+ )
170
+ );
171
+ }
172
+ })}
173
+ </WizardNav>
174
+ );
175
+ }, [activeStep?.id, goToStepByIndex, isNavOpen, nav, steps]);
176
+
177
+ return (
178
+ <>
179
+ <button
180
+ onClick={() => setIsNavOpen(prevIsOpen => !prevIsOpen)}
181
+ className={css(styles.wizardToggle, isNavOpen && 'pf-m-expanded')}
182
+ aria-label={ariaLabel}
183
+ aria-expanded={isNavOpen}
184
+ >
185
+ <span className={css(styles.wizardToggleList)}>
186
+ <span className={css(styles.wizardToggleListItem)}>
187
+ {activeStep?.name}
188
+ {isActiveSubStep && <AngleRightIcon className={css(styles.wizardToggleSeparator)} aria-hidden="true" />}
189
+ </span>
190
+ {isActiveSubStep && <span className={css(styles.wizardToggleListItem)}>{activeStep?.name}</span>}
191
+ </span>
192
+
193
+ <span className={css(styles.wizardToggleIcon)}>
194
+ <CaretDownIcon aria-hidden="true" />
195
+ </span>
196
+ </button>
197
+ <div className={css(styles.wizardOuterWrap)}>
198
+ <div className={css(styles.wizardInnerWrap)}>
199
+ {wizardNav}
200
+ {bodyContent}
201
+ </div>
202
+
203
+ {footer}
204
+ </div>
205
+ </>
206
+ );
207
+ };
208
+
209
+ WizardToggle.displayName = 'WizardToggle';
@@ -0,0 +1,298 @@
1
+ import React from 'react';
2
+
3
+ import { render, screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ import { DefaultWizardNavProps, WizardControlStep, DefaultWizardFooterProps } from '../types';
7
+ import { WizardNav, WizardNavItem, Wizard, WizardStep } from '../';
8
+
9
+ describe('Wizard', () => {
10
+ it('renders step when child is of type WizardStep', () => {
11
+ render(
12
+ <Wizard>
13
+ <WizardStep id="test-step" name="Test step">
14
+ Step content
15
+ </WizardStep>
16
+ </Wizard>
17
+ );
18
+
19
+ expect(screen.getByRole('button', { name: 'Test step' })).toBeVisible();
20
+ expect(screen.getByText('Step content')).toBeVisible();
21
+ });
22
+
23
+ it('renders step when child has required props; name, id, children', () => {
24
+ const CustomStep = props => <div {...props} />;
25
+
26
+ render(
27
+ <Wizard>
28
+ <CustomStep id="test-step" name="Test step">
29
+ Custom step content
30
+ </CustomStep>
31
+ </Wizard>
32
+ );
33
+
34
+ expect(screen.getByRole('button', { name: 'Test step' })).toBeVisible();
35
+ expect(screen.getByText('Custom step content')).toBeVisible();
36
+ });
37
+
38
+ it('renders a header when specified', () => {
39
+ render(
40
+ <Wizard header="Some header">
41
+ <WizardStep id="test-step" name="Test step" />
42
+ </Wizard>
43
+ );
44
+
45
+ expect(screen.getByText('Some header')).toBeVisible();
46
+ });
47
+
48
+ it('renders default footer without specifying the footer prop', () => {
49
+ render(
50
+ <Wizard>
51
+ <WizardStep id="test-step" name="Test step" />
52
+ </Wizard>
53
+ );
54
+
55
+ expect(screen.getByRole('button', { name: 'Next' })).toBeVisible();
56
+ expect(screen.getByRole('button', { name: 'Back' })).toBeVisible();
57
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
58
+ });
59
+
60
+ it('renders default footer with custom props', () => {
61
+ const footer: DefaultWizardFooterProps = {
62
+ nextButtonText: <>Proceed with caution</>,
63
+ backButtonText: 'Turn back!',
64
+ cancelButtonText: 'Leave now!'
65
+ };
66
+
67
+ render(
68
+ <Wizard footer={footer}>
69
+ <WizardStep id="test-step" name="Test step" />
70
+ </Wizard>
71
+ );
72
+
73
+ expect(screen.getByRole('button', { name: 'Proceed with caution' })).toBeVisible();
74
+ expect(screen.getByRole('button', { name: 'Turn back!' })).toBeVisible();
75
+ expect(screen.getByRole('button', { name: 'Leave now!' })).toBeVisible();
76
+ });
77
+
78
+ it('renders custom footer', () => {
79
+ render(
80
+ <Wizard footer={<>Some footer</>}>
81
+ <WizardStep id="test-step" name="Test step" />
82
+ </Wizard>
83
+ );
84
+
85
+ expect(screen.getByText('Some footer')).toBeVisible();
86
+ });
87
+
88
+ it('renders default nav without specifying the nav prop', () => {
89
+ render(
90
+ <Wizard>
91
+ <WizardStep id="test-step" name="Test step" />
92
+ </Wizard>
93
+ );
94
+
95
+ expect(screen.getByRole('navigation')).toBeVisible();
96
+ expect(screen.getByRole('button', { name: 'Test step' })).toBeVisible();
97
+ });
98
+
99
+ it('renders default nav with custom props', () => {
100
+ const nav: DefaultWizardNavProps = {
101
+ isExpandable: true,
102
+ ariaLabel: 'Some nav label',
103
+ ariaLabelledBy: 'wizard-id',
104
+ forceStepVisit: true
105
+ };
106
+
107
+ render(
108
+ <Wizard id="wizard-id" nav={nav}>
109
+ <WizardStep id="step-1" name="Test step 1" steps={[<WizardStep id="nested-step" name="Nested step" />]} />
110
+ <WizardStep id="step-2" name="Test step 2" />
111
+ </Wizard>
112
+ );
113
+
114
+ const navElement = screen.getByLabelText('Some nav label');
115
+
116
+ expect(navElement).toBeVisible();
117
+ expect(navElement).toHaveAttribute('aria-labelledby', 'wizard-id');
118
+ expect(screen.getByRole('button', { name: 'Test step 1' }).parentElement).toHaveClass('pf-m-expandable');
119
+ expect(screen.getByRole('button', { name: 'Test step 2' })).toHaveAttribute('disabled');
120
+ });
121
+
122
+ it('renders custom nav', () => {
123
+ const nav = (
124
+ isOpen: boolean,
125
+ steps: WizardControlStep[],
126
+ activeStep: WizardControlStep,
127
+ goToStepByIndex: (index: number) => void
128
+ ) => (
129
+ <WizardNav isOpen={isOpen}>
130
+ {steps.map((step, index) => (
131
+ <WizardNavItem
132
+ key={step.id}
133
+ id={step.id}
134
+ content={step.name}
135
+ isCurrent={activeStep.id === step.id}
136
+ isDisabled={step.isDisabled}
137
+ step={index + 1}
138
+ onNavItemClick={goToStepByIndex}
139
+ />
140
+ ))}
141
+ </WizardNav>
142
+ );
143
+
144
+ render(
145
+ <Wizard nav={nav}>
146
+ <WizardStep id="test-step" name="Test step" />
147
+ </Wizard>
148
+ );
149
+
150
+ expect(screen.getByRole('navigation')).toBeVisible();
151
+ expect(screen.getByRole('button', { name: 'Test step' })).toBeVisible();
152
+ });
153
+
154
+ it('starts at the first step as the active one by default', () => {
155
+ render(
156
+ <Wizard>
157
+ <WizardStep id="step-1" name="Test step 1">
158
+ Step 1 content
159
+ </WizardStep>
160
+ <WizardStep id="step-2" name="Test step 2" />
161
+ </Wizard>
162
+ );
163
+
164
+ expect(screen.getByText('Step 1 content')).toBeVisible();
165
+ expect(screen.getByRole('button', { name: 'Test step 1' })).toHaveClass('pf-m-current');
166
+ });
167
+
168
+ it(`can start at a step that isn't the first by specifying 'startIndex'`, () => {
169
+ render(
170
+ <Wizard startIndex={2}>
171
+ <WizardStep id="step-1" name="Test step 1" />
172
+ <WizardStep id="step-2" name="Test step 2">
173
+ Step 2 content
174
+ </WizardStep>
175
+ </Wizard>
176
+ );
177
+
178
+ expect(screen.getByText('Step 2 content')).toBeVisible();
179
+ expect(screen.getByRole('button', { name: 'Test step 2' })).toHaveClass('pf-m-current');
180
+ });
181
+
182
+ it(`can use custom classNames and spread other props into the wizard's div`, () => {
183
+ render(
184
+ <Wizard className="some-class" data-testid="wizard-id">
185
+ <WizardStep id="test-step" name="Test step" />
186
+ </Wizard>
187
+ );
188
+
189
+ expect(screen.getByTestId('wizard-id')).toHaveClass('some-class');
190
+ });
191
+
192
+ it(`can customize the wizard's height and width`, () => {
193
+ render(
194
+ <Wizard height={500} width={500} data-testid="wizard-id">
195
+ <WizardStep id="test-step" name="Test step" />
196
+ </Wizard>
197
+ );
198
+
199
+ const wizard = screen.getByTestId('wizard-id');
200
+
201
+ expect(wizard).toHaveStyle('height: 500px');
202
+ expect(wizard).toHaveStyle('width: 500px');
203
+ });
204
+
205
+ it('calls onNavByIndex on nav item click', () => {
206
+ const onNavByIndex = jest.fn();
207
+
208
+ render(
209
+ <Wizard onNavByIndex={onNavByIndex}>
210
+ <WizardStep id="step-1" name="Test step 1" />
211
+ <WizardStep id="step-2" name="Test step 2" />
212
+ </Wizard>
213
+ );
214
+
215
+ userEvent.click(screen.getByRole('button', { name: 'Test step 2' }));
216
+ expect(onNavByIndex).toHaveBeenCalled();
217
+ });
218
+
219
+ it('calls onNext and not onSave on next button click when not on the last step', () => {
220
+ const onNext = jest.fn();
221
+ const onSave = jest.fn();
222
+
223
+ render(
224
+ <Wizard onNext={onNext} onSave={onSave}>
225
+ <WizardStep id="step-1" name="Test step 1" />
226
+ <WizardStep id="step-2" name="Test step 2" />
227
+ </Wizard>
228
+ );
229
+
230
+ userEvent.click(screen.getByRole('button', { name: 'Next' }));
231
+
232
+ expect(onNext).toHaveBeenCalled();
233
+ expect(onSave).not.toHaveBeenCalled();
234
+ });
235
+
236
+ it('calls onBack on back button click', () => {
237
+ const onBack = jest.fn();
238
+
239
+ render(
240
+ <Wizard onBack={onBack}>
241
+ <WizardStep id="step-1" name="Test step 1" />
242
+ <WizardStep id="step-2" name="Test step 2" />
243
+ </Wizard>
244
+ );
245
+
246
+ userEvent.click(screen.getByRole('button', { name: 'Test step 2' }));
247
+ userEvent.click(screen.getByRole('button', { name: 'Back' }));
248
+
249
+ expect(onBack).toHaveBeenCalled();
250
+ });
251
+
252
+ it('calls onSave and not onClose on next button click when on the last step', () => {
253
+ const onSave = jest.fn();
254
+ const onClose = jest.fn();
255
+
256
+ render(
257
+ <Wizard onSave={onSave} onClose={onClose}>
258
+ <WizardStep id="step-1" name="Test step 1" />
259
+ <WizardStep id="step-2" name="Test step 2" />
260
+ </Wizard>
261
+ );
262
+
263
+ userEvent.click(screen.getByRole('button', { name: 'Test step 2' }));
264
+ userEvent.click(screen.getByRole('button', { name: 'Next' }));
265
+
266
+ expect(onSave).toHaveBeenCalled();
267
+ expect(onClose).not.toHaveBeenCalled();
268
+ });
269
+
270
+ it('calls onClose when onSave is not specified on next button click when on the last step', () => {
271
+ const onClose = jest.fn();
272
+
273
+ render(
274
+ <Wizard onClose={onClose}>
275
+ <WizardStep id="step-1" name="Test step 1" />
276
+ <WizardStep id="step-2" name="Test step 2" />
277
+ </Wizard>
278
+ );
279
+
280
+ userEvent.click(screen.getByRole('button', { name: 'Test step 2' }));
281
+ userEvent.click(screen.getByRole('button', { name: 'Next' }));
282
+
283
+ expect(onClose).toHaveBeenCalled();
284
+ });
285
+
286
+ it('calls onClose on cancel link click', () => {
287
+ const onClose = jest.fn();
288
+
289
+ render(
290
+ <Wizard onClose={onClose}>
291
+ <WizardStep id="test-step" name="Test step" />
292
+ </Wizard>
293
+ );
294
+
295
+ userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
296
+ expect(onClose).toHaveBeenCalled();
297
+ });
298
+ });