fix(sidebar): make file tree rows reliably clickable (#2368)

Native HTML5 drag starts after ~4px of pointer travel and suppresses the
click event for the gesture, so the draggable sidebar file tree rows
randomly ignored clicks on macOS trackpads/Magic Mouse: folders neither
expanded nor collapsed and files did not open.

Keep the rows draggable (dragging a file into the chat input inserts an
@mention) and recover the suppressed click on dragend: a drag that ended
within a small slop radius of its origin without dropping anywhere runs
the row's primary action. Also drop the misleading grab cursor so rows
read as clickable, matching FilesView.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Serhii Dziupin
2026-08-06 19:55:42 +00:00
co-authored by Serhii Dziupin
parent fe331c093b
commit 652e9c0124
3 changed files with 165 additions and 3 deletions
@@ -45,6 +45,7 @@ import { Icon } from "@/components/icon/Icon";
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
import { isBrowserClientRuntime } from '@/lib/desktop';
import { useI18n } from '@/lib/i18n';
import { recordFileTreeDragStart, shouldTreatFileTreeDragEndAsClick } from './fileTreeDragClick';
type FileNode = {
name: string;
@@ -327,12 +328,20 @@ const FileRow: React.FC<FileRowProps> = ({
);
const handleDragStart = React.useCallback((e: React.DragEvent) => {
recordFileTreeDragStart(e);
const path = getRelativePath(root, node.path);
if (!path || path === '.') return;
e.dataTransfer.setData('application/x-openchamber-file-path', path);
e.dataTransfer.effectAllowed = 'copy';
}, [node.path, root]);
const handleDragEnd = React.useCallback((e: React.DragEvent) => {
// A micro-drag suppressed the click this gesture was meant to be (#2368).
if (shouldTreatFileTreeDragEndAsClick(e)) {
handleInteraction();
}
}, [handleInteraction]);
return (
<ContextMenu open={rightClickOpen} onOpenChange={setRightClickOpen}>
<ContextMenuTrigger render={<div className="group relative flex items-center" onContextMenu={handleContextMenu} />}>
@@ -342,10 +351,10 @@ const FileRow: React.FC<FileRowProps> = ({
onContextMenu={handleContextMenu}
draggable
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
className={cn(
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors pr-8 select-none',
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40',
'cursor-grab active:cursor-grabbing'
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40'
)}
>
{isDir ? (
@@ -1199,13 +1208,20 @@ export const SidebarFilesTree: React.FC = () => {
onClick={() => handleOpenFile(node)}
draggable
onDragStart={(e) => {
recordFileTreeDragStart(e);
const path = node.relativePath || getRelativePath(root ?? '', node.path);
if (!path || path === '.') return;
e.dataTransfer.setData('application/x-openchamber-file-path', path);
e.dataTransfer.effectAllowed = 'copy';
}}
onDragEnd={(e) => {
// A micro-drag suppressed the click this gesture was meant to be (#2368).
if (shouldTreatFileTreeDragEndAsClick(e)) {
void handleOpenFile(node);
}
}}
className={cn(
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors cursor-grab active:cursor-grabbing',
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors',
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40'
)}
title={node.path}