Skip to content

Commit 8eab73f

Browse files
committed
refactor: simplify scroll-to-top visibility and interaction cleanup
1 parent 7eca5d0 commit 8eab73f

File tree

2 files changed

+29
-38
lines changed

2 files changed

+29
-38
lines changed

app/components/ScrollToTop.client.vue

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,23 @@ const SCROLL_TO_TOP_DURATION = 500
1010
1111
const isMounted = useMounted()
1212
const isTouchDeviceClient = shallowRef(false)
13-
const isVisible = shallowRef(false)
1413
const scrollThreshold = 300
14+
const { y: scrollTop } = useScroll(window)
15+
const isVisible = computed(() => {
16+
if (supportsScrollStateQueries.value) return false
17+
return scrollTop.value > scrollThreshold
18+
})
1519
const { isSupported: supportsScrollStateQueries } = useCssSupports(
1620
'container-type',
1721
'scroll-state',
1822
{ ssrValue: false },
1923
)
2024
const shouldShowButton = computed(() => isActive.value && isTouchDeviceClient.value)
2125
22-
function onScroll() {
23-
if (supportsScrollStateQueries.value) {
24-
return
25-
}
26-
isVisible.value = window.scrollY > scrollThreshold
27-
}
28-
2926
const { scrollToTop } = useScrollToTop({ duration: SCROLL_TO_TOP_DURATION })
3027
31-
useEventListener('scroll', onScroll, { passive: true })
32-
3328
onMounted(() => {
3429
isTouchDeviceClient.value = isTouchDevice()
35-
onScroll()
3630
})
3731
</script>
3832

@@ -43,7 +37,7 @@ onMounted(() => {
4337
type="button"
4438
class="scroll-to-top-css fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
4539
:aria-label="$t('common.scroll_to_top')"
46-
@click="scrollToTop()"
40+
@click="() => scrollToTop()"
4741
>
4842
<span class="i-lucide:arrow-up w-5 h-5" aria-hidden="true" />
4943
</button>
@@ -63,7 +57,7 @@ onMounted(() => {
6357
type="button"
6458
class="fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
6559
:aria-label="$t('common.scroll_to_top')"
66-
@click="scrollToTop()"
60+
@click="() => scrollToTop()"
6761
>
6862
<span class="i-lucide:arrow-up w-5 h-5" aria-hidden="true" />
6963
</button>

app/composables/useScrollToTop.ts

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ interface UseScrollToTopOptions {
55
duration?: number
66
}
77

8+
// Easing function for the scroll animation
9+
const easeOutQuad = (t: number) => t * (2 - t)
10+
811
/**
912
* Scroll to the top of the page with a smooth animation.
1013
* @param options - Configuration options for the scroll animation.
@@ -16,24 +19,11 @@ export function useScrollToTop(options: UseScrollToTopOptions) {
1619
// Check if prefers-reduced-motion is enabled
1720
const preferReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')
1821

19-
// Easing function for the scroll animation
20-
const easeOutQuad = (t: number) => t * (2 - t)
21-
2222
/**
2323
* Active requestAnimationFrame id for the current auto-scroll animation
2424
*/
2525
let rafId: number | null = null
26-
/**
27-
* Disposer for temporary interaction listeners attached during auto-scroll
28-
*/
29-
let stopInteractionListeners: (() => void) | null = null
30-
31-
function cleanupInteractionListeners() {
32-
if (stopInteractionListeners) {
33-
stopInteractionListeners()
34-
stopInteractionListeners = null
35-
}
36-
}
26+
const isScrolling = ref(false)
3727

3828
/**
3929
* Stop any in-flight auto-scroll before starting a new one.
@@ -43,8 +33,21 @@ export function useScrollToTop(options: UseScrollToTopOptions) {
4333
cancelAnimationFrame(rafId)
4434
rafId = null
4535
}
36+
isScrolling.value = false
37+
}
38+
39+
// Cancel scroll on user interaction
40+
const onInteraction = () => {
41+
if (isScrolling.value) {
42+
cancel()
43+
}
44+
}
4645

47-
cleanupInteractionListeners()
46+
if (import.meta.client) {
47+
const listenerOptions = { passive: true }
48+
useEventListener(window, 'wheel', onInteraction, listenerOptions)
49+
useEventListener(window, 'touchstart', onInteraction, listenerOptions)
50+
useEventListener(window, 'mousedown', onInteraction, listenerOptions)
4851
}
4952

5053
function scrollToTop() {
@@ -58,17 +61,11 @@ export function useScrollToTop(options: UseScrollToTopOptions) {
5861
const start = window.scrollY
5962
if (start <= 0) return
6063

64+
isScrolling.value = true
65+
6166
const startTime = performance.now()
6267
const change = -start
6368

64-
const cleanup = [
65-
useEventListener(window, 'wheel', cancel, { passive: true }),
66-
useEventListener(window, 'touchstart', cancel, { passive: true }),
67-
useEventListener(window, 'mousedown', cancel, { passive: true }),
68-
]
69-
70-
stopInteractionListeners = () => cleanup.forEach(stop => stop())
71-
7269
// Start the frame-by-frame scroll animation.
7370
function animate() {
7471
const elapsed = performance.now() - startTime
@@ -77,7 +74,7 @@ export function useScrollToTop(options: UseScrollToTopOptions) {
7774

7875
window.scrollTo({ top: y })
7976

80-
if (t < 1) {
77+
if (t < 1 && isScrolling.value) {
8178
rafId = requestAnimationFrame(animate)
8279
} else {
8380
cancel()
@@ -87,7 +84,7 @@ export function useScrollToTop(options: UseScrollToTopOptions) {
8784
rafId = requestAnimationFrame(animate)
8885
}
8986

90-
onBeforeUnmount(cancel)
87+
tryOnScopeDispose(cancel)
9188

9289
return {
9390
scrollToTop,

0 commit comments

Comments
 (0)