From 9aa132c02fc79774eed4cc59118686ecd08035fb Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Wed, 28 Oct 2026 20:05:13 +0100
Subject: [PATCH] feat: flash the settings section a jump-row button scrolls to
---
.../__tests__/settings-section-links.test.tsx | 18 ++++++++++++++++++
components/settings/settings-section.tsx | 16 +++++++++++++++-
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/components/settings/__tests__/settings-section-links.test.tsx b/components/settings/__tests__/settings-section-links.test.tsx
index 273a95e4b..bfae802e1 100644
--- a/components/settings/__tests__/settings-section-links.test.tsx
+++ b/components/settings/__tests__/settings-section-links.test.tsx
@@ -93,6 +93,24 @@ describe('settings section links', () => {
expect(scrolled).toContain('tags');
});
+ it('flashes the section a jump-row button scrolls to', async () => {
+ const { container } = render();
+ const nav = screen.getByTestId('settings-section-nav');
+ await waitFor(() => expect(nav).not.toHaveAttribute('hidden'));
+ vi.useFakeTimers();
+ try {
+ fireEvent.click(screen.getByRole('button', { name: 'Tags' }));
+ const tags = container.querySelector('[data-settings-section="tags"]')!;
+ const folders = container.querySelector('[data-settings-section="folders"]')!;
+ expect(tags).toHaveClass('settings-search-highlight');
+ expect(folders).not.toHaveClass('settings-search-highlight');
+ act(() => { vi.advanceTimersByTime(1800); });
+ expect(tags).not.toHaveClass('settings-search-highlight');
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
it('follows a tab-change event that names a retired tab to its new section', async () => {
render();
act(() => { window.dispatchEvent(new CustomEvent('settings-tab-change', { detail: 'identities' })); });
diff --git a/components/settings/settings-section.tsx b/components/settings/settings-section.tsx
index 738f8f05e..5fd01802e 100644
--- a/components/settings/settings-section.tsx
+++ b/components/settings/settings-section.tsx
@@ -96,11 +96,25 @@ export function AdvancedOnly({ children }: { children: ReactNode }) {
return visible ? <>{children}> : null;
}
-/** Scrolls the section with this id into view, if the open page has it. */
+const highlightTimers = new WeakMap>();
+
+/**
+ * Scrolls the section with this id into view, if the open page has it, and
+ * flashes the same halo a settings search result gets.
+ */
export function scrollToSettingsSection(id: string, root: ParentNode = document): boolean {
const el = root.querySelector(`[data-settings-section="${id.replace(/"/g, '\\"')}"]`);
if (!el) return false;
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ // Remove + reflow + add restarts the animation on a second click.
+ clearTimeout(highlightTimers.get(el));
+ el.classList.remove('settings-search-highlight');
+ void el.offsetWidth;
+ el.classList.add('settings-search-highlight');
+ highlightTimers.set(el, setTimeout(() => {
+ el.classList.remove('settings-search-highlight');
+ highlightTimers.delete(el);
+ }, 1800));
return true;
}