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.
This commit is contained in:
2026-04-21 12:19:55 +03:00
parent 3ae59dae1d
commit 9efc76bab1
2 changed files with 82 additions and 0 deletions
@@ -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(
<CityAutocomplete
label="City"
placeholder="Pick"
value=""
onChange={onChange}
dictionaries={dictionaries}
testIdPrefix="test"
/>,
);
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(
<CityAutocomplete
label="City"
placeholder="Pick"
value=""
onChange={onChange}
dictionaries={dictionaries}
testIdPrefix="test"
/>,
);
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(
<CityAutocomplete
label="City"
placeholder="Pick"
value=""
onChange={onChange}
dictionaries={dictionaries}
testIdPrefix="test"
/>,
);
fireEvent.change(screen.getByTestId("test-input"), {
target: { value: "Мос" },
});
expect(onChange).not.toHaveBeenCalled();
});
it("does not open popup when dictionaries is null", () => {
render(
<CityAutocomplete
@@ -66,6 +66,34 @@ export const CityAutocomplete: FC<CityAutocompleteProps> = ({
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) {