Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
1cb8fff
feat: implement second calendar
GDamyanov Feb 2, 2026
f033bf1
feat: create resize handler
GDamyanov Feb 2, 2026
bf64d21
feat: add modals for other pickers
GDamyanov Feb 3, 2026
53c4c20
feat: enhance calendar header in multiple months mode
GDamyanov Feb 24, 2026
dd82ece
feat: show pickers as overlay
GDamyanov Feb 25, 2026
9794c43
fix: style overlays pickers
GDamyanov Feb 25, 2026
714007c
fix: add year range button
GDamyanov Feb 25, 2026
3d2e44f
feat: style pickers
GDamyanov Feb 26, 2026
9188576
feat: style header buttons
GDamyanov Mar 4, 2026
a52202a
feat: start to implement mobile footer
GDamyanov Mar 5, 2026
0ea3a31
Merge branch 'main' into twocalendars
GDamyanov Mar 5, 2026
945568f
feat: create mobile footer
GDamyanov Mar 5, 2026
7cda354
Merge branch 'main' into twocalendars
GDamyanov Mar 5, 2026
d4cfeb5
Merge branch 'main' into twocalendars
GDamyanov Mar 5, 2026
3d12c7d
feat: finish mobile view
GDamyanov Mar 5, 2026
343eac2
Merge branch 'main' into twocalendars
GDamyanov Mar 6, 2026
43a608f
fix: style overlay popup
GDamyanov Mar 6, 2026
4e5c571
refactor: extract duplicated code in template
GDamyanov Mar 6, 2026
2c4d4b3
test: added tests
GDamyanov Mar 6, 2026
122aaff
Merge branch 'main' into twocalendars
GDamyanov Mar 9, 2026
aecc1a0
feat: add classes
GDamyanov Mar 9, 2026
2522faa
feat: style overlay picker containers
GDamyanov Mar 9, 2026
2bd5da3
Merge branch 'main' into twocalendars
GDamyanov Mar 9, 2026
c0d44e4
fix: lint errors
GDamyanov Mar 9, 2026
f228db4
refactor: extract the logic for the header in header template
GDamyanov Mar 9, 2026
a4c87bc
Merge branch 'main' into twocalendars
GDamyanov Mar 9, 2026
49169e3
fix: lint errors
GDamyanov Mar 9, 2026
b6a3d0a
docs: add sample
GDamyanov Mar 9, 2026
da05f34
refactor: remove redundant text
GDamyanov Mar 10, 2026
23785d0
Merge branch 'main' into twocalendars
GDamyanov Mar 10, 2026
5518c95
fix: add again portrait mode
GDamyanov Mar 10, 2026
d066a03
refactor: refactor calendar template in horizontal mode because of he…
GDamyanov Mar 12, 2026
470e405
feat: make day picker in multiple mode not focusalble when there is o…
GDamyanov Mar 12, 2026
36a44fa
feat: implement compact mode
GDamyanov Mar 12, 2026
e30ced7
Merge branch 'main' into twocalendars
GDamyanov Mar 12, 2026
a8a577c
refactor: refactor template
GDamyanov Mar 13, 2026
8847f00
refactor: rename properties
GDamyanov Mar 13, 2026
b30fbdc
refactor: consume updated properties in tests
GDamyanov Mar 13, 2026
6eb9c3e
refactor: remove unused properties
GDamyanov Mar 13, 2026
d0683c4
fix: style header buttons for compact
GDamyanov Mar 13, 2026
fefab82
fix: lint errors
GDamyanov Mar 13, 2026
8441f0e
Merge branch 'main' into twocalendars
GDamyanov Mar 16, 2026
806aace
Merge branch 'main' into twocalendars
GDamyanov Mar 17, 2026
feb02cc
Merge branch 'main' into twocalendars
GDamyanov Mar 18, 2026
3adc903
fix: remove unused class
GDamyanov Mar 18, 2026
e96d503
fix: fix ok button handler for mobile and selectedDateChange
GDamyanov Mar 18, 2026
5bec443
fix: remove jsdoc
GDamyanov Mar 18, 2026
dbc0dcc
fix: remove unused property
GDamyanov Mar 18, 2026
7124a8f
fix: update resize handler
GDamyanov Mar 18, 2026
7f119dc
fix: clear value on cancel
GDamyanov Mar 18, 2026
1fc25f6
fix: reuse method
GDamyanov Mar 18, 2026
b3c061a
fix: refactor conditions
GDamyanov Mar 18, 2026
89eadfc
refactor: refactor template
GDamyanov Mar 18, 2026
0858110
fix: provide fallbacks
GDamyanov Mar 18, 2026
04950fc
fix: remove duplicated css variable
GDamyanov Mar 18, 2026
2ad838d
fix: rename css variable
GDamyanov Mar 18, 2026
284e412
fix: remove unused classes
GDamyanov Mar 18, 2026
f35a9ef
fix: refactor css
GDamyanov Mar 18, 2026
c9f23dc
Merge branch 'main' into twocalendars
GDamyanov Mar 18, 2026
937a4b0
test: add test suite for mobile
GDamyanov Mar 18, 2026
25f3b9a
test: fix tests
GDamyanov Mar 18, 2026
34c4b39
test: refactor tests
GDamyanov Mar 18, 2026
e78e92c
fix: remove unused style
GDamyanov Mar 18, 2026
68eca07
fix: fix issue when there are two months and no week numbering
GDamyanov Mar 19, 2026
c371cee
Merge branch 'main' into twocalendars
GDamyanov Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
465 changes: 465 additions & 0 deletions packages/main/cypress/specs/Calendar.cy.tsx

