mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-22 08:13:52 +02:00
fix @tanstack/virtual not fully support smooth scroll behavior on dynamic height containers
This commit is contained in:
@@ -5,12 +5,15 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
// scroll DOM ref
|
||||||
target: { type: [String, Object], default: null },
|
target: { type: [String, Object], default: null },
|
||||||
|
// @tanstack/virtual instance
|
||||||
|
virtualizer: { type: [Object], default: null },
|
||||||
|
|
||||||
bottom: { type: Number, default: 20 },
|
bottom: { type: Number, default: 20 },
|
||||||
right: { type: Number, default: 20 },
|
right: { type: Number, default: 20 },
|
||||||
|
|
||||||
visibilityHeight: { type: Number, default: 200 },
|
visibilityHeight: { type: Number, default: 400 },
|
||||||
|
|
||||||
behavior: {
|
behavior: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -21,28 +24,26 @@
|
|||||||
tooltip: { type: Boolean, default: true },
|
tooltip: { type: Boolean, default: true },
|
||||||
tooltipText: { type: String, default: 'Back to top' },
|
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);
|
const visible = ref(false);
|
||||||
let containerEl = null;
|
let containerEl = null;
|
||||||
|
|
||||||
function resolveTarget() {
|
function resolveElement(target) {
|
||||||
if (!props.target) return null;
|
if (!target) return null;
|
||||||
if (typeof props.target === 'string') {
|
if (typeof target === 'string') return document.querySelector(target);
|
||||||
return document.querySelector(props.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') {
|
function getVirtualizer() {
|
||||||
if ('value' in props.target) {
|
if (!props.virtualizer) return null;
|
||||||
return props.target.value;
|
return 'value' in props.virtualizer ? props.virtualizer.value : props.virtualizer;
|
||||||
}
|
|
||||||
if ('$el' in props.target) {
|
|
||||||
return props.target.$el;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.target;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScrollTop() {
|
function getScrollTop() {
|
||||||
@@ -58,15 +59,21 @@
|
|||||||
|
|
||||||
function scrollToTop() {
|
function scrollToTop() {
|
||||||
const behavior = props.behavior === 'auto' ? 'auto' : 'smooth';
|
const behavior = props.behavior === 'auto' ? 'auto' : 'smooth';
|
||||||
if (!containerEl || typeof containerEl.scrollTo !== 'function') {
|
const v = getVirtualizer();
|
||||||
window.scrollTo({ top: 0, behavior });
|
if (v?.scrollToIndex) {
|
||||||
|
v.scrollToIndex(0, { align: 'start', behavior: 'auto' });
|
||||||
return;
|
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() {
|
function bind() {
|
||||||
containerEl = resolveTarget();
|
containerEl = resolveElement(props.target);
|
||||||
|
|
||||||
const target = containerEl && typeof containerEl.addEventListener === 'function' ? containerEl : window;
|
const target = containerEl && typeof containerEl.addEventListener === 'function' ? containerEl : window;
|
||||||
target.addEventListener('scroll', handleScroll, { passive: true });
|
target.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
@@ -94,13 +101,25 @@
|
|||||||
unbind();
|
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(
|
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;`
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport v-if="teleport" to="body">
|
<Teleport v-if="teleportTarget" :to="teleportTarget">
|
||||||
<Transition name="back-to-top">
|
<Transition name="back-to-top">
|
||||||
<div v-if="visible" :style="wrapperStyle">
|
<div v-if="visible" :style="wrapperStyle">
|
||||||
<TooltipProvider v-if="tooltip">
|
<TooltipProvider v-if="tooltip">
|
||||||
@@ -115,7 +134,7 @@
|
|||||||
<ArrowUp class="h-4 w-4" />
|
<ArrowUp class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="left">
|
<TooltipContent side="top">
|
||||||
{{ tooltipText }}
|
{{ tooltipText }}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -136,7 +155,26 @@
|
|||||||
|
|
||||||
<Transition v-else name="back-to-top">
|
<Transition v-else name="back-to-top">
|
||||||
<div v-if="visible" :style="wrapperStyle">
|
<div v-if="visible" :style="wrapperStyle">
|
||||||
|
<TooltipProvider v-if="tooltip">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
class="rounded-full shadow"
|
||||||
|
aria-label="Back to top"
|
||||||
|
@click="scrollToTop">
|
||||||
|
<ArrowUp class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
{{ tooltipText }}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
v-else
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="rounded-full shadow"
|
class="rounded-full shadow"
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
||||||
import { ArrowUp } from 'lucide-vue-next';
|
|
||||||
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: 4 },
|
|
||||||
right: { type: Number, default: 10 },
|
|
||||||
|
|
||||||
visibilityHeight: { type: Number, default: 500 },
|
|
||||||
|
|
||||||
behavior: {
|
|
||||||
type: String,
|
|
||||||
default: 'smooth',
|
|
||||||
validator: (value) => value === 'auto' || value === 'smooth'
|
|
||||||
},
|
|
||||||
|
|
||||||
teleportTo: { type: [String, Object], default: null }
|
|
||||||
});
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
let containerEl = null;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVirtualizer() {
|
|
||||||
if (!props.virtualizer) return null;
|
|
||||||
return 'value' in props.virtualizer ? props.virtualizer.value : props.virtualizer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScrollTop() {
|
|
||||||
if (!containerEl || typeof containerEl.scrollTop !== 'number') {
|
|
||||||
return window.scrollY || document.documentElement.scrollTop || 0;
|
|
||||||
}
|
|
||||||
return containerEl.scrollTop || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleScroll() {
|
|
||||||
visible.value = getScrollTop() >= props.visibilityHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollToTop() {
|
|
||||||
const behavior = props.behavior === 'auto' ? 'auto' : 'smooth';
|
|
||||||
const v = getVirtualizer();
|
|
||||||
if (v?.scrollToIndex) {
|
|
||||||
v.scrollToIndex(0, { align: 'start', behavior });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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 = resolveElement(props.target);
|
|
||||||
const target = containerEl && typeof containerEl.addEventListener === 'function' ? containerEl : window;
|
|
||||||
target.addEventListener('scroll', handleScroll, { passive: true });
|
|
||||||
handleScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function unbind() {
|
|
||||||
const target = containerEl || window;
|
|
||||||
target.removeEventListener('scroll', handleScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
bind();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.target,
|
|
||||||
() => {
|
|
||||||
unbind();
|
|
||||||
bind();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
unbind();
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapperStyle = computed(
|
|
||||||
() => `position:absolute; right:${props.right}px; bottom:${props.bottom}px; z-index:10;`
|
|
||||||
);
|
|
||||||
|
|
||||||
const teleportTarget = computed(() => resolveElement(props.teleportTo));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Teleport v-if="teleportTarget" :to="teleportTarget">
|
|
||||||
<Transition name="back-to-top">
|
|
||||||
<div v-if="visible" :style="wrapperStyle">
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="secondary"
|
|
||||||
class="rounded-full shadow"
|
|
||||||
aria-label="Back to top"
|
|
||||||
@click="scrollToTop">
|
|
||||||
<ArrowUp />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
|
|
||||||
<Transition v-else name="back-to-top">
|
|
||||||
<div v-if="visible" :style="wrapperStyle">
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="secondary"
|
|
||||||
class="rounded-full shadow"
|
|
||||||
aria-label="Back to top"
|
|
||||||
@click="scrollToTop">
|
|
||||||
<ArrowUp />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.back-to-top-enter-active,
|
|
||||||
.back-to-top-leave-active {
|
|
||||||
transition:
|
|
||||||
opacity 160ms ease,
|
|
||||||
transform 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-to-top-enter-from,
|
|
||||||
.back-to-top-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-to-top-enter-to,
|
|
||||||
.back-to-top-leave-from {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.back-to-top-enter-active,
|
|
||||||
.back-to-top-leave-active {
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BackToTopVirtual :virtualizer="virtualizer" :target="scrollViewportRef" :teleport-to="scrollRootRef" />
|
<BackToTop :virtualizer="virtualizer" :target="scrollViewportRef" :tooltip="false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
import { isRealInstance, userImage, userStatusClass } from '../../../shared/utils';
|
import { isRealInstance, userImage, userStatusClass } from '../../../shared/utils';
|
||||||
import { getFriendsLocations } from '../../../shared/utils/location.js';
|
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 FriendItem from './FriendItem.vue';
|
||||||
import Location from '../../../components/Location.vue';
|
import Location from '../../../components/Location.vue';
|
||||||
import configRepository from '../../../service/config';
|
import configRepository from '../../../service/config';
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BackToTopVirtual :virtualizer="virtualizer" :target="scrollViewportRef" :teleport-to="scrollRootRef" />
|
<BackToTop :virtualizer="virtualizer" :target="scrollViewportRef" :tooltip="false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
import { useAppearanceSettingsStore, useGroupStore } from '../../../stores';
|
import { useAppearanceSettingsStore, useGroupStore } from '../../../stores';
|
||||||
import { convertFileUrlToImageUrl } from '../../../shared/utils';
|
import { convertFileUrlToImageUrl } from '../../../shared/utils';
|
||||||
|
|
||||||
import BackToTopVirtual from '../../../components/BackToTopVirtual.vue';
|
import BackToTop from '../../../components/BackToTop.vue';
|
||||||
import Location from '../../../components/Location.vue';
|
import Location from '../../../components/Location.vue';
|
||||||
|
|
||||||
const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore());
|
const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore());
|
||||||
|
|||||||
Reference in New Issue
Block a user