From 55d1bc0ae512f9696999c02e4265cfc8c321cc29 Mon Sep 17 00:00:00 2001 From: Marco Gualberto Date: Sun, 11 Oct 2020 19:00:40 +0200 Subject: [PATCH 01/10] feat(b-calendar): add tabIndex property --- src/components/calendar/calendar.js | 9 ++++++++- src/components/calendar/calendar.spec.js | 1 + src/components/calendar/package.json | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js index 73a7fc37326..a6745ccf890 100644 --- a/src/components/calendar/calendar.js +++ b/src/components/calendar/calendar.js @@ -187,6 +187,10 @@ export const BCalendar = Vue.extend({ type: String // default: null }, + tabIndex: { + type: String, + default: '0' + }, roleDescription: { type: String // default: null @@ -920,6 +924,8 @@ export const BCalendar = Vue.extend({ attrs: { title: label || null, type: 'button', + disabled: btnDisabled, + tabindex: this.tabIndex, 'aria-label': label || null, 'aria-disabled': btnDisabled ? 'true' : null, 'aria-keyshortcuts': shortcut || null @@ -938,6 +944,7 @@ export const BCalendar = Vue.extend({ attrs: { id: navId, role: 'group', + tabindex: this.disabled ? null : this.tabIndex, 'aria-hidden': this.disabled ? 'true' : null, 'aria-label': this.labelNav || null, 'aria-controls': gridId @@ -1142,7 +1149,7 @@ export const BCalendar = Vue.extend({ attrs: { id: gridId, role: 'application', - tabindex: this.disabled ? null : '0', + tabindex: this.disabled ? null : this.tabIndex, 'data-month': activeYMD.slice(0, -3), // `YYYY-MM`, mainly for testing 'aria-roledescription': this.labelCalendar || null, 'aria-labelledby': gridCaptionId, diff --git a/src/components/calendar/calendar.spec.js b/src/components/calendar/calendar.spec.js index 65631508ff8..b2eebfa1ce5 100644 --- a/src/components/calendar/calendar.spec.js +++ b/src/components/calendar/calendar.spec.js @@ -31,6 +31,7 @@ describe('calendar', () => { expect($header.find('output').attributes('aria-atomic')).toEqual('true') expect(wrapper.find('.b-calendar>div>div.b-calendar-nav').exists()).toBe(true) expect(wrapper.find('.b-calendar>div>div.b-calendar-nav').attributes('role')).toEqual('group') + expect(wrapper.find('.b-calendar>div>div.b-calendar-nav').attributes('tabindex')).toEqual('0') expect(wrapper.findAll('.b-calendar>div>div.b-calendar-nav>button').length).toBe(5) expect(wrapper.find('.b-calendar>div>div[role="application"]').exists()).toBe(true) diff --git a/src/components/calendar/package.json b/src/components/calendar/package.json index dcb530dc984..9a157419b5d 100644 --- a/src/components/calendar/package.json +++ b/src/components/calendar/package.json @@ -89,6 +89,10 @@ "prop": "ariaControls", "description": "If the calendar controls another component/element, set this prop to the ID of the element the calendar controls" }, + { + "prop": "tabIndex", + "description": "Sets a tabindex property on all inner calendar components." + }, { "prop": "hideHeader", "description": "When `true`, visually hides the selected date header" From b61abd74e74f3126252ebdf1946f1902e08041fe Mon Sep 17 00:00:00 2001 From: Marco Gualberto Date: Wed, 14 Oct 2020 01:28:01 +0200 Subject: [PATCH 02/10] feat(b-calendar): change tabIndex property to noKeyNav --- src/components/calendar/calendar.js | 14 +++++++------- src/components/calendar/calendar.spec.js | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js index a6745ccf890..1b7db009955 100644 --- a/src/components/calendar/calendar.js +++ b/src/components/calendar/calendar.js @@ -187,9 +187,9 @@ export const BCalendar = Vue.extend({ type: String // default: null }, - tabIndex: { - type: String, - default: '0' + noKeyNav: { + type: Boolean, + default: false }, roleDescription: { type: String @@ -699,6 +699,7 @@ export const BCalendar = Vue.extend({ /* istanbul ignore next */ return } + if (this.noKeyNav) return stopEvent(evt) let activeDate = createDate(this.activeDate) let checkDate = createDate(this.activeDate) @@ -924,8 +925,7 @@ export const BCalendar = Vue.extend({ attrs: { title: label || null, type: 'button', - disabled: btnDisabled, - tabindex: this.tabIndex, + tabindex: this.noKeyNav ? -1 : null, 'aria-label': label || null, 'aria-disabled': btnDisabled ? 'true' : null, 'aria-keyshortcuts': shortcut || null @@ -944,7 +944,7 @@ export const BCalendar = Vue.extend({ attrs: { id: navId, role: 'group', - tabindex: this.disabled ? null : this.tabIndex, + tabindex: this.noKeyNav ? -1 : 0, 'aria-hidden': this.disabled ? 'true' : null, 'aria-label': this.labelNav || null, 'aria-controls': gridId @@ -1149,7 +1149,7 @@ export const BCalendar = Vue.extend({ attrs: { id: gridId, role: 'application', - tabindex: this.disabled ? null : this.tabIndex, + tabindex: this.noKeyNav ? -1 : 0, 'data-month': activeYMD.slice(0, -3), // `YYYY-MM`, mainly for testing 'aria-roledescription': this.labelCalendar || null, 'aria-labelledby': gridCaptionId, diff --git a/src/components/calendar/calendar.spec.js b/src/components/calendar/calendar.spec.js index b2eebfa1ce5..d603f3607b2 100644 --- a/src/components/calendar/calendar.spec.js +++ b/src/components/calendar/calendar.spec.js @@ -358,4 +358,23 @@ describe('calendar', () => { expect(buttons.at(3).classes()).toContain('btn-outline-primary') expect(buttons.at(4).classes()).toContain('btn-outline-primary') }) + + it('should disable key navigation', () => { + const wrapper = mount(BCalendar, { + attachTo: createContainer(), + propsData: { + noKeyNav: true, + navButtonVariant: 'primary' + } + }) + + const nav = wrapper.find('.b-calendar-nav') + const buttons = nav.findAll('button[tabindex="-1"]') + + expect(nav.attributes('tabindex')).toEqual('-1') + expect(buttons.length).toEqual(5) + expect(wrapper.find('.b-calendar>div>div[role="application"]').attributes('tabindex')).toEqual( + '-1' + ) + }) }) From 9ed27455530d439522892d2fe53aa35c75477701 Mon Sep 17 00:00:00 2001 From: Marco Gualberto Date: Wed, 14 Oct 2020 09:50:10 +0200 Subject: [PATCH 03/10] feat(b-calendar): add noKeyNav prop description --- src/components/calendar/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/calendar/package.json b/src/components/calendar/package.json index 9a157419b5d..7712a8d00c2 100644 --- a/src/components/calendar/package.json +++ b/src/components/calendar/package.json @@ -90,8 +90,8 @@ "description": "If the calendar controls another component/element, set this prop to the ID of the element the calendar controls" }, { - "prop": "tabIndex", - "description": "Sets a tabindex property on all inner calendar components." + "prop": "noKeyNav", + "description": "Disable keyboard navigation of the calendar components" }, { "prop": "hideHeader", From 31fa5fd70944f50d8893886bb1c7b3e358920e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Thu, 15 Oct 2020 10:45:30 +0200 Subject: [PATCH 04/10] Update calendar.js --- src/components/calendar/calendar.js | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js index a2d4f656ab2..b5aa852e4bd 100644 --- a/src/components/calendar/calendar.js +++ b/src/components/calendar/calendar.js @@ -680,6 +680,9 @@ export const BCalendar = Vue.extend({ // Calendar keyboard navigation // Handles PAGEUP/PAGEDOWN/END/HOME/LEFT/UP/RIGHT/DOWN // Focuses grid after updating + if (this.noKeyNav) { + return + } const { altKey, ctrlKey, keyCode } = evt if ( !arrayIncludes( @@ -699,7 +702,6 @@ export const BCalendar = Vue.extend({ /* istanbul ignore next */ return } - if (this.noKeyNav) return stopEvent(evt) let activeDate = createDate(this.activeDate) let checkDate = createDate(this.activeDate) @@ -836,6 +838,8 @@ export const BCalendar = Vue.extend({ gridCaptionId, gridHelpId, activeId, + disabled, + noKeyNav, isLive, isRTL, activeYMD, @@ -851,12 +855,12 @@ export const BCalendar = Vue.extend({ 'output', { staticClass: 'form-control form-control-sm text-center', - class: { 'text-muted': this.disabled, readonly: this.readonly || this.disabled }, + class: { 'text-muted': disabled, readonly: this.readonly || disabled }, attrs: { id: valueId, for: gridId, role: 'status', - tabindex: this.disabled ? null : '-1', + tabindex: disabled ? null : '-1', // Mainly for testing purposes, as we do not know // the exact format `Intl` will format the date string 'data-selected': toString(selectedYMD), @@ -925,7 +929,7 @@ export const BCalendar = Vue.extend({ attrs: { title: label || null, type: 'button', - tabindex: this.noKeyNav ? -1 : null, + tabindex: noKeyNav ? '-1' : null, 'aria-label': label || null, 'aria-disabled': btnDisabled ? 'true' : null, 'aria-keyshortcuts': shortcut || null @@ -944,8 +948,8 @@ export const BCalendar = Vue.extend({ attrs: { id: navId, role: 'group', - tabindex: this.noKeyNav ? -1 : 0, - 'aria-hidden': this.disabled ? 'true' : null, + tabindex: noKeyNav ? '-1' : null, + 'aria-hidden': disabled ? 'true' : null, 'aria-label': this.labelNav || null, 'aria-controls': gridId } @@ -1013,7 +1017,7 @@ export const BCalendar = Vue.extend({ { key: 'grid-caption', staticClass: 'b-calendar-grid-caption text-center font-weight-bold', - class: { 'text-muted': this.disabled }, + class: { 'text-muted': disabled }, attrs: { id: gridCaptionId, 'aria-live': isLive ? 'polite' : null, @@ -1036,7 +1040,7 @@ export const BCalendar = Vue.extend({ { key: idx, staticClass: 'col text-truncate', - class: { 'text-muted': this.disabled }, + class: { 'text-muted': disabled }, attrs: { title: d.label === d.text ? null : d.label, 'aria-label': d.label @@ -1064,7 +1068,7 @@ export const BCalendar = Vue.extend({ // Give the fake button a focus ring focus: isActive && this.gridHasFocus, // Styling - disabled: day.isDisabled || this.disabled, + disabled: day.isDisabled || disabled, active: isSelected, // makes the button look "pressed" // Selected date style (need to computed from variant) [this.computedVariant]: isSelected, @@ -1096,7 +1100,7 @@ export const BCalendar = Vue.extend({ 'data-date': day.ymd, // Primarily for testing purposes // Only days in the month are presented as buttons to screen readers 'aria-hidden': day.isThisMonth ? null : 'true', - 'aria-disabled': day.isDisabled || this.disabled ? 'true' : null, + 'aria-disabled': day.isDisabled || disabled ? 'true' : null, 'aria-label': [ day.label, isSelected ? `(${this.labelSelected})` : null, @@ -1125,7 +1129,7 @@ export const BCalendar = Vue.extend({ // A key is only required on the body if we add in transition support // key: this.activeYMD.slice(0, -3), staticClass: 'b-calendar-grid-body', - style: this.disabled ? { pointerEvents: 'none' } : {} + style: disabled ? { pointerEvents: 'none' } : {} }, $gridBody ) @@ -1149,15 +1153,15 @@ export const BCalendar = Vue.extend({ attrs: { id: gridId, role: 'application', - tabindex: this.noKeyNav ? -1 : 0, + tabindex: noKeyNav ? '-1' : '0', 'data-month': activeYMD.slice(0, -3), // `YYYY-MM`, mainly for testing 'aria-roledescription': this.labelCalendar || null, 'aria-labelledby': gridCaptionId, 'aria-describedby': gridHelpId, // `aria-readonly` is not considered valid on `role="application"` // https://www.w3.org/TR/wai-aria-1.1/#aria-readonly - // 'aria-readonly': this.readonly && !this.disabled ? 'true' : null, - 'aria-disabled': this.disabled ? 'true' : null, + // 'aria-readonly': this.readonly && !disabled ? 'true' : null, + 'aria-disabled': disabled ? 'true' : null, 'aria-activedescendant': activeId }, on: { @@ -1183,7 +1187,7 @@ export const BCalendar = Vue.extend({ dir: isRTL ? 'rtl' : 'ltr', lang: this.computedLocale || null, role: 'group', - 'aria-disabled': this.disabled ? 'true' : null, + 'aria-disabled': disabled ? 'true' : null, // If datepicker controls an input, this will specify the ID of the input 'aria-controls': this.ariaControls || null, // This should be a prop (so it can be changed to Date picker, etc, localized From 541f37f6be0e64eecda9b82bc630fd34652b42b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Thu, 15 Oct 2020 10:45:36 +0200 Subject: [PATCH 05/10] Update calendar.spec.js --- src/components/calendar/calendar.spec.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/calendar/calendar.spec.js b/src/components/calendar/calendar.spec.js index d603f3607b2..b688477162d 100644 --- a/src/components/calendar/calendar.spec.js +++ b/src/components/calendar/calendar.spec.js @@ -31,7 +31,6 @@ describe('calendar', () => { expect($header.find('output').attributes('aria-atomic')).toEqual('true') expect(wrapper.find('.b-calendar>div>div.b-calendar-nav').exists()).toBe(true) expect(wrapper.find('.b-calendar>div>div.b-calendar-nav').attributes('role')).toEqual('group') - expect(wrapper.find('.b-calendar>div>div.b-calendar-nav').attributes('tabindex')).toEqual('0') expect(wrapper.findAll('.b-calendar>div>div.b-calendar-nav>button').length).toBe(5) expect(wrapper.find('.b-calendar>div>div[role="application"]').exists()).toBe(true) @@ -359,7 +358,7 @@ describe('calendar', () => { expect(buttons.at(4).classes()).toContain('btn-outline-primary') }) - it('should disable key navigation', () => { + it('should disable key navigation when `no-key-nav` prop set', () => { const wrapper = mount(BCalendar, { attachTo: createContainer(), propsData: { @@ -368,11 +367,11 @@ describe('calendar', () => { } }) - const nav = wrapper.find('.b-calendar-nav') - const buttons = nav.findAll('button[tabindex="-1"]') + const $nav = wrapper.find('.b-calendar-nav') + const $buttons = $nav.findAll('button[tabindex="-1"]') - expect(nav.attributes('tabindex')).toEqual('-1') - expect(buttons.length).toEqual(5) + expect($nav.attributes('tabindex')).toEqual('-1') + expect($buttons.length).toEqual(5) expect(wrapper.find('.b-calendar>div>div[role="application"]').attributes('tabindex')).toEqual( '-1' ) From 396049230eea1fcbdbf133d332714892b9aeb7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Thu, 15 Oct 2020 10:47:38 +0200 Subject: [PATCH 06/10] Update calendar.js --- src/components/calendar/calendar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js index b5aa852e4bd..ec82e361d71 100644 --- a/src/components/calendar/calendar.js +++ b/src/components/calendar/calendar.js @@ -1153,7 +1153,7 @@ export const BCalendar = Vue.extend({ attrs: { id: gridId, role: 'application', - tabindex: noKeyNav ? '-1' : '0', + tabindex: noKeyNav ? '-1' : disabled ? null : '0', 'data-month': activeYMD.slice(0, -3), // `YYYY-MM`, mainly for testing 'aria-roledescription': this.labelCalendar || null, 'aria-labelledby': gridCaptionId, From 46e2945c112fc43260c36800e9a18d1b627b3f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Thu, 15 Oct 2020 11:09:58 +0200 Subject: [PATCH 07/10] Update calendar.spec.js --- src/components/calendar/calendar.spec.js | 36 ++++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/calendar/calendar.spec.js b/src/components/calendar/calendar.spec.js index b688477162d..57a2475fe83 100644 --- a/src/components/calendar/calendar.spec.js +++ b/src/components/calendar/calendar.spec.js @@ -340,24 +340,6 @@ describe('calendar', () => { wrapper.destroy() }) - it('`nav-button-variant` changes nav button class', async () => { - const wrapper = mount(BCalendar, { - attachTo: createContainer(), - propsData: { - navButtonVariant: 'primary' - } - }) - - const nav = wrapper.find('.b-calendar-nav') - const buttons = nav.findAll('button') - expect(buttons.length).toBe(5) - expect(buttons.at(0).classes()).toContain('btn-outline-primary') - expect(buttons.at(1).classes()).toContain('btn-outline-primary') - expect(buttons.at(2).classes()).toContain('btn-outline-primary') - expect(buttons.at(3).classes()).toContain('btn-outline-primary') - expect(buttons.at(4).classes()).toContain('btn-outline-primary') - }) - it('should disable key navigation when `no-key-nav` prop set', () => { const wrapper = mount(BCalendar, { attachTo: createContainer(), @@ -376,4 +358,22 @@ describe('calendar', () => { '-1' ) }) + + it('`nav-button-variant` changes nav button class', async () => { + const wrapper = mount(BCalendar, { + attachTo: createContainer(), + propsData: { + navButtonVariant: 'primary' + } + }) + + const nav = wrapper.find('.b-calendar-nav') + const buttons = nav.findAll('button') + expect(buttons.length).toBe(5) + expect(buttons.at(0).classes()).toContain('btn-outline-primary') + expect(buttons.at(1).classes()).toContain('btn-outline-primary') + expect(buttons.at(2).classes()).toContain('btn-outline-primary') + expect(buttons.at(3).classes()).toContain('btn-outline-primary') + expect(buttons.at(4).classes()).toContain('btn-outline-primary') + }) }) From bcb874e3c806660937fe01c7dcbb184874e0a8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Thu, 15 Oct 2020 11:10:43 +0200 Subject: [PATCH 08/10] Update calendar.spec.js --- src/components/calendar/calendar.spec.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/calendar/calendar.spec.js b/src/components/calendar/calendar.spec.js index 57a2475fe83..f32a655df67 100644 --- a/src/components/calendar/calendar.spec.js +++ b/src/components/calendar/calendar.spec.js @@ -367,13 +367,14 @@ describe('calendar', () => { } }) - const nav = wrapper.find('.b-calendar-nav') - const buttons = nav.findAll('button') - expect(buttons.length).toBe(5) - expect(buttons.at(0).classes()).toContain('btn-outline-primary') - expect(buttons.at(1).classes()).toContain('btn-outline-primary') - expect(buttons.at(2).classes()).toContain('btn-outline-primary') - expect(buttons.at(3).classes()).toContain('btn-outline-primary') - expect(buttons.at(4).classes()).toContain('btn-outline-primary') + const $nav = wrapper.find('.b-calendar-nav') + const $buttons = $nav.findAll('button') + + expect($buttons.length).toBe(5) + expect($buttons.at(0).classes()).toContain('btn-outline-primary') + expect($buttons.at(1).classes()).toContain('btn-outline-primary') + expect($buttons.at(2).classes()).toContain('btn-outline-primary') + expect($buttons.at(3).classes()).toContain('btn-outline-primary') + expect($buttons.at(4).classes()).toContain('btn-outline-primary') }) }) From 4acb26864257d0567a1aad073fda05dd468b058b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Fri, 16 Oct 2020 08:31:01 +0200 Subject: [PATCH 09/10] Update calendar.js --- src/components/calendar/calendar.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js index ec82e361d71..19bcd58008b 100644 --- a/src/components/calendar/calendar.js +++ b/src/components/calendar/calendar.js @@ -386,9 +386,8 @@ export const BCalendar = Vue.extend({ return isLocaleRTL(this.computedLocale) }, context() { - const selectedYMD = this.selectedYMD + const { selectedYMD, activeYMD } = this const selectedDate = parseYMD(selectedYMD) - const activeYMD = this.activeYMD const activeDate = parseYMD(activeYMD) return { // The current value of the `v-model` @@ -412,11 +411,10 @@ export const BCalendar = Vue.extend({ // Computed props that return a function reference dateOutOfRange() { // Check whether a date is within the min/max range - // returns a new function ref if the pops change + // Returns a new function ref if the pops change // We do this as we need to trigger the calendar computed prop // to update when these props update - const min = this.computedMin - const max = this.computedMax + const { computedMin: min, computedMax: max } = this return date => { // Handle both `YYYY-MM-DD` and `Date` objects date = parseYMD(date) @@ -776,8 +774,7 @@ export const BCalendar = Vue.extend({ }, onClickDay(day) { // Clicking on a date "button" to select it - const selectedDate = this.selectedDate - const activeDate = this.activeDate + const { selectedDate, activeDate } = this const clickedDate = parseYMD(day.ymd) if (!this.disabled && !day.isDisabled && !this.dateDisabled(clickedDate)) { if (!this.readonly) { From 17e418b320dd38d75b285ed78a6a824a290cdda6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Fri, 16 Oct 2020 08:46:22 +0200 Subject: [PATCH 10/10] Update calendar.js --- src/components/calendar/calendar.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js index 19bcd58008b..1cec00454ce 100644 --- a/src/components/calendar/calendar.js +++ b/src/components/calendar/calendar.js @@ -679,6 +679,7 @@ export const BCalendar = Vue.extend({ // Handles PAGEUP/PAGEDOWN/END/HOME/LEFT/UP/RIGHT/DOWN // Focuses grid after updating if (this.noKeyNav) { + /* istanbul ignore next */ return } const { altKey, ctrlKey, keyCode } = evt