Skip to content

Commit 5446403

Browse files
committed
feat(Page): Add PageHeader component
PageHeader can be used to wrap masthead or hold a third-party custom header. Fixes #12624 Assisted-by: Cursor
1 parent 60807bf commit 5446403

7 files changed

Lines changed: 167 additions & 3 deletions

File tree

packages/react-core/src/components/Page/Page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ export interface PageProps extends React.HTMLProps<HTMLDivElement> {
2828
* will handle toggling the visibility of the text in individual isDocked components.
2929
*/
3030
isDockTextExpanded?: boolean;
31-
/** The horizontal masthead content (e.g. <Masthead />). When using the docked variant, this content will only render at mobile viewports. */
31+
/** The horizontal masthead content (e.g. <Masthead /> or <PageHeader />). PageHeader is an alternative to Masthead
32+
* and can wrap a Masthead or custom header content. When using the docked variant, this content will only render at
33+
* mobile viewports.
34+
*/
3235
masthead?: React.ReactNode;
3336
/** @beta Content to render in the vertical dock when variant of docked is used. At mobile viewports, this content will be replaced with the content passed to masthead. */
3437
dockContent?: React.ReactNode;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import styles from '@patternfly/react-styles/css/components/Page/page';
2+
import { css } from '@patternfly/react-styles';
3+
4+
export interface PageHeaderProps extends React.HTMLProps<HTMLElement> {
5+
/** Content rendered inside the page header. This can be a Masthead or custom header content. */
6+
children?: React.ReactNode;
7+
/** Additional classes added to the page header */
8+
className?: string;
9+
/** Sets the base component to render. Defaults to div */
10+
component?: keyof React.JSX.IntrinsicElements;
11+
}
12+
13+
export const PageHeader: React.FunctionComponent<PageHeaderProps> = ({
14+
className,
15+
children,
16+
component = 'div',
17+
...props
18+
}: PageHeaderProps) => {
19+
const Component = component as any;
20+
21+
return (
22+
<Component {...props} className={css(styles.pageHeader, className)}>
23+
{children}
24+
</Component>
25+
);
26+
};
27+
28+
PageHeader.displayName = 'PageHeader';

packages/react-core/src/components/Page/__tests__/Page.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Nav, NavList, NavItem } from '../../Nav';
99
import { SkipToContent } from '../../SkipToContent';
1010
import { PageBreadcrumb } from '../PageBreadcrumb';
1111
import { PageGroup } from '../PageGroup';
12+
import { PageHeader } from '../PageHeader';
1213
import { Masthead } from '../../Masthead';
1314

1415
import styles from '@patternfly/react-styles/css/components/Page/page';
@@ -487,4 +488,33 @@ describe('Page docked variant', () => {
487488
const pageDockMain = screen.getByText('Dock content').closest(`.${styles.pageDockMain}`);
488489
expect(pageDockMain).toBeInTheDocument();
489490
});
491+
492+
test('Renders PageHeader when passed to the masthead prop', () => {
493+
render(
494+
<Page {...props} masthead={<PageHeader>Custom header</PageHeader>}>
495+
<PageSection>Custom content</PageSection>
496+
</Page>
497+
);
498+
499+
const header = screen.getByText('Custom header');
500+
expect(header).toHaveClass(styles.pageHeader);
501+
expect(header.parentElement).toHaveClass(styles.page);
502+
});
503+
504+
test('Renders Masthead inside PageHeader when passed to the masthead prop', () => {
505+
render(
506+
<Page
507+
{...props}
508+
masthead={
509+
<PageHeader>
510+
<Masthead>Logo</Masthead>
511+
</PageHeader>
512+
}
513+
>
514+
<PageSection>Custom content</PageSection>
515+
</Page>
516+
);
517+
518+
expect(screen.getByText('Logo').closest(`.${styles.pageHeader}`)).toBeInTheDocument();
519+
});
490520
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { render, screen } from '@testing-library/react';
2+
import styles from '@patternfly/react-styles/css/components/Page/page';
3+
import { PageHeader } from '../PageHeader';
4+
5+
test('Renders children', () => {
6+
render(<PageHeader>Header content</PageHeader>);
7+
expect(screen.getByText('Header content')).toBeVisible();
8+
});
9+
10+
test(`Renders with class ${styles.pageHeader} by default`, () => {
11+
render(<PageHeader>Header content</PageHeader>);
12+
expect(screen.getByText('Header content')).toHaveClass(styles.pageHeader);
13+
});
14+
15+
test('Renders as a div by default', () => {
16+
render(<PageHeader>Header content</PageHeader>);
17+
expect(screen.getByText('Header content').tagName).toBe('DIV');
18+
});
19+
20+
test('Renders as a custom component when component is passed', () => {
21+
render(<PageHeader component="header">Header content</PageHeader>);
22+
expect(screen.getByText('Header content').tagName).toBe('HEADER');
23+
});
24+
25+
test('Renders with custom classes when className is passed', () => {
26+
render(<PageHeader className="custom-class">Header content</PageHeader>);
27+
expect(screen.getByText('Header content')).toHaveClass('custom-class');
28+
});
29+
30+
test('Renders with spread props', () => {
31+
render(<PageHeader id="custom-id">Header content</PageHeader>);
32+
expect(screen.getByText('Header content')).toHaveAttribute('id', 'custom-id');
33+
});

