Skip to content
Permalink
Browse files
feat(b-calendar, b-form-datepicker): add optional decade navigation b…
…uttons (addresses #4976) (#5112)

Co-authored-by: Jacob Müller
  • Loading branch information
tmorehouse committed Apr 7, 2020
1 parent 73966db commit b1f74a84f4021022e606360ee6824c6645b6fbd0
Showing 25 changed files with 295 additions and 95 deletions.
@@ -330,4 +330,4 @@ Avatars are based upon `<b-badge>` and `<b-button>` components, and as such, rel
`badge-*` and `btn-*` variant classes, as well as the `rounded-*`
[utility classes](/docs/reference/utility-classes).

`<b-avatar>` also requires BootstrapVue's custom CSS for proper styling.
`<b-avatar>` also requires BootstrapVue's custom CSS for proper styling.
@@ -91,12 +91,12 @@ Enable optional keyboard navigation by setting the prop `key-nav`.

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

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

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

```html
@@ -290,6 +290,18 @@ formatted in the locale's language.
You can hide this header via the `hide-header` prop. Note this only _visually hides_ the selected
date, while keeping it available to screen reader users as an `aria-live` region.

For example usage, refer to the [Internationalization section](#internationalization) below.

### Optional decade navigation buttons

Set the prop `show-decade-nav` to enable the previous and next decade buttons in the calendar's date
navigation toolbar.

The props `label-prev-decade` and `label-next-decade` props can be used to provide custom label text
for the decade buttons.

For example usage, refer to the [Internationalization section](#internationalization) below.

### Border and padding

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`).
<b-col cols="12" class="mb-3">
<label for="example-locales">Locale:</label>
<b-form-select id="example-locales" v-model="locale" :options="locales"></b-form-select>
<label for="example-weekdays">Start weekday:</label>
<label for="example-weekdays" class="mt-2">Start weekday:</label>
<b-form-select id="example-weekdays" v-model="weekday" :options="weekdays"></b-form-select>
<b-form-checkbox v-model="showDecadeNav" switch inline class="my-2">
Show decade navigation buttons
</b-form-checkbox>
<b-form-checkbox v-model="hideHeader" switch inline class="my-2">
Hide the date header
</b-form-checkbox>
</b-col>
<b-col md="auto">
<b-calendar
v-model="value"
v-bind="labels[locale] || {}"
:locale="locale"
:start-weekday="weekday"
:hide-header="hideHeader"
:show-decade-nav="showDecadeNav"
@context="onContext"
></b-calendar>
</b-col>
@@ -515,6 +535,8 @@ the same locale as requested, depending on the supported locales of `Intl`).
return {
value: '',
context: null,
showDecadeNav: false,
hideHeader: false,
locale: 'en-US',
locales: [
{ value: 'en-US', text: 'English US (en-US)' },
@@ -530,11 +552,13 @@ the same locale as requested, depending on the supported locales of `Intl`).
],
labels: {
de: {
labelPrevDecade: 'Vorheriges Jahrzehnt',
labelPrevYear: 'Vorheriges Jahr',
labelPrevMonth: 'Vorheriger Monat',
labelCurrentMonth: 'Aktueller Monat',
labelNextMonth: 'Nächster Monat',
labelNextYear: 'Nächstes Jahr',
labelNextDecade: 'Nächstes Jahrzehnt',
labelToday: 'Heute',
labelSelected: 'Ausgewähltes Datum',
labelNoDateSelected: 'Kein Datum gewählt',
@@ -543,11 +567,13 @@ the same locale as requested, depending on the supported locales of `Intl`).
labelHelp: 'Mit den Pfeiltasten durch den Kalender navigieren'
},
'ar-EG': {
labelPrevDecade: 'العقد السابق',
labelPrevYear: 'العام السابق',
labelPrevMonth: 'الشهر السابق',
labelCurrentMonth: 'الشهر الحالي',
labelNextMonth: 'الشهر المقبل',
labelNextYear: 'العام المقبل',
labelNextDecade: 'العقد القادم',
labelToday: 'اليوم',
labelSelected: 'التاريخ المحدد',
labelNoDateSelected: 'لم يتم اختيار تاريخ',
@@ -556,11 +582,13 @@ the same locale as requested, depending on the supported locales of `Intl`).
labelHelp: 'استخدم مفاتيح المؤشر للتنقل في التواريخ'
},
zh: {
labelPrevDecade: '过去十年',
labelPrevYear: '上一年',
labelPrevMonth: '上个月',
labelCurrentMonth: '当前月份',
labelNextMonth: '下个月',
labelNextYear: '明年',
labelNextDecade: '下一个十年',
labelToday: '今天',
labelSelected: '选定日期',
labelNoDateSelected: '未选择日期',
@@ -610,6 +638,10 @@ Keyboard navigation:
- <kbd>PageDown</kbd> moves to the same day in the next month
- <kbd>Alt</kbd>+<kbd>PageUp</kbd> moves to the same day and month in the previous year
- <kbd>Alt</kbd>+<kbd>PageDown</kbd> moves to the same day and month in the next year
- <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>PageUp</kbd> moves to the same day and month in the previous
decade
- <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>PageDown</kbd> moves to the same day and month in the next
decade
- <kbd>Home</kbd> moves to today's date
- <kbd>End</kbd> moves to the current selected date, or today if no selected date
- <kbd>Enter</kbd> or <kbd>Space</kbd> selects the currently highlighted (focused) day
@@ -16,6 +16,8 @@ import {
oneMonthAhead,
oneYearAgo,
oneYearAhead,
oneDecadeAgo,
oneDecadeAhead,
parseYMD,
resolveLocale
} from '../../utils/date'
@@ -26,7 +28,12 @@ import { toInteger } from '../../utils/number'
import { toString } from '../../utils/string'
import idMixin from '../../mixins/id'
import normalizeSlotMixin from '../../mixins/normalize-slot'
import { BIconChevronLeft, BIconChevronDoubleLeft, BIconCircleFill } from '../../icons/icons'
import {
BIconChevronLeft,
BIconChevronDoubleLeft,
BIconChevronBarLeft,
BIconCircleFill
} from '../../icons/icons'

// --- Constants ---

@@ -141,6 +148,11 @@ export const BCalendar = Vue.extend({
type: Boolean,
default: false
},
showDecadeNav: {
// When `true` enables the decade navigation buttons
type: Boolean,
default: false
},
hidden: {
// When `true`, renders a comment node, but keeps the component instance active
// 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({
// default: null
},
// Labels for buttons and keyboard shortcuts
labelPrevDecade: {
type: String,
default: () => getComponentConfig(NAME, 'labelPrevDecade')
},
labelPrevYear: {
type: String,
default: () => getComponentConfig(NAME, 'labelPrevYear')
@@ -178,6 +194,10 @@ export const BCalendar = Vue.extend({
type: String,
default: () => getComponentConfig(NAME, 'labelNextYear')
},
labelNextDecade: {
type: String,
default: () => getComponentConfig(NAME, 'labelNextDecade')
},
labelToday: {
type: String,
default: () => getComponentConfig(NAME, 'labelToday')
@@ -397,6 +417,10 @@ export const BCalendar = Vue.extend({
return createDateFormatter(this.calendarLocale, { day: 'numeric', calendar: 'gregory' })
},
// Disabled states for the nav buttons
prevDecadeDisabled() {
const min = this.computedMin
return this.disabled || (min && lastDateOfMonth(oneDecadeAgo(this.activeDate)) < min)
},
prevYearDisabled() {
const min = this.computedMin
return this.disabled || (min && lastDateOfMonth(oneYearAgo(this.activeDate)) < min)
@@ -417,7 +441,11 @@ export const BCalendar = Vue.extend({
const max = this.computedMax
return this.disabled || (max && firstDateOfMonth(oneYearAhead(this.activeDate)) > max)
},
// Calendar generation
nextDecadeDisabled() {
const max = this.computedMax
return this.disabled || (max && firstDateOfMonth(oneDecadeAhead(this.activeDate)) > max)
},
// Calendar dates generation
calendar() {
const matrix = []
const firstDay = this.calendarFirstDay
@@ -571,8 +599,7 @@ export const BCalendar = Vue.extend({
// Calendar keyboard navigation
// Handles PAGEUP/PAGEDOWN/END/HOME/LEFT/UP/RIGHT/DOWN
// Focuses grid after updating
const keyCode = evt.keyCode
const altKey = evt.altKey
const { altKey, ctrlKey, keyCode } = evt
if (!arrayIncludes([PAGEUP, PAGEDOWN, END, HOME, LEFT, UP, RIGHT, DOWN], keyCode)) {
/* istanbul ignore next */
return
@@ -586,13 +613,15 @@ export const BCalendar = Vue.extend({
const isRTL = this.isRTL
if (keyCode === PAGEUP) {
// PAGEUP - Previous month/year
activeDate = (altKey ? oneYearAgo : oneMonthAgo)(activeDate)
activeDate = (altKey ? (ctrlKey ? oneDecadeAgo : oneYearAgo) : oneMonthAgo)(activeDate)
// We check the first day of month to be in rage
checkDate = createDate(activeDate)
checkDate.setDate(1)
} else if (keyCode === PAGEDOWN) {
// PAGEDOWN - Next month/year
activeDate = (altKey ? oneYearAhead : oneMonthAhead)(activeDate)
activeDate = (altKey ? (ctrlKey ? oneDecadeAhead : oneYearAhead) : oneMonthAhead)(
activeDate
)
// We check the last day of month to be in rage
checkDate = createDate(activeDate)
checkDate.setMonth(checkDate.getMonth() + 1)
@@ -670,6 +699,9 @@ export const BCalendar = Vue.extend({
this.focus()
}
},
gotoPrevDecade() {
this.activeYMD = formatYMD(this.constrainDate(oneDecadeAgo(this.activeDate)))
},
gotoPrevYear() {
this.activeYMD = formatYMD(this.constrainDate(oneYearAgo(this.activeDate)))
},
@@ -686,6 +718,9 @@ export const BCalendar = Vue.extend({
gotoNextYear() {
this.activeYMD = formatYMD(this.constrainDate(oneYearAhead(this.activeDate)))
},
gotoNextDecade() {
this.activeYMD = formatYMD(this.constrainDate(oneDecadeAhead(this.activeDate)))
},
onHeaderClick() {
if (!this.disabled) {
this.activeYMD = this.selectedYMD || formatYMD(this.getToday())
@@ -700,6 +735,7 @@ export const BCalendar = Vue.extend({
}

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

// Content for the date navigation buttons
const $prevDecadeIcon = h(BIconChevronBarLeft, { props: { shiftV: 0.5, flipH: isRTL } })
const $prevYearIcon = h(BIconChevronDoubleLeft, { props: { shiftV: 0.5, flipH: isRTL } })
const $prevMonthIcon = h(BIconChevronLeft, { props: { shiftV: 0.5, flipH: isRTL } })
const $thisMonthIcon = h(BIconCircleFill, { props: { shiftV: 0.5 } })
const $nextMonthIcon = h(BIconChevronLeft, { props: { shiftV: 0.5, flipH: !isRTL } })
const $nextYearIcon = h(BIconChevronDoubleLeft, { props: { shiftV: 0.5, flipH: !isRTL } })
const $nextDecadeIcon = h(BIconChevronBarLeft, { props: { shiftV: 0.5, flipH: !isRTL } })

// Utility to create the date navigation buttons
const makeNavBtn = (content, label, handler, btnDisabled, shortcut) => {
@@ -802,6 +840,15 @@ export const BCalendar = Vue.extend({
}
},
[
hideDecadeNav
? h()
: makeNavBtn(
$prevDecadeIcon,
this.labelPrevDecade,
this.gotoPrevDecade,
this.prevDecadeDisabled,
'Ctrl+Alt+PageDown'
),
makeNavBtn(
$prevYearIcon,
this.labelPrevYear,
@@ -836,7 +883,16 @@ export const BCalendar = Vue.extend({
this.gotoNextYear,
this.nextYearDisabled,
'Alt+PageUp'
)
),
hideDecadeNav
? h()
: makeNavBtn(
$nextDecadeIcon,
this.labelNextDecade,
this.gotoNextDecade,
this.nextDecadeDisabled,
'Ctrl+Alt+PageUp'
)
]
)

@@ -126,6 +126,7 @@ describe('calendar', () => {
const wrapper = mount(BCalendar, {
attachToDocument: true,
propsData: {
showDecadeNav: true,
value: '2020-02-15' // Leap year
}
})
@@ -139,28 +140,40 @@ describe('calendar', () => {
expect($grid.attributes('data-month')).toBe('2020-02')

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

// Prev Month
$navBtns.at(1).trigger('click')
$navBtns.at(2).trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
expect($grid.attributes('data-month')).toBe('2020-01')

// Next Month
$navBtns.at(3).trigger('click')
$navBtns.at(4).trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
expect($grid.attributes('data-month')).toBe('2020-02')

// Prev Year
$navBtns.at(0).trigger('click')
$navBtns.at(1).trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
expect($grid.attributes('data-month')).toBe('2019-02')

// Next Year
$navBtns.at(4).trigger('click')
$navBtns.at(5).trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
expect($grid.attributes('data-month')).toBe('2020-02')

// Prev Decade
$navBtns.at(0).trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
expect($grid.attributes('data-month')).toBe('2010-02')

// Next Decade
$navBtns.at(6).trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
expect($grid.attributes('data-month')).toBe('2020-02')
@@ -169,7 +182,7 @@ describe('calendar', () => {
// Handle the rare case this test is run right at midnight where
// the current month rolled over at midnight when clicked
const thisMonth1 = formatYMD(new Date()).slice(0, -3)
$navBtns.at(2).trigger('click')
$navBtns.at(3).trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
const thisMonth2 = formatYMD(new Date()).slice(0, -3)

0 comments on commit b1f74a8

Please sign in to comment.