From 2da23823feaf741a258117a691629e8e3454a4fe Mon Sep 17 00:00:00 2001 From: Cyril Date: Mon, 13 Oct 2025 10:20:02 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8(frontend)=20fix=20phantom=20selec?= =?UTF-8?q?tion=20issue=20between=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prevents invisible selections when switching between node elements Signed-off-by: Cyril --- CHANGELOG.md | 4 +++ src/components/tree-view/TreeView.tsx | 39 ++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb9584d..81f52fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Patch Changes + +- Clicking between nodes no longer triggers phantom selection. + ## 0.17.1 ### Patch Changes diff --git a/src/components/tree-view/TreeView.tsx b/src/components/tree-view/TreeView.tsx index b55afeb..c9a9be5 100644 --- a/src/components/tree-view/TreeView.tsx +++ b/src/components/tree-view/TreeView.tsx @@ -218,7 +218,7 @@ export const TreeView = ({ width={width} paddingTop={10} paddingBottom={10} - rowHeight={35} + rowHeight={50} disableDrag={disableDrag} disableDrop={({ parentNode, dragNodes, index }) => { if (canDrop) { @@ -327,7 +327,8 @@ const Row = ({ children, ...props }: RowProps) => { const target = e.target as HTMLElement | null; if (target) { const isInActionsToolbar = target.closest(".actions"); - const interactiveSelector = 'button, a[href], input, textarea, select, [role="menuitem"], [role="button"]'; + const interactiveSelector = + 'button, a[href], input, textarea, select, [role="menuitem"], [role="button"]'; const isInteractive = target.closest(interactiveSelector); if (isInActionsToolbar || isInteractive) { return; @@ -346,6 +347,37 @@ const Row = ({ children, ...props }: RowProps) => { } }; + // Block everything if clicking outside the actual row content (between items). + const handleRowMouseEvent = (e: React.MouseEvent) => { + const target = e.target as HTMLElement; + const rowContent = e.currentTarget.querySelector( + ".c__tree-view--row-content" + ); + + // Allow toolbar/interactives (menus, links, inputs…) + const interactiveSelector = + 'button, a[href], input, textarea, select, [role="menuitem"], [role="button"]'; + const inToolbar = target.closest(".actions"); + const isInteractive = target.closest(interactiveSelector); + + // Only allow primary click (leave context-menu/drag) + const isPrimaryClick = "button" in e && e.button === 0; + if (!isPrimaryClick) return; + + const inside = !!rowContent && rowContent.contains(target); + const outside = !inside && !inToolbar && !isInteractive; + + if (outside) { + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.type === "click") { + props.node.handleClick(e); + } + }; + if (isTitle || isSeparator) { return (
({ children, ...props }: RowProps) => { key={props.node.id} ref={props.innerRef} onFocus={(e) => e.stopPropagation()} - onClick={props.node.handleClick} + onMouseDown={handleRowMouseEvent} + onClick={handleRowMouseEvent} onKeyDown={handleKeyDown} >
{children}
From 7f76e5961faa828913b2ebff89f3006a977da3de Mon Sep 17 00:00:00 2001 From: Cyril Date: Thu, 16 Oct 2025 11:51:36 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fixup!=20=E2=9C=A8(frontend)=20fix=20phanto?= =?UTF-8?q?m=20selection=20issue=20between=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/tree-view/TreeView.tsx | 38 +++------------------------ src/components/tree-view/index.scss | 3 +++ 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/src/components/tree-view/TreeView.tsx b/src/components/tree-view/TreeView.tsx index c9a9be5..d2328b3 100644 --- a/src/components/tree-view/TreeView.tsx +++ b/src/components/tree-view/TreeView.tsx @@ -26,6 +26,7 @@ export type TreeViewProps = { dndRootElement?: HTMLElement | null; selectedNodeId?: string; rootNodeId: string; + rowHeight?: number; canDrop?: (args: { parentNode: NodeApi> | null; dragNodes: NodeApi>[]; @@ -43,6 +44,7 @@ export const TreeView = ({ selectedNodeId, rootNodeId, renderNode, + rowHeight = 35, canDrop, canDrag, afterMove, @@ -218,7 +220,7 @@ export const TreeView = ({ width={width} paddingTop={10} paddingBottom={10} - rowHeight={50} + rowHeight={rowHeight} disableDrag={disableDrag} disableDrop={({ parentNode, dragNodes, index }) => { if (canDrop) { @@ -347,37 +349,6 @@ const Row = ({ children, ...props }: RowProps) => { } }; - // Block everything if clicking outside the actual row content (between items). - const handleRowMouseEvent = (e: React.MouseEvent) => { - const target = e.target as HTMLElement; - const rowContent = e.currentTarget.querySelector( - ".c__tree-view--row-content" - ); - - // Allow toolbar/interactives (menus, links, inputs…) - const interactiveSelector = - 'button, a[href], input, textarea, select, [role="menuitem"], [role="button"]'; - const inToolbar = target.closest(".actions"); - const isInteractive = target.closest(interactiveSelector); - - // Only allow primary click (leave context-menu/drag) - const isPrimaryClick = "button" in e && e.button === 0; - if (!isPrimaryClick) return; - - const inside = !!rowContent && rowContent.contains(target); - const outside = !inside && !inToolbar && !isInteractive; - - if (outside) { - e.preventDefault(); - e.stopPropagation(); - return; - } - - if (e.type === "click") { - props.node.handleClick(e); - } - }; - if (isTitle || isSeparator) { return (
({ children, ...props }: RowProps) => { key={props.node.id} ref={props.innerRef} onFocus={(e) => e.stopPropagation()} - onMouseDown={handleRowMouseEvent} - onClick={handleRowMouseEvent} + onClick={props.node.handleClick} onKeyDown={handleKeyDown} >
{children}
diff --git a/src/components/tree-view/index.scss b/src/components/tree-view/index.scss index 1cfd148..51039c4 100644 --- a/src/components/tree-view/index.scss +++ b/src/components/tree-view/index.scss @@ -29,6 +29,8 @@ .c__tree-view--row { outline: none; + margin: 2px 0; + pointer-events: none; &:focus-visible .c__tree-view--node { box-shadow: 0 0 0 2px @@ -63,6 +65,7 @@ border: 1.5px solid rgba(0, 0, 0, 0); cursor: pointer; padding: var(--c--globals--spacings--4xs) 0; + pointer-events: auto; gap: var(--c--globals--spacings--3xs); &:not(.willReceiveDrop, .isSelected):hover {