Skip to content

Commit 955ad63

Browse files
feat(b-calendar): add no-key-nav property (closes #5861) (#5883)
* feat(b-calendar): add tabIndex property * feat(b-calendar): change tabIndex property to noKeyNav * feat(b-calendar): add noKeyNav prop description * Update calendar.js * Update calendar.spec.js * Update calendar.js * Update calendar.spec.js * Update calendar.spec.js * Update calendar.js * Update calendar.js Co-authored-by: Jacob Müller <jacob.mueller.elz@gmail.com>
1 parent f1de1b3 commit 955ad63

File tree

3 files changed

+60
-27
lines changed

3 files changed

+60
-27
lines changed

src/components/calendar/calendar.js

+28-19
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ export const BCalendar = Vue.extend({
187187
type: String
188188
// default: null
189189
},
190+
noKeyNav: {
191+
type: Boolean,
192+
default: false
193+
},
190194
roleDescription: {
191195
type: String
192196
// default: null
@@ -382,9 +386,8 @@ export const BCalendar = Vue.extend({
382386
return isLocaleRTL(this.computedLocale)
383387
},
384388
context() {
385-
const selectedYMD = this.selectedYMD
389+
const { selectedYMD, activeYMD } = this
386390
const selectedDate = parseYMD(selectedYMD)
387-
const activeYMD = this.activeYMD
388391
const activeDate = parseYMD(activeYMD)
389392
return {
390393
// The current value of the `v-model`
@@ -408,11 +411,10 @@ export const BCalendar = Vue.extend({
408411
// Computed props that return a function reference
409412
dateOutOfRange() {
410413
// Check whether a date is within the min/max range
411-
// returns a new function ref if the pops change
414+
// Returns a new function ref if the pops change
412415
// We do this as we need to trigger the calendar computed prop
413416
// to update when these props update
414-
const min = this.computedMin
415-
const max = this.computedMax
417+
const { computedMin: min, computedMax: max } = this
416418
return date => {
417419
// Handle both `YYYY-MM-DD` and `Date` objects
418420
date = parseYMD(date)
@@ -676,6 +678,10 @@ export const BCalendar = Vue.extend({
676678
// Calendar keyboard navigation
677679
// Handles PAGEUP/PAGEDOWN/END/HOME/LEFT/UP/RIGHT/DOWN
678680
// Focuses grid after updating
681+
if (this.noKeyNav) {
682+
/* istanbul ignore next */
683+
return
684+
}
679685
const { altKey, ctrlKey, keyCode } = evt
680686
if (
681687
!arrayIncludes(
@@ -769,8 +775,7 @@ export const BCalendar = Vue.extend({
769775
},
770776
onClickDay(day) {
771777
// Clicking on a date "button" to select it
772-
const selectedDate = this.selectedDate
773-
const activeDate = this.activeDate
778+
const { selectedDate, activeDate } = this
774779
const clickedDate = parseYMD(day.ymd)
775780
if (!this.disabled && !day.isDisabled && !this.dateDisabled(clickedDate)) {
776781
if (!this.readonly) {
@@ -831,6 +836,8 @@ export const BCalendar = Vue.extend({
831836
gridCaptionId,
832837
gridHelpId,
833838
activeId,
839+
disabled,
840+
noKeyNav,
834841
isLive,
835842
isRTL,
836843
activeYMD,
@@ -846,12 +853,12 @@ export const BCalendar = Vue.extend({
846853
'output',
847854
{
848855
staticClass: 'form-control form-control-sm text-center',
849-
class: { 'text-muted': this.disabled, readonly: this.readonly || this.disabled },
856+
class: { 'text-muted': disabled, readonly: this.readonly || disabled },
850857
attrs: {
851858
id: valueId,
852859
for: gridId,
853860
role: 'status',
854-
tabindex: this.disabled ? null : '-1',
861+
tabindex: disabled ? null : '-1',
855862
// Mainly for testing purposes, as we do not know
856863
// the exact format `Intl` will format the date string
857864
'data-selected': toString(selectedYMD),
@@ -920,6 +927,7 @@ export const BCalendar = Vue.extend({
920927
attrs: {
921928
title: label || null,
922929
type: 'button',
930+
tabindex: noKeyNav ? '-1' : null,
923931
'aria-label': label || null,
924932
'aria-disabled': btnDisabled ? 'true' : null,
925933
'aria-keyshortcuts': shortcut || null
@@ -938,7 +946,8 @@ export const BCalendar = Vue.extend({
938946
attrs: {
939947
id: navId,
940948
role: 'group',
941-
'aria-hidden': this.disabled ? 'true' : null,
949+
tabindex: noKeyNav ? '-1' : null,
950+
'aria-hidden': disabled ? 'true' : null,
942951
'aria-label': this.labelNav || null,
943952
'aria-controls': gridId
944953
}
@@ -1006,7 +1015,7 @@ export const BCalendar = Vue.extend({
10061015
{
10071016
key: 'grid-caption',
10081017
staticClass: 'b-calendar-grid-caption text-center font-weight-bold',
1009-
class: { 'text-muted': this.disabled },
1018+
class: { 'text-muted': disabled },
10101019
attrs: {
10111020
id: gridCaptionId,
10121021
'aria-live': isLive ? 'polite' : null,
@@ -1029,7 +1038,7 @@ export const BCalendar = Vue.extend({
10291038
{
10301039
key: idx,
10311040
staticClass: 'col text-truncate',
1032-
class: { 'text-muted': this.disabled },
1041+
class: { 'text-muted': disabled },
10331042
attrs: {
10341043
title: d.label === d.text ? null : d.label,
10351044
'aria-label': d.label
@@ -1057,7 +1066,7 @@ export const BCalendar = Vue.extend({
10571066
// Give the fake button a focus ring
10581067
focus: isActive && this.gridHasFocus,
10591068
// Styling
1060-
disabled: day.isDisabled || this.disabled,
1069+
disabled: day.isDisabled || disabled,
10611070
active: isSelected, // makes the button look "pressed"
10621071
// Selected date style (need to computed from variant)
10631072
[this.computedVariant]: isSelected,
@@ -1089,7 +1098,7 @@ export const BCalendar = Vue.extend({
10891098
'data-date': day.ymd, // Primarily for testing purposes
10901099
// Only days in the month are presented as buttons to screen readers
10911100
'aria-hidden': day.isThisMonth ? null : 'true',
1092-
'aria-disabled': day.isDisabled || this.disabled ? 'true' : null,
1101+
'aria-disabled': day.isDisabled || disabled ? 'true' : null,
10931102
'aria-label': [
10941103
day.label,
10951104
isSelected ? `(${this.labelSelected})` : null,
@@ -1118,7 +1127,7 @@ export const BCalendar = Vue.extend({
11181127
// A key is only required on the body if we add in transition support
11191128
// key: this.activeYMD.slice(0, -3),
11201129
staticClass: 'b-calendar-grid-body',
1121-
style: this.disabled ? { pointerEvents: 'none' } : {}
1130+
style: disabled ? { pointerEvents: 'none' } : {}
11221131
},
11231132
$gridBody
11241133
)
@@ -1142,15 +1151,15 @@ export const BCalendar = Vue.extend({
11421151
attrs: {
11431152
id: gridId,
11441153
role: 'application',
1145-
tabindex: this.disabled ? null : '0',
1154+
tabindex: noKeyNav ? '-1' : disabled ? null : '0',
11461155
'data-month': activeYMD.slice(0, -3), // `YYYY-MM`, mainly for testing
11471156
'aria-roledescription': this.labelCalendar || null,
11481157
'aria-labelledby': gridCaptionId,
11491158
'aria-describedby': gridHelpId,
11501159
// `aria-readonly` is not considered valid on `role="application"`
11511160
// https://www.w3.org/TR/wai-aria-1.1/#aria-readonly
1152-
// 'aria-readonly': this.readonly && !this.disabled ? 'true' : null,
1153-
'aria-disabled': this.disabled ? 'true' : null,
1161+
// 'aria-readonly': this.readonly && !disabled ? 'true' : null,
1162+
'aria-disabled': disabled ? 'true' : null,
11541163
'aria-activedescendant': activeId
11551164
},
11561165
on: {
@@ -1176,7 +1185,7 @@ export const BCalendar = Vue.extend({
11761185
dir: isRTL ? 'rtl' : 'ltr',
11771186
lang: this.computedLocale || null,
11781187
role: 'group',
1179-
'aria-disabled': this.disabled ? 'true' : null,
1188+
'aria-disabled': disabled ? 'true' : null,
11801189
// If datepicker controls an input, this will specify the ID of the input
11811190
'aria-controls': this.ariaControls || null,
11821191
// This should be a prop (so it can be changed to Date picker, etc, localized

src/components/calendar/calendar.spec.js

+28-8
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,25 @@ describe('calendar', () => {
340340
wrapper.destroy()
341341
})
342342

343+
it('should disable key navigation when `no-key-nav` prop set', () => {
344+
const wrapper = mount(BCalendar, {
345+
attachTo: createContainer(),
346+
propsData: {
347+
noKeyNav: true,
348+
navButtonVariant: 'primary'
349+
}
350+
})
351+
352+
const $nav = wrapper.find('.b-calendar-nav')
353+
const $buttons = $nav.findAll('button[tabindex="-1"]')
354+
355+
expect($nav.attributes('tabindex')).toEqual('-1')
356+
expect($buttons.length).toEqual(5)
357+
expect(wrapper.find('.b-calendar>div>div[role="application"]').attributes('tabindex')).toEqual(
358+
'-1'
359+
)
360+
})
361+
343362
it('`nav-button-variant` changes nav button class', async () => {
344363
const wrapper = mount(BCalendar, {
345364
attachTo: createContainer(),
@@ -348,13 +367,14 @@ describe('calendar', () => {
348367
}
349368
})
350369

351-
const nav = wrapper.find('.b-calendar-nav')
352-
const buttons = nav.findAll('button')
353-
expect(buttons.length).toBe(5)
354-
expect(buttons.at(0).classes()).toContain('btn-outline-primary')
355-
expect(buttons.at(1).classes()).toContain('btn-outline-primary')
356-
expect(buttons.at(2).classes()).toContain('btn-outline-primary')
357-
expect(buttons.at(3).classes()).toContain('btn-outline-primary')
358-
expect(buttons.at(4).classes()).toContain('btn-outline-primary')
370+
const $nav = wrapper.find('.b-calendar-nav')
371+
const $buttons = $nav.findAll('button')
372+
373+
expect($buttons.length).toBe(5)
374+
expect($buttons.at(0).classes()).toContain('btn-outline-primary')
375+
expect($buttons.at(1).classes()).toContain('btn-outline-primary')
376+
expect($buttons.at(2).classes()).toContain('btn-outline-primary')
377+
expect($buttons.at(3).classes()).toContain('btn-outline-primary')
378+
expect($buttons.at(4).classes()).toContain('btn-outline-primary')
359379
})
360380
})

src/components/calendar/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@
8989
"prop": "ariaControls",
9090
"description": "If the calendar controls another component/element, set this prop to the ID of the element the calendar controls"
9191
},
92+
{
93+
"prop": "noKeyNav",
94+
"description": "Disable keyboard navigation of the calendar components"
95+
},
9296
{
9397
"prop": "hideHeader",
9498
"description": "When `true`, visually hides the selected date header"

0 commit comments

Comments
 (0)