From 79d026e756e19b1221322f7b8daca0866bf614be Mon Sep 17 00:00:00 2001 From: BOHA Date: Wed, 10 Jun 2026 19:30:01 +0200 Subject: [PATCH] fix(dnd): mobile item reorder works via long-press instead of scrolling PointerSensor also receives touch-derived pointer events and its 5px distance constraint fired before the TouchSensor's hold delay - the browser claimed the gesture for scrolling (pointercancel) and the drag died. Replaced with MouseSensor (desktop unchanged), TouchSensor now activates on a 300ms hold (tolerance 8px), and all drag handles carry touch-action: none so the browser never starts a scroll from them. Applies to DocumentItemsEditor (offers/issued orders) and the invoice items list. Co-Authored-By: Claude Fable 5 --- .../document/DocumentItemsEditor.tsx | 24 +++++++++++++++---- src/admin/pages/InvoiceDetail.tsx | 20 ++++++++++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/admin/components/document/DocumentItemsEditor.tsx b/src/admin/components/document/DocumentItemsEditor.tsx index 9760553..78d05c8 100644 --- a/src/admin/components/document/DocumentItemsEditor.tsx +++ b/src/admin/components/document/DocumentItemsEditor.tsx @@ -15,7 +15,7 @@ import { DndContext, closestCenter, KeyboardSensor, - PointerSensor, + MouseSensor, TouchSensor, useSensor, useSensors, @@ -170,7 +170,11 @@ function SortableItemRow({ {...listeners} title="Přetáhnout" aria-label="Přetáhnout" - sx={{ cursor: "grab", color: "text.secondary" }} + sx={{ + cursor: "grab", + color: "text.secondary", + touchAction: "none", + }} > {DragIcon} @@ -316,7 +320,11 @@ function SortableItemRow({ {...listeners} title="Přetáhnout" aria-label="Přetáhnout" - sx={{ cursor: "grab", color: "text.secondary" }} + sx={{ + cursor: "grab", + color: "text.secondary", + touchAction: "none", + }} > {DragIcon} @@ -455,10 +463,16 @@ export default function DocumentItemsEditor({ }: DocumentItemsEditorProps) { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + // MouseSensor (NOT PointerSensor): PointerSensor also receives touch-derived + // pointer events and its 5px distance constraint fires before the + // TouchSensor's long-press delay — the browser then claims the gesture for + // scrolling (pointercancel) and the drag dies. Mouse handles desktop, + // TouchSensor handles touch via long-press; the drag handles additionally + // carry touch-action: none so the browser never starts a scroll from them. const dndSensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + useSensor(MouseSensor, { activationConstraint: { distance: 5 } }), useSensor(TouchSensor, { - activationConstraint: { delay: 200, tolerance: 5 }, + activationConstraint: { delay: 300, tolerance: 8 }, }), useSensor(KeyboardSensor), ); diff --git a/src/admin/pages/InvoiceDetail.tsx b/src/admin/pages/InvoiceDetail.tsx index a25c1bd..8eef025 100644 --- a/src/admin/pages/InvoiceDetail.tsx +++ b/src/admin/pages/InvoiceDetail.tsx @@ -31,7 +31,7 @@ import { DndContext, closestCenter, KeyboardSensor, - PointerSensor, + MouseSensor, TouchSensor, useSensor, useSensors, @@ -278,7 +278,11 @@ function SortableInvoiceRow({ {...listeners} title="Přetáhnout" aria-label="Přetáhnout" - sx={{ cursor: "grab", color: "text.secondary" }} + sx={{ + cursor: "grab", + color: "text.secondary", + touchAction: "none", + }} > {DragIcon} @@ -388,7 +392,7 @@ function SortableInvoiceRow({ {...listeners} title="Přetáhnout" aria-label="Přetáhnout" - sx={{ cursor: "grab", color: "text.secondary" }} + sx={{ cursor: "grab", color: "text.secondary", touchAction: "none" }} > {DragIcon} @@ -511,10 +515,16 @@ export default function InvoiceDetail() { [], ); + // MouseSensor (NOT PointerSensor): PointerSensor also receives touch-derived + // pointer events and its 5px distance constraint fires before the + // TouchSensor's long-press delay — the browser then claims the gesture for + // scrolling (pointercancel) and the drag dies. Mouse handles desktop, + // TouchSensor handles touch via long-press; the drag handles additionally + // carry touch-action: none so the browser never starts a scroll from them. const dndSensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + useSensor(MouseSensor, { activationConstraint: { distance: 5 } }), useSensor(TouchSensor, { - activationConstraint: { delay: 200, tolerance: 5 }, + activationConstraint: { delay: 300, tolerance: 8 }, }), useSensor(KeyboardSensor), );