feat: keyboard navigation for mention and slash command dropdowns
Arrow up/down cycles through items with highlight. Enter selects the highlighted item. Escape dismisses the dropdown. Index resets when the query changes.
This commit is contained in:
@@ -9,7 +9,7 @@ import { useMemberStore } from "../stores/member.ts";
|
||||
import { useThreadStore } from "../stores/thread.ts";
|
||||
import { GiphyPicker, type Gif } from "./GiphyPicker";
|
||||
import { CommandDropdown } from "./CommandDropdown";
|
||||
import { findCommand } from "../lib/slashCommands";
|
||||
import { findCommand, SLASH_COMMANDS } from "../lib/slashCommands";
|
||||
import { PollDisplay, CreatePollModal } from "./Poll.tsx";
|
||||
import { MentionDropdown } from "./MentionDropdown";
|
||||
import { useReadStatesStore } from "../stores/readStates.ts";
|
||||
@@ -274,7 +274,11 @@ export function ChatArea() {
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
||||
const [commandQuery, setCommandQuery] = useState<string | null>(null);
|
||||
const [dropdownIndex, setDropdownIndex] = useState(0);
|
||||
const [showPollModal, setShowPollModal] = useState(false);
|
||||
|
||||
// Reset dropdown index when query changes
|
||||
useEffect(() => { setDropdownIndex(0); }, [mentionQuery, commandQuery]);
|
||||
const [slowmodeRemaining, setSlowmodeRemaining] = useState(0);
|
||||
const [selectMode, setSelectMode] = useState(false);
|
||||
const [showThreads, setShowThreads] = useState(false);
|
||||
@@ -479,6 +483,7 @@ export function ChatArea() {
|
||||
const next = `${before}@${username}${separator}${after}`;
|
||||
setInput(next);
|
||||
setMentionQuery(null);
|
||||
setDropdownIndex(0);
|
||||
// Also set the DOM value immediately so cursor positioning works
|
||||
inputEl.value = next;
|
||||
const pos = atIndex + username.length + 2;
|
||||
@@ -739,18 +744,69 @@ export function ChatArea() {
|
||||
if (e.ctrlKey && e.key === 'e') {
|
||||
e.preventDefault();
|
||||
setShowKaomoji((prev) => !prev);
|
||||
return;
|
||||
}
|
||||
|
||||
const isDropdownOpen = mentionQuery !== null || commandQuery !== null;
|
||||
if (!isDropdownOpen) return;
|
||||
|
||||
// Compute item count for the active dropdown
|
||||
const itemCount = mentionQuery !== null
|
||||
? members.filter((m) =>
|
||||
m.username.toLowerCase().includes(mentionQuery.toLowerCase()) ||
|
||||
m.display_name?.toLowerCase().includes(mentionQuery.toLowerCase())
|
||||
).slice(0, 6).length
|
||||
: commandQuery !== null
|
||||
? SLASH_COMMANDS.filter((c) => c.name.startsWith(commandQuery.toLowerCase())).slice(0, 8).length
|
||||
: 0;
|
||||
|
||||
if (itemCount === 0) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setDropdownIndex((prev) => (prev + 1) % itemCount);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setDropdownIndex((prev) => (prev - 1 + itemCount) % itemCount);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (mentionQuery !== null) {
|
||||
const q = mentionQuery.toLowerCase();
|
||||
const filtered = members.filter((m) =>
|
||||
m.username.toLowerCase().includes(q) ||
|
||||
m.display_name?.toLowerCase().includes(q)
|
||||
).slice(0, 6);
|
||||
if (filtered[dropdownIndex]) {
|
||||
handleMentionSelect(filtered[dropdownIndex].username);
|
||||
}
|
||||
} else if (commandQuery !== null) {
|
||||
const q = commandQuery.toLowerCase();
|
||||
const filtered = SLASH_COMMANDS.filter((c) => c.name.startsWith(q)).slice(0, 8);
|
||||
if (filtered[dropdownIndex]) {
|
||||
setInput('/' + filtered[dropdownIndex].name + ' ');
|
||||
setCommandQuery(null);
|
||||
setDropdownIndex(0);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
setMentionQuery(null);
|
||||
setCommandQuery(null);
|
||||
setDropdownIndex(0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{mentionQuery !== null && (
|
||||
<MentionDropdown query={mentionQuery} members={members} onSelect={handleMentionSelect} />
|
||||
<MentionDropdown query={mentionQuery} members={members} selectedIndex={dropdownIndex} onSelect={handleMentionSelect} />
|
||||
)}
|
||||
{commandQuery !== null && (
|
||||
<CommandDropdown
|
||||
query={commandQuery}
|
||||
selectedIndex={dropdownIndex}
|
||||
onSelect={(name) => {
|
||||
setInput('/' + name + ' ');
|
||||
setCommandQuery(null);
|
||||
setDropdownIndex(0);
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user