From 5b3fccae9684a136e1b2c39a79ab91fe4b27ba2d Mon Sep 17 00:00:00 2001 From: dev_agent Date: Tue, 6 Oct 2026 12:31:57 -0400 Subject: [PATCH] Keep the settings sub-tabs mounted below 768 px Crossing the 768 px breakpoint used to swap SettingsTabs for the mobile list, unmounting the AI assistant, Household and Recipe search drafts and their leave guard. SettingsPage now keeps SettingsTabs mounted at every width and hides it on mobile, so an unsaved draft survives a resize or rotation and the leave guard still asks before the list navigates away. Tests that render the mobile settings home now use a data router, which the mounted leave guard needs, and the shared settings stub answers the AI models and recipe search reads the hidden sub-tabs make. Co-Authored-By: Claude Opus 5.5 --- web/src/settings/SettingsPage.test.tsx | 12 ++++++--- web/src/settings/SettingsPage.tsx | 12 +++++++-- web/src/settings/SettingsTabs.module.css | 3 ++- web/src/settings/SettingsTabs.test.tsx | 33 ++++++++++++++++++++---- web/src/settings/SettingsTabs.tsx | 5 ++-- web/src/shell/BottomBar.test.tsx | 6 ++--- web/src/test/ApiProviders.tsx | 8 +++++- 7 files changed, 61 insertions(+), 18 deletions(-) diff --git a/web/src/settings/SettingsPage.test.tsx b/web/src/settings/SettingsPage.test.tsx index c520ecd..afb28dd 100644 --- a/web/src/settings/SettingsPage.test.tsx +++ b/web/src/settings/SettingsPage.test.tsx @@ -1,11 +1,12 @@ import { QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen, within } from '@testing-library/react' -import { MemoryRouter, Route, Routes, useLocation } from 'react-router' +import { Route, Routes, useLocation } from 'react-router' import { afterEach, describe, expect, it, vi } from 'vitest' import { ApiProvider, createApiClient, createQueryClient, createVerifiedSession } from '../api/index.ts' import { createNetworkStatus } from '../pwa/networkStatus.ts' import { AI_SETTINGS, HOUSEHOLD_SETTINGS, SETTINGS_API } from '../test/ApiProviders.tsx' import { declaredStyle } from '../test/cssRules.ts' +import { InDataRouter } from '../test/dataRouter.tsx' import { errorResponse, fakeLocation, jsonResponse, stubFetchByPath } from '../test/fetchStub.ts' import { expectMinHitArea } from '../test/hitArea.ts' import { mockViewport } from '../test/matchMedia.ts' @@ -39,7 +40,8 @@ function CurrentPath() { return

{useLocation().pathname}

} -// renderSettings renders the settings home on a 390 px phone (mock 1p); +// renderSettings renders the settings home on a 390 px phone (mock 1p) in a +// data router (the hidden sub-tabs' leave guard needs one); // SettingsTabs.test.tsx covers 768 px and up. function renderSettings() { mockViewport(390) @@ -47,12 +49,12 @@ function renderSettings() { return render( - + } /> } /> - + , ) @@ -95,9 +97,11 @@ describe('SettingsPage', () => { expect(fetchMock.mock.calls.map(([input]) => String(input)).sort()).toEqual([ REGULARS_PATH, '/api/v1/settings/ai', + '/api/v1/settings/ai/models', CALENDAR_PATH, '/api/v1/settings/household', NOTIFICATIONS_PATH, + '/api/v1/settings/recipe-search', ]) }) diff --git a/web/src/settings/SettingsPage.tsx b/web/src/settings/SettingsPage.tsx index 7d5f066..e96881e 100644 --- a/web/src/settings/SettingsPage.tsx +++ b/web/src/settings/SettingsPage.tsx @@ -25,9 +25,17 @@ import styles from './SettingsPage.module.css' import { SettingsTabs } from './SettingsTabs.tsx' // Settings home: the list of settings pages on mobile (mock 1p), one page -// with sub-tabs from 768 px (mock 2e). +// with sub-tabs from 768 px (mock 2e). The sub-tabs stay mounted, hidden, on +// mobile, so an unsaved draft survives the viewport crossing 768 px and its +// leave guard keeps asking before the list navigates away. export function SettingsPage() { - return useBreakpoint() === 'mobile' ? : + const mobile = useBreakpoint() === 'mobile' + return ( + <> + {mobile && } +