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 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-10 19:30:01 +02:00
parent 40a859f5e1
commit 79d026e756
2 changed files with 34 additions and 10 deletions

View File

@@ -15,7 +15,7 @@ import {
DndContext, DndContext,
closestCenter, closestCenter,
KeyboardSensor, KeyboardSensor,
PointerSensor, MouseSensor,
TouchSensor, TouchSensor,
useSensor, useSensor,
useSensors, useSensors,
@@ -170,7 +170,11 @@ function SortableItemRow({
{...listeners} {...listeners}
title="Přetáhnout" title="Přetáhnout"
aria-label="Přetáhnout" aria-label="Přetáhnout"
sx={{ cursor: "grab", color: "text.secondary" }} sx={{
cursor: "grab",
color: "text.secondary",
touchAction: "none",
}}
> >
{DragIcon} {DragIcon}
</IconButton> </IconButton>
@@ -316,7 +320,11 @@ function SortableItemRow({
{...listeners} {...listeners}
title="Přetáhnout" title="Přetáhnout"
aria-label="Přetáhnout" aria-label="Přetáhnout"
sx={{ cursor: "grab", color: "text.secondary" }} sx={{
cursor: "grab",
color: "text.secondary",
touchAction: "none",
}}
> >
{DragIcon} {DragIcon}
</IconButton> </IconButton>
@@ -455,10 +463,16 @@ export default function DocumentItemsEditor({
}: DocumentItemsEditorProps) { }: DocumentItemsEditorProps) {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); 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( const dndSensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(MouseSensor, { activationConstraint: { distance: 5 } }),
useSensor(TouchSensor, { useSensor(TouchSensor, {
activationConstraint: { delay: 200, tolerance: 5 }, activationConstraint: { delay: 300, tolerance: 8 },
}), }),
useSensor(KeyboardSensor), useSensor(KeyboardSensor),
); );

View File

@@ -31,7 +31,7 @@ import {
DndContext, DndContext,
closestCenter, closestCenter,
KeyboardSensor, KeyboardSensor,
PointerSensor, MouseSensor,
TouchSensor, TouchSensor,
useSensor, useSensor,
useSensors, useSensors,
@@ -278,7 +278,11 @@ function SortableInvoiceRow({
{...listeners} {...listeners}
title="Přetáhnout" title="Přetáhnout"
aria-label="Přetáhnout" aria-label="Přetáhnout"
sx={{ cursor: "grab", color: "text.secondary" }} sx={{
cursor: "grab",
color: "text.secondary",
touchAction: "none",
}}
> >
{DragIcon} {DragIcon}
</IconButton> </IconButton>
@@ -388,7 +392,7 @@ function SortableInvoiceRow({
{...listeners} {...listeners}
title="Přetáhnout" title="Přetáhnout"
aria-label="Přetáhnout" aria-label="Přetáhnout"
sx={{ cursor: "grab", color: "text.secondary" }} sx={{ cursor: "grab", color: "text.secondary", touchAction: "none" }}
> >
{DragIcon} {DragIcon}
</IconButton> </IconButton>
@@ -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( const dndSensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(MouseSensor, { activationConstraint: { distance: 5 } }),
useSensor(TouchSensor, { useSensor(TouchSensor, {
activationConstraint: { delay: 200, tolerance: 5 }, activationConstraint: { delay: 300, tolerance: 8 },
}), }),
useSensor(KeyboardSensor), useSensor(KeyboardSensor),
); );