From 50e5e7244f935e6bc3dc31f4dea9136376184e18 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Tue, 8 Sep 2026 15:05:14 -0400 Subject: [PATCH 1/4] feat(Page): add PageFooter and isPlain --- packages/react-core/package.json | 2 +- .../react-core/src/components/Page/Page.tsx | 11 ++++- .../src/components/Page/PageFooter.tsx | 28 +++++++++++ .../components/Page/__tests__/Page.test.tsx | 46 +++++++++++++++++++ .../Page/__tests__/PageFooter.test.tsx | 33 +++++++++++++ .../src/components/Page/examples/Page.md | 8 +++- ...ent.tsx => PageHeaderAndFooterContent.tsx} | 7 +-- .../react-core/src/components/Page/index.ts | 1 + packages/react-docs/package.json | 2 +- packages/react-icons/package.json | 2 +- packages/react-styles/package.json | 2 +- packages/react-tokens/package.json | 2 +- yarn.lock | 18 ++++---- 13 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 packages/react-core/src/components/Page/PageFooter.tsx create mode 100644 packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx rename packages/react-core/src/components/Page/examples/{PageHeaderContent.tsx => PageHeaderAndFooterContent.tsx} (69%) diff --git a/packages/react-core/package.json b/packages/react-core/package.json index 3472d07eaf1..36609ae845d 100644 --- a/packages/react-core/package.json +++ b/packages/react-core/package.json @@ -54,7 +54,7 @@ "tslib": "^2.8.1" }, "devDependencies": { - "@patternfly/patternfly": "6.6.0-prerelease.39", + "@patternfly/patternfly": "6.6.0-prerelease.40", "case-anything": "^3.1.2", "css": "^3.0.0", "fs-extra": "^11.3.3" diff --git a/packages/react-core/src/components/Page/Page.tsx b/packages/react-core/src/components/Page/Page.tsx index c2a109b6348..b596964d995 100644 --- a/packages/react-core/src/components/Page/Page.tsx +++ b/packages/react-core/src/components/Page/Page.tsx @@ -118,6 +118,10 @@ export interface PageProps extends React.HTMLProps { breadcrumbProps?: PageBreadcrumbProps; /** Enables children to fill the available vertical space. Child page sections or groups that should fill should be passed the isFilled property. */ isContentFilled?: boolean; + /** 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. */ + isPlain?: boolean; + /** Content rendered inside the page footer */ + footer?: React.ReactNode; } export interface PageState { @@ -141,7 +145,8 @@ class Page extends Component { mainComponent: 'main', getBreakpoint, getVerticalBreakpoint, - mainRef: undefined + mainRef: undefined, + isPlain: false }; mainRef = this.props?.mainRef ? this.props.mainRef : createRef(); pageRef = createRef(); @@ -284,6 +289,8 @@ class Page extends Component { isContentFilled, // eslint-disable-next-line @typescript-eslint/no-unused-vars mainRef, + isPlain, + footer, ...rest } = this.props; const { mobileView, mobileIsSidebarOpen, desktopIsSidebarOpen, width, height } = this.state; @@ -367,6 +374,7 @@ class Page extends Component { width !== null && `pf-m-breakpoint-${getBreakpoint(width)}`, height !== null && `pf-m-height-breakpoint-${getVerticalBreakpoint(height)}`, sidebar === null && styles.modifiers.noSidebar, + isPlain && styles.modifiers.plain, className )} > @@ -397,6 +405,7 @@ class Page extends Component { )} {!notificationDrawer && main} + {footer && footer} ); diff --git a/packages/react-core/src/components/Page/PageFooter.tsx b/packages/react-core/src/components/Page/PageFooter.tsx new file mode 100644 index 00000000000..ef2e8210cb3 --- /dev/null +++ b/packages/react-core/src/components/Page/PageFooter.tsx @@ -0,0 +1,28 @@ +import styles from '@patternfly/react-styles/css/components/Page/page'; +import { css } from '@patternfly/react-styles'; + +export interface PageFooterProps extends React.HTMLProps { + /** Content rendered inside the page header. This should be custom header content, rather than the PatternFly Masthead. */ + children?: React.ReactNode; + /** Additional classes added to the page header */ + className?: string; + /** Sets the base component to render. Defaults to header */ + component?: keyof React.JSX.IntrinsicElements; +} + +export const PageFooter: React.FunctionComponent = ({ + className, + children, + component = 'footer', + ...props +}: PageFooterProps) => { + const Component = component as any; + + return ( + + {children} + + ); +}; + +PageFooter.displayName = 'PageFooter'; diff --git a/packages/react-core/src/components/Page/__tests__/Page.test.tsx b/packages/react-core/src/components/Page/__tests__/Page.test.tsx index de1b87266af..7ba961abb90 100644 --- a/packages/react-core/src/components/Page/__tests__/Page.test.tsx +++ b/packages/react-core/src/components/Page/__tests__/Page.test.tsx @@ -13,6 +13,7 @@ import { PageHeader } from '../PageHeader'; import { Masthead } from '../../Masthead'; import styles from '@patternfly/react-styles/css/components/Page/page'; +import { PageFooter } from '../PageFooter'; const props = { 'aria-label': 'Page layout', @@ -520,4 +521,49 @@ describe('Page docked variant', () => { expect(header).toHaveClass(styles.pageHeader); expect(header.parentElement).toHaveClass(styles.page); }); + + test('Renders PageFooter when passed to the footer prop', () => { + render( + Custom footer}> + Custom content + + ); + + const footer = screen.getByText('Custom footer'); + expect(footer).toHaveClass(styles.pageFooter); + expect(footer.parentElement).toHaveClass(styles.page); + }); + + test(`Renders with ${styles.modifiers.plain} when isPlain is true`, () => { + render( + + Custom content + + ); + + const page = screen.getByTestId('page'); + expect(page).toHaveClass(styles.modifiers.plain); + }); + + test(`Does not render with ${styles.modifiers.plain} when isPlain is false`, () => { + render( + + Custom content + + ); + + const page = screen.getByTestId('page'); + expect(page).not.toHaveClass(styles.modifiers.plain); + }); + + test(`Does not render with ${styles.modifiers.plain} when isPlain is not passed`, () => { + render( + + Custom content + + ); + + const page = screen.getByTestId('page'); + expect(page).not.toHaveClass(styles.modifiers.plain); + }); }); diff --git a/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx b/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx new file mode 100644 index 00000000000..45ae678e821 --- /dev/null +++ b/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from '@testing-library/react'; +import styles from '@patternfly/react-styles/css/components/Page/page'; +import { PageFooter } from '../PageFooter'; + +test('Renders children', () => { + render(Footer content); + expect(screen.getByText('Footer content')).toBeVisible(); +}); + +test(`Renders with class ${styles.pageFooter} by default`, () => { + render(Footer content); + expect(screen.getByText('Footer content')).toHaveClass(styles.pageFooter, { exact: true }); +}); + +test('Renders as a div by default', () => { + render(Footer content); + expect(screen.getByText('Footer content').tagName).toBe('FOOTER'); +}); + +test('Renders as a custom component when component is passed', () => { + render(Footer content); + expect(screen.getByText('Footer content').tagName).toBe('DIV'); +}); + +test('Renders with custom classes when className is passed', () => { + render(Footer content); + expect(screen.getByText('Footer content')).toHaveClass('custom-class'); +}); + +test('Renders with spread props', () => { + render(Footer content); + expect(screen.getByText('Footer content')).toHaveAttribute('id', 'custom-id'); +}); diff --git a/packages/react-core/src/components/Page/examples/Page.md b/packages/react-core/src/components/Page/examples/Page.md index 943ea3acd73..62043f64e43 100644 --- a/packages/react-core/src/components/Page/examples/Page.md +++ b/packages/react-core/src/components/Page/examples/Page.md @@ -33,11 +33,15 @@ The `` component includes the smaller area that typically contains - 1 or more `` components inside `` for vertical navigation or other sidebar content - 1 or more `` components -### Page header +### Page header and footer To use a page header instead of passing a [masthead](/components/masthead) directly, pass a `` to the `masthead` property. `` should only be used to wrap custom header content. -```ts file="./PageHeaderContent.tsx" +`` can be passed to the `footer` property, and should be used to wrap custom footer content. + +When using custom a `` and ``, the `isPlain` flag should be passed to ``. 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. + +```ts file="./PageHeaderAndFooterContent.tsx" ``` diff --git a/packages/react-core/src/components/Page/examples/PageHeaderContent.tsx b/packages/react-core/src/components/Page/examples/PageHeaderAndFooterContent.tsx similarity index 69% rename from packages/react-core/src/components/Page/examples/PageHeaderContent.tsx rename to packages/react-core/src/components/Page/examples/PageHeaderAndFooterContent.tsx index 27b0be187dd..3dd2160b419 100644 --- a/packages/react-core/src/components/Page/examples/PageHeaderContent.tsx +++ b/packages/react-core/src/components/Page/examples/PageHeaderAndFooterContent.tsx @@ -1,10 +1,11 @@ -import { Page, PageHeader, PageSection } from '@patternfly/react-core'; +import { Page, PageHeader, PageFooter, PageSection } from '@patternfly/react-core'; -export const PageHeaderContent: React.FunctionComponent = () => { +export const PageHeaderAndFooterContent: React.FunctionComponent = () => { const pageHeader = Page header; + const pageFooter = Page footer; return ( - +

Page header example section 1

diff --git a/packages/react-core/src/components/Page/index.ts b/packages/react-core/src/components/Page/index.ts index 55f94c2bffe..e8e258f868f 100644 --- a/packages/react-core/src/components/Page/index.ts +++ b/packages/react-core/src/components/Page/index.ts @@ -1,6 +1,7 @@ export * from './Page'; export * from './PageBody'; export * from './PageBreadcrumb'; +export * from './PageFooter'; export * from './PageGroup'; export * from './PageHeader'; export * from './PageSidebar'; diff --git a/packages/react-docs/package.json b/packages/react-docs/package.json index 1e883306a45..eb01eb6f8b1 100644 --- a/packages/react-docs/package.json +++ b/packages/react-docs/package.json @@ -23,7 +23,7 @@ "test:a11y": "patternfly-a11y --config patternfly-a11y.config" }, "dependencies": { - "@patternfly/patternfly": "6.6.0-prerelease.39", + "@patternfly/patternfly": "6.6.0-prerelease.40", "@patternfly/react-charts": "workspace:^", "@patternfly/react-code-editor": "workspace:^", "@patternfly/react-core": "workspace:^", diff --git a/packages/react-icons/package.json b/packages/react-icons/package.json index 0f8f7688aa6..7d4430ef10c 100644 --- a/packages/react-icons/package.json +++ b/packages/react-icons/package.json @@ -38,7 +38,7 @@ "@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4", - "@patternfly/patternfly": "6.6.0-prerelease.39", + "@patternfly/patternfly": "6.6.0-prerelease.40", "@rhds/icons": "^2.3.1", "fs-extra": "^11.3.3" }, diff --git a/packages/react-styles/package.json b/packages/react-styles/package.json index 282d3dc5366..1cfbd54f5cc 100644 --- a/packages/react-styles/package.json +++ b/packages/react-styles/package.json @@ -19,7 +19,7 @@ "clean": "rimraf dist css" }, "devDependencies": { - "@patternfly/patternfly": "6.6.0-prerelease.39", + "@patternfly/patternfly": "6.6.0-prerelease.40", "change-case": "^5.4.4", "fs-extra": "^11.3.3" }, diff --git a/packages/react-tokens/package.json b/packages/react-tokens/package.json index 738e6b39dba..7119c06b643 100644 --- a/packages/react-tokens/package.json +++ b/packages/react-tokens/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@adobe/css-tools": "^4.4.4", - "@patternfly/patternfly": "6.6.0-prerelease.39", + "@patternfly/patternfly": "6.6.0-prerelease.40", "fs-extra": "^11.3.3" } } diff --git a/yarn.lock b/yarn.lock index e6e11f857fd..a603b957435 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5070,10 +5070,10 @@ __metadata: languageName: node linkType: hard -"@patternfly/patternfly@npm:6.6.0-prerelease.39": - version: 6.6.0-prerelease.39 - resolution: "@patternfly/patternfly@npm:6.6.0-prerelease.39" - checksum: 10c0/e3ad085429507c23912bf50b84a178c9b2f05fe58dbbecce9eb76be9f70eb4b116e81a611f5eaa8dc388d4081ae5f36ab09725268041e5557a85c20cb07200c3 +"@patternfly/patternfly@npm:6.6.0-prerelease.40": + version: 6.6.0-prerelease.40 + resolution: "@patternfly/patternfly@npm:6.6.0-prerelease.40" + checksum: 10c0/33401d343f40467a94265965c25c8b40b16154d3bed3e4aab860662fae04947266778180ce8a076431b611420c913abedc672b9a3c26917635ef458bcb5c6826 languageName: node linkType: hard @@ -5171,7 +5171,7 @@ __metadata: version: 0.0.0-use.local resolution: "@patternfly/react-core@workspace:packages/react-core" dependencies: - "@patternfly/patternfly": "npm:6.6.0-prerelease.39" + "@patternfly/patternfly": "npm:6.6.0-prerelease.40" "@patternfly/react-icons": "workspace:^" "@patternfly/react-styles": "workspace:^" "@patternfly/react-tokens": "workspace:^" @@ -5192,7 +5192,7 @@ __metadata: resolution: "@patternfly/react-docs@workspace:packages/react-docs" dependencies: "@patternfly/documentation-framework": "npm:^6.40.0" - "@patternfly/patternfly": "npm:6.6.0-prerelease.39" + "@patternfly/patternfly": "npm:6.6.0-prerelease.40" "@patternfly/patternfly-a11y": "npm:5.2.1" "@patternfly/react-charts": "workspace:^" "@patternfly/react-code-editor": "workspace:^" @@ -5232,7 +5232,7 @@ __metadata: "@fortawesome/free-brands-svg-icons": "npm:^5.15.4" "@fortawesome/free-regular-svg-icons": "npm:^5.15.4" "@fortawesome/free-solid-svg-icons": "npm:^5.15.4" - "@patternfly/patternfly": "npm:6.6.0-prerelease.39" + "@patternfly/patternfly": "npm:6.6.0-prerelease.40" "@rhds/icons": "npm:^2.3.1" fs-extra: "npm:^11.3.3" tslib: "npm:^2.8.1" @@ -5319,7 +5319,7 @@ __metadata: version: 0.0.0-use.local resolution: "@patternfly/react-styles@workspace:packages/react-styles" dependencies: - "@patternfly/patternfly": "npm:6.6.0-prerelease.39" + "@patternfly/patternfly": "npm:6.6.0-prerelease.40" change-case: "npm:^5.4.4" fs-extra: "npm:^11.3.3" languageName: unknown @@ -5361,7 +5361,7 @@ __metadata: resolution: "@patternfly/react-tokens@workspace:packages/react-tokens" dependencies: "@adobe/css-tools": "npm:^4.4.4" - "@patternfly/patternfly": "npm:6.6.0-prerelease.39" + "@patternfly/patternfly": "npm:6.6.0-prerelease.40" fs-extra: "npm:^11.3.3" languageName: unknown linkType: soft From a938afd715493b26f171d608242a42f225bbf40f Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Wed, 9 Sep 2026 11:59:21 -0400 Subject: [PATCH 2/4] update wording on prop, update to footer --- packages/react-core/src/components/Page/Page.tsx | 2 +- packages/react-core/src/components/Page/PageFooter.tsx | 6 +++--- .../src/components/Page/__tests__/PageFooter.test.tsx | 2 +- packages/react-core/src/components/Page/examples/Page.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react-core/src/components/Page/Page.tsx b/packages/react-core/src/components/Page/Page.tsx index b596964d995..117bd1305ae 100644 --- a/packages/react-core/src/components/Page/Page.tsx +++ b/packages/react-core/src/components/Page/Page.tsx @@ -118,7 +118,7 @@ export interface PageProps extends React.HTMLProps { breadcrumbProps?: PageBreadcrumbProps; /** Enables children to fill the available vertical space. Child page sections or groups that should fill should be passed the isFilled property. */ isContentFilled?: boolean; - /** 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. */ + /** Flag indicating the page should render without the content area background and overflow scroll. */ isPlain?: boolean; /** Content rendered inside the page footer */ footer?: React.ReactNode; diff --git a/packages/react-core/src/components/Page/PageFooter.tsx b/packages/react-core/src/components/Page/PageFooter.tsx index ef2e8210cb3..0677d485754 100644 --- a/packages/react-core/src/components/Page/PageFooter.tsx +++ b/packages/react-core/src/components/Page/PageFooter.tsx @@ -2,11 +2,11 @@ import styles from '@patternfly/react-styles/css/components/Page/page'; import { css } from '@patternfly/react-styles'; export interface PageFooterProps extends React.HTMLProps { - /** Content rendered inside the page header. This should be custom header content, rather than the PatternFly Masthead. */ + /** Content rendered inside the footer */ children?: React.ReactNode; - /** Additional classes added to the page header */ + /** Additional classes added to the footer */ className?: string; - /** Sets the base component to render. Defaults to header */ + /** Sets the base component to render. Defaults to footer */ component?: keyof React.JSX.IntrinsicElements; } diff --git a/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx b/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx index 45ae678e821..a41802f28d0 100644 --- a/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx +++ b/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx @@ -12,7 +12,7 @@ test(`Renders with class ${styles.pageFooter} by default`, () => { expect(screen.getByText('Footer content')).toHaveClass(styles.pageFooter, { exact: true }); }); -test('Renders as a div by default', () => { +test('Renders as a footer by default', () => { render(Footer content); expect(screen.getByText('Footer content').tagName).toBe('FOOTER'); }); diff --git a/packages/react-core/src/components/Page/examples/Page.md b/packages/react-core/src/components/Page/examples/Page.md index 62043f64e43..5076db896ca 100644 --- a/packages/react-core/src/components/Page/examples/Page.md +++ b/packages/react-core/src/components/Page/examples/Page.md @@ -39,7 +39,7 @@ To use a page header instead of passing a [masthead](/components/masthead) direc `` can be passed to the `footer` property, and should be used to wrap custom footer content. -When using custom a `` and ``, the `isPlain` flag should be passed to ``. 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. +When using a custom `` and ``, the `isPlain` flag should be passed to ``. 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. ```ts file="./PageHeaderAndFooterContent.tsx" From cd18b6d0328bf9610a5829a3c4d8757d471789f3 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Wed, 9 Sep 2026 12:00:06 -0400 Subject: [PATCH 3/4] update wording --- packages/react-core/src/components/Page/examples/Page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/components/Page/examples/Page.md b/packages/react-core/src/components/Page/examples/Page.md index 5076db896ca..280e5ec2e48 100644 --- a/packages/react-core/src/components/Page/examples/Page.md +++ b/packages/react-core/src/components/Page/examples/Page.md @@ -39,7 +39,7 @@ To use a page header instead of passing a [masthead](/components/masthead) direc `` can be passed to the `footer` property, and should be used to wrap custom footer content. -When using a custom `` and ``, the `isPlain` flag should be passed to ``. 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. +When using a custom `` and ``, the `isPlain` flag should be passed to ``. This will remove styling on the main container, the height constraints on the page wrapper (so it can grow beyond the viewport), and remove the scroll management from the content section so the window will be what scrolls. ```ts file="./PageHeaderAndFooterContent.tsx" From a182f6034dfbd7488ad483330613e5d04c037237 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Thu, 10 Sep 2026 15:02:24 -0400 Subject: [PATCH 4/4] Address Eric's feedback --- .../src/components/Page/__tests__/Page.test.tsx | 2 +- .../components/Page/__tests__/PageFooter.test.tsx | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/react-core/src/components/Page/__tests__/Page.test.tsx b/packages/react-core/src/components/Page/__tests__/Page.test.tsx index 7ba961abb90..6102e894202 100644 --- a/packages/react-core/src/components/Page/__tests__/Page.test.tsx +++ b/packages/react-core/src/components/Page/__tests__/Page.test.tsx @@ -529,7 +529,7 @@ describe('Page docked variant', () => { ); - const footer = screen.getByText('Custom footer'); + const footer = screen.getByRole('contentinfo'); expect(footer).toHaveClass(styles.pageFooter); expect(footer.parentElement).toHaveClass(styles.page); }); diff --git a/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx b/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx index a41802f28d0..be10733edf8 100644 --- a/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx +++ b/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx @@ -4,17 +4,22 @@ import { PageFooter } from '../PageFooter'; test('Renders children', () => { render(Footer content); - expect(screen.getByText('Footer content')).toBeVisible(); + expect(screen.getByRole('contentinfo')).toBeVisible(); +}); + +test('Renders without children', () => { + render(); + expect(screen.getByTestId('footer')).toBeVisible(); }); test(`Renders with class ${styles.pageFooter} by default`, () => { render(Footer content); - expect(screen.getByText('Footer content')).toHaveClass(styles.pageFooter, { exact: true }); + expect(screen.getByRole('contentinfo')).toHaveClass(styles.pageFooter, { exact: true }); }); test('Renders as a footer by default', () => { render(Footer content); - expect(screen.getByText('Footer content').tagName).toBe('FOOTER'); + expect(screen.getByRole('contentinfo').tagName).toBe('FOOTER'); }); test('Renders as a custom component when component is passed', () => { @@ -24,10 +29,10 @@ test('Renders as a custom component when component is passed', () => { test('Renders with custom classes when className is passed', () => { render(Footer content); - expect(screen.getByText('Footer content')).toHaveClass('custom-class'); + expect(screen.getByRole('contentinfo')).toHaveClass('custom-class'); }); test('Renders with spread props', () => { render(Footer content); - expect(screen.getByText('Footer content')).toHaveAttribute('id', 'custom-id'); + expect(screen.getByRole('contentinfo')).toHaveAttribute('id', 'custom-id'); });