feat: flash the settings section a jump-row button scrolls to

This commit is contained in:
Linus Rath
2026-10-28 20:05:13 +01:00
parent 1c8579fdbc
commit 9aa132c02f
2 changed files with 33 additions and 1 deletions
@@ -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(<SettingsApp linkSegments={['organize']} />);
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(<SettingsApp linkSegments={[]} />);
act(() => { window.dispatchEvent(new CustomEvent('settings-tab-change', { detail: 'identities' })); });
+15 -1
View File
@@ -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<HTMLElement, ReturnType<typeof setTimeout>>();
/**
* 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<HTMLElement>(`[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;
}