Large diffs are not rendered by default.

473 changes: 472 additions & 1 deletion packages/main/cypress/specs/DateRangePicker.cy.tsx

Large diffs are not rendered by default.

236 changes: 236 additions & 0 deletions packages/main/cypress/specs/DateRangePicker.mobile.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import DateRangePicker from "../../src/DateRangePicker.js";

type DateTimePickerTemplateOptions = Partial<{
formatPattern: string;
delimiter: string;
onChange: () => void;
value: string;
minDate: string;
maxDate: string;
}>

function DateRangePickerTemplate(options: DateTimePickerTemplateOptions) {
return <DateRangePicker {...options} />
}

describe("DateRangePicker mobile footer interactions", () => {
beforeEach(() => {
cy.ui5SimulateDevice("phone");
});

it("OK button is disabled when no dates are selected", () => {
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" />);

cy.get<DateRangePicker>("[ui5-daterange-picker]")
.as("dateRangePicker")
.shadow()
.find("[ui5-datetime-input]")
.realClick()
.should("be.focused");

cy.realPress("F4");

cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerExpectToBeOpen();

// Get the responsive popover and look for the button in its light DOM
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerGetPopover()
.within(() => {
cy.get("#ok")
.should("exist")
.and("have.attr", "disabled");
});
});

it("OK button is disabled when only one date is selected", () => {
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" />);

cy.get<DateRangePicker>("[ui5-daterange-picker]")
.as("dateRangePicker")
.shadow()
.find("[ui5-datetime-input]")
.realClick()
.should("be.focused");

cy.realPress("F4");

cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerExpectToBeOpen();

// Select first date
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerSelectRange(5);

// OK button should still be disabled
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerGetPopover()
.within(() => {
cy.get("#ok")
.should("exist")
.and("have.attr", "disabled");
});
});

it("OK button is enabled when two dates are selected", () => {
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" />);

cy.get<DateRangePicker>("[ui5-daterange-picker]")
.as("dateRangePicker")
.shadow()
.find("[ui5-datetime-input]")
.realClick()
.should("be.focused");

cy.realPress("F4");

cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerExpectToBeOpen();

// Select date range
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerSelectRange(5, 15);

cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerGetPopover()
.within(() => {
cy.get("#ok")
.should("exist")
.and("not.have.attr", "disabled");
});
});

it("OK button confirms the selection and closes the picker", () => {
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" onChange={cy.stub().as("changeStub")} />);

cy.get<DateRangePicker>("[ui5-daterange-picker]")
.as("dateRangePicker")
.shadow()
.find("[ui5-datetime-input]")
.realClick()
.should("be.focused");

cy.realPress("F4");

cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerExpectToBeOpen();

// Select date range
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerSelectRange(5, 15);

// Click OK button
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerGetPopover()
.within(() => {
cy.get("#ok").realClick();
});

// Picker should be closed
cy.get<DateRangePicker>("@dateRangePicker")
.should("have.prop", "open", false);

// Change event should be fired
cy.get("@changeStub")
.should("be.calledOnce");

// Value should be set
cy.get<DateRangePicker>("@dateRangePicker")
.should("have.attr", "value")
.and("match", /\d{2}\/\d{2}\/\d{4} - \d{2}\/\d{2}\/\d{4}/);
});

it("Cancel button clears the selection and closes the picker", () => {
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" value="01/01/2020 - 05/01/2020" />);

cy.get<DateRangePicker>("[ui5-daterange-picker]")
.as("dateRangePicker")
.shadow()
.find("[ui5-datetime-input]")
.realClick()
.should("be.focused");

cy.realPress("F4");

cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerExpectToBeOpen();

// Click Cancel button
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerGetPopover()
.within(() => {
cy.get("#cancel").realClick();
});

// Picker should be closed
cy.get<DateRangePicker>("@dateRangePicker")
.should("have.prop", "open", false);

// Value should be cleared
cy.get("@dateRangePicker")
.should("have.attr", "value", "");
});

it("Change event is not fired immediately on date selection in mobile mode", () => {
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" onChange={cy.stub().as("changeStub")} />);

cy.get<DateRangePicker>("[ui5-daterange-picker]")
.as("dateRangePicker")
.shadow()
.find("[ui5-datetime-input]")
.realClick()
.should("be.focused");

cy.realPress("F4");

cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerExpectToBeOpen();

// Select date range
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerSelectRange(5, 15);

// Change event should not be fired yet (only value-changed is fired internally)
cy.get("@changeStub")
.should("not.be.called");

// Picker should still be open
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerExpectToBeOpen();
});

it("Change event is fired only after OK button click on mobile", () => {
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" onChange={cy.stub().as("changeStub")} />);

cy.get<DateRangePicker>("[ui5-daterange-picker]")
.as("dateRangePicker")
.shadow()
.find("[ui5-datetime-input]")
.realClick()
.should("be.focused");

cy.realPress("F4");

cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerExpectToBeOpen();

// Select date range
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerSelectRange(5, 15);

// Change event should not be fired yet
cy.get("@changeStub")
.should("not.be.called");

// Click OK button
cy.get<DateRangePicker>("@dateRangePicker")
.ui5DateRangePickerGetPopover()
.within(() => {
cy.get("#ok").realClick();
});

// Now change event should be fired
cy.get("@changeStub")
.should("be.calledOnce");
});
});
141 changes: 139 additions & 2 deletions packages/main/cypress/support/commands/DateRangePicker.commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,108 @@ Cypress.Commands.add("ui5DateRangePickerOpen", { prevSubject: true }, (subject:
cy.wrap(subject).ui5DateRangePickerExpectToBeOpen()
});

