diff --git a/packages/react-core/package.json b/packages/react-core/package.json index cd5084773ec..c08c84ee853 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.75", + "@patternfly/patternfly": "6.5.0-prerelease.78", "case-anything": "^3.1.2", "css": "^3.0.0", "fs-extra": "^11.3.3" diff --git a/packages/react-core/src/components/Compass/Compass.tsx b/packages/react-core/src/components/Compass/Compass.tsx index f48d401d8e6..c152a015cc8 100644 --- a/packages/react-core/src/components/Compass/Compass.tsx +++ b/packages/react-core/src/components/Compass/Compass.tsx @@ -8,9 +8,17 @@ import compassBackgroundImageDark from '@patternfly/react-tokens/dist/esm/c_comp export interface CompassProps extends React.HTMLProps { /** Additional classes added to the Compass. */ className?: string; + /** The horizontal masthead content (e.g. ). This masthead will only render when dock content is passed and only at mobile viewports. */ + masthead?: React.ReactNode; /** Content of the docked navigation area of the layout */ dock?: React.ReactNode; - /** Content placed at the top of the layout */ + /** @beta Flag indicating the docked nav is expanded on mobile. Only applies when dock content is passed. */ + isDockExpanded?: boolean; + /** @beta Flag indicating the docked nav should display text on desktop. Only applies when dock content is passed, and + * will handle toggling the visibility of the text in individual isDocked components. + */ + isDockTextExpanded?: boolean; + /** Content placed at the top of the compass layout */ header?: React.ReactNode; /** Flag indicating if the header is expanded */ isHeaderExpanded?: boolean; @@ -40,7 +48,10 @@ export interface CompassProps extends React.HTMLProps { export const Compass: React.FunctionComponent = ({ className, + masthead, dock, + isDockExpanded, + isDockTextExpanded, header, isHeaderExpanded = true, sidebarStart, @@ -72,7 +83,18 @@ export const Compass: React.FunctionComponent = ({ {...props} style={{ ...props.style, ...backgroundImageStyles }} > - {dock &&
{dock}
} + {dock && masthead} + {dock && ( +
+ {dock} +
+ )} {header && (
{ + /** Additional classes added to the compass dock main container. */ + className?: string; + /** Content of the compass dock main container. */ + children?: React.ReactNode; +} + +export const CompassDockMain: React.FunctionComponent = ({ + className, + children, + ...props +}: CompassDockMainProps) => ( +
+ {children} +
+); + +CompassDockMain.displayName = 'CompassDockMain'; 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 175b1effe0f..91973224496 100644 --- a/packages/react-core/src/components/Compass/__tests__/Compass.test.tsx +++ b/packages/react-core/src/components/Compass/__tests__/Compass.test.tsx @@ -180,3 +180,33 @@ test(`Does not render with ${styles.modifiers.docked} class when dock is not pas render(); expect(screen.getByTestId('compass')).not.toHaveClass(styles.modifiers.docked); }); + +test('Does not render masthead content when dock is not passed', () => { + render(); + expect(screen.queryByText('Masthead content')).not.toBeInTheDocument(); +}); + +test('Renders masthead content when dock is passed', () => { + render(Masthead content
} dock={
Dock content
} />); + expect(screen.getByText('Masthead content')).toBeVisible(); +}); + +test(`Renders dock with ${styles.modifiers.expanded} class when isDockExpanded is true`, () => { + render(); + expect(screen.getByText('Dock content')).toHaveClass(styles.modifiers.expanded); +}); + +test(`Renders dock without ${styles.modifiers.expanded} class when isDockExpanded is false`, () => { + render(); + expect(screen.getByText('Dock content')).not.toHaveClass(styles.modifiers.expanded); +}); + +test(`Renders dock with ${styles.modifiers.textExpanded} class when isDockTextExpanded is true`, () => { + render(); + expect(screen.getByText('Dock content')).toHaveClass(styles.modifiers.textExpanded); +}); + +test(`Renders dock without ${styles.modifiers.textExpanded} class when isDockTextExpanded is false`, () => { + render(); + expect(screen.getByText('Dock content')).not.toHaveClass(styles.modifiers.textExpanded); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassDockMain.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassDockMain.test.tsx new file mode 100644 index 00000000000..18798a0b020 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/CompassDockMain.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from '@testing-library/react'; +import { CompassDockMain } from '../CompassDockMain'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; + +test('Renders without children', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('test-compass-dock-main').firstChild).toBeVisible(); +}); + +test('Renders with children', () => { + render(Test content); + expect(screen.getByText('Test content')).toBeVisible(); +}); + +test('Renders with custom class name when className prop is provided', () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass('custom-class'); +}); + +test(`Renders with default ${styles.compassDockMain} class`, () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass(styles.compassDockMain, { exact: true }); +}); + +test('Renders with additional props spread to the component', () => { + render(Test); + expect(screen.getByText('Test')).toHaveAttribute('id', 'custom-id'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render( + +
Test content
+
+ ); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassDockMain.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassDockMain.test.tsx.snap new file mode 100644 index 00000000000..b9198688601 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassDockMain.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + +
+
+ Test content +
+
+
+`; diff --git a/packages/react-core/src/components/Compass/index.ts b/packages/react-core/src/components/Compass/index.ts index 497058dc247..c229910c584 100644 --- a/packages/react-core/src/components/Compass/index.ts +++ b/packages/react-core/src/components/Compass/index.ts @@ -1,5 +1,6 @@ export * from './Compass'; export * from './CompassContent'; +export * from './CompassDockMain'; export * from './CompassHeader'; export * from './CompassHero'; export * from './CompassMainHeader'; diff --git a/packages/react-core/src/components/Page/Page.tsx b/packages/react-core/src/components/Page/Page.tsx index a8e0cabf7c5..37b97f709d3 100644 --- a/packages/react-core/src/components/Page/Page.tsx +++ b/packages/react-core/src/components/Page/Page.tsx @@ -24,9 +24,9 @@ export interface PageProps extends React.HTMLProps { 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 on desktop. Only applies when variant is docked, and will handle - * setting isTextExpanded on individual isDocked components. - * */ + /** @beta Flag indicating the docked nav should display text on desktop. Only applies when variant is docked, and + * will handle toggling the visibility of the text in individual isDocked components. + */ isDockTextExpanded?: boolean; /** The horizontal masthead content (e.g. ). When using the docked variant, this content will only render at mobile viewports. */ masthead?: React.ReactNode; diff --git a/packages/react-core/src/demos/Compass/Compass.md b/packages/react-core/src/demos/Compass/Compass.md index 48283de9a2e..81e2b148bcc 100644 --- a/packages/react-core/src/demos/Compass/Compass.md +++ b/packages/react-core/src/demos/Compass/Compass.md @@ -4,7 +4,7 @@ section: AI subsection: Generative UIs --- -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'; @@ -14,8 +14,11 @@ import FolderIcon from '@patternfly/react-icons/dist/esm/icons/folder-icon'; import RhUiQuestionMarkCircleFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-question-mark-circle-fill-icon'; import CloudIcon from '@patternfly/react-icons/dist/esm/icons/cloud-icon'; import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import imgAvatar from '../assets/avatarImg.svg'; +import ThIcon from '@patternfly/react-icons/dist/esm/icons/th-icon'; import pfLogo from '../assets/PF-IconLogo-color.svg'; +import globalBreakpointLg from '@patternfly/react-tokens/dist/esm/t_global_breakpoint_lg'; ## Demos diff --git a/packages/react-core/src/demos/Compass/examples/CompassDockDemo.tsx b/packages/react-core/src/demos/Compass/examples/CompassDockDemo.tsx index 35b6100ff4e..59f09ddbcf1 100644 --- a/packages/react-core/src/demos/Compass/examples/CompassDockDemo.tsx +++ b/packages/react-core/src/demos/Compass/examples/CompassDockDemo.tsx @@ -1,8 +1,9 @@ -import { useRef, useState } from 'react'; +import { useRef, useState, useEffect } from 'react'; import { Compass, CompassContent, CompassMainHeader, + CompassDockMain, Panel, PanelMain, PanelMainBody, @@ -15,28 +16,27 @@ import { MastheadBrand, MastheadContent, MastheadMain, + MastheadToggle, Masthead, Toolbar, ToolbarContent, ToolbarItem, ToolbarGroup, - Dropdown, - DropdownList, MenuToggle, - DropdownItem, Button, - ButtonVariant, - Avatar, Tooltip, - Divider + Divider, + PageToggleButton } 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 RhUiQuestionMarkCircleFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-question-mark-circle-fill-icon'; import CloudIcon from '@patternfly/react-icons/dist/esm/icons/cloud-icon'; import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import pfLogo from '../../assets/PF-IconLogo-color.svg'; -import imgAvatar from '../../assets/avatarImg.svg'; +import ThIcon from '@patternfly/react-icons/dist/esm/icons/th-icon'; +import globalBreakpointLg from '@patternfly/react-tokens/dist/esm/t_global_breakpoint_lg'; interface NavOnSelectProps { groupId: number | string; @@ -46,27 +46,26 @@ interface NavOnSelectProps { export const CompassDockDemo: React.FunctionComponent = () => { const [activeItem, setActiveItem] = useState(0); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isDockExpanded, setIsDockExpanded] = useState(false); + const [isDockTextExpanded, setIsDockTextExpanded] = useState(false); + const [isMobile, setIsMobile] = useState(false); - const onNavSelect = (_event: React.FormEvent, selectedItem: NavOnSelectProps) => { - typeof selectedItem.itemId === 'number' && setActiveItem(selectedItem.itemId); - }; + useEffect(() => { + const mobileBreakpoint = Number.parseInt(globalBreakpointLg.value) * 16; + const mediaQuery = window.matchMedia(`(max-width: ${mobileBreakpoint}px)`); + const handleResize = (e: MediaQueryListEvent | MediaQueryList) => { + setIsMobile(e.matches); + }; - const onDropdownToggle = () => { - setIsDropdownOpen((prevIsOpen) => !prevIsOpen); - }; + handleResize(mediaQuery); + mediaQuery.addEventListener('change', handleResize); - const onDropdownSelect = () => { - setIsDropdownOpen(false); - }; + return () => mediaQuery.removeEventListener('change', handleResize); + }, []); - const userDropdownItems = [ - <> - My profile - User management - Logout - - ]; + const onNavSelect = (_event: React.FormEvent, selectedItem: NavOnSelectProps) => { + typeof selectedItem.itemId === 'number' && setActiveItem(selectedItem.itemId); + }; const navItem1Ref = useRef(null); const navItem2Ref = useRef(null); @@ -74,127 +73,306 @@ export const CompassDockDemo: React.FunctionComponent = () => { const navItem4Ref = useRef(null); const settingsRef = useRef(null); const helpRef = useRef(null); - const userMenuRef = useRef(null); + const appsRef = useRef(null); + const mobileToggleRef = useRef(null); + const dockedToggleRef = useRef(null); - const dockContent = ( - + const onMobileToggle = () => { + setIsDockExpanded(!isDockExpanded); + + setTimeout(() => { + dockedToggleRef.current?.focus(); + }, 200); + }; + + const onToggleDock = () => { + if (isMobile) { + setIsDockExpanded(!isDockExpanded); + + if (isDockExpanded) { + setTimeout(() => { + mobileToggleRef.current?.focus(); + }, 200); + } + } else { + setIsDockTextExpanded(!isDockTextExpanded); + } + }; + + const mobileTextLogo = ( + + PatternFly + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + + const dockTextLogo = ( + + PatternFly + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + + // Mobile masthead - shown on mobile viewports only, hidden on desktop + const mobileMasthead = ( + + + + - }> - - + }>{mobileTextLogo} - - + - - - - - - + + + ) : ( + + + + )} + + + {isDockTextExpanded || isDockExpanded ? ( + } - /> - - - - - - setIsDropdownOpen(isOpen)} - toggle={{ - toggleNode: ( - + isDocked + aria-label="Help" + > + Help + + ) : ( + } + ref={helpRef} variant="plain" - aria-label="User menu" - /> + icon={} + isDocked + aria-label="Help" + > + Help + - ), - toggleRef: userMenuRef - }} - > - {userDropdownItems} - - - - - - + )} + + + + + + + ); const mainContent = ( @@ -212,7 +390,10 @@ export const CompassDockDemo: React.FunctionComponent = () => { return ( { const [activeItem, setActiveItem] = useState(1); const [isDockExpanded, setIsDockExpanded] = useState(false); const [isDockTextExpanded, setIsDockTextExpanded] = useState(false); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const mobileBreakpoint = Number.parseInt(globalBreakpointXl.value) * 16; + const mediaQuery = window.matchMedia(`(max-width: ${mobileBreakpoint}px)`); + const handleResize = (e: MediaQueryListEvent | MediaQueryList) => { + setIsMobile(e.matches); + }; + + handleResize(mediaQuery); + mediaQuery.addEventListener('change', handleResize); + + return () => mediaQuery.removeEventListener('change', handleResize); + }, []); const onNavSelect = (_event: React.FormEvent, selectedItem: NavOnSelectProps) => { typeof selectedItem.itemId === 'number' && setActiveItem(selectedItem.itemId); @@ -180,12 +195,14 @@ export const NavDockedNav: React.FunctionComponent = () => { }; const onToggleDock = () => { - if (isDockExpanded) { + if (isMobile) { setIsDockExpanded(!isDockExpanded); - setTimeout(() => { - mobileToggleRef.current?.focus(); - }, 200); + if (isDockExpanded) { + setTimeout(() => { + mobileToggleRef.current?.focus(); + }, 200); + } } else { setIsDockTextExpanded(!isDockTextExpanded); } @@ -223,7 +240,12 @@ export const NavDockedNav: React.FunctionComponent = () => { // Docked masthead - vertical navigation sidebar const dockedMasthead = ( - + @@ -340,7 +362,7 @@ export const NavDockedNav: React.FunctionComponent = () => { )} - {isDockTextExpanded ? ( + {isDockTextExpanded || isDockExpanded ? (