75 lines
2.9 KiB
TypeScript
75 lines
2.9 KiB
TypeScript
import { onPage } from "./lifecycle"
|
||
|
||
/**
|
||
* 回到顶部按钮。
|
||
*
|
||
* 显隐**默认交给 CSS**:`animation-timeline: scroll(root block)` 让按钮随滚动位置
|
||
* 淡入,不需要任何 scroll 监听(见 global.css 的 .back-to-top)。
|
||
* 这里只负责两件事:
|
||
* 1. 点击后平滑回到顶部;
|
||
* 2. 兜住「CSS 那条动画没生效」的所有情况 —— 既包括不支持滚动驱动动画的浏览器
|
||
* (Firefox < 144 / Safari < 26),也包括规则被构建压缩弄坏的情况(见下)。
|
||
*/
|
||
|
||
/** 与 global.css 里 animation-range 的起点呼应 */
|
||
const REVEAL_AFTER_PX = 480
|
||
|
||
/** global.css 中 @keyframes 的名字,用来实测动画有没有真的挂上 */
|
||
const ANIMATION_NAME = "back-to-top-reveal"
|
||
|
||
/**
|
||
* 「CSS 那条滚动驱动动画到底有没有生效」——直接读计算样式,而不是做特性检测。
|
||
*
|
||
* 为什么不用 CSS.supports("animation-timeline: scroll()"):它只回答「这个语法浏览器认不认」,
|
||
* 回答不了「这条规则有没有在构建压缩中活下来」。实际的坑是:Lightning CSS 会把
|
||
* animation-timeline 合并进 animation 简写,而 WebKit 不认简写里的 timeline 分量,
|
||
* 整条声明被丢弃 —— 此时 CSS.supports 仍然返回 true,于是降级分支永远不执行,
|
||
* 按钮就永远停在 opacity:0。改读 animationName 之后,CSS 那边无论因为什么原因没生效,
|
||
* 这里都能立刻发现并接管(详见 global.css 里 .back-to-top 的注释)。
|
||
*/
|
||
const cssDrivesVisibility = (button: HTMLElement) =>
|
||
window
|
||
.getComputedStyle(button)
|
||
.animationName.split(",")
|
||
.some((part) => part.trim() === ANIMATION_NAME)
|
||
|
||
onPage("back-to-top", () => {
|
||
const button = document.querySelector<HTMLElement>("[data-back-to-top]")
|
||
if (!button) return
|
||
|
||
const onClick = () => {
|
||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||
window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" })
|
||
}
|
||
button.addEventListener("click", onClick)
|
||
|
||
if (cssDrivesVisibility(button)) {
|
||
return () => button.removeEventListener("click", onClick)
|
||
}
|
||
|
||
// ——— 降级路径 ———
|
||
// rAF 节流,且只在跨越阈值时才写 class:滚动本身不产生布局读取(只用 scrollY)
|
||
const root = document.documentElement
|
||
root.classList.add("no-scroll-timeline")
|
||
|
||
let frame = 0
|
||
const update = () => {
|
||
frame = 0
|
||
button.classList.toggle("is-visible", window.scrollY > REVEAL_AFTER_PX)
|
||
}
|
||
const onScroll = () => {
|
||
if (frame) return
|
||
frame = window.requestAnimationFrame(update)
|
||
}
|
||
|
||
window.addEventListener("scroll", onScroll, { passive: true })
|
||
update()
|
||
|
||
return () => {
|
||
window.removeEventListener("scroll", onScroll)
|
||
if (frame) window.cancelAnimationFrame(frame)
|
||
button.removeEventListener("click", onClick)
|
||
root.classList.remove("no-scroll-timeline")
|
||
}
|
||
})
|