From 3b2bcca639bc8b2411fe6478e9a79b336e226e1c Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Sun, 24 May 2026 13:28:58 -0500 Subject: [PATCH] fix: Prevent swiping AND scrolling on shopping list (#7659) --- .../Domain/ShoppingList/ShoppingListItem.vue | 107 ++++++++++++------ 1 file changed, 75 insertions(+), 32 deletions(-) diff --git a/frontend/app/components/Domain/ShoppingList/ShoppingListItem.vue b/frontend/app/components/Domain/ShoppingList/ShoppingListItem.vue index 2ecefd99d..e29c17e9b 100644 --- a/frontend/app/components/Domain/ShoppingList/ShoppingListItem.vue +++ b/frontend/app/components/Domain/ShoppingList/ShoppingListItem.vue @@ -10,24 +10,9 @@ }" > @@ -214,9 +199,23 @@ const emit = defineEmits<{ }>(); const SWIPE_THRESHOLD = 50; -const SCROLL_THRESHOLD = 50; const { isRtl } = useRtl(); +const swipeRowRef = ref | null>(null); + +onMounted(() => { + const el = swipeRowRef.value?.$el as HTMLElement | undefined; + if (!el) return; + el.addEventListener( + "touchmove", + (e: TouchEvent) => { + if (swipeInfo.value.gesture === "swipe") { + e.preventDefault(); + } + }, + { passive: false }, + ); +}); const i18n = useI18n(); const displayRecipeRefs = ref(false); const itemLabelCols = computed(() => (model.value?.checked ? "auto" : "6")); @@ -267,22 +266,66 @@ function save() { edit.value = false; } -const swipeInfo: Ref<{ touchstartX?: number; touchendX?: number; touchstartY?: number; touchendY?: number }> = ref({}); -const swiping = computed(() => { - const { touchstartX, touchendX, touchstartY, touchendY } = swipeInfo.value ?? {}; - if (touchstartX === undefined || touchendX === undefined) { - return 0; - } - const deltaX = isRtl.value ? touchstartX - touchendX : touchendX - touchstartX; +type SwipeGesture = null | "scroll" | "swipe"; - // If there's significant vertical movement, treat as a scroll gesture and ignore - if (touchstartY !== undefined && touchendY !== undefined) { - const deltaY = Math.abs(touchendY - touchstartY); - if (deltaY > SCROLL_THRESHOLD) { - return 0; +const swipeInfo = ref({ + touchstartX: 0, + touchstartY: 0, + touchendX: 0, + touchendY: 0, + gesture: null as SwipeGesture, +}); + +function getSwipePoint(e: any) { + const touch = e?.touches?.[0] ?? e?.changedTouches?.[0] ?? e; + return { x: touch?.clientX ?? 0, y: touch?.clientY ?? 0 }; +} + +function resetSwipe() { + swipeInfo.value = { touchstartX: 0, touchstartY: 0, touchendX: 0, touchendY: 0, gesture: null }; +} + +function onSwipeStart(payload: any) { + const { x, y } = getSwipePoint(payload.originalEvent); + swipeInfo.value = { touchstartX: x, touchstartY: y, touchendX: x, touchendY: y, gesture: null }; +} + +function onSwipeMove(payload: any) { + const { x, y } = getSwipePoint(payload.originalEvent); + swipeInfo.value.touchendX = x; + swipeInfo.value.touchendY = y; + + if (!swipeInfo.value.gesture) { + const deltaX = Math.abs(x - swipeInfo.value.touchstartX); + const deltaY = Math.abs(y - swipeInfo.value.touchstartY); + if (deltaY > 8 && deltaY > deltaX) { + swipeInfo.value.gesture = "scroll"; + } + else if (deltaX > 8 && deltaX > deltaY) { + swipeInfo.value.gesture = "swipe"; + } + else if (deltaX > 8 || deltaY > 8) { + // Diagonal / ambiguous — default to scroll + swipeInfo.value.gesture = "scroll"; } } - return Math.min(Math.max(0, deltaX), 100); +} + +function onSwipeEnd() { + if (swipeInfo.value.gesture === "swipe" && swiping.value >= SWIPE_THRESHOLD) { + toggleChecked(); + } + resetSwipe(); +} + +const swiping = computed(() => { + if (swipeInfo.value.gesture !== "swipe") { + return 0; + } + const deltaX = isRtl.value + ? swipeInfo.value.touchstartX - swipeInfo.value.touchendX + : swipeInfo.value.touchendX - swipeInfo.value.touchstartX; + return Math.max(0, Math.min(deltaX, 100)); }); const recipeList = computed(() => {