Skip to content

Commit d52059b

Browse files
fbassomaxokorokov
authored andcommitted
feat(datepicker): add 'aria-label' attribute for days
Closes #2319 BREAKING CHANGE: if you're using a custom `NgbDatepickerI18n` implementation, you'll have to implement an additional method: `getDayAriaLabel(date: NgbDateStruct): string`. It returns the string that will be set for the `aria-label` attribute for each displayed day. If you're not using the custom service, the `aria-label` will default to the value returned by the angular `DatePipe` with `'fullDate'` format.
1 parent 6961cb3 commit d52059b

13 files changed

+157
-58
lines changed

demo/src/app/components/datepicker/demos/calendars/datepicker-calendars.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export class IslamicI18n extends NgbDatepickerI18n {
2424
getMonthFullName(month: number) {
2525
return this.getMonthShortName(month);
2626
}
27+
28+
getDayAriaLabel(date: NgbDateStruct): string {
29+
return `${date.day}-${date.month}-${date.year}`;
30+
}
2731
}
2832

2933
@Component({

demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, Injectable} from '@angular/core';
2-
import {NgbDatepickerI18n} from '@ng-bootstrap/ng-bootstrap';
2+
import { NgbDatepickerI18n, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
33

44
const I18N_VALUES = {
55
'fr': {
@@ -33,6 +33,10 @@ export class CustomDatepickerI18n extends NgbDatepickerI18n {
3333
getMonthFullName(month: number): string {
3434
return this.getMonthShortName(month);
3535
}
36+
37+
getDayAriaLabel(date: NgbDateStruct): string {
38+
return `${date.day}-${date.month}-${date.year}`;
39+
}
3640
}
3741

3842
@Component({

src/datepicker/datepicker-i18n.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import {NgbDatepickerI18nDefault} from './datepicker-i18n';
22
import {TestBed} from '@angular/core/testing';
33
import {LOCALE_ID} from '@angular/core';
4+
import {DatePipe} from '@angular/common';
45

56
describe('ngb-datepicker-i18n-default', () => {
67

78
let i18n: NgbDatepickerI18nDefault;
89

910
beforeEach(() => {
11+
TestBed.configureTestingModule({providers: [DatePipe]});
12+
1013
const locale: string = TestBed.get(LOCALE_ID);
11-
i18n = new NgbDatepickerI18nDefault(locale);
14+
const datePipe: DatePipe = TestBed.get(DatePipe);
15+
i18n = new NgbDatepickerI18nDefault(locale, datePipe);
1216
});
1317

1418
it('should return abbreviated month name', () => {

src/datepicker/datepicker-i18n.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
22
import {FormStyle, getLocaleDayNames, getLocaleMonthNames, TranslationWidth} from '@angular/common';
3+
import {DatePipe} from '@angular/common';
4+
import {NgbDateStruct} from './ngb-date-struct';
35

46
/**
57
* Type of the service supplying month and weekday names to to NgbDatepicker component.
@@ -26,6 +28,11 @@ export abstract class NgbDatepickerI18n {
2628
* With default calendar we use ISO 8601: 'month' is 1=January ... 12=December
2729
*/
2830
abstract getMonthFullName(month: number): string;
31+
32+
/**
33+
* Returns the aria-label string for a day
34+
*/
35+
abstract getDayAriaLabel(date: NgbDateStruct): string;
2936
}
3037

3138
@Injectable()
@@ -34,19 +41,24 @@ export class NgbDatepickerI18nDefault extends NgbDatepickerI18n {
3441
private _monthsShort: Array<string>;
3542
private _monthsFull: Array<string>;
3643

37-
constructor(@Inject(LOCALE_ID) locale: string) {
44+
constructor(@Inject(LOCALE_ID) private _locale: string, private _datePipe: DatePipe) {
3845
super();
3946

40-
const weekdaysStartingOnSunday = getLocaleDayNames(locale, FormStyle.Standalone, TranslationWidth.Short);
47+
const weekdaysStartingOnSunday = getLocaleDayNames(_locale, FormStyle.Standalone, TranslationWidth.Short);
4148
this._weekdaysShort = weekdaysStartingOnSunday.map((day, index) => weekdaysStartingOnSunday[(index + 1) % 7]);
4249

43-
this._monthsShort = getLocaleMonthNames(locale, FormStyle.Standalone, TranslationWidth.Abbreviated);
44-
this._monthsFull = getLocaleMonthNames(locale, FormStyle.Standalone, TranslationWidth.Wide);
50+
this._monthsShort = getLocaleMonthNames(_locale, FormStyle.Standalone, TranslationWidth.Abbreviated);
51+
this._monthsFull = getLocaleMonthNames(_locale, FormStyle.Standalone, TranslationWidth.Wide);
4552
}
4653

4754
getWeekdayShortName(weekday: number): string { return this._weekdaysShort[weekday - 1]; }
4855

4956
getMonthShortName(month: number): string { return this._monthsShort[month - 1]; }
5057

5158
getMonthFullName(month: number): string { return this._monthsFull[month - 1]; }
59+
60+
getDayAriaLabel(date: NgbDateStruct): string {
61+
const jsDate = new Date(date.year, date.month - 1, date.day);
62+
return this._datePipe.transform(jsDate, 'fullDate', null, this._locale);
63+
}
5264
}

src/datepicker/datepicker-month-view.spec.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,8 @@ class TestComponent {
230230
focused: false,
231231
selected: false
232232
},
233-
tabindex: -1
233+
tabindex: -1,
234+
ariaLabel: 'Monday'
234235
},
235236
{
236237
date: new NgbDate(2016, 8, 23),
@@ -241,7 +242,8 @@ class TestComponent {
241242
focused: false,
242243
selected: false
243244
},
244-
tabindex: -1
245+
tabindex: -1,
246+
ariaLabel: 'Tuesday'
245247
}
246248
]
247249
}]
@@ -267,7 +269,8 @@ class TestComponent {
267269
focused: false,
268270
selected: false
269271
},
270-
tabindex: -1
272+
tabindex: -1,
273+
ariaLabel: 'Monday'
271274
},
272275
{
273276
date: new NgbDate(2016, 8, 1),
@@ -278,7 +281,8 @@ class TestComponent {
278281
focused: false,
279282
selected: false
280283
},
281-
tabindex: -1
284+
tabindex: -1,
285+
ariaLabel: 'Monday'
282286
}
283287
]
284288
},
@@ -295,7 +299,8 @@ class TestComponent {
295299
focused: false,
296300
selected: false
297301
},
298-
tabindex: -1
302+
tabindex: -1,
303+
ariaLabel: 'Friday'
299304
},
300305
{
301306
date: new NgbDate(2016, 8, 3),
@@ -306,7 +311,8 @@ class TestComponent {
306311
focused: false,
307312
selected: false
308313
},
309-
tabindex: -1
314+
tabindex: -1,
315+
ariaLabel: 'Saturday'
310316
}
311317
]
312318
},
@@ -323,7 +329,8 @@ class TestComponent {
323329
focused: false,
324330
selected: false
325331
},
326-
tabindex: -1
332+
tabindex: -1,
333+
ariaLabel: 'Sunday'
327334
},
328335
{
329336
date: new NgbDate(2016, 9, 1),
@@ -334,7 +341,8 @@ class TestComponent {
334341
focused: false,
335342
selected: false
336343
},
337-
tabindex: -1
344+
tabindex: -1,
345+
ariaLabel: 'Saturday'
338346
}
339347
]
340348
},
@@ -351,7 +359,8 @@ class TestComponent {
351359
focused: false,
352360
selected: false
353361
},
354-
tabindex: -1
362+
tabindex: -1,
363+
ariaLabel: 'Sunday'
355364
},
356365
{
357366
date: new NgbDate(2016, 9, 3),
@@ -362,7 +371,8 @@ class TestComponent {
362371
focused: false,
363372
selected: false
364373
},
365-
tabindex: -1
374+
tabindex: -1,
375+
ariaLabel: 'Monday'
366376
}
367377
]
368378
}

