From 42b7e5422aeda65f5aeca04fe1cae06fb87411b9 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 12 Dec 2025 15:07:00 -0500 Subject: [PATCH 01/17] feat(docked nav): add support for docked nav layout --- packages/react-core/package.json | 2 +- .../src/components/Compass/Compass.tsx | 68 +++-- .../Compass/__tests__/Compass.test.tsx | 10 + .../components/Compass/examples/Compass.md | 23 +- .../Compass/examples/CompassDockDemo.tsx | 223 +++++++++++++++ .../Compass/examples/CompassDockLayout.tsx | 25 ++ .../components/Compass/examples/compass.css | 12 + .../src/components/Masthead/Masthead.tsx | 4 + .../Masthead/__tests__/Masthead.test.tsx | 26 +- .../react-core/src/components/Nav/Nav.tsx | 5 +- .../react-core/src/components/Nav/NavItem.tsx | 25 +- .../src/components/Nav/__tests__/Nav.test.tsx | 15 + .../react-core/src/components/Page/Page.tsx | 6 +- .../components/Page/__tests__/Page.test.tsx | 17 ++ .../src/components/Toolbar/Toolbar.tsx | 4 + packages/react-core/src/demos/Page.md | 13 +- .../src/demos/assets/PF-IconLogo-color.svg | 17 ++ .../src/demos/examples/Page/PageDockedNav.tsx | 269 ++++++++++++++++++ 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 +- 23 files changed, 741 insertions(+), 49 deletions(-) create mode 100644 packages/react-core/src/components/Compass/examples/CompassDockDemo.tsx create mode 100644 packages/react-core/src/components/Compass/examples/CompassDockLayout.tsx create mode 100644 packages/react-core/src/demos/assets/PF-IconLogo-color.svg create mode 100644 packages/react-core/src/demos/examples/Page/PageDockedNav.tsx diff --git a/packages/react-core/package.json b/packages/react-core/package.json index afd48725963..249fb001c09 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.5.0-prerelease.27", + "@patternfly/patternfly": "6.5.0-prerelease.32", "case-anything": "^3.1.2", "css": "^3.0.0", "fs-extra": "^11.3.0" diff --git a/packages/react-core/src/components/Compass/Compass.tsx b/packages/react-core/src/components/Compass/Compass.tsx index 74c75fd9cf7..619d034d4ac 100644 --- a/packages/react-core/src/components/Compass/Compass.tsx +++ b/packages/react-core/src/components/Compass/Compass.tsx @@ -8,6 +8,8 @@ import compassBackgroundImageDark from '@patternfly/react-tokens/dist/esm/c_comp export interface CompassProps extends React.HTMLProps { /** Additional classes added to the Compass. */ className?: string; + /** Content of the docked navigation area of the layout */ + dock?: React.ReactNode; /** Content placed at the top of the layout */ header?: React.ReactNode; /** Flag indicating if the header is expanded */ @@ -38,6 +40,7 @@ export interface CompassProps extends React.HTMLProps { export const Compass: React.FunctionComponent = ({ className, + dock, header, isHeaderExpanded = true, sidebarStart, @@ -64,32 +67,45 @@ export const Compass: React.FunctionComponent = ({ } const compassContent = ( -
-
- {header} -
-
- {sidebarStart} -
-
{main}
-
- {sidebarEnd} -
-
- {footer} -
+
+ {dock &&
{dock}
} + {header && ( +
+ {header} +
+ )} + {sidebarStart && ( +
+ {sidebarStart} +
+ )} + {main &&
{main}
} + {sidebarEnd && ( +
+ {sidebarEnd} +
+ )} + {footer && ( +
+ {footer} +
+ )}
); diff --git a/packages/react-core/src/components/Compass/__tests__/Compass.test.tsx b/packages/react-core/src/components/Compass/__tests__/Compass.test.tsx index 932dcb61233..3ef5b23583c 100644 --- a/packages/react-core/src/components/Compass/__tests__/Compass.test.tsx +++ b/packages/react-core/src/components/Compass/__tests__/Compass.test.tsx @@ -170,3 +170,13 @@ test('Matches the snapshot with drawer', () => { ); expect(asFragment()).toMatchSnapshot(); }); + +test(`Renders with ${styles.modifiers.dock} class when dock is passed`, () => { + render(Dock content
} data-testid="compass" />); + expect(screen.getByTestId('compass')).toHaveClass(styles.modifiers.dock); +}); + +test(`Does not render with ${styles.modifiers.dock} class when dock is not passed`, () => { + render(); + expect(screen.getByTestId('compass')).not.toHaveClass(styles.modifiers.dock); +}); diff --git a/packages/react-core/src/components/Compass/examples/Compass.md b/packages/react-core/src/components/Compass/examples/Compass.md index adbf3f21f61..d316f752ce2 100644 --- a/packages/react-core/src/components/Compass/examples/Compass.md +++ b/packages/react-core/src/components/Compass/examples/Compass.md @@ -17,11 +17,18 @@ propComponents: ] --- -import { useRef, useState } from 'react'; +import { useRef, useState, useEffect } from 'react'; import PlayIcon from '@patternfly/react-icons/dist/esm/icons/play-icon'; import OutlinedPlusSquare from '@patternfly/react-icons/dist/esm/icons/outlined-plus-square-icon'; import OutlinedCopy from '@patternfly/react-icons/dist/esm/icons/outlined-copy-icon'; import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon'; +import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; +import FolderIcon from '@patternfly/react-icons/dist/esm/icons/folder-icon'; +import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; +import CloudIcon from '@patternfly/react-icons/dist/esm/icons/cloud-icon'; +import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon'; +import imgAvatar from '../../assets/avatarImg.svg'; +import pfLogo from '../../assets/PF-IconLogo-color.svg'; import './compass.css'; @@ -51,6 +58,20 @@ When `footer` is used, its content will fill the width of the screen. By default ``` +### With docked nav + +As an alternative navigation, a `CompassDock` component may be passed to `Compass` via the `dock` prop. This component will allocate a thin sidebar intended for icons to the start of the screen. The `CompassDock` component has three sub-areas, from top to bottom: logo, main, and tools. Typically a `Brand` or other logo should be passed to the `logo` prop. The `main` and `tools` are flexible and can be passed `Nav`, `ActionList`, or `Toolbar` vertical variants depending on the use case. Account or profile avatars and links would typically be passed as part of the `tools` section at the bottom of the page. + +```ts file="CompassDockLayout.tsx" + +``` + +### Docked nav demo + +```ts isFullscreen file="CompassDockDemo.tsx" + +``` + ## Composable structure When building a more custom implementation with Compass components, there are some intended or expected structures that must remain present. diff --git a/packages/react-core/src/components/Compass/examples/CompassDockDemo.tsx b/packages/react-core/src/components/Compass/examples/CompassDockDemo.tsx new file mode 100644 index 00000000000..2e15163ac0c --- /dev/null +++ b/packages/react-core/src/components/Compass/examples/CompassDockDemo.tsx @@ -0,0 +1,223 @@ +import { useRef, useState, useEffect } from 'react'; +import { + Compass, + CompassContent, + CompassMainHeader, + CompassPanel, + Title, + NavItem, + NavList, + Nav, + Brand, + MastheadLogo, + MastheadBrand, + MastheadContent, + MastheadMain, + Masthead, + Toolbar, + ToolbarContent, + ToolbarItem, + ToolbarGroup, + Dropdown, + DropdownList, + MenuToggle, + MenuToggleElement, + DropdownItem, + Button, + ButtonVariant, + Avatar, + Tooltip, + Divider +} from '@patternfly/react-core'; +import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; +import FolderIcon from '@patternfly/react-icons/dist/esm/icons/folder-icon'; +import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; +import CloudIcon from '@patternfly/react-icons/dist/esm/icons/cloud-icon'; +import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon'; +import pfLogo from '../../assets/PF-IconLogo-color.svg'; +import imgAvatar from '../../assets/avatarImg.svg'; + +interface NavOnSelectProps { + groupId: number | string; + itemId: number | string; + to: string; +} + +export const CompassDockDemo: React.FunctionComponent = () => { + const [activeItem, setActiveItem] = useState(0); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + const toggleRef = useRef(null); + + const onNavSelect = (_event: React.FormEvent, selectedItem: NavOnSelectProps) => { + typeof selectedItem.itemId === 'number' && setActiveItem(selectedItem.itemId); + }; + + const onDropdownToggle = () => { + setIsDropdownOpen(!isDropdownOpen); + }; + + const onDropdownSelect = () => { + setIsDropdownOpen(!isDropdownOpen); + }; + + const handleMenuKeys = (event: KeyboardEvent) => { + if (!isOpen) { + return; + } + if (menuRef.current?.contains(event.target as Node) || toggleRef.current?.contains(event.target as Node)) { + if (event.key === 'Escape') { + setIsOpen(!isOpen); + toggleRef.current?.focus(); + } + } + }; + + const handleClickOutside = (event: MouseEvent) => { + if (isOpen && !menuRef.current?.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + useEffect(() => { + window.addEventListener('keydown', handleMenuKeys); + window.addEventListener('click', handleClickOutside); + + return () => { + window.removeEventListener('keydown', handleMenuKeys); + window.removeEventListener('click', handleClickOutside); + }; + }, [isOpen, menuRef]); + + const userDropdownItems = [ + <> + My profile + User management + Logout + + ]; + + const navItem1Ref = useRef(null); + const navItem2Ref = useRef(null); + const navItem3Ref = useRef(null); + const navItem4Ref = useRef(null); + const dockContent = ( + + + + }> + + + + + + + + + + + + + + + + + + +