Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(EmptyState): update EmptyState with updates for penta #9947

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ import {
ButtonVariant,
EmptyState,
EmptyStateBody,
EmptyStateIcon,
EmptyStateActions,
EmptyStateVariant,
EmptyStateFooter,
getResizeObserver,
Popover,
PopoverProps,
TooltipPosition,
EmptyStateHeader
TooltipPosition
} from '@patternfly/react-core';
import MonacoEditor, { ChangeHandler, EditorDidMount } from 'react-monaco-editor';
import { editor } from 'monaco-editor/esm/vs/editor/editor.api';
Expand Down Expand Up @@ -520,12 +518,7 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
const emptyState =
providedEmptyState ||
(isUploadEnabled ? (
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader
titleText={emptyStateTitle}
icon={<EmptyStateIcon icon={CodeIcon} />}
headingLevel="h4"
/>
<EmptyState variant={EmptyStateVariant.sm} titleText={emptyStateTitle} icon={CodeIcon} headingLevel="h4">
<EmptyStateBody>{emptyStateBody}</EmptyStateBody>
{!isReadOnly && (
<EmptyStateFooter>
Expand All @@ -543,12 +536,7 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
)}
</EmptyState>
) : (
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader
titleText={emptyStateTitle}
icon={<EmptyStateIcon icon={CodeIcon} />}
headingLevel="h4"
/>
<EmptyState variant={EmptyStateVariant.sm} titleText={emptyStateTitle} icon={CodeIcon} headingLevel="h4">
{!isReadOnly && (
<EmptyStateFooter>
<EmptyStateActions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ import {
SearchInput,
EmptyState,
EmptyStateVariant,
EmptyStateHeader,
EmptyStateFooter,
EmptyStateBody,
EmptyStateIcon,
EmptyStateActions
} from '@patternfly/react-core';
import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon';
Expand Down Expand Up @@ -139,8 +137,7 @@ export const DualListSelectorComposable: React.FunctionComponent = () => {
};

const buildEmptyState = (isAvailable: boolean) => (
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader headingLevel="h4" titleText="No results found" icon={<EmptyStateIcon icon={SearchIcon} />} />
<EmptyState headingLevel="h4" titleText="No results found" icon={SearchIcon} variant={EmptyStateVariant.sm}>
<EmptyStateBody>No results match the filter criteria. Clear all filters and try again.</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import {
Button,
EmptyState,
EmptyStateVariant,
EmptyStateHeader,
EmptyStateFooter,
EmptyStateBody,
EmptyStateActions,
EmptyStateIcon
EmptyStateActions
} from '@patternfly/react-core';
import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon';
import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon';
Expand Down Expand Up @@ -258,12 +256,7 @@ export const DualListSelectorComposableTree: React.FunctionComponent<ExampleProp
listMinHeight="300px"
>
{filterApplied && options.length === 0 && (
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader
headingLevel="h4"
titleText="No results found"
icon={<EmptyStateIcon icon={SearchIcon} />}
/>
<EmptyState headingLevel="h4" titleText="No results found" icon={SearchIcon} variant={EmptyStateVariant.sm}>
<EmptyStateBody>No results match the filter criteria. Clear all filters and try again.</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
Expand Down
74 changes: 57 additions & 17 deletions packages/react-core/src/components/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/EmptyState/empty-state';
import { EmptyStateHeader } from './EmptyStateHeader';
import { statusIcons } from '../../helpers';

export enum EmptyStateVariant {
'xs' = 'xs',
Expand All @@ -10,37 +12,75 @@ export enum EmptyStateVariant {
full = 'full'
}

export enum EmptyStateStatus {
danger = 'danger',
warning = 'warning',
success = 'success',
info = 'info',
custom = 'custom'
}

export interface EmptyStateProps extends React.HTMLProps<HTMLDivElement> {
/** Additional classes added to the empty state */
className?: string;
/** Content rendered inside the empty state */
children: React.ReactNode;
children?: React.ReactNode;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

children changed to optional because titleText has been lifted and changed to required.

/** Modifies empty state max-width and sizes of icon, title and body */
variant?: 'xs' | 'sm' | 'lg' | 'xl' | 'full';
/** Cause component to consume the available height of its container */
isFullHeight?: boolean;
/** Status of the empty state, will set a default status icon and color. Icon can be overwritten using the icon prop */
status?: 'danger' | 'warning' | 'success' | 'info' | 'custom';
/** Additional class names to apply to the empty state header */
headerClassName?: string;
/** Additional classes added to the title inside empty state header */
titleClassName?: string;
/** Text of the title inside empty state header, will be wrapped in headingLevel */
titleText: React.ReactNode;
/** Empty state icon element to be rendered. Can also be a spinner component */
icon?: React.ComponentType<any>;
/** The heading level to use, default is h1 */
headingLevel?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
}

export const EmptyState: React.FunctionComponent<EmptyStateProps> = ({
children,
className,
variant = EmptyStateVariant.full,
isFullHeight,
status,
icon: customIcon,
titleText,
titleClassName,
headerClassName,
headingLevel,
...props
}: EmptyStateProps) => (
<div
className={css(
styles.emptyState,
variant === 'xs' && styles.modifiers.xs,
variant === 'sm' && styles.modifiers.sm,
variant === 'lg' && styles.modifiers.lg,
variant === 'xl' && styles.modifiers.xl,
isFullHeight && styles.modifiers.fullHeight,
className
)}
{...props}
>
<div className={css(styles.emptyStateContent)}>{children}</div>
</div>
);
}: EmptyStateProps) => {
const statusIcon = status && statusIcons[status];
const icon = customIcon || statusIcon;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One decision point: if we have the status prop set a default icon should the icon prop override it, or the other way around?

I think as a consumer I would prefer the former so that I can have the status apply custom colors to my passed icon, so that's what I went with, but I'm not dead set.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I only have one hesitation with that, but it's something that can be covered in a11y docs (passing an icon that doesn't visually represent the status)


return (
<div
className={css(
styles.emptyState,
variant !== 'full' && styles.modifiers[variant],
isFullHeight && styles.modifiers.fullHeight,
status && styles.modifiers[status],
className
)}
{...props}
>
<div className={css(styles.emptyStateContent)}>
<EmptyStateHeader
icon={icon}
titleText={titleText}
titleClassName={titleClassName}
className={headerClassName}
headingLevel={headingLevel}
/>
{children}
</div>
</div>
);
};
EmptyState.displayName = 'EmptyState';
38 changes: 20 additions & 18 deletions packages/react-core/src/components/EmptyState/EmptyStateHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/EmptyState/empty-state';
import { EmptyStateIconProps } from './EmptyStateIcon';
import { EmptyStateIcon } from './EmptyStateIcon';

export enum EmptyStateHeadingLevel {
h1 = 'h1',
h2 = 'h2',
h3 = 'h3',
h4 = 'h4',
h5 = 'h5',
h6 = 'h6'
}

export interface EmptyStateHeaderProps extends React.HTMLProps<HTMLDivElement> {
/** Content rendered inside the empty state header, either in addition to or instead of the titleText prop */
children?: React.ReactNode;
/** Additional classes added to the empty state header */
className?: string;
/** Additional classes added to the title inside empty state header */
titleClassName?: string;
/** Text of the title inside empty state header, will be wrapped in headingLevel */
titleText?: React.ReactNode;
/** Empty state icon element to be rendered */
icon?: React.ReactElement<EmptyStateIconProps>;
titleText: React.ReactNode;
/** Empty state icon element to be rendered. Can also be a spinner component */
icon?: React.ComponentType<any>;
/** The heading level to use, default is h1 */
headingLevel?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
}

export const EmptyStateHeader: React.FunctionComponent<EmptyStateHeaderProps> = ({
children,
className,
titleClassName,
titleText,
headingLevel: HeadingLevel = 'h1',
icon,
headingLevel: HeadingLevel = EmptyStateHeadingLevel.h1,
icon: Icon,
...props
}: EmptyStateHeaderProps) => (
<div className={css(`${styles.emptyState}__header`, className)} {...props}>
{icon}
{(titleText || children) && (
<div className={css(`${styles.emptyState}__title`)}>
{titleText && (
<HeadingLevel className={css(styles.emptyStateTitleText, titleClassName)}>{titleText}</HeadingLevel>
)}
{children}
</div>
)}
{Icon && <EmptyStateIcon icon={Icon} />}
<div className={css(`${styles.emptyState}__title`)}>
<HeadingLevel className={css(styles.emptyStateTitleText, titleClassName)}>{titleText}</HeadingLevel>
</div>
</div>
);

EmptyStateHeader.displayName = 'EmptyStateHeader';
16 changes: 3 additions & 13 deletions packages/react-core/src/components/EmptyState/EmptyStateIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@ import * as React from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/EmptyState/empty-state';
import { Spinner } from '../Spinner';
import cssIconColor from '@patternfly/react-tokens/dist/esm/c_empty_state__icon_Color';

export interface IconProps extends Omit<React.HTMLProps<SVGElement>, 'size'> {
/** Changes the color of the icon. */
color?: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Color prop removed as the status prop will allow color customization in a themed way.

If they really need to consumers can still apply colors directly to the icon they pass in.

}

export interface EmptyStateIconProps extends IconProps {
export interface EmptyStateIconProps {
/** Additional classes added to the empty state icon */
className?: string;
/** Icon component to be rendered. Can also be a spinner component */
Expand All @@ -21,15 +14,12 @@ const isSpinner = (icon: React.ReactElement<any>) => icon.type === Spinner;
export const EmptyStateIcon: React.FunctionComponent<EmptyStateIconProps> = ({
className,
icon: IconComponent,
color,
...props
}: EmptyStateIconProps) => {
const iconIsSpinner = isSpinner(<IconComponent />);

return (
<div
className={css(styles.emptyStateIcon)}
{...(color && !iconIsSpinner && { style: { [cssIconColor.name]: color } as React.CSSProperties })}
>
<div className={css(styles.emptyStateIcon)}>
<IconComponent className={className} aria-hidden={!iconIsSpinner} {...props} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import { EmptyStateActions } from '../EmptyStateActions';
import { Button } from '../../Button';
import { EmptyStateHeader } from '../EmptyStateHeader';
import { EmptyStateFooter } from '../EmptyStateFooter';
import { EmptyStateIcon } from '../../../../dist/esm';
import styles from '@patternfly/react-styles/css/components/EmptyState/empty-state';

describe('EmptyState', () => {
test('Main', () => {
const { asFragment } = render(
<EmptyState>
<EmptyStateHeader titleText="HTTP Proxies" />
<EmptyState titleText="HTTP Proxies">
<EmptyStateBody>
Defining HTTP Proxies that exist on your network allows you to perform various actions through those proxies.
</EmptyStateBody>
Expand All @@ -38,27 +36,21 @@ describe('EmptyState', () => {

test('Main variant large', () => {
const { asFragment } = render(
<EmptyState variant={EmptyStateVariant.lg}>
<EmptyStateHeader titleText="EmptyState large" />
</EmptyState>
<EmptyState titleText="EmptyState large" variant={EmptyStateVariant.lg}></EmptyState>
);
expect(asFragment()).toMatchSnapshot();
});

test('Main variant small', () => {
const { asFragment } = render(
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader titleText="EmptyState small" />
</EmptyState>
<EmptyState titleText="EmptyState small" variant={EmptyStateVariant.sm}></EmptyState>
);
expect(asFragment()).toMatchSnapshot();
});

test('Main variant xs', () => {
const { asFragment } = render(
<EmptyState variant={EmptyStateVariant.xs}>
<EmptyStateHeader titleText="EmptyState extra small" />
</EmptyState>
<EmptyState titleText="EmptyState extra small" variant={EmptyStateVariant.xs}></EmptyState>
);
expect(asFragment()).toMatchSnapshot();
});
Expand All @@ -75,15 +67,13 @@ describe('EmptyState', () => {

test('Full height', () => {
const { asFragment } = render(
<EmptyState isFullHeight variant={EmptyStateVariant.lg}>
<EmptyStateHeader titleText="EmptyState large" />
</EmptyState>
<EmptyState titleText="EmptyState large" isFullHeight variant={EmptyStateVariant.lg}></EmptyState>
);
expect(asFragment()).toMatchSnapshot();
});

test('Header with icon', () => {
const { asFragment } = render(<EmptyStateHeader icon={<EmptyStateIcon icon={AddressBookIcon} />} />);
const { asFragment } = render(<EmptyStateHeader titleText="Empty state" icon={AddressBookIcon} />);
expect(asFragment()).toMatchSnapshot();
});

Expand All @@ -102,11 +92,6 @@ describe('EmptyState', () => {
expect(screen.getByRole('heading', { level: 3, name: 'Empty state' })).toHaveClass(styles.emptyStateTitleText);
});

test('Headers render children', () => {
render(<EmptyStateHeader>Title text</EmptyStateHeader>);
expect(screen.getByText('Title text')).toBeVisible();
});

test('Footer', () => {
render(<EmptyStateFooter className="custom-empty-state-footer" data-testid="actions-test-id" />);
expect(screen.getByTestId('actions-test-id')).toHaveClass('custom-empty-state-footer');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import { Spinner } from '../../../Spinner/Spinner';
it('EmptyStateIcon should match snapshot (auto-generated)', () => {
const { asFragment } = render(
<EmptyStateIcon
color={'string'}
title={'string'}
className={"''"}
icon={UserIcon}
/>
Expand All @@ -23,8 +21,6 @@ it('EmptyStateIcon should match snapshot (auto-generated)', () => {
it('EmptyStateIcon should match snapshot for variant container', () => {
const { asFragment } = render(
<EmptyStateIcon
color={'string'}
title={'string'}
className={"''"}
icon={Spinner}
/>
Expand Down
Loading
Loading