refactor scrolling with a reusable component

This commit is contained in:
Pas 2025-11-08 11:32:57 -07:00
parent 1199a21df5
commit 544fe97c5e
3 changed files with 102 additions and 38 deletions

View file

@ -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",
});

View file

@ -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",
});
}
}, []);

87
src/utils/scroll.ts Normal file
View file

@ -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);
}