1
0
mirror of https://github.com/sendevia/website.git synced 2026-03-05 23:32:45 +08:00

feat: add scroll-to-top component with improved scroll detection

This commit is contained in:
2025-10-16 23:44:38 +08:00
parent db3ac30cb2
commit b9dad47419
2 changed files with 124 additions and 43 deletions

View File

@@ -0,0 +1,122 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
const visible = ref(false);
let container: HTMLElement | Window = window;
function isScrollable(el: HTMLElement) {
const style = window.getComputedStyle(el);
const overflowY = style.overflowY;
return overflowY === "auto" || overflowY === "scroll" || el.scrollHeight > el.clientHeight;
}
function detectContainer() {
const el = document.getElementById("layout-content-flow");
if (el && isScrollable(el)) return el;
return window;
}
function onScroll() {
try {
const y = container === window ? window.scrollY || window.pageYOffset : (container as HTMLElement).scrollTop;
visible.value = y > 240;
} catch (e) {
visible.value = false;
}
}
function scrollToTop() {
if (container === window) {
window.scrollTo({ top: 0, behavior: "smooth" });
} else {
(container as HTMLElement).scrollTo({ top: 0, behavior: "smooth" });
}
}
onMounted(() => {
container = detectContainer();
const target: any = container;
target.addEventListener("scroll", onScroll, { passive: true });
if (container !== window) window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
});
onBeforeUnmount(() => {
const target: any = container;
target.removeEventListener("scroll", onScroll);
if (container !== window) window.removeEventListener("scroll", onScroll);
});
</script>
<template>
<div id="layout-scrolltop" :class="{ visible: visible }" @click="scrollToTop">
<span id="scrolltop-button" role="button" aria-label="Scroll to top"> arrow_upward </span>
</div>
</template>
<style scoped lang="scss">
@use "../styles/mixin";
#layout-scrolltop {
display: flex;
align-items: center;
grid-column: 11/13;
justify-content: center;
position: sticky;
bottom: 72px;
right: 0px;
height: 100%;
width: 100%;
-moz-user-select: none;
opacity: 0;
transition: var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect);
user-select: none;
visibility: hidden;
z-index: 21;
#scrolltop-button {
@include mixin.material-symbols($size: 24, $line-height: 84);
position: relative;
height: 84px;
width: 84px;
color: var(--md-sys-color-outline);
text-align: center;
border-radius: var(--md-sys-shape-corner-full);
border: 1px solid var(--md-sys-color-outline-variant);
background-color: var(--md-sys-color-surface-container-low);
cursor: pointer;
overflow: hidden;
transition: var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect);
&:hover {
background-color: var(--md-sys-color-surface-container-high);
}
}
&.visible {
opacity: 1;
visibility: visible;
}
}
@media screen and (max-width: 1600px) {
}
@media screen and (max-width: 1200px) {
}
@media screen and (max-width: 840px) {
}
@media screen and (max-width: 600px) {
}
</style>

View File

@@ -5,6 +5,7 @@ import NotFoundLayout from "./NotFound.vue";
import SearchPostsLayout from "./SearchPosts.vue";
import Footer from "../components/Footer.vue";
import Sidebar from "../components/Sidebar.vue";
import ScrollToTop from "../components/ScrollToTop.vue";
import { argbFromHex } from "@material/material-color-utilities";
import { generateColorPalette } from "../utils/colorPalette";
import { onMounted, nextTick, computed } from "vue";
@@ -107,6 +108,7 @@ function onAfterEnter() {
</div>
</div>
<component v-else :is="currentLayout" />
<ScrollToTop />
<Footer />
</div>
</Transition>
@@ -193,45 +195,6 @@ function onAfterEnter() {
mask: var(--via-svg-mask) no-repeat 0 / 100%;
}
}
#layout-scrolltop {
display: flex;
align-items: center;
justify-content: center;
grid-column: 11/13;
position: sticky;
bottom: 72px;
right: 0px;
height: 100%;
width: 100%;
opacity: 0;
transition: var(--md-sys-motion-duration-medium4) var(--md-sys-motion-easing-standard);
visibility: hidden;
z-index: 21;
#layout-scrolltop-desktop {
@include mixin.material-symbols($size: 24);
position: relative;
height: 84px;
min-width: 84px;
width: 84px;
color: var(--md-sys-color-outline);
border-radius: var(--md-sys-shape-corner-full);
border: 1px solid var(--md-sys-color-outline-variant);
background-color: var(--md-sys-color-surface-container-low);
cursor: pointer;
overflow: hidden;
}
}
}
@media screen and (max-width: 1600px) {
@@ -246,10 +209,6 @@ function onAfterEnter() {
grid-template-columns: minmax(50vw, 70%) minmax(300px, 30%);
}
#layout-scrolltop {
grid-column: $columns;
}
hr {
grid-column: span $columns;
}