-
Notifications
You must be signed in to change notification settings - Fork 0
β‘ Bolt: Optimize BackToTop scroll listener #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
335abfd
7cb5add
25915e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { render, fireEvent, act } from "@testing-library/react"; | ||
| import BackToTop from "@/components/elements/BackToTop"; | ||
|
|
||
| describe("BackToTop", () => { | ||
| beforeAll(() => { | ||
| // Mock window.scrollTo | ||
| Object.defineProperty(window, "scrollTo", { | ||
| value: jest.fn(), | ||
| writable: true, | ||
| }); | ||
| // Mock requestAnimationFrame to execute callback immediately | ||
| jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb: FrameRequestCallback) => { | ||
| cb(0); | ||
| return 1; | ||
| }); | ||
| jest.spyOn(window, "cancelAnimationFrame").mockImplementation(() => {}); | ||
| }); | ||
|
|
||
| afterAll(() => { | ||
| (window.requestAnimationFrame as jest.Mock).mockRestore(); | ||
| (window.cancelAnimationFrame as jest.Mock).mockRestore(); | ||
| }); | ||
|
|
||
| it("should render nothing initially (scrollY <= 100)", () => { | ||
| const { container } = render(<BackToTop target="#top" />); | ||
| expect(container.firstChild).toBeNull(); | ||
| }); | ||
|
|
||
| it("should appear after scrolling down (> 100px)", () => { | ||
| const { container } = render(<BackToTop target="#top" />); | ||
|
|
||
| act(() => { | ||
| // Mock scrollY | ||
| Object.defineProperty(window, "scrollY", { | ||
| value: 200, | ||
| writable: true, | ||
| }); | ||
| window.dispatchEvent(new Event("scroll")); | ||
| }); | ||
|
|
||
| const button = container.querySelector(".paginacontainer"); | ||
| expect(button).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should disappear after scrolling back up (<= 100px)", () => { | ||
| const { container } = render(<BackToTop target="#top" />); | ||
|
|
||
| // Scroll down first | ||
| act(() => { | ||
| Object.defineProperty(window, "scrollY", { | ||
| value: 200, | ||
| writable: true, | ||
| }); | ||
| window.dispatchEvent(new Event("scroll")); | ||
| }); | ||
|
|
||
| expect(container.querySelector(".paginacontainer")).toBeInTheDocument(); | ||
|
|
||
| // Scroll back up | ||
| act(() => { | ||
| Object.defineProperty(window, "scrollY", { | ||
| value: 50, | ||
| writable: true, | ||
| }); | ||
| window.dispatchEvent(new Event("scroll")); | ||
| }); | ||
|
|
||
| expect(container.querySelector(".paginacontainer")).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should scroll to top when clicked", () => { | ||
| // Mock the target element | ||
| document.body.innerHTML = '<div id="top"></div>'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| const targetElement = document.getElementById("top"); | ||
| if (targetElement) { | ||
| Object.defineProperty(targetElement, "offsetTop", { | ||
| value: 0, | ||
| writable: true, | ||
| }); | ||
| } | ||
|
|
||
| const { container } = render(<BackToTop target="#top" />); | ||
|
|
||
| // Scroll down to make button visible | ||
| act(() => { | ||
| Object.defineProperty(window, "scrollY", { | ||
| value: 200, | ||
| writable: true, | ||
| }); | ||
| window.dispatchEvent(new Event("scroll")); | ||
| }); | ||
|
|
||
| const button = container.querySelector(".paginacontainer"); | ||
| expect(button).toBeInTheDocument(); | ||
|
|
||
| if (button) { | ||
| fireEvent.click(button); | ||
| expect(window.scrollTo).toHaveBeenCalledWith({ | ||
| top: 0, | ||
| behavior: "smooth", | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,19 +5,42 @@ export default function BackToTop({ target }: any) { | |||||||||||||||||||||
| const [hasScrolled, setHasScrolled] = useState(false); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
| let ticking = false; | ||||||||||||||||||||||
| let rafId: number; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const onScroll = () => { | ||||||||||||||||||||||
| setHasScrolled(window.scrollY > 100); | ||||||||||||||||||||||
| if (!ticking) { | ||||||||||||||||||||||
| ticking = true; | ||||||||||||||||||||||
| rafId = window.requestAnimationFrame(() => { | ||||||||||||||||||||||
| setHasScrolled(window.scrollY > 100); | ||||||||||||||||||||||
| ticking = false; | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| window.addEventListener("scroll", onScroll); | ||||||||||||||||||||||
| return () => window.removeEventListener("scroll", onScroll); | ||||||||||||||||||||||
| // Use passive listener for better scrolling performance | ||||||||||||||||||||||
| window.addEventListener("scroll", onScroll, { passive: true }); | ||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||
| window.removeEventListener("scroll", onScroll); | ||||||||||||||||||||||
| if (rafId) { | ||||||||||||||||||||||
| window.cancelAnimationFrame(rafId); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleClick = () => { | ||||||||||||||||||||||
| window.scrollTo({ | ||||||||||||||||||||||
| top: document.querySelector(target).offsetTop, | ||||||||||||||||||||||
| behavior: "smooth", | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| const targetElement = document.querySelector(target); | ||||||||||||||||||||||
| if (targetElement) { | ||||||||||||||||||||||
| window.scrollTo({ | ||||||||||||||||||||||
| top: (targetElement as HTMLElement).offsetTop, | ||||||||||||||||||||||
| behavior: "smooth", | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
Comment on lines
+33
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a type assertion with
Suggested change
|
||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| window.scrollTo({ | ||||||||||||||||||||||
| top: 0, | ||||||||||||||||||||||
| behavior: "smooth", | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test suite is very thorough! To make it even more robust, consider adding a test case for the scenario where an invalid
targetselector is provided. This would ensure the fallback behavior (scrolling to the top) is explicitly tested.