Cypress.Commands.add("ui5DateRangePickerGetPopover", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
return cy.wrap(subject)
.shadow()
.find<ResponsivePopover>("[ui5-responsive-popover]");
});

Cypress.Commands.add("ui5DateRangePickerExpectToBeOpen", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
cy.wrap(subject)
.should("have.prop", "open", true);

cy.wrap(subject)
.shadow()
.find<ResponsivePopover>("[ui5-responsive-popover]")
.ui5DateRangePickerGetPopover()
.ui5ResponsivePopoverOpened();

return cy.wrap(subject);
});

Cypress.Commands.add("ui5DateRangePickerSelectRange", { prevSubject: true }, (subject: JQuery<DateRangePicker>, startIndex: number, endIndex?: number) => {
cy.wrap(subject)
.shadow()
.find("[ui5-calendar]")
.shadow()
.find("[ui5-daypicker]")
.shadow()
.find(".ui5-dp-root .ui5-dp-content div > .ui5-dp-item")
.as("dateItems");

cy.get("@dateItems")
.eq(startIndex)
.realClick();

if (endIndex) {
cy.get("@dateItems")
.eq(endIndex)
.realClick();
}
});

Cypress.Commands.add("ui5DateRangePickerGetCalendar", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
return cy.wrap(subject)
.shadow()
.find("[ui5-calendar]");
});

Cypress.Commands.add("ui5DateRangePickerGetMonthContainers", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
return cy.wrap(subject)
.ui5DateRangePickerGetCalendar()
.shadow()
.find(".ui5-cal-month-container");
});

Cypress.Commands.add("ui5DateRangePickerExpectMonthContainerCount", { prevSubject: true }, (subject: JQuery<DateRangePicker>, count: number) => {
cy.wrap(subject)
.ui5DateRangePickerGetMonthContainers()
.should("have.length", count);
});

