Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
59 changes: 59 additions & 0 deletions cypress/component/Status.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react';
import { ExclamationTriangleIcon, CheckCircleIcon, BanIcon } from '@patternfly/react-icons';
import { Button, ButtonVariant, ButtonSize } from '@patternfly/react-core';
import { Status, StatusVariant } from '../../packages/module/dist/dynamic/Status';

describe('Status', () => {

it('should render with label and icon', () => {
cy.mount(<Status label='Warning' icon={<ExclamationTriangleIcon color='var(--pf-v5-global--warning-color--100)'/>} />);
cy.get(`[data-ouia-component-id="Status-label"]`).should('be.visible');
cy.get(`[data-ouia-component-id="Status-icon"]`).should('be.visible');
});

it('should render with iconOnly', () => {
cy.mount(<Status iconOnly label='Warning' icon={<ExclamationTriangleIcon color='var(--pf-v5-global--warning-color--100)'/>} />);
cy.get(`[data-ouia-component-id="Status-label"]`).should('not.exist');
cy.get(`[data-ouia-component-id="Status-icon"]`).should('be.visible');
});

it('should render link variant and handle click', () => {
const handleClick = cy.stub().as('handleClick');
cy.mount(<Status variant={StatusVariant.link} label='Ready' onClick={handleClick} icon={<CheckCircleIcon color='var(--pf-v5-global--success-color--100)'/>} />);
cy.get(`[data-ouia-component-id="Status-label"]`).should('be.visible');
cy.get(`[data-ouia-component-id="Status-icon"]`).should('be.visible');
cy.get(`[data-ouia-component-id="Status-link-icon"]`).click();
cy.get('@handleClick').should('have.been.calledOnce');

});

it('should render popover variant and handle open/close', () => {
cy.mount(
<Status
variant={StatusVariant.popover}
label='Not Ready'
icon={<BanIcon color='var(--pf-v5-global--danger-color--100)'/>}
popoverProps={{
bodyContent: 'Example state description',
footerContent: <Button size={ButtonSize.sm} variant={ButtonVariant.link} isInline>Action</Button>
}}
/>
);
cy.get(`[data-ouia-component-id="Status-label"]`).should('be.visible');
cy.get(`[data-ouia-component-id="Status-icon"]`).should('be.visible');

cy.get(`[data-ouia-component-id="Status-popover-icon"]`).click();
cy.get('[role="dialog"]').should('be.visible');
cy.get('body').click(0, 0);
cy.get('[role="dialog"]').should('not.exist');

});

it('should render with description', () => {
cy.mount(<Status label='Warning' description='1 issue found' icon={<ExclamationTriangleIcon color='var(--pf-v5-global--warning-color--100)'/>} />);
cy.get(`[data-ouia-component-id="Status-label"]`).should('be.visible');
cy.get(`[data-ouia-component-id="Status-icon"]`).should('be.visible');
cy.get(`[data-ouia-component-id="Status-description"]`).should('be.visible');
});

});
2 changes: 1 addition & 1 deletion cypress/e2e/CloseButton.spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ describe('Test the close button', () => {
it('passes', () => {
cy.visit('http://localhost:8006/extensions/component-groups/about-component-groups', { onBeforeLoad: (win) => {cy.stub(win.console, 'log').as('consoleLog');}, });
cy.wait(1000);
cy.get('a[href="/extensions/component-groups/closebutton"]').click();
cy.get('a[href="/extensions/component-groups/close-button"]').click();
cy.wait(1000);

cy.get('[data-test-id="close-button-example"]').click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ section: extensions
subsection: Component groups
# Sidenav secondary level section
# should be the same for all markdown files
id: CloseButton
id: Close button
# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
source: react
# If you use typescript, the name of the interface to display props for
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import { Status } from '@patternfly/react-component-groups/dist/dynamic/Status';
import { ExclamationTriangleIcon } from '@patternfly/react-icons/';

export const BasicExample: React.FunctionComponent = () => (
<Status iconOnly label='Warning' icon={<ExclamationTriangleIcon color='var(--pf-v5-global--warning-color--100)'/>}/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import { Status, StatusVariant } from '@patternfly/react-component-groups/dist/dynamic/Status';
import { CheckCircleIcon } from '@patternfly/react-icons/';

export const BasicExample: React.FunctionComponent = () => (
// eslint-disable-next-line no-console
<Status variant={StatusVariant.link} label='Ready' onClick={() => console.log('Link status clicked')} icon={<CheckCircleIcon color='var(--pf-v5-global--success-color--100)'/>}/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { Status, StatusVariant } from '@patternfly/react-component-groups/dist/dynamic/Status';
import { BanIcon } from '@patternfly/react-icons/';
import { Button, ButtonSize, ButtonVariant } from '@patternfly/react-core';

export const BasicExample: React.FunctionComponent = () => (
<Status
variant={StatusVariant.popover}
label='Not Ready'
icon={<BanIcon color='var(--pf-v5-global--danger-color--100)'/>}
popoverProps={{
bodyContent: 'Example state description',
footerContent: <Button size={ButtonSize.sm} variant={ButtonVariant.link} isInline>Action</Button>
}}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
# Sidenav top-level section
# should be the same for all markdown files
section: extensions
subsection: Component groups
# Sidenav secondary level section
# should be the same for all markdown files
id: Status
# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
source: react
# If you use typescript, the name of the interface to display props for
# These are found through the sourceProps function provided in patternfly-docs.source.js
propComponents: ['Status']
beta: true
sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/Status/Status.md
---
import { BanIcon, CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon } from '@patternfly/react-icons/';
import { Status, StatusVariant } from '@patternfly/react-component-groups/dist/dynamic/Status';

The **status** component's purpose is to display status with icon and text to the user.

### Basic status

Status component consinsts of an icon configured using `icon` and a message, specified with `label`.

```js file="./StatusExample.tsx"

```

### Status with description

Optionally a description can be displayed by passing it to the `description` property.

```js file="./StatusDescriptionExample.tsx"

```

### Icon only status

The `iconOnly` flag allows to hide the status message and show only an icon with a tooltip.

```js file="./IconOnlyStatusExample.tsx"

```

### Link status

You can use the link `variant` to display the link button status.

```js file="./LinkStatusExample.tsx"

```

### Popover status

You can use popover `variant` to display the status details in a popover.

```js file="./PopoverStatusExample.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import { Status } from '@patternfly/react-component-groups/dist/dynamic/Status';
import { ExclamationTriangleIcon } from '@patternfly/react-icons/';

export const BasicExample: React.FunctionComponent = () => (
<Status label='Warning' description='1 issue found' icon={<ExclamationTriangleIcon color='var(--pf-v5-global--warning-color--100)'/>}/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import { Status } from '@patternfly/react-component-groups/dist/dynamic/Status';
import { ExclamationTriangleIcon } from '@patternfly/react-icons/';

export const BasicExample: React.FunctionComponent = () => (
<Status label='Warning' icon={<ExclamationTriangleIcon color='var(--pf-v5-global--warning-color--100)'/>}/>
);
36 changes: 36 additions & 0 deletions packages/module/src/Status/Status.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { render } from '@testing-library/react';
import Status, { StatusVariant } from './Status';
import { BanIcon, CheckCircleIcon, ExclamationTriangleIcon } from '@patternfly/react-icons';
import { Button, ButtonSize, ButtonVariant } from '@patternfly/react-core';

describe('Status component', () => {
it('should render correctly', () => {
expect(render(<Status label='Warning' icon={<ExclamationTriangleIcon color='var(--pf-v5-global--warning-color--100)'/>}/>)).toMatchSnapshot();
});

it('should render iconOnly correctly', () => {
expect(render(<Status iconOnly label='Warning' icon={<ExclamationTriangleIcon color='var(--pf-v5-global--warning-color--100)'/>} />)).toMatchSnapshot();
});

it('should render correctly with description', () => {
expect(render(<Status label='Warning' description='1 issue found' icon={<ExclamationTriangleIcon color='var(--pf-v5-global--warning-color--100)'/>}/>)).toMatchSnapshot();
});

it('should render correctly link', () => {
// eslint-disable-next-line no-console
expect(render(<Status variant={StatusVariant.link} label='Ready' onClick={() => console.log('Link status clicked')} icon={<CheckCircleIcon color='var(--pf-v5-global--success-color--100)'/>}/>)).toMatchSnapshot();
});

it('should render correctly popover', () => {
expect(render(<Status
variant={StatusVariant.popover}
label='Not Ready'
icon={<BanIcon color='var(--pf-v5-global--danger-color--100)'/>}
popoverProps={{
bodyContent: 'Example state description',
footerContent: <Button size={ButtonSize.sm} variant={ButtonVariant.link} isInline>Action</Button>
}}
/>)).toMatchSnapshot();
});
});
92 changes: 92 additions & 0 deletions packages/module/src/Status/Status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as React from 'react';
import clsx from 'clsx';
import { Button, ButtonVariant, Flex, FlexItem, Popover, PopoverPosition, PopoverProps, Text, TextVariants, } from '@patternfly/react-core';
import { createUseStyles } from 'react-jss';

export const StatusVariant = {
link: 'link',
popover: 'popover',
plain: 'plain'
} as const;

export type StatusVariant = typeof StatusVariant[keyof typeof StatusVariant];

export interface StatusProps extends React.PropsWithChildren {
/** Status label text */
label?: string;
/** Description to be displayed under the label */
description?: string;
/** If true, only displays icon */
iconOnly?: boolean;
/** Variant of the status component to be displayed */
variant?: StatusVariant;
/** Status icon */
icon?: React.ReactElement;
/** Custom OUIA ID */
ouiaId?: string | number;
/** Props for the optional popover */
popoverProps?: PopoverProps,
/** Optional link variant onClick callback */
onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
}

const useStyles = createUseStyles({
icon: {
margin: 0
},
statusLabel: {
lineHeight: 'var(--pf-v5-global--LineHeight--sm)',
},
statusDescription: {
color: 'var(--pf-v5-c-content--small--Color)',
}
})

export const Status: React.FC<StatusProps> = ({ variant = StatusVariant.plain, label, children, iconOnly, icon, ouiaId = 'Status', popoverProps, onClick, description, ...props }: StatusProps) => {
const classes = useStyles();

const statusBody = (
<Flex title={label} alignItems={{ default: 'alignItemsCenter' }} {...props}>
{ icon && (
<FlexItem className={classes.icon}>
{React.cloneElement(icon, { className: clsx('pf-v5-u-mr-md', icon?.props?.className), title: label, 'data-ouia-component-id': `${ouiaId}-icon` })}
Copy link
Contributor

Choose a reason for hiding this comment

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

I would recommend that the icon be wrapped in a PF Icon component and use the status prop on that component to control color rather than hardcoding a color in the passed icon.

Copy link
Contributor

Choose a reason for hiding this comment

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

By reusing the label as the icon's title, it causes the screen reader to repeat itself when it reads the icon and then reads the label.

And since the label is marked as optional, when people don't want to display a label, they may leave it blank and leave the icon with no title - thus leaving it inaccessible. I wonder if the icon's title should be an additional field which defaults to the value of the status passed to the Icon component? and the status should be a required field.

Then it should be overridden and given a more descriptive title when using the iconOnly prop. Maybe we could print a message in the console if someone uses iconOnly and then doesn't provide a descriptive Icon title.

</FlexItem>
)}
{ !iconOnly && (
<FlexItem>
<Text ouiaId={`${ouiaId}-label`} className={classes.statusLabel}>{label}</Text>
<Text component={TextVariants.small} ouiaId={`${ouiaId}-description`} className={classes.statusDescription}>{description}</Text>
Copy link
Contributor

Choose a reason for hiding this comment

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

This should only be rendered if there is a description to display.

</FlexItem>
)}
</Flex>
);

if (variant === StatusVariant.link) {
return (
<Button variant={ButtonVariant.link} title={label} onClick={onClick} ouiaId={`${ouiaId}-link-icon`}>
{statusBody}
</Button>
);
}

if (variant === StatusVariant.popover) {
return (
<Popover
position={PopoverPosition.right}
headerContent={label}
bodyContent={children}
aria-label={label}
data-ouia-component-id={`${ouiaId}-popover`}
{...popoverProps}
>
<Button variant={ButtonVariant.link} isInline style={{ textDecoration: 'none' }} ouiaId={`${ouiaId}-popover-icon`}>
{statusBody}
</Button>
</Popover>
)
};

return statusBody
};

export default Status;
Loading