Skip to content

Commit 4f9fdb3

Browse files
atrakhConvex, Inc.
authored and
Convex, Inc.
committed
design-system: support disabled combobox items and clicking on links in tooltips (#36547)
- Adds support for disabled items in Combobox - Add support for clicking on links and inside of tooltips (usually links inside of tooltips) that are clicked on while the combobox is opened. Normally, clicking outside of the combobox list just closes it, but this is necessary if we want tooltip that display next to options to have clickable links GitOrigin-RevId: 99de41d63125cbe1be3535fbcc61860eb1652e3c
1 parent 192fbfa commit 4f9fdb3

File tree

3 files changed

+50
-8
lines changed

3 files changed

+50
-8
lines changed

npm-packages/@convex-dev/design-system/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@convex-dev/design-system",
3-
"version": "0.1.5",
3+
"version": "0.1.7",
44
"type": "module",
55
"sideEffects": false,
66
"files": [

npm-packages/@convex-dev/design-system/src/Combobox.tsx

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { usePopper } from "react-popper";
1010

1111
const { test } = fuzzy;
1212

13-
export type Option<T> = { label: string; value: T };
13+
export type Option<T> = { label: string; value: T; disabled?: boolean };
1414

1515
export function Combobox<T>({
1616
options,
@@ -53,7 +53,12 @@ export function Combobox<T>({
5353
buttonProps?: Omit<ButtonProps, "href">;
5454
innerButtonClasses?: string;
5555
allowCustomValue?: boolean;
56-
Option?: React.ComponentType<{ label: string; value: T; inButton: boolean }>;
56+
Option?: React.ComponentType<{
57+
label: string;
58+
value: T;
59+
inButton: boolean;
60+
disabled?: boolean;
61+
}>;
5762
disabled?: boolean;
5863
unknownLabel?: (value: T) => string;
5964
processFilterOption?: (option: string) => string;
@@ -76,6 +81,10 @@ export function Combobox<T>({
7681

7782
const [isOpen, setIsOpen] = useState(false);
7883

84+
// Keeps track of the whether we should prevent the dropdown from closing
85+
// This is used to prevent the dropdown from closing when clicking on a link or tooltip
86+
const [preventClose, setPreventClose] = useState(false);
87+
7988
const { styles, attributes, update } = usePopper(
8089
referenceElement,
8190
popperElement,
@@ -123,14 +132,27 @@ export function Combobox<T>({
123132
}
124133
}, [isOpen, update]);
125134

135+
// Reset preventClose after a short delay
136+
useEffect(() => {
137+
if (preventClose) {
138+
const timer = setTimeout(() => {
139+
setPreventClose(false);
140+
}, 200);
141+
142+
return () => clearTimeout(timer);
143+
}
144+
}, [preventClose]);
145+
126146
return (
127147
<HeadlessCombobox
128148
value={
129149
options.find((o) => isEqual(selectedOption, o.value))?.value || null
130150
}
131151
onChange={(option) => {
132-
setSelectedOption(option);
133-
setQuery("");
152+
if (!preventClose) {
153+
setSelectedOption(option);
154+
setQuery("");
155+
}
134156
}}
135157
disabled={disabled}
136158
>
@@ -146,7 +168,7 @@ export function Combobox<T>({
146168
<>
147169
<HeadlessCombobox.Label
148170
hidden={labelHidden}
149-
className="text-left text-sm text-content-primary"
171+
className="text-content-primary text-left text-sm"
150172
>
151173
{label}
152174
</HeadlessCombobox.Label>
@@ -179,6 +201,7 @@ export function Combobox<T>({
179201
inButton
180202
label={selectedOptionData.label}
181203
value={selectedOptionData.value}
204+
disabled={selectedOptionData.disabled}
182205
/>
183206
) : (
184207
selectedOptionData?.label || (
@@ -228,7 +251,7 @@ export function Combobox<T>({
228251
)}
229252
<div className="min-w-fit">
230253
{!disableSearch && (
231-
<div className="sticky top-0 z-10 flex w-full items-center gap-2 border-b bg-background-secondary px-3 pt-1">
254+
<div className="bg-background-secondary sticky top-0 z-10 flex w-full items-center gap-2 border-b px-3 pt-1">
232255
<MagnifyingGlassIcon className="text-content-secondary" />
233256
<HeadlessCombobox.Input
234257
onChange={(event) => setQuery(event.target.value)}
@@ -246,10 +269,13 @@ export function Combobox<T>({
246269
<HeadlessCombobox.Option
247270
key={idx}
248271
value={option.value}
272+
disabled={option.disabled}
249273
className={({ active }) =>
250274
cn(
251275
"w-fit min-w-full relative cursor-pointer select-none py-1.5 px-3 text-content-primary",
252276
active && "bg-background-tertiary",
277+
option.disabled &&
278+
"cursor-not-allowed text-content-secondary opacity-75",
253279
)
254280
}
255281
>
@@ -259,6 +285,21 @@ export function Combobox<T>({
259285
"block w-full whitespace-nowrap",
260286
selected && "font-semibold",
261287
)}
288+
onPointerDownCapture={(e) => {
289+
// Stop propagation if clicking on a link or tooltip
290+
if (e.target instanceof HTMLElement) {
291+
const closest = e.target.closest(
292+
'a, [role="tooltip"]',
293+
);
294+
if (closest) {
295+
e.stopPropagation();
296+
e.preventDefault();
297+
298+
// Set preventClose to true
299+
setPreventClose(true);
300+
}
301+
}
302+
}}
262303
>
263304
{Option ? (
264305
<Option
@@ -291,7 +332,7 @@ export function Combobox<T>({
291332
)}
292333

293334
{filtered.length === 0 && !allowCustomValue && (
294-
<div className="overflow-hidden text-ellipsis py-1 pl-4 text-content-primary">
335+
<div className="text-content-primary overflow-hidden text-ellipsis py-1 pl-4">
295336
No options matching "{query}".
296337
</div>
297338
)}

npm-packages/@convex-dev/design-system/src/Tooltip.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function Tooltip({
3737
side={side}
3838
align={align}
3939
className="z-50 max-w-[16rem] break-words rounded border bg-background-secondary/70 p-1 text-center text-xs shadow-sm backdrop-blur-[2px] transition-opacity"
40+
role="tooltip"
4041
sideOffset={5}
4142
>
4243
{tip}

0 commit comments

Comments
 (0)