Skip to content

Commit 743a7fe

Browse files
authored
feat(ui5-daterange-picker): two months mode is implemented (#13196)
This PR adds a new showTwoCalendars property to the DateRangePicker component that displays two consecutive months side by side, making it easier to select date ranges that span multiple months. New Property - showTwoMonths (boolean, default: false) When enabled, the calendar popup displays two consecutive months instead of one Automatically adapts to mobile devices, showing a single calendar on phones Works seamlessly with existing date range selectiond)
1 parent 3fb9b5c commit 743a7fe

21 files changed

Lines changed: 2124 additions & 157 deletions

packages/main/cypress/specs/Calendar.cy.tsx

Lines changed: 465 additions & 0 deletions
Large diffs are not rendered by default.

packages/main/cypress/specs/DateRangePicker.cy.tsx

Lines changed: 473 additions & 1 deletion
Large diffs are not rendered by default.
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import DateRangePicker from "../../src/DateRangePicker.js";
2+
3+
type DateTimePickerTemplateOptions = Partial<{
4+
formatPattern: string;
5+
delimiter: string;
6+
onChange: () => void;
7+
value: string;
8+
minDate: string;
9+
maxDate: string;
10+
}>
11+
12+
function DateRangePickerTemplate(options: DateTimePickerTemplateOptions) {
13+
return <DateRangePicker {...options} />
14+
}
15+
16+
describe("DateRangePicker mobile footer interactions", () => {
17+
beforeEach(() => {
18+
cy.ui5SimulateDevice("phone");
19+
});
20+
21+
it("OK button is disabled when no dates are selected", () => {
22+
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" />);
23+
24+
cy.get<DateRangePicker>("[ui5-daterange-picker]")
25+
.as("dateRangePicker")
26+
.shadow()
27+
.find("[ui5-datetime-input]")
28+
.realClick()
29+
.should("be.focused");
30+
31+
cy.realPress("F4");
32+
33+
cy.get<DateRangePicker>("@dateRangePicker")
34+
.ui5DateRangePickerExpectToBeOpen();
35+
36+
// Get the responsive popover and look for the button in its light DOM
37+
cy.get<DateRangePicker>("@dateRangePicker")
38+
.ui5DateRangePickerGetPopover()
39+
.within(() => {
40+
cy.get("#ok")
41+
.should("exist")
42+
.and("have.attr", "disabled");
43+
});
44+
});
45+
46+
it("OK button is disabled when only one date is selected", () => {
47+
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" />);
48+
49+
cy.get<DateRangePicker>("[ui5-daterange-picker]")
50+
.as("dateRangePicker")
51+
.shadow()
52+
.find("[ui5-datetime-input]")
53+
.realClick()
54+
.should("be.focused");
55+
56+
cy.realPress("F4");
57+
58+
cy.get<DateRangePicker>("@dateRangePicker")
59+
.ui5DateRangePickerExpectToBeOpen();
60+
61+
// Select first date
62+
cy.get<DateRangePicker>("@dateRangePicker")
63+
.ui5DateRangePickerSelectRange(5);
64+
65+
// OK button should still be disabled
66+
cy.get<DateRangePicker>("@dateRangePicker")
67+
.ui5DateRangePickerGetPopover()
68+
.within(() => {
69+
cy.get("#ok")
70+
.should("exist")
71+
.and("have.attr", "disabled");
72+
});
73+
});
74+
75+
it("OK button is enabled when two dates are selected", () => {
76+
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" />);
77+
78+
cy.get<DateRangePicker>("[ui5-daterange-picker]")
79+
.as("dateRangePicker")
80+
.shadow()
81+
.find("[ui5-datetime-input]")
82+
.realClick()
83+
.should("be.focused");
84+
85+
cy.realPress("F4");
86+
87+
cy.get<DateRangePicker>("@dateRangePicker")
88+
.ui5DateRangePickerExpectToBeOpen();
89+
90+
// Select date range
91+
cy.get<DateRangePicker>("@dateRangePicker")
92+
.ui5DateRangePickerSelectRange(5, 15);
93+
94+
cy.get<DateRangePicker>("@dateRangePicker")
95+
.ui5DateRangePickerGetPopover()
96+
.within(() => {
97+
cy.get("#ok")
98+
.should("exist")
99+
.and("not.have.attr", "disabled");
100+
});
101+
});
102+
103+
it("OK button confirms the selection and closes the picker", () => {
104+
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" onChange={cy.stub().as("changeStub")} />);
105+
106+
cy.get<DateRangePicker>("[ui5-daterange-picker]")
107+
.as("dateRangePicker")
108+
.shadow()
109+
.find("[ui5-datetime-input]")
110+
.realClick()
111+
.should("be.focused");
112+
113+
cy.realPress("F4");
114+
115+
cy.get<DateRangePicker>("@dateRangePicker")
116+
.ui5DateRangePickerExpectToBeOpen();
117+
118+
// Select date range
119+
cy.get<DateRangePicker>("@dateRangePicker")
120+
.ui5DateRangePickerSelectRange(5, 15);
121+
122+
// Click OK button
123+
cy.get<DateRangePicker>("@dateRangePicker")
124+
.ui5DateRangePickerGetPopover()
125+
.within(() => {
126+
cy.get("#ok").realClick();
127+
});
128+
129+
// Picker should be closed
130+
cy.get<DateRangePicker>("@dateRangePicker")
131+
.should("have.prop", "open", false);
132+
133+
// Change event should be fired
134+
cy.get("@changeStub")
135+
.should("be.calledOnce");
136+
137+
// Value should be set
138+
cy.get<DateRangePicker>("@dateRangePicker")
139+
.should("have.attr", "value")
140+
.and("match", /\d{2}\/\d{2}\/\d{4} - \d{2}\/\d{2}\/\d{4}/);
141+
});
142+
143+
it("Cancel button closes the picker", () => {
144+
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" value="01/01/2020 - 05/01/2020" />);
145+
146+
cy.get<DateRangePicker>("[ui5-daterange-picker]")
147+
.as("dateRangePicker")
148+
.shadow()
149+
.find("[ui5-datetime-input]")
150+
.realClick()
151+
.should("be.focused");
152+
153+
cy.realPress("F4");
154+
155+
cy.get<DateRangePicker>("@dateRangePicker")
156+
.ui5DateRangePickerExpectToBeOpen();
157+
158+
// Click Cancel button
159+
cy.get<DateRangePicker>("@dateRangePicker")
160+
.ui5DateRangePickerGetPopover()
161+
.within(() => {
162+
cy.get("#cancel").realClick();
163+
});
164+
165+
// Picker should be closed
166+
cy.get<DateRangePicker>("@dateRangePicker")
167+
.should("have.prop", "open", false);
168+
});
169+
170+
it("Change event is not fired immediately on date selection in mobile mode", () => {
171+
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" onChange={cy.stub().as("changeStub")} />);
172+
173+
cy.get<DateRangePicker>("[ui5-daterange-picker]")
174+
.as("dateRangePicker")
175+
.shadow()
176+
.find("[ui5-datetime-input]")
177+
.realClick()
178+
.should("be.focused");
179+
180+
cy.realPress("F4");
181+
182+
cy.get<DateRangePicker>("@dateRangePicker")
183+
.ui5DateRangePickerExpectToBeOpen();
184+
185+
// Select date range
186+
cy.get<DateRangePicker>("@dateRangePicker")
187+
.ui5DateRangePickerSelectRange(5, 15);
188+
189+
// Change event should not be fired yet (only value-changed is fired internally)
190+
cy.get("@changeStub")
191+
.should("not.be.called");
192+
193+
// Picker should still be open
194+
cy.get<DateRangePicker>("@dateRangePicker")
195+
.ui5DateRangePickerExpectToBeOpen();
196+
});
197+
198+
it("Change event is fired only after OK button click on mobile", () => {
199+
cy.mount(<DateRangePickerTemplate formatPattern="dd/MM/yyyy" onChange={cy.stub().as("changeStub")} />);
200+
201+
cy.get<DateRangePicker>("[ui5-daterange-picker]")
202+
.as("dateRangePicker")
203+
.shadow()
204+
.find("[ui5-datetime-input]")
205+
.realClick()
206+
.should("be.focused");
207+
208+
cy.realPress("F4");
209+
210+
cy.get<DateRangePicker>("@dateRangePicker")
211+
.ui5DateRangePickerExpectToBeOpen();
212+
213+
// Select date range
214+
cy.get<DateRangePicker>("@dateRangePicker")
215+
.ui5DateRangePickerSelectRange(5, 15);
216+
217+
// Change event should not be fired yet
218+
cy.get("@changeStub")
219+
.should("not.be.called");
220+
221+
// Click OK button
222+
cy.get<DateRangePicker>("@dateRangePicker")
223+
.ui5DateRangePickerGetPopover()
224+
.within(() => {
225+
cy.get("#ok").realClick();
226+
});
227+
228+
// Now change event should be fired
229+
cy.get("@changeStub")
230+
.should("be.calledOnce");
231+
});
232+
});

