fix(combobox): merged input ref so the field is typeable by mouse

The <input> carried only floating-ui's reference ref, so the component's own
inputRef stayed null. The field container's onMouseDown then preventDefault()-ed
every press (its `e.target !== inputRef.current` guard was always true) and the
compensating inputRef.current?.focus() was a no-op — so clicking the box opened
the list but never focused the input, and mouse users could not type to filter.
Tab focus still worked, which is why it read as 'click, can't type'.

Feed a merged callback ref that assigns the node to both floating-ui's
setReference and inputRef. Fixes the Channel, Your tags, Topic, and Language
sidebar filters at once (all use Combobox); the channel list's 50-row cap is now
reachable past 50 via search. Single-select commit no longer re-focuses (that now
actually fires and would re-open the just-closed list). Regression test presses
the field chrome and asserts the input receives focus.

Verified live: clicking the Channel field focuses it and typing 'cold' narrows
the list to ColdFusion / wehatethecold.
This commit is contained in:
2026-08-22 03:22:47 +02:00
parent cda70164ba
commit d2193ccd1b
2 changed files with 33 additions and 4 deletions
@@ -64,6 +64,19 @@ describe("Combobox", () => {
expect(document.getElementById(controls!)?.getAttribute("role")).toBe("listbox");
});
it("focuses the input when the field chrome is pressed (merged ref, not a dead inputRef)", () => {
// Regression: the input carried only floating-ui's ref, so `inputRef` stayed null. The field's
// onMouseDown then preventDefault()-ed every press (its `e.target !== inputRef.current` guard was
// always true) and its compensating inputRef.current?.focus() was a no-op → the box couldn't be
// typed into with the mouse. A merged ref feeds the node to both, so pressing the chrome focuses it.
wrap(<Combobox label="Genre" options={OPTIONS} selected={[]} onChange={vi.fn()} />);
const input = screen.getByRole("combobox");
const field = input.parentElement as HTMLElement;
expect(document.activeElement).not.toBe(input);
fireEvent.mouseDown(field); // press the field padding, not the input itself
expect(document.activeElement).toBe(input);
});
it("keeps the option list unmounted until the field is focused", () => {
wrap(<Combobox label="Genre" options={OPTIONS} selected={[]} onChange={vi.fn()} />);
expect(screen.queryByRole("option", { hidden: true })).toBeNull();