diff --git a/packages/main/cypress/specs/Calendar.cy.tsx b/packages/main/cypress/specs/Calendar.cy.tsx
index 522d5e83e77f..d9a3766656d2 100644
--- a/packages/main/cypress/specs/Calendar.cy.tsx
+++ b/packages/main/cypress/specs/Calendar.cy.tsx
@@ -8,6 +8,7 @@ import "@ui5/webcomponents-localization/dist/features/calendar/Islamic.js";
import "@ui5/webcomponents-localization/dist/features/calendar/Gregorian.js";
import { resetConfiguration } from "@ui5/webcomponents-base/dist/InitialConfiguration.js";
import { getFirstDayOfWeek } from "@ui5/webcomponents-base/dist/config/FormatSettings.js";
+import CalendarSelectionMode from "../../src/types/CalendarSelectionMode.js";
const getDefaultCalendar = (date: Date) => {
const calDate = new Date(date);
@@ -1765,3 +1766,467 @@ describe("Calendar Global Configuration", () => {
.should("have.text", "Sat");
});
});
+
+describe("Calendar - Multiple Months Mode", () => {
+ describe("Two Calendars Display", () => {
+ it("should display two calendars when _showTwoMonths is true", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .should("have.length", 2);
+ });
+
+ it("should display only one calendar when _showTwoMonths is false", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .should("not.exist");
+
+ cy.get("#cal")
+ .shadow()
+ .find("[id$='-daypicker']")
+ .should("exist");
+ });
+
+ it("should display consecutive months in two calendar mode", () => {
+ cy.mount(
+
+
+
+ );
+
+ // First calendar should show January
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "January");
+
+ // Second calendar should show February
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "February");
+ });
+
+ it("should have correct CSS classes for multiple months mode", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-root")
+ .should("have.class", "ui5-dt-cal--multiple");
+
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-content")
+ .should("have.class", "ui5-cal-content-multiple");
+
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-multiple-months-wrapper")
+ .should("exist");
+ });
+ });
+
+ describe("Navigation in Multiple Months Mode", () => {
+ it("should show prev button only in first calendar header", () => {
+ cy.mount(
+
+
+
+ );
+
+ // First calendar should have prev button
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[data-ui5-cal-header-btn-prev]")
+ .should("exist");
+
+ // Second calendar should have spacer instead of prev button
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[data-ui5-cal-header-btn-prev]")
+ .should("not.exist");
+
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find(".ui5-calheader-spacer")
+ .should("exist");
+ });
+
+ it("should show next button only in last calendar header (desktop)", () => {
+ cy.mount(
+
+
+
+ );
+
+ // First calendar should have spacer instead of next button
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[data-ui5-cal-header-btn-next]")
+ .should("not.exist");
+
+ // Second calendar should have next button
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[data-ui5-cal-header-btn-next]")
+ .should("exist");
+ });
+
+ it("should navigate both calendars when clicking prev button", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Click prev button
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-prev]")
+ .realClick();
+
+ // First calendar should show February
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "February");
+
+ // Second calendar should show March
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "March");
+ });
+
+ it("should navigate both calendars when clicking next button", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Click next button
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-next]")
+ .realClick();
+
+ // First calendar should show February
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "February");
+
+ // Second calendar should show March
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "March");
+ });
+ });
+
+ describe("Picker Overlays in Multiple Months Mode", () => {
+ it("should show month picker as overlay when clicking month button", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Click month button in first calendar
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[data-ui5-cal-header-btn-month]")
+ .realClick();
+
+ // Month picker should be visible in overlay container
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .should("not.have.class", "ui5-cal-overlay-hidden");
+
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .find("[id$='-MP']")
+ .should("not.have.attr", "hidden");
+ });
+
+ it("should show year picker as overlay when clicking year button", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Click year button in first calendar
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[data-ui5-cal-header-btn-year]")
+ .realClick();
+
+ // Year picker should be visible in overlay container
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .should("not.have.class", "ui5-cal-overlay-hidden");
+
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .find("[id$='-YP']")
+ .should("not.have.attr", "hidden");
+ });
+
+ it("should show overlay effect on day pickers when picker is open", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Click month button to open month picker
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-month]")
+ .first()
+ .realClick();
+
+ // Overlay should be visible
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-daypicker-overlay")
+ .should("be.visible");
+ });
+
+ it("should hide overlay when selecting a month", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Open month picker
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-month]")
+ .first()
+ .realClick();
+
+ // Select a month
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .find("[id$='-MP']")
+ .shadow()
+ .find("[data-sap-timestamp]")
+ .first()
+ .realClick();
+
+ // Overlay should be hidden
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .should("have.class", "ui5-cal-overlay-hidden");
+ });
+ });
+
+ describe("Date Selection in Multiple Months Mode", () => {
+ it("should allow selecting dates from both calendars", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Click date in second calendar (February)
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[id$='-daypicker-1']")
+ .shadow()
+ .find("[data-sap-timestamp]")
+ .first()
+ .realClick();
+
+ // Selected date should be updated
+ cy.get("#cal")
+ .then($cal => {
+ const selectedDates = $cal[0].selectedDates;
+ expect(selectedDates).to.have.length(1);
+ });
+ });
+
+ it("should support range selection across both calendars", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Select start date in January (first calendar)
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[id$='-daypicker-0']")
+ .shadow()
+ .find("[data-sap-timestamp]")
+ .eq(14) // Jan 15
+ .realClick();
+
+ // Select end date in February (second calendar)
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[id$='-daypicker-1']")
+ .shadow()
+ .find("[data-sap-timestamp]")
+ .eq(13) // Feb 14
+ .realClick();
+
+ // Should have range selected
+ cy.get("#cal")
+ .then($cal => {
+ const selectedDates = $cal[0].selectedDates;
+ expect(selectedDates).to.have.length.greaterThan(1);
+ });
+ });
+ });
+
+ describe("Header Buttons in Multiple Months Mode", () => {
+ it("should hide month button when year or year-range picker is shown", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Open year picker
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-year]")
+ .first()
+ .realClick();
+
+ // Month button should be hidden in unified header
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-calheader-default-multiple")
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("have.attr", "hidden");
+ });
+
+ it("should show all header buttons in default day picker mode", () => {
+ cy.mount(
+
+
+
+ );
+
+ // Both calendars should show month and year buttons
+ cy.get("#cal")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .each(($container) => {
+ cy.wrap($container)
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("not.have.attr", "hidden");
+
+ cy.wrap($container)
+ .find("[data-ui5-cal-header-btn-year]")
+ .should("not.have.attr", "hidden");
+ });
+ });
+ });
+
+ describe("Accessibility in Multiple Months Mode", () => {
+ it("should have proper ARIA labels on navigation buttons", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-prev]")
+ .should("have.attr", "aria-label");
+
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-next]")
+ .should("have.attr", "aria-label");
+ });
+
+ it("should have proper role attributes on header buttons", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-prev]")
+ .should("have.attr", "role", "button");
+
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-next]")
+ .should("have.attr", "role", "button");
+
+ cy.get("#cal")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-month]")
+ .first()
+ .should("have.attr", "role", "button");
+ });
+ });
+});
diff --git a/packages/main/cypress/specs/DateRangePicker.cy.tsx b/packages/main/cypress/specs/DateRangePicker.cy.tsx
index 2269b1337c75..04977181039c 100644
--- a/packages/main/cypress/specs/DateRangePicker.cy.tsx
+++ b/packages/main/cypress/specs/DateRangePicker.cy.tsx
@@ -968,4 +968,482 @@ describe("Validation inside a form", () => {
cy.get("#dateRangePicker:invalid")
.should("not.exist");
});
-});
\ No newline at end of file
+});
+
+describe("DateRangePicker - Two Calendars Feature", () => {
+ describe("Basic Two Calendars Display", () => {
+ it("should display two calendars when showTwoMonths is true", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .shadow()
+ .find("[ui5-datetime-input]")
+ .realClick()
+ .should("be.focused");
+
+ cy.realPress("F4");
+
+ cy.get("@dateRangePicker")
+ .ui5DateRangePickerExpectToBeOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .should("have.length", 2);
+ });
+
+ it("should display one calendar when showTwoMonths is false", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .shadow()
+ .find("[ui5-datetime-input]")
+ .realClick()
+ .should("be.focused");
+
+ cy.realPress("F4");
+
+ cy.get("@dateRangePicker")
+ .ui5DateRangePickerExpectToBeOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .should("not.exist");
+ });
+
+ it("should show consecutive months in two calendars mode", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .shadow()
+ .find("[ui5-datetime-input]")
+ .realClick()
+ .should("be.focused");
+
+ cy.realPress("F4");
+
+ cy.get("@dateRangePicker")
+ .ui5DateRangePickerExpectToBeOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-calheader-middlebtn")
+ .should("have.length.greaterThan", 1);
+ });
+ });
+
+ describe("Date Range Selection with Two Calendars", () => {
+ it("should allow selecting range across both calendars", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .ui5DateRangePickerOpen();
+
+ // Select start date in first calendar
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[id$='-daypicker-0']")
+ .shadow()
+ .find("[data-sap-timestamp]")
+ .eq(14)
+ .realClick();
+
+ // Select end date in second calendar
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[id$='-daypicker-1']")
+ .shadow()
+ .find("[data-sap-timestamp]")
+ .eq(9)
+ .realClick();
+
+ cy.get("@dateRangePicker")
+ .invoke("attr", "value")
+ .should("exist")
+ .and("not.be.empty");
+ });
+
+ it("should highlight selection across both calendars", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .ui5DateRangePickerOpen();
+
+ // First calendar should have selected dates
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[id$='-daypicker-0']")
+ .shadow()
+ .find(".ui5-dp-item--selected")
+ .should("exist");
+
+ // Second calendar should have selected dates
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[id$='-daypicker-1']")
+ .shadow()
+ .find(".ui5-dp-item--selected")
+ .should("exist");
+ });
+
+ it("should update value when selecting new range", () => {
+ cy.mount(
+
+ );
+
+ const changeSpy = cy.spy().as("changeSpy");
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .then($drp => {
+ $drp[0].addEventListener("change", changeSpy);
+ })
+ .ui5DateRangePickerOpen();
+
+ // Select new start date
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[id$='-daypicker-0']")
+ .shadow()
+ .find("[data-sap-timestamp]")
+ .eq(9)
+ .realClick();
+
+ // Select new end date
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[id$='-daypicker-1']")
+ .shadow()
+ .find("[data-sap-timestamp]")
+ .eq(14)
+ .realClick();
+
+ cy.get("@changeSpy").should("have.been.called");
+ });
+ });
+
+ describe("Navigation in Two Calendars Mode", () => {
+ it("should navigate both calendars forward", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .ui5DateRangePickerOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-next]")
+ .realClick();
+
+ // First calendar should show February
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "February");
+
+ // Second calendar should show March
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "March");
+ });
+
+ it("should navigate both calendars backward", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .ui5DateRangePickerOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-prev]")
+ .realClick();
+
+ // First calendar should show February
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .first()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "February");
+
+ // Second calendar should show March
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .last()
+ .find("[data-ui5-cal-header-btn-month]")
+ .should("contain.text", "March");
+ });
+ });
+
+ describe("Picker Overlays", () => {
+ it("should show month picker overlay when clicking month button", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .ui5DateRangePickerOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-month]")
+ .first()
+ .realClick();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .should("not.have.class", "ui5-cal-overlay-hidden");
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .find("[id$='-MP']")
+ .should("not.have.attr", "hidden");
+ });
+
+ it("should show year picker overlay when clicking year button", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .ui5DateRangePickerOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-year]")
+ .first()
+ .realClick();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .should("not.have.class", "ui5-cal-overlay-hidden");
+ });
+
+ it("should return to day pickers after selecting from month picker", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .ui5DateRangePickerOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-month]")
+ .first()
+ .realClick();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .find("[id$='-MP']")
+ .shadow()
+ .find("[data-sap-timestamp]")
+ .first()
+ .realClick();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .should("have.class", "ui5-cal-overlay-hidden");
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .should("have.length", 2);
+ });
+ });
+
+ describe("Keyboard Navigation", () => {
+ it("should allow keyboard navigation through header buttons", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .shadow()
+ .find("[ui5-datetime-input]")
+ .realClick()
+ .should("be.focused");
+
+ cy.realPress("F4");
+
+ cy.get("@dateRangePicker")
+ .ui5DateRangePickerExpectToBeOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-calheader")
+ .should("have.length.greaterThan", 0);
+ });
+
+ it("should activate buttons with Space key", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .ui5DateRangePickerOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find("[data-ui5-cal-header-btn-month]")
+ .first()
+ .focus();
+
+ cy.realPress("Space");
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-overlay-container")
+ .should("not.have.class", "ui5-cal-overlay-hidden");
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle year boundary correctly", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .shadow()
+ .find("[ui5-datetime-input]")
+ .realClick()
+ .should("be.focused");
+
+ cy.realPress("F4");
+
+ cy.get("@dateRangePicker")
+ .ui5DateRangePickerExpectToBeOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-calheader-middlebtn")
+ .should("have.length.greaterThan", 2);
+ });
+
+ it("should handle empty initial value", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-daterange-picker]")
+ .as("dateRangePicker")
+ .ui5DateRangePickerOpen();
+
+ cy.get("@dateRangePicker")
+ .shadow()
+ .find("[ui5-calendar]")
+ .shadow()
+ .find(".ui5-cal-month-container")
+ .should("have.length", 2);
+ });
+ });
+});
+
+
diff --git a/packages/main/src/Calendar.ts b/packages/main/src/Calendar.ts
index 7089649be053..205b3b9f0cc8 100644
--- a/packages/main/src/Calendar.ts
+++ b/packages/main/src/Calendar.ts
@@ -36,6 +36,9 @@ import type CalendarLegend from "./CalendarLegend.js";
import type { CalendarLegendItemSelectionChangeEventDetail } from "./CalendarLegend.js";
import type SpecialCalendarDate from "./SpecialCalendarDate.js";
import type CalendarLegendItemType from "./types/CalendarLegendItemType.js";
+import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
+import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
+import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
// Default calendar for bundling
import "@ui5/webcomponents-localization/dist/features/calendar/Gregorian.js";
@@ -63,6 +66,9 @@ import {
CALENDAR_HEADER_YEAR_RANGE_PREVIOUS_BUTTON_TITLE,
} from "./generated/i18n/i18n-defaults.js";
import type { YearRangePickerChangeEventDetail } from "./YearRangePicker.js";
+import getEffectiveContentDensity from "@ui5/webcomponents-base/dist/util/getEffectiveContentDensity.js";
+
+const PHONE_MODE_BREAKPOINT = 640; // px
interface ICalendarPicker extends HTMLElement {
_showPreviousPage: () => void,
@@ -280,6 +286,18 @@ class Calendar extends CalendarPart {
@property({ type: Boolean })
hideWeekNumbers = false;
+ /**
+ * Defines whether the component displays two months side by side in the picker popup.
+ * @default false
+ * @private
+ * @since 2.21.0
+ */
+ @property({ type: Boolean })
+ _showTwoMonths = false;
+
+ @property({ type: Boolean })
+ stretch = false;
+
/**
* Which picker is currently visible to the user: day/month/year/yearRange
* @private
@@ -357,6 +375,14 @@ class Calendar extends CalendarPart {
@property()
_selectedItemType: `${CalendarLegendItemType}` = "None";
+ @property({ type: Boolean, noAttribute: true })
+ _phoneMode = false;
+
+ @property({ type: Boolean, noAttribute: true })
+ _portraitMode = false;
+
+ _handleResizeBound: ResizeObserverCallback;
+
@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;
@@ -364,6 +390,113 @@ class Calendar extends CalendarPart {
super();
this._valueIsProcessed = false;
+ this._handleResizeBound = this._handleResize.bind(this);
+ }
+
+ onEnterDOM() {
+ ResizeHandler.register(document.body, this._handleResizeBound);
+ // Initialize modes on first load
+ this._handleResize();
+ }
+
+ get _phoneView() {
+ return isPhone() || this._phoneMode;
+ }
+
+ get _portraitView() {
+ return this._portraitMode;
+ }
+
+ /**
+ * Handles document resize to switch between `phoneMode` and `portraitMode`.
+ * - `_phoneMode`: Only when it's an actual phone device (isPhone() returns true)
+ * - `_portraitMode`: When resolution is under PHONE_MODE_BREAKPOINT (regardless of device type)
+ */
+ _handleResize() {
+ const documentWidth = document.body.offsetWidth;
+ const underBreakpoint = documentWidth <= PHONE_MODE_BREAKPOINT;
+
+ // Phone mode: only when it's an actual phone device
+ const phoneModeChange = (underBreakpoint && !this._phoneMode) || (!underBreakpoint && this._phoneMode);
+
+ if (phoneModeChange) {
+ this._phoneMode = underBreakpoint;
+ }
+
+ // Portrait mode: when resolution is under breakpoint (can be tablet, desktop in narrow window, etc.)
+ const toPortraitMode = underBreakpoint;
+ const portraitModeChange = (toPortraitMode && !this._portraitMode) || (!toPortraitMode && this._portraitMode);
+
+ if (portraitModeChange) {
+ this._portraitMode = toPortraitMode;
+ }
+ }
+
+ onExitDOM() {
+ ResizeHandler.deregister(document.body, this._handleResizeBound);
+ }
+
+ /**
+ * Returns the timestamp for a specific month index when displaying multiple months
+ * @private
+ */
+ _getMonthTimestamp(monthIndex: number): number {
+ if (monthIndex === 0) {
+ return this._timestamp;
+ }
+
+ const calendarDate = CalendarDateComponent.fromTimestamp(this._timestamp * 1000, this._primaryCalendarType);
+
+ // Set day to 1 to avoid day-of-month overflow issues
+ // (e.g., Jan 31 + 1 month would overflow to March if Feb doesn't have 31 days)
+ calendarDate.setDate(1);
+
+ // Add months one by one to handle month boundaries correctly
+ for (let i = 0; i < monthIndex; i++) {
+ const currentMonth = calendarDate.getMonth();
+ const currentYear = calendarDate.getYear();
+
+ if (currentMonth === 11) {
+ // December -> January of next year
+ calendarDate.setYear(currentYear + 1);
+ calendarDate.setMonth(0);
+ } else {
+ // Just increment the month
+ calendarDate.setMonth(currentMonth + 1);
+ }
+ }
+
+ return calendarDate.valueOf() / 1000;
+ }
+
+ /**
+ * Generates header button text (month and year) for a specific month timestamp
+ * @private
+ */
+ _getHeaderTextForMonth(monthTimestamp: number): { monthText: string, yearText: string, secondMonthText?: string, secondYearText?: string } {
+ const calendarDate = CalendarDateComponent.fromTimestamp(monthTimestamp * 1000, this._primaryCalendarType);
+ const localeData = getCachedLocaleDataInstance(getLocale());
+ const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this.primaryCalendarType });
+
+ const monthText = localeData.getMonthsStandAlone("wide", this.primaryCalendarType)[calendarDate.getMonth()];
+ const localDate = calendarDate.toLocalJSDate();
+ const yearText = String(yearFormat.format(localDate, true));
+
+ const result: { monthText: string, yearText: string, secondMonthText?: string, secondYearText?: string } = {
+ monthText,
+ yearText,
+ };
+
+ if (this.hasSecondaryCalendarType) {
+ const secondaryDate = transformDateToSecondaryType(this.primaryCalendarType, this._secondaryCalendarType, monthTimestamp, true);
+ const secondaryCalendarDate = secondaryDate.firstDate || secondaryDate.lastDate;
+ const secondaryLocaleData = getCachedLocaleDataInstance(getLocale());
+ result.secondMonthText = secondaryLocaleData.getMonthsStandAlone("wide", this._secondaryCalendarType)[secondaryCalendarDate.getMonth()];
+ const secondaryYearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType });
+ result.secondYearText = String(secondaryYearFormat.format(secondaryCalendarDate.toLocalJSDate(), true));
+ }
+
+ return result;
}
/**
@@ -675,12 +808,21 @@ class Calendar extends CalendarPart {
};
}
+ get _isCompactMode() {
+ return getEffectiveContentDensity(this) === "compact";
+ }
+
+ get _monthsToShow() {
+ const monthsToShow = this._showTwoMonths ? 2 : 1;
+ return isPhone() ? 1 : monthsToShow;
+ }
+
/**
* The month button is hidden when the month picker or year picker is shown
* @private
*/
get _isHeaderMonthButtonHidden(): boolean {
- return this._currentPicker !== "day";
+ return this._showTwoMonths ? this._currentPicker === "yearrange" || this._currentPicker === "year" : this._currentPicker !== "day";
}
/**
@@ -700,6 +842,10 @@ class Calendar extends CalendarPart {
}
get _isDayPickerHidden() {
+ // In multi-month mode (monthsToShow > 1), keep day pickers visible even when other pickers are shown
+ if (this._showTwoMonths) {
+ return false;
+ }
return this._currentPicker !== "day";
}
@@ -715,6 +861,18 @@ class Calendar extends CalendarPart {
return this._currentPicker !== "yearrange";
}
+ get _isDefaultHeaderModeInMultipleMonths() {
+ return !this._isDayPickerHidden && this._isYearPickerHidden;
+ }
+
+ get _shouldShowOnePickerHeaderButtonInMultipleMonths() {
+ return !this._isDayPickerHidden && !this._isYearPickerHidden;
+ }
+
+ get _areDayPickersInert() {
+ return this._showTwoMonths && (!this._isMonthPickerHidden || !this._isYearPickerHidden || !this._isYearRangePickerHidden);
+ }
+
get _currentYearRange(): CalendarYearRangeT {
const rangeSize = this.hasSecondaryCalendarType ? 8 : 20;
const yearsOffset = this.hasSecondaryCalendarType ? 2 : 9;
diff --git a/packages/main/src/CalendarHeaderTemplate.tsx b/packages/main/src/CalendarHeaderTemplate.tsx
index f5a53839efcb..4b6f2bd57d1b 100644
--- a/packages/main/src/CalendarHeaderTemplate.tsx
+++ b/packages/main/src/CalendarHeaderTemplate.tsx
@@ -4,91 +4,154 @@ import Icon from "./Icon.js";
import slimArowLeft from "@ui5/webcomponents-icons/dist/slim-arrow-left.js";
import slimArowRight from "@ui5/webcomponents-icons/dist/slim-arrow-right.js";
-export default function CalendarTemplate(this: Calendar) {
+interface CalendarHeaderOptions {
+ headerText?: {
+ monthText: string;
+ yearText: string;
+ secondMonthText?: string;
+ secondYearText?: string;
+ };
+ isFirst?: boolean;
+ isLast?: boolean;
+ isMultiple?: boolean;
+}
+
+export default function CalendarHeaderTemplate(this: Calendar, options?: CalendarHeaderOptions) {
+ const headerText = options?.headerText;
+ const isFirst = options?.isFirst ?? true;
+ const isLast = options?.isLast ?? true;
+ const isMultiple = options?.isMultiple ?? false;
+
+ const monthText = headerText?.monthText ?? this._headerMonthButtonText;
+ const yearText = headerText?.yearText ?? this._headerYearButtonText;
+ const secondMonthText = headerText?.secondMonthText ?? this.secondMonthButtonText;
+ const secondYearText = headerText?.secondYearText ?? this._headerYearButtonTextSecType;
+
return (
-