packages/main/cypress/support/commands/DateRangePicker.commands.ts

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,108 @@ Cypress.Commands.add("ui5DateRangePickerOpen", { prevSubject: true }, (subject:
77
cy.wrap(subject).ui5DateRangePickerExpectToBeOpen()
88
});
99

10+
Cypress.Commands.add("ui5DateRangePickerGetPopover", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
11+
return cy.wrap(subject)
12+
.shadow()
13+
.find<ResponsivePopover>("[ui5-responsive-popover]");
14+
});
15+
1016
Cypress.Commands.add("ui5DateRangePickerExpectToBeOpen", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
1117
cy.wrap(subject)
1218
.should("have.prop", "open", true);
1319

1420
cy.wrap(subject)
15-
.shadow()
16-
.find<ResponsivePopover>("[ui5-responsive-popover]")
21+
.ui5DateRangePickerGetPopover()
1722
.ui5ResponsivePopoverOpened();
23+
24+
return cy.wrap(subject);
25+
});
26+
27+
Cypress.Commands.add("ui5DateRangePickerSelectRange", { prevSubject: true }, (subject: JQuery<DateRangePicker>, startIndex: number, endIndex?: number) => {
28+
cy.wrap(subject)
29+
.shadow()
30+
.find("[ui5-calendar]")
31+
.shadow()
32+
.find("[ui5-daypicker]")
33+
.shadow()
34+
.find(".ui5-dp-root .ui5-dp-content div > .ui5-dp-item")
35+
.as("dateItems");
36+
37+
cy.get("@dateItems")
38+
.eq(startIndex)
39+
.realClick();
40+
41+
if (endIndex) {
42+
cy.get("@dateItems")
43+
.eq(endIndex)
44+
.realClick();
45+
}
46+
});
47+
48+
Cypress.Commands.add("ui5DateRangePickerGetCalendar", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
49+
return cy.wrap(subject)
50+
.shadow()
51+
.find("[ui5-calendar]");
52+
});
53+
54+
Cypress.Commands.add("ui5DateRangePickerGetMonthContainers", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
55+
return cy.wrap(subject)
56+
.ui5DateRangePickerGetCalendar()
57+
.shadow()
58+
.find(".ui5-cal-month-container");
59+
});
60+
61+
Cypress.Commands.add("ui5DateRangePickerExpectMonthContainerCount", { prevSubject: true }, (subject: JQuery<DateRangePicker>, count: number) => {
62+
cy.wrap(subject)
63+
.ui5DateRangePickerGetMonthContainers()
64+
.should("have.length", count);
65+
});
66+
67+
Cypress.Commands.add("ui5DateRangePickerGetDayPicker", { prevSubject: true }, (subject: JQuery<DateRangePicker>, index: number) => {
68+
return cy.wrap(subject)
69+
.ui5DateRangePickerGetMonthContainers()
70+
.eq(index)
71+
.find(`[id$='-daypicker-${index}']`);
72+
});
73+
74+
Cypress.Commands.add("ui5DateRangePickerGetCalendarHeaders", { prevSubject: true }, (subject: JQuery<DateRangePicker>) => {
75+
return cy.wrap(subject)
76+
.ui5DateRangePickerGetCalendar()
77+
.shadow()
78+
.find(".ui5-calheader");
79+
});
80+
81+
Cypress.Commands.add("ui5DateRangePickerClickDateInCalendar", { prevSubject: true }, (subject: JQuery<DateRangePicker>, calendarIndex: number, dateIndex: number) => {
82+
cy.wrap(subject)
83+
.ui5DateRangePickerGetDayPicker(calendarIndex)
84+
.shadow()
85+
.find("[data-sap-timestamp]")
86+
.eq(dateIndex)
87+
.realClick();
88+
});
89+
90+
Cypress.Commands.add("ui5DateRangePickerVerifySelectedDatesInCalendar", { prevSubject: true }, (subject: JQuery<DateRangePicker>, calendarIndex: number) => {
91+
cy.wrap(subject)
92+
.ui5DateRangePickerGetDayPicker(calendarIndex)
93+
.shadow()
94+
.find(".ui5-dp-item--selected")
95+
.should("exist");
96+
});
97+
98+
Cypress.Commands.add("ui5DateRangePickerClickNavigationButton", { prevSubject: true }, (subject: JQuery<DateRangePicker>, button: "next" | "prev") => {
99+
cy.wrap(subject)
100+
.ui5DateRangePickerGetCalendar()
101+
.shadow()
102+
.find(`[data-ui5-cal-header-btn-${button}]`)
103+
.realClick();
104+
});
105+
106+
Cypress.Commands.add("ui5DateRangePickerVerifyMonthText", { prevSubject: true }, (subject: JQuery<DateRangePicker>, headerIndex: number, expectedText: string) => {
107+
cy.wrap(subject)
108+
.ui5DateRangePickerGetCalendarHeaders()
109+
.eq(headerIndex)
110+
.find("[data-ui5-cal-header-btn-month]")
111+
.should("contain.text", expectedText);
18112
});
19113

