Skip to content

Commit 19a267b

Browse files
svenefftingeroboquat
authored andcommitted
[dashboard] new workspace with options
1 parent edbaaae commit 19a267b

15 files changed

+670
-169
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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, RefObject, useCallback, useEffect, useMemo, useRef, 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 nodeRef: RefObject<HTMLDivElement> = useRef(null);
27+
const onSelected = useCallback(
28+
(elementId: string) => {
29+
props.onSelectionChange(elementId);
30+
setShowDropDown(false);
31+
},
32+
[props],
33+
);
34+
const [search, setSearch] = useState<string>("");
35+
const filteredOptions = useMemo(() => props.getElements(search), [props, search]);
36+
const [selectedElementTemp, setSelectedElementTemp] = useState<string | undefined>(filteredOptions[0]?.id);
37+
38+
// reset search when the drop down is expanded or closed
39+
useEffect(() => {
40+
setSearch("");
41+
if (showDropDown && selectedElementTemp) {
42+
document.getElementById(selectedElementTemp)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
43+
}
44+
// we only want this behavior when showDropDown changes to true.
45+
// eslint-disable-next-line react-hooks/exhaustive-deps
46+
}, [showDropDown]);
47+
48+
const toggleDropDown = useCallback(() => {
49+
setShowDropDown(!showDropDown);
50+
}, [setShowDropDown, showDropDown]);
51+
52+
const setFocussedElement = useCallback(
53+
(element: string) => {
54+
setSelectedElementTemp(element);
55+
document.getElementById(element)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
56+
document.getElementById(element)?.focus();
57+
},
58+
[setSelectedElementTemp],
59+
);
60+
61+
const onKeyDown = useCallback(
62+
(e: React.KeyboardEvent) => {
63+
if (showDropDown && e.key === "ArrowDown") {
64+
e.preventDefault();
65+
let idx = filteredOptions.findIndex((e) => e.id === selectedElementTemp);
66+
while (idx++ < filteredOptions.length - 1) {
67+
const candidate = filteredOptions[idx];
68+
if (candidate.isSelectable) {
69+
setFocussedElement(candidate.id);
70+
return;
71+
}
72+
}
73+
return;
74+
}
75+
if (showDropDown && e.key === "ArrowUp") {
76+
e.preventDefault();
77+
let idx = filteredOptions.findIndex((e) => e.id === selectedElementTemp);
78+
while (idx-- > 0) {
79+
const candidate = filteredOptions[idx];
80+
if (candidate.isSelectable) {
81+
setFocussedElement(candidate.id);
82+
return;
83+
}
84+
}
85+
return;
86+
}
87+
if (showDropDown && e.key === "Escape") {
88+
setShowDropDown(false);
89+
e.preventDefault();
90+
}
91+
if (e.key === "Enter") {
92+
if (showDropDown && selectedElementTemp && filteredOptions.some((e) => e.id === selectedElementTemp)) {
93+
e.preventDefault();
94+
props.onSelectionChange(selectedElementTemp);
95+
setShowDropDown(false);
96+
}
97+
if (!showDropDown) {
98+
toggleDropDown();
99+
e.preventDefault();
100+
}
101+
}
102+
if (e.key === " ") {
103+
toggleDropDown();
104+
e.preventDefault();
105+
}
106+
},
107+
[filteredOptions, props, selectedElementTemp, setFocussedElement, showDropDown, toggleDropDown],
108+
);
109+
110+
const handleBlur = useCallback(
111+
(e: React.FocusEvent) => {
112+
// postpone a little, so it doesn't fire before a click event for the main element.
113+
setTimeout(() => {
114+
// only close if the focussed element is not child
115+
if (!nodeRef?.current?.contains(window.document.activeElement)) {
116+
setShowDropDown(false);
117+
}
118+
}, 100);
119+
},
120+
[setShowDropDown],
121+
);
122+
123+
return (
124+
<div
125+
onKeyDown={onKeyDown}
126+
onBlur={handleBlur}
127+
ref={nodeRef}
128+
tabIndex={0}
129+
className={"relative flex flex-col rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-300"}
130+
>
131+
<div
132+
className={
133+
"h-16 bg-gray-100 dark:bg-gray-800 flex items-center px-2 " +
134+
(showDropDown
135+
? "rounded-t-lg"
136+
: "rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer")
137+
}
138+
onClick={toggleDropDown}
139+
>
140+
{props.children}
141+
<div className="flex-grow" />
142+
<div className="mr-2">
143+
<Arrow direction={showDropDown ? "up" : "down"} />
144+
</div>
145+
</div>
146+
{showDropDown && (
147+
<>
148+
<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">
149+
{!props.disableSearch && (
150+
<div className="h-12">
151+
<input
152+
type="text"
153+
autoFocus
154+
className={"w-full focus rounded-lg"}
155+
placeholder={props.searchPlaceholder}
156+
value={search}
157+
onChange={(e) => setSearch(e.target.value)}
158+
/>
159+
</div>
160+
)}
161+
<ul>
162+
{filteredOptions.length > 0 ? (
163+
filteredOptions.map((element) => {
164+
let selectionClasses = `dark:bg-gray-800 cursor-pointer`;
165+
if (element.id === selectedElementTemp) {
166+
selectionClasses = `bg-gray-200 dark:bg-gray-700 cursor-pointer focus:outline-none focus:ring-0`;
167+
}
168+
if (!element.isSelectable) {
169+
selectionClasses = ``;
170+
}
171+
return (
172+
<li
173+
key={element.id}
174+
id={element.id}
175+
tabIndex={0}
176+
className={"h-16 rounded-lg flex items-center px-2 " + selectionClasses}
177+
onMouseDown={() => {
178+
if (element.isSelectable) {
179+
setFocussedElement(element.id);
180+
onSelected(element.id);
181+
}
182+
}}
183+
onMouseOver={() => setFocussedElement(element.id)}
184+
onFocus={() => setFocussedElement(element.id)}
185+
>
186+
{element.element}
187+
</li>
188+
);
189+
})
190+
) : (
191+
<li key="no-elements" className={"rounded-md "}>
192+
<div className="h-12 pl-8 py-3 text-gray-800 dark:text-gray-200">No results</div>
193+
</li>
194+
)}
195+
</ul>
196+
</div>
197+
</>
198+
)}
199+
</div>
200+
);
201+
};

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)