From 824497e3a7008f80f208af2f607d3a1c83562ed2 Mon Sep 17 00:00:00 2001 From: moreih29 Date: Thu, 25 Jun 2026 11:04:27 +0900 Subject: [PATCH 1/3] fix(file-tree): allow folders to be multi-selected with shift/cmd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folder rows bound onClick to onToggle (no event), so handleRowClick never saw shift/cmd modifiers and folder range/toggle selection silently no-op'd while files worked. Make the click binding type-agnostic so folders forward the event too; the parent already branches plain-click (folder→expand, file→open) vs modified-click (extend/toggle selection). Matches VSCode's type-agnostic explorer selection model. Removes the now-orphaned onToggle prop and adds a regression guard asserting dir rows forward modifier clicks. Co-Authored-By: Claude Opus 4.8 --- .../components/files/file-tree/row.tsx | 13 +- .../files/file-tree/virtual-body.tsx | 1 - .../files/file-tree-row-4state.test.tsx | 1 - .../files/file-tree-row-dir-click.test.tsx | 166 ++++++++++++++++++ 4 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 tests/unit/renderer/components/files/file-tree-row-dir-click.test.tsx diff --git a/src/renderer/components/files/file-tree/row.tsx b/src/renderer/components/files/file-tree/row.tsx index e00f684f..d3c2ecaf 100644 --- a/src/renderer/components/files/file-tree/row.tsx +++ b/src/renderer/components/files/file-tree/row.tsx @@ -58,8 +58,14 @@ interface FileTreeRowProps { isIgnored?: boolean; /** True when this row is in the cut clipboard (VSCode parity: dimmed). */ isCut?: boolean; - onToggle: () => void; // dir click - onClick: (e: React.MouseEvent) => void; // file click + /** + * Row click. Always receives the event so the parent's handler can read + * shift/cmd modifiers for range/toggle multi-selection. Applies uniformly + * to files AND folders — the parent (handleRowClick) decides the primary + * action: a plain (unmodified) folder click toggles expand, a plain file + * click opens it, while modified clicks extend/toggle the selection set. + */ + onClick: (e: React.MouseEvent) => void; /** * File-only double-click. Mirrors VSCode explorer's "double-click = * open as a permanent (non-preview) tab" gesture. @@ -91,7 +97,6 @@ export function FileTreeRow({ decoration, isIgnored = false, isCut = false, - onToggle, onClick, onDoubleClick, onContextMenu, @@ -174,7 +179,7 @@ export function FileTreeRow({ aria-level={depth + 1} aria-expanded={isDir ? isExpanded : undefined} aria-selected={isSelected} - onClick={isDir ? onToggle : (e) => onClick(e)} + onClick={(e) => onClick(e)} onDoubleClick={isDir ? undefined : onDoubleClick} onContextMenu={onContextMenu} title={node.name} diff --git a/src/renderer/components/files/file-tree/virtual-body.tsx b/src/renderer/components/files/file-tree/virtual-body.tsx index 5206a020..a470e26c 100644 --- a/src/renderer/components/files/file-tree/virtual-body.tsx +++ b/src/renderer/components/files/file-tree/virtual-body.tsx @@ -158,7 +158,6 @@ export function FileTreeVirtualBody({ isLoading={tree?.loading.has(item.absPath) ?? false} decoration={decoration} isIgnored={isIgnored} - onToggle={() => onRowClick(flatIdx, item)} onClick={(e) => onRowClick(flatIdx, item, e)} onDoubleClick={() => onRowDoubleClick(flatIdx, item)} onContextMenu={() => onRowContextMenu(flatIdx, item)} diff --git a/tests/unit/renderer/components/files/file-tree-row-4state.test.tsx b/tests/unit/renderer/components/files/file-tree-row-4state.test.tsx index b3a5257c..f0e01549 100644 --- a/tests/unit/renderer/components/files/file-tree-row-4state.test.tsx +++ b/tests/unit/renderer/components/files/file-tree-row-4state.test.tsx @@ -36,7 +36,6 @@ function renderRow(props: Partial> = {} isExpanded: false, isSelected: false, isFocused: false, - onToggle: () => {}, onClick: () => {}, ...props, }), diff --git a/tests/unit/renderer/components/files/file-tree-row-dir-click.test.tsx b/tests/unit/renderer/components/files/file-tree-row-dir-click.test.tsx new file mode 100644 index 00000000..01a0b07a --- /dev/null +++ b/tests/unit/renderer/components/files/file-tree-row-dir-click.test.tsx @@ -0,0 +1,166 @@ +/** + * Regression guard — FileTreeRow forwards the click event for FOLDERS too. + * + * The bug: row.tsx bound `onClick={isDir ? onToggle : (e) => onClick(e)}`. + * Folders got `onToggle` (called with NO event), so the parent's + * handleRowClick never saw shift/cmd modifiers and folder multi-selection + * (range / toggle) silently no-op'd. Files worked because they forwarded `e`. + * + * The fix makes the binding type-agnostic: `onClick={(e) => onClick(e)}`. + * Both files AND folders forward the event; the parent decides the primary + * action (plain folder click → expand, modified click → extend/toggle). + * + * HOW THIS TEST RENDERS WITHOUT A DOM + * ----------------------------------- + * DOM mounting is intentionally avoided in this project (happy-dom caused + * hangs — see portal-fiber-identity.test.ts). Mirroring browser-view.test.tsx, + * we mock `react` so its hooks forward to React's live dispatcher slot + * (`__CLIENT_INTERNALS…H`). Outside our render that slot is React's real + * dispatcher, so the mock is transparent and never leaks into other test + * files. During our render we point the slot at a minimal slot-indexed + * dispatcher, invoke the FileTreeRow function directly, and inspect the + * returned