diff --git a/components/dashboard/README.md b/components/dashboard/README.md index f33fbab9b2516b..aad6079c3bda1b 100644 --- a/components/dashboard/README.md +++ b/components/dashboard/README.md @@ -1,4 +1,4 @@ -# Dashboard Redesign POC +# Dashboard The dashboard is written in TypeScript and React. For styling it uses TailwindCSS which is a bit nicer than inlining CSS as it supports pseudo classes and a is a little more abstract/reusable. @@ -28,4 +28,4 @@ const FeaturePreview = React.lazy(() => import('./settings/FeaturePreview')); const GitIntegration = React.lazy(() => import('./settings/GitIntegration')); ``` -Global state is passed through `React.Context`. \ No newline at end of file +Global state is passed through `React.Context`. diff --git a/components/dashboard/src/App.tsx b/components/dashboard/src/App.tsx index 1b402259b929bd..88b68ed9ea765f 100644 --- a/components/dashboard/src/App.tsx +++ b/components/dashboard/src/App.tsx @@ -75,7 +75,7 @@ function App() { } /> - + @@ -102,7 +102,8 @@ const renderMenu = () => ( }, { title: 'Settings', - link: '/account' + link: '/settings', + matches: /^(?!.*workspace).*$/ }, ]} right={[ diff --git a/components/dashboard/src/components/ContextMenu.tsx b/components/dashboard/src/components/ContextMenu.tsx index dd5b3426ee25f0..edc0a6c4d4c4d3 100644 --- a/components/dashboard/src/components/ContextMenu.tsx +++ b/components/dashboard/src/components/ContextMenu.tsx @@ -4,7 +4,8 @@ * See License-AGPL.txt in the project root for license information. */ -import { MouseEvent, useState } from 'react'; +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; export interface ContextMenuProps { children: React.ReactChild[] | React.ReactChild; @@ -20,8 +21,9 @@ export interface ContextMenuEntry { */ separator?: boolean; customFontStyle?: string; - onClick?: (event: MouseEvent)=>void; + onClick?: (event: React.MouseEvent) => void; href?: string; + link?: string; } function ContextMenu(props: ContextMenuProps) { @@ -30,23 +32,26 @@ function ContextMenu(props: ContextMenuProps) { setExpanded(!expanded); } - if (expanded) { - // HACK! I want to skip the bubbling phase of the current click - setTimeout(() => { - window.addEventListener('click', () => setExpanded(false), { once: true }); - }, 0); - } - - const enhancedEntries = props.menuEntries.map(e => { - return { - ... e, - onClick: (event: MouseEvent) => { - e.onClick && e.onClick(event); - toggleExpanded(); - event.preventDefault(); - } + const handler = (evt: KeyboardEvent) => { + if (evt.key === 'Escape') { + setExpanded(false); } - }) + } + + const clickHandler = (evt: MouseEvent) => { + setExpanded(false); + } + + useEffect(() => { + window.addEventListener('keydown', handler); + window.addEventListener('click', clickHandler); + // Remove event listeners on cleanup + return () => { + window.removeEventListener('keydown', handler); + window.removeEventListener('click', clickHandler); + }; + }, []); // Empty array ensures that effect is only run on mount and unmount + const font = "text-gray-600 hover:text-gray-800" @@ -56,26 +61,35 @@ function ContextMenu(props: ContextMenuProps) {
{ toggleExpanded(); - e.preventDefault(); + e.stopPropagation(); }}> {props.children}
- {expanded? + {expanded ?
- {enhancedEntries.map((e, index) => { - const clickable = e.href || e.onClick; - const entry =
-
{e.title}
{e.active ?
: null} + {props.menuEntries.map((e, index) => { + const clickable = e.href || e.onClick || e.link; + const entry =
+
{e.title}
{e.active ?
: null}
- if (!clickable) { - return entry; + const key = `entry-${menuId}-${index}-${e.title}`; + if (e.link) { + return + {entry} + ; + } else if (e.href) { + return + {entry} + ; + } else { + return
+ {entry} +
} - return - {entry} - + })}
- : + : null }
diff --git a/components/dashboard/src/components/Menu.tsx b/components/dashboard/src/components/Menu.tsx index 99ab069195a1f6..263774c2365f31 100644 --- a/components/dashboard/src/components/Menu.tsx +++ b/components/dashboard/src/components/Menu.tsx @@ -11,13 +11,16 @@ import { gitpodHostUrl } from "../service/service"; import { UserContext } from "../user-context"; import ContextMenu from "./ContextMenu"; import * as images from '../images'; +import { useLocation } from "react-router"; interface Entry { - title: string, link: string + title: string, link: string, matches?: RegExp } function MenuItem(entry: Entry) { - let classes = "flex block text-sm font-medium lg:px-3 px-0 py-1.5 rounded-md"; - if (window.location.pathname.toLowerCase() === entry.link.toLowerCase()) { + const location = useLocation(); + let classes = "flex block text-sm font-medium px-3 px-0 py-1.5 rounded-md"; + if (location.pathname.toLowerCase() === entry.link.toLowerCase() || + entry.matches && entry.matches.test(location.pathname.toLowerCase())) { classes += " bg-gray-200"; } else { classes += " text-gray-600 hover:bg-gray-100 "; @@ -38,35 +41,20 @@ function Menu(props: { left: Entry[], right: Entry[] }) { return (
-