diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 1aae08d6..7c25198b 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -170,6 +170,7 @@ export function AccountSettings(props: { export function SettingsPage() { const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState(null); + const prevCategoryRef = useRef(null); useEffect(() => { const hash = window.location.hash; @@ -194,6 +195,22 @@ export function SettingsPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Scroll to top when category changes (but not on initial load or when searching) + useEffect(() => { + if ( + prevCategoryRef.current !== null && + prevCategoryRef.current !== selectedCategory && + !searchQuery.trim() + ) { + // Only scroll to top if we're actually switching categories (not initial load) + // Use requestAnimationFrame to ensure DOM has updated + requestAnimationFrame(() => { + window.scrollTo({ top: 0, behavior: "smooth" }); + }); + } + prevCategoryRef.current = selectedCategory; + }, [selectedCategory, searchQuery]); + const { t } = useTranslation(); const activeTheme = useThemeStore((s) => s.theme); const setTheme = useThemeStore((s) => s.setTheme); diff --git a/src/pages/parts/settings/SidebarPart.tsx b/src/pages/parts/settings/SidebarPart.tsx index abeda750..d4971016 100644 --- a/src/pages/parts/settings/SidebarPart.tsx +++ b/src/pages/parts/settings/SidebarPart.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import Sticky from "react-sticky-el"; import { Icons } from "@/components/Icon"; import { SidebarLink, SidebarSection } from "@/components/layout/Sidebar"; @@ -9,8 +8,6 @@ import { useIsMobile } from "@/hooks/useIsMobile"; import { AppInfoPart } from "./AppInfoPart"; -const rem = 16; - export function SidebarPart(props: { selectedCategory: string | null; setSelectedCategory: (category: string | null) => void; @@ -54,36 +51,46 @@ export function SidebarPart(props: { useEffect(() => { // Only track active link when searching (to show all sections) if (props.searchQuery.trim()) { + let ticking = false; const recheck = () => { - const windowHeight = - window.innerHeight || document.documentElement.clientHeight; - const centerTarget = windowHeight / 4; + if (!ticking) { + window.requestAnimationFrame(() => { + const windowHeight = + window.innerHeight || document.documentElement.clientHeight; + const centerTarget = windowHeight / 4; - const viewList = settingLinks - .map((link) => { - const el = document.getElementById(link.id); - if (!el) return { distance: Infinity, link: link.id }; - const rect = el.getBoundingClientRect(); - const distanceTop = Math.abs(centerTarget - rect.top); - const distanceBottom = Math.abs(centerTarget - rect.bottom); - const distance = Math.min(distanceBottom, distanceTop); - return { distance, link: link.id }; - }) - .sort((a, b) => a.distance - b.distance); + const viewList = settingLinks + .map((link) => { + const el = document.getElementById(link.id); + if (!el) return { distance: Infinity, link: link.id }; + const rect = el.getBoundingClientRect(); + const distanceTop = Math.abs(centerTarget - rect.top); + const distanceBottom = Math.abs(centerTarget - rect.bottom); + const distance = Math.min(distanceBottom, distanceTop); + return { distance, link: link.id }; + }) + .sort((a, b) => a.distance - b.distance); - // Check if user has scrolled past the bottom of the page - if (window.innerHeight + window.scrollY >= document.body.offsetHeight) { - setActiveLink(settingLinks[settingLinks.length - 1].id); - return; + // Check if user has scrolled past the bottom of the page + if ( + window.innerHeight + window.scrollY >= + document.body.offsetHeight + ) { + setActiveLink(settingLinks[settingLinks.length - 1].id); + } else { + // shortest distance to the part of the screen we want is the active link + setActiveLink(viewList[0]?.link ?? ""); + } + ticking = false; + }); + ticking = true; } - // shortest distance to the part of the screen we want is the active link - setActiveLink(viewList[0]?.link ?? ""); }; - document.addEventListener("scroll", recheck); + window.addEventListener("scroll", recheck, { passive: true }); recheck(); return () => { - document.removeEventListener("scroll", recheck); + window.removeEventListener("scroll", recheck); }; } // When not searching, set active link to selected category @@ -101,12 +108,18 @@ export function SidebarPart(props: { return (
-
- + ); }