nandi/oripublic Fork 0
76d62ac0da1600e1c208017584c3fa22e54feaa3
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

CompletionPopup.tsx · 74 lines · 2.1 KBTypeScript Blame HistoryRaw
 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>
	);
}