Skip to content

Commit 5519389

Browse files
authored
feat(docked nav): add support for docked nav layout (#12175)
1 parent d5053dd commit 5519389

File tree

26 files changed

+762
-52
lines changed

26 files changed

+762
-52
lines changed

packages/react-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"tslib": "^2.8.1"
5555
},
5656
"devDependencies": {
57-
"@patternfly/patternfly": "6.5.0-prerelease.27",
57+
"@patternfly/patternfly": "6.5.0-prerelease.33",
5858
"case-anything": "^3.1.2",
5959
"css": "^3.0.0",
6060
"fs-extra": "^11.3.0"

packages/react-core/src/components/Compass/Compass.tsx

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import compassBackgroundImageDark from '@patternfly/react-tokens/dist/esm/c_comp
88
export interface CompassProps extends React.HTMLProps<HTMLDivElement> {
99
/** Additional classes added to the Compass. */
1010
className?: string;
11+
/** Content of the docked navigation area of the layout */
12+
dock?: React.ReactNode;
1113
/** Content placed at the top of the layout */
1214
header?: React.ReactNode;
1315
/** Flag indicating if the header is expanded */
@@ -38,6 +40,7 @@ export interface CompassProps extends React.HTMLProps<HTMLDivElement> {
3840

3941
export const Compass: React.FunctionComponent<CompassProps> = ({
4042
className,
43+
dock,
4144
header,
4245
isHeaderExpanded = true,
4346
sidebarStart,
@@ -64,32 +67,45 @@ export const Compass: React.FunctionComponent<CompassProps> = ({
6467
}
6568

6669
const compassContent = (
67-
<div className={css(styles.compass, className)} {...props} style={{ ...props.style, ...backgroundImageStyles }}>
68-
<div
69-
className={css(styles.compassHeader, isHeaderExpanded && 'pf-m-expanded')}
70-
{...(!isHeaderExpanded && { inert: 'true' })}
71-
>
72-
{header}
73-
</div>
74-
<div
75-
className={css(styles.compassSidebar, styles.modifiers.start, isSidebarStartExpanded && 'pf-m-expanded')}
76-
{...(!isSidebarStartExpanded && { inert: 'true' })}
77-
>
78-
{sidebarStart}
79-
</div>
80-
<div className={css(styles.compassMain)}>{main}</div>
81-
<div
82-
className={css(styles.compassSidebar, styles.modifiers.end, isSidebarEndExpanded && 'pf-m-expanded')}
83-
{...(!isSidebarEndExpanded && { inert: 'true' })}
84-
>
85-
{sidebarEnd}
86-
</div>
87-
<div
88-
className={css(styles.compassFooter, isFooterExpanded && 'pf-m-expanded')}
89-
{...(!isFooterExpanded && { inert: 'true' })}
90-
>
91-
{footer}
92-
</div>
70+
<div
71+
className={css(styles.compass, dock !== undefined && styles.modifiers.dock, className)}
72+
{...props}
73+
style={{ ...props.style, ...backgroundImageStyles }}
74+
>
75+
{dock && <div className={css(`${styles.compass}__dock`)}>{dock}</div>}
76+
{header && (
77+
<div
78+
className={css(styles.compassHeader, isHeaderExpanded && 'pf-m-expanded')}
79+
{...(!isHeaderExpanded && { inert: 'true' })}
80+
>
81+
{header}
82+
</div>
83+
)}
84+
{sidebarStart && (
85+
<div
86+
className={css(styles.compassSidebar, styles.modifiers.start, isSidebarStartExpanded && 'pf-m-expanded')}
87+
{...(!isSidebarStartExpanded && { inert: 'true' })}
88+
>
89+
{sidebarStart}
90+
</div>
91+
)}
92+
{main && <div className={css(styles.compassMain)}>{main}</div>}
93+
{sidebarEnd && (
94+
<div
95+
className={css(styles.compassSidebar, styles.modifiers.end, isSidebarEndExpanded && 'pf-m-expanded')}
96+
{...(!isSidebarEndExpanded && { inert: 'true' })}
97+
>
98+
{sidebarEnd}
99+
</div>
100+
)}
101+
{footer && (
102+
<div
103+
className={css(styles.compassFooter, isFooterExpanded && 'pf-m-expanded')}
104+
{...(!isFooterExpanded && { inert: 'true' })}
105+
>
106+
{footer}
107+
</div>
108+
)}
93109
</div>
94110
);
95111

packages/react-core/src/components/Compass/__tests__/Compass.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,13 @@ test('Matches the snapshot with drawer', () => {
170170
);
171171
expect(asFragment()).toMatchSnapshot();
172172
});
173+
174+
test(`Renders with ${styles.modifiers.dock} class when dock is passed`, () => {
175+
render(<Compass dock={<div>Dock content</div>} data-testid="compass" />);
176+
expect(screen.getByTestId('compass')).toHaveClass(styles.modifiers.dock);
177+
});
178+
179+
test(`Does not render with ${styles.modifiers.dock} class when dock is not passed`, () => {
180+
render(<Compass data-testid="compass" />);
181+
expect(screen.getByTestId('compass')).not.toHaveClass(styles.modifiers.dock);
182+
});

packages/react-core/src/components/Compass/examples/Compass.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ propComponents:
1717
]
1818
---
1919

20-
import { useRef, useState } from 'react';
20+
import { useRef, useState, useEffect } from 'react';
2121
import PlayIcon from '@patternfly/react-icons/dist/esm/icons/play-icon';
2222
import OutlinedPlusSquare from '@patternfly/react-icons/dist/esm/icons/outlined-plus-square-icon';
2323
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
5151

5252
```
5353

54+
### With docked nav
55+
56+
```ts file="CompassDockLayout.tsx"
57+
58+
```
59+
5460
## Composable structure
5561

5662
When building a more custom implementation with Compass components, there are some intended or expected structures that must remain present.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {
2+
Compass,
3+
CompassContent,
4+
CompassMainHeader,
5+
CompassPanel,
6+
CompassMainHeaderContent
7+
} from '@patternfly/react-core';
8+
import './compass.css';
9+
10+
export const CompassBasic: React.FunctionComponent = () => {
11+
const dockContent = <div>Content</div>;
12+
const mainContent = (
13+
<CompassContent>
14+
<CompassMainHeader>
15+
<CompassPanel>
16+
<CompassMainHeaderContent>
17+
<div>Content title</div>
18+
</CompassMainHeaderContent>
19+
</CompassPanel>
20+
</CompassMainHeader>
21+
<div>Content</div>
22+
</CompassContent>
23+
);
24+
return <Compass dock={dockContent} main={mainContent} />;
25+
};

packages/react-core/src/components/Compass/examples/compass.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,16 @@
2020
inset: 0;
2121
border: var(--pf-t--global--border--width--regular) dashed var(--pf-t--global--border--color--default);
2222
pointer-events: none;
23+
}
24+
25+
#ws-react-a-compass-with-docked-nav [class*="pf-v6-c-compass"] {
26+
position: relative;
27+
}
28+
29+
#ws-react-a-compass-with-docked-nav [class*="pf-v6-c-compass"]:not([class*="footer"])::after {
30+
content: "";
31+
position: absolute;
32+
inset: 0;
33+
border: var(--pf-t--global--border--width--regular) dashed var(--pf-t--global--border--color--default);
34+
pointer-events: none;
2335
}

packages/react-core/src/components/Masthead/Masthead.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export interface MastheadProps extends React.DetailedHTMLProps<React.HTMLProps<H
2727
xl?: 'insetNone' | 'insetXs' | 'insetSm' | 'insetMd' | 'insetLg' | 'insetXl' | 'inset2xl' | 'inset3xl';
2828
'2xl'?: 'insetNone' | 'insetXs' | 'insetSm' | 'insetMd' | 'insetLg' | 'insetXl' | 'inset2xl' | 'inset3xl';
2929
};
30+
/** @beta Indicates the variant of the masthead */
31+
variant?: 'default' | 'docked';
3032
}
3133

3234
export const Masthead: React.FunctionComponent<MastheadProps> = ({
@@ -36,13 +38,15 @@ export const Masthead: React.FunctionComponent<MastheadProps> = ({
3638
md: 'inline'
3739
},
3840
inset,
41+
variant = 'default',
3942
...props
4043
}: MastheadProps) => {
4144
const { width, getBreakpoint } = useContext(PageContext);
4245
return (
4346
<header
4447
className={css(
4548
styles.masthead,
49+
variant === 'docked' && styles.modifiers.docked,
4650
formatBreakpointMods(display, styles, 'display-', getBreakpoint(width)),
4751
formatBreakpointMods(inset, styles, '', getBreakpoint(width)),
4852
className

packages/react-core/src/components/Masthead/__tests__/Masthead.test.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { render } from '@testing-library/react';
1+
import { render, screen } from '@testing-library/react';
22
import { Masthead, MastheadMain, MastheadLogo, MastheadContent, MastheadBrand, MastheadToggle } from '../index';
3+
import styles from '@patternfly/react-styles/css/components/Masthead/masthead';
34

45
describe('Masthead', () => {
56
test('verify basic', () => {
@@ -71,6 +72,29 @@ describe('Masthead', () => {
7172
expect(asFragment()).toMatchSnapshot();
7273
});
7374
});
75+
76+
test(`Renders with ${styles.modifiers.docked} class when variant is docked`, () => {
77+
render(
78+
<Masthead variant="docked" data-testid="masthead">
79+
test
80+
</Masthead>
81+
);
82+
expect(screen.getByTestId('masthead')).toHaveClass(styles.modifiers.docked);
83+
});
84+
85+
test(`Does not render with ${styles.modifiers.docked} class when variant is default`, () => {
86+
render(
87+
<Masthead variant="default" data-testid="masthead">
88+
test
89+
</Masthead>
90+
);
91+
expect(screen.getByTestId('masthead')).not.toHaveClass(styles.modifiers.docked);
92+
});
93+
94+
test(`Does not render with ${styles.modifiers.docked} class when variant is not passed`, () => {
95+
render(<Masthead data-testid="masthead">test</Masthead>);
96+
expect(screen.getByTestId('masthead')).not.toHaveClass(styles.modifiers.docked);
97+
});
7498
});
7599

76100
describe('MastheadLogo', () => {

packages/react-core/src/components/Nav/Nav.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export interface NavProps
3535
) => void;
3636
/** Accessible label for the nav when there are multiple navs on the page */
3737
'aria-label'?: string;
38-
/** For horizontal navs */
39-
variant?: 'default' | 'horizontal' | 'horizontal-subnav';
38+
/** The nav variant to use. Docked is in beta. */
39+
variant?: 'default' | 'horizontal' | 'horizontal-subnav' | 'docked';
4040
/** Value to overwrite the randomly generated data-ouia-component-id.*/
4141
ouiaId?: number | string;
4242
/** 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<
154154
className={css(
155155
styles.nav,
156156
isHorizontal && styles.modifiers.horizontal,
157+
variant === 'docked' && styles.modifiers.docked,
157158
variant === 'horizontal-subnav' && styles.modifiers.subnav,
158159
this.state.isScrollable && styles.modifiers.scrollable,
159160
className

packages/react-core/src/components/Nav/NavItem.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
import { cloneElement, Fragment, isValidElement, useContext, useEffect, useRef, useState } from 'react';
1+
import {
2+
cloneElement,
3+
Fragment,
4+
isValidElement,
5+
useContext,
6+
useEffect,
7+
useRef,
8+
useState,
9+
forwardRef,
10+
MutableRefObject
11+
} from 'react';
212
import styles from '@patternfly/react-styles/css/components/Nav/nav';
313
import menuStyles from '@patternfly/react-styles/css/components/Menu/menu';
414
import dividerStyles from '@patternfly/react-styles/css/components/Divider/divider';
@@ -42,9 +52,13 @@ export interface NavItemProps extends Omit<React.HTMLProps<HTMLAnchorElement>, '
4252
ouiaId?: number | string;
4353
/** 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. */
4454
ouiaSafe?: boolean;
55+
/** React ref for the anchor element within the nav item. */
56+
anchorRef?: React.Ref<HTMLAnchorElement>;
57+
/** @hide Forwarded ref */
58+
innerRef?: React.Ref<HTMLLIElement>;
4559
}
4660

47-
export const NavItem: React.FunctionComponent<NavItemProps> = ({
61+
const NavItemBase: React.FunctionComponent<NavItemProps> = ({
4862
children,
4963
styleChildren = true,
5064
className,
@@ -61,13 +75,16 @@ export const NavItem: React.FunctionComponent<NavItemProps> = ({
6175
ouiaSafe,
6276
zIndex = 9999,
6377
icon,
78+
innerRef,
79+
anchorRef,
6480
...props
6581
}: NavItemProps) => {
6682
const { flyoutRef, setFlyoutRef, navRef } = useContext(NavContext);
6783
const { isSidebarOpen } = useContext(PageSidebarContext);
6884
const [flyoutTarget, setFlyoutTarget] = useState(null);
6985
const [isHovered, setIsHovered] = useState(false);
70-
const ref = useRef<HTMLLIElement>(undefined);
86+
const _ref = useRef<HTMLLIElement>(undefined);
87+
const ref = (innerRef as MutableRefObject<HTMLLIElement>) || _ref;
7188
const flyoutVisible = ref === flyoutRef;
7289
const popperRef = useRef<HTMLDivElement>(undefined);
7390
const hasFlyout = flyout !== undefined;
@@ -180,6 +197,7 @@ export const NavItem: React.FunctionComponent<NavItemProps> = ({
180197
const preventLinkDefault = preventDefault || !to;
181198
return (
182199
<Component
200+
ref={anchorRef}
183201
href={to}
184202
onClick={(e: any) => context.onSelect(e, groupId, itemId, to, preventLinkDefault, onClick)}
185203
className={css(
@@ -208,6 +226,7 @@ export const NavItem: React.FunctionComponent<NavItemProps> = ({
208226
className: css(styles.navLink, isActive && styles.modifiers.current, child.props && child.props.className)
209227
}),
210228
tabIndex: child.props.tabIndex || tabIndex,
229+
ref: anchorRef,
211230
children: hasFlyout ? (
212231
<Fragment>
213232
{child.props.children}
@@ -267,4 +286,9 @@ export const NavItem: React.FunctionComponent<NavItemProps> = ({
267286

268287
return navItem;
269288
};
289+
290+
export const NavItem = forwardRef<HTMLLIElement, NavItemProps>((props, ref) => (
291+
<NavItemBase {...props} innerRef={ref} />
292+
));
293+
270294
NavItem.displayName = 'NavItem';

0 commit comments

Comments
 (0)