🌐 AI搜索 & 代理 主页
Skip to content

Commit 1d957eb

Browse files
feat(b-calendar, b-for-datepicker): add new initial-date prop, and constrain today/current month buttons between min and max (closes #4899) (#4906)
* feat(b-calendar, b-for-datepicker): add new initial-date prop (closes #4899) * Update date.js * Update date.spec.js * Update date.spec.js * Update form-datepicker.js * Update date.js * Update calendar.js * Update date.js * Update form-datepicker.js * Update form-datepicker.js * Update package.json * Update package.json * Update README.md * Update README.md * Update README.md * Fix typos Co-authored-by: Jacob Müller <jacob.mueller.elz@gmail.com>
1 parent 134d64d commit 1d957eb

File tree

10 files changed

+114
-20
lines changed

10 files changed

+114
-20
lines changed

src/components/calendar/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,14 @@ fit the width of the parent element. The `width` prop has no effect when `block`
230230
Note it is _not recommended_ to set a width below `260px`, otherwise truncation and layout issues
231231
with the component may occur.
232232

233+
### Initial open calendar date
234+
235+
By default, when no date is selected, the calendar view will be set to the current month (or the
236+
`min` or `max` date if today's date is out of range of `min` or `max`). You can change this
237+
behaviour by specifying a date via the `initial-date` prop. The initial date prop will be used to
238+
determine the calendar month to be initially presented to the user. It does not set the component's
239+
value.
240+
233241
### Date string format
234242

235243
<span class="badge badge-info small">v2.6.0+</span>
@@ -632,5 +640,5 @@ verbosity and to provide consistency across various screen readers (NVDA, when e
632640
## See also
633641

634642
- [`<b-form-datepicker>` Date picker custom form input](/docs/components/form-datepicker)
635-
- [`<b-form-timepicker>` Time picker custom form input](/docs/comonents/form-timepicker)
643+
- [`<b-form-timepicker>` Time picker custom form input](/docs/components/form-timepicker)
636644
- [`<b-time>` Time date selection widget](/docs/components/calendar)

src/components/calendar/calendar.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getComponentConfig } from '../../utils/config'
77
import {
88
createDate,
99
createDateFormatter,
10+
constrainDate,
1011
datesEqual,
1112
firstDateOfMonth,
1213
formatYMD,
@@ -58,6 +59,13 @@ export const BCalendar = Vue.extend({
5859
type: Boolean,
5960
default: false
6061
},
62+
initialDate: {
63+
// This specifies the calendar year/month/day that will be shown when
64+
// first opening the datepicker if no v-model value is provided
65+
// Default is the current date (or `min`/`max`)
66+
type: [String, Date],
67+
default: null
68+
},
6169
disabled: {
6270
type: Boolean,
6371
default: false
@@ -212,7 +220,9 @@ export const BCalendar = Vue.extend({
212220
// Selected date
213221
selectedYMD: selected,
214222
// Date in calendar grid that has `tabindex` of `0`
215-
activeYMD: selected || formatYMD(this.getToday()),
223+
activeYMD:
224+
selected ||
225+
formatYMD(constrainDate(this.initialDate || this.getToday()), this.min, this.max),
216226
// Will be true if the calendar grid has/contains focus
217227
gridHasFocus: false,
218228
// Flag to enable the `aria-live` region(s) after mount
@@ -361,6 +371,7 @@ export const BCalendar = Vue.extend({
361371
// Merge in user supplied options
362372
...this.dateFormatOptions,
363373
// Ensure hours/minutes/seconds are not shown
374+
// As we do not support the time portion (yet)
364375
hour: undefined,
365376
minute: undefined,
366377
second: undefined,
@@ -487,7 +498,9 @@ export const BCalendar = Vue.extend({
487498
},
488499
hidden(newVal) {
489500
// Reset the active focused day when hidden
490-
this.activeYMD = this.selectedYMD || formatYMD(this.value) || formatYMD(this.getToday())
501+
this.activeYMD =
502+
this.selectedYMD ||
503+
formatYMD(this.value || this.constrainDate(this.initialDate || this.getToday()))
491504
// Enable/disable the live regions
492505
this.setLive(!newVal)
493506
}
@@ -541,10 +554,7 @@ export const BCalendar = Vue.extend({
541554
constrainDate(date) {
542555
// Constrains a date between min and max
543556
// returns a new `Date` object instance
544-
date = parseYMD(date)
545-
const min = this.computedMin || date
546-
const max = this.computedMax || date
547-
return createDate(date < min ? min : date > max ? max : date)
557+
return constrainDate(date, this.computedMin, this.computedMax)
548558
},
549559
emitSelected(date) {
550560
// Performed in a `$nextTick()` to (probably) ensure
@@ -573,6 +583,7 @@ export const BCalendar = Vue.extend({
573583
let activeDate = createDate(this.activeDate)
574584
let checkDate = createDate(this.activeDate)
575585
const day = activeDate.getDate()
586+
const constrainedToday = this.constrainDate(this.getToday())
576587
const isRTL = this.isRTL
577588
if (keyCode === PAGEUP) {
578589
// PAGEUP - Previous month/year
@@ -605,11 +616,11 @@ export const BCalendar = Vue.extend({
605616
checkDate = activeDate
606617
} else if (keyCode === HOME) {
607618
// HOME - Today
608-
activeDate = this.getToday()
619+
activeDate = constrainedToday
609620
checkDate = activeDate
610621
} else if (keyCode === END) {
611622
// END - Selected date, or today if no selected date
612-
activeDate = parseYMD(this.selectedDate) || this.getToday()
623+
activeDate = parseYMD(this.selectedDate) || constrainedToday
613624
checkDate = activeDate
614625
}
615626
if (!this.dateOutOfRange(checkDate) && !datesEqual(activeDate, this.activeDate)) {
@@ -664,7 +675,7 @@ export const BCalendar = Vue.extend({
664675
},
665676
gotoCurrentMonth() {
666677
// TODO: Maybe this goto date should be configurable?
667-
this.activeYMD = formatYMD(this.getToday())
678+
this.activeYMD = formatYMD(this.constrainDate(this.getToday()))
668679
},
669680
gotoNextMonth() {
670681
this.activeYMD = formatYMD(this.constrainDate(oneMonthAhead(this.activeDate)))
@@ -694,7 +705,7 @@ export const BCalendar = Vue.extend({
694705
// Flag for making the `aria-live` regions live
695706
const isLive = this.isLive
696707
// Pre-compute some IDs
697-
// Thes should be computed props
708+
// This should be computed props
698709
const idValue = safeId()
699710
const idWidget = safeId('_calendar-wrapper_')
700711
const idNav = safeId('_calendar-nav_')

src/components/calendar/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
"prop": "valueAsDate",
2121
"description": "Returns a `Date` object for the v-model instead of a `YYYY-MM-DD` string"
2222
},
23+
{
24+
"prop": "initialDate",
25+
"version": "2.7.0",
26+
"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)"
27+
},
2328
{
2429
"prop": "disabled",
2530
"description": "Places the calendar in a non-interactive disabled state"

src/components/form-datepicker/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ The text for the optional buttons can be set via the `label-today-button`, `labe
288288
the `label-close-button` props. Due to the limited width of the footer section, it is recommended to
289289
keep these labels short.
290290

291+
Note that the `Set Today` button may not set the control today's date, if today's date is outside of
292+
the `min` or `max` date range restrictions. In the case it is outside of the range, it will set to
293+
either `min` or `max` (depending on which is closes to today's date).
294+
291295
### Dropdown placement
292296

293297
Use the dropdown props `right`, `dropup`, `dropright`, `dropleft`, `no-flip`, and `offset` to
@@ -296,6 +300,14 @@ control the positioning of the popup calendar.
296300
Refer to the [`<b-dropdown>` documentation](/docs/components/dropdown) for details on the effects
297301
and usage of these props.
298302

303+
### Initial open calendar date
304+
305+
By default, when no date is selected, the calendar view will be set to the current month (or the
306+
`min` or `max` date if today's date is out of range of `min` or `max`) when opened. You can change
307+
this behaviour by specifying a date via the `initial-date` prop. The initial date prop will be used
308+
to determine the calendar month to be initially presented to the user. It does not set the
309+
component's value.
310+
299311
### Dark mode
300312

301313
Want a fancy popup with a dark background instead of a light background? Set the `dark` prop to

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Vue from '../../utils/vue'
22
import { BVFormBtnLabelControl, dropdownProps } from '../../utils/bv-form-btn-label-control'
33
import { getComponentConfig } from '../../utils/config'
4-
import { createDate, formatYMD, parseYMD } from '../../utils/date'
4+
import { createDate, constrainDate, formatYMD, parseYMD } from '../../utils/date'
55
import { isUndefinedOrNull } from '../../utils/inspect'
66
import idMixin from '../../mixins/id'
77
import { BButton } from '../button/button'
@@ -31,6 +31,14 @@ const propsMixin = {
3131
type: [String, Date],
3232
default: ''
3333
},
34+
initialDate: {
35+
// This specifies the calendar year/month/day that will be shown when
36+
// first opening the datepicker if no v-model value is provided
37+
// Default is the current date (or `min`/`max`)
38+
// Passed directly to <b-calendar>
39+
type: [String, Date],
40+
default: null
41+
},
3442
placeholder: {
3543
type: String,
3644
// Defaults to `labelNoDateSelected` from calendar context
@@ -241,13 +249,13 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
241249
return {
242250
// We always use `YYYY-MM-DD` value internally
243251
localYMD: formatYMD(this.value) || '',
252+
// If the popup is open
253+
isVisible: false,
244254
// Context data from BCalendar
245255
localLocale: null,
246256
isRTL: false,
247257
formattedValue: '',
248-
activeYMD: '',
249-
// If the popup is open
250-
isVisible: false
258+
activeYMD: ''
251259
}
252260
},
253261
computed: {
@@ -265,6 +273,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
265273
value: self.localYMD,
266274
min: self.min,
267275
max: self.max,
276+
initialDate: self.initialDate,
268277
readonly: self.readonly,
269278
disabled: self.disabled,
270279
locale: self.locale,
@@ -293,7 +302,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
293302
return (this.localLocale || '').replace(/-u-.*$/i, '') || null
294303
},
295304
computedResetValue() {
296-
return formatYMD(this.resetValue) || ''
305+
return formatYMD(constrainDate(this.resetValue)) || ''
297306
}
298307
},
299308
watch: {
@@ -361,7 +370,8 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
361370
this.$emit('context', ctx)
362371
},
363372
onTodayButton() {
364-
this.setAndClose(formatYMD(createDate()))
373+
// Set to today (or min/max if today is out of range)
374+
this.setAndClose(formatYMD(constrainDate(createDate(), this.min, this.max)))
365375
},
366376
onResetButton() {
367377
this.setAndClose(this.computedResetValue)

src/components/form-datepicker/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
"prop": "resetValue",
2929
"description": "When the optional `reset` button is clicked, the selected date will be set to this value. Default is to clear the selected value"
3030
},
31+
{
32+
"prop": "initialDate",
33+
"version": "2.7.0",
34+
"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)"
35+
},
3136
{
3237
"prop": "disabled",
3338
"description": "Places the calendar in a non-interactive disabled state"

src/components/form-timepicker/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
},
3737
{
3838
"prop": "showSeconds",
39-
"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`"
39+
"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`"
4040
},
4141
{
4242
"prop": "hour12",

src/components/time/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
{
1919
"prop": "showSeconds",
20-
"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`"
20+
"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`"
2121
},
2222
{
2323
"prop": "hour12",

src/utils/date.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,14 @@ export const oneYearAhead = date => {
110110
}
111111
return date
112112
}
113+
114+
// Helper function to constrain a date between two values
115+
// Always returns a `Date` object or `null` if no date passed
116+
export const constrainDate = (date, min = null, max = null) => {
117+
// Ensure values are `Date` objects (or `null`)
118+
date = parseYMD(date)
119+
min = parseYMD(min) || date
120+
max = parseYMD(max) || date
121+
// Return a new `Date` object (or `null`)
122+
return date ? (date < min ? min : date > max ? max : date) : null
123+
}

src/utils/date.spec.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
oneMonthAgo,
88
oneMonthAhead,
99
oneYearAgo,
10-
oneYearAhead
10+
oneYearAhead,
11+
constrainDate
1112
} from './date'
1213

1314
describe('utils/date', () => {
@@ -94,4 +95,35 @@ describe('utils/date', () => {
9495
expect(formatYMD(oneYearAhead(parseYMD('2020-11-30')))).toEqual('2021-11-30')
9596
expect(formatYMD(oneYearAhead(parseYMD('2020-12-31')))).toEqual('2021-12-31')
9697
})
98+
99+
it('costrainDate works', async () => {
100+
const min = parseYMD('2020-01-05')
101+
const max = parseYMD('2020-01-15')
102+
const date1 = parseYMD('2020-01-10')
103+
const date2 = parseYMD('2020-01-01')
104+
const date3 = parseYMD('2020-01-20')
105+
106+
expect(constrainDate(null, null, null)).toEqual(null)
107+
expect(constrainDate(null, min, max)).toEqual(null)
108+
109+
expect(constrainDate(date1, null, null)).not.toEqual(null)
110+
expect(constrainDate(date1, null, null).toISOString()).toEqual(date1.toISOString())
111+
112+
expect(constrainDate(date1, min, max)).not.toEqual(null)
113+
expect(constrainDate(date1, min, max).toISOString()).toEqual(date1.toISOString())
114+
115+
expect(constrainDate(date2, min, max)).not.toEqual(null)
116+
expect(constrainDate(date2, min, max).toISOString()).toEqual(min.toISOString())
117+
expect(constrainDate(date2, '', max)).not.toEqual(null)
118+
expect(constrainDate(date2, '', max).toISOString()).toEqual(date2.toISOString())
119+
expect(constrainDate(date2, null, max)).not.toEqual(null)
120+
expect(constrainDate(date2, null, max).toISOString()).toEqual(date2.toISOString())
121+
122+
expect(constrainDate(date3, min, max)).not.toEqual(null)
123+
expect(constrainDate(date3, min, max).toISOString()).toEqual(max.toISOString())
124+
expect(constrainDate(date3, min, '')).not.toEqual(null)
125+
expect(constrainDate(date3, min, '').toISOString()).toEqual(date3.toISOString())
126+
expect(constrainDate(date3, min, null)).not.toEqual(null)
127+
expect(constrainDate(date3, min, null).toISOString()).toEqual(date3.toISOString())
128+
})
97129
})

0 commit comments

Comments
 (0)