Skip to content

Commit b1f74a8

Browse files
authored
feat(b-calendar, b-form-datepicker): add optional decade navigation buttons (addresses #4976) (#5112)
Co-authored-by: Jacob Müller
1 parent 73966db commit b1f74a8

File tree

25 files changed

+295
-95
lines changed

25 files changed

+295
-95
lines changed

src/components/avatar/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,4 @@ Avatars are based upon `<b-badge>` and `<b-button>` components, and as such, rel
330330
`badge-*` and `btn-*` variant classes, as well as the `rounded-*`
331331
[utility classes](/docs/reference/utility-classes).
332332

333-
`<b-avatar>` also requires BootstrapVue's custom CSS for proper styling.
333+
`<b-avatar>` also requires BootstrapVue's custom CSS for proper styling.

src/components/button-toolbar/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,12 @@ Enable optional keyboard navigation by setting the prop `key-nav`.
9191

9292
| Keypress | Action |
9393
| --------------------------------------------------------------------- | ----------------------------------------------------- |
94-
| <kbd>LEFT</kbd> or <kbd>UP</kbd> | Move to the previous non-disabled item in the toolbar |
95-
| <kbd>RIGHT</kbd> or <kbd>DOWN</kbd> | Move to the next non-disabled item in the toolbar |
96-
| <kbd>SHIFT</kbd>+<kbd>LEFT</kbd> or <kbd>SHIFT</kbd>+<kbd>UP</kbd> | Move to the first non-disabled item in the toolbar |
97-
| <kbd>SHIFT</kbd>+<kbd>RIGHT</kbd> or <kbd>SHIFT</kbd>+<kbd>DOWN</kbd> | Move to the last non-disabled item in the toolbar |
98-
| <kbd>TAB</kbd> | Move to the next control on the page |
99-
| <kbd>SHIFT</kbd>+<kbd>TAB</kbd> | Move to the previous control on the page |
94+
| <kbd>Left</kbd> or <kbd>Up</kbd> | Move to the previous non-disabled item in the toolbar |
95+
| <kbd>Right</kbd> or <kbd>Down</kbd> | Move to the next non-disabled item in the toolbar |
96+
| <kbd>Shift</kbd>+<kbd>Left</kbd> or <kbd>Shift</kbd>+<kbd>Up</kbd> | Move to the first non-disabled item in the toolbar |
97+
| <kbd>Shift</kbd>+<kbd>Right</kbd> or <kbd>Shift</kbd>+<kbd>Down</kbd> | Move to the last non-disabled item in the toolbar |
98+
| <kbd>Tab</kbd> | Move to the next control on the page |
99+
| <kbd>Shift</kbd>+<kbd>Tab</kbd> | Move to the previous control on the page |
100100

101101
**Caution:** If you have text or text-like inputs in your toolbar, leave keyboard navigation off, as
102102
it is not possible to use key presses to jump out of a text (or test-like) inputs.

src/components/button/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
## Overview
77

8-
BootstrapVue's `<b-button>` component generates either a `<button>` element, `<a>` element, or
8+
BootstrapVue's `<b-button>` component generates either a `<button>` element, `<a>` element, or
99
`<router-link>` component with the styling of a button.
1010

1111
```html

src/components/calendar/README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,18 @@ formatted in the locale's language.
290290
You can hide this header via the `hide-header` prop. Note this only _visually hides_ the selected
291291
date, while keeping it available to screen reader users as an `aria-live` region.
292292

293+
For example usage, refer to the [Internationalization section](#internationalization) below.
294+
295+
### Optional decade navigation buttons
296+
297+
Set the prop `show-decade-nav` to enable the previous and next decade buttons in the calendar's date
298+
navigation toolbar.
299+
300+
The props `label-prev-decade` and `label-next-decade` props can be used to provide custom label text
301+
for the decade buttons.
302+
303+
For example usage, refer to the [Internationalization section](#internationalization) below.
304+
293305
### Border and padding
294306

295307
Fancy a calendar with a border with padding? Use Bootstrap's
@@ -489,15 +501,23 @@ the same locale as requested, depending on the supported locales of `Intl`).
489501
<b-col cols="12" class="mb-3">
490502
<label for="example-locales">Locale:</label>
491503
<b-form-select id="example-locales" v-model="locale" :options="locales"></b-form-select>
492-
<label for="example-weekdays">Start weekday:</label>
504+
<label for="example-weekdays" class="mt-2">Start weekday:</label>
493505
<b-form-select id="example-weekdays" v-model="weekday" :options="weekdays"></b-form-select>
506+
<b-form-checkbox v-model="showDecadeNav" switch inline class="my-2">
507+
Show decade navigation buttons
508+
</b-form-checkbox>
509+
<b-form-checkbox v-model="hideHeader" switch inline class="my-2">
510+
Hide the date header
511+
</b-form-checkbox>
494512
</b-col>
495513
<b-col md="auto">
496514
<b-calendar
497515
v-model="value"
498516
v-bind="labels[locale] || {}"
499517
:locale="locale"
500518
:start-weekday="weekday"
519+
:hide-header="hideHeader"
520+
:show-decade-nav="showDecadeNav"
501521
@context="onContext"
502522
></b-calendar>
503523
</b-col>
@@ -515,6 +535,8 @@ the same locale as requested, depending on the supported locales of `Intl`).
515535
return {
516536
value: '',
517537
context: null,
538+
showDecadeNav: false,
539+
hideHeader: false,
518540
locale: 'en-US',
519541
locales: [
520542
{ value: 'en-US', text: 'English US (en-US)' },
@@ -530,11 +552,13 @@ the same locale as requested, depending on the supported locales of `Intl`).
530552
],
531553
labels: {
532554
de: {
555+
labelPrevDecade: 'Vorheriges Jahrzehnt',
533556
labelPrevYear: 'Vorheriges Jahr',
534557
labelPrevMonth: 'Vorheriger Monat',
535558
labelCurrentMonth: 'Aktueller Monat',
536559
labelNextMonth: 'Nächster Monat',
537560
labelNextYear: 'Nächstes Jahr',
561+
labelNextDecade: 'Nächstes Jahrzehnt',
538562
labelToday: 'Heute',
539563
labelSelected: 'Ausgewähltes Datum',
540564
labelNoDateSelected: 'Kein Datum gewählt',
@@ -543,11 +567,13 @@ the same locale as requested, depending on the supported locales of `Intl`).
543567
labelHelp: 'Mit den Pfeiltasten durch den Kalender navigieren'
544568
},
545569
'ar-EG': {
570+
labelPrevDecade: 'العقد السابق',
546571
labelPrevYear: 'العام السابق',
547572
labelPrevMonth: 'الشهر السابق',
548573
labelCurrentMonth: 'الشهر الحالي',
549574
labelNextMonth: 'الشهر المقبل',
550575
labelNextYear: 'العام المقبل',
576+
labelNextDecade: 'العقد القادم',
551577
labelToday: 'اليوم',
552578
labelSelected: 'التاريخ المحدد',
553579
labelNoDateSelected: 'لم يتم اختيار تاريخ',
@@ -556,11 +582,13 @@ the same locale as requested, depending on the supported locales of `Intl`).
556582
labelHelp: 'استخدم مفاتيح المؤشر للتنقل في التواريخ'
557583
},
558584
zh: {
585+
labelPrevDecade: '过去十年',
559586
labelPrevYear: '上一年',
560587
labelPrevMonth: '上个月',
561588
labelCurrentMonth: '当前月份',
562589
labelNextMonth: '下个月',
563590
labelNextYear: '明年',
591+
labelNextDecade: '下一个十年',
564592
labelToday: '今天',
565593
labelSelected: '选定日期',
566594
labelNoDateSelected: '未选择日期',
@@ -610,6 +638,10 @@ Keyboard navigation:
610638
- <kbd>PageDown</kbd> moves to the same day in the next month
611639
- <kbd>Alt</kbd>+<kbd>PageUp</kbd> moves to the same day and month in the previous year
612640
- <kbd>Alt</kbd>+<kbd>PageDown</kbd> moves to the same day and month in the next year
641+
- <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>PageUp</kbd> moves to the same day and month in the previous
642+
decade
643+
- <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>PageDown</kbd> moves to the same day and month in the next
644+
decade
613645
- <kbd>Home</kbd> moves to today's date
614646
- <kbd>End</kbd> moves to the current selected date, or today if no selected date
615647
- <kbd>Enter</kbd> or <kbd>Space</kbd> selects the currently highlighted (focused) day

src/components/calendar/calendar.js

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
oneMonthAhead,
1717
oneYearAgo,
1818
oneYearAhead,
19+
oneDecadeAgo,
20+
oneDecadeAhead,
1921
parseYMD,
2022
resolveLocale
2123
} from '../../utils/date'
@@ -26,7 +28,12 @@ import { toInteger } from '../../utils/number'
2628
import { toString } from '../../utils/string'
2729
import idMixin from '../../mixins/id'
2830
import normalizeSlotMixin from '../../mixins/normalize-slot'
29-
import { BIconChevronLeft, BIconChevronDoubleLeft, BIconCircleFill } from '../../icons/icons'
31+
import {
32+
BIconChevronLeft,
33+
BIconChevronDoubleLeft,
34+
BIconChevronBarLeft,
35+
BIconCircleFill
36+
} from '../../icons/icons'
3037

3138
// --- Constants ---
3239

@@ -141,6 +148,11 @@ export const BCalendar = Vue.extend({
141148
type: Boolean,
142149
default: false
143150
},
151+
showDecadeNav: {
152+
// When `true` enables the decade navigation buttons
153+
type: Boolean,
154+
default: false
155+
},
144156
hidden: {
145157
// When `true`, renders a comment node, but keeps the component instance active
146158
// Mainly for <b-form-date>, so that we can get the component's value and locale
@@ -158,6 +170,10 @@ export const BCalendar = Vue.extend({
158170
// default: null
159171
},
160172
// Labels for buttons and keyboard shortcuts
173+
labelPrevDecade: {
174+
type: String,
175+
default: () => getComponentConfig(NAME, 'labelPrevDecade')
176+
},
161177
labelPrevYear: {
162178
type: String,
163179
default: () => getComponentConfig(NAME, 'labelPrevYear')
@@ -178,6 +194,10 @@ export const BCalendar = Vue.extend({
178194
type: String,
179195
default: () => getComponentConfig(NAME, 'labelNextYear')
180196
},
197+
labelNextDecade: {
198+
type: String,
199+
default: () => getComponentConfig(NAME, 'labelNextDecade')
200+
},
181201
labelToday: {
182202
type: String,
183203
default: () => getComponentConfig(NAME, 'labelToday')
@@ -397,6 +417,10 @@ export const BCalendar = Vue.extend({
397417
return createDateFormatter(this.calendarLocale, { day: 'numeric', calendar: 'gregory' })
398418
},
399419
// Disabled states for the nav buttons
420+
prevDecadeDisabled() {
421+
const min = this.computedMin
422+
return this.disabled || (min && lastDateOfMonth(oneDecadeAgo(this.activeDate)) < min)
423+
},
400424
prevYearDisabled() {
401425
const min = this.computedMin
402426
return this.disabled || (min && lastDateOfMonth(oneYearAgo(this.activeDate)) < min)
@@ -417,7 +441,11 @@ export const BCalendar = Vue.extend({
417441
const max = this.computedMax
418442
return this.disabled || (max && firstDateOfMonth(oneYearAhead(this.activeDate)) > max)
419443
},
420-
// Calendar generation
444+
nextDecadeDisabled() {
445+
const max = this.computedMax
446+
return this.disabled || (max && firstDateOfMonth(oneDecadeAhead(this.activeDate)) > max)
447+
},
448+
// Calendar dates generation
421449
calendar() {
422450
const matrix = []
423451
const firstDay = this.calendarFirstDay
@@ -571,8 +599,7 @@ export const BCalendar = Vue.extend({
571599
// Calendar keyboard navigation
572600
// Handles PAGEUP/PAGEDOWN/END/HOME/LEFT/UP/RIGHT/DOWN
573601
// Focuses grid after updating
574-
const keyCode = evt.keyCode
575-
const altKey = evt.altKey
602+
const { altKey, ctrlKey, keyCode } = evt
576603
if (!arrayIncludes([PAGEUP, PAGEDOWN, END, HOME, LEFT, UP, RIGHT, DOWN], keyCode)) {
577604
/* istanbul ignore next */
578605
return
@@ -586,13 +613,15 @@ export const BCalendar = Vue.extend({
586613
const isRTL = this.isRTL
587614
if (keyCode === PAGEUP) {
588615
// PAGEUP - Previous month/year
589-
activeDate = (altKey ? oneYearAgo : oneMonthAgo)(activeDate)
616+
activeDate = (altKey ? (ctrlKey ? oneDecadeAgo : oneYearAgo) : oneMonthAgo)(activeDate)
590617
// We check the first day of month to be in rage
591618
checkDate = createDate(activeDate)
592619
checkDate.setDate(1)
593620
} else if (keyCode === PAGEDOWN) {
594621
// PAGEDOWN - Next month/year
595-
activeDate = (altKey ? oneYearAhead : oneMonthAhead)(activeDate)
622+
activeDate = (altKey ? (ctrlKey ? oneDecadeAhead : oneYearAhead) : oneMonthAhead)(
623+
activeDate
624+
)
596625
// We check the last day of month to be in rage
597626
checkDate = createDate(activeDate)
598627
checkDate.setMonth(checkDate.getMonth() + 1)
@@ -670,6 +699,9 @@ export const BCalendar = Vue.extend({
670699
this.focus()
671700
}
672701
},
702+
gotoPrevDecade() {
703+
this.activeYMD = formatYMD(this.constrainDate(oneDecadeAgo(this.activeDate)))
704+
},
673705
gotoPrevYear() {
674706
this.activeYMD = formatYMD(this.constrainDate(oneYearAgo(this.activeDate)))
675707
},
@@ -686,6 +718,9 @@ export const BCalendar = Vue.extend({
686718
gotoNextYear() {
687719
this.activeYMD = formatYMD(this.constrainDate(oneYearAhead(this.activeDate)))
688720
},
721+
gotoNextDecade() {
722+
this.activeYMD = formatYMD(this.constrainDate(oneDecadeAhead(this.activeDate)))
723+
},
689724
onHeaderClick() {
690725
if (!this.disabled) {
691726
this.activeYMD = this.selectedYMD || formatYMD(this.getToday())
@@ -700,6 +735,7 @@ export const BCalendar = Vue.extend({
700735
}
701736

702737
const isRTL = this.isRTL
738+
const hideDecadeNav = !this.showDecadeNav
703739
const todayYMD = formatYMD(this.getToday())
704740
const selectedYMD = this.selectedYMD
705741
const activeYMD = this.activeYMD
@@ -762,11 +798,13 @@ export const BCalendar = Vue.extend({
762798
)
763799

764800
// Content for the date navigation buttons
801+
const $prevDecadeIcon = h(BIconChevronBarLeft, { props: { shiftV: 0.5, flipH: isRTL } })
765802
const $prevYearIcon = h(BIconChevronDoubleLeft, { props: { shiftV: 0.5, flipH: isRTL } })
766803
const $prevMonthIcon = h(BIconChevronLeft, { props: { shiftV: 0.5, flipH: isRTL } })
767804
const $thisMonthIcon = h(BIconCircleFill, { props: { shiftV: 0.5 } })
768805
const $nextMonthIcon = h(BIconChevronLeft, { props: { shiftV: 0.5, flipH: !isRTL } })
769806
const $nextYearIcon = h(BIconChevronDoubleLeft, { props: { shiftV: 0.5, flipH: !isRTL } })
807+
const $nextDecadeIcon = h(BIconChevronBarLeft, { props: { shiftV: 0.5, flipH: !isRTL } })
770808

771809
// Utility to create the date navigation buttons
772810
const makeNavBtn = (content, label, handler, btnDisabled, shortcut) => {
@@ -802,6 +840,15 @@ export const BCalendar = Vue.extend({
802840
}
803841
},
804842
[
843+
hideDecadeNav
844+
? h()
845+
: makeNavBtn(
846+
$prevDecadeIcon,
847+
this.labelPrevDecade,
848+
this.gotoPrevDecade,
849+
this.prevDecadeDisabled,
850+
'Ctrl+Alt+PageDown'
851+
),
805852
makeNavBtn(
806853
$prevYearIcon,
807854
this.labelPrevYear,
@@ -836,7 +883,16 @@ export const BCalendar = Vue.extend({
836883
this.gotoNextYear,
837884
this.nextYearDisabled,
838885
'Alt+PageUp'
839-
)
886+
),
887+
hideDecadeNav
888+
? h()
889+
: makeNavBtn(
890+
$nextDecadeIcon,
891+
this.labelNextDecade,
892+
this.gotoNextDecade,
893+
this.nextDecadeDisabled,
894+
'Ctrl+Alt+PageUp'
895+
)
840896
]
841897
)
842898

src/components/calendar/calendar.spec.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ describe('calendar', () => {
126126
const wrapper = mount(BCalendar, {
127127
attachToDocument: true,
128128
propsData: {
129+
showDecadeNav: true,
129130
value: '2020-02-15' // Leap year
130131
}
131132
})
@@ -139,28 +140,40 @@ describe('calendar', () => {
139140
expect($grid.attributes('data-month')).toBe('2020-02')
140141

141142
const $navBtns = wrapper.findAll('.b-calendar-nav button')
142-
expect($navBtns.length).toBe(5)
143+
expect($navBtns.length).toBe(7)
143144

144145
// Prev Month
145-
$navBtns.at(1).trigger('click')
146+
$navBtns.at(2).trigger('click')
146147
await waitNT(wrapper.vm)
147148
await waitRAF()
148149
expect($grid.attributes('data-month')).toBe('2020-01')
149150

150151
// Next Month
151-
$navBtns.at(3).trigger('click')
152+
$navBtns.at(4).trigger('click')
152153
await waitNT(wrapper.vm)
153154
await waitRAF()
154155
expect($grid.attributes('data-month')).toBe('2020-02')
155156

156157
// Prev Year
157-
$navBtns.at(0).trigger('click')
158+
$navBtns.at(1).trigger('click')
158159
await waitNT(wrapper.vm)
159160
await waitRAF()
160161
expect($grid.attributes('data-month')).toBe('2019-02')
161162

162163
// Next Year
163-
$navBtns.at(4).trigger('click')
164+
$navBtns.at(5).trigger('click')
165+
await waitNT(wrapper.vm)
166+
await waitRAF()
167+
expect($grid.attributes('data-month')).toBe('2020-02')
168+
169+
// Prev Decade
170+
$navBtns.at(0).trigger('click')
171+
await waitNT(wrapper.vm)
172+
await waitRAF()
173+
expect($grid.attributes('data-month')).toBe('2010-02')
174+
175+
// Next Decade
176+
$navBtns.at(6).trigger('click')
164177
await waitNT(wrapper.vm)
165178
await waitRAF()
166179
expect($grid.attributes('data-month')).toBe('2020-02')
@@ -169,7 +182,7 @@ describe('calendar', () => {
169182
// Handle the rare case this test is run right at midnight where
170183
// the current month rolled over at midnight when clicked
171184
const thisMonth1 = formatYMD(new Date()).slice(0, -3)
172-
$navBtns.at(2).trigger('click')
185+
$navBtns.at(3).trigger('click')
173186
await waitNT(wrapper.vm)
174187
await waitRAF()
175188
const thisMonth2 = formatYMD(new Date()).slice(0, -3)

0 commit comments

Comments
 (0)