fix(ui): keep a deleted-from search query instead of blanking the box

The paginated search select diffs the input against the selected option's
label to recover what the user just typed, so a query started from a picked
value searches for the new text rather than the label plus the new text.
That diff can only express an insertion: it walks a common prefix and a
common suffix and returns what sits between them. A deletion leaves nothing
between them, so every deletion-only edit returned the empty string.

Backspacing once in a field showing a selected label therefore threw the
edit away. The empty result was stored as the query, the controlled input
re-rendered blank, and the server was asked for the unfiltered page instead
of the text the user left in the box.

An edit that yields no insertion but did change the value is a deletion, and
there the remaining text is the query the user means. Insertions and
whole-selection replacements are untouched.
This commit is contained in:
Yuneng Jiang 2026-08-29 15:52:48 -07:00
parent 36ea28b092
commit 02046fb7b7
No known key found for this signature in database
2 changed files with 34 additions and 1 deletions

View file

@ -361,4 +361,32 @@ describe("PaginatedSearchSelect", () => {
await user.click(screen.getByRole("combobox"));
expect(await screen.findByTestId("paginated-search-select-loading-more")).toBeInTheDocument();
});
it("queries the trimmed label when a character is deleted from the end of the selection", async () => {
const user = userEvent.setup();
const onSearchChange = vi.fn();
renderSelect({ onSearchChange, value: "alias-alpha" });
const input = screen.getByRole("combobox") as HTMLInputElement;
input.focus();
input.setSelectionRange(input.value.length, input.value.length);
await user.keyboard("{Backspace}");
expect(input).toHaveValue("alias-alph");
await waitFor(() => expect(onSearchChange).toHaveBeenLastCalledWith("alias-alph"));
});
it("queries what is left when a character is deleted from inside the selection", async () => {
const user = userEvent.setup();
const onSearchChange = vi.fn();
renderSelect({ onSearchChange, value: "alias-alpha" });
const input = screen.getByRole("combobox") as HTMLInputElement;
input.focus();
input.setSelectionRange(6, 6);
await user.keyboard("{Backspace}");
expect(input).toHaveValue("aliasalpha");
await waitFor(() => expect(onSearchChange).toHaveBeenLastCalledWith("aliasalpha"));
});
});

View file

@ -50,6 +50,11 @@ const typedInsertion = (previous: string, next: string): string => {
return next.slice(start, next.length - end);
};
const editedQuery = (label: string, next: string): string => {
const inserted = typedInsertion(label, next);
return inserted === "" && next !== label ? next : inserted;
};
export function PaginatedSearchSelect({
options,
value,
@ -101,7 +106,7 @@ export function PaginatedSearchSelect({
const replacedWholeInput = wholeSelectionRef.current;
wholeSelectionRef.current = false;
handleInputValueChange(
typedQuery === null && !replacedWholeInput ? typedInsertion(selected?.label ?? "", next) : next,
typedQuery === null && !replacedWholeInput ? editedQuery(selected?.label ?? "", next) : next,
reason,
);
};