Skip to content
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

feat(b-calendar, b-for-datepicker): add new initial-date prop, and constrain today/current month buttons between min and max (closes #4899) #4906

Merged
merged 19 commits into from Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/components/calendar/README.md
Expand Up @@ -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

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

- [`<b-form-datepicker>` Date picker custom form input](/docs/components/form-datepicker)
- [`<b-form-timepicker>` Time picker custom form input](/docs/comonents/form-timepicker)
- [`<b-form-timepicker>` Time picker custom form input](/docs/components/form-timepicker)
- [`<b-time>` Time date selection widget](/docs/components/calendar)
31 changes: 21 additions & 10 deletions src/components/calendar/calendar.js
Expand Up @@ -7,6 +7,7 @@ import { getComponentConfig } from '../../utils/config'
import {
createDate,
createDateFormatter,
constrainDate,
datesEqual,
firstDateOfMonth,
formatYMD,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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_')
Expand Down
5 changes: 5 additions & 0 deletions src/components/calendar/package.json
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions src/components/form-datepicker/README.md
Expand Up @@ -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
Expand All @@ -296,6 +300,14 @@ control the positioning of the popup calendar.
Refer to the [`<b-dropdown>` 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
Expand Down
22 changes: 16 additions & 6 deletions 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'
Expand Down Expand Up @@ -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 <b-calendar>
type: [String, Date],
default: null
},
placeholder: {
type: String,
// Defaults to `labelNoDateSelected` from calendar context
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions src/components/form-datepicker/package.json
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/components/form-timepicker/package.json
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/components/time/package.json
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions src/utils/date.js
Expand Up @@ -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
}
34 changes: 33 additions & 1 deletion src/utils/date.spec.js
Expand Up @@ -7,7 +7,8 @@ import {
oneMonthAgo,
oneMonthAhead,
oneYearAgo,
oneYearAhead
oneYearAhead,
constrainDate
} from './date'

describe('utils/date', () => {
Expand Down Expand Up @@ -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())
})
})