Skip to content

Commit 5c9c318

Browse files
committed
Make Kanban cards actually draggable
Cards were structured as a draggable parent wrapping <button> children for the checkbox, content, and open-arrow. Chromium absorbs the mousedown on <button> elements and refuses to start a drag on the parent — so users grabbing the bulk of the card (the content button) saw nothing happen. Restructured: the content area is now a div with role=button + tabIndex for keyboard parity, and the small interactive buttons (checkbox, open arrow) explicitly opt out of being drag sources with draggable=false + onMouseDown stopPropagation. Drags from the card body now reliably fire. Also moved the cardRef from the (removed) content button to the outer card div so scrollIntoView still tracks the focused card.
1 parent 327dcc6 commit 5c9c318

1 file changed

Lines changed: 31 additions & 9 deletions

File tree

packages/app-core/src/components/TasksKanban.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export function TasksKanban({ tasks, today, onOpenTask, onToggleTask }: Props):
188188
const [cardIdx, setCardIdx] = useState(0)
189189
const [draggingId, setDraggingId] = useState<string | null>(null)
190190
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null)
191-
const cardRef = useRef<HTMLButtonElement | null>(null)
191+
const cardRef = useRef<HTMLDivElement | null>(null)
192192

193193
const columns = useMemo(() => buildColumns(groupBy, tasks, today), [groupBy, tasks, today])
194194

@@ -365,7 +365,7 @@ export function TasksKanban({ tasks, today, onOpenTask, onToggleTask }: Props):
365365
isFocused={isFocused}
366366
isDragging={isDragging}
367367
draggable={dndEnabled}
368-
buttonRef={isFocused ? cardRef : null}
368+
cardRef={isFocused ? cardRef : null}
369369
onClickRow={() => {
370370
setColIdx(ci)
371371
setCardIdx(ti)
@@ -417,7 +417,7 @@ interface CardProps {
417417
isFocused: boolean
418418
isDragging: boolean
419419
draggable: boolean
420-
buttonRef?: React.RefObject<HTMLButtonElement> | null
420+
cardRef?: React.RefObject<HTMLDivElement> | null
421421
onClickRow: () => void
422422
onOpen: () => void
423423
onToggle: () => void
@@ -438,7 +438,7 @@ function TaskCard({
438438
isFocused,
439439
isDragging,
440440
draggable,
441-
buttonRef,
441+
cardRef,
442442
onClickRow,
443443
onOpen,
444444
onToggle,
@@ -447,6 +447,7 @@ function TaskCard({
447447
}: CardProps): JSX.Element {
448448
return (
449449
<div
450+
ref={cardRef ?? undefined}
450451
onClick={onClickRow}
451452
draggable={draggable}
452453
onDragStart={(e) => {
@@ -466,10 +467,19 @@ function TaskCard({
466467
].join(' ')}
467468
>
468469
<div className="flex items-start gap-2">
470+
{/* The interactive controls (checkbox, open arrow) are nested
471+
inside the draggable card. Chromium absorbs mousedown on
472+
<button> children and refuses to start the parent's drag,
473+
so the buttons explicitly opt out of being drag sources
474+
(`draggable={false}`) AND swallow mousedown when the user
475+
actually wants a drag. The drag is then initiated from the
476+
non-button content area below. */}
469477
<button
470478
type="button"
471479
role="checkbox"
472480
aria-checked={task.checked}
481+
draggable={false}
482+
onMouseDown={(e) => e.stopPropagation()}
473483
onClick={(e) => {
474484
e.stopPropagation()
475485
onToggle()
@@ -496,24 +506,36 @@ function TaskCard({
496506
</svg>
497507
)}
498508
</button>
499-
<button
500-
type="button"
501-
ref={buttonRef ?? undefined}
509+
{/* The card body. Was a <button> — switched to a div with
510+
role/tabIndex so the parent's `draggable=true` works
511+
reliably from the bulk of the card. */}
512+
<div
513+
role="button"
514+
tabIndex={0}
502515
onClick={(e) => {
503516
e.stopPropagation()
504517
onOpen()
505518
}}
519+
onKeyDown={(e) => {
520+
if (e.key === 'Enter' || e.key === ' ') {
521+
e.preventDefault()
522+
e.stopPropagation()
523+
onOpen()
524+
}
525+
}}
506526
className={[
507-
'min-w-0 flex-1 text-left text-sm',
527+
'min-w-0 flex-1 text-left text-sm select-none',
508528
task.checked ? 'text-current/50 line-through' : 'text-current/90'
509529
].join(' ')}
510530
>
511531
{task.content || '(empty task)'}
512-
</button>
532+
</div>
513533
<button
514534
type="button"
515535
aria-label={`Open ${task.noteTitle}`}
516536
title="Open note (Enter)"
537+
draggable={false}
538+
onMouseDown={(e) => e.stopPropagation()}
517539
onClick={(e) => {
518540
e.stopPropagation()
519541
onOpen()

0 commit comments

Comments
 (0)