diff --git a/src/features/flights-map/components/FlightsMapFilter.test.tsx b/src/features/flights-map/components/FlightsMapFilter.test.tsx
index 784fc3eb..0fc955d3 100644
--- a/src/features/flights-map/components/FlightsMapFilter.test.tsx
+++ b/src/features/flights-map/components/FlightsMapFilter.test.tsx
@@ -24,6 +24,9 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
placeholder={props["placeholder"] as string}
/>
),
+ SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
+
+ ),
}));
vi.mock("@/shared/dictionaries/index.js", () => ({
diff --git a/src/features/flights-map/components/FlightsMapFilter.tsx b/src/features/flights-map/components/FlightsMapFilter.tsx
index 570934ee..2f7b60a5 100644
--- a/src/features/flights-map/components/FlightsMapFilter.tsx
+++ b/src/features/flights-map/components/FlightsMapFilter.tsx
@@ -11,7 +11,7 @@ import { type FC, useCallback, useEffect, useMemo, type FormEvent } from "react"
import { Calendar } from "primereact/calendar";
import { useTranslation } from "@/i18n/provider.js";
import { useLocale } from "@/i18n/useLocale.js";
-import { CityAutocomplete } from "@/ui/city-autocomplete/index.js";
+import { CityAutocomplete, SwapCityButton } from "@/ui/city-autocomplete/index.js";
import { DayQuickPick } from "@/ui/calendar/DayQuickPick.js";
import { useDictionaries, findCityByCoord } from "@/shared/dictionaries/index.js";
import {
@@ -185,33 +185,11 @@ export const FlightsMapFilter: FC = ({
testIdPrefix="fm-departure"
/>
-
+ ariaLabel={t("SHARED.CITY_CHANGE")}
+ testId="fm-exchange-btn"
+ />
({
defaultValue={(props["value"] as string) ?? ""}
/>
),
+ SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
+
+ ),
}));
// ---------------------------------------------------------------------------
diff --git a/src/features/online-board/components/OnlineBoardFilter.tsx b/src/features/online-board/components/OnlineBoardFilter.tsx
index a32c8d60..1fdb4821 100644
--- a/src/features/online-board/components/OnlineBoardFilter.tsx
+++ b/src/features/online-board/components/OnlineBoardFilter.tsx
@@ -14,7 +14,7 @@ import { useLocale } from "@/i18n/useLocale.js";
import { Calendar } from "primereact/calendar";
import { Slider, type SliderChangeEvent } from "primereact/slider";
import { useTranslation } from "@/i18n/provider.js";
-import { CityAutocomplete } from "@/ui/city-autocomplete/index.js";
+import { CityAutocomplete, SwapCityButton } from "@/ui/city-autocomplete/index.js";
import { DayQuickPick } from "@/ui/calendar/DayQuickPick.js";
import { useDictionaries } from "@/shared/dictionaries/index.js";
import { buildOnlineBoardUrl } from "../url.js";
@@ -536,18 +536,10 @@ export const OnlineBoardFilter: FC = ({
testIdPrefix="route-departure"
/>
-
-
-
+
({
defaultValue={(props["value"] as string) ?? ""}
/>
),
+ SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
+
+ ),
}));
// ---------------------------------------------------------------------------
diff --git a/src/features/schedule/components/ScheduleFilter.scss b/src/features/schedule/components/ScheduleFilter.scss
index db4d9454..53e9d1a2 100644
--- a/src/features/schedule/components/ScheduleFilter.scss
+++ b/src/features/schedule/components/ScheduleFilter.scss
@@ -41,27 +41,8 @@
width: 100%;
}
- &__swap {
- display: flex;
- justify-content: center;
- padding: vars.$space-s 0;
- }
-
- &__swap-btn {
- background: transparent;
- border: 0;
- padding: 4px;
- cursor: pointer;
- color: colors.$blue;
- border-radius: vars.$border-radius;
-
- &:hover { background: colors.$blue-extra-light; }
-
- .svg--change-city {
- width: 24px;
- height: 24px;
- }
- }
+ // Swap-cities button moved to the shared `SwapCityButton` component —
+ // classes `.change-container` + `.button-change` are now global.
&__checkbox {
display: flex;
diff --git a/src/features/schedule/components/ScheduleFilter.test.tsx b/src/features/schedule/components/ScheduleFilter.test.tsx
index aee49cd0..dea5a71b 100644
--- a/src/features/schedule/components/ScheduleFilter.test.tsx
+++ b/src/features/schedule/components/ScheduleFilter.test.tsx
@@ -70,6 +70,9 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
defaultValue={(props["value"] as string) ?? ""}
/>
),
+ SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
+
+ ),
}));
vi.mock("@/shared/dateWindow.js", () => ({
diff --git a/src/features/schedule/components/ScheduleFilter.tsx b/src/features/schedule/components/ScheduleFilter.tsx
index 702ca3b2..4ded6e81 100644
--- a/src/features/schedule/components/ScheduleFilter.tsx
+++ b/src/features/schedule/components/ScheduleFilter.tsx
@@ -14,7 +14,7 @@ import { Calendar } from "primereact/calendar";
import { Slider, type SliderChangeEvent } from "primereact/slider";
import { useTranslation } from "@/i18n/provider.js";
import { useLocale } from "@/i18n/useLocale.js";
-import { CityAutocomplete } from "@/ui/city-autocomplete/index.js";
+import { CityAutocomplete, SwapCityButton } from "@/ui/city-autocomplete/index.js";
import { useDictionaries } from "@/shared/dictionaries/index.js";
import { buildScheduleUrl } from "../url.js";
import type { ScheduleParams } from "../url.js";
@@ -315,19 +315,10 @@ export const ScheduleFilter: FC = ({
testIdPrefix="schedule-departure"
/>
-
-
-
+
({
defaultValue={(props["value"] as string) ?? ""}
/>
),
+ SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
+
+ ),
}));
vi.mock("@/ui/layout/SearchHistory.js", () => ({
diff --git a/src/ui/city-autocomplete/CityAutocomplete.scss b/src/ui/city-autocomplete/CityAutocomplete.scss
index 05399592..393072f4 100644
--- a/src/ui/city-autocomplete/CityAutocomplete.scss
+++ b/src/ui/city-autocomplete/CityAutocomplete.scss
@@ -12,7 +12,9 @@
align-items: center;
justify-content: space-between;
width: 100%;
- margin-bottom: vars.$space-s2;
+ // Matches Angular `city-autocomplete__labels-container { margin-bottom: $space-m }`
+ // in `components/city-autocomplete/city-autocomplete.component.scss`.
+ margin-bottom: vars.$space-m;
}
&__label {
diff --git a/src/ui/city-autocomplete/SwapCityButton.scss b/src/ui/city-autocomplete/SwapCityButton.scss
new file mode 100644
index 00000000..1bbcd540
--- /dev/null
+++ b/src/ui/city-autocomplete/SwapCityButton.scss
@@ -0,0 +1,30 @@
+@use "../../styles/colors" as colors;
+@use "../../styles/variables" as vars;
+
+// Shared swap-cities button styling for every filter block. Mirrors Angular's
+// `.change-container .button-change` in `online-board-route-filter.scss`
+// byte-for-byte so the three filter sidebars render an identical 35×40 pill.
+.change-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+
+ .button-change {
+ margin-top: vars.$space-m;
+ margin-bottom: -(vars.$space-m);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: vars.$medium-button-height;
+ width: 35px;
+ background-color: colors.$white;
+ border: none;
+ box-shadow: none;
+ cursor: pointer;
+
+ &:hover {
+ background-color: colors.$white;
+ }
+ }
+}
diff --git a/src/ui/city-autocomplete/SwapCityButton.tsx b/src/ui/city-autocomplete/SwapCityButton.tsx
new file mode 100644
index 00000000..848957a4
--- /dev/null
+++ b/src/ui/city-autocomplete/SwapCityButton.tsx
@@ -0,0 +1,43 @@
+import "./SwapCityButton.scss";
+
+/**
+ * Swap-cities button used between the departure and arrival CityAutocomplete
+ * inputs in every filter (OnlineBoard route, Schedule, FlightsMap).
+ *
+ * The DOM shape mirrors Angular's `.change-container > .button-change >
+ * .svg--change-city` so the global `.svg--change-city` rule in
+ * `styles/_icons.scss` renders the sprite identically on every page.
+ * Each caller keeps its own onClick handler — swap semantics differ between
+ * pages (Schedule clears validation, FlightsMap routes through a state
+ * service, OnlineBoard just swaps values), so only the DOM/CSS is shared.
+ */
+
+export interface SwapCityButtonProps {
+ onClick: () => void;
+ /** data-testid for the inner