From 8ca83137a1d5fcbd93798ffaafd18f70bc0cc009 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Thu, 25 Jun 2026 09:16:26 +0700 Subject: [PATCH] fix(detail-navigation): re-seed the sheet search mirror from the controller on open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DetailNavigationSheet keeps a local input mirror (localQuery) seeded once from controller.searchQuery — a deliberate perf decouple (92a9e59) so typing in the sheet doesn't re-render the whole detail page. But in the controller's documented controlled mode (a page-level search box that owns searchQuery), an external change made while the sheet was closed left the mirror stale: reopening the sheet showed the old text. Re-seed the mirror from the controller on each open, folded into the existing open-transition reconciliation. The sheet is modal, so searchQuery can only change externally while closed — on-open re-seeding suffices and never clobbers in-progress typing. Add a controlled-mode test that reproduces the stale input (red before, green after). Co-Authored-By: Claude Opus 4.8 --- .../detail-navigation-sheet.test.tsx | 62 +++++++++++++++++++ .../detail-navigation-sheet.tsx | 8 +++ 2 files changed, 70 insertions(+) diff --git a/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.test.tsx b/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.test.tsx index 0c176f80..a19fc29a 100644 --- a/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.test.tsx +++ b/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.test.tsx @@ -505,3 +505,65 @@ describe('DetailNavigationSheet — virtualization (>100 items)', () => { }); }); }); + +// Controlled mode (the documented "page-level search box" use case): the parent +// owns searchQuery, so the sheet's local input mirror must re-seed from the +// controller when it changes outside the sheet. +const ControlledSheet = ({ open, query }: { open: boolean; query: string }) => { + const nav = useDetailNavigation({ + currentId: 'c', + getHref, + getLabel, + getSearchableText, + items: ITEMS, + onOpenChange: () => {}, + onSearchQueryChange: () => {}, + open, + searchDebounceMs: 0, + searchQuery: query, + }); + + return ( + + controller={nav} + sheetTitle="Items" + /> + ); +}; + +const renderControlled = (props: { open: boolean; query: string }) => + render(, { + wrapper: ({ children }: { children: ReactNode }) => ( + + + + {children}} + path="/items/:id" + /> + + + + ), + }); + +describe('DetailNavigationSheet — controlled search resync', () => { + it('reflects an external searchQuery change made while the sheet was closed', () => { + const { rerender } = renderControlled({ open: false, query: '' }); + + rerender( + , + ); + rerender( + , + ); + + expect(screen.getByRole('textbox').value).toBe('Bravo'); + }); +}); diff --git a/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.tsx b/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.tsx index b8df16fa..6fa7e647 100644 --- a/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.tsx +++ b/frontend/src/components/shared/detail-navigation/detail-navigation-sheet.tsx @@ -140,6 +140,14 @@ export function DetailNavigationSheet({ if (lastOpen !== open) { setLastOpen(open); + + // Re-seed the input mirror from the controller on open so a controlled + // searchQuery changed from outside isn't shown stale. The sheet is modal, + // so searchQuery only changes externally while closed — on-open re-seed + // suffices and never clobbers in-progress typing. + if (open) { + setLocalQuery(searchQuery); + } } if (desiredFocusId !== focusedId) {