Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion components/pages/home/FeaturedTags.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
}>()

const { isPremium } = useUserData()
const featuredTagsRef = ref<HTMLOListElement | null>(null)

useDesktopHorizontalScroll(featuredTagsRef)

const tagsKey = 'preselectedTags:' + `${props.domain}:` + props.tags.map((tag) => tag.media.length).join('-')

Expand All @@ -32,7 +35,10 @@
</script>

<template>
<ol class="scrollbar-hide grid grid-flow-col gap-4 overflow-x-auto">
<ol
ref="featuredTagsRef"
class="scrollbar-hide grid grid-flow-col gap-4 overflow-x-auto"
>
<template
v-for="(tag, index) in preselectedTags"
:key="tag.name"
Expand Down
8 changes: 7 additions & 1 deletion components/pages/posts/navigation/search/SearchMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
}>()

const isTagCollectionsActive = ref(false)
const filtersRowRef = ref<HTMLElement | null>(null)

useDesktopHorizontalScroll(filtersRowRef)

/**
* Info: ShallowRef will only update when the entire value changes
Expand Down Expand Up @@ -260,7 +263,10 @@
</HeadlessCombobox>

<!-- Filters -->
<section class="scrollbar-hide -mx-5 mt-8 flex gap-4 overflow-x-auto py-1 pr-3 before:w-1 after:w-1">
<section
ref="filtersRowRef"
class="scrollbar-hide -mx-5 mt-8 flex gap-4 overflow-x-auto py-1 pr-3 before:w-1 after:w-1"
>
<!-- -->

<!-- Tag Collections Toggler -->
Expand Down
127 changes: 127 additions & 0 deletions composables/useDesktopHorizontalScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useEventListener } from '@vueuse/core'
import type { Ref } from 'vue'

const DRAG_THRESHOLD = 4

export function useDesktopHorizontalScroll<T extends HTMLElement>(scrollContainerRef: Ref<T | null>) {
let activePointerId: number | null = null
let dragStartX = 0
let dragStartScrollLeft = 0
let didDrag = false
let shouldPreventClick = false

function onWheel(event: WheelEvent) {
const scrollContainer = scrollContainerRef.value

if (!scrollContainer) {
return
}

// Preserve touchpad/native horizontal behavior and only map normal mouse wheel.
if (event.deltaX !== 0 || event.deltaY === 0 || !isLikelyMouseWheel(event)) {
return
}

const maxScrollLeft = scrollContainer.scrollWidth - scrollContainer.clientWidth

if (maxScrollLeft <= 0) {
return
}

const canScrollLeft = scrollContainer.scrollLeft > 0
const canScrollRight = scrollContainer.scrollLeft < maxScrollLeft

if ((event.deltaY < 0 && !canScrollLeft) || (event.deltaY > 0 && !canScrollRight)) {
return
}

event.preventDefault()

scrollContainer.scrollLeft += event.deltaY
}
Comment on lines +20 to +41
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In browser wheel event handling, does preventDefault() on a wheel event with ctrlKey=true suppress page zoom behavior?

💡 Result:

Yes—when the wheel event is cancelable and your listener is non-passive, calling event.preventDefault() on a wheel event with ctrlKey === true suppresses the browser’s default page-zoom (Ctrl+wheel / trackpad-pinch-zoom) behavior. MDN notes that zoom gestures fire wheel events with ctrlKey set, and that canceling the event prevents “scrolling or zooming.” [1]

Practical requirements / caveats:

  • Register the listener with { passive: false }, otherwise the browser may ignore preventDefault() for performance reasons. [1]
  • Some browsers may make only the first event in a wheel sequence cancelable (later events can be non-cancelable), and non-cancelable events can’t be prevented. [1], [2]
  • Chromium explicitly changed behavior so Ctrl+wheel goes to the page first and only triggers browser zoom if unhandled (i.e., not prevented). [3]

Sources: [1] MDN wheel event docs, [2] W3C Wheel Events spec, [3] Chromium blink-dev “ctrl+mousewheel events can now be overridden”.


🏁 Script executed:

fd -t f "useDesktopHorizontalScroll.ts"

Repository: Rule-34/App

Length of output: 96


🏁 Script executed:

cat -n composables/useDesktopHorizontalScroll.ts

Repository: Rule-34/App

Length of output: 4317


Add early return for zoom gestures (Ctrl+wheel) in wheel handler.

The wheel event listener at line 113 is registered with { passive: false }, which means event.preventDefault() at line 38 will effectively suppress browser default behavior. Without checking event.ctrlKey first, this prevents page zoom when using Ctrl+wheel. Add a check for zoom gestures to the early return condition:

Proposed patch
   // Preserve touchpad/native horizontal behavior and only map normal mouse wheel.
-  if (event.deltaX !== 0 || event.deltaY === 0 || !isLikelyMouseWheel(event)) {
+  if (event.ctrlKey || event.deltaX !== 0 || event.deltaY === 0 || !isLikelyMouseWheel(event)) {
     return
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Preserve touchpad/native horizontal behavior and only map normal mouse wheel.
if (event.deltaX !== 0 || event.deltaY === 0 || !isLikelyMouseWheel(event)) {
return
}
const maxScrollLeft = scrollContainer.scrollWidth - scrollContainer.clientWidth
if (maxScrollLeft <= 0) {
return
}
const canScrollLeft = scrollContainer.scrollLeft > 0
const canScrollRight = scrollContainer.scrollLeft < maxScrollLeft
if ((event.deltaY < 0 && !canScrollLeft) || (event.deltaY > 0 && !canScrollRight)) {
return
}
event.preventDefault()
scrollContainer.scrollLeft += event.deltaY
}
// Preserve touchpad/native horizontal behavior and only map normal mouse wheel.
if (event.ctrlKey || event.deltaX !== 0 || event.deltaY === 0 || !isLikelyMouseWheel(event)) {
return
}
const maxScrollLeft = scrollContainer.scrollWidth - scrollContainer.clientWidth
if (maxScrollLeft <= 0) {
return
}
const canScrollLeft = scrollContainer.scrollLeft > 0
const canScrollRight = scrollContainer.scrollLeft < maxScrollLeft
if ((event.deltaY < 0 && !canScrollLeft) || (event.deltaY > 0 && !canScrollRight)) {
return
}
event.preventDefault()
scrollContainer.scrollLeft += event.deltaY
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useDesktopHorizontalScroll.ts` around lines 20 - 41, Update the
wheel handler in useDesktopHorizontalScroll (the function that checks
isLikelyMouseWheel and currently does event.preventDefault() before horizontal
scrolling) to early-return when the wheel is being used as a zoom gesture by
checking event.ctrlKey (and optionally event.metaKey for macOS); add this check
to the existing early-return condition(s) so zoom (Ctrl+wheel) is not blocked
before calling event.preventDefault() and the horizontal scroll logic.


function onPointerDown(event: PointerEvent) {
const scrollContainer = scrollContainerRef.value

if (!scrollContainer || event.pointerType !== 'mouse' || event.button !== 0) {
return
}

const maxScrollLeft = scrollContainer.scrollWidth - scrollContainer.clientWidth

if (maxScrollLeft <= 0) {
return
}

activePointerId = event.pointerId
dragStartX = event.clientX
dragStartScrollLeft = scrollContainer.scrollLeft
didDrag = false

scrollContainer.setPointerCapture(event.pointerId)
}

function onPointerMove(event: PointerEvent) {
const scrollContainer = scrollContainerRef.value

if (!scrollContainer || activePointerId !== event.pointerId) {
return
}

const dragDistance = event.clientX - dragStartX

if (!didDrag && Math.abs(dragDistance) >= DRAG_THRESHOLD) {
didDrag = true
}

if (!didDrag) {
return
}

event.preventDefault()

scrollContainer.scrollLeft = dragStartScrollLeft - dragDistance
}

function endDrag(event: PointerEvent) {
const scrollContainer = scrollContainerRef.value

if (!scrollContainer || activePointerId !== event.pointerId) {
return
}

if (scrollContainer.hasPointerCapture(event.pointerId)) {
scrollContainer.releasePointerCapture(event.pointerId)
}

shouldPreventClick = didDrag
didDrag = false
activePointerId = null
}

function onClickCapture(event: MouseEvent) {
if (!shouldPreventClick) {
return
}

event.preventDefault()
event.stopPropagation()

shouldPreventClick = false
}

useEventListener(scrollContainerRef, 'wheel', onWheel, { passive: false })
useEventListener(scrollContainerRef, 'pointerdown', onPointerDown)
useEventListener(scrollContainerRef, 'pointermove', onPointerMove)
useEventListener(scrollContainerRef, 'pointerup', endDrag)
useEventListener(scrollContainerRef, 'pointercancel', endDrag)
useEventListener(scrollContainerRef, 'click', onClickCapture, { capture: true })
}

function isLikelyMouseWheel(event: WheelEvent) {
if (event.deltaMode !== WheelEvent.DOM_DELTA_PIXEL) {
return true
}

return Number.isInteger(event.deltaY) && Math.abs(event.deltaY) >= 40
}