Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(Wizard): add className props and spread them when possible #10670

Merged
merged 3 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/react-core/src/components/Wizard/WizardBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import { getResizeObserver } from '../../helpers/resizeObserver';
* Used as a wrapper for WizardStep content, where the wrapping element is customizable.
*/

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

export const WizardBody = ({
children,
className,
hasNoPadding = false,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
component = 'div'
component = 'div',
...props
}: WizardBodyProps) => {
const [hasScrollbar, setHasScrollbar] = React.useState(false);
const [previousWidth, setPreviousWidth] = React.useState<number | undefined>(undefined);
Expand Down Expand Up @@ -74,7 +78,8 @@ export const WizardBody = ({
{...(shouldFocusContent && { tabIndex: -1 })}
{...(component === 'div' && hasScrollbar && { role: 'region' })}
{...(hasScrollbar && { 'aria-label': defaultAriaLabel, 'aria-labelledby': ariaLabelledBy, tabIndex: 0 })}
className={css(styles.wizardMain)}
className={css(styles.wizardMain, className)}
{...props}
>
<div className={css(styles.wizardMainBody, hasNoPadding && styles.modifiers.noPadding)}>{children}</div>
</WrapperComponent>
Expand Down
19 changes: 13 additions & 6 deletions packages/react-core/src/components/Wizard/WizardFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ActionList, ActionListGroup, ActionListItem } from '../ActionList';
* Hosts the standard structure of a footer with ties to the active step so that text for buttons can vary from step to step.
*/

export interface WizardFooterProps {
export interface WizardFooterProps extends React.HTMLProps<HTMLElement> {
/** The active step */
activeStep: WizardStepType;
/** Next button callback */
Expand Down Expand Up @@ -39,18 +39,23 @@ export interface WizardFooterProps {
backButtonProps?: Omit<WizardFooterButtonProps, 'isDisabled'>;
/** Additional props for the Cancel button. */
cancelButtonProps?: WizardFooterButtonProps;
/** Additional classes spread to the wizard footer */
className?: string;
}

/**
* Applies default wizard footer styling any number of child elements.
*/

interface WizardFooterWrapperProps {
interface WizardFooterWrapperProps extends React.HTMLProps<HTMLElement> {
children: React.ReactNode;
className?: string;
}

export const WizardFooterWrapper = ({ children }: WizardFooterWrapperProps) => (
<footer className={css(styles.wizardFooter)}>{children}</footer>
export const WizardFooterWrapper = ({ children, className, ...props }: WizardFooterWrapperProps) => (
<footer className={css(styles.wizardFooter, className)} {...props}>
{children}
</footer>
);

export const WizardFooter = ({ activeStep, ...internalProps }: WizardFooterProps) => {
Expand All @@ -71,9 +76,11 @@ const InternalWizardFooter = ({
cancelButtonText = 'Cancel',
nextButtonProps,
backButtonProps,
cancelButtonProps
cancelButtonProps,
className,
...props
}: Omit<WizardFooterProps, 'activeStep'>) => (
<WizardFooterWrapper>
<WizardFooterWrapper className={className} {...props}>
<ActionList>
<ActionListGroup>
{!isBackHidden && (
Expand Down
10 changes: 7 additions & 3 deletions packages/react-core/src/components/Wizard/WizardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { css } from '@patternfly/react-styles';
import { Button } from '../Button';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';

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

export const WizardHeader: React.FunctionComponent<WizardHeaderProps> = ({
Expand All @@ -31,9 +33,11 @@ export const WizardHeader: React.FunctionComponent<WizardHeaderProps> = ({
closeButtonAriaLabel,
titleId,
descriptionComponent: Component = 'div',
descriptionId
descriptionId,
className,
...props
}: WizardHeaderProps) => (
<div className={css(styles.wizardHeader)}>
<div className={css(styles.wizardHeader, className)} {...props}>
{!isCloseHidden && (
<div className={css(styles.wizardClose)}>
<Button variant="plain" aria-label={closeButtonAriaLabel} onClick={onClose}>
Expand Down
13 changes: 9 additions & 4 deletions packages/react-core/src/components/Wizard/WizardNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
import { css } from '@patternfly/react-styles';

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

export const WizardNav: React.FunctionComponent<WizardNavProps> = ({
children,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
isExpanded = false,
isInnerList = false
isInnerList = false,
className,
...props
}: WizardNavProps) => {
if (isInnerList) {
return (
<ol className={css(styles.wizardNavList)} role="list">
<ol className={css(styles.wizardNavList, className)} role="list" {...props}>
{children}
</ol>
);
}

return (
<nav
className={css(styles.wizardNav, isExpanded && styles.modifiers.expanded)}
className={css(styles.wizardNav, isExpanded && styles.modifiers.expanded, className)}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
{...props}
>
<ol className={css(styles.wizardNavList)} role="list">
{children}
Expand Down
16 changes: 12 additions & 4 deletions packages/react-core/src/components/Wizard/WizardNavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclam
import { OUIAProps, useOUIAProps } from '../../helpers';
import { WizardNavItemStatus } from './types';

export interface WizardNavItemProps extends OUIAProps {
export interface WizardNavItemProps
extends Omit<React.HTMLProps<HTMLLIElement>, 'onClick' | 'id' | 'content' | 'type'>,
OUIAProps {
/** Additional classes spread to the wizard nav item */
className?: string;
/** Can nest a WizardNav component for substeps */
children?: React.ReactNode;
/** The content to display in the navigation item */
Expand Down Expand Up @@ -53,7 +57,9 @@ export const WizardNavItem = ({
status = 'default',
target,
ouiaId,
ouiaSafe = true
ouiaSafe = true,
className,
...props
}: WizardNavItemProps) => {
const [isExpanded, setIsExpanded] = React.useState(false);
const ouiaProps = useOUIAProps(WizardNavItem.displayName, ouiaId, ouiaSafe);
Expand All @@ -72,8 +78,10 @@ export const WizardNavItem = ({
className={css(
styles.wizardNavItem,
isExpandable && styles.modifiers.expandable,
isExpandable && isExpanded && styles.modifiers.expanded
isExpandable && isExpanded && styles.modifiers.expanded,
className
)}
{...props}
>
<NavItemComponent
{...(NavItemComponent === 'a'
Expand All @@ -97,7 +105,7 @@ export const WizardNavItem = ({
>
{status === WizardNavItemStatus.Error && (
<>
<span className="pf-v5-screen-reader">, {status}</span>
<span className="pf-v6-screen-reader">, {status}</span>
<span className={css(styles.wizardNavLinkStatusIcon)}>
<ExclamationCircleIcon />
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ import { render, screen } from '@testing-library/react';
import { WizardBody } from '../WizardBody';

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

expect(container).toHaveTextContent('content');
expect(container).not.toHaveAttribute('aria-label');
expect(container).not.toHaveAttribute('aria-labelledby');
expect(screen.getByTestId('test-id')).toHaveTextContent('content');
expect(screen.getByTestId('test-id')).not.toHaveAttribute('aria-label');
expect(screen.getByTestId('test-id')).not.toHaveAttribute('aria-labelledby');
});

test(`Renders with additional classes when className is passed`, () => {
render(
<WizardBody className="custom-class" data-testid="test-id">
Test
</WizardBody>
);
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
});

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

test('wrapper element is of type div when component is not specified', () => {
const { container } = render(<WizardBody aria-label="Wizard body">content</WizardBody>);
expect(container.firstElementChild?.tagName).toEqual('DIV');
render(
<WizardBody data-testid="test-id" aria-label="Wizard body">
content
</WizardBody>
);
expect(screen.getByTestId('test-id').tagName).toEqual('DIV');
});

test('renders with custom component', () => {
const { container } = render(<WizardBody component="main">content</WizardBody>);
expect(container.firstElementChild?.tagName).toEqual('MAIN');
render(
<WizardBody component="main" data-testid="test-id">
content
</WizardBody>
);
expect(screen.getByTestId('test-id').tagName).toEqual('MAIN');
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ test('has button names of "Next", "Back", and "Cancel" by default', () => {
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
});

test(`Renders with additional classes when className is passed`, () => {
render(
<WizardFooter {...defaultProps} className="custom-class" data-testid="test-id">
Test
</WizardFooter>
);
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
});

test('calls onNext when the next button is clicked', async () => {
const onNext = jest.fn();
const user = userEvent.setup();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';

import { render, screen } from '@testing-library/react';
import { WizardHeader } from '../WizardHeader';
import userEvent from '@testing-library/user-event';

// close button related tests
test(`Renders close button by default`, () => {
render(<WizardHeader data-testid="header-component" />);
expect(screen.getByRole('button')).toBeInTheDocument();
});

test(`Close button is hidden when isCloseHidden is passed`, () => {
render(<WizardHeader isCloseHidden />);
expect(screen.queryByRole('button')).toBeNull();
});

test(`Close button renders passed aria label`, () => {
render(<WizardHeader closeButtonAriaLabel="test aria label" />);
expect(screen.getByRole('button', { name: 'test aria label' })).toBeInTheDocument();
});

test(`Callback function fires when onClose passed`, async () => {
const onClose = jest.fn();
const user = userEvent.setup();
render(<WizardHeader onClose={onClose} />);
await user.click(screen.getByRole('button'));
expect(onClose).toHaveBeenCalled();
});

// description related tests
test(`Renders a description when passed description`, () => {
render(<WizardHeader description="test description" />);
expect(screen.getByText('test description')).toBeVisible();
});

test(`Renders the id passed via descriptionId`, () => {
render(<WizardHeader description="test description" descriptionId="test-id" />);
expect(screen.queryByText('test description')).toHaveAttribute('id', 'test-id');
});

// other prop tests
test(`Renders title when prop is passed`, () => {
render(<WizardHeader title="Test" />);
expect(screen.getByText('Test')).toBeVisible();
});

test(`Renders with additional classes when className is passed`, () => {
render(<WizardHeader className="custom-class" data-testid="header-component" />);
expect(screen.getByTestId('header-component')).toHaveClass('custom-class');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';

import { render, screen } from '@testing-library/react';
import { WizardNav } from '../WizardNav';
import styles from '@patternfly/react-styles/css/components/Wizard/wizard';

test(`Renders with additional classes when className is passed`, () => {
render(
<WizardNav className="custom-class" data-testid="test-id">
Test
</WizardNav>
);
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
});

test(`Renders with accessible aria-label when passed`, () => {
render(
<WizardNav aria-label="test aria label" data-testid="test-id">
Test
</WizardNav>
);
expect(screen.getByTestId('test-id')).toHaveAccessibleName('test aria label');
});

test(`Renders with accessible aria-labelledby when passed`, () => {
render(
<WizardNav aria-labelledby="test-labelledby" data-testid="test-id">
Test
</WizardNav>
);
expect(screen.getByTestId('test-id')).toHaveAttribute('aria-labelledby');
});

test(`Renders with expanded styles when prop passed`, () => {
render(
<WizardNav isExpanded data-testid="test-id">
Test
</WizardNav>
);
expect(screen.getByTestId('test-id')).toHaveClass(styles.modifiers.expanded);
});

test(`Renders with expanded styles when prop passed`, () => {
render(<WizardNav isInnerList>Test</WizardNav>);
expect(screen.getByRole('list')).toBeInTheDocument();
});
Loading
Loading