Skip to content

Commit fb49c93

Browse files
committed
feat(Page): add PageFooter and isPlain
1 parent b73e9dd commit fb49c93

13 files changed

Lines changed: 142 additions & 20 deletions

File tree

packages/react-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"tslib": "^2.8.1"
5555
},
5656
"devDependencies": {
57-
"@patternfly/patternfly": "6.6.0-prerelease.39",
57+
"@patternfly/patternfly": "6.6.0-prerelease.40",
5858
"case-anything": "^3.1.2",
5959
"css": "^3.0.0",
6060
"fs-extra": "^11.3.3"

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ export interface PageProps extends React.HTMLProps<HTMLDivElement> {
118118
breadcrumbProps?: PageBreadcrumbProps;
119119
/** Enables children to fill the available vertical space. Child page sections or groups that should fill should be passed the isFilled property. */
120120
isContentFilled?: boolean;
121+
/** Flag indicating the page has non-PatternFly elements for header and footer and should be rendered plainly. Use PageHeader and PageFooter to wrap custom header and footer content to ensure the layout is maintained. */
122+
isPlain?: boolean;
123+
/** Content rendered inside the page footer */
124+
footer?: React.ReactNode;
121125
}
122126

123127
export interface PageState {
@@ -141,7 +145,8 @@ class Page extends Component<PageProps, PageState> {
141145
mainComponent: 'main',
142146
getBreakpoint,
143147
getVerticalBreakpoint,
144-
mainRef: undefined
148+
mainRef: undefined,
149+
isPlain: false
145150
};
146151
mainRef = this.props?.mainRef ? this.props.mainRef : createRef<HTMLDivElement>();
147152
pageRef = createRef<HTMLDivElement>();
@@ -284,6 +289,8 @@ class Page extends Component<PageProps, PageState> {
284289
isContentFilled,
285290
// eslint-disable-next-line @typescript-eslint/no-unused-vars
286291
mainRef,
292+
isPlain,
293+
footer,
287294
...rest
288295
} = this.props;
289296
const { mobileView, mobileIsSidebarOpen, desktopIsSidebarOpen, width, height } = this.state;
@@ -367,6 +374,7 @@ class Page extends Component<PageProps, PageState> {
367374
width !== null && `pf-m-breakpoint-${getBreakpoint(width)}`,
368375
height !== null && `pf-m-height-breakpoint-${getVerticalBreakpoint(height)}`,
369376
sidebar === null && styles.modifiers.noSidebar,
377+
isPlain && styles.modifiers.plain,
370378
className
371379
)}
372380
>
@@ -397,6 +405,7 @@ class Page extends Component<PageProps, PageState> {
397405
</div>
398406
)}
399407
{!notificationDrawer && main}
408+
{footer && footer}
400409
</div>
401410
</PageContextProvider>
402411
);
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 PageFooterProps extends React.HTMLProps<HTMLElement> {
5+
/** Content rendered inside the page header. This should be custom header content, rather than the PatternFly Masthead. */
6+
children?: React.ReactNode;
7+
/** Additional classes added to the page header */
8+
className?: string;
9+
/** Sets the base component to render. Defaults to header */
10+
component?: keyof React.JSX.IntrinsicElements;
11+
}
12+
13+
export const PageFooter: React.FunctionComponent<PageFooterProps> = ({
14+
className,
15+
children,
16+
component = 'footer',
17+
...props
18+
}: PageFooterProps) => {
19+
const Component = component as any;
20+
21+
return (
22+
<Component {...props} className={css(styles.pageFooter, className)}>
23+
{children}
24+
</Component>
25+
);
26+
};
27+
28+
PageFooter.displayName = 'PageFooter';

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { PageHeader } from '../PageHeader';
1313
import { Masthead } from '../../Masthead';
1414

1515
import styles from '@patternfly/react-styles/css/components/Page/page';
16+
import { PageFooter } from '../PageFooter';
1617

