refactor(stream): simplify FilterChips by dropping the 1.5s scroll watcher
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 38s

The defensive rAF + scroll loop and its touching guard were added to
fight an iOS sticky-relayout quirk, but the module-level lastScrollLeft
plus the useLayoutEffect mount restore already cover the common case.
The watch loop also interfered with a fresh slide gesture immediately
after a filter tap. Strip it out together with the surrounding inline
comments so the component is the minimum needed: gold active state on
click and a remount-surviving scroll position.
This commit is contained in:
TerryM
2026-06-03 14:32:47 +08:00
parent f2f2572cd2
commit 53614189ce
2 changed files with 35 additions and 60 deletions

View File

@@ -19,21 +19,12 @@ export type FilterChipsProps = {
onTypeChange: (next: string) => void;
};
// Persist the horizontal scroll position OUTSIDE the component lifecycle.
// `PublicLayout` wraps every page in an `AnimatePresence` keyed by
// `pathname + search`, so changing the `?type=…` query unmounts the current
// `FilterChips` and mounts a fresh one. A `useRef` would reset on every
// filter switch. A module-level value survives the re-mount and lets the
// new instance restore the user's last scroll position synchronously on
// first paint.
let lastScrollLeft = 0;
export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
const { t } = useI18n();
const scrollRef = useRef<HTMLDivElement>(null);
// Restore the saved scroll position before paint so the bar never flashes
// at scrollLeft=0 after a filter-driven re-mount.
useLayoutEffect(() => {
const el = scrollRef.current;
if (!el) return;
@@ -42,9 +33,6 @@ export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
}
}, []);
// Let a mouse wheel scroll the row horizontally when it overflows — desktop
// mice have no horizontal wheel and the scrollbar is hidden, so otherwise the
// last filters are unreachable. Touch/trackpad scroll natively.
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
@@ -57,11 +45,6 @@ export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
return () => el.removeEventListener("wheel", onWheel);
}, []);
// Save the position only on user-initiated input (touchend, pointerup,
// wheel). The sibling `ScrollToTop` calls `window.scrollTo` after the
// re-mount, which on iOS Safari can collapse this sticky row's
// scrollLeft to 0 — that fires a `scroll` event too, but it's not a
// user gesture and we must not let it overwrite the saved value.
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
@@ -69,7 +52,6 @@ export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
lastScrollLeft = el.scrollLeft;
};
const saveDeferred = () => {
// Wait one frame so the momentum scroll has had a chance to update.
window.requestAnimationFrame(save);
};
el.addEventListener("touchend", saveDeferred, { passive: true });
@@ -82,48 +64,6 @@ export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
};
}, []);
// After mount, watch for the iOS Safari quirk that asynchronously resets
// scrollLeft to 0 (triggered by the page-level scroll-to-top that runs
// after a filter-driven re-mount). Re-apply the saved value for ~1.5s,
// skipping while the user is actively touching so a fresh scroll gesture
// isn't yanked back.
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
if (lastScrollLeft <= 0) return;
const target = lastScrollLeft;
let cancelled = false;
let touching = false;
const onTouchStart = () => {
touching = true;
};
const onTouchEnd = () => {
touching = false;
};
const deadline = performance.now() + 1500;
const apply = () => {
if (cancelled || !el || touching) return;
if (el.scrollLeft === 0 && target > 0) el.scrollLeft = target;
};
const tick = () => {
if (cancelled) return;
apply();
if (performance.now() < deadline) window.requestAnimationFrame(tick);
};
window.requestAnimationFrame(tick);
el.addEventListener("scroll", apply, { passive: true });
el.addEventListener("touchstart", onTouchStart, { passive: true });
el.addEventListener("touchend", onTouchEnd, { passive: true });
el.addEventListener("touchcancel", onTouchEnd, { passive: true });
return () => {
cancelled = true;
el.removeEventListener("scroll", apply);
el.removeEventListener("touchstart", onTouchStart);
el.removeEventListener("touchend", onTouchEnd);
el.removeEventListener("touchcancel", onTouchEnd);
};
}, []);
const tabClass = (active: boolean) =>
[
"relative flex h-[52px] shrink-0 items-center whitespace-nowrap px-3 pb-4 pt-3 text-[15px] leading-6 outline-none transition-colors md:h-auto md:px-1 md:py-3 md:leading-none",