Skip to content

Commit 18312a9

Browse files
committed
feat(Nav,Compass,Page): add support for expandable nav items in docked nav
1 parent f20f073 commit 18312a9

22 files changed

Lines changed: 372 additions & 49 deletions

File tree

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.6.0-prerelease.20",
57+
"@patternfly/patternfly": "6.6.0-prerelease.37",
5858
"case-anything": "^3.1.2",
5959
"css": "^3.0.0",
6060
"fs-extra": "^11.3.3"

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ export interface CompassProps extends React.HTMLProps<HTMLDivElement> {
1010
masthead?: React.ReactNode;
1111
/** Content of the docked navigation area of the layout */
1212
dock?: React.ReactNode;
13-
/** @beta Flag indicating the docked nav is expanded on mobile. Only applies when dock content is passed. */
13+
/** @beta Flag indicating the docked nav is expande on mobile. Only applies when dock content is passed. */
1414
isDockExpanded?: boolean;
15+
/** @beta Flag indicating a docked nav is expanded as an overlay, triggered by expandable nav children. Only applies when dock content is passed. */
16+
isDockExpandableExpanded?: boolean;
1517
/** @beta Flag indicating the docked nav should display text on desktop. Only applies when dock content is passed, and
1618
* will handle toggling the visibility of the text in individual isDocked components.
1719
*/
@@ -45,6 +47,7 @@ export const Compass: React.FunctionComponent<CompassProps> = ({
4547
masthead,
4648
dock,
4749
isDockExpanded,
50+
isDockExpandableExpanded,
4851
isDockTextExpanded,
4952
header,
5053
isHeaderExpanded = true,
@@ -69,6 +72,7 @@ export const Compass: React.FunctionComponent<CompassProps> = ({
6972
className={css(
7073
`${styles.compass}__dock`,
7174
isDockExpanded && styles.modifiers.expanded,
75+
isDockExpandableExpanded && styles.modifiers.expandableExpanded,
7276
isDockTextExpanded && styles.modifiers.textExpanded
7377
)}
7478
>

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,21 @@ test('Renders footer without expanded class and with inert when isFooterExpanded
9494
expect(footerElement).toHaveAttribute('inert');
9595
});
9696

97+
test(`Renders with ${styles.modifiers.expandableExpanded} class when isDockExpandableExpanded is true`, () => {
98+
render(<Compass dock={<div>Dock content</div>} isDockExpandableExpanded />);
99+
expect(screen.getByText('Dock content').parentElement).toHaveClass(styles.modifiers.expandableExpanded);
100+
});
101+
102+
test(`Does not render with ${styles.modifiers.expandableExpanded} class when isDockExpandableExpanded is false`, () => {
103+
render(<Compass dock={<div>Dock content</div>} isDockExpandableExpanded={false} />);
104+
expect(screen.getByText('Dock content').parentElement).not.toHaveClass(styles.modifiers.expandableExpanded);
105+
});
106+
107+
test(`Does not render with ${styles.modifiers.expandableExpanded} class by default`, () => {
108+
render(<Compass dock={<div>Dock content</div>} />);
109+
expect(screen.getByText('Dock content').parentElement).not.toHaveClass(styles.modifiers.expandableExpanded);
110+
});
111+
97112
test('Renders with drawer when drawerContent is provided', () => {
98113
render(<Compass drawerContent={<div>Drawer content</div>} />);
99114
expect(screen.getByText('Drawer content')).toBeVisible();

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PickOptional } from '../../helpers/typeUtils';
88
import { getOUIAProps, OUIAProps } from '../../helpers';
99
import { SSRSafeIds } from '../../helpers/SSRSafeIds/SSRSafeIds';
1010
import { IS_INERT } from '../../helpers/inert';
11+
import RhUiEllipsisHorizontalFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ellipsis-horizontal-fill-icon';
1112

1213
export interface NavExpandableProps
1314
extends Omit<React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>, 'title'>, OUIAProps {
@@ -17,6 +18,8 @@ export interface NavExpandableProps
1718
srText?: string;
1819
/** Boolean to pragmatically expand or collapse section */
1920
isExpanded?: boolean;
21+
/** Adds an expandable icon to indicate the section is expandable */
22+
hasExpandableIcon?: boolean;
2023
/** Anything that can be rendered inside of the expandable list */
2124
children?: React.ReactNode;
2225
/** Additional classes added to the container */
@@ -50,7 +53,8 @@ class NavExpandable extends Component<NavExpandableProps, NavExpandableState> {
5053
className: '',
5154
groupId: null as string,
5255
isActive: false,
53-
id: ''
56+
id: '',
57+
hasExpandableIcon: false
5458
};
5559

5660
state = {
@@ -100,6 +104,7 @@ class NavExpandable extends Component<NavExpandableProps, NavExpandableState> {
100104
id,
101105
// eslint-disable-next-line @typescript-eslint/no-unused-vars
102106
isExpanded,
107+
hasExpandableIcon,
103108
buttonProps,
104109
// eslint-disable-next-line @typescript-eslint/no-unused-vars
105110
onExpand,
@@ -137,7 +142,12 @@ class NavExpandable extends Component<NavExpandableProps, NavExpandableState> {
137142
{...buttonProps}
138143
>
139144
{icon && <span className={css(styles.navLinkIcon)}>{icon}</span>}
140-
{typeof title !== 'string' ? <span className={css(styles.navLinkText)}>{title}</span> : title}
145+
{hasExpandableIcon && (
146+
<span className={css(styles.navLinkExpandableIcon)}>
147+
<RhUiEllipsisHorizontalFillIcon />
148+
</span>
149+
)}
150+
<span className={css(styles.navLinkText)}>{title}</span>
141151
<span className={css(styles.navToggle)}>
142152
<span className={css(styles.navToggleIcon)}>
143153
<RhMicronsCaretDownIcon />

packages/react-core/src/components/Nav/__tests__/Generated/__snapshots__/NavExpandable.test.tsx.snap

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ exports[`NavExpandable should match snapshot (auto-generated) 1`] = `
1212
aria-expanded="false"
1313
class="pf-v6-c-nav__link"
1414
>
15-
string
15+
<span
16+
class="pf-v6-c-nav__link-text"
17+
>
18+
string
19+
</span>
1620
<span
1721
class="pf-v6-c-nav__toggle"
1822
>

packages/react-core/src/components/Nav/__tests__/NavExpandable.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,24 @@ test('Does not render nav link icon wrapper when icon prop is not provided', ()
3030
const button = screen.getByRole('button', { name: 'NavExpandable' });
3131
expect(button.querySelector('.pf-v6-c-nav__link-icon')).not.toBeInTheDocument();
3232
});
33+
34+
test(`Renders with expandable icon when hasExpandableIcon is true`, () => {
35+
render(<NavExpandable id="grp-1" title="NavExpandable" hasExpandableIcon={true}></NavExpandable>);
36+
37+
const button = screen.getByRole('button', { name: 'NavExpandable' });
38+
expect(button.querySelector('.pf-v6-c-nav__link-expandable-icon')).toBeInTheDocument();
39+
});
40+
41+
test(`Does not render expandable icon when hasExpandableIcon is false`, () => {
42+
render(<NavExpandable id="grp-1" title="NavExpandable" hasExpandableIcon={false}></NavExpandable>);
43+
44+
const button = screen.getByRole('button', { name: 'NavExpandable' });
45+
expect(button.querySelector('.pf-v6-c-nav__link-expandable-icon')).not.toBeInTheDocument();
46+
});
47+
48+
test(`Does not render expandable icon by default`, () => {
49+
render(<NavExpandable id="grp-1" title="NavExpandable"></NavExpandable>);
50+
51+
const button = screen.getByRole('button', { name: 'NavExpandable' });
52+
expect(button.querySelector('.pf-v6-c-nav__link-expandable-icon')).not.toBeInTheDocument();
53+
});

packages/react-core/src/components/Nav/__tests__/__snapshots__/Nav.test.tsx.snap

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,11 @@ exports[`Nav Expandable Nav List - Trigger toggle 1`] = `
196196
class="pf-v6-c-nav__link"
197197
id="grp-1"
198198
>
199-
Section 1
199+
<span
200+
class="pf-v6-c-nav__link-text"
201+
>
202+
Section 1
203+
</span>
200204
<span
201205
class="pf-v6-c-nav__toggle"
202206
>
@@ -329,7 +333,11 @@ exports[`Nav Expandable Nav List 1`] = `
329333
class="pf-v6-c-nav__link"
330334
id="grp-1"
331335
>
332-
Section 1
336+
<span
337+
class="pf-v6-c-nav__link-text"
338+
>
339+
Section 1
340+
</span>
333341
<span
334342
class="pf-v6-c-nav__toggle"
335343
>
@@ -461,7 +469,11 @@ exports[`Nav Expandable Nav List with aria label 1`] = `
461469
aria-expanded="false"
462470
class="pf-v6-c-nav__link"
463471
>
464-
Section 1
472+
<span
473+
class="pf-v6-c-nav__link-text"
474+
>
475+
Section 1
476+
</span>
465477
<span
466478
class="pf-v6-c-nav__toggle"
467479
>
@@ -1018,10 +1030,7 @@ exports[`Nav Nav List with flyout 1`] = `
10181030
</ul>
10191031
<div
10201032
class=""
1021-
data-popper-escaped="true"
1022-
data-popper-placement="right-start"
1023-
data-popper-reference-hidden="true"
1024-
style="position: absolute; left: 0px; top: 0px; z-index: 9999; opacity: 1; transition: opacity 0ms cubic-bezier(.54, 1.5, .38, 1.11); min-width: 0px; transform: translate(0px, 0px);"
1033+
style="position: absolute; left: 0px; top: 0px; z-index: 9999; opacity: 0; transition: opacity 0ms cubic-bezier(.54, 1.5, .38, 1.11); min-width: 0px;"
10251034
>
10261035
<div>
10271036
Flyout test

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ ouia: true
99
import { useState } from 'react';
1010
import './nav.css';
1111
import RhMicronsCaretRightIcon from '@patternfly/react-icons/dist/esm/icons/rh-microns-caret-right-icon';
12-
import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon';
13-
import RhUiFolderFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-fill-icon';
14-
import RhUiFolderOpenFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-open-fill-icon';
15-
import RhUiCloudFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-cloud-fill-icon';
12+
import RhUiCubesIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-cubes-icon';
13+
import RhUiFolderIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-icon';
14+
import RhUiFolderOpenIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-open-icon';
15+
import RhUiCloudIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-cloud-icon';
1616
import RhUiLinkIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-link-icon';
17+
import RhUiCodeIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-code-icon';
1718

1819
## Examples
1920

@@ -81,6 +82,18 @@ A flyout should be a `Menu` component. Press `space` or `right arrow` to open a
8182

8283
```
8384

85+
### Docked
86+
87+
The docked variant of `Navigation` displays only icons passed to child `NavItems` or `NavExpandable`. Text becomes visible when `.pf-m-text-expanded` or `.pf-m-expandable-expanded` are applied to an outer page or compass dock.
88+
89+
`NavExpandable` items should include the `hasExpandableIcon` prop to indicate their expandable nature while in the collapsed state of the docked nav as the caret will not be rendered.
90+
91+
See the [docked nav demo](/components/navigation/react-demos#docked-nav) for a fully functional example.
92+
93+
```ts file="./NavDocked.tsx"
94+
95+
```
96+
8497
## Types
8598

8699
### NavSelectClickHandler
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useState } from 'react';
2+
import { Nav, NavItem, NavList, NavExpandable } from '@patternfly/react-core';
3+
import RhUiCubesIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-cubes-icon';
4+
import RhUiFolderIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-icon';
5+
import RhUiCodeIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-code-icon';
6+
import RhUiCloudIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-cloud-icon';
7+
8+
export const NavDocked: React.FunctionComponent = () => {
9+
const [activeItem, setActiveItem] = useState(0);
10+
const [isGroupExpanded, setIsGroupExpanded] = useState(false);
11+
12+
const onSelect = (_event: React.FormEvent<HTMLInputElement>, result: { itemId: number | string }) => {
13+
setActiveItem(result.itemId as number);
14+
};
15+
16+
const onToggle = (
17+
_event: React.MouseEvent<HTMLButtonElement>,
18+
result: { groupId: number | string; isExpanded: boolean }
19+
) => {
20+
setIsGroupExpanded(result.isExpanded);
21+
};
22+
23+
return (
24+
<Nav variant="docked" onSelect={onSelect} onToggle={onToggle} aria-label="Default global" ouiaId="DefaultNav">
25+
<NavList>
26+
<NavItem
27+
icon={<RhUiCubesIcon />}
28+
preventDefault
29+
id="nav-default-link1"
30+
to="#nav-default-link1"
31+
itemId={0}
32+
isActive={activeItem === 0}
33+
>
34+
Default Link 1
35+
</NavItem>
36+
<NavItem
37+
icon={<RhUiCloudIcon />}
38+
preventDefault
39+
id="nav-default-link2"
40+
to="#nav-default-link2"
41+
itemId={1}
42+
isActive={activeItem === 1}
43+
>
44+
Default Link 2
45+
</NavItem>
46+
<NavItem
47+
icon={<RhUiCodeIcon />}
48+
preventDefault
49+
id="nav-default-link3"
50+
to="#nav-default-link3"
51+
itemId={2}
52+
isActive={activeItem === 2}
53+
>
54+
Default Link 3
55+
</NavItem>
56+
<NavExpandable
57+
title="Expandable Group 1"
58+
groupId="nav-expandable-group-1"
59+
icon={<RhUiFolderIcon />}
60+
isExpanded={isGroupExpanded}
61+
hasExpandableIcon
62+
>
63+
<NavItem
64+
preventDefault
65+
id="expandable-1"
66+
to="#expandable-1"
67+
groupId="nav-expandable-group-1"
68+
itemId={3}
69+
isActive={activeItem === 3}
70+
>
71+
Subnav 1 Link 1
72+
</NavItem>
73+
<NavItem
74+
preventDefault
75+
id="expandable-2"
76+
to="#expandable-2"
77+
groupId="nav-expandable-group-1"
78+
itemId={4}
79+
isActive={activeItem === 4}
80+
>
81+
Subnav 1 Link 2
82+
</NavItem>
83+
<NavItem
84+
preventDefault
85+
id="expandable-3"
86+
to="#expandable-3"
87+
groupId="nav-expandable-group-1"
88+
itemId={5}
89+
isActive={activeItem === 5}
90+
>
91+
Subnav 1 Link 3
92+
</NavItem>
93+
</NavExpandable>
94+
</NavList>
95+
</Nav>
96+
);
97+
};

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useState } from 'react';
22
import { Nav, NavExpandable, NavItem, NavList } from '@patternfly/react-core';
3-
import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon';
4-
import RhUiFolderFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-fill-icon';
5-
import RhUiFolderOpenFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-open-fill-icon';
6-
import RhUiCloudFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-cloud-fill-icon';
3+
import RhUiCubesIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-cubes-icon';
4+
import RhUiFolderIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-icon';
5+
import RhUiFolderOpenIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-open-icon';
6+
import RhUiCloudIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-cloud-icon';
77
import RhUiLinkIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-link-icon';
88

99
export const NavIcons: React.FunctionComponent = () => {
@@ -22,7 +22,7 @@ export const NavIcons: React.FunctionComponent = () => {
2222
to="#nav-icon-link1"
2323
itemId={0}
2424
isActive={activeItem === 0}
25-
icon={<CubeIcon />}
25+
icon={<RhUiCubesIcon />}
2626
>
2727
Link 1
2828
</NavItem>
@@ -32,7 +32,7 @@ export const NavIcons: React.FunctionComponent = () => {
3232
to="#nav-icon-link2"
3333
itemId={1}
3434
isActive={activeItem === 1}
35-
icon={<RhUiFolderFillIcon />}
35+
icon={<RhUiFolderIcon />}
3636
>
3737
Link 2
3838
</NavItem>
@@ -42,7 +42,7 @@ export const NavIcons: React.FunctionComponent = () => {
4242
to="#nav-icon-link3"
4343
itemId={2}
4444
isActive={activeItem === 2}
45-
icon={<RhUiCloudFillIcon />}
45+
icon={<RhUiCloudIcon />}
4646
>
4747
Link 3
4848
</NavItem>
@@ -56,7 +56,7 @@ export const NavIcons: React.FunctionComponent = () => {
5656
>
5757
Link 4
5858
</NavItem>
59-
<NavExpandable title="Expandable" icon={<RhUiFolderOpenFillIcon />} groupId="nav-icon-expandable">
59+
<NavExpandable title="Expandable" icon={<RhUiFolderOpenIcon />} groupId="nav-icon-expandable">
6060
<NavItem
6161
preventDefault
6262
id="nav-icon-expandable-link1"

0 commit comments

Comments
 (0)