From db776ce8d1feb23027df1d529b7d1f54da9f73a4 Mon Sep 17 00:00:00 2001 From: alex <86322239+aferd@users.noreply.github.com> Date: Fri, 7 Jun 2024 17:33:30 -0400 Subject: [PATCH] RHCLOUD-30755 add ContentHeader component (#143) --- cypress/component/ContentHeader.cy.tsx | 11 ++ .../assets/icons/content-header-icon.svg | 1 + .../examples/ContentHeader/ContentHeader.md | 56 ++++++ .../ContentHeaderActionsExample.tsx | 63 +++++++ .../ContentHeaderBreadCrumbExample.tsx | 34 ++++ .../ContentHeader/ContentHeaderExample.tsx | 9 + .../ContentHeaderIconExample.tsx | 12 ++ .../ContentHeaderLabelLinkExample.tsx | 15 ++ .../src/ContentHeader/ContentHeader.test.tsx | 9 + .../src/ContentHeader/ContentHeader.tsx | 116 +++++++++++++ .../__snapshots__/ContentHeader.test.tsx.snap | 162 ++++++++++++++++++ packages/module/src/ContentHeader/index.ts | 2 + packages/module/src/index.ts | 3 + 13 files changed, 493 insertions(+) create mode 100644 cypress/component/ContentHeader.cy.tsx create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/assets/icons/content-header-icon.svg create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeader.md create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderActionsExample.tsx create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderBreadCrumbExample.tsx create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderExample.tsx create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderIconExample.tsx create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderLabelLinkExample.tsx create mode 100644 packages/module/src/ContentHeader/ContentHeader.test.tsx create mode 100644 packages/module/src/ContentHeader/ContentHeader.tsx create mode 100644 packages/module/src/ContentHeader/__snapshots__/ContentHeader.test.tsx.snap create mode 100644 packages/module/src/ContentHeader/index.ts diff --git a/cypress/component/ContentHeader.cy.tsx b/cypress/component/ContentHeader.cy.tsx new file mode 100644 index 00000000..b38d8bc0 --- /dev/null +++ b/cypress/component/ContentHeader.cy.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ContentHeader from '../../packages/module/dist/dynamic/ContentHeader'; + +describe('ContentHeader', () => { + it('should render ContentHeader title and subtitle', () => { + cy.mount(); + cy.get('title').should('exist') + cy.get('[data-ouia-component-id="ContentHeader-title"]').should('have.text', 'My title') + cy.get('[data-ouia-component-id="ContentHeader-subtitle"]').should('have.text', 'This is a subtitle for your content header') + }) +}); \ No newline at end of file diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/assets/icons/content-header-icon.svg b/packages/module/patternfly-docs/content/extensions/component-groups/assets/icons/content-header-icon.svg new file mode 100644 index 00000000..936c4217 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/assets/icons/content-header-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeader.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeader.md new file mode 100644 index 00000000..0b9bdd89 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeader.md @@ -0,0 +1,56 @@ +--- +section: extensions +subsection: Component groups +id: Content header +source: react +propComponents: ['ContentHeader'] +sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeader.md +--- + +import ContentHeader from "@patternfly/react-component-groups/dist/dynamic/ContentHeader" +import { EllipsisVIcon } from '@patternfly/react-icons'; +import contentHeaderIcon from '../../assets/icons/content-header-icon.svg' + +The **content header** component displays a page header section with a title, subtitle and other optional content. + +## Examples + +### Basic content header + +In order to display a basic content header, pass the `title` and `subtitle`. + +```js file="./ContentHeaderExample.tsx" + +``` + +### Content header with breadcrumbs + +You can display breadcrumbs above the title using the `breadcrumbs` property. + +```js file="./ContentHeaderBreadCrumbExample.tsx" + +``` + +### Content header with icon + +Use the `icon` property to display your custom page icon separated with a [divider](/components/divider). + +```js file="./ContentHeaderIconExample.tsx" + +``` + +### Content header with label and link + +To add specific element captions for user clarity and convenience, you can use the `label` property together with [label](/components/label) or your custom component. The `linkProps` can be used to define a link displayed under the subtitle. + +```js file="./ContentHeaderLabelLinkExample.tsx" + +``` + +### Content header with actions menu + +In case you want to display actions in your header, you can use the `actionsMenu` property. + +```js file="./ContentHeaderActionsExample.tsx" + +``` \ No newline at end of file diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderActionsExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderActionsExample.tsx new file mode 100644 index 00000000..40048de9 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderActionsExample.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import ContentHeader from '@patternfly/react-component-groups/dist/dynamic/ContentHeader'; +import { ActionList, ActionListItem, Dropdown, DropdownItem, DropdownList, MenuToggle, MenuToggleElement } from '@patternfly/react-core'; +import { EllipsisVIcon } from '@patternfly/react-icons'; + +export const ActionsExample: React.FunctionComponent = () => { + const [ isOpen, setIsOpen ] = React.useState(false); + + const onToggle = () => { + setIsOpen(!isOpen); + }; + + const onSelect = (event: React.MouseEvent | undefined) => { + event?.stopPropagation(); + setIsOpen(!isOpen); + }; + + const dropdownItems = ( + <> + + Link + + Action + + Disabled Link + + + ); + + return ( + + + + ) => ( + + + + )} + isOpen={isOpen} + onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} + > + {dropdownItems} + + + + } + /> + + ) +}; diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderBreadCrumbExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderBreadCrumbExample.tsx new file mode 100644 index 00000000..7c273259 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderBreadCrumbExample.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import ContentHeader from '@patternfly/react-component-groups/dist/dynamic/ContentHeader'; +import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; + +export const BreadCrumbExample: React.FunctionComponent = () => ( + + + Section home + + + Section title + + + Section title + + + } + title='My Title' + subtitle='This is a subtitle for your content header' + /> +); diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderExample.tsx new file mode 100644 index 00000000..e5c584ba --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderExample.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ContentHeader from '@patternfly/react-component-groups/dist/dynamic/ContentHeader'; + +export const BasicExample: React.FunctionComponent = () => ( + +); diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderIconExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderIconExample.tsx new file mode 100644 index 00000000..e55a12ee --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderIconExample.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import ContentHeader from '@patternfly/react-component-groups/dist/dynamic/ContentHeader'; +import contentHeaderIcon from '../../assets/icons/content-header-icon.svg'; + + +export const IconExample: React.FunctionComponent = () => ( + } + /> +); diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderLabelLinkExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderLabelLinkExample.tsx new file mode 100644 index 00000000..c7c140ca --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ContentHeader/ContentHeaderLabelLinkExample.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ContentHeader from '@patternfly/react-component-groups/dist/dynamic/ContentHeader'; +import { Label } from '@patternfly/react-core'; + +export const BasicExample: React.FunctionComponent = () => ( + Org. Administrator} + linkProps={{ + label: 'Go to this link', + isExternal: true, + }} + /> +); diff --git a/packages/module/src/ContentHeader/ContentHeader.test.tsx b/packages/module/src/ContentHeader/ContentHeader.test.tsx new file mode 100644 index 00000000..0b05339e --- /dev/null +++ b/packages/module/src/ContentHeader/ContentHeader.test.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import ContentHeader from './ContentHeader'; + +describe('Contentheader component', () => { + test('should render', () => { + expect(render()).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/packages/module/src/ContentHeader/ContentHeader.tsx b/packages/module/src/ContentHeader/ContentHeader.tsx new file mode 100644 index 00000000..7e0f2b97 --- /dev/null +++ b/packages/module/src/ContentHeader/ContentHeader.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { + Flex, + FlexItem, + Split, + SplitItem, + Text, + PageSection, + TextContent, + Button, + ButtonVariant, + ButtonProps, + Divider, +} from '@patternfly/react-core'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons'; +import { createUseStyles } from 'react-jss'; + +export interface PageHeaderLinkProps extends ButtonProps { + /** Title for the link */ + label: string; + /** Indicates if the link points to an external page */ + isExternal?: boolean; +} + +export interface ContentHeaderProps { + /** Title for content header */ + title: string; + /** Subtitle for content header */ + subtitle: string; + /** Optional link below subtitle */ + linkProps?: PageHeaderLinkProps; + /** Optional icon for content header (appears to the left of the content header's title with a divider) */ + icon?: React.ReactNode; + /** Optional label for content header (appears to the right of the content header's title) */ + label?: React.ReactNode; + /** Breadcrumbs component */ + breadcrumbs?: React.ReactNode; + /** Menu that appears to the far right of the title */ + actionMenu?: React.ReactNode; + /** Custom OUIA ID */ + ouiaId?: string | number; +} + +const useStyles = createUseStyles({ + iconMinWidth: { + minWidth: '48px', + } +}); + +export const ContentHeader: React.FunctionComponent> = ({ + title, + subtitle, + linkProps, + icon, + label, + breadcrumbs = null, + actionMenu, + ouiaId = 'ContentHeader', +}: ContentHeaderProps) => { + const classes = useStyles(); + + return ( + + { breadcrumbs && ( +
+ {breadcrumbs} +
+ )} + + {icon && ( + <> + + {icon} + + + + )} + + + + + + {title} + + + + {label && ( + + {label} + + )} + + {actionMenu && ( + + {actionMenu} + + )} + + + + {subtitle} + + {linkProps && ( + + )} + + + +
+ )}; + +export default ContentHeader; diff --git a/packages/module/src/ContentHeader/__snapshots__/ContentHeader.test.tsx.snap b/packages/module/src/ContentHeader/__snapshots__/ContentHeader.test.tsx.snap new file mode 100644 index 00000000..fe3e1d17 --- /dev/null +++ b/packages/module/src/ContentHeader/__snapshots__/ContentHeader.test.tsx.snap @@ -0,0 +1,162 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Contentheader component should render 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+
+
+
+
+
+

+ My title +

+
+
+
+
+
+

+ This is a subtitle for your content header +

+
+
+
+
+
+ , + "container":
+
+
+
+
+
+
+

+ My title +

+
+
+
+
+
+

+ This is a subtitle for your content header +

+
+
+
+
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/packages/module/src/ContentHeader/index.ts b/packages/module/src/ContentHeader/index.ts new file mode 100644 index 00000000..98181e78 --- /dev/null +++ b/packages/module/src/ContentHeader/index.ts @@ -0,0 +1,2 @@ +export { default } from './ContentHeader' +export * from './ContentHeader' \ No newline at end of file diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index a9bacb62..03a83e87 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -18,6 +18,9 @@ export * from './CloseButton'; export { default as ColumnManagementModal } from './ColumnManagementModal'; export * from './ColumnManagementModal'; +export { default as ContentHeader } from './ContentHeader'; +export * from './ContentHeader'; + export { default as DetailsPage } from './DetailsPage'; export * from './DetailsPage';