20114
declare global {
@@ -23,8 +117,51 @@ declare global {
23117
ui5DateRangePickerOpen(
24118
this: Chainable<JQuery<DateRangePicker>>,
25119
): Chainable<void>;
120+
ui5DateRangePickerGetPopover(
121+
this: Chainable<JQuery<DateRangePicker>>,
122+
): Chainable<JQuery<ResponsivePopover>>;
26123
ui5DateRangePickerExpectToBeOpen(
27124
this: Chainable<JQuery<DateRangePicker>>,
125+
): Chainable<JQuery<DateRangePicker>>;
126+
ui5DateRangePickerSelectRange(
127+
this: Chainable<JQuery<DateRangePicker>>,
128+
startIndex: number,
129+
endIndex?: number,
130+
): Chainable<void>;
131+
ui5DateRangePickerGetCalendar(
132+
this: Chainable<JQuery<DateRangePicker>>,
133+
): Chainable<JQuery<HTMLElement>>;
134+
ui5DateRangePickerGetMonthContainers(
135+
this: Chainable<JQuery<DateRangePicker>>,
136+
): Chainable<JQuery<HTMLElement>>;
137+
ui5DateRangePickerExpectMonthContainerCount(
138+
this: Chainable<JQuery<DateRangePicker>>,
139+
count: number,
140+
): Chainable<void>;
141+
ui5DateRangePickerGetDayPicker(
142+
this: Chainable<JQuery<DateRangePicker>>,
143+
index: number,
144+
): Chainable<JQuery<HTMLElement>>;
145+
ui5DateRangePickerGetCalendarHeaders(
146+
this: Chainable<JQuery<DateRangePicker>>,
147+
): Chainable<JQuery<HTMLElement>>;
148+
ui5DateRangePickerClickDateInCalendar(
149+
this: Chainable<JQuery<DateRangePicker>>,
150+
calendarIndex: number,
151+
dateIndex: number,
152+
): Chainable<void>;
153+
ui5DateRangePickerVerifySelectedDatesInCalendar(
154+
this: Chainable<JQuery<DateRangePicker>>,
155+
calendarIndex: number,
156+
): Chainable<void>;
157+
ui5DateRangePickerClickNavigationButton(
158+
this: Chainable<JQuery<DateRangePicker>>,
159+
button: "next" | "prev",
160+
): Chainable<void>;
161+
ui5DateRangePickerVerifyMonthText(
162+
this: Chainable<JQuery<DateRangePicker>>,
163+
headerIndex: number,
164+
expectedText: string,
28165
): Chainable<void>;
29166
}
30167
}

0 commit comments

Comments
 (0)