diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index 67d3cfa4e..e9ba9badf 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -3,9 +3,9 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static targets = ["section", "handle"]; - // Short delay to prevent accidental touches on the grip handle + // Hold delay to require deliberate press-and-hold before activating drag mode static values = { - holdDelay: { type: Number, default: 150 }, + holdDelay: { type: Number, default: 800 }, }; connect() { @@ -22,6 +22,14 @@ export default class extends Controller { // ===== Mouse Drag Events ===== dragStart(event) { + // If a touch interaction is in progress, cancel native drag — + // use touch events with hold delay instead. + // This avoids blocking mouse/trackpad drag on touch-capable laptops. + if (this.isTouching || this.pendingSection) { + event.preventDefault(); + return; + } + this.draggedElement = event.currentTarget; this.draggedElement.classList.add("opacity-50"); this.draggedElement.setAttribute("aria-grabbed", "true"); @@ -88,6 +96,10 @@ export default class extends Controller { this.currentTouchY = this.touchStartY; this.holdActivated = false; + // Prevent text selection while waiting for hold to activate + section.style.userSelect = "none"; + section.style.webkitUserSelect = "none"; + // Start hold timer this.holdTimer = setTimeout(() => { this.activateDrag(); @@ -110,11 +122,25 @@ export default class extends Controller { } touchMove(event) { - if (!this.holdActivated || !this.isTouching || !this.draggedElement) return; + const touchX = event.touches[0].clientX; + const touchY = event.touches[0].clientY; + + // If hold hasn't activated yet, cancel if user moves too far (scrolling or swiping) + // Uses Euclidean distance to catch diagonal gestures too + if (!this.holdActivated) { + const dx = touchX - this.touchStartX; + const dy = touchY - this.touchStartY; + if (dx * dx + dy * dy > 100) { // 10px radius + this.cancelHold(); + } + return; + } + + if (!this.isTouching || !this.draggedElement) return; event.preventDefault(); - this.currentTouchX = event.touches[0].clientX; - this.currentTouchY = event.touches[0].clientY; + this.currentTouchX = touchX; + this.currentTouchY = touchY; const afterElement = this.getDragAfterElement(this.currentTouchX, this.currentTouchY); this.clearPlaceholders(); @@ -159,6 +185,16 @@ export default class extends Controller { } resetTouchState() { + // Restore text selection + if (this.pendingSection) { + this.pendingSection.style.userSelect = ""; + this.pendingSection.style.webkitUserSelect = ""; + } + if (this.draggedElement) { + this.draggedElement.style.userSelect = ""; + this.draggedElement.style.webkitUserSelect = ""; + } + this.isTouching = false; this.draggedElement = null; this.pendingSection = null; diff --git a/app/views/pages/dashboard.html.erb b/app/views/pages/dashboard.html.erb index 5e9127d9d..39db5e283 100644 --- a/app/views/pages/dashboard.html.erb +++ b/app/views/pages/dashboard.html.erb @@ -51,6 +51,7 @@ touchstart->dashboard-sortable#touchStart touchmove->dashboard-sortable#touchMove touchend->dashboard-sortable#touchEnd + touchcancel->dashboard-sortable#touchEnd keydown->dashboard-sortable#handleKeyDown">