tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
oops forgot to push a file
cozylittle.house
3 weeks ago
1f5b557c
f9bced1e
+152
1 changed file
expand all
collapse all
unified
split
components
Combobox.tsx
+152
components/Combobox.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { createContext, useContext, useEffect, useRef, useState } from "react";
2
+
import * as Popover from "@radix-ui/react-popover";
3
+
import { NestedCardThemeProvider } from "components/ThemeManager/ThemeProvider";
4
+
import { create } from "zustand";
5
+
6
+
export const useComboboxState = create(() => ({
7
+
open: false,
8
+
}));
9
+
10
+
export const Combobox = ({
11
+
results,
12
+
onSelect,
13
+
children,
14
+
onOpenChange,
15
+
highlighted,
16
+
setHighlighted,
17
+
trigger,
18
+
triggerClassName,
19
+
sideOffset,
20
+
}: {
21
+
children: React.ReactNode;
22
+
trigger?: React.ReactNode;
23
+
triggerClassName?: string;
24
+
results: string[];
25
+
onSelect?: () => void;
26
+
onOpenChange?: (open: boolean) => void;
27
+
highlighted: string | undefined;
28
+
setHighlighted: (h: string | undefined) => void;
29
+
sideOffset?: number;
30
+
}) => {
31
+
let ref = useRef<HTMLDivElement>(null);
32
+
33
+
let open = useComboboxState((s) => s.open);
34
+
35
+
useEffect(() => {
36
+
if (!highlighted || !results.find((result) => result === highlighted))
37
+
setHighlighted(results[0]);
38
+
if (results.length === 1) {
39
+
setHighlighted(results[0]);
40
+
}
41
+
}, [results, setHighlighted, highlighted]);
42
+
43
+
useEffect(() => {
44
+
let listener = async (e: KeyboardEvent) => {
45
+
let reverseDir = ref.current?.dataset.side === "top";
46
+
let currentHighlightIndex = results.findIndex(
47
+
(result) => highlighted && result === highlighted,
48
+
);
49
+
50
+
if (reverseDir ? e.key === "ArrowUp" : e.key === "ArrowDown") {
51
+
setHighlighted(
52
+
results[
53
+
currentHighlightIndex === results.length - 1 ||
54
+
currentHighlightIndex === undefined
55
+
? 0
56
+
: currentHighlightIndex + 1
57
+
],
58
+
);
59
+
return;
60
+
}
61
+
if (reverseDir ? e.key === "ArrowDown" : e.key === "ArrowUp") {
62
+
setHighlighted(
63
+
results[
64
+
currentHighlightIndex === 0 ||
65
+
currentHighlightIndex === undefined ||
66
+
currentHighlightIndex === -1
67
+
? results.length - 1
68
+
: currentHighlightIndex - 1
69
+
],
70
+
);
71
+
return;
72
+
}
73
+
74
+
// on enter, select the highlighted item
75
+
if (e.key === "Enter") {
76
+
onSelect?.();
77
+
useComboboxState.setState({
78
+
open: false,
79
+
});
80
+
}
81
+
};
82
+
83
+
window.addEventListener("keydown", listener);
84
+
85
+
return () => window.removeEventListener("keydown", listener);
86
+
}, [highlighted, setHighlighted, results]);
87
+
88
+
return (
89
+
<Popover.Root
90
+
open={open}
91
+
onOpenChange={(newOpen) => {
92
+
useComboboxState.setState({
93
+
open: newOpen,
94
+
});
95
+
onOpenChange?.(newOpen);
96
+
}}
97
+
>
98
+
<Popover.Trigger asChild className={`${triggerClassName}`}>
99
+
<div>{trigger}</div>
100
+
</Popover.Trigger>
101
+
<Popover.Portal>
102
+
<Popover.Content
103
+
align="start"
104
+
sideOffset={sideOffset ? sideOffset : 16}
105
+
collisionPadding={16}
106
+
ref={ref}
107
+
onOpenAutoFocus={(e) => e.preventDefault()}
108
+
className={`
109
+
commandMenuContent group/cmd-menu
110
+
z-20 w-[264px]
111
+
flex data-[side=top]:items-end items-start
112
+
`}
113
+
>
114
+
<NestedCardThemeProvider>
115
+
<div className="commandMenuResults w-full max-h-(--radix-popover-content-available-height) overflow-auto flex flex-col group-data-[side=top]/cmd-menu:flex-col-reverse bg-bg-page py-1 gap-0.5 border border-border rounded-md shadow-md">
116
+
{children}
117
+
</div>
118
+
</NestedCardThemeProvider>
119
+
</Popover.Content>
120
+
</Popover.Portal>
121
+
</Popover.Root>
122
+
);
123
+
};
124
+
125
+
export const ComboboxResult = (props: {
126
+
result: string;
127
+
children: React.ReactNode;
128
+
onSelect: () => void;
129
+
highlighted: string | undefined;
130
+
setHighlighted: (state: string | undefined) => void;
131
+
className?: string;
132
+
}) => {
133
+
let isHighlighted = props.highlighted === props.result;
134
+
135
+
return (
136
+
<button
137
+
className={`comboboxResult menuItem text-secondary font-normal! py-0.5! mx-1 ${props.className} ${isHighlighted && "bg-[var(--accent-light)]!"}`}
138
+
onMouseOver={() => {
139
+
props.setHighlighted(props.result);
140
+
}}
141
+
onMouseDown={(e) => {
142
+
e.preventDefault();
143
+
props.onSelect();
144
+
useComboboxState.setState({
145
+
open: false,
146
+
});
147
+
}}
148
+
>
149
+
<div className="truncate">{props.children}</div>
150
+
</button>
151
+
);
152
+
};