feat: Add SmoothScroll component

This commit is contained in:
Gauthier Daniels 2025-04-21 17:31:11 +02:00
parent 27c0c687b1
commit b3afa6615b
5 changed files with 85 additions and 5 deletions

View File

@ -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 (
<div {...props} onWheel={handleWheel} style={{ "touch-action": "auto" }}>
{props.children}
</div>
);
}

View File

@ -68,7 +68,7 @@ export function Snippet(props: SnippetProps) {
<div class="pt-4 pl-4">
<TrafficLightsIcon class="h-2.5 w-auto stroke-slate-500/30" />
<div class="mt-4 flex space-x-2 text-xs">
<div class="mt-4 flex space-x-2 text-xs overflow-x-auto">
<For each={props.snippets}>
{(tab) => (
<div

View File

@ -1,6 +1,5 @@
import type { JSXElement } from "solid-js";
// import { TableOfContents } from "@/partials/TableOfContents";
import { PrevNextLinks } from "@/components/PrevNextLinks";
import { usePageContext } from "vike-solid/usePageContext";
import { clientOnly } from "vike-solid/clientOnly";

View File

@ -1,6 +1,7 @@
import type { JSXElement } from "solid-js";
import { usePageContext } from "vike-solid/usePageContext";
import { SmoothScroll } from "@/components/SmoothScroll";
import { HeroSection } from "@/partials/HeroSection";
import { Navigation } from "@/partials/Navigation";
import { clientOnly } from "vike-solid/clientOnly";
@ -24,7 +25,7 @@ export default function DefaultLayout(props: DefaultLayoutProps) {
return (
<>
<div class="flex w-full flex-col font-sans">
<SmoothScroll class="flex w-full flex-col font-sans">
<Header />
<ReadProgressBar />
@ -43,7 +44,7 @@ export default function DefaultLayout(props: DefaultLayoutProps) {
</div>
<Footer />
</div>
</SmoothScroll>
<Toaster />
</>

View File

@ -43,7 +43,7 @@ export function ReadProgressBar() {
class="sticky inset-x-0 top-20 z-50 h-1 w-full bg-violet-50"
>
<div
class="bg-violet-300 h-full transition-all duration-75"
class="bg-violet-300 h-full transition-all duration-300 ease-in-out"
style={{ width: width() }}
/>
</div>