From 190f18ecae2aa96254c1415cf8ed1b029ade3279 Mon Sep 17 00:00:00 2001 From: Illia Filippov Date: Tue, 23 Dec 2025 17:45:04 +0100 Subject: [PATCH] feat(ProjectSelector): add auto-scroll and improved UX for project picker --- .../project-selector-with-options.tsx | 6 +- .../components/sortable-project-item.tsx | 5 +- .../sidebar/hooks/use-project-picker.ts | 81 +++++++++++++------ apps/ui/src/styles/global.css | 26 ++++++ 4 files changed, 90 insertions(+), 28 deletions(-) diff --git a/apps/ui/src/components/layout/sidebar/components/project-selector-with-options.tsx b/apps/ui/src/components/layout/sidebar/components/project-selector-with-options.tsx index db0dee52..7235b8d7 100644 --- a/apps/ui/src/components/layout/sidebar/components/project-selector-with-options.tsx +++ b/apps/ui/src/components/layout/sidebar/components/project-selector-with-options.tsx @@ -64,6 +64,7 @@ export function ProjectSelectorWithOptions({ setProjectSearchQuery, selectedProjectIndex, projectSearchInputRef, + scrollContainerRef, filteredProjects, } = useProjectPicker({ projects, @@ -171,7 +172,10 @@ export function ProjectSelectorWithOptions({ items={filteredProjects.map((p) => p.id)} strategy={verticalListSortingStrategy} > -
+
{filteredProjects.map((project, index) => ( onSelect(project)} > {/* Drag Handle */} - {/* Project content - clickable area */} -
onSelect(project)}> + {/* Project content */} +
(null); + const scrollContainerRef = useRef(null); // Filtered projects based on search query const filteredProjects = useMemo(() => { @@ -29,38 +30,67 @@ export function useProjectPicker({ return projects.filter((project) => project.name.toLowerCase().includes(query)); }, [projects, projectSearchQuery]); - // Reset selection when filtered results change and project picker is open - useEffect(() => { - if (!isProjectPickerOpen) { - return; + // Helper function to scroll to a specific project + const scrollToProject = useCallback((projectId: string) => { + if (!scrollContainerRef.current) return; + + const element = scrollContainerRef.current.querySelector( + `[data-testid="project-option-${projectId}"]` + ) as HTMLElement; + + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); } + }, []); - if (!projectSearchQuery.trim()) { - const currentIndex = currentProject - ? filteredProjects.findIndex((p) => p.id === currentProject.id) - : -1; - if (currentIndex !== -1) { - setSelectedProjectIndex(currentIndex); - return; - } - } - - setSelectedProjectIndex(0); - }, [isProjectPickerOpen, filteredProjects.length, projectSearchQuery, currentProject]); - - // Reset search query when dropdown closes, set to current project index when it opens + // Initialize state when dropdown opens, reset when it closes useEffect(() => { if (!isProjectPickerOpen) { setProjectSearchQuery(''); setSelectedProjectIndex(0); - } else { - // Focus the search input when dropdown opens - // Small delay to ensure the dropdown is rendered - setTimeout(() => { - projectSearchInputRef.current?.focus(); - }, 0); + return; } - }, [isProjectPickerOpen]); + + // When opening, find and select the current project + const currentIndex = currentProject + ? filteredProjects.findIndex((p) => p.id === currentProject.id) + : -1; + + const initialIndex = currentIndex !== -1 ? currentIndex : 0; + setSelectedProjectIndex(initialIndex); + + // Focus search input and scroll to current project after DOM renders + requestAnimationFrame(() => { + projectSearchInputRef.current?.focus(); + + // Scroll to the current project + const targetProject = filteredProjects[initialIndex]; + if (targetProject) { + scrollToProject(targetProject.id); + } + }); + }, [isProjectPickerOpen, currentProject?.id]); + + // Update selection when search query changes (while picker is open) + useEffect(() => { + if (!isProjectPickerOpen || !projectSearchQuery.trim()) return; + + // When searching, reset to first result + setSelectedProjectIndex(0); + }, [isProjectPickerOpen, projectSearchQuery]); + + // Scroll to highlighted item when selection changes via keyboard + useEffect(() => { + if (!isProjectPickerOpen) return; + + const targetProject = filteredProjects[selectedProjectIndex]; + if (targetProject) { + scrollToProject(targetProject.id); + } + }, [selectedProjectIndex, isProjectPickerOpen, filteredProjects, scrollToProject]); // Handle selecting the currently highlighted project const selectHighlightedProject = useCallback(() => { @@ -111,6 +141,7 @@ export function useProjectPicker({ selectedProjectIndex, setSelectedProjectIndex, projectSearchInputRef, + scrollContainerRef, filteredProjects, selectHighlightedProject, }; diff --git a/apps/ui/src/styles/global.css b/apps/ui/src/styles/global.css index cd7f8145..802b1da0 100644 --- a/apps/ui/src/styles/global.css +++ b/apps/ui/src/styles/global.css @@ -870,6 +870,32 @@ animation: accordion-up 0.2s ease-out forwards; } +/* Project picker scrollbar styling */ +.project-picker-scroll { + scrollbar-width: thin; + scrollbar-color: var(--muted-foreground) transparent; +} + +.project-picker-scroll::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.project-picker-scroll::-webkit-scrollbar-track { + background: transparent; + border-radius: 3px; +} + +.project-picker-scroll::-webkit-scrollbar-thumb { + background: var(--muted-foreground); + border-radius: 3px; + min-height: 30px; +} + +.project-picker-scroll::-webkit-scrollbar-thumb:hover { + background: var(--foreground-secondary); +} + /* Terminal scrollbar theming */ .xterm-viewport::-webkit-scrollbar { width: 8px;