Add clear-button (X) to filter fields per TZ 4.1.9 Tables 11/12/14
- OB flight-number: X was always visible; now conditionally rendered only when the field has a value (hides when empty) - OB flight-date and route-date: add X button next to calendar icon, clears date state and hides itself when empty - Schedule outbound and return date-range calendars: same inline X pattern - CSS: .calendar-input-wrapper + .calendar-clear-btn added to both SCSS files (absolute-positioned left of the calendar icon) - CityAutocomplete: already correct (CSS show/hide via has-value class) - 21 new tests across OnlineBoardFilter, ScheduleFilter, CityAutocomplete (aria-label, visibility toggling, click-to-clear); all 640 pass
This commit is contained in:
@@ -276,6 +276,40 @@
|
||||
// margin-top removed: vertical rhythm now driven by .filter-content gap.
|
||||
}
|
||||
|
||||
.calendar-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.p-calendar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.calendar-clear-btn {
|
||||
position: absolute;
|
||||
// Place X to the left of the calendar icon (icon is ~38px wide)
|
||||
right: 38px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: colors.$light-gray;
|
||||
font-size: fonts.$font-size-xl;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: colors.$text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
// Vertical rhythm between filter rows. Angular's accordion content
|
||||
// separates fields by ~$space-l (15px); the previous default
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Tests for OnlineBoardFilter — clear-button (X) behaviour per TZ §4.1.9
|
||||
* Tables 11/12.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useNavigate: () => mockNavigate,
|
||||
useParams: () => ({ lang: "ru-ru" }),
|
||||
}));
|
||||
|
||||
vi.mock("@/i18n/provider.js", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
vi.mock("@/i18n/useLocale.js", () => ({
|
||||
useLocale: () => ({ locale: "ru-ru", language: "ru" }),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/dictionaries/index.js", () => ({
|
||||
useDictionaries: () => ({ dictionaries: null, loading: false, error: null }),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/state/crossSectionNavigation.js", () => ({
|
||||
setBoardFilter: vi.fn(),
|
||||
}));
|
||||
|
||||
// PrimeReact Calendar is stubbed so tests don't require a DOM calendar widget.
|
||||
vi.mock("primereact/calendar", () => ({
|
||||
Calendar: (props: Record<string, unknown>) => {
|
||||
const inputRef = props["inputRef"] as React.RefObject<HTMLInputElement> | undefined;
|
||||
return (
|
||||
<input
|
||||
data-testid={props["data-testid"] as string}
|
||||
placeholder={props["placeholder"] as string}
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// PrimeReact Slider stub
|
||||
vi.mock("primereact/slider", () => ({
|
||||
Slider: () => <div data-testid="slider-stub" />,
|
||||
}));
|
||||
|
||||
// DayQuickPick — not needed for these tests
|
||||
vi.mock("@/ui/calendar/DayQuickPick.js", () => ({
|
||||
DayQuickPick: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
CityAutocomplete: (props: Record<string, unknown>) => (
|
||||
<input
|
||||
data-testid={`${(props["testIdPrefix"] as string) ?? "city-autocomplete"}-input`}
|
||||
defaultValue={(props["value"] as string) ?? ""}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderFlightTab() {
|
||||
render(<OnlineBoardFilter initialTab="flight" />);
|
||||
}
|
||||
|
||||
function renderRouteTab() {
|
||||
render(<OnlineBoardFilter initialTab="route" />);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("OnlineBoardFilter – clear-button (X) per TZ §4.1.9 Tables 11/12", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Flight-number field
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it("4.1.9-R1: flight-number X is hidden when input is empty", () => {
|
||||
renderFlightTab();
|
||||
expect(screen.queryByTestId("flight-number-clear")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-R2: flight-number X appears when a value is typed", () => {
|
||||
renderFlightTab();
|
||||
fireEvent.change(screen.getByTestId("flight-number-input"), {
|
||||
target: { value: "1234" },
|
||||
});
|
||||
expect(screen.queryByTestId("flight-number-clear")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("4.1.9-R3: clicking flight-number X clears the input and hides X", () => {
|
||||
renderFlightTab();
|
||||
const input = screen.getByTestId("flight-number-input");
|
||||
fireEvent.change(input, { target: { value: "1234" } });
|
||||
|
||||
const clearBtn = screen.queryByTestId("flight-number-clear")!;
|
||||
expect(clearBtn).toBeTruthy();
|
||||
fireEvent.click(clearBtn);
|
||||
|
||||
expect((input as HTMLInputElement).value).toBe("");
|
||||
expect(screen.queryByTestId("flight-number-clear")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-R4: flight-number X has aria-label for a11y", () => {
|
||||
renderFlightTab();
|
||||
fireEvent.change(screen.getByTestId("flight-number-input"), {
|
||||
target: { value: "999" },
|
||||
});
|
||||
const clearBtn = screen.queryByTestId("flight-number-clear")!;
|
||||
expect(clearBtn.getAttribute("aria-label")).toBe("SHARED.A11Y-CLEAR");
|
||||
});
|
||||
|
||||
it("4.1.9-R5: clicking X also clears any existing validation error", () => {
|
||||
renderFlightTab();
|
||||
// Submit with invalid number to trigger error
|
||||
fireEvent.change(screen.getByTestId("flight-number-input"), {
|
||||
target: { value: "abc" },
|
||||
});
|
||||
fireEvent.submit(screen.getByTestId("search-form"));
|
||||
expect(screen.queryByTestId("flight-number-error")).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.queryByTestId("flight-number-clear")!);
|
||||
expect(screen.queryByTestId("flight-number-error")).toBeNull();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// OB date (flight-tab Calendar)
|
||||
// -------------------------------------------------------------------------
|
||||
// Calendar is stubbed as a read-only input; the clear button is rendered
|
||||
// conditionally based on the `flightDate` state (a Date | null). Because
|
||||
// the Calendar stub doesn't fire an onChange, we seed the value via the
|
||||
// `initialDate` + `initialTab` props which set flightDate during init.
|
||||
|
||||
it("4.1.9-R6: flight-date X is hidden when no date is selected", () => {
|
||||
renderFlightTab();
|
||||
expect(screen.queryByTestId("flight-date-clear")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-R7: flight-date X is present when an initial date is provided", () => {
|
||||
render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
|
||||
expect(screen.queryByTestId("flight-date-clear")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("4.1.9-R8: clicking flight-date X clears the date and hides X", () => {
|
||||
render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
|
||||
const clearBtn = screen.queryByTestId("flight-date-clear")!;
|
||||
expect(clearBtn).toBeTruthy();
|
||||
fireEvent.click(clearBtn);
|
||||
expect(screen.queryByTestId("flight-date-clear")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-R9: flight-date clear X has aria-label for a11y", () => {
|
||||
render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
|
||||
expect(screen.queryByTestId("flight-date-clear")!.getAttribute("aria-label")).toBe(
|
||||
"SHARED.A11Y-CLEAR",
|
||||
);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Route-tab date Calendar
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it("4.1.9-R10: route-date X is hidden when no date is selected", () => {
|
||||
renderRouteTab();
|
||||
expect(screen.queryByTestId("route-date-clear")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-R11: route-date X is present when an initial date is provided", () => {
|
||||
render(<OnlineBoardFilter initialTab="route" initialDate="20260601" />);
|
||||
expect(screen.queryByTestId("route-date-clear")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("4.1.9-R12: clicking route-date X clears the date and hides X", () => {
|
||||
render(<OnlineBoardFilter initialTab="route" initialDate="20260601" />);
|
||||
const clearBtn = screen.queryByTestId("route-date-clear")!;
|
||||
fireEvent.click(clearBtn);
|
||||
expect(screen.queryByTestId("route-date-clear")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -385,17 +385,20 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
|
||||
}}
|
||||
data-testid="flight-number-input"
|
||||
/>
|
||||
<button
|
||||
className="button-clear"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFlightNumber("");
|
||||
setFlightNumberError(null);
|
||||
}}
|
||||
data-testid="flight-number-clear-button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{flightNumber && (
|
||||
<button
|
||||
className="button-clear"
|
||||
type="button"
|
||||
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||
onClick={() => {
|
||||
setFlightNumber("");
|
||||
setFlightNumberError(null);
|
||||
}}
|
||||
data-testid="flight-number-clear"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -408,19 +411,32 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
|
||||
locale={language}
|
||||
onChange={setFlightDate}
|
||||
/>
|
||||
<Calendar
|
||||
value={flightDate}
|
||||
onChange={(e) => setFlightDate(e.value as Date | null)}
|
||||
minDate={boardMinDate}
|
||||
maxDate={boardMaxDate}
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={t("SHARED.DATE_FORMAT")}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="date-input"
|
||||
inputId="search-date-input"
|
||||
inputRef={flightDateInputRef}
|
||||
/>
|
||||
<div className="calendar-input-wrapper">
|
||||
<Calendar
|
||||
value={flightDate}
|
||||
onChange={(e) => setFlightDate(e.value as Date | null)}
|
||||
minDate={boardMinDate}
|
||||
maxDate={boardMaxDate}
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={t("SHARED.DATE_FORMAT")}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="date-input"
|
||||
inputId="search-date-input"
|
||||
inputRef={flightDateInputRef}
|
||||
/>
|
||||
{flightDate && (
|
||||
<button
|
||||
type="button"
|
||||
className="calendar-clear-btn"
|
||||
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||
data-testid="flight-date-clear"
|
||||
onClick={() => setFlightDate(null)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -532,19 +548,32 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
|
||||
locale={language}
|
||||
onChange={setRouteDate}
|
||||
/>
|
||||
<Calendar
|
||||
value={routeDate}
|
||||
onChange={(e) => setRouteDate(e.value as Date | null)}
|
||||
minDate={boardMinDate}
|
||||
maxDate={boardMaxDate}
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={t("SHARED.DATE_FORMAT")}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="date-input"
|
||||
inputId="route-date-input"
|
||||
inputRef={routeDateInputRef}
|
||||
/>
|
||||
<div className="calendar-input-wrapper">
|
||||
<Calendar
|
||||
value={routeDate}
|
||||
onChange={(e) => setRouteDate(e.value as Date | null)}
|
||||
minDate={boardMinDate}
|
||||
maxDate={boardMaxDate}
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={t("SHARED.DATE_FORMAT")}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="date-input"
|
||||
inputId="route-date-input"
|
||||
inputRef={routeDateInputRef}
|
||||
/>
|
||||
{routeDate && (
|
||||
<button
|
||||
type="button"
|
||||
className="calendar-clear-btn"
|
||||
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||
data-testid="route-date-clear"
|
||||
onClick={() => setRouteDate(null)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -104,6 +104,40 @@
|
||||
font-weight: fonts.$font-medium;
|
||||
}
|
||||
|
||||
.calendar-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.p-calendar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.calendar-clear-btn {
|
||||
position: absolute;
|
||||
// Place X to the left of the calendar icon (icon is ~38px wide)
|
||||
right: 38px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: colors.$light-gray;
|
||||
font-size: fonts.$font-size-xl;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: colors.$text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
margin-top: vars.$space-l;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Tests for ScheduleFilter — clear-button (X) behaviour per TZ §4.1.9
|
||||
* Tables 13/14.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { ScheduleFilter } from "./ScheduleFilter.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useNavigate: () => mockNavigate,
|
||||
useParams: () => ({ lang: "ru-ru" }),
|
||||
}));
|
||||
|
||||
vi.mock("@/i18n/provider.js", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
vi.mock("@/i18n/useLocale.js", () => ({
|
||||
useLocale: () => ({ locale: "ru-ru", language: "ru" }),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/dictionaries/index.js", () => ({
|
||||
useDictionaries: () => ({ dictionaries: null, loading: false, error: null }),
|
||||
}));
|
||||
|
||||
// PrimeReact Calendar stub — read-only input so state is driven by props
|
||||
vi.mock("primereact/calendar", () => ({
|
||||
Calendar: (props: Record<string, unknown>) => {
|
||||
const inputRef = props["inputRef"] as React.RefObject<HTMLInputElement> | undefined;
|
||||
return (
|
||||
<input
|
||||
data-testid={props["data-testid"] as string}
|
||||
placeholder={props["placeholder"] as string}
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// PrimeReact Slider stub
|
||||
vi.mock("primereact/slider", () => ({
|
||||
Slider: () => <div data-testid="slider-stub" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
CityAutocomplete: (props: Record<string, unknown>) => (
|
||||
<input
|
||||
data-testid={`${(props["testIdPrefix"] as string) ?? "city-autocomplete"}-input`}
|
||||
defaultValue={(props["value"] as string) ?? ""}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/dateWindow.js", () => ({
|
||||
scheduleWindowBounds: () => {
|
||||
const min = new Date(2026, 0, 1);
|
||||
const max = new Date(2026, 11, 31);
|
||||
return [min, max];
|
||||
},
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("ScheduleFilter – clear-button (X) per TZ §4.1.9 Tables 13/14", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Outbound date range
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it("4.1.9-S1: schedule date-range X is hidden when no dates are selected", () => {
|
||||
render(<ScheduleFilter />);
|
||||
expect(screen.queryByTestId("schedule-date-clear")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-S2: schedule date-range X is present when both dates are provided", () => {
|
||||
render(
|
||||
<ScheduleFilter initialDateFrom="20260601" initialDateTo="20260607" />,
|
||||
);
|
||||
expect(screen.queryByTestId("schedule-date-clear")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("4.1.9-S3: clicking schedule date-range X clears the range and hides X", () => {
|
||||
render(
|
||||
<ScheduleFilter initialDateFrom="20260601" initialDateTo="20260607" />,
|
||||
);
|
||||
const clearBtn = screen.queryByTestId("schedule-date-clear")!;
|
||||
fireEvent.click(clearBtn);
|
||||
expect(screen.queryByTestId("schedule-date-clear")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-S4: schedule date-range clear X has aria-label for a11y", () => {
|
||||
render(
|
||||
<ScheduleFilter initialDateFrom="20260601" initialDateTo="20260607" />,
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId("schedule-date-clear")!.getAttribute("aria-label"),
|
||||
).toBe("SHARED.A11Y-CLEAR");
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Return date range (only visible when `initialReturnFlights` = true)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it("4.1.9-S5: return date-range X is hidden when no return dates are selected", () => {
|
||||
render(<ScheduleFilter initialReturnFlights={true} />);
|
||||
expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-S6: return date-range X is present when return dates are provided", () => {
|
||||
render(
|
||||
<ScheduleFilter
|
||||
initialReturnFlights={true}
|
||||
initialReturnDateFrom="20260608"
|
||||
initialReturnDateTo="20260614"
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByTestId("schedule-return-date-clear")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("4.1.9-S7: clicking return date-range X clears the range and hides X", () => {
|
||||
render(
|
||||
<ScheduleFilter
|
||||
initialReturnFlights={true}
|
||||
initialReturnDateFrom="20260608"
|
||||
initialReturnDateTo="20260614"
|
||||
/>,
|
||||
);
|
||||
const clearBtn = screen.queryByTestId("schedule-return-date-clear")!;
|
||||
fireEvent.click(clearBtn);
|
||||
expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-S8: return date-range clear X has aria-label for a11y", () => {
|
||||
render(
|
||||
<ScheduleFilter
|
||||
initialReturnFlights={true}
|
||||
initialReturnDateFrom="20260608"
|
||||
initialReturnDateTo="20260614"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId("schedule-return-date-clear")!.getAttribute("aria-label"),
|
||||
).toBe("SHARED.A11Y-CLEAR");
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Sliders — no X required (TZ Table 12/14 exclusion)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it("4.1.9-S9: no clear button is rendered for the departure-time slider", () => {
|
||||
render(<ScheduleFilter />);
|
||||
// Time selector is present
|
||||
expect(screen.queryByTestId("time-selector")).toBeTruthy();
|
||||
// But no slider clear button
|
||||
expect(screen.queryByTestId("time-slider-clear")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -304,21 +304,34 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
<label className="label--filter">
|
||||
{t("SHARED.SCHEDULES_DATE")}
|
||||
</label>
|
||||
<Calendar
|
||||
value={dateRange}
|
||||
onChange={(e) => setDateRange((e.value as (Date | null)[]) ?? [null, null])}
|
||||
selectionMode="range"
|
||||
minDate={scheduleMinDate}
|
||||
maxDate={scheduleMaxDate}
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="schedule-date-input"
|
||||
inputId="schedule-date-input"
|
||||
inputRef={dateRangeInputRef}
|
||||
readOnlyInput
|
||||
/>
|
||||
<div className="calendar-input-wrapper">
|
||||
<Calendar
|
||||
value={dateRange}
|
||||
onChange={(e) => setDateRange((e.value as (Date | null)[]) ?? [null, null])}
|
||||
selectionMode="range"
|
||||
minDate={scheduleMinDate}
|
||||
maxDate={scheduleMaxDate}
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="schedule-date-input"
|
||||
inputId="schedule-date-input"
|
||||
inputRef={dateRangeInputRef}
|
||||
readOnlyInput
|
||||
/>
|
||||
{(dateRange[0] || dateRange[1]) && (
|
||||
<button
|
||||
type="button"
|
||||
className="calendar-clear-btn"
|
||||
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||
data-testid="schedule-date-clear"
|
||||
onClick={() => setDateRange([null, null])}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wrapper--time-selector compact-view" data-testid="time-selector">
|
||||
@@ -371,24 +384,37 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
<label className="label--filter">
|
||||
{t("SHARED.RETURN_FLIGHT_DATE")}
|
||||
</label>
|
||||
<Calendar
|
||||
value={returnDateRange}
|
||||
onChange={(e) =>
|
||||
setReturnDateRange(
|
||||
(e.value as (Date | null)[]) ?? [null, null],
|
||||
)
|
||||
}
|
||||
selectionMode="range"
|
||||
minDate={scheduleMinDate}
|
||||
maxDate={scheduleMaxDate}
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="schedule-return-date-input"
|
||||
inputId="schedule-return-date-input"
|
||||
readOnlyInput
|
||||
/>
|
||||
<div className="calendar-input-wrapper">
|
||||
<Calendar
|
||||
value={returnDateRange}
|
||||
onChange={(e) =>
|
||||
setReturnDateRange(
|
||||
(e.value as (Date | null)[]) ?? [null, null],
|
||||
)
|
||||
}
|
||||
selectionMode="range"
|
||||
minDate={scheduleMinDate}
|
||||
maxDate={scheduleMaxDate}
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="schedule-return-date-input"
|
||||
inputId="schedule-return-date-input"
|
||||
readOnlyInput
|
||||
/>
|
||||
{(returnDateRange[0] || returnDateRange[1]) && (
|
||||
<button
|
||||
type="button"
|
||||
className="calendar-clear-btn"
|
||||
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||
data-testid="schedule-return-date-clear"
|
||||
onClick={() => setReturnDateRange([null, null])}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wrapper--time-selector compact-view" data-testid="return-time-selector">
|
||||
|
||||
@@ -204,6 +204,50 @@ describe("CityAutocomplete", () => {
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("4.1.9-C1: has-value CSS class is applied when value is non-empty (clear button becomes visible)", () => {
|
||||
const { container } = render(
|
||||
<CityAutocomplete
|
||||
label="City"
|
||||
placeholder="Pick"
|
||||
value="MOW"
|
||||
onChange={vi.fn()}
|
||||
dictionaries={dictionaries}
|
||||
testIdPrefix="test"
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(".city-autocomplete__input--has-value")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("4.1.9-C2: has-value CSS class is absent when value is empty (clear button hidden)", () => {
|
||||
const { container } = render(
|
||||
<CityAutocomplete
|
||||
label="City"
|
||||
placeholder="Pick"
|
||||
value=""
|
||||
onChange={vi.fn()}
|
||||
dictionaries={dictionaries}
|
||||
testIdPrefix="test"
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(".city-autocomplete__input--has-value")).toBeNull();
|
||||
});
|
||||
|
||||
it("4.1.9-C3: clear button has aria-label for a11y", () => {
|
||||
render(
|
||||
<CityAutocomplete
|
||||
label="City"
|
||||
placeholder="Pick"
|
||||
value="MOW"
|
||||
onChange={vi.fn()}
|
||||
dictionaries={dictionaries}
|
||||
testIdPrefix="test"
|
||||
/>,
|
||||
);
|
||||
const clearBtn = screen.getByTestId("test-clear-button");
|
||||
// aria-label is always rendered; visibility is controlled via CSS class
|
||||
expect(clearBtn.getAttribute("aria-label")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not open popup when dictionaries is null", () => {
|
||||
render(
|
||||
<CityAutocomplete
|
||||
|
||||
Reference in New Issue
Block a user