diff --git a/packages/react-core/package.json b/packages/react-core/package.json index afd48725963..e516fb47463 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.33", "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..a1f9d597df6 100644 --- a/packages/react-core/src/components/Compass/examples/Compass.md +++ b/packages/react-core/src/components/Compass/examples/Compass.md @@ -17,7 +17,7 @@ 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'; @@ -51,6 +51,12 @@ When `footer` is used, its content will fill the width of the screen. By default ``` +### With docked nav + +```ts file="CompassDockLayout.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/CompassDockLayout.tsx b/packages/react-core/src/components/Compass/examples/CompassDockLayout.tsx new file mode 100644 index 00000000000..9d7357467c7 --- /dev/null +++ b/packages/react-core/src/components/Compass/examples/CompassDockLayout.tsx @@ -0,0 +1,25 @@ +import { + Compass, + CompassContent, + CompassMainHeader, + CompassPanel, + CompassMainHeaderContent +} from '@patternfly/react-core'; +import './compass.css'; + +export const CompassBasic: React.FunctionComponent = () => { + const dockContent =
Content
; + const mainContent = ( + + + + +
Content title
+
+
+
+
Content
+
+ ); + return ; +}; diff --git a/packages/react-core/src/components/Compass/examples/compass.css b/packages/react-core/src/components/Compass/examples/compass.css index c5583d03c67..26242064ee7 100644 --- a/packages/react-core/src/components/Compass/examples/compass.css +++ b/packages/react-core/src/components/Compass/examples/compass.css @@ -20,4 +20,16 @@ inset: 0; border: var(--pf-t--global--border--width--regular) dashed var(--pf-t--global--border--color--default); pointer-events: none; +} + +#ws-react-a-compass-with-docked-nav [class*="pf-v6-c-compass"] { + position: relative; +} + +#ws-react-a-compass-with-docked-nav [class*="pf-v6-c-compass"]:not([class*="footer"])::after { + content: ""; + position: absolute; + inset: 0; + border: var(--pf-t--global--border--width--regular) dashed var(--pf-t--global--border--color--default); + pointer-events: none; } \ No newline at end of file diff --git a/packages/react-core/src/components/Masthead/Masthead.tsx b/packages/react-core/src/components/Masthead/Masthead.tsx index d9b1001759c..a56af1c31a0 100644 --- a/packages/react-core/src/components/Masthead/Masthead.tsx +++ b/packages/react-core/src/components/Masthead/Masthead.tsx @@ -27,6 +27,8 @@ export interface MastheadProps extends React.DetailedHTMLProps = ({ @@ -36,6 +38,7 @@ export const Masthead: React.FunctionComponent = ({ md: 'inline' }, inset, + variant = 'default', ...props }: MastheadProps) => { const { width, getBreakpoint } = useContext(PageContext); @@ -43,6 +46,7 @@ export const Masthead: React.FunctionComponent = ({
{ test('verify basic', () => { @@ -71,6 +72,29 @@ describe('Masthead', () => { expect(asFragment()).toMatchSnapshot(); }); }); + + test(`Renders with ${styles.modifiers.docked} class when variant is docked`, () => { + render( + + test + + ); + expect(screen.getByTestId('masthead')).toHaveClass(styles.modifiers.docked); + }); + + test(`Does not render with ${styles.modifiers.docked} class when variant is default`, () => { + render( + + test + + ); + expect(screen.getByTestId('masthead')).not.toHaveClass(styles.modifiers.docked); + }); + + test(`Does not render with ${styles.modifiers.docked} class when variant is not passed`, () => { + render(test); + expect(screen.getByTestId('masthead')).not.toHaveClass(styles.modifiers.docked); + }); }); describe('MastheadLogo', () => { diff --git a/packages/react-core/src/components/Nav/Nav.tsx b/packages/react-core/src/components/Nav/Nav.tsx index 6907d6f3617..db70864202c 100644 --- a/packages/react-core/src/components/Nav/Nav.tsx +++ b/packages/react-core/src/components/Nav/Nav.tsx @@ -35,8 +35,8 @@ export interface NavProps ) => void; /** Accessible label for the nav when there are multiple navs on the page */ 'aria-label'?: string; - /** For horizontal navs */ - variant?: 'default' | 'horizontal' | 'horizontal-subnav'; + /** The nav variant to use. Docked is in beta. */ + variant?: 'default' | 'horizontal' | 'horizontal-subnav' | 'docked'; /** 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. */ @@ -154,6 +154,7 @@ class Nav extends Component< className={css( styles.nav, isHorizontal && styles.modifiers.horizontal, + variant === 'docked' && styles.modifiers.docked, variant === 'horizontal-subnav' && styles.modifiers.subnav, this.state.isScrollable && styles.modifiers.scrollable, className diff --git a/packages/react-core/src/components/Nav/NavItem.tsx b/packages/react-core/src/components/Nav/NavItem.tsx index aea1b9b4ae6..c3f097e577a 100644 --- a/packages/react-core/src/components/Nav/NavItem.tsx +++ b/packages/react-core/src/components/Nav/NavItem.tsx @@ -1,4 +1,14 @@ -import { cloneElement, Fragment, isValidElement, useContext, useEffect, useRef, useState } from 'react'; +import { + cloneElement, + Fragment, + isValidElement, + useContext, + useEffect, + useRef, + useState, + forwardRef, + MutableRefObject +} from 'react'; import styles from '@patternfly/react-styles/css/components/Nav/nav'; import menuStyles from '@patternfly/react-styles/css/components/Menu/menu'; import dividerStyles from '@patternfly/react-styles/css/components/Divider/divider'; @@ -42,9 +52,13 @@ export interface NavItemProps extends Omit, ' 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. */ ouiaSafe?: boolean; + /** React ref for the anchor element within the nav item. */ + anchorRef?: React.Ref; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } -export const NavItem: React.FunctionComponent = ({ +const NavItemBase: React.FunctionComponent = ({ children, styleChildren = true, className, @@ -61,13 +75,16 @@ export const NavItem: React.FunctionComponent = ({ ouiaSafe, zIndex = 9999, icon, + innerRef, + anchorRef, ...props }: NavItemProps) => { const { flyoutRef, setFlyoutRef, navRef } = useContext(NavContext); const { isSidebarOpen } = useContext(PageSidebarContext); const [flyoutTarget, setFlyoutTarget] = useState(null); const [isHovered, setIsHovered] = useState(false); - const ref = useRef(undefined); + const _ref = useRef(undefined); + const ref = (innerRef as MutableRefObject) || _ref; const flyoutVisible = ref === flyoutRef; const popperRef = useRef(undefined); const hasFlyout = flyout !== undefined; @@ -180,6 +197,7 @@ export const NavItem: React.FunctionComponent = ({ const preventLinkDefault = preventDefault || !to; return ( context.onSelect(e, groupId, itemId, to, preventLinkDefault, onClick)} className={css( @@ -208,6 +226,7 @@ export const NavItem: React.FunctionComponent = ({ className: css(styles.navLink, isActive && styles.modifiers.current, child.props && child.props.className) }), tabIndex: child.props.tabIndex || tabIndex, + ref: anchorRef, children: hasFlyout ? ( {child.props.children} @@ -267,4 +286,9 @@ export const NavItem: React.FunctionComponent = ({ return navItem; }; + +export const NavItem = forwardRef((props, ref) => ( + +)); + NavItem.displayName = 'NavItem'; diff --git a/packages/react-core/src/components/Nav/__tests__/Nav.test.tsx b/packages/react-core/src/components/Nav/__tests__/Nav.test.tsx index 6153183accb..81bd5b3f57f 100644 --- a/packages/react-core/src/components/Nav/__tests__/Nav.test.tsx +++ b/packages/react-core/src/components/Nav/__tests__/Nav.test.tsx @@ -259,4 +259,19 @@ describe('Nav', () => { ); expect(screen.getAllByText('this is an icon')[0].parentElement).toHaveClass(styles.navLinkIcon); }); + + test(`Renders with ${styles.modifiers.docked} class when variant is docked`, () => { + renderNav( + + ); + expect(screen.getByTestId('docked-nav')).toHaveClass(styles.modifiers.docked); + }); }); diff --git a/packages/react-core/src/components/Page/Page.tsx b/packages/react-core/src/components/Page/Page.tsx index 65c8589c9a6..d2d4cab4e67 100644 --- a/packages/react-core/src/components/Page/Page.tsx +++ b/packages/react-core/src/components/Page/Page.tsx @@ -20,6 +20,8 @@ export interface PageProps extends React.HTMLProps { children?: React.ReactNode; /** Additional classes added to the page layout */ className?: string; + /** @beta Indicates the layout variant */ + variant?: 'default' | 'docked'; /** Masthead component (e.g. ) */ masthead?: React.ReactNode; /** Sidebar component for a side nav, recommended to be a PageSidebar. If set to null, the page grid layout @@ -229,6 +231,7 @@ class Page extends Component { isBreadcrumbWidthLimited, className, children, + variant, masthead, sidebar, notificationDrawer, @@ -336,6 +339,7 @@ class Page extends Component { {...rest} className={css( styles.page, + variant === 'docked' && styles.modifiers.dock, width !== null && height !== null && 'pf-m-resize-observer', width !== null && `pf-m-breakpoint-${getBreakpoint(width)}`, height !== null && `pf-m-height-breakpoint-${getVerticalBreakpoint(height)}`, @@ -344,7 +348,7 @@ class Page extends Component { )} > {skipToContent} - {masthead} + {variant === 'docked' ?
{masthead}
: masthead} {sidebar} {notificationDrawer && (
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 87bc72d9396..78ca03fb714 100644 --- a/packages/react-core/src/components/Page/__tests__/Page.test.tsx +++ b/packages/react-core/src/components/Page/__tests__/Page.test.tsx @@ -389,4 +389,21 @@ describe('Page', () => { expect(screen.getByRole('main').parentElement).not.toHaveClass(styles.modifiers.noFill); }); + + test(`Renders with ${styles.modifiers.dock} class when variant is docked`, () => { + render(); + + expect(screen.getByTestId('page')).toHaveClass(styles.modifiers.dock); + }); + + test(`Does not render with ${styles.modifiers.dock} class when variant is default`, () => { + render(); + + expect(screen.getByTestId('page')).not.toHaveClass(styles.modifiers.dock); + }); + + test(`Does not render with ${styles.modifiers.dock} class when variant is not passed`, () => { + render(); + expect(screen.getByTestId('page')).not.toHaveClass(styles.modifiers.dock); + }); }); diff --git a/packages/react-core/src/components/Toolbar/Toolbar.tsx b/packages/react-core/src/components/Toolbar/Toolbar.tsx index 8941ba29493..1b64ab84461 100644 --- a/packages/react-core/src/components/Toolbar/Toolbar.tsx +++ b/packages/react-core/src/components/Toolbar/Toolbar.tsx @@ -40,6 +40,8 @@ export interface ToolbarProps extends React.HTMLProps, OUIAProps isStatic?: boolean; /** Flag indicating the toolbar should stick to the top of its container */ isSticky?: boolean; + /** @beta Flag indicating the toolbar has a vertical orientation */ + isVertical?: boolean; /** Insets at various breakpoints. */ inset?: { default?: 'insetNone' | 'insetSm' | 'insetMd' | 'insetLg' | 'insetXl' | 'inset2xl'; @@ -143,6 +145,7 @@ class Toolbar extends Component { isStatic, inset, isSticky, + isVertical, ouiaId, numberOfFiltersText, customLabelGroupContent, @@ -167,6 +170,7 @@ class Toolbar extends Component { isFullHeight && styles.modifiers.fullHeight, isStatic && styles.modifiers.static, isSticky && styles.modifiers.sticky, + isVertical && styles.modifiers.vertical, formatBreakpointMods(inset, styles, '', getBreakpoint(width)), colorVariant === 'primary' && styles.modifiers.primary, colorVariant === 'secondary' && styles.modifiers.secondary, diff --git a/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx b/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx index 8c44cc8d360..fcd5c5b6219 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx +++ b/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx @@ -54,7 +54,7 @@ describe('Toolbar', () => { ); - expect(screen.getByTestId('toolbar')).not.toHaveClass('pf-m-no-padding'); + expect(screen.getByTestId('toolbar')).not.toHaveClass(styles.modifiers.noPadding); }); it(`should render toolbar with ${styles.modifiers.noPadding} class when hasNoPadding is true`, () => { @@ -205,4 +205,37 @@ describe('Toolbar', () => { }); }); }); + + it(`Renders toolbar without ${styles.modifiers.vertical} by default`, () => { + render( + + + Test + Test 2 + + Test 3 + + + ); + expect(screen.getByTestId('Toolbar-test-is-not-vertical')).not.toHaveClass(styles.modifiers.vertical); + }); + + it('Renders with class ${styles.modifiers.vertical} when isVertical is true', () => { + const items = ( + + Test + Test 2 + + Test 3 + + ); + + render( + + {items} + + ); + + expect(screen.getByTestId('Toolbar-test-is-vertical')).toHaveClass(styles.modifiers.vertical); + }); }); diff --git a/packages/react-core/src/demos/Compass/Compass.md b/packages/react-core/src/demos/Compass/Compass.md index 16ba72b1624..3cd92210b53 100644 --- a/packages/react-core/src/demos/Compass/Compass.md +++ b/packages/react-core/src/demos/Compass/Compass.md @@ -9,6 +9,13 @@ 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'; ## Demos @@ -24,4 +31,10 @@ This demo showcases an implementation of the following Compass features: ```ts isFullscreen file="./examples/CompassDemo.tsx" -``` \ No newline at end of file +``` + +### Docked nav demo + +```ts isFullscreen file="./examples/CompassDockDemo.tsx" + +``` diff --git a/packages/react-core/src/demos/Compass/examples/CompassDockDemo.tsx b/packages/react-core/src/demos/Compass/examples/CompassDockDemo.tsx new file mode 100644 index 00000000000..77f52467356 --- /dev/null +++ b/packages/react-core/src/demos/Compass/examples/CompassDockDemo.tsx @@ -0,0 +1,215 @@ +import { useRef, useState } from 'react'; +import { + Compass, + CompassContent, + CompassMainHeader, + CompassPanel, + Title, + NavItem, + NavList, + Nav, + Brand, + MastheadLogo, + MastheadBrand, + MastheadContent, + MastheadMain, + Masthead, + Toolbar, + ToolbarContent, + ToolbarItem, + ToolbarGroup, + Dropdown, + DropdownList, + MenuToggle, + 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 onNavSelect = (_event: React.FormEvent, selectedItem: NavOnSelectProps) => { + typeof selectedItem.itemId === 'number' && setActiveItem(selectedItem.itemId); + }; + + const onDropdownToggle = () => { + setIsDropdownOpen((prevIsOpen) => !prevIsOpen); + }; + + const onDropdownSelect = () => { + setIsDropdownOpen(false); + }; + + const userDropdownItems = [ + <> + My profile + User management + Logout + + ]; + + const navItem1Ref = useRef(null); + const navItem2Ref = useRef(null); + const navItem3Ref = useRef(null); + const navItem4Ref = useRef(null); + const settingsRef = useRef(null); + const helpRef = useRef(null); + const userMenuRef = useRef(null); + + const dockContent = ( + + + + }> + + + + + + + + + + + + + + + + + + + +