Skip to content

Commit e6a0cd8

Browse files
authored
feat(ui5-date-picker): add screen reader support (#2224)
Fixes: #1279
1 parent 9511d7a commit e6a0cd8

9 files changed

+201
-53
lines changed

packages/main/src/Calendar.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ class Calendar extends UI5Element {
458458

459459
if (lastDayOfMonthIndex !== -1) {
460460
// find the DOM for the last day index
461-
const lastDay = dayPicker.shadowRoot.querySelector(".ui5-dp-items-container").children[parseInt(lastDayOfMonthIndex / weekDaysCount)].children[(lastDayOfMonthIndex % weekDaysCount)];
461+
const lastDay = dayPicker.shadowRoot.querySelector(".ui5-dp-content").children[parseInt(lastDayOfMonthIndex / weekDaysCount) + 1].children[(lastDayOfMonthIndex % weekDaysCount)];
462462

463463
// update current item in ItemNavigation
464464
dayPicker._itemNav.current = lastDayOfMonthIndex;

packages/main/src/CalendarHeader.hbs

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class="{{_btnPrev.classes}}"
99
@click={{_handlePrevPress}}
1010
data-sap-cal-head-button="Prev"
11+
title="{{_prevButtonText}}"
1112
>
1213
<ui5-icon
1314
class="ui5-calheader-arrowicon"
@@ -45,6 +46,7 @@
4546
@click={{_handleNextPress}}
4647
id="{{_id}}-btnNext"
4748
data-sap-cal-head-button="Next"
49+
title={{_nextButtonText}}
4850
>
4951
<ui5-icon
5052
class="ui5-calheader-arrowicon"

packages/main/src/CalendarHeader.js

+19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
22
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
33
import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js";
4+
import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
45
import "@ui5/webcomponents-icons/dist/icons/slim-arrow-left.js";
56
import "@ui5/webcomponents-icons/dist/icons/slim-arrow-right.js";
67
import Button from "./Button.js";
78
import Icon from "./Icon.js";
89
import ButtonDesign from "./types/ButtonDesign.js";
910
import CalendarHeaderTemplate from "./generated/templates/CalendarHeaderTemplate.lit.js";
11+
import {
12+
CALENDAR_HEADER_NEXT_BUTTON,
13+
CALENDAR_HEADER_PREVIOUS_BUTTON,
14+
} from "./generated/i18n/i18n-defaults.js";
1015

1116
// Styles
1217
import styles from "./generated/themes/CalendarHeader.css.js";
@@ -81,6 +86,8 @@ class CalendarHeader extends UI5Element {
8186

8287
this._btn2 = {};
8388
this._btn2.type = ButtonDesign.Transparent;
89+
90+
this.i18nBundle = getI18nBundle("@ui5/webcomponents");
8491
}
8592

8693
onBeforeRendering() {
@@ -123,6 +130,18 @@ class CalendarHeader extends UI5Element {
123130
}
124131
}
125132
}
133+
134+
static async onDefine() {
135+
await fetchI18nBundle("@ui5/webcomponents");
136+
}
137+
138+
get _prevButtonText() {
139+
return this.i18nBundle.getText(CALENDAR_HEADER_PREVIOUS_BUTTON);
140+
}
141+
142+
get _nextButtonText() {
143+
return this.i18nBundle.getText(CALENDAR_HEADER_NEXT_BUTTON);
144+
}
126145
}
127146

128147
CalendarHeader.define();

packages/main/src/DayPicker.hbs

+24-24
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,8 @@
55
@mousedown={{_onmousedown}}
66
@mouseup={{_onmouseup}}
77
>
8-
9-
{{#unless _hideWeekNumbers}}
10-
<div class="ui5-dp-weeknumber-container">
11-
{{#each _weekNumbers}}
12-
<div class="ui5-dp-weekname-container">
13-
<span class="ui5-dp-weekname">{{this}}</span>
14-
</div>
15-
{{/each}}
16-
</div>
17-
{{/unless}}
18-
19-
<div id="{{_id}}-content" class="ui5-dp-content">
8+
9+
<div id="{{_id}}-content" class="ui5-dp-content" role="grid" aria-roledescription="Calendar">
2010
<div role="row" class="ui5-dp-days-names-container">
2111
{{#each _dayNames}}
2212
<div
@@ -28,18 +18,20 @@
2818
</div>
2919
{{/each}}
3020
</div>
31-
<div id="{{_id}}-days" class="ui5-dp-items-container" tabindex="-1">
32-
{{#each _weeks}}
33-
{{#if this.length}}
34-
<div style="display: flex;" @mouseover="{{../_onitemmouseover}}" @keydown="{{../_onitemkeydown}}">
35-
{{#each this}}
21+
{{#each _weeks}}
22+
{{#if this.length}}
23+
<div style="display: flex;" role="row" @mouseover="{{../_onitemmouseover}}" @keydown="{{../_onitemkeydown}}">
24+
{{#each this}}
25+
{{#if this.timestamp}}
3626
<div
3727
id="{{this.id}}"
3828
tabindex="{{this._tabIndex}}"
3929
data-sap-timestamp="{{this.timestamp}}"
4030
data-sap-index="{{this._index}}"
4131
role="gridcell"
4232
aria-selected="{{this.selected}}"
33+
aria-label="{{this.ariaLabel}}"
34+
aria-disabled="{{this.ariaDisabled}}"
4335
class="{{this.classes}}">
4436
<span
4537
class="ui5-dp-daytext"
@@ -48,12 +40,20 @@
4840
{{this.iDay}}
4941
</span>
5042
</div>
51-
{{/each}}
52-
</div>
53-
{{else}}
54-
<div class="sapWCEmptyWeek"></div>
55-
{{/if}}
56-
{{/each}}
57-
</div>
43+
{{else}}
44+
{{#unless this.isHidden}}
45+
<div class="ui5-dp-weekname-container"
46+
role="rowheader"
47+
aria-label="Calendar Week {{this.weekNum}}">
48+
<span class="ui5-dp-weekname">{{this.weekNum}}</span>
49+
</div>
50+
{{/unless}}
51+
{{/if}}
52+
{{/each}}
53+
</div>
54+
{{else}}
55+
<div class="sapWCEmptyWeek"></div>
56+
{{/if}}
57+
{{/each}}
5858
</div>
5959
</div>

packages/main/src/DayPicker.js

+43-20
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDat
1313
import calculateWeekNumber from "@ui5/webcomponents-localization/dist/dates/calculateWeekNumber.js";
1414
import CalendarType from "@ui5/webcomponents-base/dist/types/CalendarType.js";
1515
import ItemNavigationBehavior from "@ui5/webcomponents-base/dist/types/ItemNavigationBehavior.js";
16+
import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
1617
import DayPickerTemplate from "./generated/templates/DayPickerTemplate.lit.js";
1718

19+
import {
20+
DAY_PICKER_WEEK_NUMBER_TEXT,
21+
DAY_PICKER_NON_WORKING_DAY,
22+
} from "./generated/i18n/i18n-defaults.js";
23+
1824
// Styles
1925
import dayPickerCSS from "./generated/themes/DayPicker.css.js";
2026

@@ -134,15 +140,6 @@ const metadata = {
134140
multiple: true,
135141
},
136142

137-
/**
138-
* @type {Object}
139-
* @private
140-
*/
141-
_weekNumbers: {
142-
type: Object,
143-
multiple: true,
144-
},
145-
146143
/**
147144
* @type {boolean}
148145
* @private
@@ -229,6 +226,8 @@ class DayPicker extends UI5Element {
229226
"PageTop",
230227
this._handleMonthTopOverflow.bind(this)
231228
);
229+
230+
this.i18nBundle = getI18nBundle("@ui5/webcomponents");
232231
}
233232

234233
onBeforeRendering() {
@@ -243,6 +242,8 @@ class DayPicker extends UI5Element {
243242
let week = [];
244243
this._weekNumbers = [];
245244
let weekday;
245+
const _monthsNameWide = this._oLocaleData.getMonths("wide", this._calendarDate._oUDate.sCalendarType);
246+
246247
if (this.minDate) {
247248
this._minDateObject = new Date(this._minDate);
248249
}
@@ -260,6 +261,9 @@ class DayPicker extends UI5Element {
260261
if (weekday < 0) {
261262
weekday += 7;
262263
}
264+
265+
const nonWorkingAriaLabel = this._isWeekend(oCalDate) ? `${this._dayPickerNonWorkingDay} ` : "";
266+
263267
day = {
264268
timestamp: timestamp.toString(),
265269
selected: this._selectedDates.some(d => {
@@ -271,16 +275,9 @@ class DayPicker extends UI5Element {
271275
iDay: oCalDate.getDate(),
272276
_index: i.toString(),
273277
classes: `ui5-dp-item ui5-dp-wday${weekday}`,
278+
ariaLabel: `${nonWorkingAriaLabel}${_monthsNameWide[oCalDate.getMonth()]} ${oCalDate.getDate()}, ${oCalDate.getYear()}`,
274279
};
275280

276-
const weekNumber = calculateWeekNumber(getFirstDayOfWeek(), oCalDate.toUTCJSDate(), oCalDate.getYear(), this._oLocale, this._oLocaleData);
277-
278-
if (lastWeekNumber !== weekNumber) {
279-
this._weekNumbers.push(weekNumber);
280-
281-
lastWeekNumber = weekNumber;
282-
}
283-
284281
const isToday = oCalDate.isSame(CalendarDate.fromLocalJSDate(new Date(), this._primaryCalendarType));
285282

286283
week.push(day);
@@ -301,10 +298,12 @@ class DayPicker extends UI5Element {
301298
if (isToday) {
302299
day.classes += " ui5-dp-item--now";
303300
todayIndex = i;
301+
day.ariaLabel = `today ${day.ariaLabel}`;
304302
}
305303

306304
if (oCalDate.getMonth() !== this._month) {
307305
day.classes += " ui5-dp-item--othermonth";
306+
day.ariaDisabled = "true";
308307
}
309308

310309
day.id = `${this._id}-${timestamp}`;
@@ -317,8 +316,20 @@ class DayPicker extends UI5Element {
317316
day.disabled = true;
318317
}
319318

319+
this._hideWeekNumbers = this.shouldHideWeekNumbers;
320+
320321
if (day.classes.indexOf("ui5-dp-wday6") !== -1
321322
|| _aVisibleDays.length - 1 === i) {
323+
const weekNumber = calculateWeekNumber(getFirstDayOfWeek(), oCalDate.toUTCJSDate(), oCalDate.getYear(), this._oLocale, this._oLocaleData);
324+
if (lastWeekNumber !== weekNumber) {
325+
const weekNum = {
326+
weekNum: weekNumber,
327+
isHidden: this._hideWeekNumbers,
328+
};
329+
week.unshift(weekNum);
330+
lastWeekNumber = weekNumber;
331+
}
332+
322333
this._weeks.push(week);
323334
week = [];
324335
}
@@ -338,6 +349,10 @@ class DayPicker extends UI5Element {
338349
let dayName;
339350

340351
this._dayNames = [];
352+
this._dayNames.push({
353+
classes: "ui5-dp-dayname",
354+
name: this._dayPickerWeekNumberText,
355+
});
341356
for (let i = 0; i < 7; i++) {
342357
weekday = i + this._getFirstDayOfWeek();
343358
if (weekday > 6) {
@@ -353,8 +368,7 @@ class DayPicker extends UI5Element {
353368
this._dayNames.push(dayName);
354369
}
355370

356-
this._dayNames[0].classes += " ui5-dp-firstday";
357-
this._hideWeekNumbers = this.shouldHideWeekNumbers;
371+
this._dayNames[1].classes += " ui5-dp-firstday";
358372
}
359373

360374
onAfterRendering() {
@@ -489,13 +503,21 @@ class DayPicker extends UI5Element {
489503
const focusableDays = [];
490504

491505
for (let i = 0; i < this._weeks.length; i++) {
492-
const week = this._weeks[i].filter(x => !x.disabled);
506+
const week = this._weeks[i].slice(1).filter(x => !x.disabled);
493507
focusableDays.push(week);
494508
}
495509

496510
return [].concat(...focusableDays);
497511
}
498512

513+
get _dayPickerWeekNumberText() {
514+
return this.i18nBundle.getText(DAY_PICKER_WEEK_NUMBER_TEXT);
515+
}
516+
517+
get _dayPickerNonWorkingDay() {
518+
return this.i18nBundle.getText(DAY_PICKER_NON_WORKING_DAY);
519+
}
520+
499521
_modifySelectionAndNotifySubscribers(sNewDate, bAdd) {
500522
if (bAdd) {
501523
this.selectedDates = [...this._selectedDates, sNewDate];
@@ -737,6 +759,7 @@ class DayPicker extends UI5Element {
737759
static async onDefine() {
738760
await Promise.all([
739761
fetchCldr(getLocale().getLanguage(), getLocale().getRegion(), getLocale().getScript()),
762+
fetchI18nBundle("@ui5/webcomponents"),
740763
]);
741764
}
742765
}

packages/main/src/i18n/messagebundle.properties

+12
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,15 @@ VALUE_STATE_INFORMATION=Informative entry
360360

361361
#XTOL: text that is appended to the tooltips of input fields etc. which are marked to be in success state
362362
VALUE_STATE_SUCCESS=Entry successfully validated
363+
364+
#XBUT: Tooltip text for 'Next' button in the Calendar Header
365+
CALENDAR_HEADER_NEXT_BUTTON = Next
366+
367+
#XBUT: Tooltip text for 'Previous' button in the Calendar Header
368+
CALENDAR_HEADER_PREVIOUS_BUTTON = Previous
369+
370+
#XBUT: Text for 'Week number' in the DayPicker
371+
DAY_PICKER_WEEK_NUMBER_TEXT = Week number
372+
373+
#XBUT: Text for 'Non-Working Day' in the DayPicker
374+
DAY_PICKER_NON_WORKING_DAY = Non-Working Day

packages/main/test/pageobjects/DatePickerTestPage.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,26 @@ class DatePickerTestPage {
9999

100100
getDisplayedDay(index) {
101101
return browser
102-
.$(`.${this.staticAreaItemClassName}`).shadow$(`ui5-calendar`).shadow$(`ui5-daypicker`).shadow$(`.ui5-dp-root`).$(".ui5-dp-content").$(".ui5-dp-items-container")
103-
.$$(".ui5-dp-item")[index];
102+
.$(`.${this.staticAreaItemClassName}`).shadow$(`ui5-calendar`).shadow$(`ui5-daypicker`).shadow$(`.ui5-dp-root`).$(".ui5-dp-content").$$(".ui5-dp-item")[index];
103+
}
104+
105+
getDayPickerContent() {
106+
return browser
107+
.$(`.${this.staticAreaItemClassName}`).shadow$(`ui5-calendar`).shadow$(`ui5-daypicker`).shadow$(`.ui5-dp-root`).$$(".ui5-dp-content > div");
108+
}
109+
110+
getDayPickerDayNames() {
111+
const dayNames = Array.from(this.getDayPickerContent());
112+
return dayNames[0].$$("div");
113+
}
114+
115+
getDayPickerDatesRow(index) {
116+
const data = Array.from(this.getDayPickerContent());
117+
return data[index].$$("div");
118+
}
119+
120+
getDayPickerNumbers() {
121+
return Array.from(this.getDayPickerContent());
104122
}
105123

106124
isValid(value) {

0 commit comments

Comments
 (0)