fix sticky and scrolling of SidebarPart

This commit is contained in:
Pas 2025-11-07 23:20:38 -07:00
parent 14a46c4b85
commit ba7c079b75
2 changed files with 62 additions and 32 deletions

View file

@ -170,6 +170,7 @@ export function AccountSettings(props: {
export function SettingsPage() { export function SettingsPage() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null); const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const prevCategoryRef = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
const hash = window.location.hash; const hash = window.location.hash;
@ -194,6 +195,22 @@ export function SettingsPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 { t } = useTranslation();
const activeTheme = useThemeStore((s) => s.theme); const activeTheme = useThemeStore((s) => s.theme);
const setTheme = useThemeStore((s) => s.setTheme); const setTheme = useThemeStore((s) => s.setTheme);

View file

@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Sticky from "react-sticky-el";
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { SidebarLink, SidebarSection } from "@/components/layout/Sidebar"; import { SidebarLink, SidebarSection } from "@/components/layout/Sidebar";
@ -9,8 +8,6 @@ import { useIsMobile } from "@/hooks/useIsMobile";
import { AppInfoPart } from "./AppInfoPart"; import { AppInfoPart } from "./AppInfoPart";
const rem = 16;
export function SidebarPart(props: { export function SidebarPart(props: {
selectedCategory: string | null; selectedCategory: string | null;
setSelectedCategory: (category: string | null) => void; setSelectedCategory: (category: string | null) => void;
@ -54,36 +51,46 @@ export function SidebarPart(props: {
useEffect(() => { useEffect(() => {
// Only track active link when searching (to show all sections) // Only track active link when searching (to show all sections)
if (props.searchQuery.trim()) { if (props.searchQuery.trim()) {
let ticking = false;
const recheck = () => { const recheck = () => {
const windowHeight = if (!ticking) {
window.innerHeight || document.documentElement.clientHeight; window.requestAnimationFrame(() => {
const centerTarget = windowHeight / 4; const windowHeight =
window.innerHeight || document.documentElement.clientHeight;
const centerTarget = windowHeight / 4;
const viewList = settingLinks const viewList = settingLinks
.map((link) => { .map((link) => {
const el = document.getElementById(link.id); const el = document.getElementById(link.id);
if (!el) return { distance: Infinity, link: link.id }; if (!el) return { distance: Infinity, link: link.id };
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const distanceTop = Math.abs(centerTarget - rect.top); const distanceTop = Math.abs(centerTarget - rect.top);
const distanceBottom = Math.abs(centerTarget - rect.bottom); const distanceBottom = Math.abs(centerTarget - rect.bottom);
const distance = Math.min(distanceBottom, distanceTop); const distance = Math.min(distanceBottom, distanceTop);
return { distance, link: link.id }; return { distance, link: link.id };
}) })
.sort((a, b) => a.distance - b.distance); .sort((a, b) => a.distance - b.distance);
// Check if user has scrolled past the bottom of the page // Check if user has scrolled past the bottom of the page
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) { if (
setActiveLink(settingLinks[settingLinks.length - 1].id); window.innerHeight + window.scrollY >=
return; 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(); recheck();
return () => { return () => {
document.removeEventListener("scroll", recheck); window.removeEventListener("scroll", recheck);
}; };
} }
// When not searching, set active link to selected category // When not searching, set active link to selected category
@ -101,12 +108,18 @@ export function SidebarPart(props: {
return ( return (
<div className="text-settings-sidebar-type-inactive sidebar-boundary"> <div className="text-settings-sidebar-type-inactive sidebar-boundary">
<Sticky <div
topOffset={-6 * rem} className={
stickyClassName="pt-[6rem]" isMobile ? "" : "sticky top-32 self-start will-change-transform"
disabled={isMobile} }
hideOnBoundaryHit={false} style={
boundaryElement=".sidebar-boundary" isMobile
? undefined
: {
// Use CSS transform for better performance
transform: "translateZ(0)",
}
}
> >
<SidebarSection title={t("global.pages.settings")}> <SidebarSection title={t("global.pages.settings")}>
<SidebarLink <SidebarLink
@ -137,7 +150,7 @@ export function SidebarPart(props: {
<div className="hidden lg:block"> <div className="hidden lg:block">
<AppInfoPart /> <AppInfoPart />
</div> </div>
</Sticky> </div>
</div> </div>
); );
} }