packages/react-core/src/components/Page/examples/Page.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@ id: Page
33
section: components
44
cssPrefix: pf-v6-c-page
55
propComponents:
6-
['Page', 'PageSidebar', 'PageSidebarBody', 'PageSection', 'PageGroup', 'PageBreadcrumb', 'PageToggleButton']
6+
[
7+
'Page',
8+
'PageHeader',
9+
'PageSidebar',
10+
'PageSidebarBody',
11+
'PageSection',
12+
'PageGroup',
13+
'PageBreadcrumb',
14+
'PageToggleButton'
15+
]
716
---
817

918
import { useState, useLayoutEffect, useRef } from 'react';
@@ -16,14 +25,22 @@ import pageSectionWidthLimitMaxWidth from '@patternfly/react-tokens/dist/esm/c_p
1625

1726
A page will typically contain the following components:
1827

19-
- A `<Page>` with a `masthead` prop that often contains a [masthead](/components/masthead) component
28+
- A `<Page>` with a `masthead` prop that often contains a [masthead](/components/masthead) or a `<PageHeader>`
2029

2130
The `<MastheadMain>` component includes the smaller area that typically contains the `<MastheadToggle>` and a `<MastheadLogo>`. `<MastheadContent>` represents the main portion of the masthead, and will typically contain a `<Toolbar>` or other menu-like components, like a `<Dropdown>`.
2231

2332
- Mastheads contain a `<MastheadMain>` component, which includes the `<MastheadToggle>`, a `<MastheadLogo>`, and the page's toolbar (via `<MastheadContent>`.) The `<MastheadToggle>` component contains a `<PageToggleButton>`, and the `<MastheadLogo>` component contains a `<MastheadBrand>`.
2433
- 1 or more `<PageSidebarBody>` components inside `<PageSidebar>` for vertical navigation or other sidebar content
2534
- 1 or more `<PageSection>` components
2635

36+
### Page header
37+
38+
To use a page header instead of passing a [masthead](/components/masthead) directly, pass a `<PageHeader>` to the `masthead` property. `<PageHeader>` can wrap a `<Masthead>` or custom header content.
39+
40+
```ts file="./PageHeaderContent.tsx"
41+
42+
```
43+
2744
### Vertical navigation
2845

2946
To add a vertical sidebar to a `<Page>`, pass a `<PageSidebar>` component into the `sidebar` property. To render navigation in the sidebar, pass a `<PageSidebarBody>` component to `<PageSidebar>`.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
Page,
3+
PageHeader,
4+
Masthead,
5+
MastheadMain,
6+
MastheadBrand,
7+
MastheadLogo,
8+
MastheadContent,
9+
PageSection,
10+
Toolbar,
11+
ToolbarContent,
12+
ToolbarItem
13+
} from '@patternfly/react-core';
14+
15+
export const PageHeaderContent: React.FunctionComponent = () => {
16+
const headerToolbar = (
17+
<Toolbar id="page-header-content-toolbar">
18+
<ToolbarContent>
19+
<ToolbarItem>header-tools</ToolbarItem>
20+
</ToolbarContent>
21+
</Toolbar>
22+
);
23+
24+
const pageHeader = (
25+
<PageHeader>
26+
<Masthead>
27+
<MastheadMain>
28+
<MastheadBrand>
29+
<MastheadLogo href="https://patternfly.org" target="_blank">
30+
Logo
31+
</MastheadLogo>
32+
</MastheadBrand>
33+
</MastheadMain>
34+
<MastheadContent>{headerToolbar}</MastheadContent>
35+
</Masthead>
36+
</PageHeader>
37+
);
38+
39+
return (
40+
<Page masthead={pageHeader}>
41+
<PageSection aria-labelledby="section-1">
42+
<h2 id="section-1">Page header example section 1</h2>
43+
</PageSection>
44+
<PageSection variant="secondary" aria-labelledby="section-2">
45+
<h2 id="section-2">Page header example section 2 with secondary variant styling</h2>
46+
</PageSection>
47+
<PageSection aria-labelledby="section-3">
48+
<h2 id="section-3">Page header example section 3</h2>
49+
</PageSection>
50+
</Page>
51+
);
52+
};

packages/react-core/src/components/Page/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './Page';
22
export * from './PageBody';
33
export * from './PageBreadcrumb';
44
export * from './PageGroup';
5+
export * from './PageHeader';
56
export * from './PageSidebar';
67
export * from './PageSidebarBody';
78
export * from './PageSection';

0 commit comments

Comments
 (0)