Extract SwapCityButton so all 3 filter blocks share the same DOM
OnlineBoard, Schedule and FlightsMap each inlined their own swap-cities wrapper — three different class names and, in FlightsMap's case, a different inline SVG. Angular keeps logic separate per filter (Schedule/FlightsMap clear validation on swap, OnlineBoard doesn't) but its DOM is identical across the three. Mirror that: ship a shared <SwapCityButton> that owns the `.change-container > .button-change > .svg--change-city` markup and CSS, keep each caller's onClick local. Also align filter visuals: FlightsMapFilter row gap $space-m → $space-l to match OnlineBoard/Schedule, and CityAutocomplete label gutter $space-s2 → $space-m to match Angular's city-autocomplete.component.scss.
This commit is contained in:
@@ -24,6 +24,9 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
placeholder={props["placeholder"] as string}
|
||||
/>
|
||||
),
|
||||
SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
|
||||
<button type="button" data-testid={props.testId} onClick={props.onClick} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/dictionaries/index.js", () => ({
|
||||
|
||||
@@ -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<FlightsMapFilterProps> = ({
|
||||
testIdPrefix="fm-departure"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flights-map-filter__exchange"
|
||||
<SwapCityButton
|
||||
onClick={handleExchange}
|
||||
aria-label={t("SHARED.CITY_CHANGE")}
|
||||
data-testid="fm-exchange-btn"
|
||||
>
|
||||
<svg
|
||||
className="flights-map-filter__exchange-icon"
|
||||
width="12"
|
||||
height="25"
|
||||
viewBox="0 0 12 25"
|
||||
fill="#1b62b4"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 21.25H12L9.5 24.25L7 21.25H9V3.25H10V21.25Z"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 3.25H0L2.5 0.25L5 3.25H3V21.25H2V3.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
ariaLabel={t("SHARED.CITY_CHANGE")}
|
||||
testId="fm-exchange-btn"
|
||||
/>
|
||||
|
||||
<CityAutocomplete
|
||||
label={t("SHARED.ARRIVAL_CITY")}
|
||||
|
||||
@@ -169,7 +169,9 @@
|
||||
padding: vars.$space-xl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: vars.$space-m;
|
||||
// Match OnlineBoard + Schedule filter row gap ($space-l = 15px) — Angular's
|
||||
// `.filter-content` uses $space-l everywhere.
|
||||
gap: vars.$space-l;
|
||||
|
||||
.flights-map-filter-header {
|
||||
padding: vars.$space-m 0;
|
||||
@@ -216,32 +218,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors Angular's `.change-container .button-change` under
|
||||
// left-accordeon: a 35×40 pill sitting between the two city pickers,
|
||||
// shifted up/down by $space-m so the button overlaps the two inputs
|
||||
// rather than sitting in the vertical gap between them.
|
||||
&__exchange {
|
||||
align-self: center;
|
||||
width: 35px;
|
||||
height: vars.$medium-button-height;
|
||||
margin-top: vars.$space-m;
|
||||
margin-bottom: -(vars.$space-m);
|
||||
background: colors.$white;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
|
||||
&:hover { background-color: colors.$white; }
|
||||
}
|
||||
|
||||
&__exchange-icon {
|
||||
width: 12px;
|
||||
height: 25px;
|
||||
}
|
||||
// Swap-cities button moved to shared `SwapCityButton` component —
|
||||
// classes `.change-container` + `.button-change` are now global.
|
||||
|
||||
&__info {
|
||||
padding: vars.$space-s 0;
|
||||
|
||||
@@ -149,37 +149,10 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.svg--change-city {
|
||||
width: 25px;
|
||||
height: 12px;
|
||||
fill: colors.$blue;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Swap-cities button styling moved to `ui/city-autocomplete/SwapCityButton.scss`
|
||||
// so every filter sidebar (OnlineBoard / Schedule / FlightsMap) shares the
|
||||
// exact same DOM + CSS. The `.svg--change-city` sizing lives globally in
|
||||
// `styles/_icons.scss`.
|
||||
|
||||
.wrapper--time-selector {
|
||||
margin-top: vars.$space-xl;
|
||||
|
||||
@@ -73,6 +73,9 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
defaultValue={(props["value"] as string) ?? ""}
|
||||
/>
|
||||
),
|
||||
SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
|
||||
<button type="button" data-testid={props.testId} onClick={props.onClick} />
|
||||
),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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<OnlineBoardFilterProps> = ({
|
||||
testIdPrefix="route-departure"
|
||||
/>
|
||||
|
||||
<div className="change-container">
|
||||
<button
|
||||
className="button-change"
|
||||
type="button"
|
||||
onClick={handleExchange}
|
||||
data-testid="swap-cities-button"
|
||||
>
|
||||
<svg className="svg--change-city">
|
||||
<use xlinkHref="/assets/img/sprite.svg#changeCity" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<SwapCityButton
|
||||
onClick={handleExchange}
|
||||
testId="swap-cities-button"
|
||||
/>
|
||||
|
||||
<CityAutocomplete
|
||||
label={t("SHARED.ARRIVAL_CITY")}
|
||||
|
||||
@@ -93,6 +93,9 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
defaultValue={(props["value"] as string) ?? ""}
|
||||
/>
|
||||
),
|
||||
SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
|
||||
<button type="button" data-testid={props.testId} onClick={props.onClick} />
|
||||
),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -70,6 +70,9 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
defaultValue={(props["value"] as string) ?? ""}
|
||||
/>
|
||||
),
|
||||
SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
|
||||
<button type="button" data-testid={props.testId} onClick={props.onClick} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/dateWindow.js", () => ({
|
||||
|
||||
@@ -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<ScheduleFilterProps> = ({
|
||||
testIdPrefix="schedule-departure"
|
||||
/>
|
||||
|
||||
<div className="schedule-filter__swap">
|
||||
<button
|
||||
type="button"
|
||||
className="schedule-filter__swap-btn"
|
||||
onClick={handleSwap}
|
||||
aria-label="swap"
|
||||
data-testid="swap-cities-button"
|
||||
>
|
||||
<svg className="svg--change-city">
|
||||
<use xlinkHref="/assets/img/sprite.svg#changeCity" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<SwapCityButton
|
||||
onClick={handleSwap}
|
||||
testId="swap-cities-button"
|
||||
/>
|
||||
|
||||
<CityAutocomplete
|
||||
label={t("SHARED.ARRIVAL_CITY")}
|
||||
|
||||
@@ -355,36 +355,6 @@
|
||||
padding-right: 2rem; // leave room for the trigger icon on the right
|
||||
}
|
||||
|
||||
// Swap button — match Angular's flat style
|
||||
.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;
|
||||
}
|
||||
|
||||
.svg--change-city {
|
||||
width: 25px;
|
||||
height: 12px;
|
||||
fill: colors.$blue;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Swap-cities button styling moved to the shared `SwapCityButton` component
|
||||
// — classes `.change-container` + `.button-change` are now global.
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
defaultValue={(props["value"] as string) ?? ""}
|
||||
/>
|
||||
),
|
||||
SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
|
||||
<button type="button" data-testid={props.testId} onClick={props.onClick} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/ui/layout/SearchHistory.js", () => ({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <button>. */
|
||||
testId?: string;
|
||||
/** Accessible label. Defaults to "swap". */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export function SwapCityButton({
|
||||
onClick,
|
||||
testId,
|
||||
ariaLabel = "swap",
|
||||
}: SwapCityButtonProps): JSX.Element {
|
||||
return (
|
||||
<div className="change-container">
|
||||
<button
|
||||
type="button"
|
||||
className="button-change"
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId}
|
||||
>
|
||||
<svg className="svg--change-city">
|
||||
<use xlinkHref="/assets/img/sprite.svg#changeCity" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,5 +2,7 @@ export { CityAutocomplete } from "./CityAutocomplete.js";
|
||||
export type { CityAutocompleteProps } from "./CityAutocomplete.js";
|
||||
export { CityPickerPopup } from "./CityPickerPopup.js";
|
||||
export type { CityPickerPopupProps } from "./CityPickerPopup.js";
|
||||
export { SwapCityButton } from "./SwapCityButton.js";
|
||||
export type { SwapCityButtonProps } from "./SwapCityButton.js";
|
||||
export { buildCountryCityRows } from "./buildCountryCityRows.js";
|
||||
export type { ICountryCityRow } from "./buildCountryCityRows.js";
|
||||
|
||||
@@ -56,6 +56,9 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
placeholder={props["placeholder"] as string}
|
||||
/>
|
||||
),
|
||||
SwapCityButton: (props: { onClick: () => void; testId?: string }) => (
|
||||
<button type="button" data-testid={props.testId} onClick={props.onClick} />
|
||||
),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user