src/datepicker/datepicker-month-view.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ import {DayTemplateContext} from './datepicker-day-template-context';
5151
<div *ngFor="let day of week.days" (click)="doSelect(day)" class="ngb-dp-day" role="gridcell"
5252
[class.disabled]="day.context.disabled"
5353
[tabindex]="day.tabindex"
54-
[class.hidden]="isHidden(day)">
54+
[class.hidden]="isHidden(day)"
55+
[attr.aria-label]="day.ariaLabel">
5556
<ng-template [ngIf]="!isHidden(day)">
5657
<ng-template [ngTemplateOutlet]="dayTemplate" [ngTemplateOutletContext]="day.context"></ng-template>
5758
</ng-template>

src/datepicker/datepicker-service.spec.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {NgbDate} from './ngb-date';
55
import {Subscription} from 'rxjs';
66
import {DatepickerViewModel} from './datepicker-view-model';
77
import {NgbDateStruct} from './ngb-date-struct';
8+
import {NgbDatepickerI18n, NgbDatepickerI18nDefault} from './datepicker-i18n';
9+
import {DatePipe} from '@angular/common';
810

911
describe('ngb-datepicker-service', () => {
1012

@@ -23,8 +25,12 @@ describe('ngb-datepicker-service', () => {
2325
const getDayCtx = (n: number) => getDay(n).context;
2426

2527
beforeEach(() => {
26-
TestBed.configureTestingModule(
27-
{providers: [NgbDatepickerService, {provide: NgbCalendar, useClass: NgbCalendarGregorian}]});
28+
TestBed.configureTestingModule({
29+
providers: [
30+
NgbDatepickerService, {provide: NgbCalendar, useClass: NgbCalendarGregorian},
31+
{provide: NgbDatepickerI18n, useClass: NgbDatepickerI18nDefault}, DatePipe
32+
]
33+
});
2834

2935
calendar = TestBed.get(NgbCalendar);
3036
service = TestBed.get(NgbDatepickerService);
@@ -320,6 +326,25 @@ describe('ngb-datepicker-service', () => {
320326
expect(getDayInMonth(1, 0, 6).tabindex).toEqual(0); // 1st april in the second month block
321327

322328
});
329+
330+
it(`should set the aria-label when changing the current month`, () => {
331+
service.displayMonths = 2;
332+
service.focus(new NgbDate(2018, 3, 31));
333+
334+
expect(getDayInMonth(0, 4, 5).ariaLabel)
335+
.toEqual('Saturday, March 31, 2018'); // 31 march in the first month block
336+
expect(getDayInMonth(1, 0, 5).ariaLabel)
337+
.toEqual('Saturday, March 31, 2018'); // 31 march in the second month block
338+
339+
service.focusMove('d', 1);
340+
expect(getDayInMonth(0, 4, 5).ariaLabel)
341+
.toEqual('Saturday, March 31, 2018'); // 31 march in the first month block
342+
expect(getDayInMonth(1, 0, 5).ariaLabel)
343+
.toEqual('Saturday, March 31, 2018'); // 31 march in the second month block
344+
expect(getDayInMonth(0, 4, 6).ariaLabel).toEqual('Sunday, April 1, 2018'); // 1st april in the first month block
345+
expect(getDayInMonth(1, 0, 6).ariaLabel).toEqual('Sunday, April 1, 2018'); // 1st april in the second month block
346+
347+
});
323348
});
324349

325350
describe(`disabled`, () => {

src/datepicker/datepicker-service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from './datepicker-tools';
1919

2020
import {filter} from 'rxjs/operators';
21+
import {NgbDatepickerI18n} from './datepicker-i18n';
2122

2223
@Injectable()
2324
export class NgbDatepickerService {
@@ -94,7 +95,7 @@ export class NgbDatepickerService {
9495
}
9596
}
9697

97-
constructor(private _calendar: NgbCalendar) {}
98+
constructor(private _calendar: NgbCalendar, private _i18n: NgbDatepickerI18n) {}
9899

99100
focus(date: NgbDate) {
100101
if (!this._state.disabled && this._calendar.isValid(date) && isChangedDate(this._state.focusDate, date)) {
@@ -159,7 +160,6 @@ export class NgbDatepickerService {
159160

160161
day.tabindex =
161162
(!state.disabled && day.date.equals(state.focusDate) && state.focusDate.month === month.number) ? 0 : -1;
162-
163163
// override context disabled
164164
if (state.disabled === true) {
165165
day.context.disabled = true;
@@ -221,7 +221,7 @@ export class NgbDatepickerService {
221221
const forceRebuild = 'firstDayOfWeek' in patch || 'markDisabled' in patch || 'minDate' in patch ||
222222
'maxDate' in patch || 'disabled' in patch;
223223

224-
const months = buildMonths(this._calendar, startDate, state, forceRebuild);
224+
const months = buildMonths(this._calendar, startDate, state, this._i18n, forceRebuild);
225225

226226
// updating months and boundary dates
227227
state.months = months;

0 commit comments

Comments
 (0)