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:
@@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user