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..117bd1305ae 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 should render without the content area background and overflow scroll. */ + 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..0677d485754 --- /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 footer */ + children?: React.ReactNode; + /** Additional classes added to the footer */ + className?: string; + /** Sets the base component to render. Defaults to footer */ + 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..6102e894202 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.getByRole('contentinfo'); + 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..be10733edf8 --- /dev/null +++ b/packages/react-core/src/components/Page/__tests__/PageFooter.test.tsx @@ -0,0 +1,38 @@ +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.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.getByRole('contentinfo')).toHaveClass(styles.pageFooter, { exact: true }); +}); + +test('Renders as a footer by default', () => { + render(Footer content); + expect(screen.getByRole('contentinfo').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.getByRole('contentinfo')).toHaveClass('custom-class'); +}); + +test('Renders with spread props', () => { + render(Footer content); + expect(screen.getByRole('contentinfo')).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..280e5ec2e48 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 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" ``` 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