From 605a877865bd3e5f4245b0e0dd52b9e85b617e6b Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 8 Sep 2026 12:28:12 -0400 Subject: [PATCH] feat(Drawer): Add full-size drawer Use isViewport and place drawer below Page to achieve a full-height drawer that allows Page content to scroll behind it. Fixes https://github.com/patternfly/patternfly-react/issues/12635 --- packages/react-core/package.json | 2 +- .../src/components/Drawer/Drawer.tsx | 12 +- .../src/components/Drawer/DrawerContent.tsx | 2 +- .../components/Drawer/DrawerPanelContent.tsx | 50 ++++---- .../Drawer/__tests__/Drawer.test.tsx | 53 +++++++++ .../src/components/Drawer/examples/Drawer.md | 10 +- .../Drawer/examples/DrawerViewport.tsx | 112 ++++++++++++++++++ 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 +-- 12 files changed, 224 insertions(+), 43 deletions(-) create mode 100644 packages/react-core/src/components/Drawer/examples/DrawerViewport.tsx diff --git a/packages/react-core/package.json b/packages/react-core/package.json index 3472d07eaf1..02eb8864a24 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.41", "case-anything": "^3.1.2", "css": "^3.0.0", "fs-extra": "^11.3.3" diff --git a/packages/react-core/src/components/Drawer/Drawer.tsx b/packages/react-core/src/components/Drawer/Drawer.tsx index f26869bbe09..0d11c7271c1 100644 --- a/packages/react-core/src/components/Drawer/Drawer.tsx +++ b/packages/react-core/src/components/Drawer/Drawer.tsx @@ -23,6 +23,8 @@ export interface DrawerProps extends React.HTMLProps, OUIAProps isInline?: boolean; /** @beta Indicates if the drawer will have pill styles */ isPill?: boolean; + /** @beta Positions the drawer as fixed to fill the viewport. Place the drawer after Page as a sibling. */ + isViewport?: boolean; /** Indicates if the drawer will always show both content and panel. */ isStatic?: boolean; /** Position of the drawer panel. left and right are deprecated, use start and end instead. */ @@ -43,6 +45,7 @@ export interface DrawerContextProps { drawerRef?: React.RefObject; drawerContentRef?: React.RefObject; isInline: boolean; + isViewport: boolean; } export const DrawerContext = createContext>({ @@ -52,7 +55,8 @@ export const DrawerContext = createContext>({ position: 'end', drawerRef: null, drawerContentRef: null, - isInline: false + isInline: false, + isViewport: false }); export const Drawer: React.FunctionComponent = ({ @@ -61,6 +65,7 @@ export const Drawer: React.FunctionComponent = ({ isExpanded = false, isInline = false, isPill = false, + isViewport = false, isStatic = false, position = 'end', onExpand = () => {}, @@ -73,13 +78,16 @@ export const Drawer: React.FunctionComponent = ({ const drawerContentRef = useRef(undefined); return ( - +
{ /** Additional classes added to the Drawer. */ className?: string; - /** Content to be rendered in the drawer. */ + /** Content to be rendered in the drawer. Can be omitted to leave the content container empty. */ children?: React.ReactNode; /** Content rendered in the drawer panel. */ panelContent: React.ReactNode; diff --git a/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx b/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx index 14e2f40bce1..14b8c4eb056 100644 --- a/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx +++ b/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx @@ -100,7 +100,8 @@ export const DrawerPanelContent: React.FunctionComponent(undefined); const splitterRef = useRef(undefined); const [separatorValue, setSeparatorValue] = useState(0); - const { position, isExpanded, isStatic, onExpand, drawerRef, drawerContentRef, isInline } = useContext(DrawerContext); + const { position, isExpanded, isStatic, onExpand, drawerRef, drawerContentRef, isInline, isViewport } = + useContext(DrawerContext); const hidden = isStatic ? false : !isExpanded; const [isExpandedInternal, setIsExpandedInternal] = useState(!hidden); const [isFocusTrapActive, setIsFocusTrapActive] = useState(false); @@ -126,9 +127,22 @@ export const DrawerPanelContent: React.FunctionComponent { + if (isViewport) { + return drawerRef?.current ?? drawerContentRef?.current; + } + return drawerContentRef?.current ?? drawerRef?.current; + }; + const calcValueNow = () => { let splitterPos; let drawerSize; + const sizeEl = getSizeElement(); + + if (!sizeEl || !panel.current || !splitterRef.current || !drawerRef?.current) { + return 0; + } + const isRTL = getLanguageDirection(panel.current) === 'rtl'; if (isInline && (position === 'end' || position === 'right')) { @@ -149,37 +163,23 @@ export const DrawerPanelContent: React.FunctionComponent { ); expect(screen.getByTestId('drawer')).toHaveAttribute('data-ouia-safe', 'false'); }); + +test(`Does not render with ${styles.modifiers.viewport} class by default`, () => { + render( + + panel}> + content + + + ); + + expect(screen.getByTestId('drawer')).not.toHaveClass(styles.modifiers.viewport); +}); + +test(`Renders with ${styles.modifiers.viewport} when isViewport is specified`, () => { + render( + + panel}> + content + + + ); + + expect(screen.getByTestId('drawer')).toHaveClass(styles.modifiers.viewport); +}); + +test('Resizeable DrawerPanelContent without drawer content does not throw', async () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(); + + const panelContent = ( + + + drawer-panel + + + + + + ); + + const user = userEvent.setup(); + + render( + + + + ); + + await user.tab(); + await user.keyboard(`{${KeyTypes.ArrowLeft}}`); + + expect(consoleError).not.toHaveBeenCalled(); + consoleError.mockRestore(); +}); diff --git a/packages/react-core/src/components/Drawer/examples/Drawer.md b/packages/react-core/src/components/Drawer/examples/Drawer.md index 22d79215fe6..5b1f36fa1e3 100644 --- a/packages/react-core/src/components/Drawer/examples/Drawer.md +++ b/packages/react-core/src/components/Drawer/examples/Drawer.md @@ -13,7 +13,7 @@ propComponents: DrawerCloseButton, DrawerPanelDescription, DrawerPanelBody, - DrawerPanelFocusTrapObject, + DrawerPanelFocusTrapObject ] section: components --- @@ -156,3 +156,11 @@ To customize which element receives focus when the drawer panel expands, use the ```ts file="./DrawerPillInline.tsx" ``` + +### Viewport + +Use `isViewport` to position the drawer as `fixed` so it fills the viewport. Place the drawer after `` as a sibling. Omit children of `` so the content container stays empty; the empty container is still required so the panel can overlay the page. + +```ts file="./DrawerViewport.tsx" isFullscreen isBeta + +``` diff --git a/packages/react-core/src/components/Drawer/examples/DrawerViewport.tsx b/packages/react-core/src/components/Drawer/examples/DrawerViewport.tsx new file mode 100644 index 00000000000..b158eb226a4 --- /dev/null +++ b/packages/react-core/src/components/Drawer/examples/DrawerViewport.tsx @@ -0,0 +1,112 @@ +import { Fragment, useRef, useState } from 'react'; +import { + Backdrop, + Button, + Drawer, + DrawerActions, + DrawerCloseButton, + DrawerContent, + DrawerHead, + DrawerPanelContent, + Masthead, + MastheadBrand, + MastheadContent, + MastheadLogo, + MastheadMain, + MastheadToggle, + Page, + PageSection, + PageSidebar, + PageSidebarBody, + PageToggleButton, + Toolbar, + ToolbarContent, + ToolbarItem +} from '@patternfly/react-core'; + +export const DrawerViewport: React.FunctionComponent = () => { + const [isExpanded, setIsExpanded] = useState(true); + const drawerRef = useRef(null); + + const onExpand = () => { + drawerRef.current && drawerRef.current.focus(); + }; + + const onClick = () => { + setIsExpanded(!isExpanded); + }; + + const onCloseClick = () => { + setIsExpanded(false); + }; + + const onResize = (_event: MouseEvent | TouchEvent | React.KeyboardEvent, newWidth: number, id: string) => { + // eslint-disable-next-line no-console + console.log(`${id} has new width of: ${newWidth}`); + }; + + const headerToolbar = ( + + + + + + + + ); + + const masthead = ( + + + + + + + + Logo + + + + {headerToolbar} + + ); + + const sidebar = ( + + Navigation + + ); + + const panelContent = ( + + + + Drawer panel header + + + + + + + ); + + return ( + + + +

Viewport drawer example

+

+ The viewport drawer is a sibling of Page. DrawerContent is left empty so the panel overlays the page without + affecting page layout or scrolling. +

+
+
+ {isExpanded && } + + + +
+ ); +}; diff --git a/packages/react-docs/package.json b/packages/react-docs/package.json index 1e883306a45..2f917cd2980 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.41", "@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..d1c6fec44db 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.41", "@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..f3ab44d51b4 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.41", "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..010b12a0981 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.41", "fs-extra": "^11.3.3" } } diff --git a/yarn.lock b/yarn.lock index e6e11f857fd..bd7e19b785b 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.41": + version: 6.6.0-prerelease.41 + resolution: "@patternfly/patternfly@npm:6.6.0-prerelease.41" + checksum: 10c0/1b9fa30c2a8b5713d5bcbfc8194dcfff85c3cdc1028eb0b44945b8bbdd8e98d9cb348e2cd4be85de6784c595f79e6a9f0a839e4f1b58ac6004405cf956052b42 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.41" "@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.41" "@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.41" "@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.41" 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.41" fs-extra: "npm:^11.3.3" languageName: unknown linkType: soft