Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"tslib": "^2.8.1"
},
"devDependencies": {
"@patternfly/patternfly": "6.5.0-prerelease.27",
"@patternfly/patternfly": "6.5.0-prerelease.32",
"case-anything": "^3.1.2",
"css": "^3.0.0",
"fs-extra": "^11.3.0"
Expand Down
68 changes: 42 additions & 26 deletions packages/react-core/src/components/Compass/Compass.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import compassBackgroundImageDark from '@patternfly/react-tokens/dist/esm/c_comp
export interface CompassProps extends React.HTMLProps<HTMLDivElement> {
/** Additional classes added to the Compass. */
className?: string;
/** Content of the docked navigation area of the layout */
dock?: React.ReactNode;
Comment thread
mcoker marked this conversation as resolved.
/** Content placed at the top of the layout */
header?: React.ReactNode;
/** Flag indicating if the header is expanded */
Expand Down Expand Up @@ -38,6 +40,7 @@ export interface CompassProps extends React.HTMLProps<HTMLDivElement> {

export const Compass: React.FunctionComponent<CompassProps> = ({
className,
dock,
header,
isHeaderExpanded = true,
sidebarStart,
Expand All @@ -64,32 +67,45 @@ export const Compass: React.FunctionComponent<CompassProps> = ({
}

const compassContent = (
<div className={css(styles.compass, className)} {...props} style={{ ...props.style, ...backgroundImageStyles }}>
<div
className={css(styles.compassHeader, isHeaderExpanded && 'pf-m-expanded')}
{...(!isHeaderExpanded && { inert: 'true' })}
>
{header}
</div>
<div
className={css(styles.compassSidebar, styles.modifiers.start, isSidebarStartExpanded && 'pf-m-expanded')}
{...(!isSidebarStartExpanded && { inert: 'true' })}
>
{sidebarStart}
</div>
<div className={css(styles.compassMain)}>{main}</div>
<div
className={css(styles.compassSidebar, styles.modifiers.end, isSidebarEndExpanded && 'pf-m-expanded')}
{...(!isSidebarEndExpanded && { inert: 'true' })}
>
{sidebarEnd}
</div>
<div
className={css(styles.compassFooter, isFooterExpanded && 'pf-m-expanded')}
{...(!isFooterExpanded && { inert: 'true' })}
>
{footer}
</div>
<div
className={css(styles.compass, dock !== undefined && styles.modifiers.dock, className)}
{...props}
style={{ ...props.style, ...backgroundImageStyles }}
>
{dock && <div className={css(`${styles.compass}__dock`)}>{dock}</div>}
{header && (
<div
className={css(styles.compassHeader, isHeaderExpanded && 'pf-m-expanded')}
{...(!isHeaderExpanded && { inert: 'true' })}
>
{header}
</div>
)}
{sidebarStart && (
<div
className={css(styles.compassSidebar, styles.modifiers.start, isSidebarStartExpanded && 'pf-m-expanded')}
{...(!isSidebarStartExpanded && { inert: 'true' })}
>
{sidebarStart}
</div>
)}
{main && <div className={css(styles.compassMain)}>{main}</div>}
{sidebarEnd && (
<div
className={css(styles.compassSidebar, styles.modifiers.end, isSidebarEndExpanded && 'pf-m-expanded')}
{...(!isSidebarEndExpanded && { inert: 'true' })}
>
{sidebarEnd}
</div>
)}
{footer && (
<div
className={css(styles.compassFooter, isFooterExpanded && 'pf-m-expanded')}
{...(!isFooterExpanded && { inert: 'true' })}
>
{footer}
</div>
)}
</div>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Compass dock={<div>Dock content</div>} 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(<Compass data-testid="compass" />);
expect(screen.getByTestId('compass')).not.toHaveClass(styles.modifiers.dock);
});
23 changes: 22 additions & 1 deletion packages/react-core/src/components/Compass/examples/Compass.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@ 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';
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';

import './compass.css';

Expand Down Expand Up @@ -51,6 +58,20 @@ When `footer` is used, its content will fill the width of the screen. By default

```

### With docked nav

As an alternative navigation, a `CompassDock` component may be passed to `Compass` via the `dock` prop. This component will allocate a thin sidebar intended for icons to the start of the screen. The `CompassDock` component has three sub-areas, from top to bottom: logo, main, and tools. Typically a `Brand` or other logo should be passed to the `logo` prop. The `main` and `tools` are flexible and can be passed `Nav`, `ActionList`, or `Toolbar` vertical variants depending on the use case. Account or profile avatars and links would typically be passed as part of the `tools` section at the bottom of the page.

```ts file="CompassDockLayout.tsx"

```

### Docked nav demo

```ts isFullscreen file="CompassDockDemo.tsx"

```

Comment thread
mcoker marked this conversation as resolved.
Outdated
## Composable structure

When building a more custom implementation with Compass components, there are some intended or expected structures that must remain present.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { useRef, useState, useEffect } from 'react';
import {
Compass,
CompassContent,
CompassMainHeader,
CompassPanel,
Title,
NavItem,
NavList,
Nav,
Brand,
MastheadLogo,
MastheadBrand,
MastheadContent,
MastheadMain,
Masthead,
Toolbar,
ToolbarContent,
ToolbarItem,
ToolbarGroup,
Dropdown,
DropdownList,
MenuToggle,
MenuToggleElement,
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<number>(0);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
const menuRef = useRef<HTMLDivElement>(null);
const toggleRef = useRef<HTMLButtonElement>(null);

const onNavSelect = (_event: React.FormEvent<HTMLInputElement>, selectedItem: NavOnSelectProps) => {
typeof selectedItem.itemId === 'number' && setActiveItem(selectedItem.itemId);
};

const onDropdownToggle = () => {
setIsDropdownOpen(!isDropdownOpen);
};

const onDropdownSelect = () => {
setIsDropdownOpen(!isDropdownOpen);
};

const handleMenuKeys = (event: KeyboardEvent) => {
if (!isOpen) {
return;
}
if (menuRef.current?.contains(event.target as Node) || toggleRef.current?.contains(event.target as Node)) {
if (event.key === 'Escape') {
setIsOpen(!isOpen);
toggleRef.current?.focus();
}
}
};

const handleClickOutside = (event: MouseEvent) => {
if (isOpen && !menuRef.current?.contains(event.target as Node)) {
setIsOpen(false);
}
};

useEffect(() => {
window.addEventListener('keydown', handleMenuKeys);
window.addEventListener('click', handleClickOutside);

return () => {
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClickOutside);
};
}, [isOpen, menuRef]);

Check warning on line 91 in packages/react-core/src/components/Compass/examples/CompassDockDemo.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has missing dependencies: 'handleClickOutside' and 'handleMenuKeys'. Either include them or remove the dependency array

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const userDropdownItems = [
<>
<DropdownItem key="group 2 profile">My profile</DropdownItem>
<DropdownItem key="group 2 user">User management</DropdownItem>
<DropdownItem key="group 2 logout">Logout</DropdownItem>
</>
];

const navItem1Ref = useRef<HTMLAnchorElement>(null);
const navItem2Ref = useRef<HTMLAnchorElement>(null);
const navItem3Ref = useRef<HTMLAnchorElement>(null);
const navItem4Ref = useRef<HTMLAnchorElement>(null);
const dockContent = (
<Masthead id="icon-router-link" variant="docked">
<MastheadMain>
<MastheadBrand>
<MastheadLogo component={(props) => <a {...props} href="#" />}>
<Brand src={pfLogo} alt="PatternFly" heights={{ default: '36px' }} />
Comment thread
mcoker marked this conversation as resolved.
Outdated
</MastheadLogo>
</MastheadBrand>
</MastheadMain>
<Divider />
<MastheadContent>
<Toolbar id="toolbar" isVertical>
<ToolbarContent>
<ToolbarItem>
<Nav onSelect={onNavSelect} variant="docked" aria-label="Icon global" ouiaId="IconNav">
<NavList>
<NavItem
key="nav-icon-link1"
preventDefault
id="nav-icon-link1"
to="#nav-icon-link1"
itemId={0}
isActive={activeItem === 0}
icon={<CubeIcon />}
ref={navItem1Ref}
/>
<NavItem
key="nav-icon-link2"
preventDefault
id="nav-icon-link2"
to="#nav-icon-link2"
itemId={1}
isActive={activeItem === 1}
icon={<FolderIcon />}
ref={navItem2Ref}
/>
<NavItem
key="nav-icon-link3"
preventDefault
id="nav-icon-link3"
to="#nav-icon-link3"
itemId={0}
isActive={activeItem === 2}
icon={<CloudIcon />}
ref={navItem3Ref}
/>
<NavItem
key="nav-icon-link4"
preventDefault
id="nav-icon-link4"
to="#nav-icon-link4"
itemId={0}
isActive={activeItem === 3}
icon={<CodeIcon />}
ref={navItem4Ref}
/>
</NavList>
</Nav>
<Tooltip triggerRef={navItem1Ref} content="Link 1"></Tooltip>
<Tooltip triggerRef={navItem2Ref} content="Link 2"></Tooltip>
<Tooltip triggerRef={navItem3Ref} content="Link 3"></Tooltip>
<Tooltip triggerRef={navItem4Ref} content="Link 4"></Tooltip>
</ToolbarItem>
<ToolbarGroup
variant="action-group-plain"
align={{ default: 'alignEnd' }}
gap={{ default: 'gapNone', md: 'gapMd' }}
>
<ToolbarGroup variant="action-group-plain" visibility={{ default: 'hidden', lg: 'visible' }}>
<ToolbarItem>
<Button aria-label="Settings" isSettings variant="plain" />
</ToolbarItem>
<ToolbarItem>
<Button aria-label="Help" variant={ButtonVariant.plain} icon={<QuestionCircleIcon />} />
</ToolbarItem>
</ToolbarGroup>
</ToolbarGroup>
<ToolbarItem>
<Dropdown
isOpen={isDropdownOpen}
onSelect={onDropdownSelect}
onOpenChange={(isOpen: boolean) => setIsDropdownOpen(isOpen)}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={onDropdownToggle}
isExpanded={isDropdownOpen}
icon={<Avatar src={imgAvatar} alt="" size="sm" />}
variant="plain"
></MenuToggle>
)}
>
<DropdownList>{userDropdownItems}</DropdownList>
</Dropdown>
</ToolbarItem>
</ToolbarContent>
</Toolbar>
</MastheadContent>
</Masthead>
);

const mainContent = (
<>
<CompassMainHeader title={<Title headingLevel="h1">Content title</Title>} />
<CompassContent>
<CompassPanel>Content</CompassPanel>
</CompassContent>
</>
);

return (
<Compass
dock={dockContent}
main={mainContent}
backgroundSrcDark="/assets/images/pf-background.svg"
backgroundSrcLight="/assets/images/pf-background.svg"
/>
);
};
Loading
Loading