Show workspace board preview while dragging cards

This commit is contained in:
Neil 2026-06-16 18:49:24 -07:00 committed by GitHub
parent 3f7e869f0c
commit 896a2ae4de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 271 additions and 10 deletions

View File

@ -1408,6 +1408,26 @@
animation-timing-function: cubic-bezier(0.7, 0, 0.84, 0);
}
/* Why: this board is attached to the worktree sidebar, not floating above it;
the generic Sheet elevation casts a haze over the sidebar when left open. */
.workspace-kanban-sheet-content {
box-shadow: inset 1px 0 0 color-mix(in srgb, var(--worktree-sidebar-border) 80%, transparent);
backdrop-filter: none;
}
/* Why: a sidebar-card drag should advertise the board as a possible drop
surface without fully committing the companion panel until the pointer enters it. */
.workspace-kanban-sheet-content[data-workspace-board-drag-preview='true'] {
background: color-mix(in srgb, var(--worktree-sidebar) 88%, var(--background));
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--sidebar-ring) 58%, transparent),
0 10px 24px color-mix(in srgb, var(--foreground) 12%, transparent);
}
.workspace-kanban-sheet-content[data-workspace-board-drag-preview='true'] > * {
opacity: 0.42;
}
@keyframes workspace-kanban-sheet-in {
from {
clip-path: inset(0 100% 0 0);

View File

@ -37,6 +37,7 @@ import { useContextualTour } from '@/components/contextual-tours/use-contextual-
type WorkspaceKanbanDrawerProps = {
leftSidebarStyle?: React.CSSProperties
open: boolean
dragPreview: boolean
preserveOpenForMenu: boolean
onOpenChange: (open: boolean) => void
onMenuOpenChange: (open: boolean) => void
@ -45,6 +46,7 @@ type WorkspaceKanbanDrawerProps = {
export default function WorkspaceKanbanDrawer({
leftSidebarStyle,
open,
dragPreview,
preserveOpenForMenu,
onOpenChange,
onMenuOpenChange
@ -444,7 +446,7 @@ export default function WorkspaceKanbanDrawer({
useWorkspaceKanbanShiftWheelScroll(boardRef, laneScrollerRef, open, isPointerDragActiveRef)
useWorkspaceKanbanOutsideDismiss({ open, boardRef, preserveOpenForMenu, onOpenChange })
useContextualTour('workspace-board', open, 'workspace_board_visible')
useContextualTour('workspace-board', open && !dragPreview, 'workspace_board_visible')
useEffect(() => {
if (!open || selectedWorktreeIds.size === 0) {
@ -494,6 +496,7 @@ export default function WorkspaceKanbanDrawer({
}
data-contextual-tour-target="workspace-board-surface"
data-workspace-board-sheet=""
data-workspace-board-drag-preview={dragPreview ? 'true' : undefined}
onOpenAutoFocus={(event) => {
// Why: Radix focuses the first toolbar button on open, which opens
// its tooltip without hover and makes the drawer feel noisy.

View File

@ -142,6 +142,7 @@ import {
getWorkspaceKanbanSidebarDropGroups,
getWorkspaceKanbanSidebarDropTarget,
hasWorkspaceKanbanSidebarDropBoard,
isWorkspaceKanbanSidebarDropPointInBoard,
updateWorkspaceKanbanSidebarDropTargetVisual
} from './workspace-kanban-sidebar-drop'
import {
@ -158,6 +159,7 @@ import {
setSidebarPointerDragDocumentStyles,
updateSidebarDragPreviewPosition
} from './worktree-sidebar-pointer-drag-dom'
import { shouldStartWorkspaceBoardDragPreview } from './workspace-board-drag-preview-intent'
import {
getWorktreeSidebarDragAutoscroll,
getWorktreeSidebarDragRectsForGroup,
@ -265,6 +267,7 @@ const EMPTY_AGENT_STATUS_BY_PANE_KEY: AppState['agentStatusByPaneKey'] = {}
const EMPTY_TABS_BY_WORKTREE: AppState['tabsByWorktree'] = {}
const EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID: AppState['terminalLayoutsByTabId'] = {}
const EXPANDING_CARD_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS = 300
const NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK = (): void => {}
const WORKTREE_SIDEBAR_SCROLL_STYLE: React.CSSProperties = {
// Why: TanStack Virtual owns scroll correction. Native browser anchoring can
// fight virtual row measurement/remounts and produce visible jumps.
@ -487,6 +490,10 @@ type VirtualizedWorktreeViewportProps = {
dropIndex: number
groups: readonly WorktreeDragGroup[]
}) => void
workspaceBoardOpen: boolean
onWorkspaceBoardDragPreviewStart: () => void
onWorkspaceBoardDragPreviewCommit: () => void
onWorkspaceBoardDragPreviewCancel: () => void
shouldShowWorkspaceBoardDropIndicator: (
worktreeIds: readonly string[],
status: WorkspaceStatus
@ -740,6 +747,7 @@ type WorktreePointerDrag = {
preview: HTMLElement | null
previewOffsetX: number
previewOffsetY: number
workspaceBoardDragPreviewRequested: boolean
frameId: number | null
latestBoardDropTarget: WorkspaceKanbanCardTrackedDropTarget | null
latestStatusDropTarget: WorktreeSidebarTrackedStatusDropTarget | null
@ -1085,6 +1093,10 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
onPinWorktree,
onPinWorktrees,
onDropWorktreesOnWorkspaceBoard,
workspaceBoardOpen,
onWorkspaceBoardDragPreviewStart,
onWorkspaceBoardDragPreviewCommit,
onWorkspaceBoardDragPreviewCancel,
shouldShowWorkspaceBoardDropIndicator,
onReorderWorktrees,
scrollOffsetRef,
@ -2108,7 +2120,8 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
setDragOverStatus(null)
setPinDragOver(false)
clearWorkspaceKanbanSidebarDropTargetVisual()
}, [cancelWorktreePointerAutoscroll])
onWorkspaceBoardDragPreviewCancel()
}, [cancelWorktreePointerAutoscroll, onWorkspaceBoardDragPreviewCancel])
const clearWorktreeDrag = useCallback(() => {
cleanupWorktreePointerDrag()
@ -2157,6 +2170,25 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
clearWorktreeDrag()
return
}
const previewSidebarContainer = scrollRef.current
if (
!drag.workspaceBoardDragPreviewRequested &&
!workspaceBoardOpen &&
previewSidebarContainer
) {
const sidebarRect = previewSidebarContainer.getBoundingClientRect()
if (
shouldStartWorkspaceBoardDragPreview({
pointerX: drag.currentX,
startX: drag.startX,
sidebarRight: sidebarRect.right
}) &&
!hasWorkspaceKanbanSidebarDropBoard()
) {
drag.workspaceBoardDragPreviewRequested = true
onWorkspaceBoardDragPreviewStart()
}
}
const boardTarget = updateWorkspaceKanbanSidebarDropTargetVisual({
x: drag.currentX,
y: drag.currentY,
@ -2170,6 +2202,9 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
x: drag.currentX,
y: drag.currentY
}
if (isWorkspaceKanbanSidebarDropPointInBoard(drag.currentX, drag.currentY)) {
onWorkspaceBoardDragPreviewCommit()
}
if (boardTarget.status || boardTarget.isPinDrop) {
drag.latestStatusDropTarget = null
setDragOverStatus(null)
@ -2317,8 +2352,11 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
clearWorktreeDrag,
computeWorktreeDrop,
computeWorktreeStatusDrop,
onWorkspaceBoardDragPreviewStart,
refreshWorktreeDragSession,
onWorkspaceBoardDragPreviewCommit,
shouldShowWorkspaceBoardDropIndicator,
workspaceBoardOpen,
workspaceStatuses
])
@ -2437,7 +2475,14 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
return
}
const rects = getWorktreeSidebarDragRectsForGroup(container, sourceGroupKey)
if (rects.length <= 1 && !hasWorkspaceKanbanSidebarDropBoard()) {
const canPreviewWorkspaceBoardOnDrag =
!workspaceBoardOpen &&
onWorkspaceBoardDragPreviewStart !== NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK
if (
rects.length <= 1 &&
!hasWorkspaceKanbanSidebarDropBoard() &&
!canPreviewWorkspaceBoardOnDrag
) {
return
}
const draggedIds =
@ -2463,6 +2508,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
preview: null,
previewOffsetX: 0,
previewOffsetY: 0,
workspaceBoardDragPreviewRequested: false,
frameId: null,
latestBoardDropTarget: null,
latestStatusDropTarget: null
@ -2472,8 +2518,10 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
getReorderDraggedIds,
getReorderUnitDraggedIds,
groupKeyByRowKey,
onWorkspaceBoardDragPreviewStart,
selectedWorktreeIds,
selectedWorktrees
selectedWorktrees,
workspaceBoardOpen
]
)
@ -2528,6 +2576,9 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
x: event.clientX,
y: event.clientY
})
if (isWorkspaceKanbanSidebarDropPointInBoard(event.clientX, event.clientY)) {
onWorkspaceBoardDragPreviewCommit()
}
if (boardDropTarget.isPinDrop) {
onPinWorktrees(drag.draggedIds)
} else if (boardDropTarget.status) {
@ -2649,6 +2700,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
onDropWorktreesOnWorkspaceBoard,
onPinWorktrees,
onReorderWorktrees,
onWorkspaceBoardDragPreviewCommit,
refreshWorktreeDragSession,
scheduleWorktreePointerDragFrame,
shouldShowWorkspaceBoardDropIndicator,
@ -4268,6 +4320,10 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
type WorktreeListProps = {
scrollOffsetRef: React.MutableRefObject<number>
scrollAnchorRef: React.MutableRefObject<VirtualizedScrollAnchor>
workspaceBoardOpen?: boolean
onWorkspaceBoardDragPreviewStart?: () => void
onWorkspaceBoardDragPreviewCommit?: () => void
onWorkspaceBoardDragPreviewCancel?: () => void
}
export function installWorktreeVisibleRefreshVisibilityListener(onChange: () => void): () => void {
@ -4277,7 +4333,11 @@ export function installWorktreeVisibleRefreshVisibilityListener(onChange: () =>
const WorktreeList = React.memo(function WorktreeList({
scrollOffsetRef,
scrollAnchorRef
scrollAnchorRef,
workspaceBoardOpen = false,
onWorkspaceBoardDragPreviewStart = NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK,
onWorkspaceBoardDragPreviewCommit = NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK,
onWorkspaceBoardDragPreviewCancel = NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK
}: WorktreeListProps) {
// ── Granular selectors (each is a primitive or shallow-stable ref) ──
const allWorktrees = useAllWorktrees()
@ -5711,6 +5771,10 @@ const WorktreeList = React.memo(function WorktreeList({
onPinWorktree={pinWorktree}
onPinWorktrees={pinWorktrees}
onDropWorktreesOnWorkspaceBoard={dropWorktreesOnWorkspaceBoard}
workspaceBoardOpen={workspaceBoardOpen}
onWorkspaceBoardDragPreviewStart={onWorkspaceBoardDragPreviewStart}
onWorkspaceBoardDragPreviewCommit={onWorkspaceBoardDragPreviewCommit}
onWorkspaceBoardDragPreviewCancel={onWorkspaceBoardDragPreviewCancel}
shouldShowWorkspaceBoardDropIndicator={shouldShowWorkspaceBoardDropIndicator}
onReorderWorktrees={reorderWorktrees}
scrollOffsetRef={scrollOffsetRef}

View File

@ -43,11 +43,16 @@ function Sidebar({
const { nativeDropTarget, dropHandlers, affordance } = useSidebarProjectDrop()
const {
workspaceBoardOpen,
workspaceBoardRenderedOpen,
workspaceBoardDragPreviewOpen,
workspaceBoardMenuOpen,
toggleWorkspaceBoard,
handleWorkspaceBoardOpenChange,
setWorkspaceBoardMenuOpen,
closeWorkspaceBoard
closeWorkspaceBoard,
previewWorkspaceBoardFromDrag,
solidifyWorkspaceBoardFromDrag,
cancelWorkspaceBoardDragPreview
} = useWorkspaceBoardPanel()
const setLiveSidebarWidth = React.useCallback((width: number) => {
@ -63,10 +68,10 @@ function Sidebar({
}, [repoCount, fetchAllWorktrees])
useEffect(() => {
if (!sidebarOpen && workspaceBoardOpen) {
if (!sidebarOpen && workspaceBoardRenderedOpen) {
closeWorkspaceBoard()
}
}, [closeWorkspaceBoard, sidebarOpen, workspaceBoardOpen])
}, [closeWorkspaceBoard, sidebarOpen, workspaceBoardRenderedOpen])
const { containerRef, onResizeStart } = useSidebarResize<HTMLDivElement>({
isOpen: sidebarOpen,
@ -95,6 +100,10 @@ function Sidebar({
<WorktreeList
scrollOffsetRef={worktreeScrollOffsetRef}
scrollAnchorRef={worktreeScrollAnchorRef}
workspaceBoardOpen={workspaceBoardOpen}
onWorkspaceBoardDragPreviewStart={previewWorkspaceBoardFromDrag}
onWorkspaceBoardDragPreviewCommit={solidifyWorkspaceBoardFromDrag}
onWorkspaceBoardDragPreviewCancel={cancelWorkspaceBoardDragPreview}
/>
<SetupScriptPromptCard />
@ -146,7 +155,8 @@ function Sidebar({
</React.Suspense>
{sidebarOpen ? (
<WorkspaceKanbanDrawer
open={workspaceBoardOpen}
open={workspaceBoardRenderedOpen}
dragPreview={workspaceBoardDragPreviewOpen}
preserveOpenForMenu={workspaceBoardMenuOpen}
onOpenChange={handleWorkspaceBoardOpenChange}
onMenuOpenChange={setWorkspaceBoardMenuOpen}

View File

@ -73,14 +73,52 @@ describe('useWorkspaceBoardPanel', () => {
await updatePanel((state) => state.toggleWorkspaceBoard())
expect(panelState().workspaceBoardOpen).toBe(true)
expect(panelState().workspaceBoardRenderedOpen).toBe(true)
expect(panelState().workspaceBoardDragPreviewOpen).toBe(false)
expect(mocks.recordFeatureInteraction).toHaveBeenCalledExactlyOnceWith('workspace-board')
await updatePanel((state) => state.toggleWorkspaceBoard())
expect(panelState().workspaceBoardOpen).toBe(false)
expect(panelState().workspaceBoardRenderedOpen).toBe(false)
expect(mocks.recordFeatureInteraction).toHaveBeenCalledOnce()
})
it('renders a drag preview without recording an open interaction', async () => {
await renderHookProbe()
await updatePanel((state) => state.previewWorkspaceBoardFromDrag())
expect(panelState().workspaceBoardOpen).toBe(false)
expect(panelState().workspaceBoardRenderedOpen).toBe(true)
expect(panelState().workspaceBoardDragPreviewOpen).toBe(true)
expect(mocks.recordFeatureInteraction).not.toHaveBeenCalled()
})
it('cancels an uncommitted drag preview', async () => {
await renderHookProbe()
await updatePanel((state) => state.previewWorkspaceBoardFromDrag())
await updatePanel((state) => state.cancelWorkspaceBoardDragPreview())
expect(panelState().workspaceBoardOpen).toBe(false)
expect(panelState().workspaceBoardRenderedOpen).toBe(false)
expect(panelState().workspaceBoardDragPreviewOpen).toBe(false)
})
it('solidifies a drag preview and keeps the board open after drag cleanup', async () => {
await renderHookProbe()
await updatePanel((state) => state.previewWorkspaceBoardFromDrag())
await updatePanel((state) => state.solidifyWorkspaceBoardFromDrag())
await updatePanel((state) => state.cancelWorkspaceBoardDragPreview())
expect(panelState().workspaceBoardOpen).toBe(true)
expect(panelState().workspaceBoardRenderedOpen).toBe(true)
expect(panelState().workspaceBoardDragPreviewOpen).toBe(false)
expect(mocks.recordFeatureInteraction).toHaveBeenCalledExactlyOnceWith('workspace-board')
})
it('keeps the board open on Escape while a nested board menu is open', async () => {
await renderHookProbe()

View File

@ -13,34 +13,50 @@ const WORKSPACE_BOARD_ESCAPE_BLOCKING_OVERLAY_SELECTOR = [
export type WorkspaceBoardPanelState = {
workspaceBoardOpen: boolean
workspaceBoardRenderedOpen: boolean
workspaceBoardDragPreviewOpen: boolean
workspaceBoardMenuOpen: boolean
openWorkspaceBoard: () => void
closeWorkspaceBoard: () => void
toggleWorkspaceBoard: () => void
handleWorkspaceBoardOpenChange: (open: boolean) => void
setWorkspaceBoardMenuOpen: (open: boolean) => void
previewWorkspaceBoardFromDrag: () => void
solidifyWorkspaceBoardFromDrag: () => void
cancelWorkspaceBoardDragPreview: () => void
}
export function useWorkspaceBoardPanel(): WorkspaceBoardPanelState {
const [workspaceBoardOpen, setWorkspaceBoardOpen] = useState(false)
const [workspaceBoardDragPreviewOpen, setWorkspaceBoardDragPreviewOpen] = useState(false)
const [workspaceBoardMenuOpen, setWorkspaceBoardMenuOpen] = useState(false)
const workspaceBoardOpenRef = useRef(workspaceBoardOpen)
const workspaceBoardDragPreviewOpenRef = useRef(workspaceBoardDragPreviewOpen)
workspaceBoardOpenRef.current = workspaceBoardOpen
workspaceBoardDragPreviewOpenRef.current = workspaceBoardDragPreviewOpen
const openWorkspaceBoard = useCallback(() => {
if (workspaceBoardOpenRef.current) {
if (workspaceBoardDragPreviewOpenRef.current) {
workspaceBoardDragPreviewOpenRef.current = false
setWorkspaceBoardDragPreviewOpen(false)
}
return
}
workspaceBoardOpenRef.current = true
workspaceBoardDragPreviewOpenRef.current = false
// Why: opening the board is the user action; recording here avoids a
// post-render bookkeeping Effect in the drawer.
useAppStore.getState().recordFeatureInteraction('workspace-board')
setWorkspaceBoardOpen(true)
setWorkspaceBoardDragPreviewOpen(false)
}, [])
const closeWorkspaceBoard = useCallback(() => {
workspaceBoardOpenRef.current = false
workspaceBoardDragPreviewOpenRef.current = false
setWorkspaceBoardOpen(false)
setWorkspaceBoardDragPreviewOpen(false)
setWorkspaceBoardMenuOpen(false)
}, [])
@ -63,6 +79,37 @@ export function useWorkspaceBoardPanel(): WorkspaceBoardPanelState {
openWorkspaceBoard()
}, [closeWorkspaceBoard, openWorkspaceBoard])
const previewWorkspaceBoardFromDrag = useCallback(() => {
if (workspaceBoardOpenRef.current || workspaceBoardDragPreviewOpenRef.current) {
return
}
workspaceBoardDragPreviewOpenRef.current = true
setWorkspaceBoardDragPreviewOpen(true)
}, [])
const solidifyWorkspaceBoardFromDrag = useCallback(() => {
if (workspaceBoardOpenRef.current) {
if (workspaceBoardDragPreviewOpenRef.current) {
workspaceBoardDragPreviewOpenRef.current = false
setWorkspaceBoardDragPreviewOpen(false)
}
return
}
workspaceBoardOpenRef.current = true
workspaceBoardDragPreviewOpenRef.current = false
useAppStore.getState().recordFeatureInteraction('workspace-board')
setWorkspaceBoardOpen(true)
setWorkspaceBoardDragPreviewOpen(false)
}, [])
const cancelWorkspaceBoardDragPreview = useCallback(() => {
if (!workspaceBoardDragPreviewOpenRef.current) {
return
}
workspaceBoardDragPreviewOpenRef.current = false
setWorkspaceBoardDragPreviewOpen(false)
}, [])
useEffect(() => {
if (!workspaceBoardOpen) {
return
@ -92,11 +139,16 @@ export function useWorkspaceBoardPanel(): WorkspaceBoardPanelState {
return {
workspaceBoardOpen,
workspaceBoardRenderedOpen: workspaceBoardOpen || workspaceBoardDragPreviewOpen,
workspaceBoardDragPreviewOpen: workspaceBoardDragPreviewOpen && !workspaceBoardOpen,
workspaceBoardMenuOpen,
openWorkspaceBoard,
closeWorkspaceBoard,
toggleWorkspaceBoard,
handleWorkspaceBoardOpenChange,
setWorkspaceBoardMenuOpen
setWorkspaceBoardMenuOpen,
previewWorkspaceBoardFromDrag,
solidifyWorkspaceBoardFromDrag,
cancelWorkspaceBoardDragPreview
}
}

View File

@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { shouldStartWorkspaceBoardDragPreview } from './workspace-board-drag-preview-intent'
describe('workspace board drag preview intent', () => {
it('does not start the expensive board preview for small sidebar reorder drags', () => {
expect(
shouldStartWorkspaceBoardDragPreview({
pointerX: 132,
startX: 96,
sidebarRight: 285
})
).toBe(false)
})
it('starts the board preview after a rightward drag reaches the sidebar edge zone', () => {
expect(
shouldStartWorkspaceBoardDragPreview({
pointerX: 252,
startX: 96,
sidebarRight: 285
})
).toBe(true)
})
it('ignores edge-zone drags that did not move right enough to signal board intent', () => {
expect(
shouldStartWorkspaceBoardDragPreview({
pointerX: 252,
startX: 242,
sidebarRight: 285
})
).toBe(false)
})
})

View File

@ -0,0 +1,13 @@
const WORKSPACE_BOARD_DRAG_PREVIEW_EDGE_ZONE_PX = 48
const WORKSPACE_BOARD_DRAG_PREVIEW_MIN_RIGHTWARD_PX = 16
export function shouldStartWorkspaceBoardDragPreview(args: {
pointerX: number
startX: number
sidebarRight: number
}): boolean {
return (
args.pointerX >= args.sidebarRight - WORKSPACE_BOARD_DRAG_PREVIEW_EDGE_ZONE_PX &&
args.pointerX - args.startX >= WORKSPACE_BOARD_DRAG_PREVIEW_MIN_RIGHTWARD_PX
)
}

View File

@ -5,6 +5,7 @@ import {
clearWorkspaceKanbanSidebarDropTargetVisual,
getWorkspaceKanbanSidebarDropGroups,
getWorkspaceKanbanSidebarDropTarget,
isWorkspaceKanbanSidebarDropPointInBoard,
updateWorkspaceKanbanSidebarDropTargetVisual
} from './workspace-kanban-sidebar-drop'
@ -264,6 +265,21 @@ describe('workspace kanban sidebar drop DOM bridge', () => {
expect(lane.hasAttribute('data-workspace-board-external-drag-target')).toBe(false)
expect(document.querySelector('[data-workspace-board-card-drop-indicator]')).toBeNull()
})
it('detects pointer entry across the whole board sheet', () => {
const sheet = document.createElement('div')
sheet.setAttribute('data-workspace-board-sheet', '')
setRect(sheet, { left: 300, top: 36, right: 900, bottom: 700, width: 600, height: 664 })
const { board } = appendBoard()
board.remove()
sheet.append(board)
document.body.append(sheet)
expect(isWorkspaceKanbanSidebarDropPointInBoard(320, 60)).toBe(true)
expect(isWorkspaceKanbanSidebarDropPointInBoard(280, 60)).toBe(false)
expect(isWorkspaceKanbanSidebarDropPointInBoard(320, 720)).toBe(false)
})
})
describe('workspace kanban sidebar drop updates', () => {

View File

@ -21,6 +21,7 @@ import {
} from './workspace-kanban-card-pointer-drag-dom'
const BOARD_SELECTOR = '[data-workspace-board-selection-surface]'
const BOARD_SHEET_SELECTOR = '[data-workspace-board-sheet]'
const EXTERNAL_DRAG_TARGET_ATTR = 'data-workspace-board-external-drag-target'
let externalDragTargetElement: HTMLElement | null = null
@ -33,6 +34,16 @@ export function hasWorkspaceKanbanSidebarDropBoard(): boolean {
return getWorkspaceKanbanBoardElement() !== null
}
export function isWorkspaceKanbanSidebarDropPointInBoard(x: number, y: number): boolean {
const board = getWorkspaceKanbanBoardElement()
if (!board) {
return false
}
const hitSurface = board.closest<HTMLElement>(BOARD_SHEET_SELECTOR) ?? board
const rect = hitSurface.getBoundingClientRect()
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
}
function getStatusDropTargetElement(
board: HTMLElement,
status: WorkspaceStatus