From af0dc06f49b6ee5b92491a6099d10f828126f005 Mon Sep 17 00:00:00 2001 From: jelveh Date: Fri, 17 Jul 2026 08:47:58 -0700 Subject: [PATCH] fix(dashboard): make app drag-to-reorder work on touch (iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On iOS/WebKit the pager's `touch-action: pan-x` made horizontal touchmove non-cancelable, so the moment a long-press drag moved the finger the browser started a native page-pan and fired pointercancel, aborting the reorder before it visibly began. Preventing pointermove doesn't stop native scrolling on iOS, and the reordering `touch-action: none` rule was applied only after the drag began — too late, since touch-action is latched at gesture start. Drop the explicit pan-x (back to auto) so horizontal touchmove is cancelable, and install a non-passive touchmove listener that preventDefaults only once reordering is armed (post long-press). Before arming it's a no-op, so a quick swipe still flips pages natively. --- src/gui/src/UI/Dashboard/TabApps.js | 13 +++++++++++++ src/gui/src/css/dashboard.css | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js index 2691be6f8..ea4a96e52 100644 --- a/src/gui/src/UI/Dashboard/TabApps.js +++ b/src/gui/src/UI/Dashboard/TabApps.js @@ -603,6 +603,18 @@ const TabApps = { document.addEventListener('keydown', d.onKey); window.addEventListener('blur', d.onBlur); + // iOS/WebKit latches touch-action at gesture start and treats an + // in-progress pan as non-cancelable, so preventing pointermove can't + // stop the pager from scrolling out from under a drag (it just fires + // pointercancel and kills the reorder). A non-passive touchmove that + // preventDefaults *once reordering is armed* is what actually holds the + // scroller still. Before arming (readyToDrag false) it's a no-op, so a + // quick pre-long-press swipe still flips pages natively. + if ( pointerType === 'touch' ) { + d.onTouchMove = ev => { if ( d.readyToDrag ) ev.preventDefault(); }; + document.addEventListener('touchmove', d.onTouchMove, { passive: false }); + } + // Touch: a long-press *arms* reordering (it doesn't grab the tile yet). // Moving after that begins the drag; holding still instead lets the // native long-press context menu (Uninstall) fire. A finger that moves @@ -831,6 +843,7 @@ const TabApps = { document.removeEventListener('pointercancel', d.onCancel); document.removeEventListener('keydown', d.onKey); window.removeEventListener('blur', d.onBlur); + if ( d.onTouchMove ) document.removeEventListener('touchmove', d.onTouchMove, { passive: false }); clearTimeout(d.longPressTimer); clearTimeout(d.edgeTimer); clearTimeout(d.flipClearTimer); diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index c98ba0152..926372aa7 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -776,7 +776,12 @@ input.myapps-search::-webkit-search-decoration { padding-top: 8px; scroll-snap-type: x mandatory; overscroll-behavior-x: contain; - touch-action: pan-x; + /* Deliberately not pan-x: an explicit pan value makes horizontal touchmove + non-cancelable on iOS/WebKit, so the pager pans out from under a + long-press drag and cancels it. With the default (auto) the non-passive + touchmove listener installed during a reorder can preventDefault the + native pan; a quick pre-long-press swipe still flips pages natively. */ + touch-action: auto; scrollbar-width: none; }