refactor(lists): share the remembered lists sub-tab between desktop and phone

useListsSubTab remembers whether the packing or the to-do sub tab was
open, for ListsContainer and MTripShell. The desktop restores any stored
value, the phone restores todo and otherwise falls back to packing.
This commit is contained in:
Maurice
2026-10-08 20:47:57 +00:00
parent 71289ba43e
commit 5a0e03929d
4 changed files with 79 additions and 14 deletions
+49
View File
@@ -0,0 +1,49 @@
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { useListsSubTab } from './useListsSubTab';
// FE-HOOK-LISTSTAB-001 to FE-HOOK-LISTSTAB-006
describe('useListsSubTab', () => {
it('FE-HOOK-LISTSTAB-001: opens on packing when nothing is stored for the trip', () => {
const { result } = renderHook(() => useListsSubTab(7));
expect(result.current[0]).toBe('packing');
});
it('FE-HOOK-LISTSTAB-002: reopens the sub-tab stored for that trip, not another one', () => {
sessionStorage.setItem('trip-lists-subtab-7', 'todo');
sessionStorage.setItem('trip-lists-subtab-8', 'packing');
expect(renderHook(() => useListsSubTab(7)).result.current[0]).toBe('todo');
expect(renderHook(() => useListsSubTab(8)).result.current[0]).toBe('packing');
expect(renderHook(() => useListsSubTab(7, { onlyKnownTabs: true })).result.current[0]).toBe('todo');
});
it('FE-HOOK-LISTSTAB-003: the desktop takes whatever is stored, the phone only a tab it knows', () => {
sessionStorage.setItem('trip-lists-subtab-7', 'files');
expect(renderHook(() => useListsSubTab(7)).result.current[0]).toBe('files');
expect(renderHook(() => useListsSubTab(7, { onlyKnownTabs: true })).result.current[0]).toBe('packing');
});
it('FE-HOOK-LISTSTAB-004: an empty stored value falls back to packing on both', () => {
sessionStorage.setItem('trip-lists-subtab-7', '');
expect(renderHook(() => useListsSubTab(7)).result.current[0]).toBe('packing');
expect(renderHook(() => useListsSubTab(7, { onlyKnownTabs: true })).result.current[0]).toBe('packing');
});
it('FE-HOOK-LISTSTAB-005: switching shows the new tab and stores it under the trip key', () => {
const { result } = renderHook(() => useListsSubTab(7, { onlyKnownTabs: true }));
act(() => result.current[1]('todo'));
expect(result.current[0]).toBe('todo');
expect(sessionStorage.getItem('trip-lists-subtab-7')).toBe('todo');
act(() => result.current[1]('packing'));
expect(result.current[0]).toBe('packing');
expect(sessionStorage.getItem('trip-lists-subtab-7')).toBe('packing');
});
it('FE-HOOK-LISTSTAB-006: a choice made on one surface is what the other opens with', () => {
const desktop = renderHook(() => useListsSubTab(3));
act(() => desktop.result.current[1]('todo'));
expect(renderHook(() => useListsSubTab(3, { onlyKnownTabs: true })).result.current[0]).toBe('todo');
});
});
+25
View File
@@ -0,0 +1,25 @@
import { useState } from 'react';
/** The two lists the Lists tab switches between. */
export type ListsSubTab = 'packing' | 'todo';
const storageKey = (tripId: number) => `trip-lists-subtab-${tripId}`;
/**
* The Lists tab's sub-tab, remembered per trip for the browser session. Both the
* desktop container and the phone shell read and write the same key. The phone
* shell only takes a stored value it knows (`onlyKnownTabs`); the desktop one
* takes whatever is stored, as it always has.
*/
export function useListsSubTab(tripId: number, { onlyKnownTabs = false }: { onlyKnownTabs?: boolean } = {}) {
const [subTab, setSubTabState] = useState<ListsSubTab>(() => {
const saved = sessionStorage.getItem(storageKey(tripId));
if (onlyKnownTabs) return saved === 'todo' ? 'todo' : 'packing';
return (saved as ListsSubTab) || 'packing';
});
const setSubTab = (tab: ListsSubTab) => {
setSubTabState(tab);
sessionStorage.setItem(storageKey(tripId), tab);
};
return [subTab, setSubTab] as const;
}
+3 -10
View File
@@ -21,6 +21,7 @@ import { badgeLabel, distanceBadge } from './roadtrip/stageBadges'
import type { CorridorReach } from '../../../components/Roadtrip/corridorSearchModel'
import { useSettingsStore } from '../../../store/settingsStore'
import { useAuthStore } from '../../../store/authStore'
import { useListsSubTab, type ListsSubTab } from '../../../hooks/useListsSubTab'
import { canManageDocSync } from '../../../components/Files/docsync/useDocSync'
import { useDocSyncOffered } from '../../../components/Files/docsync/useDocSyncOffered'
import type { Day, Trip } from '../../../types'
@@ -46,7 +47,7 @@ export type MTripView = 'plan' | 'map'
export type MTripMode = 'go' | 'edit' | 'browse'
/** The road trip tab's own two halves: the chain, or the same map showing the stage. */
export type MRtView = 'list' | 'map'
export type MTripListsTab = 'packing' | 'todo'
export type MTripListsTab = ListsSubTab
export type MTripCollabTab = 'chat' | 'notes' | 'links' | 'polls'
/**
@@ -221,10 +222,7 @@ export default function MTripShell({
const [mode, setMode] = useState<MTripMode>('go')
const [browseFromEdit, setBrowseFromEdit] = useState(false)
const [sheet, setSheet] = useState<MTripSheetState | null>(null)
const [listsTab, setListsTabState] = useState<MTripListsTab>(() => {
const saved = sessionStorage.getItem(`trip-lists-subtab-${tripId}`)
return saved === 'todo' ? 'todo' : 'packing'
})
const [listsTab, setListsTab] = useListsSubTab(tripId, { onlyKnownTabs: true })
const [collabTab, setCollabTab] = useState<MTripCollabTab>('chat')
const [transportsCompact, setTransportsCompact] = useState(false)
const [bookingsCompact, setBookingsCompact] = useState(false)
@@ -386,11 +384,6 @@ export default function MTripShell({
const mapFront = (trTab === 'plan' && view === 'map') || (trTab === 'roadtrip' && rtView === 'map')
const setListsTab = (tab: MTripListsTab) => {
setListsTabState(tab)
sessionStorage.setItem(`trip-lists-subtab-${tripId}`, tab)
}
const openSheet = (id: string, payload?: unknown) => setSheet({ id, payload })
const closeSheet = () => setSheet(null)
@@ -6,15 +6,13 @@ import { useTranslation } from '../../i18n'
import { useAuthStore } from '../../store/authStore'
import { useTripStore } from '../../store/tripStore'
import { useCanDo } from '../../store/permissionsStore'
import { useListsSubTab } from '../../hooks/useListsSubTab'
import type { PackingItem, TodoItem } from '../../types'
import { PackingListPanel, TodoListPanel } from './plannerLazy'
import { LazyPanel } from './LazyPanel'
export function ListsContainer({ tripId, packingItems, todoItems }: Readonly<{ tripId: number; packingItems: PackingItem[]; todoItems: TodoItem[] }>) {
const [subTab, setSubTab] = useState<'packing' | 'todo'>(() => {
return (sessionStorage.getItem(`trip-lists-subtab-${tripId}`) as 'packing' | 'todo') || 'packing'
})
const setSubTabPersist = (tab: 'packing' | 'todo') => { setSubTab(tab); sessionStorage.setItem(`trip-lists-subtab-${tripId}`, tab) }
const [subTab, setSubTabPersist] = useListsSubTab(tripId)
const [importPackingSignal, setImportPackingSignal] = useState(0)
const [addCategorySignal, setAddCategorySignal] = useState(0)
const [saveTemplateSignal, setSaveTemplateSignal] = useState(0)