fix: open each settings page at its top and keep the place in the phone's page list

This commit is contained in:
Linus Rath
2026-11-03 05:06:26 +01:00
parent d9b9c97b58
commit 308178beda
2 changed files with 38 additions and 4 deletions
@@ -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(<SettingsApp linkSegments={[]} />);
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);
});
});
+17 -3
View File
@@ -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 =
<PhoneHeaderActions />
</div>
<AppMain embedded={isEmbedded} label={activeTabLabel} className="flex-1 overflow-y-auto p-4">
{/* A key per page: a page opens at its top, not at the scroll offset of the one before. */}
<AppMain key={effectiveActiveTab} embedded={isEmbedded} label={activeTabLabel} className="flex-1 overflow-y-auto p-4">
{renderTabContent()}
</AppMain>
@@ -952,7 +958,15 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
<PhoneHeaderActions />
</div>
<AppMain embedded={isEmbedded} label={t('title')} className="flex-1 overflow-y-auto" data-tour="settings-tabs">
<AppMain
key="settings-page-list"
ref={attachMobileList}
onScroll={(e) => { mobileListScrollTop.current = e.currentTarget.scrollTop; }}
embedded={isEmbedded}
label={t('title')}
className="flex-1 overflow-y-auto"
data-tour="settings-tabs"
>
<div className="px-4 pt-3 pb-1">
<ToolbarSearch
value={searchQuery}
@@ -1047,7 +1061,7 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
// Desktop layout
const settingsMain = (
<AppMain ref={attachContentScroll as (el: HTMLElement | null) => void} embedded={isEmbedded} label={activeTabLabel} className="flex-1 overflow-y-auto">
<AppMain key={effectiveActiveTab} ref={attachContentScroll as (el: HTMLElement | null) => void} embedded={isEmbedded} label={activeTabLabel} className="flex-1 overflow-y-auto">
<div className={effectiveActiveTab.startsWith(PLUGIN_PAGE_PREFIX) ? "px-6 py-6" : "max-w-3xl mx-auto px-6 py-6"}>
{/* The page's heading for screen readers; the sidebar shows which
page is open (the phone header has a visible one). */}