mirror of
https://github.com/ChenQihan666/MyBlog-Next.git
synced 2026-08-14 11:29:33 +08:00
feat: improve back-to-top button behavior (#520)
Make the back-to-top button appear based on scroll position_ improving accessibility and UX.
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
|
||||||
|
|
||||||
|
/*
|
||||||
|
In #btt-btn-container, left position is `left-[calc((var(--container-3xl)/2)+50%+1rem)]`.
|
||||||
|
This is because we use `max-w-3xl` in `PostDetails.astro`.
|
||||||
|
If `max-width` in `PostDetails.astro` get updated, make sure to update the `var(--container-3xl)` as well.
|
||||||
|
max-w-3xl => var(--container-3xl)
|
||||||
|
max-w-4xl => var(--container-4xl)
|
||||||
|
max-w-5xl => var(--container-5xl)
|
||||||
|
etc...
|
||||||
|
*/
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="btt-btn-container"
|
||||||
|
class:list={[
|
||||||
|
"fixed right-4 bottom-8 xl:right-auto xl:left-[calc((var(--container-3xl)/2)+50%+1rem)]",
|
||||||
|
"translate-y-14 opacity-0 transition-all duration-500",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-button="back-to-top"
|
||||||
|
class:list={[
|
||||||
|
"group relative",
|
||||||
|
"size-14 rounded-full px-2 py-1 shadow-xl lg:h-8 lg:w-fit lg:rounded-md lg:shadow-none",
|
||||||
|
"bg-background lg:bg-transparent",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
id="progress-indicator"
|
||||||
|
class="absolute inset-0 -z-10 block size-14 scale-110 rounded-full bg-transparent lg:hidden lg:h-8 lg:rounded-md"
|
||||||
|
></span>
|
||||||
|
<IconChevronLeft class="inline-block rotate-90 lg:hidden" />
|
||||||
|
<span
|
||||||
|
class="sr-only underline decoration-dashed decoration-2 underline-offset-4 group-hover:text-accent lg:not-sr-only"
|
||||||
|
>
|
||||||
|
<span class="text-xl" tabindex="0">↑</span> Back To Top
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script is:inline data-astro-rerun>
|
||||||
|
/** Scrolls the document to the top when
|
||||||
|
* the "Back to Top" button is clicked. */
|
||||||
|
function backToTop() {
|
||||||
|
const rootElement = document.documentElement;
|
||||||
|
const btnContainer = document.querySelector("#btt-btn-container");
|
||||||
|
const backToTopBtn = document.querySelector("[data-button='back-to-top']");
|
||||||
|
const progressIndicator = document.querySelector("#progress-indicator");
|
||||||
|
|
||||||
|
if (!rootElement || !btnContainer || !backToTopBtn || !progressIndicator)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Attach click event handler for back-to-top button
|
||||||
|
backToTopBtn.addEventListener("click", () => {
|
||||||
|
document.body.scrollTop = 0; // For Safari
|
||||||
|
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle button visibility according to scroll position
|
||||||
|
let lastVisible = null;
|
||||||
|
function handleScroll() {
|
||||||
|
const scrollTotal = rootElement.scrollHeight - rootElement.clientHeight;
|
||||||
|
const scrollTop = rootElement.scrollTop;
|
||||||
|
const scrollPercent = Math.floor((scrollTop / scrollTotal) * 100);
|
||||||
|
|
||||||
|
progressIndicator.style.setProperty(
|
||||||
|
"background-image",
|
||||||
|
`conic-gradient(var(--accent), var(--accent) ${scrollPercent}%, transparent ${scrollPercent}%)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const isVisible = scrollTop / scrollTotal > 0.3;
|
||||||
|
|
||||||
|
if (isVisible !== lastVisible) {
|
||||||
|
btnContainer.classList.toggle("opacity-100", isVisible);
|
||||||
|
btnContainer.classList.toggle("translate-y-0", isVisible);
|
||||||
|
btnContainer.classList.toggle("opacity-0", !isVisible);
|
||||||
|
btnContainer.classList.toggle("translate-y-14", !isVisible);
|
||||||
|
lastVisible = isVisible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ticking = false;
|
||||||
|
document.addEventListener("scroll", () => {
|
||||||
|
if (!ticking) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
handleScroll();
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
backToTop();
|
||||||
|
</script>
|
||||||
@@ -8,6 +8,7 @@ import Datetime from "@/components/Datetime.astro";
|
|||||||
import EditPost from "@/components/EditPost.astro";
|
import EditPost from "@/components/EditPost.astro";
|
||||||
import ShareLinks from "@/components/ShareLinks.astro";
|
import ShareLinks from "@/components/ShareLinks.astro";
|
||||||
import BackButton from "@/components/BackButton.astro";
|
import BackButton from "@/components/BackButton.astro";
|
||||||
|
import BackToTopButton from "@/components/BackToTopButton.astro";
|
||||||
import { getPath } from "@/utils/getPath";
|
import { getPath } from "@/utils/getPath";
|
||||||
import { slugifyStr } from "@/utils/slugify";
|
import { slugifyStr } from "@/utils/slugify";
|
||||||
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
|
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
|
||||||
@@ -109,22 +110,16 @@ const nextPost =
|
|||||||
|
|
||||||
<EditPost class="sm:hidden" {hideEditPost} {post} />
|
<EditPost class="sm:hidden" {hideEditPost} {post} />
|
||||||
|
|
||||||
<ul class="mt-4 mb-8 sm:my-8">
|
<BackToTopButton />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-between gap-6 sm:flex-row sm:items-start sm:gap-8"
|
||||||
|
>
|
||||||
|
<ul class="self-start">
|
||||||
{tags.map(tag => <Tag tag={slugifyStr(tag)} tagName={tag} />)}
|
{tags.map(tag => <Tag tag={slugifyStr(tag)} tagName={tag} />)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-center justify-between gap-6 sm:flex-row sm:items-end sm:gap-4"
|
|
||||||
>
|
|
||||||
<ShareLinks />
|
<ShareLinks />
|
||||||
|
|
||||||
<button
|
|
||||||
id="back-to-top"
|
|
||||||
class="focus-outline py-1 whitespace-nowrap hover:opacity-75"
|
|
||||||
>
|
|
||||||
<IconChevronLeft class="inline-block rotate-90" />
|
|
||||||
<span>Back to Top</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-6 border-dashed" />
|
<hr class="my-6 border-dashed" />
|
||||||
@@ -270,16 +265,6 @@ const nextPost =
|
|||||||
}
|
}
|
||||||
attachCopyButtons();
|
attachCopyButtons();
|
||||||
|
|
||||||
/** Scrolls the document to the top when
|
|
||||||
* the "Back to Top" button is clicked. */
|
|
||||||
function backToTop() {
|
|
||||||
document.querySelector("#back-to-top")?.addEventListener("click", () => {
|
|
||||||
document.body.scrollTop = 0; // For Safari
|
|
||||||
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
|
|
||||||
});
|
|
||||||
}
|
|
||||||
backToTop();
|
|
||||||
|
|
||||||
/* Go to page start after page swap */
|
/* Go to page start after page swap */
|
||||||
document.addEventListener("astro:after-swap", () =>
|
document.addEventListener("astro:after-swap", () =>
|
||||||
window.scrollTo({ left: 0, top: 0, behavior: "instant" })
|
window.scrollTo({ left: 0, top: 0, behavior: "instant" })
|
||||||
|
|||||||
Reference in New Issue
Block a user