Skip to content

feat(b-calendar, b-form-datepicker): add scoped slots for date navigation buttons (closes #5117) #5147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 15, 2020
Merged
25 changes: 25 additions & 0 deletions src/components/calendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,31 @@ slot can be used to add buttons such as `Select Today` or `Reset`, etc.
<!-- b-calendar-default-slot.vue -->
```

### Date navigation button slots

<span class="badge badge-info small">2.12.0+</span>

To change the content of the calendar's date navigation buttons, BootstrapVue provides scoped slots
for each button:

- `'nav-prev-decade'`
- `'nav-prev-year'`
- `'nav-prev-month'`
- `'nav-this-month'` (the go to selected/today button)
- `'nav-next-month'`
- `'nav-next-year'`
- `'nav-next-decade'`

All seven slots have the same scoped property available:

| Property | Type | Description |
| -------- | ------- | --------------------------------------------------------------------- |
| `isRTL` | Boolean | Will be `true` when the date navigation bar is rendered right-to-left |

You can use the `isRTL` scoped property to "flip" the prev vs next button content to handle the
left-to-right to right-to-left orientation change &mdash; i.e. the previous year button will be on
the right when `isRTL` is `true`, instead of the left.

### Adding CSS classes to specific dates

If you need to highlight a specific date or dates, set the `date-info-fn` prop to a reference to a
Expand Down
30 changes: 22 additions & 8 deletions src/components/calendar/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -796,14 +796,28 @@ export const BCalendar = Vue.extend({
)

// Content for the date navigation buttons
// TODO: add slots for the nav button content
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 } })
const navScope = { isRTL }
const navProps = { shiftV: 0.5 }
const navPrevProps = { ...navProps, flipH: isRTL }
const navNextProps = { ...navProps, flipH: !isRTL }
const $prevDecadeIcon =
this.normalizeSlot('nav-prev-decade', navScope) ||
h(BIconChevronBarLeft, { props: navPrevProps })
const $prevYearIcon =
this.normalizeSlot('nav-prev-year', navScope) ||
h(BIconChevronDoubleLeft, { props: navPrevProps })
const $prevMonthIcon =
this.normalizeSlot('nav-prev-month', navScope) || h(BIconChevronLeft, { props: navPrevProps })
const $thisMonthIcon =
this.normalizeSlot('nav-this-month', navScope) || h(BIconCircleFill, { props: navProps })
const $nextMonthIcon =
this.normalizeSlot('nav-next-month', navScope) || h(BIconChevronLeft, { props: navNextProps })
const $nextYearIcon =
this.normalizeSlot('nav-next-year', navScope) ||
h(BIconChevronDoubleLeft, { props: navNextProps })
const $nextDecadeIcon =
this.normalizeSlot('nav-next-decade', navScope) ||
h(BIconChevronBarLeft, { props: navNextProps })

// Utility to create the date navigation buttons
const makeNavBtn = (content, label, handler, btnDisabled, shortcut) => {
Expand Down
84 changes: 84 additions & 0 deletions src/components/calendar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,90 @@
{
"name": "default",
"description": "Used to place custom controls at the bottom of the calendar component"
},
{
"name": "nav-prev-decade",
"version": "2.12.0",
"description": "Used to place custom content in the previous decade navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the nav bar is rendered right to left"
}
]
},
{
"name": "nav-prev-year",
"version": "2.12.0",
"description": "Used to place custom content in the previous year navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-prev-month",
"version": "2.12.0",
"description": "Used to place custom content in the previous month navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-this-month",
"version": "2.12.0",
"description": "Used to place custom content in the this month/day navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-next-month",
"version": "2.12.0",
"description": "Used to place custom content in the next month navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-next-year",
"version": "2.12.0",
"description": "Used to place custom content in the next year navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-next-decade",
"version": "2.12.0",
"description": "Used to place custom content in the next decade navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
}
]
}
Expand Down
25 changes: 25 additions & 0 deletions src/components/form-datepicker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,31 @@ Notes:
- `year`, `month` and `day` will always be shown. If you need to leave out a value, set the property
to `undefined`, although this is highly discouraged for accessibility reasons

### Date navigation button slots

<span class="badge badge-info small">2.12.0+</span>

To change the content of the calendar's date navigation buttons, BootstrapVue provides scoped slots
for each button:

- `'nav-prev-decade'`
- `'nav-prev-year'`
- `'nav-prev-month'`
- `'nav-this-month'` (the go to selected/today button)
- `'nav-next-month'`
- `'nav-next-year'`
- `'nav-next-decade'`

All seven slots have the same scoped property available:

| Property | Type | Description |
| -------- | ------- | --------------------------------------------------------------------- |
| `isRTL` | Boolean | Will be `true` when the date navigation bar is rendered right-to-left |

You can use the `isRTL` scoped property to "flip" the prev vs next button content to handle the
left-to-right to right-to-left orientation change &mdash; i.e. the previous year button will be on
the right when `isRTL` is `true`, instead of the left.

## Internationalization

Internationalization of the date picker's calendar is provided via
Expand Down
15 changes: 13 additions & 2 deletions src/components/form-datepicker/form-datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BVFormBtnLabelControl, dropdownProps } from '../../utils/bv-form-btn-la
import { getComponentConfig } from '../../utils/config'
import { createDate, constrainDate, formatYMD, parseYMD } from '../../utils/date'
import { isUndefinedOrNull } from '../../utils/inspect'
import { pick } from '../../utils/object'
import idMixin from '../../mixins/id'
import { BButton } from '../button/button'
import { BCalendar } from '../calendar/calendar'
Expand Down Expand Up @@ -431,6 +432,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
}
},
render(h) {
const $scopedSlots = this.$scopedSlots
const localYMD = this.localYMD
const disabled = this.disabled
const readonly = this.readonly
Expand Down Expand Up @@ -513,7 +515,16 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
selected: this.onSelected,
input: this.onInput,
context: this.onContext
}
},
scopedSlots: pick($scopedSlots, [
'nav-prev-decade',
'nav-prev-year',
'nav-prev-month',
'nav-this-month',
'nav-next-month',
'nav-next-year',
'nav-next-decade'
])
},
$footer
)
Expand Down Expand Up @@ -541,7 +552,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
hidden: this.onHidden
},
scopedSlots: {
'button-content': this.$scopedSlots['button-content'] || this.defaultButtonFn
'button-content': $scopedSlots['button-content'] || this.defaultButtonFn
}
},
[$calendar]
Expand Down
84 changes: 84 additions & 0 deletions src/components/form-datepicker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,90 @@
"description": "The visibility state of the popup. `true` if the popup is visible and `false` if not"
}
]
},
{
"name": "nav-prev-decade",
"version": "2.12.0",
"description": "Used to place custom content in the previous decade navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-prev-year",
"version": "2.12.0",
"description": "Used to place custom content in the previous year navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-prev-month",
"version": "2.12.0",
"description": "Used to place custom content in the previous month navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-this-month",
"version": "2.12.0",
"description": "Used to place custom content in the this month/day navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-next-month",
"version": "2.12.0",
"description": "Used to place custom content in the next month navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-next-year",
"version": "2.12.0",
"description": "Used to place custom content in the next year navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
},
{
"name": "nav-next-decade",
"version": "2.12.0",
"description": "Used to place custom content in the next decade navigation button",
"scope": [
{
"prop": "isRTL",
"type": "Boolean",
"description": "Will be `true` if the date navigation bar is rendered right to left"
}
]
}
]
}
Expand Down
15 changes: 12 additions & 3 deletions src/utils/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { isArray } from './array'
export const assign = (...args) => Object.assign(...args)
export const create = (proto, optionalProps) => Object.create(proto, optionalProps)
export const defineProperties = (obj, props) => Object.defineProperties(obj, props)
export const defineProperty = (obj, prop, descr) => Object.defineProperty(obj, prop, descr)
export const defineProperty = (obj, prop, descriptor) =>
Object.defineProperty(obj, prop, descriptor)
export const freeze = obj => Object.freeze(obj)
export const getOwnPropertyNames = obj => Object.getOwnPropertyNames(obj)
export const getOwnPropertyDescriptor = (obj, prop) => Object.getOwnPropertyDescriptor(obj, prop)
Expand Down Expand Up @@ -43,8 +44,16 @@ export const isPlainObject = obj => Object.prototype.toString.call(obj) === '[ob
export const clone = obj => ({ ...obj })

/**
* Return a shallow copy of object with
* the specified properties omitted
* Return a shallow copy of object with the specified properties only
* @link https://gist.github.com/bisubus/2da8af7e801ffd813fab7ac221aa7afc
*/
export const pick = (obj, props) =>
keys(obj)
.filter(key => props.indexOf(key) !== -1)
.reduce((result, key) => ({ ...result, [key]: obj[key] }), {})

/**
* Return a shallow copy of object with the specified properties omitted
* @link https://gist.github.com/bisubus/2da8af7e801ffd813fab7ac221aa7afc
*/
export const omit = (obj, props) =>
Expand Down
19 changes: 19 additions & 0 deletions src/utils/object.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { pick, omit } from './object'

describe('utils/object', () => {
it('pick() works', async () => {
const obj = { a: 1, b: 2, c: 3, d: null, e: [] }

expect(pick(obj, ['a', 'b', 'c'])).toEqual({ a: 1, b: 2, c: 3 })
expect(pick(obj, Object.keys(obj))).toEqual(obj)
expect(pick(obj, [])).toEqual({})
})

it('omit() works', async () => {
const obj = { a: 1, b: 2, c: 3, d: null, e: [] }

expect(omit(obj, ['a', 'b', 'c'])).toEqual({ d: null, e: [] })
expect(omit(obj, Object.keys(obj))).toEqual({})
expect(omit(obj, [])).toEqual(obj)
})
})