From dbc98cdc4e15b4f5f746c867c52a0fc145d214d0 Mon Sep 17 00:00:00 2001 From: pa Date: Sun, 25 Jan 2026 20:43:04 +0900 Subject: [PATCH] fix @tanstack/virtual not fully support smooth scroll behavior on dynamic height containers --- src/components/BackToTop.vue | 84 ++++++--- src/components/BackToTopVirtual.vue | 161 ------------------ .../Sidebar/components/FriendsSidebar.vue | 4 +- .../Sidebar/components/GroupsSidebar.vue | 4 +- 4 files changed, 65 insertions(+), 188 deletions(-) delete mode 100644 src/components/BackToTopVirtual.vue diff --git a/src/components/BackToTop.vue b/src/components/BackToTop.vue index 54403c35..926821d0 100644 --- a/src/components/BackToTop.vue +++ b/src/components/BackToTop.vue @@ -5,12 +5,15 @@ import { Button } from '@/components/ui/button'; const props = defineProps({ + // scroll DOM ref target: { type: [String, Object], default: null }, + // @tanstack/virtual instance + virtualizer: { type: [Object], default: null }, bottom: { type: Number, default: 20 }, right: { type: Number, default: 20 }, - visibilityHeight: { type: Number, default: 200 }, + visibilityHeight: { type: Number, default: 400 }, behavior: { type: String, @@ -21,28 +24,26 @@ tooltip: { type: Boolean, default: true }, tooltipText: { type: String, default: 'Back to top' }, - teleport: { type: Boolean, default: true } + teleport: { type: Boolean, default: true }, + teleportTo: { type: [Boolean, String, Object], default: null } }); const visible = ref(false); let containerEl = null; - function resolveTarget() { - if (!props.target) return null; - if (typeof props.target === 'string') { - return document.querySelector(props.target); + function resolveElement(target) { + if (!target) return null; + if (typeof target === 'string') return document.querySelector(target); + if (typeof target === 'object') { + if ('value' in target) return target.value; + if ('$el' in target) return target.$el; } + return target; + } - if (typeof props.target === 'object') { - if ('value' in props.target) { - return props.target.value; - } - if ('$el' in props.target) { - return props.target.$el; - } - } - - return props.target; + function getVirtualizer() { + if (!props.virtualizer) return null; + return 'value' in props.virtualizer ? props.virtualizer.value : props.virtualizer; } function getScrollTop() { @@ -58,15 +59,21 @@ function scrollToTop() { const behavior = props.behavior === 'auto' ? 'auto' : 'smooth'; - if (!containerEl || typeof containerEl.scrollTo !== 'function') { - window.scrollTo({ top: 0, behavior }); + const v = getVirtualizer(); + if (v?.scrollToIndex) { + v.scrollToIndex(0, { align: 'start', behavior: 'auto' }); return; } - containerEl.scrollTo({ top: 0, behavior }); + const target = containerEl || resolveElement(props.target); + if (target && typeof target.scrollTo === 'function') { + target.scrollTo({ top: 0, behavior }); + return; + } + window.scrollTo({ top: 0, behavior }); } function bind() { - containerEl = resolveTarget(); + containerEl = resolveElement(props.target); const target = containerEl && typeof containerEl.addEventListener === 'function' ? containerEl : window; target.addEventListener('scroll', handleScroll, { passive: true }); @@ -94,13 +101,25 @@ unbind(); }); + const teleportTarget = computed(() => { + if (props.teleportTo !== null && props.teleportTo !== undefined) { + if (props.teleportTo === true) return 'body'; + if (props.teleportTo === false) return null; + return resolveElement(props.teleportTo); + } + return props.teleport ? 'body' : null; + }); + + const isBodyTeleport = computed(() => teleportTarget.value === 'body' || teleportTarget.value === document.body); + const wrapperStyle = computed( - () => `position:fixed; right:${props.right}px; bottom:${props.bottom}px; z-index:50;` + () => + `position:${isBodyTeleport.value ? 'fixed' : 'absolute'}; right:${props.right}px; bottom:${props.bottom}px; z-index:50;` ); @@ -107,7 +107,7 @@ import { isRealInstance, userImage, userStatusClass } from '../../../shared/utils'; import { getFriendsLocations } from '../../../shared/utils/location.js'; - import BackToTopVirtual from '../../../components/BackToTopVirtual.vue'; + import BackToTop from '../../../components/BackToTop.vue'; import FriendItem from './FriendItem.vue'; import Location from '../../../components/Location.vue'; import configRepository from '../../../service/config'; diff --git a/src/views/Sidebar/components/GroupsSidebar.vue b/src/views/Sidebar/components/GroupsSidebar.vue index cd3818dc..357ffaa9 100644 --- a/src/views/Sidebar/components/GroupsSidebar.vue +++ b/src/views/Sidebar/components/GroupsSidebar.vue @@ -52,7 +52,7 @@ - + @@ -65,7 +65,7 @@ import { useAppearanceSettingsStore, useGroupStore } from '../../../stores'; import { convertFileUrlToImageUrl } from '../../../shared/utils'; - import BackToTopVirtual from '../../../components/BackToTopVirtual.vue'; + import BackToTop from '../../../components/BackToTop.vue'; import Location from '../../../components/Location.vue'; const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore());