Skip to content

Commit

Permalink
Start investigating panel
Browse files Browse the repository at this point in the history
Looking for best accessible path forward here. We're in spaghetti territory - I'll clean up once I know directionally where I'm heading with this.
  • Loading branch information
rebeccaalpert committed Oct 24, 2024
1 parent ea5cbc0 commit c391b17
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 50 deletions.
68 changes: 18 additions & 50 deletions src/app/AppLayout/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { NavLink, Outlet, useLocation } from 'react-router-dom';
import { Outlet, useLoaderData } from 'react-router-dom';
import {
Brand,
Button,
Expand All @@ -8,22 +8,31 @@ import {
MastheadLogo,
MastheadMain,
MastheadToggle,
Nav,
NavExpandable,
NavItem,
NavList,
Page,
PageSidebar,
PageSidebarBody,
SkipToContent,
} from '@patternfly/react-core';
import { IAppRoute, IAppRouteGroup, routes } from '@app/routes';
import { BarsIcon } from '@patternfly/react-icons';
import logo from '@app/bgimages/Logo-Red_Hat-Composer_AI_Studio-A-Standard-RGB.svg';
import logoDark from '@app/bgimages/Logo-Red_Hat-Composer_AI_Studio-A-Reverse.svg';
import SidebarWithFlyout from './SidebarWithFlyout';
import { CannedChatbot } from '@app/types/CannedChatbot';

const AppLayout: React.FunctionComponent = () => {
const [sidebarOpen, setSidebarOpen] = React.useState(true);
const { chatbots } = useLoaderData() as { chatbots: CannedChatbot[] };

// set height of flyout to match nav
React.useEffect(() => {
const sourceElement = document.getElementById('nav-primary-simple');
if (sourceElement) {
const height = sourceElement.offsetHeight;

const targetElements = document.getElementsByClassName('flyoutMenu');
for (let i = 0; i < targetElements.length; i++) {
(targetElements[i] as HTMLElement).style.height = `${height}px`;
}
}
}, [chatbots]);

const masthead = (
<Masthead>
Expand All @@ -50,48 +59,7 @@ const AppLayout: React.FunctionComponent = () => {
</Masthead>
);

const location = useLocation();

const renderNavItem = (route: IAppRoute, index: number) => (
<NavItem key={`${route.label}-${index}`} id={`${route.label}-${index}`} isActive={route.path === location.pathname}>
<NavLink
to={route.path}
reloadDocument
className={({ isActive, isPending, isTransitioning }) =>
[isPending ? 'pending' : '', isActive ? 'active' : '', isTransitioning ? 'transitioning' : ''].join(' ')
}
>
{route.label}
</NavLink>
</NavItem>
);

const renderNavGroup = (group: IAppRouteGroup, groupIndex: number) => (
<NavExpandable
key={`${group.label}-${groupIndex}`}
id={`${group.label}-${groupIndex}`}
title={group.label}
isActive={group.routes.some((route) => route.path === location.pathname)}
>
{group.routes.map((route, idx) => route.label && renderNavItem(route, idx))}
</NavExpandable>
);

const Navigation = (
<Nav id="nav-primary-simple">
<NavList id="nav-list-simple">
{routes.map(
(route, idx) => route.label && (!route.routes ? renderNavItem(route, idx) : renderNavGroup(route, idx)),
)}
</NavList>
</Nav>
);

const Sidebar = (
<PageSidebar>
<PageSidebarBody>{Navigation}</PageSidebarBody>
</PageSidebar>
);
const Sidebar = <SidebarWithFlyout />;

const pageId = 'primary-app-container';

Expand Down
172 changes: 172 additions & 0 deletions src/app/AppLayout/SidebarWithFlyout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// @ts-nocheck

Check failure on line 1 in src/app/AppLayout/SidebarWithFlyout.tsx

View workflow job for this annotation

GitHub Actions / Lint

Do not use "@ts-nocheck" because it alters compilation errors
import React, { useEffect, useRef, useState } from 'react';
import { Brand, Button, FocusTrap, Nav, NavItem, NavList, PageSidebar } from '@patternfly/react-core';
import FlyoutStartScreen from '@app/FlyoutStartScreen.tsx/FlyoutStartScreen';
import logo from '@app/bgimages/Logo-Red_Hat-Composer_AI_Studio-A-Standard-RGB.svg';
import logoDark from '@app/bgimages/Logo-Red_Hat-Composer_AI_Studio-A-Reverse.svg';

const FlyoutHeader = ({ title, hideFlyout }) => {
return (
<div className="flyout-header">
{title}{' '}
<Button onClick={hideFlyout} variant="plain">
x
</Button>
</div>
);
};
const FlyoutMenu = ({ id, height, children, hideFlyout }) => {
const previouslyFocusedElement = React.useRef(null);

const handleFlyout = (event: KeyboardEvent) => {
const key = event.key;
if (key === 'Escape') {
event.stopPropagation();
event.preventDefault();
hideFlyout();
}
};

const focusTrapProps = {
tabIndex: -1,
'aria-modal': true,
role: 'dialog',
active: true,
'aria-labelledby': id,
focusTrapOptions: {
//fallbackFocus: () => panel.current,
onActivate: () => {
if (previouslyFocusedElement.current !== document.activeElement) {
previouslyFocusedElement.current = document.activeElement;
}
},
onDeactivate: () => {
previouslyFocusedElement.current &&
previouslyFocusedElement.current.focus &&
previouslyFocusedElement.current.focus();
},
clickOutsideDeactivates: true,
returnFocusOnDeactivate: false,
// FocusTrap's initialFocus can accept false as a value to prevent initial focus.
// We want to prevent this in case false is ever passed in.
initialFocus: undefined,
escapeDeactivates: false,
},
};

return (
<FocusTrap
id={id}
className="flyout-menu"
style={{
height: `${height}px`,
}}
onKeyDown={handleFlyout}
{...focusTrapProps}
>
{children}
</FocusTrap>
);
};

const SidebarWithFlyout = () => {
const [visibleFlyout, setVisibleFlyout] = useState(null);
const sidebarRef = useRef(null);
const flyoutMenuRef = useRef(null);
const [sidebarHeight, setSidebarHeight] = useState(0);

useEffect(() => {
const updateHeight = () => {
if (sidebarRef.current) {
setSidebarHeight(sidebarRef.current.offsetHeight);
}
};

const handleClick = (event) => {
if (sidebarRef.current && !sidebarRef.current.contains(event.target)) {
setVisibleFlyout(null);
}
};

// Set initial height and add event listeners for window resize
updateHeight();
window.addEventListener('resize', updateHeight);
window.addEventListener('click', handleClick);

return () => {
window.removeEventListener('click', handleClick);
};
}, []);

const toggleFlyout = (e) => {
if (visibleFlyout === e.target.innerText) {
setVisibleFlyout(null);
} else {
setVisibleFlyout(e.target.innerText);
}
};

// Adjust flyout height to match the sidebar height when flyout is visible
useEffect(() => {
if (visibleFlyout && sidebarRef.current && flyoutMenuRef.current) {
const sidebarHeight = sidebarRef.current.offsetHeight;
flyoutMenuRef.current.style.height = `${sidebarHeight}px`;
flyoutMenuRef.current.focus();
}
}, [visibleFlyout]);

return (
<PageSidebar>
<div id="page-sidebar" ref={sidebarRef} className="pf-c-page__sidebar" style={{ height: '100%' }}>
<div className="sidebar-masthead">
<div className="show-light">
<Brand src={logo} alt="Red Hat Composer AI Studio" heights={{ default: '36px' }} />
</div>
<div className="show-dark">
<Brand src={logoDark} alt="Red Hat Composer AI Studio" heights={{ default: '36px' }} />
</div>
</div>

<Nav id="nav-primary-simple" className="pf-c-nav" aria-label="Global">
<NavList>
<NavItem to="/">Home</NavItem>
<NavItem
to=""
onClick={toggleFlyout}
aria-haspopup="menu"
aria-expanded={visibleFlyout !== null}
isActive={visibleFlyout === 'Chats'}
// button would make more sense
// probably something easier to look at it.
>
Chats
</NavItem>
<NavItem
to=""
onClick={toggleFlyout}
aria-haspopup="menu"
aria-expanded={visibleFlyout !== null}
isActive={visibleFlyout === 'Assistants'}
>
Assistants
</NavItem>
</NavList>
</Nav>
{/* Flyout menu */}
{visibleFlyout && (
<FlyoutMenu
key={visibleFlyout}
id={visibleFlyout}
height={sidebarHeight}
hideFlyout={() => setVisibleFlyout(null)}
>
<FlyoutHeader title={visibleFlyout} hideFlyout={() => setVisibleFlyout(null)} />
<FlyoutStartScreen title="test" subtitle="test" primaryButtonText="test" />
</FlyoutMenu>
)}
</div>
</PageSidebar>
);
};

export default SidebarWithFlyout;
36 changes: 36 additions & 0 deletions src/app/FlyoutStartScreen.tsx/FlyoutStartScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Button } from '@patternfly/react-core';
import * as React from 'react';

interface FlyoutStartScreenProps {
image?: string;
imageAlt?: string;
title: string;
subtitle?: string;
primaryButtonText?: string;
secondaryButtonText?: string;
}
const FlyoutStartScreen: React.FunctionComponent<FlyoutStartScreenProps> = ({
image,
imageAlt,
subtitle,
title,
primaryButtonText,
secondaryButtonText,
}: FlyoutStartScreenProps) => {
return (
<div className="start-screen">
{image && <img src={image} alt={imageAlt} />}
<h1>{title}</h1>
{subtitle && <p>{subtitle}</p>}
{primaryButtonText && <Button>{primaryButtonText}</Button>}
{secondaryButtonText && (
<>
<p>or</p>
<Button>{secondaryButtonText}</Button>
</>
)}
</div>
);
};

export default FlyoutStartScreen;
54 changes: 54 additions & 0 deletions src/app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,57 @@ pf-v6-c-page__main-container.pf-m-fill {
max-height: 80vh;
overflow-y: auto;
}

#page-sidebar {
overflow: visible;
@media screen and (max-width: 900px) {
width: 100%;
}
}

.flyout-header {
padding: var(--pf-t--global--spacer--md);
font-weight: bold;
display: flex;
justify-content: space-between;
}

.flyout-menu {
padding: var(--pf-t--global--spacer--md);
overflow: hidden;
overflow-y: auto;
position: absolute;
top: 0;
left: 100%;
width: 250px;
background: green;
display: flex;
flex-direction: column;

@media screen and (max-width: 900px) {
width: 100%;
left: 0;
}
}

.start-screen {
display: flex;
flex-direction: column;
gap: var(--pf-t--global--spacer--sm);
justify-content: center;
align-items: center;
flex: 1;
}

.pf-v6-c-masthead {
@media screen and (min-width: 1200px) {
display: none;
}
}

.sidebar-masthead {
padding: var(--pf-t--global--spacer--md);
@media screen and (max-width: 1199px) {
display: none;
}
}

0 comments on commit c391b17

Please sign in to comment.