Skip to content

Commit f1fcb4e

Browse files
committed
[dashboard] new workspace with options
1 parent 578be2a commit f1fcb4e

14 files changed

+626
-169
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import React, { FunctionComponent, useEffect, useMemo, useState } from "react";
8+
import Arrow from "./Arrow";
9+
10+
export interface DropDown2Element {
11+
id: string;
12+
element: JSX.Element;
13+
isSelectable?: boolean;
14+
}
15+
16+
export interface DropDown2Props {
17+
getElements: (searchString: string) => DropDown2Element[];
18+
searchPlaceholder?: string;
19+
disableSearch?: boolean;
20+
expanded?: boolean;
21+
onSelectionChange: (id: string) => void;
22+
}
23+
24+
export const DropDown2: FunctionComponent<DropDown2Props> = (props) => {
25+
const [showDropDown, setShowDropDown] = useState<boolean>(!!props.expanded);
26+
const onSelected = useMemo(
27+
() => (elementId: string) => {
28+
props.onSelectionChange(elementId);
29+
setShowDropDown(false);
30+
},
31+
[props],
32+
);
33+
const [search, setSearch] = useState<string>("");
34+
const filteredOptions = props.getElements(search);
35+
const [selectedElementTemp, setSelectedElementTemp] = useState<string | undefined>(filteredOptions[0]?.id);
36+
37+
// reset search when the drop down is expanded or closed
38+
useEffect(() => {
39+
setSearch("");
40+
}, [showDropDown]);
41+
42+
const onKeyDown = useMemo(
43+
() => (e: React.KeyboardEvent) => {
44+
if (!showDropDown) {
45+
return;
46+
}
47+
if (e.key === "ArrowDown") {
48+
e.preventDefault();
49+
let idx = filteredOptions.findIndex((e) => e.id === selectedElementTemp);
50+
while (idx++ < filteredOptions.length - 1) {
51+
const candidate = filteredOptions[idx];
52+
if (candidate.isSelectable) {
53+
setSelectedElementTemp(candidate.id);
54+
return;
55+
}
56+
}
57+
return;
58+
}
59+
if (e.key === "ArrowUp") {
60+
e.preventDefault();
61+
let idx = filteredOptions.findIndex((e) => e.id === selectedElementTemp);
62+
while (idx-- > 0) {
63+
const candidate = filteredOptions[idx];
64+
if (candidate.isSelectable) {
65+
setSelectedElementTemp(candidate.id);
66+
return;
67+
}
68+
}
69+
return;
70+
}
71+
if (e.key === "Escape") {
72+
setShowDropDown(false);
73+
e.preventDefault();
74+
}
75+
if (e.key === "Enter" && selectedElementTemp && filteredOptions.some((e) => e.id === selectedElementTemp)) {
76+
e.preventDefault();
77+
props.onSelectionChange(selectedElementTemp);
78+
setShowDropDown(false);
79+
}
80+
},
81+
[filteredOptions, props, selectedElementTemp, showDropDown],
82+
);
83+
84+
const handleBlur = useMemo(
85+
() => () => {
86+
// postpone a little, so it doesn't fire before a click event for the main element.
87+
setTimeout(() => setShowDropDown(false), 100);
88+
},
89+
[setShowDropDown],
90+
);
91+
92+
const toggleDropDown = useMemo(
93+
() => () => {
94+
setShowDropDown(!showDropDown);
95+
},
96+
[setShowDropDown, showDropDown],
97+
);
98+
99+
return (
100+
<div onKeyDown={onKeyDown} className="relative flex flex-col">
101+
<div
102+
className={
103+
"h-16 bg-gray-100 dark:bg-gray-800 flex items-center px-2 " +
104+
(showDropDown
105+
? "rounded-t-lg"
106+
: "rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer")
107+
}
108+
onClick={toggleDropDown}
109+
>
110+
{props.children}
111+
<div className="flex-grow" />
112+
<div className="mr-2">
113+
<Arrow direction={showDropDown ? "up" : "down"} />
114+
</div>
115+
</div>
116+
{showDropDown && (
117+
<div className="absolute w-full top-12 bg-gray-100 dark:bg-gray-800 max-h-72 overflow-auto rounded-b-lg mt-3 z-50 p-2">
118+
{
119+
<div className={props.disableSearch ? "h-0" : "h-12"} onBlur={handleBlur}>
120+
<input
121+
type="text"
122+
autoFocus
123+
className={
124+
"w-full focus rounded-lg " +
125+
(props.disableSearch &&
126+
" fixed -top-20") /* we don'T want search, but want to keep all the focus anvigation behavior. So we are moving the input element out of view. */
127+
}
128+
placeholder={props.disableSearch ? "" : props.searchPlaceholder}
129+
value={search}
130+
onChange={(e) => setSearch(e.target.value)}
131+
/>
132+
</div>
133+
}
134+
<ul>
135+
{filteredOptions.length > 0 ? (
136+
filteredOptions.map((element) => {
137+
let selectionClasses = `dark:bg-gray-800 cursor-pointer`;
138+
if (element.id === selectedElementTemp) {
139+
selectionClasses = `bg-gray-200 dark:bg-gray-700 cursor-pointer`;
140+
}
141+
if (!element.isSelectable) {
142+
selectionClasses = ``;
143+
}
144+
return (
145+
<li
146+
key={element.id}
147+
className={"h-16 rounded-lg flex items-center px-2 " + selectionClasses}
148+
onMouseDown={() => {
149+
if (element.isSelectable) {
150+
setSelectedElementTemp(element.id);
151+
onSelected(element.id);
152+
}
153+
}}
154+
onMouseOver={() => setSelectedElementTemp(element.id)}
155+
>
156+
{element.element}
157+
</li>
158+
);
159+
})
160+
) : (
161+
<li key="no-elements" className={"rounded-md "}>
162+
<div className="h-12 pl-8 py-3 text-gray-800 dark:text-gray-200">No results</div>
163+
</li>
164+
)}
165+
</ul>
166+
</div>
167+
)}
168+
</div>
169+
);
170+
};

components/dashboard/src/components/Modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ type ModalBodyProps = {
123123
export const ModalBody = ({ children, hideDivider = false }: ModalBodyProps) => {
124124
return (
125125
<div
126-
className={cn("overflow-y-auto border-gray-200 dark:border-gray-800 -mx-6 px-6 ", {
126+
className={cn("border-gray-200 dark:border-gray-800 -mx-6 px-6 ", {
127127
"border-t border-b mt-2 py-4": !hideDivider,
128128
})}
129129
>

0 commit comments

Comments
 (0)