Improve AutoScroll reliability with mouseenter/mouseleave
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 15s

Replace mouseover (which fires repeatedly on child elements) with
mouseenter/mouseleave so hover cleanly stops scrolling. On mouse leave,
sync scroll position from scrollTop so manual scrolling is respected.
Fix inverted top/bottom boundary checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 21:22:58 +00:00
parent 8406582b2b
commit 4e7377d9f0

View File

@@ -1,5 +1,5 @@
<template> <template>
<div ref="container" @mouseover="handleHover" class="overflow-y-auto"> <div ref="container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" class="overflow-y-auto">
<slot /> <slot />
</div> </div>
</template> </template>
@@ -14,8 +14,9 @@ const PAUSE = 2000; // ms at top/bottom
let pos = 0; let pos = 0;
let direction = 1; // 1 = down, -1 = up let direction = 1; // 1 = down, -1 = up
let timeoutId; let hovered = false;
let timeoutId2; let rafId = null;
let pauseTimeoutId = null;
let cachedScrollHeight = 0; let cachedScrollHeight = 0;
function measureScrollHeight() { function measureScrollHeight() {
@@ -23,34 +24,63 @@ function measureScrollHeight() {
if (el) cachedScrollHeight = el.scrollHeight; if (el) cachedScrollHeight = el.scrollHeight;
} }
function handleHover() { function stopLoop() {
cancelAnimationFrame(timeoutId); if (rafId !== null) {
clearTimeout(timeoutId2); cancelAnimationFrame(rafId);
timeoutId2 = setTimeout( rafId = null;
() => (timeoutId = requestAnimationFrame(tick)), }
PAUSE, if (pauseTimeoutId !== null) {
); clearTimeout(pauseTimeoutId);
pauseTimeoutId = null;
}
}
function startLoop() {
stopLoop();
rafId = requestAnimationFrame(tick);
}
function onMouseEnter() {
hovered = true;
stopLoop();
}
function onMouseLeave() {
hovered = false;
const el = container.value;
if (el && cachedScrollHeight > 0) {
pos = el.scrollTop / cachedScrollHeight;
}
startLoop();
}
function schedulePause(callback) {
stopLoop();
pauseTimeoutId = setTimeout(callback, PAUSE);
} }
function tick() { function tick() {
rafId = null;
const el = container.value; const el = container.value;
if (hovered) return;
if (!el || cachedScrollHeight === 0) { if (!el || cachedScrollHeight === 0) {
timeoutId = requestAnimationFrame(tick); rafId = requestAnimationFrame(tick);
return; return;
} }
const reachedBottom = pos <= 0; const reachedBottom = pos >= 1;
const reachedTop = pos >= 1; const reachedTop = pos <= 0;
if (reachedBottom) { if (reachedBottom) {
pos = 0.001;
direction = 1;
handleHover();
return;
} else if (reachedTop) {
pos = 0.999; pos = 0.999;
direction = -1; direction = -1;
handleHover(); schedulePause(startLoop);
return;
} else if (reachedTop && direction === -1) {
pos = 0.001;
direction = 1;
schedulePause(startLoop);
return; return;
} }
@@ -58,21 +88,21 @@ function tick() {
el.scrollTop = pos * cachedScrollHeight; el.scrollTop = pos * cachedScrollHeight;
timeoutId = requestAnimationFrame(tick); rafId = requestAnimationFrame(tick);
} }
let resizeObserver; let resizeObserver;
onMounted(() => { onMounted(() => {
measureScrollHeight(); measureScrollHeight();
timeoutId = requestAnimationFrame(tick); schedulePause(startLoop);
resizeObserver = new ResizeObserver(measureScrollHeight); resizeObserver = new ResizeObserver(measureScrollHeight);
resizeObserver.observe(container.value); resizeObserver.observe(container.value);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
cancelAnimationFrame(timeoutId); stopLoop();
resizeObserver?.disconnect(); resizeObserver?.disconnect();
}); });
</script> </script>