-
Notifications
You must be signed in to change notification settings - Fork 31
feat(Status): Add Status component #137
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
Changes from 5 commits
923c0ad
f1b1db2
536e1fe
a851d11
f0b77b2
3ab4645
ba580ec
4cefd14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'); | ||
| }); | ||
|
|
||
| }); |
| 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)'/>}/> | ||
| ); |
| 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(); | ||
| }); | ||
| }); |
| 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` })} | ||
|
||
| </FlexItem> | ||
| )} | ||
| { !iconOnly && ( | ||
| <FlexItem> | ||
| <Text ouiaId={`${ouiaId}-label`} className={classes.statusLabel}>{label}</Text> | ||
| <Text component={TextVariants.small} ouiaId={`${ouiaId}-description`} className={classes.statusDescription}>{description}</Text> | ||
|
||
| </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; | ||
There was a problem hiding this comment.
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
statusprop on that component to control color rather than hardcoding a color in the passed icon.