diff --git a/packages/base/src/dates/CalendarDate.js b/packages/base/src/dates/CalendarDate.js index 10655ae9d5ca..8aa4b2efb0ab 100644 --- a/packages/base/src/dates/CalendarDate.js +++ b/packages/base/src/dates/CalendarDate.js @@ -149,7 +149,7 @@ class CalendarDate { function isValidDate(date) { return date && Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date); // eslint-disable-line } - if (!isValidDate()) { + if (!isValidDate(oJSDate)) { throw new Error(`Date parameter must be a JavaScript Date object: [${oJSDate}].`); } return new CalendarDate(oJSDate.getFullYear(), oJSDate.getMonth(), oJSDate.getDate(), sCalendarType); diff --git a/packages/base/src/delegate/ItemNavigation.js b/packages/base/src/delegate/ItemNavigation.js index a08875c8da08..e11f6c64b6fe 100644 --- a/packages/base/src/delegate/ItemNavigation.js +++ b/packages/base/src/delegate/ItemNavigation.js @@ -1,3 +1,4 @@ +import RenderScheduler from "../RenderScheduler.js"; import { isDown, isUp, @@ -47,37 +48,19 @@ class ItemNavigation extends EventProvider { return this.verticalNavigationOn; } - _onKeyPress(event) { - const items = this._getItems(); - if (this.currentIndex >= items.length) { - if (this.behavior !== ItemNavigationBehavior.Cyclic) { - if (this.behavior === ItemNavigationBehavior.Paging) { - this.currentIndex = this.currentIndex - items.length; - } else { - this.currentIndex = items.length - 1; - } - this.fireEvent(ItemNavigation.BORDER_REACH, { start: false, end: true, offset: this.currentIndex }); - } else { - this.currentIndex = this.currentIndex - items.length; - } + async _onKeyPress(event) { + if (this.currentIndex >= this._getItems().length) { + this.onOverflowBottomEdge(); } else if (this.currentIndex < 0) { - if (this.behavior !== ItemNavigationBehavior.Cyclic) { - if (this.behavior === ItemNavigationBehavior.Paging) { - this.currentIndex = items.length + this.currentIndex - this.rowSize + (this.rowSize - (this._getItems().length % this.rowSize)); - } else { - this.currentIndex = 0; - } - this.fireEvent(ItemNavigation.BORDER_REACH, { start: true, end: false, offset: this.currentIndex }); - } else { - this.currentIndex = items.length + this.currentIndex; - } + this.onOverflowTopEdge(); } + event.preventDefault(); + + await RenderScheduler.whenFinished(); + this.update(); this.focusCurrent(); - - // stops browser scrolling with up/down keys - event.preventDefault(); } onkeydown(event) { @@ -227,8 +210,65 @@ class ItemNavigation extends EventProvider { set current(val) { this.currentIndex = val; } + + onOverflowBottomEdge() { + const items = this._getItems(); + const rowIndex = this.currentIndex - items.length; + if (this.behavior === ItemNavigationBehavior.Cyclic) { + return; + } + + if (this.behavior === ItemNavigationBehavior.Paging) { + this._handleNextPage(); + } + + this.fireEvent(ItemNavigation.BORDER_REACH, { + start: false, end: true, offset: this.currentIndex, rowIndex, + }); + } + + onOverflowTopEdge() { + const items = this._getItems(); + const rowIndex = this.currentIndex + this.rowSize; + + if (this.behavior === ItemNavigationBehavior.Cyclic) { + this.currentIndex = items.length + this.currentIndex; + return; + } + + if (this.behavior === ItemNavigationBehavior.Paging) { + this._handlePrevPage(); + } + + this.fireEvent(ItemNavigation.BORDER_REACH, { + start: true, end: false, offset: this.currentIndex, rowIndex, + }); + } + + _handleNextPage() { + this.fireEvent(ItemNavigation.PAGE_BOTTOM); + const items = this._getItems(); + + if (!this.hasNextPage) { + this.currentIndex = items.length - 1; + } else { + this.currentIndex = 0; + } + } + + _handlePrevPage() { + this.fireEvent(ItemNavigation.PAGE_TOP); + + if (!this.hasPrevPage) { + this.currentIndex = 0; + } else { + this.currentIndex = 41; + } + } } +ItemNavigation.PAGE_TOP = "PageTop"; +ItemNavigation.PAGE_BOTTOM = "PageBottom"; ItemNavigation.BORDER_REACH = "_borderReach"; export default ItemNavigation; diff --git a/packages/main/src/Calendar.hbs b/packages/main/src/Calendar.hbs index 779ac8dc8e00..be674a9794cb 100644 --- a/packages/main/src/Calendar.hbs +++ b/packages/main/src/Calendar.hbs @@ -9,6 +9,8 @@ @ui5-pressNext="{{_header.onPressNext}}" @ui5-btn1Press="{{_header.onBtn1Press}}" @ui5-btn2Press="{{_header.onBtn2Press}}" + ._isNextButtonDisabled="{{_header._isNextButtonDisabled}}" + ._isPrevButtonDisabled="{{_header._isPrevButtonDisabled}}" >
@@ -19,6 +21,8 @@ .selectedDates="{{_oMonth.selectedDates}}" ._hidden="{{_oMonth._hidden}}" .primaryCalendarType="{{_oMonth.primaryCalendarType}}" + .minDate="{{_oMonth.minDate}}" + .maxDate="{{_oMonth.maxDate}}" timestamp="{{_oMonth.timestamp}}" @ui5-selectionChange="{{_oMonth.onSelectedDatesChange}}" @ui5-navigate="{{_oMonth.onNavigate}}" @@ -27,8 +31,11 @@ @@ -36,8 +43,11 @@ { - if (firstDay) { - firstDay.focus(); - } - }, 100); + dayPicker._itemNav.currentIndex = fistDayOfMonthIndex; + dayPicker._itemNav.focusCurrent(); } _handleSelectedYearChange(event) { - const oOldMonth = this._calendarDate.getMonth(); - const oOldDay = this._calendarDate.getDate(); const oNewDate = CalendarDate.fromTimestamp( event.detail.timestamp * 1000, this._primaryCalendarType ); - oNewDate.setMonth(oOldMonth); - oNewDate.setDate(oOldDay); + oNewDate.setMonth(0); + oNewDate.setDate(1); this.timestamp = oNewDate.valueOf() / 1000; @@ -314,6 +405,10 @@ class Calendar extends UI5Element { return; } + if (!this.isInValidRange(nextMonth.toLocalJSDate().valueOf())) { + return; + } + this._focusFirstDayOfMonth(nextMonth); this.timestamp = nextMonth.valueOf() / 1000; } @@ -333,6 +428,10 @@ class Calendar extends UI5Element { // find the index of the last day let lastDayOfMonthIndex = -1; + if (!this.isInValidRange(currentMonthDate.toLocalJSDate().valueOf())) { + return; + } + dayPicker._getVisibleDays(lastMonthDate).forEach((date, index) => { const isSameDate = currentMonthDate.getDate() === date.getDate(); const isSameMonth = currentMonthDate.getMonth() === date.getMonth(); @@ -406,6 +505,20 @@ class Calendar extends UI5Element { return; } + if (this.minDate && !this._isYearInRange(this._yearPicker.timestamp, + YearPicker._ITEMS_COUNT - YearPicker._MIDDLE_ITEM_INDEX, + this.getFormat().parse(this.minDate).getFullYear(), + YearPicker._MAX_YEAR)) { + return; + } + + if (this.maxDate && !this._isYearInRange(this._yearPicker.timestamp, + YearPicker._ITEMS_COUNT - YearPicker._MIDDLE_ITEM_INDEX, + YearPicker._MIN_YEAR, + this.getFormat().parse(this.maxDate).getFullYear())) { + return; + } + this._yearPicker = Object.assign({}, this._yearPicker, { timestamp: this._yearPicker.timestamp + (31536000 * YearPicker._ITEMS_COUNT), }); @@ -421,6 +534,20 @@ class Calendar extends UI5Element { return; } + if (this.minDate && !this._isYearInRange(this._yearPicker.timestamp, + -YearPicker._MIDDLE_ITEM_INDEX - 1, + this.getFormat().parse(this.minDate).getFullYear(), + YearPicker._MAX_YEAR)) { + return; + } + + if (this.maxDate && !this._isYearInRange(this._yearPicker.timestamp, + -YearPicker._MIDDLE_ITEM_INDEX - 1, + YearPicker._MIN_YEAR, + this.getFormat().parse(this.maxDate).getFullYear())) { + return; + } + this._yearPicker = Object.assign({}, this._yearPicker, { timestamp: this._yearPicker.timestamp - (31536000 * YearPicker._ITEMS_COUNT), }); @@ -500,6 +627,50 @@ class Calendar extends UI5Element { }; } + /** + * Checks if a date is in range between minimum and maximum date + * @param {object} value + * @public + */ + isInValidRange(value = "") { + const pickedDate = CalendarDate.fromTimestamp(value).toLocalJSDate(), + minDate = this._minDate && new Date(this._minDate), + maxDate = this._maxDate && new Date(this._maxDate); + + if (minDate && maxDate) { + if (minDate <= pickedDate && maxDate >= pickedDate) { + return true; + } + } else if (minDate && !maxDate) { + if (minDate <= pickedDate) { + return true; + } + } else if (maxDate && !minDate) { + if (maxDate >= pickedDate) { + return true; + } + } else if (!maxDate && !minDate) { + return true; + } + + return false; + } + + getFormat() { + if (this._isPattern) { + this._oDateFormat = DateFormat.getInstance({ + pattern: this._formatPattern, + calendarType: this._primaryCalendarType, + }); + } else { + this._oDateFormat = DateFormat.getInstance({ + style: this._formatPattern, + calendarType: this._primaryCalendarType, + }); + } + return this._oDateFormat; + } + get styles() { return { main: { diff --git a/packages/main/src/CalendarHeader.hbs b/packages/main/src/CalendarHeader.hbs index 361bfc1dcb8f..e8a11ad34abf 100644 --- a/packages/main/src/CalendarHeader.hbs +++ b/packages/main/src/CalendarHeader.hbs @@ -5,7 +5,7 @@ >
@@ -41,7 +41,7 @@
parseInt(item._index) === focusableDayIdx); + focusableDayIdx = focusableItem ? dayPicker.focusableDays.indexOf(focusableItem) : focusableDayIdx; + + dayPicker._itemNav.current = focusableDayIdx; dayPicker._itemNav.update(); } }, @@ -295,11 +326,28 @@ class DatePicker extends UI5Element { this.i18nBundle = getI18nBundle("@ui5/webcomponents"); } + findFirstFocusableDay(daypicker) { + const today = new Date(); + if (!this.isInValidRange(today.getTime())) { + const focusableItems = Array.from(daypicker.shadowRoot.querySelectorAll(".ui5-dp-item")); + return focusableItems.filter(x => !x.classList.contains("ui5-dp-item--disabled"))[0]; + } + } + onBeforeRendering() { this._calendar.primaryCalendarType = this._primaryCalendarType; this._calendar.formatPattern = this._formatPattern; - if (this.isValid(this.value)) { + if (this.minDate && !this.isValid(this.minDate)) { + this.minDate = null; + console.warn(`In order for the "minDate" property to have effect, you should enter valid date format`); // eslint-disable-line + } + + if (this.maxDate && !this.isValid(this.maxDate)) { + this.maxDate = null; + console.warn(`In order for the "maxDate" property to have effect, you should enter valid date format`); // eslint-disable-line + } + if (this.isValid(this.value) && this.isInValidRange(this._getTimeStampFromString(this.value))) { this._changeCalendarSelection(); } else { this._calendar.selectedDates = []; @@ -311,6 +359,23 @@ class DatePicker extends UI5Element { } else if (this.name) { console.warn(`In order for the "name" property to have effect, you should also: import "@ui5/webcomponents/dist/features/InputElementsFormSupport.js";`); // eslint-disable-line } + + if (this.minDate) { + this._calendar.minDate = this.minDate; + } + + if (this.maxDate) { + this._calendar.maxDate = this.maxDate; + } + } + + _getTimeStampFromString(value) { + if (this.getFormat().parse(value)) { + const jsDate = new Date(this.getFormat().parse(value).getFullYear(), this.getFormat().parse(value).getMonth(), this.getFormat().parse(value).getDate()); + const oCalDate = CalendarDate.fromTimestamp(jsDate.getTime(), this._primaryCalendarType); + return oCalDate.valueOf(); + } + return undefined; } _onkeydown(event) { @@ -327,9 +392,13 @@ class DatePicker extends UI5Element { _handleInputChange() { let nextValue = this._getInput().getInputValue(); const isValid = this.isValid(nextValue); + const isInValidRange = this.isInValidRange(this._getTimeStampFromString(nextValue)); - if (isValid) { + if (isValid && isInValidRange) { nextValue = this.normalizeValue(nextValue); + this.valueState = ValueState.None; + } else { + this.valueState = ValueState.Error; } @@ -341,7 +410,7 @@ class DatePicker extends UI5Element { _handleInputLiveChange() { const nextValue = this._getInput().getInputValue(); - const isValid = this.isValid(nextValue); + const isValid = this.isValid(nextValue) && this.isInValidRange(this._getTimeStampFromString(nextValue)); this.value = nextValue; this.fireEvent("input", { value: nextValue, valid: isValid }); @@ -356,6 +425,35 @@ class DatePicker extends UI5Element { return !!(value && this.getFormat().parse(value)); } + /** + * Checks if a date is in range between minimum and maximum date + * @param {object} value + * @public + */ + isInValidRange(value = "") { + const pickedDate = new Date(value), + minDate = this._minDate && new Date(this._minDate), + maxDate = this._maxDate && new Date(this._maxDate); + + if (minDate && maxDate) { + if (minDate <= pickedDate && maxDate >= pickedDate) { + return true; + } + } else if (minDate && !maxDate) { + if (minDate <= pickedDate) { + return true; + } + } else if (maxDate && !minDate) { + if (maxDate >= pickedDate) { + return true; + } + } else if (!maxDate && !minDate) { + return true; + } + + return false; + } + // because the parser understands more than one format // but we need values in one format normalizeValue(sValue) { @@ -425,6 +523,20 @@ class DatePicker extends UI5Element { }; } + get _maxDate() { + if (this.maxDate) { + return this._getTimeStampFromString(this.maxDate); + } + return this.maxDate; + } + + get _minDate() { + if (this.minDate) { + return this._getTimeStampFromString(this.minDate); + } + return this.minDate; + } + get openIconTitle() { return this.i18nBundle.getText(DATEPICKER_OPEN_ICON_TITLE); } @@ -458,10 +570,15 @@ class DatePicker extends UI5Element { ); this._calendar.timestamp = iNewValue; this._calendar.selectedDates = event.detail.dates; - this._focusInputAfterClose = true; this.closePicker(); + if (this.isInValidRange(this._getTimeStampFromString(this.value))) { + this.valueState = ValueState.None; + } else { + this.valueState = ValueState.Error; + } + this.fireEvent("change", { value: this.value, valid: true }); // Angular two way data binding this.fireEvent("value-changed", { value: this.value, valid: true }); @@ -503,14 +620,14 @@ class DatePicker extends UI5Element { } } - _changeCalendarSelection() { + _changeCalendarSelection(focusTimestamp) { if (this._calendarDate.getYear() < 1) { // 0 is a valid year, but we cannot display it return; } const oCalDate = this._calendarDate; - const timestamp = oCalDate.valueOf() / 1000; + const timestamp = focusTimestamp || oCalDate.valueOf() / 1000; this._calendar = Object.assign({}, this._calendar); this._calendar.timestamp = timestamp; diff --git a/packages/main/src/DatePickerPopover.hbs b/packages/main/src/DatePickerPopover.hbs index 44f7f8b530af..4fe966f26e5a 100644 --- a/packages/main/src/DatePickerPopover.hbs +++ b/packages/main/src/DatePickerPopover.hbs @@ -14,6 +14,8 @@ format-pattern="{{_calendar.formatPattern}}" timestamp="{{_calendar.timestamp}}" .selectedDates="{{_calendar.selectedDates}}" + .minDate="{{_calendar.minDate}}" + .maxDate="{{_calendar.maxDate}}" @ui5-selectedDatesChange="{{_calendar.onSelectedDatesChange}}" > \ No newline at end of file diff --git a/packages/main/src/DayPicker.js b/packages/main/src/DayPicker.js index d2932f9bd7a4..3d5b619dc445 100644 --- a/packages/main/src/DayPicker.js +++ b/packages/main/src/DayPicker.js @@ -3,6 +3,7 @@ import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; import { fetchCldr } from "@ui5/webcomponents-base/dist/asset-registries/LocaleData.js"; import { getLocale } from "@ui5/webcomponents-base/dist/LocaleProvider.js"; import { getFirstDayOfWeek } from "@ui5/webcomponents-base/dist/config/FormatSettings.js"; +import DateFormat from "@ui5/webcomponents-utils/dist/sap/ui/core/format/DateFormat.js"; import { getCalendarType } from "@ui5/webcomponents-base/dist/config/CalendarType.js"; import { getFormatLocale } from "@ui5/webcomponents-base/dist/FormatSettings.js"; import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js"; @@ -18,6 +19,17 @@ import DayPickerTemplate from "./generated/templates/DayPickerTemplate.lit.js"; // Styles import dayPickerCSS from "./generated/themes/DayPicker.css.js"; +const monthDiff = (startDate, endDate) => { + let months; + const _startDate = CalendarDate.fromTimestamp(startDate).toLocalJSDate(), + _endDate = CalendarDate.fromTimestamp(endDate).toLocalJSDate(); + + months = (_endDate.getFullYear() - _startDate.getFullYear()) * 12; + months -= _startDate.getMonth(); + months += _endDate.getMonth(); + return months; +}; + /** * @public */ @@ -53,6 +65,30 @@ const metadata = { multiple: true, }, + /** + * Determines the мinimum date available for selection. + * + * @type {String} + * @defaultvalue "" + * @since 1.0.0-rc.6 + * @public + */ + minDate: { + type: String, + }, + + /** + * Determines the maximum date available for selection. + * + * @type {String} + * @defaultvalue "" + * @since 1.0.0-rc.6 + * @public + */ + maxDate: { + type: String, + }, + _weeks: { type: Object, multiple: true, @@ -66,6 +102,16 @@ const metadata = { type: Boolean, noAttribute: true, }, + /** + * Determines the format, displayed in the input field. + * + * @type {string} + * @defaultvalue "" + * @public + */ + formatPattern: { + type: String, + }, }, events: /** @lends sap.ui.webcomponents.main.DayPicker.prototype */ { /** @@ -83,8 +129,8 @@ const metadata = { }, }; -const MAX_YEAR = 9999; -const MIN_YEAR = 1; +const DEFAULT_MAX_YEAR = 9999; +const DEFAULT_MIN_YEAR = 1; /** * @class @@ -120,15 +166,29 @@ class DayPicker extends UI5Element { this._oLocale = getFormatLocale(); this._oLocaleData = new LocaleData(this._oLocale); - this._itemNav = new ItemNavigation(this, { rowSize: 7, behavior: ItemNavigationBehavior.Paging }); + this._itemNav = new ItemNavigation(this, { + rowSize: 7, + behavior: ItemNavigationBehavior.Paging, + }); + this._itemNav.getItemsCallback = function getItemsCallback() { - return [].concat(...this._weeks); + return this.focusableDays; }.bind(this); this._itemNav.attachEvent( ItemNavigation.BORDER_REACH, this._handleItemNavigationBorderReach.bind(this) ); + + this._itemNav.attachEvent( + "PageBottom", + this._handleMonthBottomOverflow.bind(this) + ); + + this._itemNav.attachEvent( + "PageTop", + this._handleMonthTopOverflow.bind(this) + ); } onBeforeRendering() { @@ -138,14 +198,18 @@ class DayPicker extends UI5Element { lastWeekNumber = -1, isDaySelected = false, todayIndex = 0; - const _aVisibleDays = this._getVisibleDays(this._calendarDate); - this._weeks = []; let week = []; this._weekNumbers = []; let weekday; + if (this.minDate) { + this._minDateObject = new Date(this._minDate); + } + if (this.maxDate) { + this._maxDateObject = new Date(this._maxDate); + } /* eslint-disable no-loop-func */ for (let i = 0; i < _aVisibleDays.length; i++) { oCalDate = _aVisibleDays[i]; @@ -203,6 +267,10 @@ class DayPicker extends UI5Element { if (this._isWeekend(oCalDate)) { day.classes += " ui5-dp-item--weeekend"; } + if ((this.minDate || this.maxDate) && this._isOutOfSelectableRange(oCalDate)) { + day.classes += " ui5-dp-item--disabled"; + day.disabled = true; + } if (day.classes.indexOf("ui5-dp-wday6") !== -1 || _aVisibleDays.length - 1 === i) { @@ -210,7 +278,6 @@ class DayPicker extends UI5Element { week = []; } } - while (this._weeks.length < 6) { this._weeks.push([]); } @@ -246,7 +313,6 @@ class DayPicker extends UI5Element { _onmousedown(event) { const target = event.target; - const dayPressed = this._isDayPressed(target); if (dayPressed) { @@ -256,8 +322,13 @@ class DayPicker extends UI5Element { for (let i = 0; i < this._weeks.length; i++) { for (let j = 0; j < this._weeks[i].length; j++) { if (parseInt(this._weeks[i][j].timestamp) === targetDate) { - this._itemNav.current = parseInt(target.getAttribute("data-sap-index")); + let index = parseInt(target.getAttribute("data-sap-index")); + if (this.minDate || this.maxDate) { + const focusableItem = this.focusableDays.find(item => parseInt(item._index) === index); + index = focusableItem ? this.focusableDays.indexOf(focusableItem) : index; + } + this._itemNav.current = index; this._itemNav.update(); break; } @@ -269,10 +340,15 @@ class DayPicker extends UI5Element { } _onmouseup(event) { + const dayPressed = this._isDayPressed(event.target); if (this.targetDate) { this._modifySelectionAndNotifySubscribers(this.targetDate, event.ctrlKey); this.targetDate = null; } + + if (!dayPressed) { + this._itemNav.focusCurrent(); + } } _onkeydown(event) { @@ -317,6 +393,10 @@ class DayPicker extends UI5Element { return CalendarDate.fromTimestamp(this._localDate.getTime(), this._primaryCalendarType); } + get _formatPattern() { + return this.formatPattern || "medium"; // get from config + } + get _month() { return this._calendarDate.getMonth(); } @@ -337,6 +417,17 @@ class DayPicker extends UI5Element { return this.primaryCalendarType || getCalendarType() || LocaleData.getInstance(getLocale()).getPreferredCalendarType(); } + get focusableDays() { + const focusableDays = []; + + for (let i = 0; i < this._weeks.length; i++) { + const week = this._weeks[i].filter(x => !x.disabled); + focusableDays.push(week); + } + + return [].concat(...focusableDays); + } + _modifySelectionAndNotifySubscribers(sNewDate, bAdd) { if (bAdd) { this.selectedDates = [...this._selectedDates, sNewDate]; @@ -347,25 +438,109 @@ class DayPicker extends UI5Element { this.fireEvent("selectionChange", { dates: [...this._selectedDates] }); } + _handleMonthBottomOverflow(event) { + this._itemNav.hasNextPage = this._hasNextMonth(); + } + + _handleMonthTopOverflow(event) { + this._itemNav.hasPrevPage = this._hasPrevMonth(); + } + + _hasNextMonth() { + let newMonth = this._month + 1; + let newYear = this._year; + + if (newMonth > 11) { + newMonth = 0; + newYear++; + } + + if (newYear > DEFAULT_MAX_YEAR && newMonth === 0) { + return false; + } + + if (!this.maxDate) { + return true; + } + + const oNewDate = this._calendarDate; + oNewDate.setDate(oNewDate.getDate()); + oNewDate.setYear(newYear); + oNewDate.setMonth(newMonth); + + const monthsBetween = monthDiff(oNewDate.valueOf(), this._maxDate); + if (monthsBetween < 0) { + return false; + } + + const lastFocusableDay = this.focusableDays[this.focusableDays.length - 1].iDay; + if (monthsBetween === 0 && CalendarDate.fromTimestamp(this._maxDate).toLocalJSDate().getDate() === lastFocusableDay) { + return false; + } + + return true; + } + + _hasPrevMonth() { + let newMonth = this._month - 1; + let newYear = this._year; + + if (newMonth < 0) { + newMonth = 11; + newYear--; + } + + if (newYear < DEFAULT_MIN_YEAR && newMonth === 11) { + return false; + } + + if (!this.minDate) { + return true; + } + + const oNewDate = this._calendarDate; + oNewDate.setDate(oNewDate.getDate()); + oNewDate.setYear(newYear); + oNewDate.setMonth(newMonth); + + const monthsBetween = monthDiff(this._minDate, oNewDate.valueOf()); + if (this.minDate && monthsBetween < 0) { + return false; + } + + return true; + } + _handleItemNavigationBorderReach(event) { const currentMonth = this._month, currentYear = this._year; - let iNewMonth, - iNewYear; + let newMonth, + newYear, + newDate, + currentDate; if (event.end) { - iNewMonth = currentMonth < 11 ? currentMonth + 1 : 0; - iNewYear = currentMonth < 11 ? currentYear : currentYear + 1; + currentDate = new Date(this._weeks[this._weeks.length - 1][event.rowIndex].timestamp * 1000); + newMonth = currentMonth < 11 ? currentMonth + 1 : 0; + newYear = currentMonth < 11 ? currentYear : currentYear + 1; + newDate = currentDate.getMonth() === newMonth ? currentDate.getDate() : currentDate.getDate() + 7; } else if (event.start) { - iNewMonth = currentMonth > 0 ? currentMonth - 1 : 11; - iNewYear = currentMonth > 0 ? currentYear : currentYear - 1; + currentDate = new Date(this._weeks[0][event.rowIndex].timestamp * 1000); + newMonth = currentMonth > 0 ? currentMonth - 1 : 11; + newYear = currentMonth > 0 ? currentYear : currentYear - 1; + newDate = currentDate.getMonth() === newMonth ? currentDate.getDate() : currentDate.getDate() - 7; } const oNewDate = this._calendarDate; - oNewDate.setYear(iNewYear); - oNewDate.setMonth(iNewMonth); + oNewDate.setDate(newDate); + oNewDate.setYear(newYear); + oNewDate.setMonth(newMonth); - if (oNewDate.getYear() < MIN_YEAR || oNewDate.getYear() > MAX_YEAR) { + if (oNewDate.getYear() < DEFAULT_MIN_YEAR || oNewDate.getYear() > DEFAULT_MAX_YEAR) { + return; + } + + if (this._isOutOfSelectableRange(oNewDate._oUDate.oDate)) { return; } @@ -386,6 +561,49 @@ class DayPicker extends UI5Element { return (target.className.indexOf("ui5-dp-item") > -1) || (targetParent && target.parentNode.classList.contains("ui5-dp-item")); } + _isOutOfSelectableRange(date) { + const currentDate = date._oUDate ? date.toLocalJSDate() : CalendarDate.fromTimestamp(date).toLocalJSDate(); + + return currentDate > this._maxDateObject || currentDate < this._minDateObject; + } + + get _maxDate() { + if (this.maxDate) { + const jsDate = new Date(this.getFormat().parse(this.maxDate).getFullYear(), this.getFormat().parse(this.maxDate).getMonth(), this.getFormat().parse(this.maxDate).getDate()); + const oCalDate = CalendarDate.fromTimestamp(jsDate.getTime(), this._primaryCalendarType); + return oCalDate.valueOf(); + } + return this.maxDate; + } + + get _minDate() { + if (this.minDate) { + const jsDate = new Date(this.getFormat().parse(this.minDate).getFullYear(), this.getFormat().parse(this.minDate).getMonth(), this.getFormat().parse(this.minDate).getDate()); + const oCalDate = CalendarDate.fromTimestamp(jsDate.getTime(), this._primaryCalendarType); + return oCalDate.valueOf(); + } + return this.minDate; + } + + getFormat() { + if (this._isPattern) { + this._oDateFormat = DateFormat.getInstance({ + pattern: this._formatPattern, + calendarType: this._primaryCalendarType, + }); + } else { + this._oDateFormat = DateFormat.getInstance({ + style: this._formatPattern, + calendarType: this._primaryCalendarType, + }); + } + return this._oDateFormat; + } + + get _isPattern() { + return this._formatPattern !== "medium" && this._formatPattern !== "short" && this._formatPattern !== "long"; + } + _getVisibleDays(oStartDate, bIncludeBCDates) { let oCalDate, iDaysOldMonth, @@ -417,12 +635,12 @@ class DayPicker extends UI5Element { for (let i = 0; i < 42; i++) { iYear = oDay.getYear(); oCalDate = new CalendarDate(oDay, this._primaryCalendarType); - if (bIncludeBCDates && iYear < MIN_YEAR) { + if (bIncludeBCDates && iYear < DEFAULT_MIN_YEAR) { // For dates before 0001-01-01 we should render only empty squares to keep // the month square matrix correct. oCalDate._bBeforeFirstYear = true; _aVisibleDays.push(oCalDate); - } else if (iYear >= MIN_YEAR && iYear <= MAX_YEAR) { + } else if (iYear >= DEFAULT_MIN_YEAR && iYear <= DEFAULT_MAX_YEAR) { // Days before 0001-01-01 or after 9999-12-31 should not be rendered. _aVisibleDays.push(oCalDate); } diff --git a/packages/main/src/MonthPicker.js b/packages/main/src/MonthPicker.js index 075d0a32e44e..3372fca36759 100644 --- a/packages/main/src/MonthPicker.js +++ b/packages/main/src/MonthPicker.js @@ -2,6 +2,7 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; import { getCalendarType } from "@ui5/webcomponents-base/dist/config/CalendarType.js"; import { getFormatLocale } from "@ui5/webcomponents-base/dist/FormatSettings.js"; +import DateFormat from "@ui5/webcomponents-utils/dist/sap/ui/core/format/DateFormat.js"; import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js"; import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/events/PseudoEvents.js"; @@ -28,6 +29,7 @@ const metadata = { timestamp: { type: Integer, }, + /** * Sets a calendar type used for display. * If not set, the calendar type of the global configuration is used. @@ -37,14 +39,50 @@ const metadata = { primaryCalendarType: { type: CalendarType, }, + + /** + * Determines the мinimum date available for selection. + * + * @type {String} + * @defaultvalue "" + * @since 1.0.0-rc.6 + * @public + */ + minDate: { + type: String, + }, + + /** + * Determines the maximum date available for selection. + * + * @type {String} + * @defaultvalue "" + * @since 1.0.0-rc.6 + * @public + */ + maxDate: { + type: String, + }, + _quarters: { type: Object, multiple: true, }, + _hidden: { type: Boolean, noAttribute: true, }, + /** + * Determines the format, displayed in the input field. + * + * @type {string} + * @defaultvalue "" + * @public + */ + formatPattern: { + type: String, + }, }, events: /** @lends sap.ui.webcomponents.main.MonthPicker.prototype */ { /** @@ -94,7 +132,14 @@ class MonthPicker extends UI5Element { this._itemNav = new ItemNavigation(this, { rowSize: 3, behavior: ItemNavigationBehavior.Cyclic }); this._itemNav.getItemsCallback = function getItemsCallback() { - return [].concat(...this._quarters); + const focusableMonths = []; + + for (let i = 0; i < this._quarters.length; i++) { + const quarter = this._quarters[i].filter(x => !x.disabled); + focusableMonths.push(quarter); + } + + return [].concat(...focusableMonths); }.bind(this); this._itemNav.setItemsCallback = function setItemsCallback(items) { this._quarters = items; @@ -121,6 +166,11 @@ class MonthPicker extends UI5Element { month.classes += " ui5-mp-item--selected"; } + if ((this.minDate || this.maxDate) && this._isOutOfSelectableRange(i)) { + month.classes += " ui5-mp-item--disabled"; + month.disabled = true; + } + const quarterIndex = parseInt(i / 3); if (quarters[quarterIndex]) { @@ -157,6 +207,10 @@ class MonthPicker extends UI5Element { return this.primaryCalendarType || getCalendarType() || LocaleData.getInstance(getLocale()).getPreferredCalendarType(); } + get _isPattern() { + return this._formatPattern !== "medium" && this._formatPattern !== "short" && this._formatPattern !== "long"; + } + _onclick(event) { if (event.target.className.indexOf("ui5-mp-item") > -1) { const timestamp = this.getTimestampFromDOM(event.target); @@ -181,6 +235,54 @@ class MonthPicker extends UI5Element { } } + _isOutOfSelectableRange(monthIndex) { + const currentDateYear = this._localDate.getFullYear(), + minDate = new Date(this._minDate), + maxDate = new Date(this._maxDate), + minDateCheck = minDate && ((currentDateYear === minDate.getFullYear() && monthIndex < minDate.getMonth()) || currentDateYear < minDate.getFullYear()), + maxDateCheck = maxDate && ((currentDateYear === maxDate.getFullYear() && monthIndex > maxDate.getMonth()) || (currentDateYear > maxDate.getFullYear())); + + return maxDateCheck || minDateCheck; + } + + get _maxDate() { + if (this.maxDate) { + const jsDate = new Date(this.getFormat().parse(this.maxDate).getFullYear(), this.getFormat().parse(this.maxDate).getMonth(), this.getFormat().parse(this.maxDate).getDate()); + const oCalDate = CalendarDate.fromTimestamp(jsDate.getTime(), this._primaryCalendarType); + return oCalDate.valueOf(); + } + return this.maxDate; + } + + get _minDate() { + if (this.minDate) { + const jsDate = new Date(this.getFormat().parse(this.minDate).getFullYear(), this.getFormat().parse(this.minDate).getMonth(), this.getFormat().parse(this.minDate).getDate()); + const oCalDate = CalendarDate.fromTimestamp(jsDate.getTime(), this._primaryCalendarType); + return oCalDate.valueOf(); + } + return this.minDate; + } + + + getFormat() { + if (this._isPattern) { + this._oDateFormat = DateFormat.getInstance({ + pattern: this._formatPattern, + calendarType: this._primaryCalendarType, + }); + } else { + this._oDateFormat = DateFormat.getInstance({ + style: this._formatPattern, + calendarType: this._primaryCalendarType, + }); + } + return this._oDateFormat; + } + + get _formatPattern() { + return this.formatPattern || "medium"; // get from config + } + getTimestampFromDOM(domNode) { const oMonthDomRef = domNode.getAttribute("data-sap-timestamp"); return parseInt(oMonthDomRef); diff --git a/packages/main/src/YearPicker.js b/packages/main/src/YearPicker.js index 6e2db466947a..8a3cf34abea8 100644 --- a/packages/main/src/YearPicker.js +++ b/packages/main/src/YearPicker.js @@ -29,6 +29,7 @@ const metadata = { timestamp: { type: Integer, }, + /** * Sets a calendar type used for display. * If not set, the calendar type of the global configuration is used. @@ -38,18 +39,56 @@ const metadata = { primaryCalendarType: { type: CalendarType, }, + + /** + * Determines the мinimum date available for selection. + * + * @type {String} + * @defaultvalue "" + * @since 1.0.0-rc.6 + * @public + */ + minDate: { + type: String, + }, + + /** + * Determines the maximum date available for selection. + * + * @type {String} + * @defaultvalue undefined + * @since 1.0.0-rc.6 + * @public + */ + maxDate: { + type: String, + defaultValue: undefined, + }, + _selectedYear: { type: Integer, noAttribute: true, }, + _yearIntervals: { type: Object, multiple: true, }, + _hidden: { type: Boolean, noAttribute: true, }, + /** + * Determines the format, displayed in the input field. + * + * @type {string} + * @defaultvalue "" + * @public + */ + formatPattern: { + type: String, + }, }, events: /** @lends sap.ui.webcomponents.main.YearPicker.prototype */ { /** @@ -97,7 +136,14 @@ class YearPicker extends UI5Element { this._itemNav = new ItemNavigation(this, { rowSize: 4 }); this._itemNav.getItemsCallback = function getItemsCallback() { - return [].concat(...this._yearIntervals); + const focusableYears = []; + + for (let i = 0; i < this._yearIntervals.length; i++) { + const yearInterval = this._yearIntervals[i].filter(x => !x.disabled); + focusableYears.push(yearInterval); + } + + return [].concat(...focusableYears); }.bind(this); this._itemNav.attachEvent( @@ -149,6 +195,11 @@ class YearPicker extends UI5Element { year.classes += " ui5-yp-item--selected"; } + if ((this.minDate || this.maxDate) && this._isOutOfSelectableRange(oCalDate.getYear())) { + year.classes += " ui5-yp-item--disabled"; + year.disabled = true; + } + if (intervals[intervalIndex]) { intervals[intervalIndex].push(year); } @@ -181,6 +232,10 @@ class YearPicker extends UI5Element { return this.primaryCalendarType || getCalendarType() || LocaleData.getInstance(getLocale()).getPreferredCalendarType(); } + get _isPattern() { + return this._formatPattern !== "medium" && this._formatPattern !== "short" && this._formatPattern !== "long"; + } + _onclick(event) { if (event.target.className.indexOf("ui5-yp-item") > -1) { const timestamp = this.getTimestampFromDom(event.target); @@ -248,9 +303,65 @@ class YearPicker extends UI5Element { return; } + if (this._isOutOfSelectableRange(oCalDate.getYear() - YearPicker._MIDDLE_ITEM_INDEX) + && this._isOutOfSelectableRange(oCalDate.getYear() + YearPicker._MIDDLE_ITEM_INDEX)) { + return; + } + + if (this._isOutOfSelectableRange(oCalDate.getYear() - YearPicker._MIDDLE_ITEM_INDEX) + && this._isOutOfSelectableRange(oCalDate.getYear() + YearPicker._MIDDLE_ITEM_INDEX)) { + return; + } + this.timestamp = oCalDate.valueOf() / 1000; } + get _formatPattern() { + return this.formatPattern || "medium"; // get from config + } + + _isOutOfSelectableRange(year) { + const minDate = new Date(this._minDate), + maxDate = new Date(this._maxDate), + minDateCheck = minDate && year < minDate.getFullYear(), + maxDateCheck = maxDate && year > maxDate.getFullYear(); + + return minDateCheck || maxDateCheck; + } + + get _maxDate() { + if (this.maxDate) { + const jsDate = new Date(this.getFormat().parse(this.maxDate).getFullYear(), this.getFormat().parse(this.maxDate).getMonth(), this.getFormat().parse(this.maxDate).getDate()); + const oCalDate = CalendarDate.fromTimestamp(jsDate.getTime(), this._primaryCalendarType); + return oCalDate.valueOf(); + } + return this.maxDate; + } + + get _minDate() { + if (this.minDate) { + const jsDate = new Date(this.getFormat().parse(this.minDate).getFullYear(), this.getFormat().parse(this.minDate).getMonth(), this.getFormat().parse(this.minDate).getDate()); + const oCalDate = CalendarDate.fromTimestamp(jsDate.getTime(), this._primaryCalendarType); + return oCalDate.valueOf(); + } + return this.minDate; + } + + getFormat() { + if (this._isPattern) { + this._oDateFormat = DateFormat.getInstance({ + pattern: this._formatPattern, + calendarType: this._primaryCalendarType, + }); + } else { + this._oDateFormat = DateFormat.getInstance({ + style: this._formatPattern, + calendarType: this._primaryCalendarType, + }); + } + return this._oDateFormat; + } + get styles() { return { main: { diff --git a/packages/main/src/themes/CalendarHeader.css b/packages/main/src/themes/CalendarHeader.css index e3c0df2d1ec9..a35c2b9832f8 100644 --- a/packages/main/src/themes/CalendarHeader.css +++ b/packages/main/src/themes/CalendarHeader.css @@ -28,6 +28,17 @@ font-size: var(--sapFontMediumSize); } +.ui5-calheader-arrowbtn.ui5-calheader-arrowbtn-disabled:hover, +.ui5-calheader-arrowbtn.ui5-calheader-arrowbtn-disabled:active, +.ui5-calheader-arrowbtn.ui5-calheader-arrowbtn-disabled:focus, +.ui5-calheader-arrowbtn.ui5-calheader-arrowbtn-disabled { + pointer-events: none; + opacity: 0.4; + outline: none; + background-color: var(--sapButton_Lite_Background); + color: var(--sapButton_TextColor); +} + .ui5-calheader-arrowbtn:focus { outline: none; } diff --git a/packages/main/src/themes/DayPicker.css b/packages/main/src/themes/DayPicker.css index 92f57b394f58..d82082fa786f 100644 --- a/packages/main/src/themes/DayPicker.css +++ b/packages/main/src/themes/DayPicker.css @@ -84,6 +84,12 @@ background: var(--_ui5_daypicker_item_weekend_background_color); } +.ui5-dp-item.ui5-dp-item--disabled { + pointer-events: none; + opacity: 0.5; +} + + .ui5-dp-item.ui5-dp-item--weeekend:hover { background: var(--_ui5_daypicker_item_weekend_hover_background_color); } diff --git a/packages/main/src/themes/MonthPicker.css b/packages/main/src/themes/MonthPicker.css index 7240a3ea6dad..fc5b4becd9ab 100644 --- a/packages/main/src/themes/MonthPicker.css +++ b/packages/main/src/themes/MonthPicker.css @@ -47,6 +47,11 @@ color: var(--sapContent_ContrastTextColor); } +.ui5-mp-item.ui5-mp-item--disabled { + pointer-events: none; + opacity: 0.5; +} + .ui5-mp-item.ui5-mp-item--selected:focus { background-color: var(--_ui5_monthpicker_item_selected_focus); } diff --git a/packages/main/src/themes/YearPicker.css b/packages/main/src/themes/YearPicker.css index 3f3e1a356350..fbf42806fb50 100644 --- a/packages/main/src/themes/YearPicker.css +++ b/packages/main/src/themes/YearPicker.css @@ -58,6 +58,11 @@ color: var(--sapContent_ContrastTextColor); } +.ui5-yp-item.ui5-yp-item--disabled { + pointer-events: none; + opacity: 0.5; +} + .ui5-yp-item.ui5-yp-item--selected:focus { background-color: var(--_ui5_yearpicker_item_selected_focus); } diff --git a/packages/main/test/pageobjects/DatePickerTestPage.js b/packages/main/test/pageobjects/DatePickerTestPage.js index d5b7e59353b5..f30ff0f75522 100644 --- a/packages/main/test/pageobjects/DatePickerTestPage.js +++ b/packages/main/test/pageobjects/DatePickerTestPage.js @@ -83,6 +83,18 @@ class DatePickerTestPage { .$$(".ui5-yp-item")[index]; } + getDisplayedMonth(index) { + return browser + .$(`.${this.staticAreaItemClassName}`).shadow$(`ui5-calendar`).shadow$(`ui5-monthpicker`).shadow$(`.ui5-mp-root`) + .$$(".ui5-mp-item")[index]; + } + + getDisplayedDay(index) { + return browser + .$(`.${this.staticAreaItemClassName}`).shadow$(`ui5-calendar`).shadow$(`ui5-daypicker`).shadow$(`.ui5-dp-root`).$(".ui5-dp-content").$(".ui5-dp-items-container") + .$$(".ui5-dp-item")[index]; + } + isValid(value) { return browser.execute((id, value) => { return document.querySelector(id).isValid(value); diff --git a/packages/main/test/pages/DatePicker.html b/packages/main/test/pages/DatePicker.html index b191b5ae473d..3d43989f4466 100644 --- a/packages/main/test/pages/DatePicker.html +++ b/packages/main/test/pages/DatePicker.html @@ -79,7 +79,25 @@

japanese calendar type

explicitly set empty placeholder

- + + +

Date picker with min and max date 9/1/2019 - 11/1/2019

+ + +

Date picker with only min date 1/1/2000

+ + +

Date picker with only max date date 1/1/2100

+ + +

1 months range

+ +

2 months range

+ +

3 months range

+ +

1 year range

+