move viewport with arrow key navigation in file-tag-typeahead-plugin and multi-file-searcharea (#1406)

This commit is contained in:
Gabriel Gordon-Hall
2025-12-02 16:03:47 +00:00
committed by GitHub
parent 72f2ab1320
commit 18637ab3e1
2 changed files with 40 additions and 1 deletions

View File

@@ -44,6 +44,7 @@ export function MultiFileSearchTextarea({
const dropdownRef = useRef<HTMLDivElement>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const searchCacheRef = useRef<Map<string, FileSearchResult[]>>(new Map());
const itemRefs = useRef<Map<number, HTMLDivElement>>(new Map());
// Search for files when query changes
useEffect(() => {
@@ -309,6 +310,16 @@ export function MultiFileSearchTextarea({
}
}, [searchResults.length, showDropdown]);
// Scroll selected item into view when navigating with arrow keys
useEffect(() => {
if (selectedIndex >= 0) {
const itemEl = itemRefs.current.get(selectedIndex);
if (itemEl) {
itemEl.scrollIntoView({ block: 'nearest' });
}
}
}, [selectedIndex]);
const dropdownPosition = getDropdownPosition();
return (
@@ -352,6 +363,10 @@ export function MultiFileSearchTextarea({
{searchResults.map((file, index) => (
<div
key={file.path}
ref={(el) => {
if (el) itemRefs.current.set(index, el);
else itemRefs.current.delete(index);
}}
className={`px-3 py-2 cursor-pointer text-sm ${
index === selectedIndex
? 'bg-blue-50 text-blue-900'

View File

@@ -1,4 +1,4 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
@@ -67,6 +67,8 @@ function getMenuPosition(anchorEl: HTMLElement) {
export function FileTagTypeaheadPlugin({ projectId }: { projectId?: string }) {
const [editor] = useLexicalComposerContext();
const [options, setOptions] = useState<FileTagOption[]>([]);
const itemRefs = useRef<Map<number, HTMLDivElement>>(new Map());
const lastSelectedIndexRef = useRef<number>(-1);
const onQueryChange = useCallback(
(query: string | null) => {
@@ -134,6 +136,20 @@ export function FileTagTypeaheadPlugin({ projectId }: { projectId?: string }) {
anchorRef.current
);
// Scroll selected item into view when navigating with arrow keys
if (
selectedIndex !== null &&
selectedIndex !== lastSelectedIndexRef.current
) {
lastSelectedIndexRef.current = selectedIndex;
setTimeout(() => {
const itemEl = itemRefs.current.get(selectedIndex);
if (itemEl) {
itemEl.scrollIntoView({ block: 'nearest' });
}
}, 0);
}
const tagResults = options.filter((r) => r.item.type === 'tag');
const fileResults = options.filter((r) => r.item.type === 'file');
@@ -167,6 +183,10 @@ export function FileTagTypeaheadPlugin({ projectId }: { projectId?: string }) {
return (
<div
key={option.key}
ref={(el) => {
if (el) itemRefs.current.set(index, el);
else itemRefs.current.delete(index);
}}
className={`px-3 py-2 cursor-pointer text-sm ${
index === selectedIndex
? 'bg-muted text-foreground'
@@ -204,6 +224,10 @@ export function FileTagTypeaheadPlugin({ projectId }: { projectId?: string }) {
return (
<div
key={option.key}
ref={(el) => {
if (el) itemRefs.current.set(index, el);
else itemRefs.current.delete(index);
}}
className={`px-3 py-2 cursor-pointer text-sm ${
index === selectedIndex
? 'bg-muted text-foreground'