Skip to content

Commit 5f69864

Browse files
authored
feat(b-calendar, b-form-datepicker): add scoped slots for date navigation buttons (closes #5117) (#5147)
Co-authored-by: Jacob Müller
1 parent fdd81b3 commit 5f69864

File tree

8 files changed

+284
-13
lines changed

8 files changed

+284
-13
lines changed

src/components/calendar/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,31 @@ slot can be used to add buttons such as `Select Today` or `Reset`, etc.
366366
<!-- b-calendar-default-slot.vue -->
367367
```
368368

369+
### Date navigation button slots
370+
371+
<span class="badge badge-info small">2.12.0+</span>
372+
373+
To change the content of the calendar's date navigation buttons, BootstrapVue provides scoped slots
374+
for each button:
375+
376+
- `'nav-prev-decade'`
377+
- `'nav-prev-year'`
378+
- `'nav-prev-month'`
379+
- `'nav-this-month'` (the go to selected/today button)
380+
- `'nav-next-month'`
381+
- `'nav-next-year'`
382+
- `'nav-next-decade'`
383+
384+
All seven slots have the same scoped property available:
385+
386+
| Property | Type | Description |
387+
| -------- | ------- | --------------------------------------------------------------------- |
388+
| `isRTL` | Boolean | Will be `true` when the date navigation bar is rendered right-to-left |
389+
390+
You can use the `isRTL` scoped property to "flip" the prev vs next button content to handle the
391+
left-to-right to right-to-left orientation change &mdash; i.e. the previous year button will be on
392+
the right when `isRTL` is `true`, instead of the left.
393+
369394
### Adding CSS classes to specific dates
370395

371396
If you need to highlight a specific date or dates, set the `date-info-fn` prop to a reference to a

src/components/calendar/calendar.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -796,14 +796,28 @@ export const BCalendar = Vue.extend({
796796
)
797797

798798
// Content for the date navigation buttons
799-
// TODO: add slots for the nav button content
800-
const $prevDecadeIcon = h(BIconChevronBarLeft, { props: { shiftV: 0.5, flipH: isRTL } })
801-
const $prevYearIcon = h(BIconChevronDoubleLeft, { props: { shiftV: 0.5, flipH: isRTL } })
802-
const $prevMonthIcon = h(BIconChevronLeft, { props: { shiftV: 0.5, flipH: isRTL } })
803-
const $thisMonthIcon = h(BIconCircleFill, { props: { shiftV: 0.5 } })
804-
const $nextMonthIcon = h(BIconChevronLeft, { props: { shiftV: 0.5, flipH: !isRTL } })
805-
const $nextYearIcon = h(BIconChevronDoubleLeft, { props: { shiftV: 0.5, flipH: !isRTL } })
806-
const $nextDecadeIcon = h(BIconChevronBarLeft, { props: { shiftV: 0.5, flipH: !isRTL } })
799+
const navScope = { isRTL }
800+
const navProps = { shiftV: 0.5 }
801+
const navPrevProps = { ...navProps, flipH: isRTL }
802+
const navNextProps = { ...navProps, flipH: !isRTL }
803+
const $prevDecadeIcon =
804+
this.normalizeSlot('nav-prev-decade', navScope) ||
805+
h(BIconChevronBarLeft, { props: navPrevProps })
806+
const $prevYearIcon =
807+
this.normalizeSlot('nav-prev-year', navScope) ||
808+
h(BIconChevronDoubleLeft, { props: navPrevProps })
809+
const $prevMonthIcon =
810+
this.normalizeSlot('nav-prev-month', navScope) || h(BIconChevronLeft, { props: navPrevProps })
811+
const $thisMonthIcon =
812+
this.normalizeSlot('nav-this-month', navScope) || h(BIconCircleFill, { props: navProps })
813+
const $nextMonthIcon =
814+
this.normalizeSlot('nav-next-month', navScope) || h(BIconChevronLeft, { props: navNextProps })
815+
const $nextYearIcon =
816+
this.normalizeSlot('nav-next-year', navScope) ||
817+
h(BIconChevronDoubleLeft, { props: navNextProps })
818+
const $nextDecadeIcon =
819+
this.normalizeSlot('nav-next-decade', navScope) ||
820+
h(BIconChevronBarLeft, { props: navNextProps })
807821

808822
// Utility to create the date navigation buttons
809823
const makeNavBtn = (content, label, handler, btnDisabled, shortcut) => {

src/components/calendar/package.json

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,90 @@
223223
{
224224
"name": "default",
225225
"description": "Used to place custom controls at the bottom of the calendar component"
226+
},
227+
{
228+
"name": "nav-prev-decade",
229+
"version": "2.12.0",
230+
"description": "Used to place custom content in the previous decade navigation button",
231+
"scope": [
232+
{
233+
"prop": "isRTL",
234+
"type": "Boolean",
235+
"description": "Will be `true` if the nav bar is rendered right to left"
236+
}
237+
]
238+
},
239+
{
240+
"name": "nav-prev-year",
241+
"version": "2.12.0",
242+
"description": "Used to place custom content in the previous year navigation button",
243+
"scope": [
244+
{
245+
"prop": "isRTL",
246+
"type": "Boolean",
247+
"description": "Will be `true` if the date navigation bar is rendered right to left"
248+
}
249+
]
250+
},
251+
{
252+
"name": "nav-prev-month",
253+
"version": "2.12.0",
254+
"description": "Used to place custom content in the previous month navigation button",
255+
"scope": [
256+
{
257+
"prop": "isRTL",
258+
"type": "Boolean",
259+
"description": "Will be `true` if the date navigation bar is rendered right to left"
260+
}
261+
]
262+
},
263+
{
264+
"name": "nav-this-month",
265+
"version": "2.12.0",
266+
"description": "Used to place custom content in the this month/day navigation button",
267+
"scope": [
268+
{
269+
"prop": "isRTL",
270+
"type": "Boolean",
271+
"description": "Will be `true` if the date navigation bar is rendered right to left"
272+
}
273+
]
274+
},
275+
{
276+
"name": "nav-next-month",
277+
"version": "2.12.0",
278+
"description": "Used to place custom content in the next month navigation button",
279+
"scope": [
280+
{
281+
"prop": "isRTL",
282+
"type": "Boolean",
283+
"description": "Will be `true` if the date navigation bar is rendered right to left"
284+
}
285+
]
286+
},
287+
{
288+
"name": "nav-next-year",
289+
"version": "2.12.0",
290+
"description": "Used to place custom content in the next year navigation button",
291+
"scope": [
292+
{
293+
"prop": "isRTL",
294+
"type": "Boolean",
295+
"description": "Will be `true` if the date navigation bar is rendered right to left"
296+
}
297+
]
298+
},
299+
{
300+
"name": "nav-next-decade",
301+
"version": "2.12.0",
302+
"description": "Used to place custom content in the next decade navigation button",
303+
"scope": [
304+
{
305+
"prop": "isRTL",
306+
"type": "Boolean",
307+
"description": "Will be `true` if the date navigation bar is rendered right to left"
308+
}
309+
]
226310
}
227311
]
228312
}

src/components/form-datepicker/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,31 @@ Notes:
439439
- `year`, `month` and `day` will always be shown. If you need to leave out a value, set the property
440440
to `undefined`, although this is highly discouraged for accessibility reasons
441441

442+
### Date navigation button slots
443+
444+
<span class="badge badge-info small">2.12.0+</span>
445+
446+
To change the content of the calendar's date navigation buttons, BootstrapVue provides scoped slots
447+
for each button:
448+
449+
- `'nav-prev-decade'`
450+
- `'nav-prev-year'`
451+
- `'nav-prev-month'`
452+
- `'nav-this-month'` (the go to selected/today button)
453+
- `'nav-next-month'`
454+
- `'nav-next-year'`
455+
- `'nav-next-decade'`
456+
457+
All seven slots have the same scoped property available:
458+
459+
| Property | Type | Description |
460+
| -------- | ------- | --------------------------------------------------------------------- |
461+
| `isRTL` | Boolean | Will be `true` when the date navigation bar is rendered right-to-left |
462+
463+
You can use the `isRTL` scoped property to "flip" the prev vs next button content to handle the
464+
left-to-right to right-to-left orientation change &mdash; i.e. the previous year button will be on
465+
the right when `isRTL` is `true`, instead of the left.
466+
442467
## Internationalization
443468

444469
Internationalization of the date picker's calendar is provided via

src/components/form-datepicker/form-datepicker.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BVFormBtnLabelControl, dropdownProps } from '../../utils/bv-form-btn-la
33
import { getComponentConfig } from '../../utils/config'
44
import { createDate, constrainDate, formatYMD, parseYMD } from '../../utils/date'
55
import { isUndefinedOrNull } from '../../utils/inspect'
6+
import { pick } from '../../utils/object'
67
import idMixin from '../../mixins/id'
78
import { BButton } from '../button/button'
89
import { BCalendar } from '../calendar/calendar'
@@ -431,6 +432,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
431432
}
432433
},
433434
render(h) {
435+
const $scopedSlots = this.$scopedSlots
434436
const localYMD = this.localYMD
435437
const disabled = this.disabled
436438
const readonly = this.readonly
@@ -513,7 +515,16 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
513515
selected: this.onSelected,
514516
input: this.onInput,
515517
context: this.onContext
516-
}
518+
},
519+
scopedSlots: pick($scopedSlots, [
520+
'nav-prev-decade',
521+
'nav-prev-year',
522+
'nav-prev-month',
523+
'nav-this-month',
524+
'nav-next-month',
525+
'nav-next-year',
526+
'nav-next-decade'
527+
])
517528
},
518529
$footer
519530
)
@@ -541,7 +552,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
541552
hidden: this.onHidden
542553
},
543554
scopedSlots: {
544-
'button-content': this.$scopedSlots['button-content'] || this.defaultButtonFn
555+
'button-content': $scopedSlots['button-content'] || this.defaultButtonFn
545556
}
546557
},
547558
[$calendar]

src/components/form-datepicker/package.json

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,90 @@
329329
"description": "The visibility state of the popup. `true` if the popup is visible and `false` if not"
330330
}
331331
]
332+
},
333+
{
334+
"name": "nav-prev-decade",
335+
"version": "2.12.0",
336+
"description": "Used to place custom content in the previous decade navigation button",
337+
"scope": [
338+
{
339+
"prop": "isRTL",
340+
"type": "Boolean",
341+
"description": "Will be `true` if the date navigation bar is rendered right to left"
342+
}
343+
]
344+
},
345+
{
346+
"name": "nav-prev-year",
347+
"version": "2.12.0",
348+
"description": "Used to place custom content in the previous year navigation button",
349+
"scope": [
350+
{
351+
"prop": "isRTL",
352+
"type": "Boolean",
353+
"description": "Will be `true` if the date navigation bar is rendered right to left"
354+
}
355+
]
356+
},
357+
{
358+
"name": "nav-prev-month",
359+
"version": "2.12.0",
360+
"description": "Used to place custom content in the previous month navigation button",
361+
"scope": [
362+
{
363+
"prop": "isRTL",
364+
"type": "Boolean",
365+
"description": "Will be `true` if the date navigation bar is rendered right to left"
366+
}
367+
]
368+
},
369+
{
370+
"name": "nav-this-month",
371+
"version": "2.12.0",
372+
"description": "Used to place custom content in the this month/day navigation button",
373+
"scope": [
374+
{
375+
"prop": "isRTL",
376+
"type": "Boolean",
377+
"description": "Will be `true` if the date navigation bar is rendered right to left"
378+
}
379+
]
380+
},
381+
{
382+
"name": "nav-next-month",
383+
"version": "2.12.0",
384+
"description": "Used to place custom content in the next month navigation button",
385+
"scope": [
386+
{
387+
"prop": "isRTL",
388+
"type": "Boolean",
389+
"description": "Will be `true` if the date navigation bar is rendered right to left"
390+
}
391+
]
392+
},
393+
{
394+
"name": "nav-next-year",
395+
"version": "2.12.0",
396+
"description": "Used to place custom content in the next year navigation button",
397+
"scope": [
398+
{
399+
"prop": "isRTL",
400+
"type": "Boolean",
401+
"description": "Will be `true` if the date navigation bar is rendered right to left"
402+
}
403+
]
404+
},
405+
{
406+
"name": "nav-next-decade",
407+
"version": "2.12.0",
408+
"description": "Used to place custom content in the next decade navigation button",
409+
"scope": [
410+
{
411+
"prop": "isRTL",
412+
"type": "Boolean",
413+
"description": "Will be `true` if the date navigation bar is rendered right to left"
414+
}
415+
]
332416
}
333417
]
334418
}

src/utils/object.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { isArray } from './array'
55
export const assign = (...args) => Object.assign(...args)
66
export const create = (proto, optionalProps) => Object.create(proto, optionalProps)
77
export const defineProperties = (obj, props) => Object.defineProperties(obj, props)
8-
export const defineProperty = (obj, prop, descr) => Object.defineProperty(obj, prop, descr)
8+
export const defineProperty = (obj, prop, descriptor) =>
9+
Object.defineProperty(obj, prop, descriptor)
910
export const freeze = obj => Object.freeze(obj)
1011
export const getOwnPropertyNames = obj => Object.getOwnPropertyNames(obj)
1112
export const getOwnPropertyDescriptor = (obj, prop) => Object.getOwnPropertyDescriptor(obj, prop)
@@ -43,8 +44,16 @@ export const isPlainObject = obj => Object.prototype.toString.call(obj) === '[ob
4344
export const clone = obj => ({ ...obj })
4445

4546
/**
46-
* Return a shallow copy of object with
47-
* the specified properties omitted
47+
* Return a shallow copy of object with the specified properties only
48+
* @link https://gist.github.com/bisubus/2da8af7e801ffd813fab7ac221aa7afc
49+
*/
50+
export const pick = (obj, props) =>
51+
keys(obj)
52+
.filter(key => props.indexOf(key) !== -1)
53+
.reduce((result, key) => ({ ...result, [key]: obj[key] }), {})
54+
55+
/**
56+
* Return a shallow copy of object with the specified properties omitted
4857
* @link https://gist.github.com/bisubus/2da8af7e801ffd813fab7ac221aa7afc
4958
*/
5059
export const omit = (obj, props) =>

src/utils/object.spec.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { pick, omit } from './object'
2+
3+
describe('utils/object', () => {
4+
it('pick() works', async () => {
5+
const obj = { a: 1, b: 2, c: 3, d: null, e: [] }
6+
7+
expect(pick(obj, ['a', 'b', 'c'])).toEqual({ a: 1, b: 2, c: 3 })
8+
expect(pick(obj, Object.keys(obj))).toEqual(obj)
9+
expect(pick(obj, [])).toEqual({})
10+
})
11+
12+
it('omit() works', async () => {
13+
const obj = { a: 1, b: 2, c: 3, d: null, e: [] }
14+
15+
expect(omit(obj, ['a', 'b', 'c'])).toEqual({ d: null, e: [] })
16+
expect(omit(obj, Object.keys(obj))).toEqual({})
17+
expect(omit(obj, [])).toEqual(obj)
18+
})
19+
})

0 commit comments

Comments
 (0)