Cypress.Commands.add("ui5DateRangePickerGetDayPicker", { prevSubject: true }, (subject: JQuery<DateRangePicker>, index: number) => {
return cy.wrap(subject)
.ui5DateRangePickerGetMonthContainers()
.eq(index)
.find(`[id$='-daypicker-${index}']`);
});

Cypress.Commands.add("ui5DateRangePickerGetCalendarHeaders", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
return cy.wrap(subject)
.ui5DateRangePickerGetCalendar()
.shadow()
.find(".ui5-calheader");
});

Cypress.Commands.add("ui5DateRangePickerClickDateInCalendar", { prevSubject: true }, (subject: JQuery<DateRangePicker>, calendarIndex: number, dateIndex: number) => {
cy.wrap(subject)
.ui5DateRangePickerGetDayPicker(calendarIndex)
.shadow()
.find("[data-sap-timestamp]")
.eq(dateIndex)
.realClick();
});

Cypress.Commands.add("ui5DateRangePickerVerifySelectedDatesInCalendar", { prevSubject: true }, (subject: JQuery<DateRangePicker>, calendarIndex: number) => {
cy.wrap(subject)
.ui5DateRangePickerGetDayPicker(calendarIndex)
.shadow()
.find(".ui5-dp-item--selected")
.should("exist");
});

Cypress.Commands.add("ui5DateRangePickerClickNavigationButton", { prevSubject: true }, (subject: JQuery<DateRangePicker>, button: "next" | "prev") => {
cy.wrap(subject)
.ui5DateRangePickerGetCalendar()
.shadow()
.find(`[data-ui5-cal-header-btn-${button}]`)
.realClick();
});

Cypress.Commands.add("ui5DateRangePickerVerifyMonthText", { prevSubject: true }, (subject: JQuery<DateRangePicker>, headerIndex: number, expectedText: string) => {
cy.wrap(subject)
.ui5DateRangePickerGetCalendarHeaders()
.eq(headerIndex)
.find("[data-ui5-cal-header-btn-month]")
.should("contain.text", expectedText);
});

declare global {
Expand All @@ -23,8 +117,51 @@ declare global {
ui5DateRangePickerOpen(
this: Chainable<JQuery<DateRangePicker>>,
): Chainable<void>;
ui5DateRangePickerGetPopover(
this: Chainable<JQuery<DateRangePicker>>,
): Chainable<JQuery<ResponsivePopover>>;
ui5DateRangePickerExpectToBeOpen(
this: Chainable<JQuery<DateRangePicker>>,
): Chainable<JQuery<DateRangePicker>>;
ui5DateRangePickerSelectRange(
this: Chainable<JQuery<DateRangePicker>>,
startIndex: number,
endIndex?: number,
): Chainable<void>;
ui5DateRangePickerGetCalendar(
this: Chainable<JQuery<DateRangePicker>>,
): Chainable<JQuery<HTMLElement>>;
ui5DateRangePickerGetMonthContainers(
this: Chainable<JQuery<DateRangePicker>>,
): Chainable<JQuery<HTMLElement>>;
ui5DateRangePickerExpectMonthContainerCount(
this: Chainable<JQuery<DateRangePicker>>,
count: number,
): Chainable<void>;
ui5DateRangePickerGetDayPicker(
this: Chainable<JQuery<DateRangePicker>>,
index: number,
): Chainable<JQuery<HTMLElement>>;
ui5DateRangePickerGetCalendarHeaders(
this: Chainable<JQuery<DateRangePicker>>,
): Chainable<JQuery<HTMLElement>>;
ui5DateRangePickerClickDateInCalendar(
this: Chainable<JQuery<DateRangePicker>>,
calendarIndex: number,
dateIndex: number,
): Chainable<void>;
ui5DateRangePickerVerifySelectedDatesInCalendar(
this: Chainable<JQuery<DateRangePicker>>,
calendarIndex: number,
): Chainable<void>;
ui5DateRangePickerClickNavigationButton(
this: Chainable<JQuery<DateRangePicker>>,
button: "next" | "prev",
): Chainable<void>;
ui5DateRangePickerVerifyMonthText(
this: Chainable<JQuery<DateRangePicker>>,
headerIndex: number,
expectedText: string,
): Chainable<void>;
}
}
Expand Down
Loading
Loading