From 10b8bcd99e4b6c2b97961841ecf708879130eb74 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Wed, 8 Apr 2026 09:53:35 -0400 Subject: [PATCH] feat(Page): added responsive docked nav --- .../src/components/Button/Button.tsx | 8 + .../src/components/Masthead/MastheadLogo.tsx | 9 +- .../src/components/MenuToggle/MenuToggle.tsx | 10 + .../react-core/src/components/Nav/Nav.tsx | 4 + .../react-core/src/components/Page/Page.tsx | 23 +- packages/react-core/src/demos/Page.md | 2 + .../src/demos/examples/Page/PageDockedNav.tsx | 430 +++++++++++++----- 7 files changed, 369 insertions(+), 117 deletions(-) diff --git a/packages/react-core/src/components/Button/Button.tsx b/packages/react-core/src/components/Button/Button.tsx index 86b40f66826..9bf88985114 100644 --- a/packages/react-core/src/components/Button/Button.tsx +++ b/packages/react-core/src/components/Button/Button.tsx @@ -109,6 +109,10 @@ export interface ButtonProps extends Omit, 'r hamburgerVariant?: 'expand' | 'collapse'; /** @beta Flag indicating the button is a circle button. Intended for buttons that only contain an icon.. */ isCircle?: boolean; + /** @beta Flag indicating the button is a dock variant button. For use in docked navigation. */ + isDock?: boolean; + /** @beta Flag indicating the dock button should display text. Only applies when isDock is true. */ + isTextExpanded?: boolean; /** @hide Forwarded ref */ innerRef?: React.Ref; /** Adds count number to button */ @@ -134,6 +138,8 @@ const ButtonBase: React.FunctionComponent = ({ isHamburger, hamburgerVariant, isCircle, + isDock = false, + isTextExpanded = false, spinnerAriaValueText, spinnerAriaLabelledBy, spinnerAriaLabel, @@ -265,6 +271,8 @@ const ButtonBase: React.FunctionComponent = ({ size === ButtonSize.sm && styles.modifiers.small, size === ButtonSize.lg && styles.modifiers.displayLg, isCircle && styles.modifiers.circle, + isDock && styles.modifiers.dock, + isTextExpanded && styles.modifiers.textExpanded, className )} disabled={isButtonElement ? isDisabled : null} diff --git a/packages/react-core/src/components/Masthead/MastheadLogo.tsx b/packages/react-core/src/components/Masthead/MastheadLogo.tsx index 0f9f8391e35..49e1f43d715 100644 --- a/packages/react-core/src/components/Masthead/MastheadLogo.tsx +++ b/packages/react-core/src/components/Masthead/MastheadLogo.tsx @@ -11,12 +11,15 @@ export interface MastheadLogoProps extends React.DetailedHTMLProps< className?: string; /** Component type of the masthead logo. */ component?: React.ElementType | React.ComponentType; + /** @beta Flag indicating the logo is a compact variant. Used in docked layouts. */ + isCompact?: boolean; } export const MastheadLogo: React.FunctionComponent = ({ children, className, component, + isCompact = false, ...props }: MastheadLogoProps) => { let Component = component as any; @@ -28,7 +31,11 @@ export const MastheadLogo: React.FunctionComponent = ({ } } return ( - + {children} ); diff --git a/packages/react-core/src/components/MenuToggle/MenuToggle.tsx b/packages/react-core/src/components/MenuToggle/MenuToggle.tsx index 565b478a928..4cdf5b4d609 100644 --- a/packages/react-core/src/components/MenuToggle/MenuToggle.tsx +++ b/packages/react-core/src/components/MenuToggle/MenuToggle.tsx @@ -51,6 +51,10 @@ export interface MenuToggleProps isCircle?: boolean; /** Flag indicating whether the toggle is a settings toggle. This will override the icon property */ isSettings?: boolean; + /** @beta Flag indicating the toggle is a dock variant. For use in docked navigation. */ + isDock?: boolean; + /** @beta Flag indicating the dock toggle should display text. Only applies when isDock is true. */ + isTextExpanded?: boolean; /** Elements to display before the toggle button. When included, renders the menu toggle as a split button. */ splitButtonItems?: React.ReactNode[]; /** Variant styles of the menu toggle */ @@ -85,6 +89,8 @@ class MenuToggleBase extends Component { isFullHeight: false, isPlaceholder: false, isCircle: false, + isDock: false, + isTextExpanded: false, size: 'default', ouiaSafe: true }; @@ -102,6 +108,8 @@ class MenuToggleBase extends Component { isPlaceholder, isCircle, isSettings, + isDock, + isTextExpanded, splitButtonItems, variant, status, @@ -185,6 +193,8 @@ class MenuToggleBase extends Component { isDisabled && styles.modifiers.disabled, isPlaceholder && styles.modifiers.placeholder, isSettings && styles.modifiers.settings, + isDock && styles.modifiers.dock, + isTextExpanded && styles.modifiers.textExpanded, size === MenuToggleSize.sm && styles.modifiers.small, className ); diff --git a/packages/react-core/src/components/Nav/Nav.tsx b/packages/react-core/src/components/Nav/Nav.tsx index 8a908a90584..28e32bf25b7 100644 --- a/packages/react-core/src/components/Nav/Nav.tsx +++ b/packages/react-core/src/components/Nav/Nav.tsx @@ -39,6 +39,8 @@ export interface NavProps 'aria-label'?: string; /** The nav variant to use. Docked is in beta. */ variant?: 'default' | 'horizontal' | 'horizontal-subnav' | 'docked'; + /** @beta Flag indicating the docked nav should display text. Only applies when variant is docked. */ + isTextExpanded?: boolean; /** Value to overwrite the randomly generated data-ouia-component-id.*/ ouiaId?: number | string; /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ @@ -119,6 +121,7 @@ class Nav extends Component { className?: string; /** @beta Indicates the layout variant */ variant?: 'default' | 'docked'; + /** @beta Flag indicating the docked nav is expanded on mobile. Only applies when variant is docked. */ + isDockExpanded?: boolean; + /** @beta Flag indicating the docked nav should display text. Only applies when variant is docked. */ + isDockTextExpanded?: boolean; /** Masthead component (e.g. ) */ masthead?: React.ReactNode; + dockedMasthead?: React.ReactNode; /** Sidebar component for a side nav, recommended to be a PageSidebar. If set to null, the page grid layout * will render without a sidebar. */ @@ -232,7 +237,10 @@ class Page extends Component { className, children, variant, + isDockExpanded = false, + isDockTextExpanded = false, masthead, + dockedMasthead, sidebar, notificationDrawer, isNotificationDrawerExpanded, @@ -349,9 +357,18 @@ class Page extends Component { > {skipToContent} {variant === 'docked' ? ( -
-
{masthead}
-
+ <> + {masthead} +
+
{dockedMasthead}
+
+ ) : ( masthead )} diff --git a/packages/react-core/src/demos/Page.md b/packages/react-core/src/demos/Page.md index 202cd65d792..b4acc662417 100644 --- a/packages/react-core/src/demos/Page.md +++ b/packages/react-core/src/demos/Page.md @@ -18,6 +18,8 @@ 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 '@patternfly/react-core/src/demos/assets/PF-HorizontalLogo-Color.svg'; import pfIconLogo from '@patternfly/react-core/src/demos/assets/PF-IconLogo-color.svg'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; +import ThIcon from '@patternfly/react-icons/dist/esm/icons/th-icon'; - All examples set the `isManagedSidebar` prop on the Page component to have the sidebar automatically close for smaller screen widths. You can also manually control this behavior by not adding the `isManagedSidebar` prop and instead: 1. Add an onNavToggle callback to PageHeader diff --git a/packages/react-core/src/demos/examples/Page/PageDockedNav.tsx b/packages/react-core/src/demos/examples/Page/PageDockedNav.tsx index 962c2210af3..3f68cae39d9 100644 --- a/packages/react-core/src/demos/examples/Page/PageDockedNav.tsx +++ b/packages/react-core/src/demos/examples/Page/PageDockedNav.tsx @@ -1,18 +1,13 @@ import { useRef, useState } from 'react'; import { - Avatar, Brand, Breadcrumb, BreadcrumbItem, Button, - ButtonVariant, Card, CardBody, Content, Divider, - Dropdown, - DropdownItem, - DropdownList, Gallery, GalleryItem, Masthead, @@ -20,12 +15,14 @@ import { MastheadLogo, MastheadContent, MastheadBrand, + MastheadToggle, MenuToggle, Nav, NavItem, NavList, Page, PageSection, + PageToggleButton, SkipToContent, Toolbar, ToolbarContent, @@ -38,7 +35,8 @@ import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; import FolderIcon from '@patternfly/react-icons/dist/esm/icons/folder-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 '@patternfly/react-core/src/components/assets/avatarImg.svg'; +import ThIcon from '@patternfly/react-icons/dist/esm/icons/th-icon'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import pfIconLogo from '@patternfly/react-core/src/demos/assets/PF-IconLogo-color.svg'; interface NavOnSelectProps { @@ -48,20 +46,109 @@ interface NavOnSelectProps { } export const PageDockedNav: React.FunctionComponent = () => { - const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [activeItem, setActiveItem] = useState(1); + const [isDockExpanded, setIsDockExpanded] = useState(false); + const [isDockTextExpanded, setIsDockTextExpanded] = useState(false); const onNavSelect = (_event: React.FormEvent, selectedItem: NavOnSelectProps) => { typeof selectedItem.itemId === 'number' && setActiveItem(selectedItem.itemId); }; - const onDropdownToggle = () => { - setIsDropdownOpen((prevIsOpen) => !prevIsOpen); - }; + const mobileTextLogo = ( + + PatternFly + + + + + + + + + + + + + + + + + + + + + + + + + + + ); - const onDropdownSelect = () => { - setIsDropdownOpen(false); - }; + const dockTextLogo = ( + + PatternFly + + + + + + + + + + + + + + + + + + + + + + + + + + + ); const dashboardBreadcrumb = ( @@ -74,142 +161,254 @@ export const PageDockedNav: React.FunctionComponent = () => { ); - const userDropdownItems = [ - <> - My profile - User management - Logout - - ]; - const navItem1Ref = useRef(null); const navItem2Ref = useRef(null); const navItem3Ref = useRef(null); const navItem4Ref = useRef(null); + const appsRef = useRef(null); const settingsRef = useRef(null); const helpRef = useRef(null); - const userMenuRef = useRef(null); + const mobileToggleRef = useRef(null); + const dockedToggleRef = useRef(null); + + const onToggleDock = () => { + const willBeExpanded = !isDockExpanded; + setIsDockExpanded(willBeExpanded); + setIsDockTextExpanded(!isDockTextExpanded); - const masthead = ( - + // Shift focus between mobile and docked toggle buttons + setTimeout(() => { + if (willBeExpanded) { + // Opening: focus the docked toggle button + dockedToggleRef.current?.focus(); + } else { + // Closing: focus the mobile toggle button + mobileToggleRef.current?.focus(); + } + }, 200); + }; + + // Mobile masthead - shown on mobile viewports only, hidden on desktop + const mobileMasthead = ( + + + + }> + {mobileTextLogo} + {/* */} + {/* */} + + + + + + + + + ) : ( - - - + )} + + + {isDockTextExpanded ? ( + } + isDock + isTextExpanded={isDockTextExpanded} + aria-label="Help" + > + Help + + ) : ( -