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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user