1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
/**
* CompletionPopup is the list shown above the composer while an "@" or "/"
* selector is active: label, description, and a source tag for skills and
* agent commands. Selection happens with the keyboard (handled by the
* composer) or the mouse.
*
* @example
* <CompletionPopup items={items} active={0} onHover={setActive} onPick={pick} />
*/
import type { CompletionItem } from "../mentions";
interface CompletionPopupProps {
items: CompletionItem[];
active: number;
onHover: (index: number) => void;
onPick: (item: CompletionItem) => void;
}
const sourceLabels: Record<CompletionItem["source"], string> = {
file: "",
skill: "skill",
command: "command",
};
// The popup is a combobox listbox driven from the textarea (which carries
// aria-activedescendant), not a form <select>: hence the explicit roles on
// focusable-but-unreachable divs.
export function CompletionPopup({
items,
active,
onHover,
onPick,
}: CompletionPopupProps) {
return (
<div
className="completion"
// biome-ignore lint/a11y/useSemanticElements: a <select> cannot host rich rows nor stay open while typing
role="listbox"
tabIndex={-1}
aria-label={items[0]?.source === "file" ? "files" : "skills"}
>
{items.map((item, index) => (
<div
key={item.id}
id={`completion-${index}`}
// biome-ignore lint/a11y/useSemanticElements: see the listbox above
role="option"
tabIndex={-1}
aria-selected={index === active}
className={`completion-item${index === active ? " completion-item-active" : ""}`}
onMouseEnter={() => onHover(index)}
// Fire before the textarea loses focus on click.
onMouseDown={(event) => {
event.preventDefault();
onPick(item);
}}
>
<span className="completion-label">{item.label}</span>
{item.description && (
<span className="completion-description">{item.description}</span>
)}
{sourceLabels[item.source] && (
<span
className={`completion-source completion-source-${item.source}`}
>
{sourceLabels[item.source]}
</span>
)}
</div>
))}
</div>
);
}
|