fix: improve settings search highlight

This commit is contained in:
Linus Rath
2026-05-06 18:58:06 +02:00
parent 2dc8537780
commit 70c1ddd48c
3 changed files with 64 additions and 25 deletions
+36 -19
View File
@@ -442,37 +442,54 @@ export default function SettingsPage() {
}, [isDesktop, mobileShowContent]);
// After clicking a search sub-result, scroll the matching setting into view
// and add a temporary highlight class. Two RAFs to wait for the tab content
// to mount and lay out before querying the DOM.
// and add a temporary highlight class. Some tabs fetch data and render
// their SettingItems only after a loading state, so retry until the element
// shows up (or we give up after ~2s).
useEffect(() => {
if (!pendingHighlight) return;
if (pendingHighlight.tab !== activeTab) return;
if (typeof window === 'undefined') return;
let cancelled = false;
let retryTimer: ReturnType<typeof setTimeout> | undefined;
let cleanupTimer: ReturnType<typeof setTimeout> | undefined;
let highlightedEl: HTMLElement | null = null;
const r1 = window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
if (cancelled) return;
const escaped = pendingHighlight.label.replace(/"/g, '\\"');
const el = document.querySelector<HTMLElement>(`[data-search-label="${escaped}"]`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add('settings-search-highlight');
highlightedEl = el;
cleanupTimer = setTimeout(() => {
el.classList.remove('settings-search-highlight');
}, 2000);
}
setPendingHighlight(null);
});
});
const escaped = pendingHighlight.label.replace(/"/g, '\\"');
const selector = `[data-search-label="${escaped}"]`;
const deadline = Date.now() + 2000;
const tryHighlight = () => {
if (cancelled) return;
const el = document.querySelector<HTMLElement>(selector);
if (!el) {
if (Date.now() < deadline) {
retryTimer = setTimeout(tryHighlight, 80);
}
return;
}
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Remove + reflow + add restarts the CSS animation if the class was
// already present (re-clicking the same sub-result).
el.classList.remove('settings-search-highlight');
void el.offsetWidth;
el.classList.add('settings-search-highlight');
highlightedEl = el;
cleanupTimer = setTimeout(() => {
el.classList.remove('settings-search-highlight');
highlightedEl = null;
}, 1800);
};
// First attempt next frame so the freshly-mounted tab content is in DOM.
const raf = window.requestAnimationFrame(tryHighlight);
// Do NOT reset pendingHighlight here — that would retrigger this effect
// and the cleanup below would strip the class right after we added it.
return () => {
cancelled = true;
window.cancelAnimationFrame(r1);
window.cancelAnimationFrame(raf);
if (retryTimer) clearTimeout(retryTimer);
if (cleanupTimer) clearTimeout(cleanupTimer);
if (highlightedEl) highlightedEl.classList.remove('settings-search-highlight');
};
+26 -4
View File
@@ -766,9 +766,31 @@ body {
cursor: col-resize;
}
/* Brief flash applied to a setting row when the user clicks a sub-result in
the settings search. The page removes the class after ~2s. */
/* Smooth halo applied to a setting row when the user clicks a sub-result in
the settings search. The element bg + a 10px box-shadow fill the row plus
a 10px ring of the same tint, then a 2px outline sits exactly at the outer
edge. Uses the primary theme color so it adapts to light/dark. */
@keyframes settings-search-pulse {
0% {
background-color: color-mix(in srgb, var(--color-primary) 0%, transparent);
box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 0%, transparent);
outline-color: color-mix(in srgb, var(--color-primary) 0%, transparent);
}
30% {
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
box-shadow: 0 0 0 10px color-mix(in srgb, var(--color-primary) 12%, transparent);
outline-color: color-mix(in srgb, var(--color-primary) 60%, transparent);
}
100% {
background-color: color-mix(in srgb, var(--color-primary) 0%, transparent);
box-shadow: 0 0 0 10px color-mix(in srgb, var(--color-primary) 0%, transparent);
outline-color: color-mix(in srgb, var(--color-primary) 0%, transparent);
}
}
.settings-search-highlight {
background-color: color-mix(in srgb, var(--color-primary) 18%, transparent);
box-shadow: 0 0 0 2px var(--color-primary);
outline: 2px solid transparent;
outline-offset: 10px;
border-radius: 8px;
animation: settings-search-pulse 1.6s ease-in-out forwards;
}