mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
Add text ticker
This commit is contained in:
@@ -4,24 +4,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import TextTicker from './TextTicker.vue'
|
||||
|
||||
function mockOverflow(
|
||||
el: HTMLElement,
|
||||
{ scrollWidth, clientWidth }: { scrollWidth: number; clientWidth: number }
|
||||
) {
|
||||
function mockScrollWidth(el: HTMLElement, scrollWidth: number) {
|
||||
Object.defineProperty(el, 'scrollWidth', {
|
||||
value: scrollWidth,
|
||||
configurable: true
|
||||
})
|
||||
Object.defineProperty(el, 'clientWidth', {
|
||||
value: clientWidth,
|
||||
configurable: true
|
||||
})
|
||||
}
|
||||
|
||||
describe('TextTicker', () => {
|
||||
let rafCallbacks: ((time: number) => void)[]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
rafCallbacks = []
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
|
||||
rafCallbacks.push(cb)
|
||||
@@ -31,6 +25,7 @@ describe('TextTicker', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
@@ -41,27 +36,50 @@ describe('TextTicker', () => {
|
||||
expect(wrapper.text()).toBe('Hello World')
|
||||
})
|
||||
|
||||
it('scrolls on hover when content overflows', async () => {
|
||||
it('scrolls on hover after delay', async () => {
|
||||
const wrapper = mount(TextTicker, {
|
||||
slots: { default: 'Very long text that overflows' },
|
||||
props: { speed: 100 }
|
||||
})
|
||||
|
||||
const el = wrapper.element as HTMLElement
|
||||
mockOverflow(el, { scrollWidth: 300, clientWidth: 100 })
|
||||
mockScrollWidth(el, 300)
|
||||
|
||||
// Allow useElementHover to set up listeners after mount
|
||||
await nextTick()
|
||||
await wrapper.trigger('mouseenter')
|
||||
await nextTick()
|
||||
|
||||
expect(rafCallbacks.length).toBe(0)
|
||||
|
||||
vi.advanceTimersByTime(350)
|
||||
await nextTick()
|
||||
expect(rafCallbacks.length).toBeGreaterThan(0)
|
||||
|
||||
// Simulate animation frame - should set scrollLeft
|
||||
rafCallbacks[0](performance.now() + 500)
|
||||
expect(el.scrollLeft).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('cancels delayed scroll on mouse leave before delay elapses', async () => {
|
||||
const wrapper = mount(TextTicker, {
|
||||
slots: { default: 'Very long text that overflows' },
|
||||
props: { speed: 100 }
|
||||
})
|
||||
|
||||
mockScrollWidth(wrapper.element as HTMLElement, 300)
|
||||
|
||||
await nextTick()
|
||||
await wrapper.trigger('mouseenter')
|
||||
await nextTick()
|
||||
|
||||
vi.advanceTimersByTime(200)
|
||||
await wrapper.trigger('mouseleave')
|
||||
await nextTick()
|
||||
|
||||
vi.advanceTimersByTime(350)
|
||||
await nextTick()
|
||||
expect(rafCallbacks.length).toBe(0)
|
||||
})
|
||||
|
||||
it('resets scroll position on mouse leave', async () => {
|
||||
const wrapper = mount(TextTicker, {
|
||||
slots: { default: 'Very long text that overflows' },
|
||||
@@ -69,11 +87,13 @@ describe('TextTicker', () => {
|
||||
})
|
||||
|
||||
const el = wrapper.element as HTMLElement
|
||||
mockOverflow(el, { scrollWidth: 300, clientWidth: 100 })
|
||||
mockScrollWidth(el, 300)
|
||||
|
||||
await nextTick()
|
||||
await wrapper.trigger('mouseenter')
|
||||
await nextTick()
|
||||
vi.advanceTimersByTime(350)
|
||||
await nextTick()
|
||||
|
||||
rafCallbacks[0](performance.now() + 500)
|
||||
expect(el.scrollLeft).toBeGreaterThan(0)
|
||||
@@ -89,12 +109,11 @@ describe('TextTicker', () => {
|
||||
slots: { default: 'Short' }
|
||||
})
|
||||
|
||||
const el = wrapper.element as HTMLElement
|
||||
mockOverflow(el, { scrollWidth: 50, clientWidth: 100 })
|
||||
|
||||
await nextTick()
|
||||
await wrapper.trigger('mouseenter')
|
||||
await nextTick()
|
||||
vi.advanceTimersByTime(350)
|
||||
await nextTick()
|
||||
|
||||
expect(rafCallbacks.length).toBe(0)
|
||||
})
|
||||
|
||||
@@ -10,50 +10,52 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementHover } from '@vueuse/core'
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useElementHover, useElementSize, useRafFn } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { speed = 80 } = defineProps<{
|
||||
const { speed = 70 } = defineProps<{
|
||||
/** Scroll speed in pixels per second */
|
||||
speed?: number
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const isHovered = useElementHover(containerRef)
|
||||
const isHovered = useElementHover(containerRef, { delayEnter: 350 })
|
||||
const { width: containerWidth } = useElementSize(containerRef)
|
||||
const isScrolling = ref(false)
|
||||
let animationId: number | null = null
|
||||
let scrollStartTime = 0
|
||||
let overflowAmount = 0
|
||||
|
||||
const { pause, resume } = useRafFn(
|
||||
({ timestamp }) => {
|
||||
const el = containerRef.value
|
||||
if (!el || overflowAmount <= 0) return pause()
|
||||
|
||||
const elapsed = timestamp - scrollStartTime
|
||||
const duration = (overflowAmount / speed) * 1000
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
el.scrollLeft = overflowAmount * progress
|
||||
|
||||
if (progress >= 1) pause()
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
function startScroll() {
|
||||
const el = containerRef.value
|
||||
if (!el) return
|
||||
|
||||
const overflow = el.scrollWidth - el.clientWidth
|
||||
if (overflow <= 0) return
|
||||
overflowAmount = el.scrollWidth - containerWidth.value
|
||||
if (overflowAmount <= 0) return
|
||||
|
||||
isScrolling.value = true
|
||||
const duration = (overflow / speed) * 1000
|
||||
const startTime = performance.now()
|
||||
|
||||
function animate(currentTime: number) {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
el!.scrollLeft = overflow * progress
|
||||
|
||||
if (progress < 1) {
|
||||
animationId = requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(animate)
|
||||
scrollStartTime = performance.now()
|
||||
resume()
|
||||
}
|
||||
|
||||
function stopScroll() {
|
||||
if (animationId !== null) {
|
||||
cancelAnimationFrame(animationId)
|
||||
animationId = null
|
||||
}
|
||||
pause()
|
||||
if (containerRef.value) {
|
||||
containerRef.value.scrollLeft = 0
|
||||
}
|
||||
@@ -64,6 +66,4 @@ watch(isHovered, (hovered) => {
|
||||
if (hovered) startScroll()
|
||||
else stopScroll()
|
||||
})
|
||||
|
||||
onBeforeUnmount(stopScroll)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user