From 9efc76bab15f54f3dd2ebb0295e5805d81a3c830 Mon Sep 17 00:00:00 2001 From: gnezim Date: Tue, 21 Apr 2026 12:19:55 +0300 Subject: [PATCH] Auto-commit exact-match typed city/airport names in CityAutocomplete Typing a full city name (or airport name) and clicking search without picking a dropdown row previously did nothing: the parent-held city code stayed empty and the submit handler silently short-circuited. Exact case-insensitive name matches now resolve to the owning city code immediately, so the Schedule and OnlineBoard start pages can act on keyboard-only input. Partial text still requires a dropdown pick. --- .../CityAutocomplete.test.tsx | 54 +++++++++++++++++++ src/ui/city-autocomplete/CityAutocomplete.tsx | 28 ++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/ui/city-autocomplete/CityAutocomplete.test.tsx b/src/ui/city-autocomplete/CityAutocomplete.test.tsx index 51eec265..65f265d2 100644 --- a/src/ui/city-autocomplete/CityAutocomplete.test.tsx +++ b/src/ui/city-autocomplete/CityAutocomplete.test.tsx @@ -150,6 +150,60 @@ describe("CityAutocomplete", () => { expect(container.querySelector(".city-autocomplete__input--has-error")).toBeTruthy(); }); + it("auto-commits a typed city name that exactly matches a dictionary entry", () => { + const onChange = vi.fn(); + render( + , + ); + fireEvent.change(screen.getByTestId("test-input"), { + target: { value: "Москва" }, + }); + expect(onChange).toHaveBeenCalledWith("MOW"); + }); + + it("auto-commits a typed airport name to its owning city code", () => { + const onChange = vi.fn(); + render( + , + ); + fireEvent.change(screen.getByTestId("test-input"), { + target: { value: "Шереметьево" }, + }); + expect(onChange).toHaveBeenCalledWith("MOW"); + }); + + it("does not auto-commit partial typed text", () => { + const onChange = vi.fn(); + render( + , + ); + fireEvent.change(screen.getByTestId("test-input"), { + target: { value: "Мос" }, + }); + expect(onChange).not.toHaveBeenCalled(); + }); + it("does not open popup when dictionaries is null", () => { render( = ({ return () => document.removeEventListener("mousedown", handler); }, []); + // Auto-commit free-typed text that exactly matches a city or airport + // name. Without this, a user who types "Москва" and clicks the search + // button without picking from the dropdown silently fails: the + // parent-held `value` (city code) stays empty while the AutoComplete's + // local input holds the typed string. Exact name match (ru + en) is + // safe — partial text is ignored so the picker still wins for + // disambiguation. + useEffect(() => { + if (!dictionaries || value) return; + if (typeof inputValue !== "string") return; + const trimmed = inputValue.trim(); + if (!trimmed) return; + const lc = trimmed.toLowerCase(); + const cityMatch = dictionaries.cities.find( + (c) => c.name.toLowerCase() === lc, + ); + if (cityMatch) { + onChange(cityMatch.code); + return; + } + const airportMatch = dictionaries.airports.find( + (a) => a.name.toLowerCase() === lc, + ); + if (airportMatch) { + onChange(airportMatch.city_code.toUpperCase()); + } + }, [inputValue, dictionaries, value, onChange]); + const handleComplete = useCallback( (event: AutoCompleteCompleteEvent) => { if (!dictionaries) {