From e88d8fbea8193800ebdf0546c1b8a50ed9fb507e Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Fri, 15 May 2026 20:34:56 -0700 Subject: [PATCH] fix: require click for workspace board (#2038) --- .../src/components/sidebar/SidebarHeader.tsx | 188 +----------------- .../sidebar/WorkspaceKanbanDrawer.tsx | 5 +- 2 files changed, 12 insertions(+), 181 deletions(-) diff --git a/src/renderer/src/components/sidebar/SidebarHeader.tsx b/src/renderer/src/components/sidebar/SidebarHeader.tsx index 0533a62a3..d875f82b9 100644 --- a/src/renderer/src/components/sidebar/SidebarHeader.tsx +++ b/src/renderer/src/components/sidebar/SidebarHeader.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { Kanban, Plus, SlidersHorizontal } from 'lucide-react' import { useAppStore } from '@/store' import { Button } from '@/components/ui/button' @@ -52,24 +52,9 @@ const SORT_OPTIONS = [ const isMac = navigator.userAgent.includes('Mac') const newWorktreeShortcutLabel = isMac ? '⌘N' : 'Ctrl+N' -const WORKSPACE_BOARD_HOVER_OPEN_DELAY_MS = 50 -// Why: gives the pointer room to travel from the header into the board before -// the temporary hover preview collapses. -const WORKSPACE_BOARD_HOVER_CLOSE_DELAY_MS = 220 -type WorkspaceBoardOpenMode = 'closed' | 'hover' | 'persistent' const SidebarHeader = React.memo(function SidebarHeader() { - const [workspaceBoardOpenMode, setWorkspaceBoardOpenMode] = - useState('closed') - const workspaceBoardOpen = workspaceBoardOpenMode !== 'closed' - const workspaceBoardPersistentOpen = workspaceBoardOpenMode === 'persistent' - // Why: hover-open and manual-open have different close semantics; keeping - // the mode explicit prevents a button click from closing a hover preview. - const workspaceBoardOpenModeRef = useRef('closed') - const workspaceBoardHoverSuppressedRef = useRef(false) - const workspaceHeaderHoveredRef = useRef(false) - const workspaceBoardHoverOpenTimerRef = useRef(null) - const workspaceBoardHoverCloseTimerRef = useRef(null) + const [workspaceBoardOpen, setWorkspaceBoardOpen] = useState(false) const openModal = useAppStore((s) => s.openModal) const repos = useAppStore((s) => s.repos) const canCreateWorktree = repos.some((repo) => isGitRepoKind(repo)) @@ -83,146 +68,13 @@ const SidebarHeader = React.memo(function SidebarHeader() { const showWorkspaceLineage = useAppStore((s) => s.showWorkspaceLineage) const setShowWorkspaceLineage = useAppStore((s) => s.setShowWorkspaceLineage) - const clearWorkspaceBoardHoverClose = useCallback(() => { - if (workspaceBoardHoverCloseTimerRef.current === null) { - return - } - window.clearTimeout(workspaceBoardHoverCloseTimerRef.current) - workspaceBoardHoverCloseTimerRef.current = null + const handleWorkspaceBoardOpenChange = useCallback((open: boolean) => { + setWorkspaceBoardOpen(open) }, []) - const clearWorkspaceBoardHoverOpen = useCallback(() => { - if (workspaceBoardHoverOpenTimerRef.current === null) { - return - } - window.clearTimeout(workspaceBoardHoverOpenTimerRef.current) - workspaceBoardHoverOpenTimerRef.current = null - }, []) - - useEffect( - () => () => { - clearWorkspaceBoardHoverOpen() - clearWorkspaceBoardHoverClose() - }, - [clearWorkspaceBoardHoverClose, clearWorkspaceBoardHoverOpen] - ) - - const setWorkspaceBoardMode = useCallback((mode: WorkspaceBoardOpenMode) => { - workspaceBoardOpenModeRef.current = mode - setWorkspaceBoardOpenMode(mode) - }, []) - - const handleWorkspaceBoardOpenChange = useCallback( - (open: boolean) => { - clearWorkspaceBoardHoverOpen() - clearWorkspaceBoardHoverClose() - if (open) { - workspaceBoardHoverSuppressedRef.current = false - setWorkspaceBoardMode('persistent') - return - } - setWorkspaceBoardMode('closed') - workspaceBoardHoverSuppressedRef.current = workspaceHeaderHoveredRef.current - }, - [clearWorkspaceBoardHoverClose, clearWorkspaceBoardHoverOpen, setWorkspaceBoardMode] - ) - - const handleWorkspaceHeaderPointerEnter = useCallback( - (event: React.PointerEvent) => { - if (event.pointerType !== 'mouse') { - return - } - workspaceHeaderHoveredRef.current = true - clearWorkspaceBoardHoverClose() - if ( - workspaceBoardOpenModeRef.current !== 'closed' || - workspaceBoardHoverSuppressedRef.current - ) { - return - } - clearWorkspaceBoardHoverOpen() - workspaceBoardHoverOpenTimerRef.current = window.setTimeout(() => { - workspaceBoardHoverOpenTimerRef.current = null - if ( - workspaceBoardHoverSuppressedRef.current || - workspaceBoardOpenModeRef.current !== 'closed' - ) { - return - } - setWorkspaceBoardMode('hover') - }, WORKSPACE_BOARD_HOVER_OPEN_DELAY_MS) - }, - [clearWorkspaceBoardHoverClose, clearWorkspaceBoardHoverOpen, setWorkspaceBoardMode] - ) - - const handleWorkspaceHeaderPointerLeave = useCallback( - (event: React.PointerEvent) => { - if (event.pointerType === 'mouse') { - const rect = event.currentTarget.getBoundingClientRect() - if ( - event.clientX >= rect.left && - event.clientX <= rect.right && - event.clientY >= rect.top && - event.clientY <= rect.bottom - ) { - return - } - } - workspaceHeaderHoveredRef.current = false - workspaceBoardHoverSuppressedRef.current = false - clearWorkspaceBoardHoverOpen() - if (event.pointerType !== 'mouse' || workspaceBoardOpenModeRef.current === 'persistent') { - return - } - clearWorkspaceBoardHoverClose() - workspaceBoardHoverCloseTimerRef.current = window.setTimeout(() => { - workspaceBoardHoverCloseTimerRef.current = null - if (workspaceBoardOpenModeRef.current === 'persistent') { - return - } - setWorkspaceBoardMode('closed') - }, WORKSPACE_BOARD_HOVER_CLOSE_DELAY_MS) - }, - [clearWorkspaceBoardHoverClose, clearWorkspaceBoardHoverOpen, setWorkspaceBoardMode] - ) - - const handleWorkspaceBoardPointerEnter = useCallback( - (event: React.PointerEvent) => { - if (event.pointerType !== 'mouse') { - return - } - clearWorkspaceBoardHoverOpen() - clearWorkspaceBoardHoverClose() - workspaceBoardHoverSuppressedRef.current = false - setWorkspaceBoardMode('persistent') - }, - [clearWorkspaceBoardHoverClose, clearWorkspaceBoardHoverOpen, setWorkspaceBoardMode] - ) - - const handleWorkspaceBoardButtonPointerDown = useCallback( - (event: React.PointerEvent) => { - if (event.button !== 0) { - return - } - clearWorkspaceBoardHoverOpen() - clearWorkspaceBoardHoverClose() - }, - [clearWorkspaceBoardHoverClose, clearWorkspaceBoardHoverOpen] - ) - const handleWorkspaceBoardToggle = useCallback(() => { - clearWorkspaceBoardHoverOpen() - clearWorkspaceBoardHoverClose() - - if (workspaceBoardOpenModeRef.current === 'persistent') { - workspaceBoardHoverSuppressedRef.current = workspaceHeaderHoveredRef.current - setWorkspaceBoardMode('closed') - return - } - - workspaceBoardHoverSuppressedRef.current = false - setWorkspaceBoardMode('persistent') - }, [clearWorkspaceBoardHoverClose, clearWorkspaceBoardHoverOpen, setWorkspaceBoardMode]) + setWorkspaceBoardOpen((open) => !open) + }, []) useEffect(() => { if (!workspaceBoardOpen) { @@ -233,31 +85,19 @@ const SidebarHeader = React.memo(function SidebarHeader() { if (event.key !== 'Escape') { return } - clearWorkspaceBoardHoverOpen() - clearWorkspaceBoardHoverClose() - workspaceBoardHoverSuppressedRef.current = workspaceHeaderHoveredRef.current event.preventDefault() - setWorkspaceBoardMode('closed') + setWorkspaceBoardOpen(false) } // Why: the workspace board is a non-modal companion panel, so focus may // be outside the sheet when Escape should still dismiss it. document.addEventListener('keydown', handleKeyDown, true) return () => document.removeEventListener('keydown', handleKeyDown, true) - }, [ - clearWorkspaceBoardHoverClose, - clearWorkspaceBoardHoverOpen, - setWorkspaceBoardMode, - workspaceBoardOpen - ]) + }, [workspaceBoardOpen]) return ( <> -
+
Workspaces @@ -269,20 +109,15 @@ const SidebarHeader = React.memo(function SidebarHeader() { size="icon-xs" className="text-muted-foreground" aria-label="Workspace board" - aria-pressed={workspaceBoardPersistentOpen} + aria-pressed={workspaceBoardOpen} data-workspace-board-trigger="" - onPointerDown={handleWorkspaceBoardButtonPointerDown} onClick={handleWorkspaceBoardToggle} > - {workspaceBoardPersistentOpen - ? 'Close workspace board' - : workspaceBoardOpen - ? 'Keep workspace board open' - : 'Workspace board'} + {workspaceBoardOpen ? 'Close workspace board' : 'Workspace board'}
@@ -417,7 +252,6 @@ const SidebarHeader = React.memo(function SidebarHeader() { ) diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx index fea4a65e5..acb46cf0f 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx @@ -19,7 +19,6 @@ import { makeWorkspaceStatusId } from '../../../../shared/workspace-statuses' type WorkspaceKanbanDrawerProps = { open: boolean onOpenChange: (open: boolean) => void - onPointerEnter?: React.PointerEventHandler } function sortBoardWorktrees(a: Worktree, b: Worktree): number { @@ -28,8 +27,7 @@ function sortBoardWorktrees(a: Worktree, b: Worktree): number { export default function WorkspaceKanbanDrawer({ open, - onOpenChange, - onPointerEnter + onOpenChange }: WorkspaceKanbanDrawerProps): React.JSX.Element { const allWorktrees = useAllWorktrees() const repoMap = useRepoMap() @@ -307,7 +305,6 @@ export default function WorkspaceKanbanDrawer({ } as React.CSSProperties } data-workspace-board-compact={workspaceBoardCompact ? 'true' : 'false'} - onPointerEnter={onPointerEnter} onOpenAutoFocus={(event) => { // Why: Radix focuses the first toolbar button on open, which opens // its tooltip without hover and makes the drawer feel noisy.