1718
const props = {
1819
'aria-label': 'Page layout',
@@ -520,4 +521,49 @@ describe('Page docked variant', () => {
520521
expect(header).toHaveClass(styles.pageHeader);
521522
expect(header.parentElement).toHaveClass(styles.page);
522523
});
524+
525+
test('Renders PageFooter when passed to the footer prop', () => {
526+
render(
527+
<Page {...props} footer={<PageFooter>Custom footer</PageFooter>}>
528+
<PageSection>Custom content</PageSection>
529+
</Page>
530+
);
531+
532+
const footer = screen.getByText('Custom footer');
533+
expect(footer).toHaveClass(styles.pageFooter);
534+
expect(footer.parentElement).toHaveClass(styles.page);
535+
});
536+
537+
test(`Renders with ${styles.modifiers.plain} when isPlain is true`, () => {
538+
render(
539+
<Page {...props} isPlain data-testid="page">
540+
<PageSection>Custom content</PageSection>
541+
</Page>
542+
);
543+
544+
const page = screen.getByTestId('page');
545+
expect(page).toHaveClass(styles.modifiers.plain);
546+
});
547+
548+
test(`Does not render with ${styles.modifiers.plain} when isPlain is false`, () => {
549+
render(
550+
<Page {...props} isPlain={false} data-testid="page">
551+
<PageSection>Custom content</PageSection>
552+
</Page>
553+
);
554+
555+
const page = screen.getByTestId('page');
556+
expect(page).not.toHaveClass(styles.modifiers.plain);
557+
});
558+
559+
test(`Does not render with ${styles.modifiers.plain} when isPlain is not passed`, () => {
560+
render(
561+
<Page {...props} data-testid="page">
562+
<PageSection>Custom content</PageSection>
563+
</Page>
564+
);
565+
566+
const page = screen.getByTestId('page');
567+
expect(page).not.toHaveClass(styles.modifiers.plain);
568+
});
523569
});
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 { PageFooter } from '../PageFooter';
4+
5+
test('Renders children', () => {
6+
render(<PageFooter>Footer content</PageFooter>);
7+
expect(screen.getByText('Footer content')).toBeVisible();
8+
});
9+
10+
test(`Renders with class ${styles.pageFooter} by default`, () => {
11+
render(<PageFooter>Footer content</PageFooter>);
12+
expect(screen.getByText('Footer content')).toHaveClass(styles.pageFooter, { exact: true });
13+
});
14+
15+
test('Renders as a div by default', () => {
16+
render(<PageFooter>Footer content</PageFooter>);
17+
expect(screen.getByText('Footer content').tagName).toBe('FOOTER');
18+
});
19+
20+
test('Renders as a custom component when component is passed', () => {
21+
render(<PageFooter component="div">Footer content</PageFooter>);
22+
expect(screen.getByText('Footer content').tagName).toBe('DIV');
23+
});
24+
25+
test('Renders with custom classes when className is passed', () => {
26+
render(<PageFooter className="custom-class">Footer content</PageFooter>);
27+
expect(screen.getByText('Footer content')).toHaveClass('custom-class');
28+
});
29+
30+
test('Renders with spread props', () => {
31+
render(<PageFooter id="custom-id">Footer content</PageFooter>);
32+
expect(screen.getByText('Footer content')).toHaveAttribute('id', 'custom-id');
33+
});

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,15 @@ The `<MastheadMain>` component includes the smaller area that typically contains
3333
- 1 or more `<PageSidebarBody>` components inside `<PageSidebar>` for vertical navigation or other sidebar content
3434
- 1 or more `<PageSection>` components
3535

36-
### Page header
36+
### Page header and footer
3737

3838
To use a page header instead of passing a [masthead](/components/masthead) directly, pass a `<PageHeader>` to the `masthead` property. `<PageHeader>` should only be used to wrap custom header content.
3939

40-
```ts file="./PageHeaderContent.tsx"
40+
`<PageFooter>` can be passed to the `footer` property, and should be used to wrap custom footer content.
41+
42+
When using custom a `<PageHeader>` and `<PageFooter>`, the `isPlain` flag should be passed to `<Page>`. This will remove styling on the main container, the height constraints on the page wrapper (so it can grow beyond the viewport), and removes the scroll management from the content section so the window will be what scrolls.
43+
44+
```ts file="./PageHeaderAndFooterContent.tsx"
4145

4246
```
4347

packages/react-core/src/components/Page/examples/PageHeaderContent.tsx renamed to packages/react-core/src/components/Page/examples/PageHeaderAndFooterContent.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { Page, PageHeader, PageSection } from '@patternfly/react-core';
1+
import { Page, PageHeader, PageFooter, PageSection } from '@patternfly/react-core';
22

3-
export const PageHeaderContent: React.FunctionComponent = () => {
3+
export const PageHeaderAndFooterContent: React.FunctionComponent = () => {
44
const pageHeader = <PageHeader>Page header</PageHeader>;
5+
const pageFooter = <PageFooter>Page footer</PageFooter>;
56

67
return (
7-
<Page masthead={pageHeader}>
8+
<Page isPlain masthead={pageHeader} footer={pageFooter}>
89
<PageSection aria-labelledby="header-example-section-1">
910
<h2 id="header-example-section-1">Page header example section 1</h2>
1011
</PageSection>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './Page';
22
export * from './PageBody';
33
export * from './PageBreadcrumb';
4+
export * from './PageFooter';
45
export * from './PageGroup';
56
export * from './PageHeader';
67
export * from './PageSidebar';

packages/react-docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"test:a11y": "patternfly-a11y --config patternfly-a11y.config"
2424
},
2525
"dependencies": {
26-
"@patternfly/patternfly": "6.6.0-prerelease.39",
26+
"@patternfly/patternfly": "6.6.0-prerelease.40",
2727
"@patternfly/react-charts": "workspace:^",
2828
"@patternfly/react-code-editor": "workspace:^",
2929
"@patternfly/react-core": "workspace:^",

packages/react-icons/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@fortawesome/free-brands-svg-icons": "^5.15.4",
3939
"@fortawesome/free-regular-svg-icons": "^5.15.4",
4040
"@fortawesome/free-solid-svg-icons": "^5.15.4",
41-
"@patternfly/patternfly": "6.6.0-prerelease.39",
41+
"@patternfly/patternfly": "6.6.0-prerelease.40",
4242
"@rhds/icons": "^2.2.0",
4343
"fs-extra": "^11.3.3"
4444
},

0 commit comments

Comments
 (0)