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} 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);