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; }