Skip to content

Commit 51d6e6e

Browse files
authored
fix(Wizard): add className and spread props when possible (#10684)
1 parent 7e51bfb commit 51d6e6e

10 files changed

Lines changed: 234 additions & 28 deletions

File tree

packages/react-core/src/components/Wizard/WizardBody.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { getResizeObserver } from '../../helpers/resizeObserver';
1010
* Used as a wrapper for WizardStep content, where the wrapping element is customizable.
1111
*/
1212

13-
export interface WizardBodyProps {
13+
export interface WizardBodyProps extends React.HTMLProps<HTMLElement> {
1414
/** Anything that can be rendered in the Wizard body */
1515
children: React.ReactNode;
16+
/** Additional classes spread to the wizard body */
17+
className?: string;
1618
/** Flag to remove the default body padding */
1719
hasNoPadding?: boolean;
1820
/** Adds an accessible name to the wrapper element when the content overflows and renders
@@ -29,10 +31,12 @@ export interface WizardBodyProps {
2931

3032
export const WizardBody = ({
3133
children,
34+
className,
3235
hasNoPadding = false,
3336
'aria-label': ariaLabel,
3437
'aria-labelledby': ariaLabelledBy,
35-
component = 'div'
38+
component = 'div',
39+
...props
3640
}: WizardBodyProps) => {
3741
const [hasScrollbar, setHasScrollbar] = React.useState(false);
3842
const [previousWidth, setPreviousWidth] = React.useState<number | undefined>(undefined);
@@ -74,7 +78,8 @@ export const WizardBody = ({
7478
{...(shouldFocusContent && { tabIndex: -1 })}
7579
{...(component === 'div' && hasScrollbar && { role: 'region' })}
7680
{...(hasScrollbar && { 'aria-label': defaultAriaLabel, 'aria-labelledby': ariaLabelledBy, tabIndex: 0 })}
77-
className={css(styles.wizardMain)}
81+
className={css(styles.wizardMain, className)}
82+
{...props}
7883
>
7984
<div className={css(styles.wizardMainBody, hasNoPadding && styles.modifiers.noPadding)}>{children}</div>
8085
</WrapperComponent>

packages/react-core/src/components/Wizard/WizardFooter.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
55

66
import { Button, ButtonVariant } from '../Button';
77
import { isCustomWizardFooter, WizardFooterButtonProps, WizardStepType } from './types';
8-
98
/**
109
* Hosts the standard structure of a footer with ties to the active step so that text for buttons can vary from step to step.
1110
*/
1211

13-
export interface WizardFooterProps {
12+
export interface WizardFooterProps extends React.HTMLProps<HTMLElement> {
1413
/** The active step */
1514
activeStep: WizardStepType;
1615
/** Next button callback */
@@ -39,18 +38,23 @@ export interface WizardFooterProps {
3938
backButtonProps?: Omit<WizardFooterButtonProps, 'isDisabled'>;
4039
/** Additional props for the Cancel button. */
4140
cancelButtonProps?: WizardFooterButtonProps;
41+
/** Additional classes spread to the wizard footer */
42+
className?: string;
4243
}
4344

4445
/**
4546
* Applies default wizard footer styling any number of child elements.
4647
*/
4748

48-
interface WizardFooterWrapperProps {
49+
interface WizardFooterWrapperProps extends React.HTMLProps<HTMLElement> {
4950
children: React.ReactNode;
51+
className?: string;
5052
}
5153

52-
export const WizardFooterWrapper = ({ children }: WizardFooterWrapperProps) => (
53-
<footer className={css(styles.wizardFooter)}>{children}</footer>
54+
export const WizardFooterWrapper = ({ children, className, ...props }: WizardFooterWrapperProps) => (
55+
<footer className={css(styles.wizardFooter, className)} {...props}>
56+
{children}
57+
</footer>
5458
);
5559

5660
export const WizardFooter = ({ activeStep, ...internalProps }: WizardFooterProps) => {
@@ -71,9 +75,11 @@ const InternalWizardFooter = ({
7175
cancelButtonText = 'Cancel',
7276
nextButtonProps,
7377
backButtonProps,
74-
cancelButtonProps
78+
cancelButtonProps,
79+
className,
80+
...props
7581
}: Omit<WizardFooterProps, 'activeStep'>) => (
76-
<WizardFooterWrapper>
82+
<WizardFooterWrapper className={className} {...props}>
7783
{!isBackHidden && (
7884
<Button variant={ButtonVariant.secondary} onClick={onBack} isDisabled={isBackDisabled} {...backButtonProps}>
7985
{backButtonText}

packages/react-core/src/components/Wizard/WizardHeader.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { css } from '@patternfly/react-styles';
44
import { Button } from '../Button';
55
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
66

7-
export interface WizardHeaderProps {
7+
export interface WizardHeaderProps extends React.HTMLProps<HTMLDivElement> {
88
/** Callback function called when the X (Close) button is clicked */
99
onClose?: (event: React.MouseEvent<HTMLButtonElement>) => void;
1010
/** Title of the wizard */
@@ -21,6 +21,8 @@ export interface WizardHeaderProps {
2121
titleId?: string;
2222
/** id for the description */
2323
descriptionId?: string;
24+
/** Additional classes spread to the wizard header */
25+
className?: string;
2426
}
2527

2628
export const WizardHeader: React.FunctionComponent<WizardHeaderProps> = ({
@@ -31,9 +33,11 @@ export const WizardHeader: React.FunctionComponent<WizardHeaderProps> = ({
3133
closeButtonAriaLabel,
3234
titleId,
3335
descriptionComponent: Component = 'div',
34-
descriptionId
36+
descriptionId,
37+
className,
38+
...props
3539
}: WizardHeaderProps) => (
36-
<div className={css(styles.wizardHeader)}>
40+
<div className={css(styles.wizardHeader, className)} {...props}>
3741
{!isCloseHidden && (
3842
<div className={css(styles.wizardClose)}>
3943
<Button variant="plain" aria-label={closeButtonAriaLabel} onClick={onClose}>

packages/react-core/src/components/Wizard/WizardNav.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
33
import { css } from '@patternfly/react-styles';
44

5-
export interface WizardNavProps {
5+
export interface WizardNavProps extends Omit<React.HTMLProps<HTMLOListElement>, 'type' | 'ref'> {
66
/** children should be WizardNavItem components */
77
children?: any;
88
/** Aria-label applied to the navigation element */
@@ -13,28 +13,33 @@ export interface WizardNavProps {
1313
isExpanded?: boolean;
1414
/** True to return the inner list without the wrapping navigation element */
1515
isInnerList?: boolean;
16+
/** Additional classes spread to the wizard nav */
17+
className?: string;
1618
}
1719

1820
export const WizardNav: React.FunctionComponent<WizardNavProps> = ({
1921
children,
2022
'aria-label': ariaLabel,
2123
'aria-labelledby': ariaLabelledBy,
2224
isExpanded = false,
23-
isInnerList = false
25+
isInnerList = false,
26+
className,
27+
...props
2428
}: WizardNavProps) => {
2529
if (isInnerList) {
2630
return (
27-
<ol className={css(styles.wizardNavList)} role="list">
31+
<ol className={css(styles.wizardNavList, className)} role="list" {...props}>
2832
{children}
2933
</ol>
3034
);
3135
}
3236

3337
return (
3438
<nav
35-
className={css(styles.wizardNav, isExpanded && styles.modifiers.expanded)}
39+
className={css(styles.wizardNav, isExpanded && styles.modifiers.expanded, className)}
3640
aria-label={ariaLabel}
3741
aria-labelledby={ariaLabelledBy}
42+
{...props}
3843
>
3944
<ol className={css(styles.wizardNavList)} role="list">
4045
{children}

packages/react-core/src/components/Wizard/WizardNavItem.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { WizardNavItemStatus } from './types';
1010
import globalSpacerSm from '@patternfly/react-tokens/dist/esm/global_spacer_sm';
1111
import globalDangerColor100 from '@patternfly/react-tokens/dist/esm/global_danger_color_100';
1212

13-
export interface WizardNavItemProps extends OUIAProps {
13+
export interface WizardNavItemProps
14+
extends Omit<React.HTMLProps<HTMLLIElement>, 'onClick' | 'id' | 'content' | 'type'>,
15+
OUIAProps {
16+
/** Additional classes spread to the wizard nav item */
17+
className?: string;
1418
/** Can nest a WizardNav component for substeps */
1519
children?: React.ReactNode;
1620
/** The content to display in the navigation item */
@@ -55,7 +59,9 @@ export const WizardNavItem = ({
5559
status = 'default',
5660
target,
5761
ouiaId,
58-
ouiaSafe = true
62+
ouiaSafe = true,
63+
className,
64+
...props
5965
}: WizardNavItemProps) => {
6066
const [isExpanded, setIsExpanded] = React.useState(false);
6167
const ouiaProps = useOUIAProps(WizardNavItem.displayName, ouiaId, ouiaSafe);
@@ -74,8 +80,10 @@ export const WizardNavItem = ({
7480
className={css(
7581
styles.wizardNavItem,
7682
isExpandable && styles.modifiers.expandable,
77-
isExpandable && isExpanded && styles.modifiers.expanded
83+
isExpandable && isExpanded && styles.modifiers.expanded,
84+
className
7885
)}
86+
{...props}
7987
>
8088
<NavItemComponent
8189
{...(NavItemComponent === 'a'

packages/react-core/src/components/Wizard/__tests__/WizardBody.test.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,20 @@ import { render, screen } from '@testing-library/react';
33
import { WizardBody } from '../WizardBody';
44

55
test('renders children without additional props', () => {
6-
const { container } = render(<WizardBody>content</WizardBody>);
6+
render(<WizardBody data-testid="test-id">content</WizardBody>);
77

8-
expect(container).toHaveTextContent('content');
9-
expect(container).not.toHaveAttribute('aria-label');
10-
expect(container).not.toHaveAttribute('aria-labelledby');
8+
expect(screen.getByTestId('test-id')).toHaveTextContent('content');
9+
expect(screen.getByTestId('test-id')).not.toHaveAttribute('aria-label');
10+
expect(screen.getByTestId('test-id')).not.toHaveAttribute('aria-labelledby');
11+
});
12+
13+
test(`Renders with additional classes when className is passed`, () => {
14+
render(
15+
<WizardBody className="custom-class" data-testid="test-id">
16+
Test
17+
</WizardBody>
18+
);
19+
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
1120
});
1221

1322
test('has no padding className when hasNoPadding is not specified', () => {
@@ -21,11 +30,19 @@ test('has padding className when hasNoPadding is specified', () => {
2130
});
2231

2332
test('wrapper element is of type div when component is not specified', () => {
24-
const { container } = render(<WizardBody aria-label="Wizard body">content</WizardBody>);
25-
expect(container.firstElementChild?.tagName).toEqual('DIV');
33+
render(
34+
<WizardBody data-testid="test-id" aria-label="Wizard body">
35+
content
36+
</WizardBody>
37+
);
38+
expect(screen.getByTestId('test-id').tagName).toEqual('DIV');
2639
});
2740

2841
test('renders with custom component', () => {
29-
const { container } = render(<WizardBody component="main">content</WizardBody>);
30-
expect(container.firstElementChild?.tagName).toEqual('MAIN');
42+
render(
43+
<WizardBody component="main" data-testid="test-id">
44+
content
45+
</WizardBody>
46+
);
47+
expect(screen.getByTestId('test-id').tagName).toEqual('MAIN');
3148
});

packages/react-core/src/components/Wizard/__tests__/WizardFooter.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ test('has button names of "Next", "Back", and "Cancel" by default', () => {
2020
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
2121
});
2222

23+
test(`Renders with additional classes when className is passed`, () => {
24+
render(
25+
<WizardFooter {...defaultProps} className="custom-class" data-testid="test-id">
26+
Test
27+
</WizardFooter>
28+
);
29+
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
30+
});
31+
2332
test('calls onNext when the next button is clicked', async () => {
2433
const onNext = jest.fn();
2534
const user = userEvent.setup();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
3+
import { render, screen } from '@testing-library/react';
4+
import { WizardHeader } from '../WizardHeader';
5+
import userEvent from '@testing-library/user-event';
6+
7+
// close button related tests
8+
test(`Renders close button by default`, () => {
9+
render(<WizardHeader data-testid="header-component" />);
10+
expect(screen.getByRole('button')).toBeInTheDocument();
11+
});
12+
13+
test(`Close button is hidden when isCloseHidden is passed`, () => {
14+
render(<WizardHeader isCloseHidden />);
15+
expect(screen.queryByRole('button')).toBeNull();
16+
});
17+
18+
test(`Close button renders passed aria label`, () => {
19+
render(<WizardHeader closeButtonAriaLabel="test aria label" />);
20+
expect(screen.getByRole('button', { name: 'test aria label' })).toBeInTheDocument();
21+
});
22+
23+
test(`Callback function fires when onClose passed`, async () => {
24+
const onClose = jest.fn();
25+
const user = userEvent.setup();
26+
render(<WizardHeader onClose={onClose} />);
27+
await user.click(screen.getByRole('button'));
28+
expect(onClose).toHaveBeenCalled();
29+
});
30+
31+
// description related tests
32+
test(`Renders a description when passed description`, () => {
33+
render(<WizardHeader description="test description" />);
34+
expect(screen.getByText('test description')).toBeVisible();
35+
});
36+
37+
test(`Renders the id passed via descriptionId`, () => {
38+
render(<WizardHeader description="test description" descriptionId="test-id" />);
39+
expect(screen.queryByText('test description')).toHaveAttribute('id', 'test-id');
40+
});
41+
42+
// other prop tests
43+
test(`Renders title when prop is passed`, () => {
44+
render(<WizardHeader title="Test" />);
45+
expect(screen.getByText('Test')).toBeVisible();
46+
});
47+
48+
test(`Renders with additional classes when className is passed`, () => {
49+
render(<WizardHeader className="custom-class" data-testid="header-component" />);
50+
expect(screen.getByTestId('header-component')).toHaveClass('custom-class');
51+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react';
2+
3+
import { render, screen } from '@testing-library/react';
4+
import { WizardNav } from '../WizardNav';
5+
import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
6+
7+
test(`Renders with additional classes when className is passed`, () => {
8+
render(
9+
<WizardNav className="custom-class" data-testid="test-id">
10+
Test
11+
</WizardNav>
12+
);
13+
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
14+
});
15+
16+
test(`Renders with accessible aria-label when passed`, () => {
17+
render(
18+
<WizardNav aria-label="test aria label" data-testid="test-id">
19+
Test
20+
</WizardNav>
21+
);
22+
expect(screen.getByTestId('test-id')).toHaveAccessibleName('test aria label');
23+
});
24+
25+
test(`Renders with accessible aria-labelledby when passed`, () => {
26+
render(
27+
<WizardNav aria-labelledby="test-labelledby" data-testid="test-id">
28+
Test
29+
</WizardNav>
30+
);
31+
expect(screen.getByTestId('test-id')).toHaveAttribute('aria-labelledby');
32+
});
33+
34+
test(`Renders with expanded styles when prop passed`, () => {
35+
render(
36+
<WizardNav isExpanded data-testid="test-id">
37+
Test
38+
</WizardNav>
39+
);
40+
expect(screen.getByTestId('test-id')).toHaveClass(styles.modifiers.expanded);
41+
});
42+
43+
test(`Renders with expanded styles when prop passed`, () => {
44+
render(<WizardNav isInnerList>Test</WizardNav>);
45+
expect(screen.getByRole('list')).toBeInTheDocument();
46+
});

0 commit comments

Comments
 (0)