From 9cd6d18d63f87fc980fea2d5fca405716432f2b5 Mon Sep 17 00:00:00 2001 From: Amund Eggen Svandal Date: Sat, 16 May 2026 19:44:59 +0200 Subject: [PATCH 1/2] fix: Show "Search for X" option when reopening UserSearch with text MUI's Autocomplete passes `inputValue: ''` to `filterOptions` when the popup reopens with the input value matching the previously selected option's label. That suppressed the free-text "Search for X" entry until the user typed a character to flip the pristine flag. Control `inputValue` ourselves and forward it to `filterOptions` so the free-text option is always shown when there's text in the input. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/UserSearch.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/UserSearch.tsx b/src/components/UserSearch.tsx index 1c946945..49e76564 100644 --- a/src/components/UserSearch.tsx +++ b/src/components/UserSearch.tsx @@ -298,9 +298,10 @@ export const UserSearch: React.FC = ({ const { uuids, filterOptions, renderOption, getOptionLabel, isOptionEqualToValue } = useUserSearchOptions(); const [loading, setLoading] = React.useState(false); + const [inputValue, setInputValue] = React.useState(""); return ( - options={uuids.map((uuid) => ({ type: "uuid" as const, uuid }))} fullWidth size={size} @@ -308,7 +309,18 @@ export const UserSearch: React.FC = ({ blurOnSelect selectOnFocus autoHighlight - filterOptions={filterOptions} + inputValue={inputValue} + onInputChange={(_, newInputValue) => { + setInputValue(newInputValue); + }} + // Always pass our controlled inputValue to filterOptions so the + // "Search for X" option appears when reopening with text still in + // the input. MUI otherwise passes inputValue: '' when the popup + // reopens and the input matches the previously selected option's + // label, which suppresses the free-text option. + filterOptions={(options, params) => + filterOptions?.(options, { ...params, inputValue }) ?? options + } renderOption={renderOption} getOptionLabel={getOptionLabel} isOptionEqualToValue={isOptionEqualToValue} From 50d030faec2fb3bdeaf94bb11be0938212051af6 Mon Sep 17 00:00:00 2001 From: Amund Eggen Svandal Date: Sat, 16 May 2026 19:55:57 +0200 Subject: [PATCH 2/2] test: UserSearch shows free-text option after reopen Asserts that the "Search for X" option appears when the search popup is reopened after a previous selection has filled the input with that option's label. Without the fix in the preceding commit, MUI's empty-string trick passes inputValue: '' to filterOptions on reopen and suppresses the free-text branch until the user edits the input. Verified the test fails on HEAD~1. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/routes/session/-uuid.ui.test.tsx | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/routes/session/-uuid.ui.test.tsx b/src/routes/session/-uuid.ui.test.tsx index af9ffe54..184f4d9a 100644 --- a/src/routes/session/-uuid.ui.test.tsx +++ b/src/routes/session/-uuid.ui.test.tsx @@ -63,6 +63,38 @@ describe("Session detail page", () => { .toBeInTheDocument(); }); + mswTest( + "search shows free-text option after reopening with previously selected text", + async () => { + const { screen } = await renderAppRoute(sessionUrl); + + const searchInput = screen.getByRole("combobox", { + name: "Search players", + }); + + // Wait for the page to populate known aliases for the current player + await expect + .poll(() => localStorage.getItem("knownAliases")) + .toContain(USERS.player1.username); + + // Type the player's name and select the auto-highlighted user + // option with Enter. The selection sets MUI's internal value and + // fills the input with the username. Since we navigate to the + // same route, the search component stays mounted afterwards. + await searchInput.fill(USERS.player1.username); + await userEvent.keyboard("{Enter}"); + + // Reopen the popup. The input still holds "PlayerOne" from the + // selection — the free-text "Search for ..." option must still + // appear instead of being suppressed by MUI's empty-string trick. + await userEvent.click(searchInput); + + await expect + .element(screen.getByRole("option", { name: /Search for/ })) + .toBeInTheDocument(); + }, + ); + mswTest("renders player head image", async () => { await renderAppRoute(sessionUrl);