feat(bases): drop cards onto empty kanban columns / below last card

This commit is contained in:
Philipinho
2026-05-24 15:38:34 +01:00
parent bdfd0413b4
commit d97d8108d2
3 changed files with 64 additions and 1 deletions
@@ -4,6 +4,7 @@ import { KanbanCard } from "./kanban-card";
import { KanbanColumnHeader } from "./kanban-column-header";
import { KanbanAddCardButton } from "./kanban-add-card-button";
import type { CardDropPayload } from "@/features/base/hooks/use-kanban-card-drag";
import { useKanbanColumnDrop } from "@/features/base/hooks/use-kanban-column-drop";
import classes from "@/features/base/styles/kanban.module.css";
type KanbanColumnProps = {
@@ -21,6 +22,11 @@ export function KanbanColumn({
onAddCard,
onCardDrop,
}: KanbanColumnProps) {
const { ref: bodyRef, isOver } = useKanbanColumnDrop({
columnKey: column.key,
onDrop: onCardDrop,
});
return (
<div className={classes.column} data-column-key={column.key}>
<KanbanColumnHeader
@@ -28,7 +34,12 @@ export function KanbanColumn({
color={column.color}
count={column.rows.length}
/>
<div className={classes.columnBody} data-column-body={column.key}>
<div
ref={bodyRef}
className={classes.columnBody}
data-column-body={column.key}
data-over={isOver || undefined}
>
{column.rows.map((row) => (
<KanbanCard
key={row.id}
@@ -0,0 +1,48 @@
import { useEffect, useRef, useState } from "react";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import type { CardDropPayload } from "./use-kanban-card-drag";
export const COLUMN_BODY_TARGET_ID = "__column-body__";
export function useKanbanColumnDrop({
columnKey,
onDrop,
}: {
columnKey: string;
onDrop: (payload: CardDropPayload) => void;
}) {
const ref = useRef<HTMLDivElement>(null);
const [isOver, setIsOver] = useState(false);
// Keep onDrop fresh without re-registering the effect each render.
const onDropRef = useRef(onDrop);
onDropRef.current = onDrop;
useEffect(() => {
const el = ref.current;
if (!el) return;
return dropTargetForElements({
element: el,
canDrop: ({ source }) => source.data.type === "base-kanban-card",
getIsSticky: () => true,
onDragEnter: () => setIsOver(true),
onDragLeave: () => setIsOver(false),
onDrop: ({ source }) => {
setIsOver(false);
if (source.data.type !== "base-kanban-card") return;
// If a card-level target inside this column already handled the
// drop, Pragmatic-dnd only invokes the innermost matching target,
// so this column-body handler won't fire. When it does fire, the
// user missed every card — append to the column.
onDropRef.current({
draggedCardId: source.data.cardId as string,
targetCardId: COLUMN_BODY_TARGET_ID,
edge: "bottom",
sourceColumnKey: source.data.columnKey as string,
targetColumnKey: columnKey,
});
},
});
}, [columnKey]);
return { ref, isOver };
}
@@ -52,6 +52,10 @@
min-height: 80px;
}
.columnBody[data-over="true"] {
background: var(--mantine-color-blue-0);
}
.card {
padding: 10px 12px;
background: var(--mantine-color-body);