From 308178beda19f92737f098095e161ca6d1658560 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 3 Nov 2026 05:06:26 +0100 Subject: [PATCH] fix: open each settings page at its top and keep the place in the phone's page list --- .../settings-phone-deep-link.test.tsx | 22 ++++++++++++++++++- components/settings/settings-app.tsx | 20 ++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/components/settings/__tests__/settings-phone-deep-link.test.tsx b/components/settings/__tests__/settings-phone-deep-link.test.tsx index b65c4b437..2b0378e09 100644 --- a/components/settings/__tests__/settings-phone-deep-link.test.tsx +++ b/components/settings/__tests__/settings-phone-deep-link.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { act, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // On the phone the tab list is a screen of its own. A link to @@ -86,4 +86,24 @@ describe('Settings deep links on the phone', () => { expect(screen.getByTestId('settings-tab-appearance')).toBeTruthy(); expect(screen.queryByText('appearance pane')).toBeNull(); }); + + it('opens a page at its top and keeps the place in the list', () => { + window.history.replaceState(null, '', '/settings'); + render(); + const list = document.querySelector('main') as HTMLElement; + list.scrollTop = 400; + fireEvent.scroll(list); + + fireEvent.click(screen.getByTestId('settings-tab-organize')); + expect(screen.getByText('organize pane')).toBeTruthy(); + const page = document.querySelector('main') as HTMLElement; + expect(page.scrollTop).toBe(0); + + page.scrollTop = 900; + act(() => { + window.dispatchEvent(new PopStateEvent('popstate')); + }); + expect(screen.getByTestId('settings-tab-organize')).toBeTruthy(); + expect((document.querySelector('main') as HTMLElement).scrollTop).toBe(400); + }); }); diff --git a/components/settings/settings-app.tsx b/components/settings/settings-app.tsx index 93a658443..5ff290c4f 100644 --- a/components/settings/settings-app.tsx +++ b/components/settings/settings-app.tsx @@ -411,6 +411,11 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps = }); const [isResizing, setIsResizing] = useState(false); const dragStartWidth = useRef(256); + // The phone's page list keeps its place while a page is open on top of it. + const mobileListScrollTop = useRef(0); + const attachMobileList = useCallback((el: HTMLElement | null) => { + if (el) el.scrollTop = mobileListScrollTop.current; + }, []); // Check auth on mount – skip when already authenticated so that navigating // between routes doesn't retrigger checkAuth's transient `{ client: null, @@ -920,7 +925,8 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps = - + {/* A key per page: a page opens at its top, not at the scroll offset of the one before. */} + {renderTabContent()} @@ -952,7 +958,15 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps = - + { mobileListScrollTop.current = e.currentTarget.scrollTop; }} + embedded={isEmbedded} + label={t('title')} + className="flex-1 overflow-y-auto" + data-tour="settings-tabs" + >
void} embedded={isEmbedded} label={activeTabLabel} className="flex-1 overflow-y-auto"> + void} embedded={isEmbedded} label={activeTabLabel} className="flex-1 overflow-y-auto">
{/* The page's heading for screen readers; the sidebar shows which page is open (the phone header has a visible one). */}