diff --git a/src/components/calendar/README.md b/src/components/calendar/README.md
index fde90bef268..2debb6ab272 100644
--- a/src/components/calendar/README.md
+++ b/src/components/calendar/README.md
@@ -230,6 +230,14 @@ fit the width of the parent element. The `width` prop has no effect when `block`
Note it is _not recommended_ to set a width below `260px`, otherwise truncation and layout issues
with the component may occur.
+### Initial open calendar date
+
+By default, when no date is selected, the calendar view will be set to the current month (or the
+`min` or `max` date if today's date is out of range of `min` or `max`). You can change this
+behaviour by specifying a date via the `initial-date` prop. The initial date prop will be used to
+determine the calendar month to be initially presented to the user. It does not set the component's
+value.
+
### Date string format
v2.6.0+
@@ -632,5 +640,5 @@ verbosity and to provide consistency across various screen readers (NVDA, when e
## See also
- [`` Date picker custom form input](/docs/components/form-datepicker)
-- [`` Time picker custom form input](/docs/comonents/form-timepicker)
+- [`` Time picker custom form input](/docs/components/form-timepicker)
- [`` Time date selection widget](/docs/components/calendar)
diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js
index d9723e48a8d..8f7c1e48869 100644
--- a/src/components/calendar/calendar.js
+++ b/src/components/calendar/calendar.js
@@ -7,6 +7,7 @@ import { getComponentConfig } from '../../utils/config'
import {
createDate,
createDateFormatter,
+ constrainDate,
datesEqual,
firstDateOfMonth,
formatYMD,
@@ -58,6 +59,13 @@ export const BCalendar = Vue.extend({
type: Boolean,
default: false
},
+ initialDate: {
+ // This specifies the calendar year/month/day that will be shown when
+ // first opening the datepicker if no v-model value is provided
+ // Default is the current date (or `min`/`max`)
+ type: [String, Date],
+ default: null
+ },
disabled: {
type: Boolean,
default: false
@@ -212,7 +220,9 @@ export const BCalendar = Vue.extend({
// Selected date
selectedYMD: selected,
// Date in calendar grid that has `tabindex` of `0`
- activeYMD: selected || formatYMD(this.getToday()),
+ activeYMD:
+ selected ||
+ formatYMD(constrainDate(this.initialDate || this.getToday()), this.min, this.max),
// Will be true if the calendar grid has/contains focus
gridHasFocus: false,
// Flag to enable the `aria-live` region(s) after mount
@@ -361,6 +371,7 @@ export const BCalendar = Vue.extend({
// Merge in user supplied options
...this.dateFormatOptions,
// Ensure hours/minutes/seconds are not shown
+ // As we do not support the time portion (yet)
hour: undefined,
minute: undefined,
second: undefined,
@@ -487,7 +498,9 @@ export const BCalendar = Vue.extend({
},
hidden(newVal) {
// Reset the active focused day when hidden
- this.activeYMD = this.selectedYMD || formatYMD(this.value) || formatYMD(this.getToday())
+ this.activeYMD =
+ this.selectedYMD ||
+ formatYMD(this.value || this.constrainDate(this.initialDate || this.getToday()))
// Enable/disable the live regions
this.setLive(!newVal)
}
@@ -541,10 +554,7 @@ export const BCalendar = Vue.extend({
constrainDate(date) {
// Constrains a date between min and max
// returns a new `Date` object instance
- date = parseYMD(date)
- const min = this.computedMin || date
- const max = this.computedMax || date
- return createDate(date < min ? min : date > max ? max : date)
+ return constrainDate(date, this.computedMin, this.computedMax)
},
emitSelected(date) {
// Performed in a `$nextTick()` to (probably) ensure
@@ -573,6 +583,7 @@ export const BCalendar = Vue.extend({
let activeDate = createDate(this.activeDate)
let checkDate = createDate(this.activeDate)
const day = activeDate.getDate()
+ const constrainedToday = this.constrainDate(this.getToday())
const isRTL = this.isRTL
if (keyCode === PAGEUP) {
// PAGEUP - Previous month/year
@@ -605,11 +616,11 @@ export const BCalendar = Vue.extend({
checkDate = activeDate
} else if (keyCode === HOME) {
// HOME - Today
- activeDate = this.getToday()
+ activeDate = constrainedToday
checkDate = activeDate
} else if (keyCode === END) {
// END - Selected date, or today if no selected date
- activeDate = parseYMD(this.selectedDate) || this.getToday()
+ activeDate = parseYMD(this.selectedDate) || constrainedToday
checkDate = activeDate
}
if (!this.dateOutOfRange(checkDate) && !datesEqual(activeDate, this.activeDate)) {
@@ -664,7 +675,7 @@ export const BCalendar = Vue.extend({
},
gotoCurrentMonth() {
// TODO: Maybe this goto date should be configurable?
- this.activeYMD = formatYMD(this.getToday())
+ this.activeYMD = formatYMD(this.constrainDate(this.getToday()))
},
gotoNextMonth() {
this.activeYMD = formatYMD(this.constrainDate(oneMonthAhead(this.activeDate)))
@@ -694,7 +705,7 @@ export const BCalendar = Vue.extend({
// Flag for making the `aria-live` regions live
const isLive = this.isLive
// Pre-compute some IDs
- // Thes should be computed props
+ // This should be computed props
const idValue = safeId()
const idWidget = safeId('_calendar-wrapper_')
const idNav = safeId('_calendar-nav_')
diff --git a/src/components/calendar/package.json b/src/components/calendar/package.json
index 6bf274e46bf..14f5d2544f3 100644
--- a/src/components/calendar/package.json
+++ b/src/components/calendar/package.json
@@ -20,6 +20,11 @@
"prop": "valueAsDate",
"description": "Returns a `Date` object for the v-model instead of a `YYYY-MM-DD` string"
},
+ {
+ "prop": "initialDate",
+ "version": "2.7.0",
+ "description": "When a `value` is not specified, sets the initial calendar month date that will be presented to the user. Accepts a value in `YYYY-MM-DD` format or a `Date` object. Defaults to the current date (or min or max if the current date is out of range)"
+ },
{
"prop": "disabled",
"description": "Places the calendar in a non-interactive disabled state"
diff --git a/src/components/form-datepicker/README.md b/src/components/form-datepicker/README.md
index 322d1f77bca..6c25c05716d 100644
--- a/src/components/form-datepicker/README.md
+++ b/src/components/form-datepicker/README.md
@@ -288,6 +288,10 @@ The text for the optional buttons can be set via the `label-today-button`, `labe
the `label-close-button` props. Due to the limited width of the footer section, it is recommended to
keep these labels short.
+Note that the `Set Today` button may not set the control today's date, if today's date is outside of
+the `min` or `max` date range restrictions. In the case it is outside of the range, it will set to
+either `min` or `max` (depending on which is closes to today's date).
+
### Dropdown placement
Use the dropdown props `right`, `dropup`, `dropright`, `dropleft`, `no-flip`, and `offset` to
@@ -296,6 +300,14 @@ control the positioning of the popup calendar.
Refer to the [`` documentation](/docs/components/dropdown) for details on the effects
and usage of these props.
+### Initial open calendar date
+
+By default, when no date is selected, the calendar view will be set to the current month (or the
+`min` or `max` date if today's date is out of range of `min` or `max`) when opened. You can change
+this behaviour by specifying a date via the `initial-date` prop. The initial date prop will be used
+to determine the calendar month to be initially presented to the user. It does not set the
+component's value.
+
### Dark mode
Want a fancy popup with a dark background instead of a light background? Set the `dark` prop to
diff --git a/src/components/form-datepicker/form-datepicker.js b/src/components/form-datepicker/form-datepicker.js
index 7fb362790a9..09c857426c5 100644
--- a/src/components/form-datepicker/form-datepicker.js
+++ b/src/components/form-datepicker/form-datepicker.js
@@ -1,7 +1,7 @@
import Vue from '../../utils/vue'
import { BVFormBtnLabelControl, dropdownProps } from '../../utils/bv-form-btn-label-control'
import { getComponentConfig } from '../../utils/config'
-import { createDate, formatYMD, parseYMD } from '../../utils/date'
+import { createDate, constrainDate, formatYMD, parseYMD } from '../../utils/date'
import { isUndefinedOrNull } from '../../utils/inspect'
import idMixin from '../../mixins/id'
import { BButton } from '../button/button'
@@ -31,6 +31,14 @@ const propsMixin = {
type: [String, Date],
default: ''
},
+ initialDate: {
+ // This specifies the calendar year/month/day that will be shown when
+ // first opening the datepicker if no v-model value is provided
+ // Default is the current date (or `min`/`max`)
+ // Passed directly to
+ type: [String, Date],
+ default: null
+ },
placeholder: {
type: String,
// Defaults to `labelNoDateSelected` from calendar context
@@ -241,13 +249,13 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
return {
// We always use `YYYY-MM-DD` value internally
localYMD: formatYMD(this.value) || '',
+ // If the popup is open
+ isVisible: false,
// Context data from BCalendar
localLocale: null,
isRTL: false,
formattedValue: '',
- activeYMD: '',
- // If the popup is open
- isVisible: false
+ activeYMD: ''
}
},
computed: {
@@ -265,6 +273,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
value: self.localYMD,
min: self.min,
max: self.max,
+ initialDate: self.initialDate,
readonly: self.readonly,
disabled: self.disabled,
locale: self.locale,
@@ -293,7 +302,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
return (this.localLocale || '').replace(/-u-.*$/i, '') || null
},
computedResetValue() {
- return formatYMD(this.resetValue) || ''
+ return formatYMD(constrainDate(this.resetValue)) || ''
}
},
watch: {
@@ -361,7 +370,8 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
this.$emit('context', ctx)
},
onTodayButton() {
- this.setAndClose(formatYMD(createDate()))
+ // Set to today (or min/max if today is out of range)
+ this.setAndClose(formatYMD(constrainDate(createDate(), this.min, this.max)))
},
onResetButton() {
this.setAndClose(this.computedResetValue)
diff --git a/src/components/form-datepicker/package.json b/src/components/form-datepicker/package.json
index 036cd59b1d9..073a2515dbb 100644
--- a/src/components/form-datepicker/package.json
+++ b/src/components/form-datepicker/package.json
@@ -28,6 +28,11 @@
"prop": "resetValue",
"description": "When the optional `reset` button is clicked, the selected date will be set to this value. Default is to clear the selected value"
},
+ {
+ "prop": "initialDate",
+ "version": "2.7.0",
+ "description": "When a `value` is not specified, sets the initial calendar month date that will be presented to the user. Accepts a value in `YYYY-MM-DD` format or a `Date` object. Defaults to the current date (or min or max if the current date is out of range)"
+ },
{
"prop": "disabled",
"description": "Places the calendar in a non-interactive disabled state"
diff --git a/src/components/form-timepicker/package.json b/src/components/form-timepicker/package.json
index 08ccf8316d4..92342824416 100644
--- a/src/components/form-timepicker/package.json
+++ b/src/components/form-timepicker/package.json
@@ -36,7 +36,7 @@
},
{
"prop": "showSeconds",
- "description": "When true, shows the seconds spinbutton. If `false` the seconds spin button will not be shown and the seconds portion of hte time will always be `0`"
+ "description": "When true, shows the seconds spinbutton. If `false` the seconds spin button will not be shown and the seconds portion of the time will always be `0`"
},
{
"prop": "hour12",
diff --git a/src/components/time/package.json b/src/components/time/package.json
index 7ba9b4a2f24..fbbbfad3950 100644
--- a/src/components/time/package.json
+++ b/src/components/time/package.json
@@ -17,7 +17,7 @@
},
{
"prop": "showSeconds",
- "description": "When true, shows the seconds spinbutton. If `false` the seconds spin button will not be shown and the seconds portion of hte time will always be `0`"
+ "description": "When true, shows the seconds spinbutton. If `false` the seconds spin button will not be shown and the seconds portion of the time will always be `0`"
},
{
"prop": "hour12",
diff --git a/src/utils/date.js b/src/utils/date.js
index e6bac5624a0..64774d66e15 100644
--- a/src/utils/date.js
+++ b/src/utils/date.js
@@ -110,3 +110,14 @@ export const oneYearAhead = date => {
}
return date
}
+
+// Helper function to constrain a date between two values
+// Always returns a `Date` object or `null` if no date passed
+export const constrainDate = (date, min = null, max = null) => {
+ // Ensure values are `Date` objects (or `null`)
+ date = parseYMD(date)
+ min = parseYMD(min) || date
+ max = parseYMD(max) || date
+ // Return a new `Date` object (or `null`)
+ return date ? (date < min ? min : date > max ? max : date) : null
+}
diff --git a/src/utils/date.spec.js b/src/utils/date.spec.js
index 31db86c8339..be014e1c406 100644
--- a/src/utils/date.spec.js
+++ b/src/utils/date.spec.js
@@ -7,7 +7,8 @@ import {
oneMonthAgo,
oneMonthAhead,
oneYearAgo,
- oneYearAhead
+ oneYearAhead,
+ constrainDate
} from './date'
describe('utils/date', () => {
@@ -94,4 +95,35 @@ describe('utils/date', () => {
expect(formatYMD(oneYearAhead(parseYMD('2020-11-30')))).toEqual('2021-11-30')
expect(formatYMD(oneYearAhead(parseYMD('2020-12-31')))).toEqual('2021-12-31')
})
+
+ it('costrainDate works', async () => {
+ const min = parseYMD('2020-01-05')
+ const max = parseYMD('2020-01-15')
+ const date1 = parseYMD('2020-01-10')
+ const date2 = parseYMD('2020-01-01')
+ const date3 = parseYMD('2020-01-20')
+
+ expect(constrainDate(null, null, null)).toEqual(null)
+ expect(constrainDate(null, min, max)).toEqual(null)
+
+ expect(constrainDate(date1, null, null)).not.toEqual(null)
+ expect(constrainDate(date1, null, null).toISOString()).toEqual(date1.toISOString())
+
+ expect(constrainDate(date1, min, max)).not.toEqual(null)
+ expect(constrainDate(date1, min, max).toISOString()).toEqual(date1.toISOString())
+
+ expect(constrainDate(date2, min, max)).not.toEqual(null)
+ expect(constrainDate(date2, min, max).toISOString()).toEqual(min.toISOString())
+ expect(constrainDate(date2, '', max)).not.toEqual(null)
+ expect(constrainDate(date2, '', max).toISOString()).toEqual(date2.toISOString())
+ expect(constrainDate(date2, null, max)).not.toEqual(null)
+ expect(constrainDate(date2, null, max).toISOString()).toEqual(date2.toISOString())
+
+ expect(constrainDate(date3, min, max)).not.toEqual(null)
+ expect(constrainDate(date3, min, max).toISOString()).toEqual(max.toISOString())
+ expect(constrainDate(date3, min, '')).not.toEqual(null)
+ expect(constrainDate(date3, min, '').toISOString()).toEqual(date3.toISOString())
+ expect(constrainDate(date3, min, null)).not.toEqual(null)
+ expect(constrainDate(date3, min, null).toISOString()).toEqual(date3.toISOString())
+ })
})