Skip to content

Commit 5111ce7

Browse files
authored
feat(ui5-calendar): improve header and day picker accessibility and focus behavior
Calendar header navigation buttons are now focusable via keyboard (tabindex set). Each header button has a descriptive title attribute for improved accessibility and user guidance.
1 parent 988e821 commit 5111ce7

14 files changed

+260
-119
lines changed

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

Lines changed: 132 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -51,47 +51,62 @@ const getCalendarWithDisabledDates = (id, formatPattern, ranges, props = {}) =>
5151
);
5252

5353
describe("Calendar general interaction", () => {
54-
it("Focus goes into the current day item of the day picker", () => {
55-
const date = new Date(Date.UTC(2000, 10, 22, 0, 0, 0));
56-
cy.mount(getDefaultCalendar(date));
57-
58-
cy.ui5CalendarGetDay("#calendar1", "974851200")
59-
.as("selectedDay");
60-
61-
cy.get("@selectedDay")
62-
.realClick();
63-
64-
cy.get("@selectedDay")
65-
.should("have.focus")
66-
.realPress("Tab");
54+
it("Focus goes into the header items and then to the current day item of the day picker", () => {
55+
const calendarTestDate = new Date(Date.UTC(2000, 10, 22, 0, 0, 0));
56+
cy.mount(getDefaultCalendar(calendarTestDate));
6757

6858
cy.get<Calendar>("#calendar1")
6959
.shadow()
7060
.find(".ui5-calheader")
7161
.as("calheader");
62+
63+
cy.ui5CalendarGetDay("#calendar1", "974851200").as("selectedDay");
64+
65+
cy.get("#calendar1")
66+
.realClick();
67+
68+
cy.realPress("Tab");
7269

7370
cy.get("@calheader")
74-
.find("[data-ui5-cal-header-btn-month]")
75-
.as("monthBtn");
71+
.find("[data-ui5-cal-header-btn-prev]")
72+
.as("prevBtn")
73+
.should("have.attr", "tabindex", "0");
74+
75+
cy.get("@prevBtn")
76+
.should("be.focused");
7677

78+
cy.realPress("Tab");
79+
80+
cy.get("@calheader")
81+
.find("[data-ui5-cal-header-btn-month]")
82+
.as("monthBtn")
83+
.should("have.attr", "tabindex", "0");;
84+
7785
cy.get("@monthBtn")
78-
.should("have.focus")
79-
.realPress("Tab");
86+
.should("be.focused");
87+
88+
cy.realPress("Tab");
8089

8190
cy.get("@calheader")
8291
.find("[data-ui5-cal-header-btn-year]")
8392
.as("yearBtn");
84-
93+
8594
cy.get("@yearBtn")
86-
.should("have.focus")
87-
.realPress(["Shift", "Tab"]);
95+
.should("be.focused");
96+
97+
cy.realPress("Tab");
8898

89-
cy.get("@monthBtn")
90-
.should("have.focus")
91-
.realPress(["Shift", "Tab"]);
99+
cy.get("@calheader")
100+
.find("[data-ui5-cal-header-btn-next]")
101+
.as("nextBtn");
102+
103+
cy.get("@nextBtn")
104+
.should("be.focused");
105+
106+
cy.realPress("Tab");
92107

93108
cy.get("@selectedDay")
94-
.should("have.focus");
109+
.should("be.focused");
95110
});
96111

97112
it("Calendar focuses the selected year when yearpicker is opened", () => {
@@ -121,11 +136,32 @@ describe("Calendar general interaction", () => {
121136
const date = new Date(Date.UTC(2000, 10, 22, 0, 0, 0));
122137
cy.mount(getDefaultCalendar(date));
123138

124-
cy.ui5CalendarGetDay("#calendar1", "974851200")
139+
cy.get<Calendar>("#calendar1")
140+
.shadow()
141+
.find(".ui5-calheader")
142+
.as("calheader");
143+
144+
cy.ui5CalendarGetDay("#calendar1", "974851200").as("selectedDay");
145+
146+
cy.get("#calendar1")
125147
.realClick();
126148

127-
cy.focused().realPress("Tab");
128-
cy.focused().realPress("Space");
149+
cy.realPress("Tab");
150+
151+
cy.get("@calheader")
152+
.find("[data-ui5-cal-header-btn-prev]")
153+
.as("prevBtn");
154+
155+
cy.get("@prevBtn")
156+
.should("be.focused");
157+
158+
cy.realPress("Tab");
159+
160+
cy.get("@calheader")
161+
.find("[data-ui5-cal-header-btn-month]")
162+
.as("monthBtn");
163+
164+
cy.realPress("Space");
129165

130166
cy.get<Calendar>("#calendar1")
131167
.shadow()
@@ -148,12 +184,46 @@ describe("Calendar general interaction", () => {
148184
const date = new Date(Date.UTC(2000, 10, 22, 0, 0, 0));
149185
cy.mount(getDefaultCalendar(date));
150186

151-
cy.ui5CalendarGetDay("#calendar1", "974851200")
152-
.realClick();
187+
188+
cy.get<Calendar>("#calendar1")
189+
.shadow()
190+
.find(".ui5-calheader")
191+
.as("calheader");
153192

154-
cy.focused().realPress("Tab");
155-
cy.focused().realPress("Tab");
156-
cy.focused().realPress("Space");
193+
cy.ui5CalendarGetDay("#calendar1", "974851200").as("selectedDay");
194+
195+
cy.get("#calendar1")
196+
.realClick();
197+
198+
cy.realPress("Tab");
199+
200+
cy.get("@calheader")
201+
.find("[data-ui5-cal-header-btn-prev]")
202+
.as("prevBtn");
203+
204+
cy.get("@prevBtn")
205+
.should("be.focused");
206+
207+
cy.realPress("Tab");
208+
209+
cy.get("@calheader")
210+
.find("[data-ui5-cal-header-btn-month]")
211+
.as("monthBtn");
212+
213+
cy.get("@monthBtn")
214+
.should("be.focused");
215+
216+
217+
cy.realPress("Tab");
218+
219+
cy.get("@calheader")
220+
.find("[data-ui5-cal-header-btn-year]")
221+
.as("yearBtn");
222+
223+
cy.get("@yearBtn")
224+
.should("be.focused");
225+
226+
cy.realPress("Space");
157227

158228
cy.get<Calendar>("#calendar1")
159229
.shadow()
@@ -427,15 +497,15 @@ describe("Calendar general interaction", () => {
427497
.should("have.focus");
428498

429499
cy.focused().realPress(["Shift", "F4"]);
430-
500+
431501
// Wait for focus to settle before proceeding
432502
cy.get<Calendar>("#calendar1")
433503
.shadow()
434504
.find("[ui5-yearpicker]")
435505
.shadow()
436506
.find("[tabindex='0']")
437507
.should("have.focus");
438-
508+
439509
cy.focused().realPress("PageUp");
440510

441511
cy.get<Calendar>("#calendar1")
@@ -1160,6 +1230,26 @@ describe("Calendar general interaction", () => {
11601230
});
11611231

11621232
describe("Calendar accessibility", () => {
1233+
it("Header prev/next buttons have correct title and tabindex", () => {
1234+
const date = new Date(Date.UTC(2025, 0, 15, 0, 0, 0));
1235+
cy.mount(getDefaultCalendar(date));
1236+
1237+
cy.get<Calendar>("#calendar1")
1238+
.shadow()
1239+
.find(".ui5-calheader")
1240+
.as("calheader");
1241+
1242+
cy.get("@calheader")
1243+
.find("[data-ui5-cal-header-btn-prev]")
1244+
.should("have.attr", "title")
1245+
.and("contain", "Previous Month (Pagedown)");
1246+
1247+
cy.get("@calheader")
1248+
.find("[data-ui5-cal-header-btn-next]")
1249+
.should("have.attr", "title")
1250+
.and("contain", "Next Month (Pageup)");
1251+
});
1252+
11631253
it("Should have proper aria-label attributes on header buttons", () => {
11641254
const date = new Date(Date.UTC(2000, 10, 22, 0, 0, 0));
11651255
cy.mount(getDefaultCalendar(date));
@@ -1435,7 +1525,7 @@ describe("Calendar accessibility", () => {
14351525
// Get the selected days and verify their aria-labels
14361526
cy.get("@selectedDays").each(($day, index) => {
14371527
cy.wrap($day).should("have.attr", "aria-label");
1438-
1528+
14391529
if (index === 0) {
14401530
// First day should contain "First date of range"
14411531
cy.wrap($day)
@@ -1457,22 +1547,22 @@ describe("Calendar accessibility", () => {
14571547
});
14581548

14591549
describe("Day Picker Tests", () => {
1460-
it.skip("Select day with Space", () => {
1550+
it.skip("Select day with Space", () => {
14611551
cy.mount(<Calendar id="calendar1"></Calendar>);
1462-
1552+
14631553
cy.get<Calendar>("#calendar1")
14641554
.shadow()
14651555
.find("[ui5-daypicker]")
14661556
.shadow()
14671557
.find(".ui5-dp-item--now")
14681558
.as("today");
1469-
1559+
14701560
cy.get("@today")
14711561
.realClick()
14721562
.should("be.focused")
14731563
.realPress("ArrowRight")
14741564
.realPress("Space");
1475-
1565+
14761566
cy.focused()
14771567
.invoke("attr", "data-sap-timestamp")
14781568
.then(timestampAttr => {
@@ -1481,7 +1571,7 @@ describe("Day Picker Tests", () => {
14811571
const expectedDate = new Date(Date.now() + 24 * 3600 * 1000).getDate();
14821572
expect(selectedDate).to.eq(expectedDate);
14831573
});
1484-
1574+
14851575
cy.get<Calendar>("#calendar1")
14861576
.should(($calendar) => {
14871577
const selectedDates = $calendar.prop("selectedDates");
@@ -1494,7 +1584,7 @@ describe("Day Picker Tests", () => {
14941584
const tomorrow = Math.floor(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate() + 1, 0, 0, 0, 0) / 1000);
14951585

14961586
cy.mount(<Calendar id="calendar1"></Calendar>);
1497-
1587+
14981588
cy.get<Calendar>("#calendar1")
14991589
.shadow()
15001590
.find("[ui5-daypicker]")
@@ -1533,7 +1623,7 @@ describe("Day Picker Tests", () => {
15331623

15341624
it("Day names are correctly displayed", () => {
15351625
cy.mount(<Calendar id="calendar1"></Calendar>);
1536-
1626+
15371627
cy.get<Calendar>("#calendar1")
15381628
.shadow()
15391629
.find("[ui5-daypicker]")
@@ -1593,7 +1683,6 @@ describe("Day Picker Tests", () => {
15931683
const timestamp = parseInt(timestampAttr!);
15941684
const todayFromTimestamp = new Date(timestamp * 1000);
15951685
const actualToday = new Date();
1596-
15971686
expect(todayFromTimestamp.getDate()).to.equal(actualToday.getDate());
15981687
expect(todayFromTimestamp.getMonth()).to.equal(actualToday.getMonth());
15991688
expect(todayFromTimestamp.getFullYear()).to.equal(actualToday.getFullYear());

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

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ describe("DateTimePicker general interaction", () => {
119119
cy.get("[ui5-calendar]")
120120
.shadow()
121121
.as("calendar");
122+
123+
cy.realPress("Tab");
124+
cy.realPress("Tab");
122125

123126
cy.get("@calendar")
124127
.find("[ui5-daypicker]")
@@ -174,7 +177,7 @@ describe("DateTimePicker general interaction", () => {
174177

175178
cy.realPress("Tab");
176179

177-
// Simulate keyboard interactions
180+
//Simulate keyboard interactions
178181
cy.get("@dtp")
179182
.shadow()
180183
.find("[ui5-datetime-input]")
@@ -300,7 +303,7 @@ describe("DateTimePicker general interaction", () => {
300303
.ui5DateTimePickerClose();
301304
});
302305

303-
// Unstable test, needs investigation
306+
//Unstable test, needs investigation
304307
it("tests selection of 12:34:56 AM", () => {
305308
setAnimationMode(AnimationMode.None);
306309

@@ -324,8 +327,8 @@ describe("DateTimePicker general interaction", () => {
324327

325328
cy.get("@daypicker")
326329
.find(".ui5-dp-item--selected")
327-
.should("be.focused")
328-
.realClick();
330+
.realClick()
331+
.should("be.focused");
329332

330333
cy.get("[ui5-time-selection-clocks]")
331334
.shadow()
@@ -389,18 +392,18 @@ describe("DateTimePicker general interaction", () => {
389392
.find("ui5-daypicker")
390393
.as("daypicker");
391394

392-
// act: open the picker
395+
//act: open the picker
393396
cy.get<DateTimePicker>("@dtp")
394397
.ui5DateTimePickerOpen();
395398

396-
// act: click today's date
399+
//act: click today's date
397400
cy.get("@daypicker")
398401
.shadow()
399402
.find("[data-sap-focus-ref]")
400-
.should("be.focused")
401-
.realClick();
403+
.realClick()
404+
.should("be.focused");
402405

403-
// act: confirm selection
406+
//act: confirm selection
404407
cy.get<DateTimePicker>("@dtp")
405408
.ui5DateTimePickerGetSubmitButton()
406409
.should("have.prop", "disabled", false);
@@ -412,7 +415,7 @@ describe("DateTimePicker general interaction", () => {
412415
cy.get<DateTimePicker>("@dtp")
413416
.ui5DateTimePickerExpectToBeClosed();
414417

415-
// assert: the value is not changed
418+
//assert: the value is not changed
416419
cy.get("@input")
417420
.should("be.focused")
418421
.and("have.attr", "value", "");
@@ -488,7 +491,7 @@ describe("DateTimePicker general interaction", () => {
488491
.should("have.text", "Invalid entry");
489492
});
490493

491-
// Unstable test, needs investigation
494+
//Unstable test, needs investigation
492495
it("tests change event is fired on submit", () => {
493496
cy.mount(<DateTimePickerTemplate onChange={cy.stub().as("changeStub")} />);
494497

@@ -525,10 +528,10 @@ describe("DateTimePicker general interaction", () => {
525528
cy.get<DateTimePicker>("@dtp")
526529
.ui5DateTimePickerExpectToBeClosed();
527530

528-
// Assert the change event was fired once
531+
//Assert the change event was fired once
529532
cy.get("@changeStub").should("have.been.calledOnce");
530533

531-
// Re-open the picker and submit without making a change
534+
//Re-open the picker and submit without making a change
532535
cy.get<DateTimePicker>("@dtp")
533536
.ui5DateTimePickerOpen();
534537

@@ -540,11 +543,11 @@ describe("DateTimePicker general interaction", () => {
540543
.ui5DateTimePickerGetSubmitButton()
541544
.realClick();
542545

543-
// Verify the picker is closed
546+
//Verify the picker is closed
544547
cy.get<DateTimePicker>("@dtp")
545548
.ui5DateTimePickerExpectToBeClosed();
546549

547-
// The change event should not have been fired a second time.
550+
//The change event should not have been fired a second time.
548551
cy.get("@changeStub").should("have.been.calledOnce");
549552
});
550553
});

0 commit comments

Comments
 (0)