mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge pull request #245 from illia1f/feature/project-picker-scroll
feat(ProjectSelector): add auto-scroll and improved UX for project picker
This commit is contained in:
@@ -64,6 +64,7 @@ export function ProjectSelectorWithOptions({
|
|||||||
setProjectSearchQuery,
|
setProjectSearchQuery,
|
||||||
selectedProjectIndex,
|
selectedProjectIndex,
|
||||||
projectSearchInputRef,
|
projectSearchInputRef,
|
||||||
|
scrollContainerRef,
|
||||||
filteredProjects,
|
filteredProjects,
|
||||||
} = useProjectPicker({
|
} = useProjectPicker({
|
||||||
projects,
|
projects,
|
||||||
@@ -171,7 +172,10 @@ export function ProjectSelectorWithOptions({
|
|||||||
items={filteredProjects.map((p) => p.id)}
|
items={filteredProjects.map((p) => p.id)}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
<div className="space-y-0.5 max-h-64 overflow-y-auto overflow-x-hidden">
|
<div
|
||||||
|
ref={scrollContainerRef}
|
||||||
|
className="space-y-0.5 max-h-64 overflow-y-auto overflow-x-hidden scroll-smooth scrollbar-styled"
|
||||||
|
>
|
||||||
{filteredProjects.map((project, index) => (
|
{filteredProjects.map((project, index) => (
|
||||||
<SortableProjectItem
|
<SortableProjectItem
|
||||||
key={project.id}
|
key={project.id}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function SortableProjectItem({
|
|||||||
isHighlighted && 'bg-brand-500/10 text-foreground ring-1 ring-brand-500/20'
|
isHighlighted && 'bg-brand-500/10 text-foreground ring-1 ring-brand-500/20'
|
||||||
)}
|
)}
|
||||||
data-testid={`project-option-${project.id}`}
|
data-testid={`project-option-${project.id}`}
|
||||||
|
onClick={() => onSelect(project)}
|
||||||
>
|
>
|
||||||
{/* Drag Handle */}
|
{/* Drag Handle */}
|
||||||
<button
|
<button
|
||||||
@@ -43,8 +44,8 @@ export function SortableProjectItem({
|
|||||||
<GripVertical className="h-3.5 w-3.5 text-muted-foreground/60" />
|
<GripVertical className="h-3.5 w-3.5 text-muted-foreground/60" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Project content - clickable area */}
|
{/* Project content */}
|
||||||
<div className="flex items-center gap-2.5 flex-1 min-w-0" onClick={() => onSelect(project)}>
|
<div className="flex items-center gap-2.5 flex-1 min-w-0">
|
||||||
<Folder
|
<Folder
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-4 w-4 shrink-0',
|
'h-4 w-4 shrink-0',
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function useProjectPicker({
|
|||||||
const [projectSearchQuery, setProjectSearchQuery] = useState('');
|
const [projectSearchQuery, setProjectSearchQuery] = useState('');
|
||||||
const [selectedProjectIndex, setSelectedProjectIndex] = useState(0);
|
const [selectedProjectIndex, setSelectedProjectIndex] = useState(0);
|
||||||
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Filtered projects based on search query
|
// Filtered projects based on search query
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
@@ -29,38 +30,66 @@ export function useProjectPicker({
|
|||||||
return projects.filter((project) => project.name.toLowerCase().includes(query));
|
return projects.filter((project) => project.name.toLowerCase().includes(query));
|
||||||
}, [projects, projectSearchQuery]);
|
}, [projects, projectSearchQuery]);
|
||||||
|
|
||||||
// Reset selection when filtered results change and project picker is open
|
// 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}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'nearest',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// On open/close, handle search query reset and focus
|
||||||
|
useEffect(() => {
|
||||||
|
if (isProjectPickerOpen) {
|
||||||
|
// Focus search input after DOM renders
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
projectSearchInputRef.current?.focus();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Reset search when closing
|
||||||
|
setProjectSearchQuery('');
|
||||||
|
}
|
||||||
|
}, [isProjectPickerOpen]);
|
||||||
|
|
||||||
|
// Update selection when search query changes (while picker is open)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isProjectPickerOpen) {
|
if (!isProjectPickerOpen) {
|
||||||
|
setSelectedProjectIndex(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!projectSearchQuery.trim()) {
|
if (projectSearchQuery.trim()) {
|
||||||
|
// When searching, reset to first result
|
||||||
|
setSelectedProjectIndex(0);
|
||||||
|
} else {
|
||||||
|
// When not searching (e.g., on open or search cleared), find and select the current project
|
||||||
const currentIndex = currentProject
|
const currentIndex = currentProject
|
||||||
? filteredProjects.findIndex((p) => p.id === currentProject.id)
|
? filteredProjects.findIndex((p) => p.id === currentProject.id)
|
||||||
: -1;
|
: -1;
|
||||||
if (currentIndex !== -1) {
|
setSelectedProjectIndex(currentIndex !== -1 ? currentIndex : 0);
|
||||||
setSelectedProjectIndex(currentIndex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [isProjectPickerOpen, projectSearchQuery, filteredProjects, currentProject]);
|
||||||
|
|
||||||
setSelectedProjectIndex(0);
|
// Scroll to highlighted item when selection changes
|
||||||
}, [isProjectPickerOpen, filteredProjects.length, projectSearchQuery, currentProject]);
|
|
||||||
|
|
||||||
// Reset search query when dropdown closes, set to current project index when it opens
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isProjectPickerOpen) {
|
if (!isProjectPickerOpen) return;
|
||||||
setProjectSearchQuery('');
|
|
||||||
setSelectedProjectIndex(0);
|
const targetProject = filteredProjects[selectedProjectIndex];
|
||||||
} else {
|
if (targetProject) {
|
||||||
// Focus the search input when dropdown opens
|
// Use requestAnimationFrame to ensure DOM is rendered before scrolling
|
||||||
// Small delay to ensure the dropdown is rendered
|
requestAnimationFrame(() => {
|
||||||
setTimeout(() => {
|
scrollToProject(targetProject.id);
|
||||||
projectSearchInputRef.current?.focus();
|
});
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
}, [isProjectPickerOpen]);
|
}, [selectedProjectIndex, isProjectPickerOpen, filteredProjects, scrollToProject]);
|
||||||
|
|
||||||
// Handle selecting the currently highlighted project
|
// Handle selecting the currently highlighted project
|
||||||
const selectHighlightedProject = useCallback(() => {
|
const selectHighlightedProject = useCallback(() => {
|
||||||
@@ -111,6 +140,7 @@ export function useProjectPicker({
|
|||||||
selectedProjectIndex,
|
selectedProjectIndex,
|
||||||
setSelectedProjectIndex,
|
setSelectedProjectIndex,
|
||||||
projectSearchInputRef,
|
projectSearchInputRef,
|
||||||
|
scrollContainerRef,
|
||||||
filteredProjects,
|
filteredProjects,
|
||||||
selectHighlightedProject,
|
selectHighlightedProject,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user