From b3afa6615b7713879309407f8d0915c1fb9b460d Mon Sep 17 00:00:00 2001 From: GauthierWebDev Date: Mon, 21 Apr 2025 17:31:11 +0200 Subject: [PATCH] feat: Add SmoothScroll component --- app/components/SmoothScroll.tsx | 80 ++++++++++++++++++++++++++++++++ app/components/Snippet.tsx | 2 +- app/layouts/DocsLayout.tsx | 1 - app/layouts/LayoutDefault.tsx | 5 +- app/partials/ReadProgressBar.tsx | 2 +- 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 app/components/SmoothScroll.tsx diff --git a/app/components/SmoothScroll.tsx b/app/components/SmoothScroll.tsx new file mode 100644 index 0000000..8a5cc45 --- /dev/null +++ b/app/components/SmoothScroll.tsx @@ -0,0 +1,80 @@ +import type { JSX } from "solid-js"; +import { createSignal, onCleanup } from "solid-js"; + +type SmoothScrollProps = JSX.IntrinsicElements["div"] & { + children: JSX.Element; +}; + +export function SmoothScroll(props: SmoothScrollProps) { + const [isScrolling, setIsScrolling] = createSignal(false); + let animationFrameId: number | null = null; + + const easeOutQuad = (t: number, b: number, c: number, d: number) => { + const time = t / d; + return -c * time * (time - 2) + b; + }; + + const smoothScroll = (deltaY: number) => { + const scrollSpeed = 3; + const currentScroll = window.scrollY; + const targetScroll = deltaY * scrollSpeed; + const duration = 300; + const startTime = performance.now(); + + const animateScroll = (currentTime: number) => { + const elapsedTime = currentTime - startTime; + const ease = easeOutQuad( + elapsedTime, + currentScroll, + targetScroll, + duration, + ); + + window.scrollTo(0, ease); + + if (elapsedTime < duration) { + animationFrameId = requestAnimationFrame(animateScroll); + } else { + setIsScrolling(false); + } + }; + + animationFrameId = requestAnimationFrame(animateScroll); + }; + + const isMobile = () => { + const regex = + /Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i; + return regex.test(navigator.userAgent); + }; + + const handleWheel = (event: WheelEvent) => { + if (isMobile()) return; + + event.preventDefault(); + if (isScrolling()) { + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId); + } + } + + requestAnimationFrame(() => { + smoothScroll(event.deltaY); + setIsScrolling(false); + }); + + setIsScrolling(true); + }; + + onCleanup(() => { + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId); + } + }); + + return ( +
+ {props.children} +
+ ); +} diff --git a/app/components/Snippet.tsx b/app/components/Snippet.tsx index e799a33..bb8efbb 100644 --- a/app/components/Snippet.tsx +++ b/app/components/Snippet.tsx @@ -68,7 +68,7 @@ export function Snippet(props: SnippetProps) {
-
+
{(tab) => (
-
+
@@ -43,7 +44,7 @@ export default function DefaultLayout(props: DefaultLayoutProps) {
-
+ diff --git a/app/partials/ReadProgressBar.tsx b/app/partials/ReadProgressBar.tsx index e1f807e..3b0d51e 100644 --- a/app/partials/ReadProgressBar.tsx +++ b/app/partials/ReadProgressBar.tsx @@ -43,7 +43,7 @@ export function ReadProgressBar() { class="sticky inset-x-0 top-20 z-50 h-1 w-full bg-violet-50" >