diff --git a/__tests__/components/layout/DynamicHeaderWrapper.test.tsx b/__tests__/components/layout/DynamicHeaderWrapper.test.tsx index 45f30ebc..bd40f679 100644 --- a/__tests__/components/layout/DynamicHeaderWrapper.test.tsx +++ b/__tests__/components/layout/DynamicHeaderWrapper.test.tsx @@ -52,6 +52,10 @@ describe("DynamicHeaderWrapper", () => { expect(screen.getByTestId("header8")).toHaveAttribute("data-scroll", "false"); Object.defineProperty(window, "scrollY", { value: 120, writable: true, configurable: true }); + window.requestAnimationFrame = (cb) => { + cb(performance.now()); + return 1; + }; fireEvent.scroll(document); expect(screen.getByTestId("header8")).toHaveAttribute("data-scroll", "true"); diff --git a/__tests__/components/layout/Layout.test.tsx b/__tests__/components/layout/Layout.test.tsx index e326e1c3..d3130390 100644 --- a/__tests__/components/layout/Layout.test.tsx +++ b/__tests__/components/layout/Layout.test.tsx @@ -96,6 +96,10 @@ describe("Layout", () => { expect(screen.getByTestId("header-1")).toHaveAttribute("data-scroll", "false"); Object.defineProperty(window, "scrollY", { value: 150, writable: true, configurable: true }); + window.requestAnimationFrame = (cb) => { + cb(performance.now()); + return 1; + }; fireEvent.scroll(document); expect(screen.getByTestId("header-1")).toHaveAttribute("data-scroll", "true"); diff --git a/__tests__/snapshots/elements/BackToTop.test.tsx b/__tests__/snapshots/elements/BackToTop.test.tsx index 25288fb9..9368ca2e 100644 --- a/__tests__/snapshots/elements/BackToTop.test.tsx +++ b/__tests__/snapshots/elements/BackToTop.test.tsx @@ -1,5 +1,5 @@ import { expect, describe, it } from "@jest/globals"; -import { render } from "@testing-library/react"; +import { render, fireEvent } from "@testing-library/react"; import BackToTop from "@/components/elements/BackToTop"; describe("BackToTop Component", () => { @@ -7,4 +7,15 @@ describe("BackToTop Component", () => { const { container } = render(); expect(container).toMatchSnapshot(); }); + + it("updates scroll state on scroll event", () => { + render(); + + Object.defineProperty(window, "scrollY", { value: 150, writable: true, configurable: true }); + window.requestAnimationFrame = (cb) => { + cb(performance.now()); + return 1; + }; + fireEvent.scroll(window); + }); }); diff --git a/components/elements/BackToTop.tsx b/components/elements/BackToTop.tsx index 3fdb6396..9aaacc42 100644 --- a/components/elements/BackToTop.tsx +++ b/components/elements/BackToTop.tsx @@ -5,11 +5,18 @@ export default function BackToTop({ target }: Readonly<{ target: string }>) { const [hasScrolled, setHasScrolled] = useState(false); useEffect(() => { + const state = { isTicking: false }; const onScroll = () => { - setHasScrolled(window.scrollY > 100); + if (!state.isTicking) { + window.requestAnimationFrame(() => { + setHasScrolled(window.scrollY > 100); + state.isTicking = false; + }); + state.isTicking = true; + } }; - window.addEventListener("scroll", onScroll); + window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); diff --git a/components/layout/DynamicHeaderWrapper.tsx b/components/layout/DynamicHeaderWrapper.tsx index 1206feb6..dbb5a706 100644 --- a/components/layout/DynamicHeaderWrapper.tsx +++ b/components/layout/DynamicHeaderWrapper.tsx @@ -16,17 +16,22 @@ export default function DynamicHeaderWrapper({ navigation }: Readonly(false); React.useEffect(() => { + const state = { isTicking: false }; const handleScroll = (): void => { - const scrollCheck: boolean = window.scrollY > 100; - if (scrollCheck !== scroll) { - setScroll(scrollCheck); + if (!state.isTicking) { + window.requestAnimationFrame(() => { + const scrollCheck: boolean = window.scrollY > 100; + setScroll((prev) => (prev !== scrollCheck ? scrollCheck : prev)); + state.isTicking = false; + }); + state.isTicking = true; } }; - document.addEventListener("scroll", handleScroll); + document.addEventListener("scroll", handleScroll, { passive: true }); return () => { document.removeEventListener("scroll", handleScroll); }; - }, [scroll]); + }, []); return ( <> diff --git a/components/layout/Layout.tsx b/components/layout/Layout.tsx index 7f289889..958cb750 100644 --- a/components/layout/Layout.tsx +++ b/components/layout/Layout.tsx @@ -79,19 +79,25 @@ export default function Layout({ headerStyle, footerStyle, breadcrumbTitle: _bre useEffect(() => { AOS.init(); + + const state = { isTicking: false }; const handleScroll = (): void => { - const scrollCheck: boolean = window.scrollY > 100; - if (scrollCheck !== scroll) { - setScroll(scrollCheck); + if (!state.isTicking) { + window.requestAnimationFrame(() => { + const scrollCheck: boolean = window.scrollY > 100; + setScroll((prev) => (prev !== scrollCheck ? scrollCheck : prev)); + state.isTicking = false; + }); + state.isTicking = true; } }; - document.addEventListener("scroll", handleScroll); + document.addEventListener("scroll", handleScroll, { passive: true }); return () => { document.removeEventListener("scroll", handleScroll); }; - }, [scroll]); + }, []); const defaultNavigation: EditionNavigation = { main: mainNavLinks,