Add text ticker overflow description

This commit is contained in:
pythongosssss
2026-02-13 09:14:08 -08:00
parent f88a42619e
commit 663efeff35
3 changed files with 174 additions and 3 deletions

View File

@@ -0,0 +1,101 @@
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import TextTicker from './TextTicker.vue'
function mockOverflow(
el: HTMLElement,
{ scrollWidth, clientWidth }: { scrollWidth: number; clientWidth: 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(() => {
rafCallbacks = []
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
rafCallbacks.push(cb)
return rafCallbacks.length
})
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('renders slot content', () => {
const wrapper = mount(TextTicker, {
slots: { default: 'Hello World' }
})
expect(wrapper.text()).toBe('Hello World')
})
it('scrolls on hover when content overflows', 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 })
// Allow useElementHover to set up listeners after mount
await nextTick()
await wrapper.trigger('mouseenter')
await nextTick()
expect(rafCallbacks.length).toBeGreaterThan(0)
// Simulate animation frame - should set scrollLeft
rafCallbacks[0](performance.now() + 500)
expect(el.scrollLeft).toBeGreaterThan(0)
})
it('resets scroll position on mouse leave', 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 })
await nextTick()
await wrapper.trigger('mouseenter')
await nextTick()
rafCallbacks[0](performance.now() + 500)
expect(el.scrollLeft).toBeGreaterThan(0)
await wrapper.trigger('mouseleave')
await nextTick()
expect(el.scrollLeft).toBe(0)
})
it('does not scroll when content fits', async () => {
const wrapper = mount(TextTicker, {
slots: { default: 'Short' }
})
const el = wrapper.element as HTMLElement
mockOverflow(el, { scrollWidth: 50, clientWidth: 100 })
await nextTick()
await wrapper.trigger('mouseenter')
await nextTick()
expect(rafCallbacks.length).toBe(0)
})
})

View File

@@ -0,0 +1,69 @@
<template>
<div
ref="containerRef"
:class="
cn('overflow-hidden whitespace-nowrap', !isScrolling && 'text-ellipsis')
"
>
<slot />
</div>
</template>
<script setup lang="ts">
import { useElementHover } from '@vueuse/core'
import { onBeforeUnmount, ref, watch } from 'vue'
import { cn } from '@/utils/tailwindUtil'
const { speed = 80 } = defineProps<{
/** Scroll speed in pixels per second */
speed?: number
}>()
const containerRef = ref<HTMLElement | null>(null)
const isHovered = useElementHover(containerRef)
const isScrolling = ref(false)
let animationId: number | null = null
function startScroll() {
const el = containerRef.value
if (!el) return
const overflow = el.scrollWidth - el.clientWidth
if (overflow <= 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)
}
function stopScroll() {
if (animationId !== null) {
cancelAnimationFrame(animationId)
animationId = null
}
if (containerRef.value) {
containerRef.value.scrollLeft = 0
}
isScrolling.value = false
}
watch(isHovered, (hovered) => {
if (hovered) startScroll()
else stopScroll()
})
onBeforeUnmount(stopScroll)
</script>

View File

@@ -2,7 +2,7 @@
<div
class="option-container flex w-full cursor-pointer items-center justify-between overflow-hidden"
>
<div class="flex flex-col gap-0.5">
<div class="flex flex-col gap-0.5 overflow-hidden">
<div class="font-semibold text-foreground flex items-center gap-2">
<span v-if="isBookmarked && !hideBookmarkIcon">
<i class="pi pi-bookmark-fill mr-1 text-sm" />
@@ -32,9 +32,9 @@
>
{{ nodeDef.nodeSource.displayText }}
</span>
<span v-if="nodeDef.description" class="truncate">
<TextTicker v-if="nodeDef.description">
{{ nodeDef.description }}
</span>
</TextTicker>
</div>
<div
v-else-if="showCategory"
@@ -81,6 +81,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import TextTicker from '@/components/common/TextTicker.vue'
import NodePricingBadge from '@/components/node/NodePricingBadge.vue'
import NodeProviderBadge from '@/components/node/NodeProviderBadge.vue'
import { useSettingStore } from '@/platform/settings/settingStore'