Skip to content

Commit 7f62eb8

Browse files
committed
feat(Calendar, DatePicker, DateRangePicker): improve accessibility handling
1 parent dce6508 commit 7f62eb8

File tree

4 files changed

+71
-25
lines changed

4 files changed

+71
-25
lines changed

docs/content/forms/date-picker.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,10 @@ const datePickerList = datePickerElementList.map(datePickerEl => {
343343
{{< bs-table >}}
344344
| Name | Type | Default | Description |
345345
| --- | --- | --- | --- |
346+
| `ariaNavNextMonthLabel` | string | `'Next month'` | A string that provides an accessible label for the button that navigates to the next month in the calendar. This label is read by screen readers to describe the action associated with the button. |
347+
| `ariaNavNextYearLabel` | string | `'Next year'` | A string that provides an accessible label for the button that navigates to the next year in the calendar. This label is intended for screen readers to help users understand the button's functionality. |
348+
| `ariaNavPrevMonthLabel` | string | `'Previous month'` | A string that provides an accessible label for the button that navigates to the previous month in the calendar. Screen readers will use this label to explain the purpose of the button. |
349+
| `ariaNavPrevYearLabel` | string | `'Previous year'` | A string that provides an accessible label for the button that navigates to the previous year in the calendar. This label helps screen reader users understand the button's function. |
346350
| `calendarDate` | date, number, string, null | `null` | Default date of the component. |
347351
| `cancelButtonLabel` | string | `'Cancel'` | Cancel button inner HTML |
348352
| `cancelButtonClasses` | array, string | `['btn', 'btn-sm', 'btn-ghost-primary']` | CSS class names that will be added to the cancel button |

docs/content/forms/date-range-picker.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,10 @@ const dateRangePickerList = dateRangePickerElementList.map(dateRangePickerEl =>
387387
{{< bs-table >}}
388388
| Name | Type | Default | Description |
389389
| --- | --- | --- | --- |
390+
| `ariaNavNextMonthLabel` | string | `'Next month'` | A string that provides an accessible label for the button that navigates to the next month in the calendar. This label is read by screen readers to describe the action associated with the button. |
391+
| `ariaNavNextYearLabel` | string | `'Next year'` | A string that provides an accessible label for the button that navigates to the next year in the calendar. This label is intended for screen readers to help users understand the button's functionality. |
392+
| `ariaNavPrevMonthLabel` | string | `'Previous month'` | A string that provides an accessible label for the button that navigates to the previous month in the calendar. Screen readers will use this label to explain the purpose of the button. |
393+
| `ariaNavPrevYearLabel` | string | `'Previous year'` | A string that provides an accessible label for the button that navigates to the previous year in the calendar. This label helps screen reader users understand the button's function. |
390394
| `calendarDate` | date, number, string, null | `null` | Default date of the component. |
391395
| `calendars` | number | `2` | The number of calendars that render on desktop devices. |
392396
| `cancelButton` | string | `'Cancel'` | Cancel button inner HTML |

js/src/calendar.js

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,16 @@ const SELECTOR_BTN_MONTH = '.btn-month'
6767
const SELECTOR_BTN_NEXT = '.btn-next'
6868
const SELECTOR_BTN_PREV = '.btn-prev'
6969
const SELECTOR_BTN_YEAR = '.btn-year'
70-
const SELECTOR_CALENDAR = '.calendar'
7170
const SELECTOR_CALENDAR_CELL = '.calendar-cell'
7271
const SELECTOR_CALENDAR_PANEL = '.calendar-panel'
7372
const SELECTOR_CALENDAR_ROW = '.calendar-row'
73+
const SELECTOR_DATA_TOGGLE = '[data-coreui-toggle="calendar"]'
7474

7575
const Default = {
76+
ariaNavNextMonthLabel: 'Next month',
77+
ariaNavNextYearLabel: 'Next year',
78+
ariaNavPrevMonthLabel: 'Previous month',
79+
ariaNavPrevYearLabel: 'Previous year',
7680
calendarDate: null,
7781
calendars: 1,
7882
disabledDates: null,
@@ -81,7 +85,7 @@ const Default = {
8185
locale: 'default',
8286
maxDate: null,
8387
minDate: null,
84-
range: true,
88+
range: false,
8589
selectAdjacementDays: false,
8690
selectEndDate: false,
8791
selectionType: 'day',
@@ -93,6 +97,10 @@ const Default = {
9397
}
9498

9599
const DefaultType = {
100+
ariaNavNextMonthLabel: 'string',
101+
ariaNavNextYearLabel: 'string',
102+
ariaNavPrevMonthLabel: 'string',
103+
ariaNavPrevYearLabel: 'string',
96104
calendarDate: '(date|number|string|null)',
97105
calendars: 'number',
98106
disabledDates: '(array|null)',
@@ -225,7 +233,7 @@ class Calendar extends BaseComponent {
225233

226234
this._hoverDate = null
227235
this._selectDate(date)
228-
this._updateClassNames()
236+
this._updateClassNamesAndAriaLabels()
229237
}
230238

231239
_handleCalendarKeydown(event) {
@@ -361,7 +369,7 @@ class Calendar extends BaseComponent {
361369
date: getDateBySelectionType(date, this._config.selectionType)
362370
})
363371

364-
this._updateClassNames()
372+
this._updateClassNamesAndAriaLabels()
365373
}
366374

367375
_handleCalendarMouseLeave() {
@@ -371,7 +379,7 @@ class Calendar extends BaseComponent {
371379
date: null
372380
})
373381

374-
this._updateClassNames()
382+
this._updateClassNamesAndAriaLabels()
375383
}
376384

377385
_addEventListeners() {
@@ -574,14 +582,14 @@ class Calendar extends BaseComponent {
574582
navigationElement.classList.add('calendar-nav')
575583
navigationElement.innerHTML = `
576584
<div class="calendar-nav-prev">
577-
<button class="btn btn-transparent btn-sm btn-double-prev">
585+
<button class="btn btn-transparent btn-sm btn-double-prev" aria-label="${this._config.ariaNavPrevYearLabel}">
578586
<span class="calendar-nav-icon calendar-nav-icon-double-prev"></span>
579587
</button>
580-
${this._view === 'days' ? `<button class="btn btn-transparent btn-sm btn-prev">
588+
${this._view === 'days' ? `<button class="btn btn-transparent btn-sm btn-prev" aria-label="${this._config.ariaNavPrevMonthLabel}">
581589
<span class="calendar-nav-icon calendar-nav-icon-prev"></span>
582590
</button>` : ''}
583591
</div>
584-
<div class="calendar-nav-date">
592+
<div class="calendar-nav-date" aria-live="polite">
585593
${this._view === 'days' ? `<button class="btn btn-transparent btn-sm btn-month">
586594
${calendarDate.toLocaleDateString(this._config.locale, { month: 'long' })}
587595
</button>` : ''}
@@ -590,10 +598,10 @@ class Calendar extends BaseComponent {
590598
</button>
591599
</div>
592600
<div class="calendar-nav-next">
593-
${this._view === 'days' ? `<button class="btn btn-transparent btn-sm btn-next">
601+
${this._view === 'days' ? `<button class="btn btn-transparent btn-sm btn-next" aria-label="${this._config.ariaNavNextMonthLabel}">
594602
<span class="calendar-nav-icon calendar-nav-icon-next"></span>
595603
</button>` : ''}
596-
<button class="btn btn-transparent btn-sm btn-double-next">
604+
<button class="btn btn-transparent btn-sm btn-double-next" aria-label="${this._config.ariaNavNextYearLabel}">
597605
<span class="calendar-nav-icon calendar-nav-icon-double-next"></span>
598606
</button>
599607
</div>
@@ -617,7 +625,7 @@ class Calendar extends BaseComponent {
617625
</th>` : ''
618626
}
619627
${weekDays.map(({ date }) => (
620-
`<th class="calendar-cell">
628+
`<th class="calendar-cell" abbr="${date.toLocaleDateString(this._config.locale, { weekday: 'long' })}">
621629
<div class="calendar-header-cell-inner">
622630
${typeof this._config.weekdayFormat === 'string' ?
623631
date.toLocaleDateString(this._config.locale, { weekday: this._config.weekdayFormat }) :
@@ -645,6 +653,7 @@ class Calendar extends BaseComponent {
645653
week.days.some(day => day.month === 'current') &&
646654
!isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates) ? 0 : -1
647655
}"
656+
${isDateSelected(date, this._startDate, this._endDate) ? 'aria-selected="true"' : ''}
648657
>
649658
${this._config.showWeekNumber ?
650659
`<th class="calendar-cell-week-number">${week.weekNumber === 0 ? 53 : week.weekNumber}</td>` : ''
@@ -658,6 +667,7 @@ class Calendar extends BaseComponent {
658667
(month === 'current' || this._config.selectAdjacementDays) &&
659668
!isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates) ? 0 : -1
660669
}"
670+
${isDateSelected(date, this._startDate, this._endDate) ? 'aria-selected="true"' : ''}
661671
data-coreui-date="${date}"
662672
>
663673
<div class="calendar-cell-inner day">
@@ -677,6 +687,7 @@ class Calendar extends BaseComponent {
677687
class="calendar-cell ${this._sharedClassNames(date)}"
678688
data-coreui-date="${date.toDateString()}"
679689
tabindex="${isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates) ? -1 : 0}"
690+
${isDateSelected(date, this._startDate, this._endDate) ? 'aria-selected="true"' : ''}
680691
>
681692
<div class="calendar-cell-inner month">
682693
${month}
@@ -695,6 +706,7 @@ class Calendar extends BaseComponent {
695706
class="calendar-cell ${this._sharedClassNames(date)}"
696707
data-coreui-date="${date.toDateString()}"
697708
tabindex="${isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates) ? -1 : 0}"
709+
${isDateSelected(date, this._startDate, this._endDate) ? 'aria-selected="true"' : ''}
698710
>
699711
<div class="calendar-cell-inner year">
700712
${year}
@@ -740,7 +752,7 @@ class Calendar extends BaseComponent {
740752
}
741753
}
742754

743-
_updateClassNames() {
755+
_updateClassNamesAndAriaLabels() {
744756
if (this._config.selectionType === 'week') {
745757
const rows = SelectorEngine.find(SELECTOR_CALENDAR_ROW, this._element)
746758

@@ -750,6 +762,12 @@ class Calendar extends BaseComponent {
750762
const classNames = this._sharedClassNames(date)
751763

752764
row.className = `${CLASS_NAME_CALENDAR_ROW} ${classNames}`
765+
766+
if (isDateSelected(date, this._startDate, this._endDate)) {
767+
row.setAttribute('aria-selected', true)
768+
} else {
769+
row.removeAttribute('aria-selected')
770+
}
753771
}
754772

755773
return
@@ -762,6 +780,12 @@ class Calendar extends BaseComponent {
762780
const classNames = this._config.selectionType === 'day' ? this._dayClassNames(date, 'current') : this._sharedClassNames(date)
763781

764782
cell.className = `${CLASS_NAME_CALENDAR_CELL} ${classNames}`
783+
784+
if (isDateSelected(date, this._startDate, this._endDate)) {
785+
cell.setAttribute('aria-selected', true)
786+
} else {
787+
cell.removeAttribute('aria-selected')
788+
}
765789
}
766790
}
767791

@@ -781,12 +805,13 @@ class Calendar extends BaseComponent {
781805
[month]: true
782806
}
783807

784-
// eslint-disable-next-line unicorn/no-array-reduce
785-
const result = Object.keys(classNames).reduce((o, key) => {
786-
// eslint-disable-next-line no-unused-expressions
787-
classNames[key] === true && (o[key] = classNames[key])
788-
return o
789-
}, {})
808+
const result = {}
809+
810+
for (const key in classNames) {
811+
if (classNames[key] === true) {
812+
result[key] = true
813+
}
814+
}
790815

791816
return Object.keys(result).join(' ')
792817
}
@@ -805,12 +830,13 @@ class Calendar extends BaseComponent {
805830
selected: isDateSelected(date, this._startDate, this._endDate)
806831
}
807832

808-
// eslint-disable-next-line unicorn/no-array-reduce
809-
const result = Object.keys(classNames).reduce((o, key) => {
810-
// eslint-disable-next-line no-unused-expressions
811-
classNames[key] === true && (o[key] = classNames[key])
812-
return o
813-
}, {})
833+
const result = {}
834+
835+
for (const key in classNames) {
836+
if (classNames[key] === true) {
837+
result[key] = true
838+
}
839+
}
814840

815841
return Object.keys(result).join(' ')
816842
}
@@ -851,7 +877,7 @@ class Calendar extends BaseComponent {
851877
*/
852878

853879
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
854-
for (const element of Array.from(document.querySelectorAll(SELECTOR_CALENDAR))) {
880+
for (const element of Array.from(document.querySelectorAll(SELECTOR_DATA_TOGGLE))) {
855881
Calendar.calendarInterface(element)
856882
}
857883
})

js/src/date-range-picker.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ const SELECTOR_INPUT = '.date-picker-input'
7171
const SELECTOR_WAS_VALIDATED = 'form.was-validated'
7272

7373
const Default = {
74+
ariaNavNextMonthLabel: 'Next month',
75+
ariaNavNextYearLabel: 'Next year',
76+
ariaNavPrevMonthLabel: 'Previous month',
77+
ariaNavPrevYearLabel: 'Previous year',
7478
calendars: 2,
7579
cancelButton: 'Cancel',
7680
cancelButtonClasses: ['btn', 'btn-sm', 'btn-ghost-primary'],
@@ -116,6 +120,10 @@ const Default = {
116120
}
117121

118122
const DefaultType = {
123+
ariaNavNextMonthLabel: 'string',
124+
ariaNavNextYearLabel: 'string',
125+
ariaNavPrevMonthLabel: 'string',
126+
ariaNavPrevYearLabel: 'string',
119127
calendars: 'number',
120128
cancelButton: '(boolean|string)',
121129
cancelButtonClasses: '(array|string)',
@@ -422,6 +430,10 @@ class DateRangePicker extends BaseComponent {
422430

423431
_getCalendarConfig() {
424432
return {
433+
ariaNavNextMonthLabel: this._config.ariaNavNextMonthLabel,
434+
ariaNavNextYearLabel: this._config.ariaNavNextYearLabel,
435+
ariaNavPrevMonthLabel: this._config.ariaNavPrevMonthLabel,
436+
ariaNavPrevYearLabel: this._config.ariaNavPrevYearLabel,
425437
calendarDate: this._calendarDate,
426438
calendars: this._config.calendars,
427439
disabledDates: this._config.disabledDates,

0 commit comments

Comments
 (0)