Skip to content

Commit 4bd98bc

Browse files
wise-king-sullymankmcfaulthatblindgeye
authored
feat(Compass): add compass nav components (#12138)
* feat(Compass): add compass nav components * refactor to bundle children into CompassNavSearch/Home components * Tweak interfaces to not duplicate props from HTMLDivElement * update tests * Add padding to compass demo nav * Update icons * Core bump * Update lockfile * Update packages/react-core/src/components/Compass/CompassNavContent.tsx Co-authored-by: kmcfaul <45077788+kmcfaul@users.noreply.github.com> * Update packages/react-core/src/components/Compass/CompassNavHome.tsx Co-authored-by: kmcfaul <45077788+kmcfaul@users.noreply.github.com> * Update packages/react-core/src/components/Compass/CompassNavMain.tsx Co-authored-by: kmcfaul <45077788+kmcfaul@users.noreply.github.com> * Update packages/react-core/src/components/Compass/CompassNavSearch.tsx Co-authored-by: kmcfaul <45077788+kmcfaul@users.noreply.github.com> * format * Fix dep issue introduced during merge conflict resolution * Add trigger ref to tooltips * Address interface/a11y feedback * Update packages/react-core/src/components/Compass/CompassNavContent.tsx Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> --------- Co-authored-by: kmcfaul <45077788+kmcfaul@users.noreply.github.com> Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com>
1 parent 2df092a commit 4bd98bc

14 files changed

+722
-40
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import styles from '@patternfly/react-styles/css/components/Compass/compass';
2+
import { css } from '@patternfly/react-styles';
3+
export interface CompassNavContentProps extends React.HTMLProps<HTMLDivElement> {
4+
/** Content of the nav content wrapper. */
5+
children: React.ReactNode;
6+
/** Additional classes added to the nav content. */
7+
className?: string;
8+
}
9+
10+
export const CompassNavContent: React.FunctionComponent<CompassNavContentProps> = ({
11+
children,
12+
className,
13+
...props
14+
}: CompassNavContentProps) => (
15+
<div className={css(styles.compassNavContent, className)} {...props}>
16+
{children}
17+
</div>
18+
);
19+
20+
CompassNavContent.displayName = 'CompassNavContent';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useRef } from 'react';
2+
import styles from '@patternfly/react-styles/css/components/Compass/compass';
3+
import { css } from '@patternfly/react-styles';
4+
import { Button } from '../Button';
5+
import { Tooltip } from '../Tooltip';
6+
7+
const CompassHomeIcon = () => (
8+
<svg
9+
width="1em"
10+
height="1em"
11+
className="pf-v6-svg"
12+
viewBox="0 0 20 20"
13+
fill="none"
14+
xmlns="http://www.w3.org/2000/svg"
15+
aria-hidden="true"
16+
>
17+
<path
18+
d="M8.33268 13.334H11.666"
19+
stroke="currentcolor"
20+
strokeWidth="1.5"
21+
strokeLinecap="round"
22+
strokeLinejoin="round"
23+
/>
24+
<path
25+
d="M1.66602 6.66602L9.73102 2.63351C9.89994 2.54905 10.0988 2.54905 10.2677 2.63351L18.3327 6.66602"
26+
stroke="currentcolor"
27+
strokeWidth="1.5"
28+
strokeLinecap="round"
29+
strokeLinejoin="round"
30+
/>
31+
<path
32+
d="M16.6673 9.16602V15.4993C16.6673 16.6039 15.7719 17.4993 14.6673 17.4993H5.33398C4.22941 17.4993 3.33398 16.6039 3.33398 15.4993V9.16602"
33+
stroke="currentcolor"
34+
strokeWidth="1.5"
35+
strokeLinecap="round"
36+
strokeLinejoin="round"
37+
/>
38+
</svg>
39+
);
40+
41+
export interface CompassNavHomeProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onClick'> {
42+
/** Content to display in the tooltip. Defaults to "Home". */
43+
tooltipContent?: React.ReactNode;
44+
/** Click handler for the home button. */
45+
onClick?: React.MouseEventHandler<HTMLButtonElement>;
46+
/** Additional classes added to the nav home wrapper. */
47+
className?: string;
48+
/** Accessible label for the nav home. */
49+
'aria-label'?: string;
50+
}
51+
52+
export const CompassNavHome: React.FunctionComponent<CompassNavHomeProps> = ({
53+
'aria-label': ariaLabel = 'Home',
54+
tooltipContent = 'Home',
55+
className,
56+
onClick,
57+
...props
58+
}: CompassNavHomeProps) => {
59+
const buttonRef = useRef<HTMLButtonElement>(null);
60+
61+
return (
62+
<div className={css(styles.compassNav + '-home', className)} {...props}>
63+
<Tooltip content={tooltipContent} position="left" aria="none" aria-live="off" triggerRef={buttonRef}>
64+
<Button
65+
isCircle
66+
variant="plain"
67+
icon={<CompassHomeIcon />}
68+
aria-label={ariaLabel}
69+
onClick={onClick}
70+
ref={buttonRef}
71+
/>
72+
</Tooltip>
73+
</div>
74+
);
75+
};
76+
77+
CompassNavHome.displayName = 'CompassNavHome';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import styles from '@patternfly/react-styles/css/components/Compass/compass';
2+
import { css } from '@patternfly/react-styles';
3+
4+
export interface CompassNavMainProps extends React.HTMLProps<HTMLDivElement> {
5+
/** Content of the nav main section (typically tabs). */
6+
children: React.ReactNode;
7+
/** Additional classes added to the nav main section. */
8+
className?: string;
9+
}
10+
11+
export const CompassNavMain: React.FunctionComponent<CompassNavMainProps> = ({
12+
children,
13+
className,
14+
...props
15+
}: CompassNavMainProps) => (
16+
<div className={css(styles.compassNavMain, className)} {...props}>
17+
{children}
18+
</div>
19+
);
20+
21+
CompassNavMain.displayName = 'CompassNavMain';
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useRef } from 'react';
2+
import styles from '@patternfly/react-styles/css/components/Compass/compass';
3+
import { css } from '@patternfly/react-styles';
4+
import { Button } from '../Button';
5+
import { Tooltip } from '../Tooltip';
6+
7+
const CompassSearchIcon = () => (
8+
<svg
9+
width="1em"
10+
height="1em"
11+
className="pf-v6-svg"
12+
viewBox="0 0 20 20"
13+
fill="none"
14+
xmlns="http://www.w3.org/2000/svg"
15+
aria-hidden="true"
16+
>
17+
<path
18+
d="M14.166 14.166L17.4993 17.4993"
19+
stroke="currentcolor"
20+
strokeWidth="1.5"
21+
strokeLinecap="round"
22+
strokeLinejoin="round"
23+
/>
24+
<path
25+
d="M2.5 9.16667C2.5 12.8486 5.48477 15.8333 9.16667 15.8333C11.0108 15.8333 12.6801 15.0846 13.887 13.8744C15.0897 12.6685 15.8333 11.0044 15.8333 9.16667C15.8333 5.48477 12.8486 2.5 9.16667 2.5C5.48477 2.5 2.5 5.48477 2.5 9.16667Z"
26+
stroke="currentcolor"
27+
strokeWidth="1.5"
28+
strokeLinecap="round"
29+
strokeLinejoin="round"
30+
/>
31+
</svg>
32+
);
33+
34+
export interface CompassNavSearchProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onClick'> {
35+
/** Content to display in the tooltip. Defaults to "Search". */
36+
tooltipContent?: React.ReactNode;
37+
/** Click handler for the search button. */
38+
onClick?: React.MouseEventHandler<HTMLButtonElement>;
39+
/** Additional classes added to the nav search wrapper. */
40+
className?: string;
41+
/** Accessible label for the nav search. */
42+
'aria-label'?: string;
43+
}
44+
45+
export const CompassNavSearch: React.FunctionComponent<CompassNavSearchProps> = ({
46+
'aria-label': ariaLabel = 'Search',
47+
tooltipContent = 'Search',
48+
className,
49+
onClick,
50+
...props
51+
}: CompassNavSearchProps) => {
52+
const buttonRef = useRef<HTMLButtonElement>(null);
53+
54+
return (
55+
<div className={css(styles.compassNav + '-search', className)} {...props}>
56+
<Tooltip content={tooltipContent} aria="none" aria-live="off" triggerRef={buttonRef}>
57+
<Button
58+
isCircle
59+
variant="plain"
60+
icon={<CompassSearchIcon />}
61+
aria-label={ariaLabel}
62+
onClick={onClick}
63+
ref={buttonRef}
64+
/>
65+
</Tooltip>
66+
</div>
67+
);
68+
};
69+
70+
CompassNavSearch.displayName = 'CompassNavSearch';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { CompassNavContent } from '../CompassNavContent';
3+
import styles from '@patternfly/react-styles/css/components/Compass/compass';
4+
5+
test('Renders with children', () => {
6+
render(<CompassNavContent>Test content</CompassNavContent>);
7+
8+
expect(screen.getByText('Test content')).toBeVisible();
9+
});
10+
11+
test('Renders with custom class name when className prop is provided', () => {
12+
render(<CompassNavContent className="custom-class">Test</CompassNavContent>);
13+
14+
expect(screen.getByText('Test')).toHaveClass('custom-class');
15+
});
16+
17+
test(`Renders with default ${styles.compassNavContent} class`, () => {
18+
render(<CompassNavContent>Test</CompassNavContent>);
19+
20+
expect(screen.getByText('Test')).toHaveClass(styles.compassNavContent, { exact: true });
21+
});
22+
23+
test('Renders with additional props spread to the component', () => {
24+
render(<CompassNavContent aria-label="Test label">Test</CompassNavContent>);
25+
26+
expect(screen.getByText('Test')).toHaveAccessibleName('Test label');
27+
});
28+
29+
test('Matches the snapshot', () => {
30+
const { asFragment } = render(
31+
<CompassNavContent>
32+
<div>Nav content wrapper</div>
33+
</CompassNavContent>
34+
);
35+
36+
expect(asFragment()).toMatchSnapshot();
37+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { CompassNavHome } from '../CompassNavHome';
4+
import styles from '@patternfly/react-styles/css/components/Compass/compass';
5+
6+
test('Renders with default aria-label', () => {
7+
render(<CompassNavHome />);
8+
9+
expect(screen.getByRole('button', { name: 'Home' })).toBeVisible();
10+
});
11+
12+
test('Renders with custom aria-label when provided', () => {
13+
render(<CompassNavHome aria-label="Custom home" />);
14+
15+
expect(screen.getByRole('button', { name: 'Custom home' })).toBeVisible();
16+
});
17+
18+
test('Renders with default tooltip content', async () => {
19+
const user = userEvent.setup();
20+
21+
render(<CompassNavHome />);
22+
23+
const button = screen.getByRole('button');
24+
25+
user.hover(button);
26+
27+
await screen.findByRole('tooltip');
28+
29+
expect(screen.getByRole('tooltip')).toHaveTextContent('Home');
30+
});
31+
32+
test('Renders with custom tooltip content when provided', async () => {
33+
const user = userEvent.setup();
34+
35+
render(<CompassNavHome tooltipContent="Custom tooltip" />);
36+
37+
const button = screen.getByRole('button');
38+
39+
user.hover(button);
40+
41+
await screen.findByRole('tooltip');
42+
expect(screen.getByRole('tooltip')).toHaveTextContent('Custom tooltip');
43+
});
44+
45+
test('Renders with custom class name when className prop is provided', () => {
46+
const { container } = render(<CompassNavHome className="custom-class" />);
47+
48+
expect(container.firstChild).toHaveClass('custom-class');
49+
});
50+
51+
test(`Renders with default class`, () => {
52+
const { container } = render(<CompassNavHome />);
53+
54+
expect(container.firstChild).toHaveClass(styles.compassNav + '-home', { exact: true });
55+
});
56+
57+
test('Calls onClick handler when button is clicked', async () => {
58+
const user = userEvent.setup();
59+
const onClick = jest.fn();
60+
61+
render(<CompassNavHome onClick={onClick} />);
62+
63+
await user.click(screen.getByRole('button', { name: 'Home' }));
64+
65+
expect(onClick).toHaveBeenCalledTimes(1);
66+
});
67+
68+
test('Renders button with plain variant and circle shape', () => {
69+
render(<CompassNavHome />);
70+
71+
const button = screen.getByRole('button', { name: 'Home' });
72+
73+
expect(button).toHaveClass('pf-m-plain');
74+
expect(button).toHaveClass('pf-m-circle');
75+
});
76+
77+
test('Matches the snapshot', () => {
78+
const { asFragment } = render(<CompassNavHome />);
79+
80+
expect(asFragment()).toMatchSnapshot();
81+
});
82+
83+
test('Matches the snapshot with custom props', () => {
84+
const { asFragment } = render(
85+
<CompassNavHome aria-label="Custom home" tooltipContent="Go home" className="custom-class" />
86+
);
87+
88+
expect(asFragment()).toMatchSnapshot();
89+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { CompassNavMain } from '../CompassNavMain';
3+
import styles from '@patternfly/react-styles/css/components/Compass/compass';
4+
5+
test('Renders with children', () => {
6+
render(<CompassNavMain>Test content</CompassNavMain>);
7+
8+
expect(screen.getByText('Test content')).toBeVisible();
9+
});
10+
11+
test('Renders with custom class name when className prop is provided', () => {
12+
render(<CompassNavMain className="custom-class">Test</CompassNavMain>);
13+
14+
expect(screen.getByText('Test')).toHaveClass('custom-class');
15+
});
16+
17+
test(`Renders with default ${styles.compassNavMain} class`, () => {
18+
render(<CompassNavMain>Test</CompassNavMain>);
19+
20+
expect(screen.getByText('Test')).toHaveClass(styles.compassNavMain, { exact: true });
21+
});
22+
23+
test('Renders with additional props spread to the component', () => {
24+
render(<CompassNavMain aria-label="Test label">Test</CompassNavMain>);
25+
26+
expect(screen.getByText('Test')).toHaveAccessibleName('Test label');
27+
});
28+
29+
test('Matches the snapshot', () => {
30+
const { asFragment } = render(
31+
<CompassNavMain>
32+
<div>Main tabs content</div>
33+
</CompassNavMain>
34+
);
35+
36+
expect(asFragment()).toMatchSnapshot();
37+
});

0 commit comments

Comments
 (0)