From 544fe97c5ea6af5ad09c5eb49d2d5afb6bbd6253 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sat, 8 Nov 2025 11:32:57 -0700 Subject: [PATCH] refactor scrolling with a reusable component --- src/components/player/atoms/Episodes.tsx | 3 +- src/pages/Settings.tsx | 50 ++++---------- src/utils/scroll.ts | 87 ++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 38 deletions(-) create mode 100644 src/utils/scroll.ts diff --git a/src/components/player/atoms/Episodes.tsx b/src/components/player/atoms/Episodes.tsx index 15608892..8b9bf424 100644 --- a/src/components/player/atoms/Episodes.tsx +++ b/src/components/player/atoms/Episodes.tsx @@ -20,6 +20,7 @@ import { PlayerMeta } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; import { usePreferencesStore } from "@/stores/preferences"; import { useProgressStore } from "@/stores/progress"; +import { scrollToElement } from "@/utils/scroll"; import { hasAired } from "../utils/aired"; @@ -832,7 +833,7 @@ export function EpisodesView({ carouselRef.current.scrollLeft += scrollPosition; } else { // vertical scroll - activeEpisodeRef.current.scrollIntoView({ + scrollToElement(activeEpisodeRef.current, { behavior: "smooth", block: "center", }); diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index e7cf4a83..a1639b68 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -37,6 +37,7 @@ import { useLanguageStore } from "@/stores/language"; import { usePreferencesStore } from "@/stores/preferences"; import { useSubtitleStore } from "@/stores/subtitles"; import { usePreviewThemeStore, useThemeStore } from "@/stores/theme"; +import { scrollToElement, scrollToHash } from "@/utils/scroll"; import { SubPageLayout } from "./layouts/SubPageLayout"; import { AppInfoPart } from "./parts/settings/AppInfoPart"; @@ -195,19 +196,11 @@ export function SettingsPage() { const categoryId = subSectionToCategory[hashId]; setSelectedCategory(categoryId); // Wait for the section to render, then scroll - setTimeout(() => { - const element = document.querySelector(hash); - if (element) { - element.scrollIntoView({ behavior: "smooth" }); - } - }, 100); + scrollToHash(hash, { delay: 100 }); } else if (validCategories.includes(hashId)) { // It's a category hash setSelectedCategory(hashId); - const element = document.querySelector(hash); - if (element) { - element.scrollIntoView({ behavior: "smooth" }); - } + scrollToHash(hash); } else { // Try to find the element anyway (might be a sub-section) const element = document.querySelector(hash); @@ -218,12 +211,10 @@ export function SettingsPage() { const categoryId = parentSection.id; if (validCategories.includes(categoryId)) { setSelectedCategory(categoryId); - setTimeout(() => { - element.scrollIntoView({ behavior: "smooth" }); - }, 100); + scrollToHash(hash, { delay: 100 }); } } else { - element.scrollIntoView({ behavior: "smooth" }); + scrollToHash(hash); } } } @@ -251,20 +242,10 @@ export function SettingsPage() { if (subSectionToCategory[hashId]) { const categoryId = subSectionToCategory[hashId]; setSelectedCategory(categoryId); - setTimeout(() => { - const element = document.querySelector(hash); - if (element) { - element.scrollIntoView({ behavior: "smooth" }); - } - }, 100); + scrollToHash(hash, { delay: 100 }); } else if (validCategories.includes(hashId)) { setSelectedCategory(hashId); - setTimeout(() => { - const element = document.querySelector(hash); - if (element) { - element.scrollIntoView({ behavior: "smooth" }); - } - }, 100); + scrollToHash(hash, { delay: 100 }); } else { const element = document.querySelector(hash); if (element) { @@ -273,12 +254,10 @@ export function SettingsPage() { const categoryId = parentSection.id; if (validCategories.includes(categoryId)) { setSelectedCategory(categoryId); - setTimeout(() => { - element.scrollIntoView({ behavior: "smooth" }); - }, 100); + scrollToHash(hash, { delay: 100 }); } } else { - element.scrollIntoView({ behavior: "smooth" }); + scrollToHash(hash); } } } @@ -372,13 +351,10 @@ export function SettingsPage() { } // Scroll to first highlighted element - const firstHighlighted = document.querySelector(".search-highlight"); - if (firstHighlighted) { - firstHighlighted.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - } + scrollToElement(".search-highlight", { + behavior: "smooth", + block: "center", + }); } }, []); diff --git a/src/utils/scroll.ts b/src/utils/scroll.ts new file mode 100644 index 00000000..dbdf3063 --- /dev/null +++ b/src/utils/scroll.ts @@ -0,0 +1,87 @@ +/** + * Scrolls an element into view with configurable options + * @param selector - CSS selector string, Element, or null + * @param options - Scroll options + * @returns void (always returns, even if element not found) + */ +export function scrollToElement( + selector: string | Element | null, + options?: { + behavior?: ScrollBehavior; + block?: ScrollLogicalPosition; + inline?: ScrollLogicalPosition; + offset?: number; // Additional offset in pixels (positive = scroll down more) + delay?: number; // Delay in milliseconds before scrolling (useful when element needs to render) + }, +): void { + const { + behavior = "smooth", + block = "start", + inline = "nearest", + offset = 0, + delay = 0, + } = options || {}; + + const scroll = (): void => { + let element: Element | null = null; + + if (selector === null) { + return; + } + + if (typeof selector === "string") { + element = document.querySelector(selector); + } else { + element = selector; + } + + if (!element) { + return; + } + + if (offset === 0) { + // Use native scrollIntoView when no offset is needed + element.scrollIntoView({ behavior, block, inline }); + return; + } + + // Custom scroll with offset + const elementRect = element.getBoundingClientRect(); + const absoluteElementTop = elementRect.top + window.pageYOffset; + const offsetPosition = absoluteElementTop - offset; + + window.scrollTo({ + top: offsetPosition, + behavior, + }); + }; + + if (delay > 0) { + setTimeout(() => { + scroll(); + }, delay); + return; + } + + scroll(); +} + +/** + * Scrolls to an element by hash (useful for hash navigation) + * @param hash - Hash string (with or without #) + * @param options - Scroll options + * @returns void (always returns, even if element not found) + */ +export function scrollToHash( + hash: string, + options?: { + behavior?: ScrollBehavior; + block?: ScrollLogicalPosition; + inline?: ScrollLogicalPosition; + offset?: number; + delay?: number; + }, +): void { + const normalizedHash = hash.startsWith("#") ? hash : `#${hash}`; + scrollToElement(normalizedHash, options); +}