From cecfdf2dcc1857fc9feda1c1bcf2fa4a6721481d Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 18 Oct 2026 13:17:43 +0200 Subject: [PATCH] fix(packing): tell a dragged category by its data type, not by a state flag A flag set on dragstart could still be unrendered when a quick drag reached the next card, and the drop was lost. The category now travels in the drag under its own type, read from the first dragover on and at the drop. --- .../Packing/PackingListPanelCategoryGroup.tsx | 31 ++++++++++++++----- .../Packing/PackingListPanelList.tsx | 6 +--- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/client/src/components/Packing/PackingListPanelCategoryGroup.tsx b/client/src/components/Packing/PackingListPanelCategoryGroup.tsx index c911c8c1b..d3f635538 100644 --- a/client/src/components/Packing/PackingListPanelCategoryGroup.tsx +++ b/client/src/components/Packing/PackingListPanelCategoryGroup.tsx @@ -39,16 +39,14 @@ interface KategorieGruppeProps { // the reader cannot see. reorderable?: boolean // Moving the whole category among the others, wired by the list, which sees every - // card. `active` is true while any category is being dragged, so a dragged ITEM - // passing over a card is left to the item rows. + // card. `onDrop` gets the category that was dragged onto this one. categoryDrag?: { - active: boolean isDragging: boolean isOver: boolean onStart: () => void onOver: () => void onEnd: () => void - onDrop: () => void + onDrop: (dragged: string) => void } // Three-tier sharing (#858) — threaded down to each item's share control. currentUserId?: number @@ -363,16 +361,28 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen type CategoryDrag = NonNullable +/** The data type a dragged category carries, its name as the value. */ +const CATEGORY_DRAG_TYPE = 'application/x-trek-packing-category' + /** * The card, and while a category is dragged its drop target. Captured, because * the item rows stop their own drops from bubbling. */ function CategoryDropFrame({ drag, children }: { drag?: CategoryDrag; children: React.ReactNode }) { - const target = drag?.active ? drag : null + // Told apart by the drag's own data type, which is there from the first event on: + // a flag set in state on dragstart can still be unrendered when a quick drag + // arrives, and a dragged item must pass through to its rows untouched. + const isCategoryDrag = (e: React.DragEvent) => e.dataTransfer.types.includes(CATEGORY_DRAG_TYPE) return (
{ e.preventDefault(); e.dataTransfer.dropEffect = 'move'; target.onOver() }) : undefined} - onDropCapture={target ? (e => { e.preventDefault(); e.stopPropagation(); target.onDrop() }) : undefined} + onDragOverCapture={drag ? (e => { + if (!isCategoryDrag(e)) return + e.preventDefault(); e.dataTransfer.dropEffect = 'move'; drag.onOver() + }) : undefined} + onDropCapture={drag ? (e => { + if (!isCategoryDrag(e)) return + e.preventDefault(); e.stopPropagation(); drag.onDrop(e.dataTransfer.getData(CATEGORY_DRAG_TYPE)) + }) : undefined} style={{ marginBottom: 6, background: 'var(--bg-card)', borderRadius: 16, border: '1px solid var(--border-secondary)', overflow: 'visible', opacity: drag?.isDragging ? 0.4 : 1, @@ -394,7 +404,12 @@ function CategoryGrip({ drag, kategorie, label }: { drag?: CategoryDrag; kategor role="button" aria-label={label} title={label} - onDragStart={e => { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', kategorie); drag.onStart() }} + onDragStart={e => { + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData(CATEGORY_DRAG_TYPE, kategorie) + e.dataTransfer.setData('text/plain', kategorie) + drag.onStart() + }} onDragEnd={drag.onEnd} style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'var(--text-faint)', flexShrink: 0, marginInlineStart: -6 }} > diff --git a/client/src/components/Packing/PackingListPanelList.tsx b/client/src/components/Packing/PackingListPanelList.tsx index ac9c950f9..dac88aa91 100644 --- a/client/src/components/Packing/PackingListPanelList.tsx +++ b/client/src/components/Packing/PackingListPanelList.tsx @@ -47,16 +47,12 @@ export function PackingList(S: PackingState) { onReorder={(orderedIds) => reorderPackingItems(tripId, orderedIds)} reorderable={sort === 'manual'} categoryDrag={canEdit ? { - active: draggedCategory !== null, isDragging: draggedCategory === kat, isOver: overCategory === kat && draggedCategory !== null && draggedCategory !== kat, onStart: () => { setDraggedCategory(kat); setOverCategory(null) }, onOver: () => setOverCategory(kat), onEnd: endCategoryDrag, - onDrop: () => { - if (draggedCategory !== null) moveCategory(draggedCategory, kat) - endCategoryDrag() - }, + onDrop: (dragged) => { moveCategory(dragged, kat); endCategoryDrag() }, } : undefined} currentUserId={currentUserId} onSetSharing={handleSetSharing}