fix(detail-navigation): re-seed the sheet search mirror from the controller on open

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 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-06-25 09:16:26 +07:00
co-authored by Claude Opus 4.8
parent 47c1ea108b
commit 8ca83137a1
2 changed files with 70 additions and 0 deletions
@@ -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<Item>({
currentId: 'c',
getHref,
getLabel,
getSearchableText,
items: ITEMS,
onOpenChange: () => {},
onSearchQueryChange: () => {},
open,
searchDebounceMs: 0,
searchQuery: query,
});
return (
<DetailNavigationSheet<Item>
controller={nav}
sheetTitle="Items"
/>
);
};
const renderControlled = (props: { open: boolean; query: string }) =>
render(<ControlledSheet {...props} />, {
wrapper: ({ children }: { children: ReactNode }) => (
<MemoryRouter initialEntries={['/items/c']}>
<TooltipProvider>
<Routes>
<Route
element={<>{children}</>}
path="/items/:id"
/>
</Routes>
</TooltipProvider>
</MemoryRouter>
),
});
describe('DetailNavigationSheet — controlled search resync', () => {
it('reflects an external searchQuery change made while the sheet was closed', () => {
const { rerender } = renderControlled({ open: false, query: '' });
rerender(
<ControlledSheet
open={false}
query="Bravo"
/>,
);
rerender(
<ControlledSheet
open
query="Bravo"
/>,
);
expect(screen.getByRole<HTMLInputElement>('textbox').value).toBe('Bravo');
});
});
@@ -140,6 +140,14 @@ export function DetailNavigationSheet<T extends { id: string }>({
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) {