From 3e684b478fb508a87f0af29f917d94e58352ffe9 Mon Sep 17 00:00:00 2001 From: Vladislav Tasev Date: Fri, 8 Jan 2021 12:05:46 +0200 Subject: [PATCH] feat: refactoring and new features for pickers (#2598) --- packages/base/src/Keys.js | 6 + packages/base/src/UI5Element.js | 8 +- packages/base/src/delegate/ItemNavigation.js | 90 +- .../base/src/delegate/ScrollEnablement.js | 25 +- .../base/src/features/browsersupport/IE11.js | 1 + .../thirdparty/Array.prototype.findIndex.js | 46 + .../thirdparty/Element.prototype.closest.js | 4 +- packages/base/src/types/CalendarSelection.js | 17 - .../base/src/types/ItemNavigationBehavior.js | 9 +- packages/base/src/util/Caret.js | 45 + .../localization/src/dates/ExtremeDates.js | 39 + .../src/dates/getRoundedTimestamp.js | 14 + .../localization/src/dates/modifyDateBy.js | 42 + packages/main/bundle.esm.js | 1 + packages/main/src/Calendar.hbs | 97 +- packages/main/src/Calendar.js | 736 ++------------- packages/main/src/CalendarHeader.hbs | 56 +- packages/main/src/CalendarHeader.js | 136 ++- packages/main/src/CalendarPart.js | 122 +++ .../{PickerBase.js => DateComponentBase.js} | 102 +- packages/main/src/DatePicker.hbs | 8 +- packages/main/src/DatePicker.js | 623 +++--------- packages/main/src/DatePickerPopover.hbs | 28 +- packages/main/src/DateRangePicker.hbs | 1 - packages/main/src/DateRangePicker.js | 400 +++----- packages/main/src/DateTimePicker.js | 480 ++-------- packages/main/src/DateTimePickerPopover.hbs | 71 +- packages/main/src/DayPicker.hbs | 20 +- packages/main/src/DayPicker.js | 890 ++++++++---------- packages/main/src/DurationPicker.hbs | 29 - packages/main/src/DurationPicker.js | 609 ++---------- packages/main/src/DurationPickerPopover.hbs | 48 - packages/main/src/Input.js | 26 + packages/main/src/MonthPicker.hbs | 12 +- packages/main/src/MonthPicker.js | 264 ++++-- packages/main/src/SuggestionListItem.js | 2 +- packages/main/src/TimePicker.hbs | 6 +- packages/main/src/TimePicker.js | 754 +-------------- packages/main/src/TimePickerBase.js | 455 +++++++++ packages/main/src/TimePickerPopover.hbs | 76 +- packages/main/src/TimeSelection.hbs | 60 ++ packages/main/src/TimeSelection.js | 494 ++++++++++ packages/main/src/WheelSlider.hbs | 8 +- packages/main/src/WheelSlider.js | 62 +- packages/main/src/YearPicker.hbs | 16 +- packages/main/src/YearPicker.js | 347 +++---- .../main/src/i18n/messagebundle.properties | 3 + packages/main/src/themes/Calendar.css | 25 +- packages/main/src/themes/CalendarHeader.css | 9 +- .../main/src/themes/DateTimePickerPopover.css | 10 +- packages/main/src/themes/DayPicker.css | 3 +- packages/main/src/themes/DurationPicker.css | 29 - .../main/src/themes/DurationPickerPopover.css | 29 - packages/main/src/themes/MonthPicker.css | 2 +- .../main/src/themes/TimePickerPopover.css | 19 - packages/main/src/themes/TimeSelection.css | 23 + packages/main/src/themes/WheelSlider.css | 30 +- packages/main/src/themes/YearPicker.css | 2 +- .../main/src/themes/base/sizes-parameters.css | 12 +- .../main/src/timepicker-utils/TimeSlider.js | 28 +- .../main/src/types/CalendarSelectionMode.js | 17 + packages/main/src/util/DateTime.js | 25 - .../test/pageobjects/DatePickerTestPage.js | 10 +- packages/main/test/pages/Calendar.html | 12 +- packages/main/test/pages/DatePicker.html | 21 +- packages/main/test/pages/DatePicker_fg.html | 164 ---- packages/main/test/pages/TimePicker.html | 3 +- packages/main/test/pages/TimeSelection.html | 34 + .../main/test/samples/Calendar.sample.html | 8 +- packages/main/test/specs/Calendar.spec.js | 104 +- .../main/test/specs/DatePicker-fg.spec.js | 33 - packages/main/test/specs/DatePicker.spec.js | 158 ++-- .../main/test/specs/DateTimePicker.spec.js | 63 +- .../main/test/specs/DurationPicker.spec.js | 36 +- packages/main/test/specs/TimePicker.spec.js | 77 +- packages/main/test/specs/WheelSlider.spec.js | 6 +- packages/tools/lib/documentation/index.js | 50 +- 77 files changed, 3413 insertions(+), 5017 deletions(-) create mode 100644 packages/base/src/thirdparty/Array.prototype.findIndex.js delete mode 100644 packages/base/src/types/CalendarSelection.js create mode 100644 packages/base/src/util/Caret.js create mode 100644 packages/localization/src/dates/ExtremeDates.js create mode 100644 packages/localization/src/dates/getRoundedTimestamp.js create mode 100644 packages/localization/src/dates/modifyDateBy.js create mode 100644 packages/main/src/CalendarPart.js rename packages/main/src/{PickerBase.js => DateComponentBase.js} (63%) delete mode 100644 packages/main/src/DateRangePicker.hbs delete mode 100644 packages/main/src/DurationPicker.hbs delete mode 100644 packages/main/src/DurationPickerPopover.hbs create mode 100644 packages/main/src/TimePickerBase.js create mode 100644 packages/main/src/TimeSelection.hbs create mode 100644 packages/main/src/TimeSelection.js delete mode 100644 packages/main/src/themes/DurationPicker.css delete mode 100644 packages/main/src/themes/DurationPickerPopover.css create mode 100644 packages/main/src/themes/TimeSelection.css create mode 100644 packages/main/src/types/CalendarSelectionMode.js delete mode 100644 packages/main/src/util/DateTime.js delete mode 100644 packages/main/test/pages/DatePicker_fg.html create mode 100644 packages/main/test/pages/TimeSelection.html delete mode 100644 packages/main/test/specs/DatePicker-fg.spec.js diff --git a/packages/base/src/Keys.js b/packages/base/src/Keys.js index 09d3652ebce2..be7d4b69bf76 100644 --- a/packages/base/src/Keys.js +++ b/packages/base/src/Keys.js @@ -105,8 +105,12 @@ const KeyCodes = { const isEnter = event => (event.key ? event.key === "Enter" : event.keyCode === KeyCodes.ENTER) && !hasModifierKeys(event); +const isEnterShift = event => (event.key ? event.key === "Enter" : event.keyCode === KeyCodes.ENTER) && checkModifierKeys(event, false, false, true); + const isSpace = event => (event.key ? (event.key === "Spacebar" || event.key === " ") : event.keyCode === KeyCodes.SPACE) && !hasModifierKeys(event); +const isSpaceShift = event => (event.key ? (event.key === "Spacebar" || event.key === " ") : event.keyCode === KeyCodes.SPACE) && checkModifierKeys(event, false, false, true); + const isLeft = event => (event.key ? (event.key === "ArrowLeft" || event.key === "Left") : event.keyCode === KeyCodes.ARROW_LEFT) && !hasModifierKeys(event); const isRight = event => (event.key ? (event.key === "ArrowRight" || event.key === "Right") : event.keyCode === KeyCodes.ARROW_RIGHT) && !hasModifierKeys(event); @@ -171,7 +175,9 @@ const checkModifierKeys = (event, bCtrlKey, bAltKey, bShiftKey) => event.shiftKe export { isEnter, + isEnterShift, isSpace, + isSpaceShift, isLeft, isRight, isUp, diff --git a/packages/base/src/UI5Element.js b/packages/base/src/UI5Element.js index 6a062da889e8..0f0a848e8faa 100644 --- a/packages/base/src/UI5Element.js +++ b/packages/base/src/UI5Element.js @@ -997,11 +997,17 @@ class UI5Element extends HTMLElement { } }, set(value) { + let isDifferent; value = this.constructor.getMetadata().constructor.validatePropertyValue(value, propData); const oldState = this._state[prop]; + if (propData.multiple && propData.compareValues) { + isDifferent = !arraysAreEqual(oldState, value); + } else { + isDifferent = oldState !== value; + } - if (oldState !== value) { + if (isDifferent) { this._state[prop] = value; _invalidate.call(this, { type: "property", diff --git a/packages/base/src/delegate/ItemNavigation.js b/packages/base/src/delegate/ItemNavigation.js index ee7797494d23..c65da45e37f3 100644 --- a/packages/base/src/delegate/ItemNavigation.js +++ b/packages/base/src/delegate/ItemNavigation.js @@ -6,8 +6,6 @@ import { isRight, isHome, isEnd, - isPageUp, - isPageDown, } from "../Keys.js"; import getActiveElement from "../util/getActiveElement.js"; @@ -23,7 +21,6 @@ import ItemNavigationBehavior from "../types/ItemNavigationBehavior.js"; * - Up/down * - Left/right * - Home/End - * - PageUp/PageDown * * Usage: * 1) Use the "getItemsCallback" constructor property to pass a callback to ItemNavigation, which, whenever called, will return the list of items to navigate among. @@ -57,11 +54,9 @@ class ItemNavigation extends EventProvider { * - currentIndex: the index of the item that will be initially selected (from which navigation will begin) * - navigationMode (Auto|Horizontal|Vertical): whether the items are displayed horizontally (Horizontal), vertically (Vertical) or as a matrix (Auto) meaning the user can navigate in both directions (up/down and left/right) * - rowSize: tells how many items per row there are when the items are not rendered as a flat list but rather as a matrix. Relevant for navigationMode=Auto - * - behavior (Static|Cycling|Paging): tells what to do when trying to navigate beyond the first and last items + * - behavior (Static|Cycling): tells what to do when trying to navigate beyond the first and last items * Static means that nothing happens if the user tries to navigate beyond the first/last item. * Cycling means that when the user navigates beyond the last item they go to the first and vice versa. - * Paging means that when the urse navigates beyond the first/last item, a new "page" of items appears (as commonly observed with calendars for example) - * - pageSize: tells how many items the user skips by using the PageUp/PageDown keys * - getItemsCallback: function that, when called, returns an array with all items the user can navigate among * - affectedPropertiesNames: a list of metadata properties on the root component which, upon user navigation, will be reassigned by address thus causing the root component to invalidate */ @@ -76,8 +71,6 @@ class ItemNavigation extends EventProvider { this.horizontalNavigationOn = autoNavigation || navigationMode === NavigationMode.Horizontal; this.verticalNavigationOn = autoNavigation || navigationMode === NavigationMode.Vertical; - this.pageSize = options.pageSize; - if (options.affectedPropertiesNames) { this.affectedPropertiesNames = options.affectedPropertiesNames; } @@ -86,10 +79,6 @@ class ItemNavigation extends EventProvider { this._getItems = options.getItemsCallback; } - const trueFunction = () => true; - this._hasNextPage = typeof options.hasNextPageCallback === "function" ? options.hasNextPageCallback : trueFunction; - this._hasPreviousPage = typeof options.hasPreviousPageCallback === "function" ? options.hasPreviousPageCallback : trueFunction; - this.rootWebComponent = rootWebComponent; this.rootWebComponent.addEventListener("keydown", this.onkeydown.bind(this)); this.rootWebComponent._onComponentStateFinalized = () => { @@ -113,9 +102,9 @@ class ItemNavigation extends EventProvider { async _onKeyPress(event) { if (this.currentIndex >= this._getItems().length) { - this.onOverflowBottomEdge(event); + this.onOverflowBottomEdge(); } else if (this.currentIndex < 0) { - this.onOverflowTopEdge(event); + this.onOverflowTopEdge(); } event.preventDefault(); @@ -124,7 +113,6 @@ class ItemNavigation extends EventProvider { this.update(); this.focusCurrent(); - this.fireEvent(ItemNavigation.AFTER_FOCUS); } onkeydown(event) { @@ -151,14 +139,6 @@ class ItemNavigation extends EventProvider { if (isEnd(event)) { return this._handleEnd(event); } - - if (isPageUp(event)) { - return this._handlePageUp(event); - } - - if (isPageDown(event)) { - return this._handlePageDown(event); - } } _handleUp(event) { @@ -205,20 +185,6 @@ class ItemNavigation extends EventProvider { } } - _handlePageUp(event) { - if (this._canNavigate()) { - this.currentIndex -= this.pageSize; - this._onKeyPress(event); - } - } - - _handlePageDown(event) { - if (this._canNavigate()) { - this.currentIndex += this.pageSize; - this._onKeyPress(event); - } - } - /** * Call this method to set a new "current" (selected) item in the item navigation * Note: the item passed to this function must be one of the items, returned by the getItemsCallback function @@ -323,71 +289,25 @@ class ItemNavigation extends EventProvider { onOverflowBottomEdge(event) { const items = this._getItems(); - const offset = (this.currentIndex - items.length) % this.rowSize; if (this.behavior === ItemNavigationBehavior.Cyclic) { this.currentIndex = 0; return; } - if (this.behavior === ItemNavigationBehavior.Paging) { - this._handleNextPage(); - } else { - this.currentIndex = items.length - 1; - } - - this.fireEvent(ItemNavigation.BORDER_REACH, { - start: false, - end: true, - originalEvent: event, - offset, - }); + this.currentIndex = items.length - 1; } onOverflowTopEdge(event) { const items = this._getItems(); - const offsetRight = (this.currentIndex + this.rowSize) % this.rowSize; - const offset = offsetRight < 0 ? (this.rowSize + offsetRight) : offsetRight; if (this.behavior === ItemNavigationBehavior.Cyclic) { this.currentIndex = items.length - 1; return; } - if (this.behavior === ItemNavigationBehavior.Paging) { - this._handlePrevPage(); - } else { - this.currentIndex = 0; - } - - this.fireEvent(ItemNavigation.BORDER_REACH, { - start: true, - end: false, - originalEvent: event, - offset, - }); - } - - _handleNextPage() { - const items = this._getItems(); - - if (!this._hasNextPage()) { - this.currentIndex = items.length - 1; - } else { - this.currentIndex -= this.pageSize; - } - } - - _handlePrevPage() { - if (!this._hasPreviousPage()) { - this.currentIndex = 0; - } else { - this.currentIndex = this.pageSize + this.currentIndex; - } + this.currentIndex = 0; } } -ItemNavigation.BORDER_REACH = "_borderReach"; -ItemNavigation.AFTER_FOCUS = "_afterFocus"; - export default ItemNavigation; diff --git a/packages/base/src/delegate/ScrollEnablement.js b/packages/base/src/delegate/ScrollEnablement.js index 41d2a123f0f1..48f15f03eba6 100644 --- a/packages/base/src/delegate/ScrollEnablement.js +++ b/packages/base/src/delegate/ScrollEnablement.js @@ -42,7 +42,30 @@ class ScrollEnablement extends EventProvider { return this._container; } - scrollTo(left, top) { + /** + * Scrolls the container to the left/top position, retrying retryCount times, if the container is not yet painted + * + * @param left + * @param top + * @param retryCount + * @param retryInterval + * @returns {Promise} resolved when scrolled successfully + */ + async scrollTo(left, top, retryCount = 0, retryInterval = 0) { + let containerPainted = this.scrollContainer.clientHeight > 0 && this.scrollContainer.clientWidth > 0; + + /* eslint-disable no-loop-func, no-await-in-loop */ + while (!containerPainted && retryCount > 0) { + await new Promise(resolve => { + setTimeout(() => { + containerPainted = this.scrollContainer.clientHeight > 0 && this.scrollContainer.clientWidth > 0; + retryCount--; + resolve(); + }, retryInterval); + }); + } + /* eslint-disable no-loop-func, no-await-in-loop */ + this._container.scrollLeft = left; this._container.scrollTop = top; } diff --git a/packages/base/src/features/browsersupport/IE11.js b/packages/base/src/features/browsersupport/IE11.js index 2115d260b428..96c67c3d18ae 100644 --- a/packages/base/src/features/browsersupport/IE11.js +++ b/packages/base/src/features/browsersupport/IE11.js @@ -10,6 +10,7 @@ import "../../thirdparty/Object.entries.js"; // Array import "../../thirdparty/Array.prototype.fill.js"; import "../../thirdparty/Array.prototype.find.js"; +import "../../thirdparty/Array.prototype.findIndex.js"; import "../../thirdparty/Array.prototype.includes.js"; // Map diff --git a/packages/base/src/thirdparty/Array.prototype.findIndex.js b/packages/base/src/thirdparty/Array.prototype.findIndex.js new file mode 100644 index 000000000000..44c636fad5dd --- /dev/null +++ b/packages/base/src/thirdparty/Array.prototype.findIndex.js @@ -0,0 +1,46 @@ +// https://tc39.github.io/ecma262/#sec-array.prototype.findindex +if (!Array.prototype.findIndex) { + Object.defineProperty(Array.prototype, 'findIndex', { + value: function(predicate) { + // 1. Let O be ? ToObject(this value). + if (this == null) { + throw new TypeError('"this" is null or not defined'); + } + + var o = Object(this); + + // 2. Let len be ? ToLength(? Get(O, "length")). + var len = o.length >>> 0; + + // 3. If IsCallable(predicate) is false, throw a TypeError exception. + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + + // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. + var thisArg = arguments[1]; + + // 5. Let k be 0. + var k = 0; + + // 6. Repeat, while k < len + while (k < len) { + // a. Let Pk be ! ToString(k). + // b. Let kValue be ? Get(O, Pk). + // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). + // d. If testResult is true, return k. + var kValue = o[k]; + if (predicate.call(thisArg, kValue, k, o)) { + return k; + } + // e. Increase k by 1. + k++; + } + + // 7. Return -1. + return -1; + }, + configurable: true, + writable: true + }); +} diff --git a/packages/base/src/thirdparty/Element.prototype.closest.js b/packages/base/src/thirdparty/Element.prototype.closest.js index 9147b41610b7..cd711cd59f49 100644 --- a/packages/base/src/thirdparty/Element.prototype.closest.js +++ b/packages/base/src/thirdparty/Element.prototype.closest.js @@ -1,9 +1,9 @@ if (!Element.prototype.closest) { Element.prototype.closest = function(s) { var el = this; - if (!document.documentElement.contains(el)) return null; + do { - if (el.matches(s)) return el; + if (Element.prototype.matches.call(el, s)) return el; el = el.parentElement || el.parentNode; } while (el !== null && el.nodeType === 1); return null; diff --git a/packages/base/src/types/CalendarSelection.js b/packages/base/src/types/CalendarSelection.js deleted file mode 100644 index 14290ca3e071..000000000000 --- a/packages/base/src/types/CalendarSelection.js +++ /dev/null @@ -1,17 +0,0 @@ -import DataType from "./DataType.js"; - -const CalendarSelections = { - Single: "Single", - Multiple: "Multiple", - Range: "Range", -}; - -class CalendarSelection extends DataType { - static isValid(value) { - return !!CalendarSelections[value]; - } -} - -CalendarSelection.generateTypeAccessors(CalendarSelections); - -export default CalendarSelection; diff --git a/packages/base/src/types/ItemNavigationBehavior.js b/packages/base/src/types/ItemNavigationBehavior.js index a08dcd6686e7..5154b941c533 100644 --- a/packages/base/src/types/ItemNavigationBehavior.js +++ b/packages/base/src/types/ItemNavigationBehavior.js @@ -4,18 +4,13 @@ */ const ItemNavigationBehavior = { /** - * Static behavior: when border of the items is reached, you can't go out of the cage. + * Static behavior: navigations stops at the first or last item. */ Static: "Static", /** - * Cycling behavior: when border of the items is reached, you can cycle through the items. + * Cycling behavior: navigating past the last item continues with the first and vice versa. */ Cyclic: "Cyclic", - - /** - * Paging behavior: when border of the items is reached, tou can go up/down based on the rowsize(e.g. DayPicker) - */ - Paging: "Paging", }; export default ItemNavigationBehavior; diff --git a/packages/base/src/util/Caret.js b/packages/base/src/util/Caret.js new file mode 100644 index 000000000000..80471daeb181 --- /dev/null +++ b/packages/base/src/util/Caret.js @@ -0,0 +1,45 @@ +/** + * Returns the caret (cursor) position of the specified text field (field). + * Return value range is 0-field.value.length. + */ +const getCaretPosition = field => { + // Initialize + let caretPos = 0; + + // IE Support + if (document.selection) { + // Set focus on the element + field.focus(); + + // To get cursor position, get empty selection range + const selection = document.selection.createRange(); + + // Move selection start to 0 position + selection.moveStart("character", -field.value.length); + + // The caret position is selection length + caretPos = selection.text.length; + } else if (field.selectionStart || field.selectionStart === "0") { // Firefox support + caretPos = field.selectionDirection === "backward" ? field.selectionStart : field.selectionEnd; + } + + return caretPos; +}; + +const setCaretPosition = (field, caretPos) => { + if (field.createTextRange) { + const range = field.createTextRange(); + range.move("character", caretPos); + range.select(); + } else if (field.selectionStart) { + field.focus(); + field.setSelectionRange(caretPos, caretPos); + } else { + field.focus(); + } +}; + +export { + getCaretPosition, + setCaretPosition, +}; diff --git a/packages/localization/src/dates/ExtremeDates.js b/packages/localization/src/dates/ExtremeDates.js new file mode 100644 index 000000000000..a4400485cfd7 --- /dev/null +++ b/packages/localization/src/dates/ExtremeDates.js @@ -0,0 +1,39 @@ +import CalendarDate from "./CalendarDate.js"; + +const cache = new Map(); + +const getMinCalendarDate = primaryCalendarType => { + const key = `min ${primaryCalendarType}`; + + if (!cache.has(key)) { + const minDate = new CalendarDate(1, 0, 1, primaryCalendarType); + minDate.setYear(1); + minDate.setMonth(0); + minDate.setDate(1); + cache.set(key, minDate); + } + + return cache.get(key); +}; + +const getMaxCalendarDate = primaryCalendarType => { + const key = `max ${primaryCalendarType}`; + + if (!cache.has(key)) { + const maxDate = new CalendarDate(1, 0, 1, primaryCalendarType); + maxDate.setYear(9999); + maxDate.setMonth(11); + const tempDate = new CalendarDate(maxDate, primaryCalendarType); + tempDate.setDate(1); + tempDate.setMonth(tempDate.getMonth() + 1, 0); + maxDate.setDate(tempDate.getDate());// 31st for Gregorian Calendar + cache.set(key, maxDate); + } + + return cache.get(key); +}; + +export { + getMinCalendarDate, + getMaxCalendarDate, +}; diff --git a/packages/localization/src/dates/getRoundedTimestamp.js b/packages/localization/src/dates/getRoundedTimestamp.js new file mode 100644 index 000000000000..d3db4e04499d --- /dev/null +++ b/packages/localization/src/dates/getRoundedTimestamp.js @@ -0,0 +1,14 @@ +/** + * Returns a timestamp with only the year, month and day (with zero hours, minutes and seconds) and without 000 for milliseconds + * @param millisecondsUTC + * @returns {number} + */ +const getRoundedTimestamp = millisecondsUTC => { + if (!millisecondsUTC) { + millisecondsUTC = new Date().getTime(); + } + const rounded = millisecondsUTC - (millisecondsUTC % (24 * 60 * 60 * 1000)); + return rounded / 1000; +}; + +export default getRoundedTimestamp; diff --git a/packages/localization/src/dates/modifyDateBy.js b/packages/localization/src/dates/modifyDateBy.js new file mode 100644 index 000000000000..e030a2f0c354 --- /dev/null +++ b/packages/localization/src/dates/modifyDateBy.js @@ -0,0 +1,42 @@ +import CalendarDate from "./CalendarDate.js"; + +/** + * Adds or subtracts a given amount of days/months/years from a date. + * If minDate or maxDate are given, the result will be enforced within these limits + * + * @param date CalendarDate instance + * @param amount how many days/months/years to add (can be a negative number) + * @param unit what to modify: "day", "month" or "year" + * @param minDate minimum date to enforce + * @param maxDate maximum date to enforce + */ +const modifyDateBy = (date, amount, unit, minDate = null, maxDate = null) => { + const newDate = new CalendarDate(date); + if (unit === "day") { + newDate.setDate(date.getDate() + amount); + } else if (unit === "month") { + newDate.setMonth(date.getMonth() + amount); + const stillSameMonth = amount === -1 && newDate.getMonth() === date.getMonth(); // f.e. PageUp remained in the same month + const monthSkipped = amount === 1 && newDate.getMonth() - date.getMonth() > 1; // f.e. PageDown skipped a whole month + if (stillSameMonth || monthSkipped) { // Select the last day of the month in any of these 2 scenarios + newDate.setDate(0); + } + } else { + newDate.setYear(date.getYear() + amount); + if (newDate.getMonth() !== date.getMonth()) { // f.e. 29th Feb to next/prev year + newDate.setDate(0); // Select the last day of the month + } + } + + if (minDate && newDate.valueOf() < minDate.valueOf()) { + return new CalendarDate(minDate); + } + + if (maxDate && newDate.valueOf() > maxDate.valueOf()) { + return new CalendarDate(maxDate); + } + + return newDate; +}; + +export default modifyDateBy; diff --git a/packages/main/bundle.esm.js b/packages/main/bundle.esm.js index 72d76ef12ca9..20912b7e06e7 100644 --- a/packages/main/bundle.esm.js +++ b/packages/main/bundle.esm.js @@ -81,6 +81,7 @@ import TableColumn from "./dist/TableColumn.js"; import TableRow from "./dist/TableRow.js"; import TableCell from "./dist/TableCell.js"; import TextArea from "./dist/TextArea.js"; +import TimeSelection from "./dist/TimeSelection.js"; import TimePicker from "./dist/TimePicker.js"; import Title from "./dist/Title.js"; import Toast from "./dist/Toast.js"; diff --git a/packages/main/src/Calendar.hbs b/packages/main/src/Calendar.hbs index 50ed216837a2..7fcc4ffda6b0 100644 --- a/packages/main/src/Calendar.hbs +++ b/packages/main/src/Calendar.hbs @@ -1,67 +1,64 @@
- - - -
+
+ + +
diff --git a/packages/main/src/Calendar.js b/packages/main/src/Calendar.js index c8a5b6cb4bf8..61453b37b3f7 100644 --- a/packages/main/src/Calendar.js +++ b/packages/main/src/Calendar.js @@ -1,20 +1,14 @@ -import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; -import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; -import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; -import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; -import CalendarSelection from "@ui5/webcomponents-base/dist/types/CalendarSelection.js"; +import RenderScheduler from "@ui5/webcomponents-base/dist/RenderScheduler.js"; import { isF4, isF4Shift, - isTabNext, - isTabPrevious, } from "@ui5/webcomponents-base/dist/Keys.js"; -import RenderScheduler from "@ui5/webcomponents-base/dist/RenderScheduler.js"; -import PickerBase from "./PickerBase.js"; +import CalendarPart from "./CalendarPart.js"; import CalendarHeader from "./CalendarHeader.js"; import DayPicker from "./DayPicker.js"; import MonthPicker from "./MonthPicker.js"; import YearPicker from "./YearPicker.js"; +import CalendarSelectionMode from "./types/CalendarSelectionMode.js"; // Default calendar for bundling import "@ui5/webcomponents-localization/dist/features/calendar/Gregorian.js"; @@ -33,20 +27,19 @@ const metadata = { properties: /** @lends sap.ui.webcomponents.main.Calendar.prototype */ { /** * Defines the type of selection used in the calendar component. - * The property takes as value an object of type CalendarSelection. * Accepted property values are:
*
    - *
  • CalendarSelection.Single - enables a single date selection.(default value)
  • - *
  • CalendarSelection.Range - enables selection of a date range.
  • - *
  • CalendarSelection.Multiple - enables selection of multiple dates.
  • + *
  • CalendarSelectionMode.Single - enables a single date selection.(default value)
  • + *
  • CalendarSelectionMode.Range - enables selection of a date range.
  • + *
  • CalendarSelectionMode.Multiple - enables selection of multiple dates.
  • *
- * @type {CalendarSelection} + * @type {CalendarSelectionMode} * @defaultvalue "Single" * @public */ - selection: { - type: CalendarSelection, - defaultValue: CalendarSelection.Single, + selectionMode: { + type: CalendarSelectionMode, + defaultValue: CalendarSelectionMode.Single, }, /** @@ -64,30 +57,20 @@ const metadata = { type: Boolean, }, - _header: { - type: Object, - }, - - _oMonth: { - type: Object, - }, - - _monthPicker: { - type: Object, - }, - - _yearPicker: { - type: Object, + /** + * Which picker is currently visible to the user: day/month/year + */ + _currentPicker: { + type: String, + defaultValue: "day", }, - _calendarWidth: { - type: String, - noAttribute: true, + _previousButtonDisabled: { + type: Boolean, }, - _calendarHeight: { - type: String, - noAttribute: true, + _nextButtonDisabled: { + type: Boolean, }, }, events: /** @lends sap.ui.webcomponents.main.Calendar.prototype */ { @@ -101,7 +84,7 @@ const metadata = { detail: { dates: { type: Array }, }, - }, + }, }, }; @@ -127,35 +110,43 @@ const metadata = { * *

Keyboard Handling

* The ui5-calendar provides advanced keyboard handling. - * If the ui5-calendar is focused the user can - * choose a picker by using the following shortcuts:
- *
    - *
  • [F4] - Shows month picker
  • - *
  • [SHIFT] + [F4] - Shows year picker
  • - *
    * When a picker is showed and focused the user can use the following keyboard * shortcuts in order to perform a navigation: *
    * - Day picker:
    *
      + *
    • [F4] - Shows month picker
    • + *
    • [SHIFT] + [F4] - Shows year picker
    • *
    • [PAGEUP] - Navigate to the previous month
    • *
    • [PAGEDOWN] - Navigate to the next month
    • *
    • [SHIFT] + [PAGEUP] - Navigate to the previous year
    • *
    • [SHIFT] + [PAGEDOWN] - Navigate to the next year
    • *
    • [CTRL] + [SHIFT] + [PAGEUP] - Navigate ten years backwards
    • *
    • [CTRL] + [SHIFT] + [PAGEDOWN] - Navigate ten years forwards
    • + *
    • [HOME] - Navigate to the first day of the week + *
    • [END] - Navigate to the last day of the week + *
    • [CTRL] + [HOME] - Navigate to the first day of the month + *
    • [CTRL] + [END] - Navigate to the last day of the month *
    *
    * - Month picker:
    *
      *
    • [PAGEUP] - Navigate to the previous month
    • *
    • [PAGEDOWN] - Navigate to the next month
    • + *
    • [HOME] - Navigate to the first month of the current row + *
    • [END] - Navigate to the last month of the current row + *
    • [CTRL] + [HOME] - Navigate to the first month of the current year + *
    • [CTRL] + [END] - Navigate to the last month of the year *
    *
    * - Year picker:
    *
      *
    • [PAGEUP] - Navigate to the previous year range
    • *
    • [PAGEDOWN] - Navigate the next year range
    • + *
    • [HOME] - Navigate to the first year of the current row + *
    • [END] - Navigate to the last year of the current row + *
    • [CTRL] + [HOME] - Navigate to the first year of the current year range + *
    • [CTRL] + [END] - Navigate to the last year of the current year range *
    *
    * @@ -166,12 +157,12 @@ const metadata = { * @constructor * @author SAP SE * @alias sap.ui.webcomponents.main.Calendar - * @extends sap.ui.webcomponents.main.PickerBase + * @extends CalendarPart * @tagname ui5-calendar * @public * @since 1.0.0-rc.11 */ -class Calendar extends PickerBase { +class Calendar extends CalendarPart { static get metadata() { return metadata; } @@ -184,639 +175,98 @@ class Calendar extends PickerBase { return calendarCSS; } - constructor() { - super(); - this._header = {}; - this._header.onPressPrevious = this._handlePrevious.bind(this); - this._header.onPressNext = this._handleNext.bind(this); - this._header.onBtn1Press = this._handleMonthButtonPress.bind(this); - this._header.onBtn2Press = this._handleYearButtonPress.bind(this); - - this._oMonth = {}; - this._oMonth.onSelectedDatesChange = this._handleSelectedDatesChange.bind(this); - this._oMonth.onNavigate = this._handleMonthNavigate.bind(this); - - - this._monthPicker = {}; - this._monthPicker._hidden = true; - this._monthPicker.onSelectedMonthChange = this._handleSelectedMonthChange.bind(this); - this._monthPicker.onNavigate = this._handleYearNavigate.bind(this); - - this._yearPicker = {}; - this._yearPicker._hidden = true; - this._yearPicker.onSelectedYearChange = this._handleSelectedYearChange.bind(this); - this._yearPicker.onNavigate = this._handleYearNavigate.bind(this); - - this._isShiftingYears = false; - } - - onBeforeRendering() { - const localeData = getCachedLocaleDataInstance(getLocale()); - const oYearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._primaryCalendarType }); - const firstDayOfCalendarTimeStamp = this._getMinCalendarDate(); - - if ((this.minDate || this.maxDate) && this._timestamp && !this.isInValidRange(this._timestamp * 1000)) { - if (this._minDate) { - this.timestamp = this._minDate / 1000; - } else { - this.timestamp = (new Date(firstDayOfCalendarTimeStamp)).getTime() / 1000; - } - } - - this._oMonth.formatPattern = this._formatPattern; - this._oMonth.timestamp = this._timestamp; - this._oMonth.selectedDates = [...this.selectedDates]; - this._oMonth.primaryCalendarType = this._primaryCalendarType; - this._oMonth.selection = this.selection; - this._oMonth.minDate = this.minDate; - this._oMonth.maxDate = this.maxDate; - this._header.monthText = localeData.getMonths("wide", this._primaryCalendarType)[this._month]; - this._header.yearText = oYearFormat.format(this._localDate, true); - this._header.tabIndex = "-1"; - - // month picker - this._monthPicker.primaryCalendarType = this._primaryCalendarType; - this._monthPicker.timestamp = this._timestamp; - - this._yearPicker.primaryCalendarType = this._primaryCalendarType; - - if (!this._isShiftingYears) { - // year picker - this._yearPicker.timestamp = this._timestamp; - } - - this._isShiftingYears = false; - - this._refreshNavigationButtonsState(); - } - - onAfterRendering() { - this._setDayPickerCurrentIndex(this._calendarDate, false); + async onAfterRendering() { + await RenderScheduler.whenFinished(); // Await for the current picker to render and then ask if it has previous/next pages + this._previousButtonDisabled = !this._currentPickerDOM._hasPreviousPage(); + this._nextButtonDisabled = !this._currentPickerDOM._hasNextPage(); } - _refreshNavigationButtonsState() { - const minDateParsed = this.minDate && this.getFormat().parse(this.minDate); - const maxDateParsed = this.maxDate && this.getFormat().parse(this.maxDate); - let currentMonth = 0; - let currentYear = 1; - - currentMonth = this.timestamp && CalendarDate.fromTimestamp(this.timestamp * 1000).getMonth(); - currentYear = this.timestamp && CalendarDate.fromTimestamp(this.timestamp * 1000).getYear(); - - if (!this._oMonth._hidden) { - if (this.minDate - && minDateParsed.getMonth() === currentMonth - && minDateParsed.getFullYear() === currentYear) { - this._header._isPrevButtonDisabled = true; - } else { - this._header._isPrevButtonDisabled = false; - } - - if (this.maxDate - && maxDateParsed.getMonth() === currentMonth - && maxDateParsed.getFullYear() === currentYear) { - this._header._isNextButtonDisabled = true; - } else { - this._header._isNextButtonDisabled = false; - } - } - - if (!this._monthPicker._hidden) { - if (this.minDate - && currentYear === minDateParsed.getFullYear()) { - this._header._isPrevButtonDisabled = true; - } else { - this._header._isPrevButtonDisabled = false; - } - - if (this.maxDate - && currentYear === maxDateParsed.getFullYear()) { - this._header._isNextButtonDisabled = true; - } else { - this._header._isNextButtonDisabled = false; - } - } - - if (!this._yearPicker._hidden) { - const cellsFromTheStart = 7; - const cellsToTheEnd = 12; - - currentYear = this._yearPicker.timestamp && CalendarDate.fromTimestamp(this._yearPicker.timestamp * 1000).getYear(); - if (this.minDate - && (currentYear - minDateParsed.getFullYear()) < cellsFromTheStart) { - this._header._isPrevButtonDisabled = true; - } else { - this._header._isPrevButtonDisabled = false; - } - - if (this.maxDate - && (maxDateParsed.getFullYear() - currentYear) < cellsToTheEnd) { - this._header._isNextButtonDisabled = true; - } else { - this._header._isNextButtonDisabled = false; - } - } - } - - get dayPicker() { - return this.shadowRoot.querySelector("ui5-daypicker"); - } - - get monthPicker() { - return this.shadowRoot.querySelector("ui5-monthpicker"); - } - - get yearPicker() { - return this.shadowRoot.querySelector("ui5-yearpicker"); - } - - get header() { - return this.shadowRoot.querySelector("ui5-calendar-header"); + /** + * The user clicked the "month" button in the header + */ + onHeaderShowMonthPress() { + this._currentPicker = "month"; } - get monthButton() { - return this.header.shadowRoot.querySelector("[data-sap-show-picker='Month']"); + /** + * The user clicked the "year" button in the header + */ + onHeaderShowYearPress() { + this._currentPicker = "year"; } - get yearButton() { - return this.header.shadowRoot.querySelector("[data-sap-show-picker='Year']"); + get _currentPickerDOM() { + return this.shadowRoot.querySelector(`[ui5-${this._currentPicker}picker]`); } - _onkeydown(event) { - if (isF4(event) && this._monthPicker._hidden) { - this._showMonthPicker(); - if (!this._yearPicker._hidden) { - this._hideYearPicker(); - } - } - - if (isF4Shift(event) && this._yearPicker._hidden) { - this._showYearPicker(); - if (!this._monthPicker._hidden) { - this._hideMonthPicker(); - } - } - - if (isTabNext(event)) { - this._handleTabNext(event); - } - - if (isTabPrevious(event)) { - this._handleTabPrevous(event); - } + /** + * The year clicked the "Previous" button in the header + */ + onHeaderPreviousPress() { + this._currentPickerDOM._showPreviousPage(); } - _handleTabNext(event) { - const target = event.target; - - if (target.tagName === "UI5-DAYPICKER" || target.tagName === "UI5-MONTHPICKER" || target.tagName === "UI5-YEARPICKER") { - if (this.monthButton.getAttribute("hidden") === null) { - this.monthButton.focus(); - } else { - this.yearButton.focus(); - } - event.preventDefault(); - } else if (target.tagName === "UI5-CALENDAR-HEADER" && event.path[0].getAttribute("data-sap-show-picker") === "Month") { - this.yearButton.focus(); - event.preventDefault(); - } else { - this._setPickerCurrentTabindex(-1); - } + /** + * The year clicked the "Next" button in the header + */ + onHeaderNextPress() { + this._currentPickerDOM._showNextPage(); } - _handleTabPrevous(event) { - const target = event.target; - - if (target.tagName === "UI5-CALENDAR-HEADER" && event.path[0].getAttribute("data-sap-show-picker") === "Month") { - this._moveFocusToPickerContent(); - event.preventDefault(); - } else if (target.tagName === "UI5-CALENDAR-HEADER" && event.path[0].getAttribute("data-sap-show-picker") === "Year") { - if (this.monthButton.getAttribute("hidden") === null) { - this.monthButton.focus(); - } else { - this._moveFocusToPickerContent(); - } - event.preventDefault(); - } + /** + * The month button is only hidden when the month picker is shown + * @returns {boolean} + * @private + */ + get _isHeaderMonthButtonHidden() { + return this._currentPicker === "month"; } - _moveFocusToPickerContent() { - if (!this._oMonth._hidden) { - this.dayPicker._itemNav.focusCurrent(); - } else if (!this._monthPicker._hidden) { - this.monthPicker._itemNav.focusCurrent(); - } else { - this.yearPicker._itemNav.focusCurrent(); - } + get _isDayPickerHidden() { + return this._currentPicker !== "day"; } - _onfocusout(event) { - this._header.tabIndex = "-1"; - this._setPickerCurrentTabindex(0); + get _isMonthPickerHidden() { + return this._currentPicker !== "month"; } - _setPickerCurrentTabindex(index) { - if (this.dayPicker) { - this.dayPicker._setCurrentItemTabIndex(index); - } - - if (this.monthPicker) { - this.monthPicker._setCurrentItemTabIndex(index); - } - - if (this.yearPicker) { - this.yearPicker._setCurrentItemTabIndex(index); - } + get _isYearPickerHidden() { + return this._currentPicker !== "year"; } - _handleSelectedDatesChange(event) { + onSelectedDatesChange(event) { + const timestamp = event.detail.timestamp; const selectedDates = event.detail.dates; - // Deselecting a date in multiple selection type - if (this.selection === CalendarSelection.Multiple && this.selectedDates.length > selectedDates.length) { - const deselectedDates = this.selectedDates.filter(timestamp => !selectedDates.includes(timestamp)); - this.timestamp = deselectedDates[0]; - } else { - this.timestamp = selectedDates[selectedDates.length - 1]; - } - - this.selectedDates = [...selectedDates]; - this.fireEvent("selected-dates-change", { dates: selectedDates }); + this.timestamp = timestamp; + this.selectedDates = selectedDates; + this.fireEvent("selected-dates-change", { timestamp, dates: [...selectedDates] }); } - _handleMonthNavigate(event) { + onSelectedMonthChange(event) { this.timestamp = event.detail.timestamp; + this._currentPicker = "day"; } - _handleYearNavigate(event) { - if (event.detail.start) { - this._handlePrevious(); - } - - if (event.detail.end) { - this._handleNext(); - } - } - - _focusFirstDayOfMonth(targetDate) { - let fistDayOfMonthIndex = -1; - - // focus first day of the month - this.dayPicker._getVisibleDays(targetDate).forEach((date, index) => { - if (date.getDate() === 1 && (fistDayOfMonthIndex === -1)) { - fistDayOfMonthIndex = index; - } - }); - - this.dayPicker._itemNav.currentIndex = fistDayOfMonthIndex; - this.dayPicker._itemNav.focusCurrent(); - } - - _handleSelectedMonthChange(event) { - const oNewDate = this._calendarDate; - const oFocusedDate = CalendarDate.fromTimestamp(event.detail.timestamp * 1000, this._primaryCalendarType); - - oNewDate.setMonth(oFocusedDate.getMonth()); - this.timestamp = oNewDate.valueOf() / 1000; - this._monthPicker.timestamp = this.timestamp; - - this._hideMonthPicker(); - this._setDayPickerCurrentIndex(oNewDate, true); - } - - _handleSelectedYearChange(event) { - const oNewDate = this._calendarDate; - const oFocusedDate = CalendarDate.fromTimestamp(event.detail.timestamp * 1000, this._primaryCalendarType); - - oNewDate.setYear(oFocusedDate.getYear()); - this.timestamp = oNewDate.valueOf() / 1000; - this._yearPicker.timestamp = this.timestamp; - - this._hideYearPicker(); - this._setDayPickerCurrentIndex(oNewDate, true); - } - - async _setDayPickerCurrentIndex(calDate, applyFocus) { - await RenderScheduler.whenFinished(); - const currentDate = new CalendarDate(calDate, this._primaryCalendarType); - const currentIndex = this.dayPicker.focusableDays.findIndex(item => { - return CalendarDate.fromLocalJSDate(new Date(item.timestamp * 1000), this._primaryCalendarType).isSame(currentDate); - }); - this.dayPicker._itemNav.currentIndex = currentIndex; - if (applyFocus) { - this.dayPicker._itemNav.focusCurrent(); - } else { - this.dayPicker._itemNav.update(); - } - } - - _handleMonthButtonPress() { - this._hideYearPicker(); - this._header._isMonthButtonHidden = true; - - this[`_${this._monthPicker._hidden ? "show" : "hide"}MonthPicker`](); - } - - _handleYearButtonPress() { - this._hideMonthPicker(); - this[`_${this._yearPicker._hidden ? "show" : "hide"}YearPicker`](); - } - - _handlePrevious() { - if (this._monthPicker._hidden && this._yearPicker._hidden) { - this._showPrevMonth(); - } else if (this._monthPicker._hidden && !this._yearPicker._hidden) { - this._showPrevPageYears(); - } else if (!this._monthPicker._hidden && this._yearPicker._hidden) { - this._showPrevYear(); - } - } - - _handleNext() { - if (this._monthPicker._hidden && this._yearPicker._hidden) { - this._showNextMonth(); - } else if (this._monthPicker._hidden && !this._yearPicker._hidden) { - this._showNextPageYears(); - } else if (!this._monthPicker._hidden && this._yearPicker._hidden) { - this._showNextYear(); - } - } - - _showNextMonth() { - const nextMonth = this._calendarDate; - const maxCalendarDateYear = CalendarDate.fromTimestamp(this._getMaxCalendarDate(), this._primaryCalendarType).getYear(); - nextMonth.setDate(1); - nextMonth.setMonth(nextMonth.getMonth() + 1); - - if (nextMonth.getYear() > maxCalendarDateYear) { - return; - } - - if (!this.isInValidRange(nextMonth.toLocalJSDate().valueOf())) { - return; - } - - this._focusFirstDayOfMonth(nextMonth); - this.timestamp = nextMonth.valueOf() / 1000; - } - - _showPrevMonth() { - let iNewMonth = this._month - 1, - iNewYear = this._calendarDate.getYear(); - - const minCalendarDateYear = CalendarDate.fromTimestamp(this._getMinCalendarDate(), this._primaryCalendarType).getYear(); - - // focus first day of the month - const currentMonthDate = this.dayPicker._calendarDate.setMonth(this.dayPicker._calendarDate.getMonth()); - const lastMonthDate = this.dayPicker._calendarDate.setMonth(this.dayPicker._calendarDate.getMonth() - 1); - - // set the date to last day of last month - currentMonthDate.setDate(-1); - - // find the index of the last day - let lastDayOfMonthIndex = -1; - - if (!this.isInValidRange(currentMonthDate.toLocalJSDate().valueOf())) { - return; - } - - this.dayPicker._getVisibleDays(lastMonthDate).forEach((date, index) => { - const isSameDate = currentMonthDate.getDate() === date.getDate(); - const isSameMonth = currentMonthDate.getMonth() === date.getMonth(); - - if (isSameDate && isSameMonth) { - lastDayOfMonthIndex = (index + 1); - } - }); - - if (lastDayOfMonthIndex !== -1) { - // find the DOM for the last day index - const lastDay = this.dayPicker.shadowRoot.querySelectorAll(".ui5-dp-content .ui5-dp-item")[lastDayOfMonthIndex]; - - // update current item in ItemNavigation - this.dayPicker._itemNav.current = lastDayOfMonthIndex; - - // focus the item - lastDay.focus(); - } - - if (iNewMonth > 11) { - iNewMonth = 0; - iNewYear = this._calendarDate.getYear() + 1; - } - - if (iNewMonth < 0) { - iNewMonth = 11; - iNewYear = this._calendarDate.getYear() - 1; - } - - const oNewDate = this._calendarDate; - oNewDate.setYear(iNewYear); - oNewDate.setMonth(iNewMonth); - - - if (oNewDate.getYear() < minCalendarDateYear) { - return; - } - this.timestamp = oNewDate.valueOf() / 1000; - } - - _showNextYear() { - const maxCalendarDateYear = CalendarDate.fromTimestamp(this._getMaxCalendarDate(), this._primaryCalendarType).getYear(); - if (this._calendarDate.getYear() === maxCalendarDateYear) { - return; - } - - const newDate = this._calendarDate; - newDate.setYear(this._calendarDate.getYear() + 1); - - this.timestamp = newDate.valueOf() / 1000; - } - - _showPrevYear() { - const minCalendarDateYear = CalendarDate.fromTimestamp(this._getMinCalendarDate(), this._primaryCalendarType).getYear(); - if (this._calendarDate.getYear() === minCalendarDateYear) { - return; - } - - const oNewDate = this._calendarDate; - oNewDate.setYear(this._calendarDate.getYear() - 1); - - this.timestamp = oNewDate.valueOf() / 1000; - } - - _showNextPageYears() { - if (!this._isYearInRange(this._yearPicker.timestamp, - YearPicker._ITEMS_COUNT - YearPicker._MIDDLE_ITEM_INDEX, - CalendarDate.fromTimestamp(this._minDate, this._primaryCalendarType).getYear(), - CalendarDate.fromTimestamp(this._maxDate, this._primaryCalendarType).getYear())) { - return; - } - - const newDate = CalendarDate.fromTimestamp(this._yearPicker.timestamp * 1000, this._primaryCalendarType); - newDate.setYear(newDate.getYear() + YearPicker._ITEMS_COUNT); - - this._yearPicker = Object.assign({}, this._yearPicker, { - timestamp: newDate.valueOf() / 1000, - }); - - this.timestamp = this._yearPicker.timestamp; - - this._isShiftingYears = true; - } - - _showPrevPageYears() { - if (!this._isYearInRange(this._yearPicker.timestamp, - -YearPicker._MIDDLE_ITEM_INDEX - 1, - CalendarDate.fromTimestamp(this._minDate, this._primaryCalendarType).getYear(), - CalendarDate.fromTimestamp(this._maxDate, this._primaryCalendarType).getYear())) { - return; - } - - const newDate = CalendarDate.fromTimestamp(this._yearPicker.timestamp * 1000, this._primaryCalendarType); - newDate.setYear(newDate.getYear() - YearPicker._ITEMS_COUNT); - - this._yearPicker = Object.assign({}, this._yearPicker, { - timestamp: newDate.valueOf() / 1000, - }); - - this.timestamp = this._yearPicker.timestamp; - - this._isShiftingYears = true; - } - - _showMonthPicker() { - this._monthPicker = Object.assign({}, this._monthPicker); - this._oMonth = Object.assign({}, this._oMonth); - - this._monthPicker.timestamp = this._timestamp; - this._monthPicker._hidden = false; - this._oMonth._hidden = true; - - const calendarRect = this.shadowRoot.querySelector(".ui5-cal-root").getBoundingClientRect(); - - this._calendarWidth = calendarRect.width.toString(); - this._calendarHeight = calendarRect.height.toString(); - - const monthPicker = this.shadowRoot.querySelector("[ui5-monthpicker]"); - monthPicker.selectedDates = [...this.selectedDates]; - const currentMonthIndex = monthPicker._itemNav._getItems().findIndex(item => { - const calDate = CalendarDate.fromTimestamp(parseInt(item.timestamp) * 1000, this._primaryCalendarType); - return calDate.getMonth() === this._calendarDate.getMonth(); - }); - monthPicker._itemNav.currentIndex = currentMonthIndex; - this._header._isMonthButtonHidden = true; - } - - _showYearPicker() { - this._yearPicker = Object.assign({}, this._yearPicker); - this._oMonth = Object.assign({}, this._oMonth); - - this._yearPicker.timestamp = this._timestamp; - this._yearPicker._selectedYear = this._calendarDate.getYear(); - this._yearPicker._hidden = false; - this._oMonth._hidden = true; - - const calendarRect = this.shadowRoot.querySelector(".ui5-cal-root").getBoundingClientRect(); - - this._calendarWidth = calendarRect.width.toString(); - this._calendarHeight = calendarRect.height.toString(); - - const yearPicker = this.shadowRoot.querySelector("[ui5-yearpicker]"); - yearPicker.selectedDates = [...this.selectedDates]; - const currentYearIndex = yearPicker._itemNav._getItems().findIndex(item => { - const calDate = CalendarDate.fromTimestamp(parseInt(item.timestamp) * 1000, this._primaryCalendarType); - return calDate.getYear() === this._calendarDate.getYear(); - }); - yearPicker._itemNav.currentIndex = currentYearIndex; + onSelectedYearChange(event) { + this.timestamp = event.detail.timestamp; + this._currentPicker = "day"; } - _hideMonthPicker() { - this._monthPicker = Object.assign({}, this._monthPicker); - this._oMonth = Object.assign({}, this._oMonth); - - if (this._yearPicker._hidden) { - this._oMonth._hidden = false; - } - this._monthPicker._hidden = true; - this._header._isMonthButtonHidden = false; + onNavigate(event) { + this.timestamp = event.detail.timestamp; } - _hideYearPicker() { - this._yearPicker = Object.assign({}, this._yearPicker); - this._oMonth = Object.assign({}, this._oMonth); - - if (this._monthPicker._hidden) { - this._oMonth._hidden = false; + _onkeydown(event) { + if (isF4(event) && this._currentPicker === "day") { + this._currentPicker = "month"; } - this._yearPicker._hidden = true; - } - _isYearInRange(timestamp, yearsoffset, minYear, maxYear) { - if (timestamp) { - const oCalDate = CalendarDate.fromTimestamp(timestamp * 1000, this._primaryCalendarType); - oCalDate.setMonth(0); - oCalDate.setDate(1); - oCalDate.setYear(oCalDate.getYear() + yearsoffset); - return oCalDate.getYear() >= minYear && oCalDate.getYear() <= maxYear; + if (isF4Shift(event) && this._currentPicker === "day") { + this._currentPicker = "year"; } } - get classes() { - return { - main: { - "ui5-cal-root": true, - }, - dayPicker: { - ".ui5-daypicker--hidden": !this._yearPicker._hidden || !this._monthPicker._hidden, - }, - yearPicker: { - "ui5-yearpicker--hidden": this._yearPicker._hidden, - }, - monthPicker: { - "ui5-monthpicker--hidden": this._monthPicker._hidden, - }, - }; - } - - /** - * 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; - } - - get styles() { - return { - main: { - "height": `${this._calendarHeight ? `${this._calendarHeight}px` : "auto"}`, - "width": `${this._calendarWidth ? `${this._calendarWidth}px` : "auto"}`, - }, - }; - } - static get dependencies() { return [ CalendarHeader, diff --git a/packages/main/src/CalendarHeader.hbs b/packages/main/src/CalendarHeader.hbs index aa23e27e30b0..7bb11fa98014 100644 --- a/packages/main/src/CalendarHeader.hbs +++ b/packages/main/src/CalendarHeader.hbs @@ -1,61 +1,45 @@
    - - +
    -
    +
    - {{_btn1.text}} + {{_monthButtonText}}
    - {{_btn2.text}} + {{_yearButtonText}}
    - - +
    diff --git a/packages/main/src/CalendarHeader.js b/packages/main/src/CalendarHeader.js index 4244902f1e21..d90d3a9153de 100644 --- a/packages/main/src/CalendarHeader.js +++ b/packages/main/src/CalendarHeader.js @@ -1,12 +1,16 @@ +import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; +import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; +import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; +import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js"; import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; +import CalendarType from "@ui5/webcomponents-base/dist/types/CalendarType.js"; import "@ui5/webcomponents-icons/dist/slim-arrow-left.js"; import "@ui5/webcomponents-icons/dist/slim-arrow-right.js"; -import Button from "./Button.js"; import Icon from "./Icon.js"; -import ButtonDesign from "./types/ButtonDesign.js"; import CalendarHeaderTemplate from "./generated/templates/CalendarHeaderTemplate.lit.js"; import { CALENDAR_HEADER_NEXT_BUTTON, @@ -19,37 +23,35 @@ import styles from "./generated/themes/CalendarHeader.css.js"; const metadata = { tag: "ui5-calendar-header", properties: { - monthText: { - type: String, + /** + * Already normalized by Calendar + * @type {Integer} + * @public + */ + timestamp: { + type: Integer, }, - yearText: { - type: String, - }, - _btnPrev: { - type: Object, - }, - _btnNext: { - type: Object, - }, - _btn1: { - type: Object, - }, - _btn2: { - type: Object, + + /** + * Already normalized by Calendar + * @type {CalendarType} + * @public + */ + primaryCalendarType: { + type: CalendarType, }, - _isNextButtonDisabled: { + + isNextButtonDisabled: { type: Boolean, }, - _isPrevButtonDisabled: { + + isPrevButtonDisabled: { type: Boolean, }, - _isMonthButtonHidden: { + + isMonthButtonHidden: { type: Boolean, }, - _tabIndex: { - type: String, - defaultValue: "0", - }, }, events: { "previous-press": {}, @@ -77,83 +79,71 @@ class CalendarHeader extends UI5Element { } static get dependencies() { - return [Button, Icon]; + return [Icon]; + } + + static async onDefine() { + await fetchI18nBundle("@ui5/webcomponents"); } constructor() { super(); - this._btnPrev = {}; - this._btnPrev.icon = "slim-arrow-left"; - - this._btnNext = {}; - this._btnNext.icon = "slim-arrow-right"; - - this._btn1 = {}; - this._btn1.type = ButtonDesign.Transparent; - - this._btn2 = {}; - this._btn2.type = ButtonDesign.Transparent; - this.i18nBundle = getI18nBundle("@ui5/webcomponents"); } onBeforeRendering() { - this._btn1.text = this.monthText; - this._btn2.text = this.yearText; - this._btnPrev.classes = "ui5-calheader-arrowbtn"; - this._btnNext.classes = "ui5-calheader-arrowbtn"; + const localeData = getCachedLocaleDataInstance(getLocale()); + const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this.primaryCalendarType }); + const localDate = new Date(this.timestamp * 1000); + const calendarDate = CalendarDate.fromTimestamp(localDate.getTime(), this.primaryCalendarType); - if (this._isNextButtonDisabled) { - this._btnNext.classes += " ui5-calheader-arrowbtn-disabled"; - } - - if (this._isPrevButtonDisabled) { - this._btnPrev.classes += " ui5-calheader-arrowbtn-disabled"; - } + this._monthButtonText = localeData.getMonths("wide", this.primaryCalendarType)[calendarDate.getMonth()]; + this._yearButtonText = yearFormat.format(localDate, true); + this._prevButtonText = this.i18nBundle.getText(CALENDAR_HEADER_PREVIOUS_BUTTON); + this._nextButtonText = this.i18nBundle.getText(CALENDAR_HEADER_NEXT_BUTTON); } - _handlePrevPress(event) { + onPrevButtonClick(event) { this.fireEvent("previous-press", event); } - _handleNextPress(event) { + onNextButtonClick(event) { this.fireEvent("next-press", event); } - _showMonthPicker(event) { + onMonthButtonClick(event) { this.fireEvent("show-month-press", event); } - _showYearPicker(event) { - this.fireEvent("show-year-press", event); - } - - _onkeydown(event) { + onMonthButtonKeyDown(event) { if (isSpace(event) || isEnter(event)) { - const showPickerButton = event.target.getAttribute("data-sap-show-picker"); - - if (showPickerButton) { - this[`_show${showPickerButton}Picker`](); - } - } - } - - _onMidContainerKeyDown(event) { - if (isSpace(event)) { event.preventDefault(); + this.fireEvent("show-month-press", event); } } - static async onDefine() { - await fetchI18nBundle("@ui5/webcomponents"); + onYearButtonClick(event) { + this.fireEvent("show-year-press", event); } - get _prevButtonText() { - return this.i18nBundle.getText(CALENDAR_HEADER_PREVIOUS_BUTTON); + onYearButtonKeyDown(event) { + if (isSpace(event) || isEnter(event)) { + event.preventDefault(); + this.fireEvent("show-year-press", event); + } } - get _nextButtonText() { - return this.i18nBundle.getText(CALENDAR_HEADER_NEXT_BUTTON); + get classes() { + return { + prevButton: { + "ui5-calheader-arrowbtn": true, + "ui5-calheader-arrowbtn-disabled": this._isPrevButtonDisabled, + }, + nextButton: { + "ui5-calheader-arrowbtn": true, + "ui5-calheader-arrowbtn-disabled": this._isNextButtonDisabled, + }, + }; } } diff --git a/packages/main/src/CalendarPart.js b/packages/main/src/CalendarPart.js new file mode 100644 index 000000000000..8ce2d2673485 --- /dev/null +++ b/packages/main/src/CalendarPart.js @@ -0,0 +1,122 @@ +import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; +import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; +import modifyDateBy from "@ui5/webcomponents-localization/dist/dates/modifyDateBy.js"; +import getRoundedTimestamp from "@ui5/webcomponents-localization/dist/dates/getRoundedTimestamp.js"; +import DateComponentBase from "./DateComponentBase.js"; + +/** + * @public + */ +const metadata = { + properties: /** @lends sap.ui.webcomponents.main.CalendarPart.prototype */ { + /** + * The timestamp of the currently focused date. Set this property to move the component's focus to a certain date. + * Node: Timestamp is 10-digit Integer representing the seconds (not milliseconds) since the Unix Epoch. + * @type {Integer} + * @public + */ + timestamp: { + type: Integer, + }, + + /** + * An array of UTC timestamps representing the selected date or dates depending on the capabilities of the picker component. + * @type {Array} + * @public + */ + selectedDates: { + type: Integer, + multiple: true, + compareValues: true, + }, + }, +}; + +/** + * @class + * + * Abstract base class for Calendar, DayPicker, MonthPicker and YearPicker that adds support for: + * - common properties (timestamp, selectedDates): declarations and methods that operate on them + * - other common code + * + * @constructor + * @author SAP SE + * @alias sap.ui.webcomponents.main.CalendarPart + * @extends DateComponentBase + * @public + */ +class CalendarPart extends DateComponentBase { + static get metadata() { + return metadata; + } + + get _minTimestamp() { + return this._minDate.valueOf() / 1000; + } + + get _maxTimestamp() { + return this._maxDate.valueOf() / 1000; + } + + /** + * Returns the effective timestamp to be used by the respective calendar part + * @protected + */ + get _timestamp() { + let timestamp = this.timestamp !== undefined ? this.timestamp : getRoundedTimestamp(); + if (timestamp < this._minTimestamp || timestamp > this._maxTimestamp) { + timestamp = this._minTimestamp; + } + return timestamp; + } + + get _localDate() { + return new Date(this._timestamp * 1000); + } + + /** + * Returns a CalendarDate instance, representing the _timestamp getter - this date is central to all components' rendering logic + * @protected + */ + get _calendarDate() { + return CalendarDate.fromTimestamp(this._localDate.getTime(), this._primaryCalendarType); + } + + /** + * Change a timestamp and enforce limits + * + * @param timestamp + * @protected + */ + _safelySetTimestamp(timestamp) { + const min = this._minDate.valueOf() / 1000; + const max = this._maxDate.valueOf() / 1000; + + if (timestamp < min) { + timestamp = min; + } + if (timestamp > max) { + timestamp = max; + } + + this.timestamp = timestamp; + } + + /** + * Modify a timestamp by a certain amount of days/months/years and enforce limits + * @param amount + * @param unit + * @protected + */ + _safelyModifyTimestampBy(amount, unit) { + const newDate = modifyDateBy(this._calendarDate, amount, unit); + this._safelySetTimestamp(newDate.valueOf() / 1000); + } + + _getTimestampFromDom(domNode) { + const oMonthDomRef = domNode.getAttribute("data-sap-timestamp"); + return parseInt(oMonthDomRef); + } +} + +export default CalendarPart; diff --git a/packages/main/src/PickerBase.js b/packages/main/src/DateComponentBase.js similarity index 63% rename from packages/main/src/PickerBase.js rename to packages/main/src/DateComponentBase.js index 000efe807b2e..988a67b4d64e 100644 --- a/packages/main/src/PickerBase.js +++ b/packages/main/src/DateComponentBase.js @@ -5,26 +5,17 @@ import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18 import { getCalendarType } from "@ui5/webcomponents-base/dist/config/CalendarType.js"; import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; -import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; import CalendarType from "@ui5/webcomponents-base/dist/types/CalendarType.js"; import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; -import { getMinCalendarDate, getMaxCalendarDate } from "./util/DateTime.js"; +import { getMaxCalendarDate, getMinCalendarDate } from "@ui5/webcomponents-localization/dist/dates/ExtremeDates.js"; /** * @public */ const metadata = { - properties: /** @lends sap.ui.webcomponents.main.MonthPicker.prototype */ { - /** - * A UNIX timestamp - seconds since 00:00:00 UTC on Jan 1, 1970. - * @type {Integer} - * @public - */ - timestamp: { - type: Integer, - }, - + languageAware: true, + properties: /** @lends sap.ui.webcomponents.main.DateComponentBase.prototype */ { /** * Sets a calendar type used for display. * If not set, the calendar type of the global configuration is used. @@ -69,34 +60,25 @@ const metadata = { formatPattern: { type: String, }, - - /** - * Defines the selected dates as UTC timestamps. - * @type {Array} - * @public - */ - selectedDates: { - type: Integer, - multiple: true, - }, }, }; /** - * Base picker component. - * * @class * - * Abstract class for Calendar, DayPicker, MonthPicker and YearPicker + * Abstract class that provides common functionality for date-related components (day picker, month picker, year picker, calendar, date picker, date range picker, date time picker) + * This includes: + * - "languageAware: true" metadata setting, CLDR fetch and i18n initialization + * - common properties (primaryCalendar, minDate, maxDate and formatPattern) declaration and methods that operate on them + * - additional common methods * * @constructor * @author SAP SE - * @alias sap.ui.webcomponents.main.PickerBase + * @alias sap.ui.webcomponents.main.DateComponentBase * @extends sap.ui.webcomponents.base.UI5Element - * @tagname ui5-monthpicker * @public */ -class PickerBase extends UI5Element { +class DateComponentBase extends UI5Element { static get metadata() { return metadata; } @@ -107,62 +89,47 @@ class PickerBase extends UI5Element { constructor() { super(); - this.i18nBundle = getI18nBundle("@ui5/webcomponents"); } - get _timestamp() { - return this.timestamp !== undefined ? this.timestamp : Math.floor(new Date().getTime() / 1000); - } - - get _localDate() { - return new Date(this._timestamp * 1000); - } - - get _calendarDate() { - return CalendarDate.fromTimestamp(this._localDate.getTime(), this._primaryCalendarType); - } - - get _month() { - return this._calendarDate.getMonth(); - } - - get _year() { - return this._calendarDate.getYear(); - } - get _primaryCalendarType() { const localeData = getCachedLocaleDataInstance(getLocale()); return this.primaryCalendarType || getCalendarType() || localeData.getPreferredCalendarType(); } - get _isPattern() { - return this._formatPattern !== "medium" && this._formatPattern !== "short" && this._formatPattern !== "long"; + get _minDate() { + return this.minDate && this.getFormat().parse(this.minDate) ? this._getCalendarDateFromString(this.minDate) : getMinCalendarDate(this._primaryCalendarType); } get _maxDate() { - return this.maxDate ? this._getTimeStampFromString(this.maxDate) : this._getMaxCalendarDate(); + return this.maxDate && this.getFormat().parse(this.maxDate) ? this._getCalendarDateFromString(this.maxDate) : getMaxCalendarDate(this._primaryCalendarType); } - get _minDate() { - return this.minDate ? this._getTimeStampFromString(this.minDate) : this._getMinCalendarDate(); + get _formatPattern() { + return this.formatPattern || "medium"; // get from config } - _getTimeStampFromString(value) { + get _isPattern() { + return this._formatPattern !== "medium" && this._formatPattern !== "short" && this._formatPattern !== "long"; + } + + _getCalendarDateFromString(value) { const jsDate = this.getFormat().parse(value); if (jsDate) { - const calDate = CalendarDate.fromLocalJSDate(jsDate, this._primaryCalendarType); - return calDate.toUTCJSDate().valueOf(); + return CalendarDate.fromLocalJSDate(jsDate, this._primaryCalendarType); } - return undefined; } - _getMinCalendarDate() { - return getMinCalendarDate(this._primaryCalendarType); + _getTimeStampFromString(value) { + const calDate = this._getCalendarDateFromString(value); + if (calDate) { + return calDate.toUTCJSDate().valueOf(); + } } - _getMaxCalendarDate() { - return getMaxCalendarDate(this._primaryCalendarType); + _getStringFromTimestamp(timestamp) { + const localDate = new Date(timestamp); + return this.getFormat().format(localDate, true); } getFormat() { @@ -181,15 +148,6 @@ class PickerBase extends UI5Element { return dateFormat; } - get _formatPattern() { - return this.formatPattern || "medium"; // get from config - } - - getTimestampFromDom(domNode) { - const oMonthDomRef = domNode.getAttribute("data-sap-timestamp"); - return parseInt(oMonthDomRef); - } - static async onDefine() { await Promise.all([ fetchCldr(getLocale().getLanguage(), getLocale().getRegion(), getLocale().getScript()), @@ -198,4 +156,4 @@ class PickerBase extends UI5Element { } } -export default PickerBase; +export default DateComponentBase; diff --git a/packages/main/src/DatePicker.hbs b/packages/main/src/DatePicker.hbs index 832a9384a0a3..2c4b0015b70a 100644 --- a/packages/main/src/DatePicker.hbs +++ b/packages/main/src/DatePicker.hbs @@ -1,8 +1,6 @@
    {{#if valueStateMessage.length}} diff --git a/packages/main/src/DatePicker.js b/packages/main/src/DatePicker.js index b3b8c39c165e..fc6fcb9d371e 100644 --- a/packages/main/src/DatePicker.js +++ b/packages/main/src/DatePicker.js @@ -1,17 +1,10 @@ -import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; -import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; -import { fetchCldr } from "@ui5/webcomponents-base/dist/asset-registries/LocaleData.js"; -import { getCalendarType } from "@ui5/webcomponents-base/dist/config/CalendarType.js"; -import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; import { getFeature } from "@ui5/webcomponents-base/dist/FeaturesRegistry.js"; -import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; -import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; -import CalendarType from "@ui5/webcomponents-base/dist/types/CalendarType.js"; import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; +import modifyDateBy from "@ui5/webcomponents-localization/dist/dates/modifyDateBy.js"; +import getRoundedTimestamp from "@ui5/webcomponents-localization/dist/dates/getRoundedTimestamp.js"; import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AriaLabelHelper.js"; import { - isEnter, isPageUp, isPageDown, isPageUpShift, @@ -22,13 +15,10 @@ import { isF4, } from "@ui5/webcomponents-base/dist/Keys.js"; import { isPhone, isIE } from "@ui5/webcomponents-base/dist/Device.js"; -import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; import "@ui5/webcomponents-icons/dist/appointment-2.js"; import "@ui5/webcomponents-icons/dist/decline.js"; -import CalendarSelection from "@ui5/webcomponents-base/dist/types/CalendarSelection.js"; -import RenderScheduler from "@ui5/webcomponents-base/dist/RenderScheduler.js"; import { DATEPICKER_OPEN_ICON_TITLE, DATEPICKER_DATE_ACC_TEXT, INPUT_SUGGESTIONS_TITLE } from "./generated/i18n/i18n-defaults.js"; -import { getMaxCalendarDate, getMinCalendarDate } from "./util/DateTime.js"; +import DateComponentBase from "./DateComponentBase.js"; import Icon from "./Icon.js"; import Button from "./Button.js"; import ResponsivePopover from "./ResponsivePopover.js"; @@ -52,7 +42,6 @@ import ResponsivePopoverCommonCss from "./generated/themes/ResponsivePopoverComm const metadata = { tag: "ui5-date-picker", altTag: "ui5-datepicker", - languageAware: true, managedSlots: true, properties: /** @lends sap.ui.webcomponents.main.DatePicker.prototype */ { /** @@ -87,63 +76,6 @@ const metadata = { defaultValue: ValueState.None, }, - /** - * Determines the format, displayed in the input field. - * - * @type {string} - * @defaultvalue "" - * @public - */ - formatPattern: { - type: String, - }, - - /** - * Determines the minimum 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, - }, - - /** - * Determines the calendar type. - * The input value is formated according to the calendar type - * and the picker shows the months and years from the specified calendar. - *

    - * Available options are: - *
      - *
    • Gregorian
    • - *
    • Islamic
    • - *
    • Japanese
    • - *
    • Buddhist
    • - *
    • Persian
    • - *
    - * - * @type {CalendarType} - * @defaultvalue "Gregorian" - * @public - */ - primaryCalendarType: { - type: CalendarType, - }, - /** * Defines whether the ui5-date-picker is required. * @@ -265,8 +197,9 @@ const metadata = { type: Object, }, - _calendar: { - type: Object, + _calendarCurrentPicker: { + type: String, + defaultValue: "day", }, }, @@ -370,19 +303,15 @@ const metadata = { * @constructor * @author SAP SE * @alias sap.ui.webcomponents.main.DatePicker - * @extends sap.ui.webcomponents.base.UI5Element + * @extends DateComponentBase * @tagname ui5-date-picker * @public */ -class DatePicker extends UI5Element { +class DatePicker extends DateComponentBase { static get metadata() { return metadata; } - static get render() { - return litRender; - } - static get template() { return DatePickerTemplate; } @@ -399,97 +328,25 @@ class DatePicker extends UI5Element { return [ResponsivePopoverCommonCss, datePickerPopoverCss]; } - constructor() { - super(); - - this._respPopoverConfig = { - allowTargetOverlap: true, - stayOpenOnScroll: true, - afterClose: () => { - this._isPickerOpen = false; - - if (isPhone()) { - // close device's keyboard and prevent further typing - this.blur(); - } else if (this._focusInputAfterClose) { - this._getInput().focus(); - this._focusInputAfterClose = false; - } - - const calendar = this.calendar; - if (calendar) { - calendar._hideMonthPicker(); - calendar._hideYearPicker(); - } - }, - afterOpen: async () => { - await RenderScheduler.whenFinished(); - const calendar = this.calendar; - - if (!calendar) { - return; - } - - const dayPicker = calendar.shadowRoot.querySelector(`#${calendar._id}-daypicker`); - const selectedDay = dayPicker.shadowRoot.querySelector(".ui5-dp-item--selected"); - const today = dayPicker.shadowRoot.querySelector(".ui5-dp-item--now"); - let focusableDay = selectedDay || today; - if (!selectedDay && (this.minDate || this.maxDate) && !this.isInValidRange((new Date().getTime()))) { - focusableDay = this.findFirstFocusableDay(dayPicker); - } - - if (this._focusInputAfterOpen) { - this._focusInputAfterOpen = false; - this._getInput().focus(); - } else if (focusableDay) { - focusableDay.focus(); - - let focusableDayIdx = parseInt(focusableDay.getAttribute("data-sap-index")); - const focusableItem = dayPicker.focusableDays.find(item => parseInt(item._index) === focusableDayIdx); - focusableDayIdx = focusableItem ? dayPicker.focusableDays.indexOf(focusableItem) : focusableDayIdx; - - dayPicker._itemNav.current = focusableDayIdx; - dayPicker._itemNav.update(); - } - }, - }; - - this._calendar = { - onSelectedDatesChange: this._handleCalendarChange.bind(this), - selection: CalendarSelection.Single, - selectedDates: [], - }; - - 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]; + /** + * @protected + */ + onResponsivePopoverAfterClose() { + this._isPickerOpen = false; + if (isPhone()) { + this.blur(); // close device's keyboard and prevent further typing + } else if (this._focusInputAfterClose) { + this._getInput().focus(); + this._focusInputAfterClose = false; } } onBeforeRendering() { - this._calendar.primaryCalendarType = this._primaryCalendarType; - this._calendar.formatPattern = this._formatPattern; - - 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._checkValueValidity(this.value)) { - this._changeCalendarSelection(); - } else if (this.value !== "") { - this._calendar.selectedDates = []; - } + ["minDate", "maxDate"].forEach(prop => { + if (this[prop] && !this.isValid(this[prop])) { + console.warn(`Invalid value for property "${prop}": ${this[prop]} is not compatible with the configured format pattern: "${this._displayFormat}"`); // eslint-disable-line + } + }); const FormSupport = getFeature("FormSupport"); if (FormSupport) { @@ -497,33 +354,56 @@ 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; - } + /** + * Override in derivatives to change calendar selection mode + * @returns {string} + * @protected + */ + get _calendarSelectionMode() { + return "Single"; + } - if (this.maxDate) { - this._calendar.maxDate = this.maxDate; + /** + * Used to provide a timestamp to the Calendar (to focus it to a relevant date when open) based on the component's state + * Override in derivatives to provide the calendar a timestamp based on their properties + * By default focus the calendar on the selected date if set, or the current day otherwise + * @protected + */ + get _calendarTimestamp() { + let millisecondsUTC; + if (this.value && this._checkValueValidity(this.value)) { + millisecondsUTC = this.dateValueUTC.getTime(); + } else { + millisecondsUTC = new Date().getTime(); } + + return getRoundedTimestamp(millisecondsUTC); } - _getTimeStampFromString(value) { - const jsDate = this.getFormat().parse(value); - if (jsDate) { - return CalendarDate.fromLocalJSDate(jsDate, this._primaryCalendarType).toUTCJSDate().valueOf(); + /** + * Used to provide selectedDates to the calendar based on the component's state + * Override in derivatives to provide different rules for setting the calendar's selected dates + * @protected + */ + get _calendarSelectedDates() { + if (!this.value) { + return []; + } + + if (this._checkValueValidity(this.value)) { + return [getRoundedTimestamp(this.dateValueUTC.getTime())]; } - return undefined; + + return []; } _onkeydown(event) { if (isShow(event)) { event.preventDefault(); // Prevent scroll on Alt/Option + Arrow Up/Down if (this.isOpen()) { - if (isF4(event)) { - if (this.calendar._monthPicker._hidden) { - this.calendar._showYearPicker(); - } - } else { + if (!isF4(event)) { this._toggleAndFocusInput(); } } else { @@ -535,108 +415,63 @@ class DatePicker extends UI5Element { return; } - if (isEnter(event)) { - this._handleEnterPressed(); - } - if (isPageUpShiftCtrl(event)) { event.preventDefault(); - this._changeDateValueWrapper(true, true, false, false); + this._modifyDateValue(1, "year"); } else if (isPageUpShift(event)) { event.preventDefault(); - this._changeDateValueWrapper(true, false, true, false); + this._modifyDateValue(1, "month"); } else if (isPageUp(event)) { event.preventDefault(); - this._changeDateValueWrapper(true, false, false, true); - } - - if (isPageDownShiftCtrl(event)) { + this._modifyDateValue(1, "day"); + } else if (isPageDownShiftCtrl(event)) { event.preventDefault(); - this._changeDateValueWrapper(false, true, false, false); + this._modifyDateValue(-1, "year"); } else if (isPageDownShift(event)) { event.preventDefault(); - this._changeDateValueWrapper(false, false, true, false); + this._modifyDateValue(-1, "month"); } else if (isPageDown(event)) { event.preventDefault(); - this._changeDateValueWrapper(false, false, false, true); + this._modifyDateValue(-1, "day"); } } /** - * This method is used in the derived classes - */ - _handleEnterPressed() {} - - /** - * This method is used in the derived classes - */ - _onfocusout() {} - - /** - * Adds or extracts a given number of measuring units from the "dateValue" property value - * @param {boolean} forward if true indicates addition - * @param {boolean} years indicates that the measuring unit is in years - * @param {boolean} months indicates that the measuring unit is in months - * @param {boolean} days indicates that the measuring unit is in days - * @param {int} step number of measuring units to substract or add defaults to 1 - */ - _changeDateValueWrapper(forward, years, months, days, step = 1) { - let date = this.dateValue; - date = this._changeDateValue(date, forward, years, months, days, step); - this.value = this.formatValue(date); - } - - /** - * Adds or extracts a given number of measuring units from the "dateValue" property value * - * @param {boolean} date js date object to be changed - * @param {boolean} years indicates that the measuring unit is in years - * @param {boolean} months indicates that the measuring unit is in months - * @param {boolean} days indicates that the measuring unit is in days - * @param {boolean} forward if true indicates addition - * @param {int} step number of measuring units to substract or add defaults ot 1 - * @returns {Object} JS date object + * @param amount + * @param unit + * @protected */ - _changeDateValue(date, forward, years, months, days, step = 1) { - if (!date) { + _modifyDateValue(amount, unit) { + if (!this.dateValue) { return; } - let calDate = CalendarDate.fromLocalJSDate(date, this._primaryCalendarType); - const oldCalDate = new CalendarDate(calDate, this._primaryCalendarType); - const incrementStep = forward ? step : -step; + const modifiedDate = modifyDateBy(CalendarDate.fromLocalJSDate(this.dateValue), amount, unit, this._minDate, this._maxDate); + const newValue = this.formatValue(modifiedDate.toUTCJSDate()); + this._updateValueAndFireEvents(newValue, true, ["change", "value-changed"]); + } - if (incrementStep === 0 || (!days && !months && !years)) { - return; + _updateValueAndFireEvents(value, normalizeValue, events) { + const valid = this._checkValueValidity(value); + if (valid && normalizeValue) { + value = this.normalizeValue(value); // transform valid values (in any format) to the correct format } - if (days) { - calDate.setDate(calDate.getDate() + incrementStep); - } else if (months) { - calDate.setMonth(calDate.getMonth() + incrementStep); - const monthDiff = (calDate.getYear() - oldCalDate.getYear()) * 12 + (calDate.getMonth() - oldCalDate.getMonth()); - - if (calDate.getMonth() === oldCalDate.getMonth() || monthDiff !== incrementStep) { - // first condition example: 31th of March increment month with -1 results in 2th of March - // second condition example: 31th of January increment month with +1 results in 2th of March - calDate.setDate(0); - } - } else if (years) { - calDate.setYear(calDate.getYear() + incrementStep); - - if (calDate.getMonth() !== oldCalDate.getMonth()) { - // day doesn't exist in this month (February 29th) - calDate.setDate(0); - } - } + this.value = value; + this._updateValueState(); // Change the value state to Error/None, but only if needed + events.forEach(event => { + this.fireEvent(event, { value, valid }); + }); + } - if (calDate.valueOf() < this._minDate) { - calDate = CalendarDate.fromTimestamp(this._minDate, this._primaryCalendarType); - } else if (calDate.valueOf() > this._maxDate) { - calDate = CalendarDate.fromTimestamp(this._maxDate, this._primaryCalendarType); + _updateValueState() { + const isValid = this._checkValueValidity(this.value); + if (!isValid) { // If not valid - always set Error regardless of the current value state + this.valueState = ValueState.Error; + } else if (isValid && this.valueState === ValueState.Error) { // However if valid, change only Error (but not the others) to None + this.valueState = ValueState.None; } - - return calDate.toLocalJSDate(); } _toggleAndFocusInput() { @@ -648,36 +483,36 @@ class DatePicker extends UI5Element { return this.shadowRoot.querySelector("[ui5-input]"); } - async _handleInputChange() { - let nextValue = await this._getInput().getInputValue(); - const emptyValue = nextValue === ""; - const isValid = emptyValue || this._checkValueValidity(nextValue); - - if (isValid) { - nextValue = this.normalizeValue(nextValue); - this.valueState = ValueState.None; - } else { - this.valueState = ValueState.Error; - } - + /** + * The ui5-input "submit" event handler - fire change event when the user presses enter + * @protected + */ + _onInputSubmit(event) {} - this.value = nextValue; - this.fireEvent("change", { value: nextValue, valid: isValid }); - // Angular two way data binding - this.fireEvent("value-changed", { value: nextValue, valid: isValid }); + /** + * The ui5-input "change" event handler - fire change event when the user focuses out of the input + * @protected + */ + _onInputChange(event) { + this._updateValueAndFireEvents(event.target.value, true, ["change", "value-changed"]); } - async _handleInputLiveChange() { - const nextValue = await this._getInput().getInputValue(); - const emptyValue = nextValue === ""; - const isValid = emptyValue || this._checkValueValidity(nextValue); - - this.value = nextValue; - this.fireEvent("input", { value: nextValue, valid: isValid }); + /** + * The ui5-input "input" event handler - fire input even when the user types + * @protected + */ + async _onInputInput(event) { + this._updateValueAndFireEvents(event.target.value, false, ["input"]); } + /** + * @protected + */ _checkValueValidity(value) { - return this.isValid(value) && this.isInValidRange(this._getTimeStampFromString(value)); + if (value === "") { + return true; + } + return this.isValid(value) && this.isInValidRange(value); } _click(event) { @@ -693,7 +528,11 @@ class DatePicker extends UI5Element { * @public */ isValid(value = "") { - return !!(value && this.getFormat().parse(value)); + if (value === "") { + return true; + } + + return !!this.getFormat().parse(value); } /** @@ -706,76 +545,29 @@ class DatePicker extends UI5Element { return true; } - const pickedDate = new Date(value), - minDate = new Date(this._minDate), - 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; + const calendarDate = this._getCalendarDateFromString(value); + return calendarDate.valueOf() >= this._minDate.valueOf() && calendarDate.valueOf() <= this._maxDate.valueOf(); } - // because the parser understands more than one format - // but we need values in one format + /** + * The parser understands many formats, but we need one format + * @protected + */ normalizeValue(value) { if (value === "") { return value; } - return this.getFormat().format(this.getFormat().parse(value)); - } - - get validValue() { - if (this.isValid(this.value)) { - return this.value; - } - return this.getFormat().format(new Date()); - } - - get calendar() { - return this.responsivePopover.querySelector(`#${this._id}-calendar`); - } - - get _calendarDate() { - const millisecondsUTC = this.getFormat().parse(this.validValue, true).getTime(); - const oCalDate = CalendarDate.fromTimestamp( - millisecondsUTC - (millisecondsUTC % (24 * 60 * 60 * 1000)), - this._primaryCalendarType - ); - return oCalDate; - } - - get _primaryCalendarType() { - const localeData = getCachedLocaleDataInstance(getLocale()); - return this.primaryCalendarType || getCalendarType() || localeData.getPreferredCalendarType(); - } - - get _formatPattern() { - return this.formatPattern || "medium"; // get from config - } - - get _isPattern() { - return this._formatPattern !== "medium" && this._formatPattern !== "short" && this._formatPattern !== "long"; + return this.getFormat().format(this.getFormat().parse(value, true), true); // it is important to both parse and format the date as UTC } get _displayFormat() { return this.getFormat().oFormatOptions.pattern; } + /** + * @protected + */ get _placeholder() { return this.placeholder !== undefined ? this.placeholder : this._displayFormat; } @@ -800,22 +592,6 @@ class DatePicker extends UI5Element { return isIE(); } - getFormat() { - let dateFormat; - if (this._isPattern) { - dateFormat = DateFormat.getInstance({ - pattern: this._formatPattern, - calendarType: this._primaryCalendarType, - }); - } else { - dateFormat = DateFormat.getInstance({ - style: this._formatPattern, - calendarType: this._primaryCalendarType, - }); - } - return dateFormat; - } - get accInfo() { return { "ariaDescribedBy": `${this._id}-date`, @@ -829,22 +605,6 @@ class DatePicker extends UI5Element { }; } - get _maxDate() { - return this.maxDate ? this._getTimeStampFromString(this.maxDate) : this._getMaxCalendarDate(); - } - - get _minDate() { - return this.minDate ? this._getTimeStampFromString(this.minDate) : this._getMinCalendarDate(); - } - - _getMinCalendarDate() { - return getMinCalendarDate(this._primaryCalendarType); - } - - _getMaxCalendarDate() { - return getMaxCalendarDate(this._primaryCalendarType); - } - get openIconTitle() { return this.i18nBundle.getText(DATEPICKER_OPEN_ICON_TITLE); } @@ -874,49 +634,19 @@ class DatePicker extends UI5Element { return !this.disabled && !this.readonly; } - _handleCalendarChange(event) { - const iNewValue = event.detail.dates && event.detail.dates[0]; - - if (this._calendar.selectedDates.indexOf(iNewValue) !== -1) { - this.closePicker(); - return false; - } - - const fireChange = this._handleCalendarSelectedDatesChange(event, iNewValue); - - if (fireChange) { - this.fireEvent("change", { value: this.value, valid: true }); - // Angular two way data binding - this.fireEvent("value-changed", { value: this.value, valid: true }); - } - - this.closePicker(); - } - - _handleCalendarSelectedDatesChange(event, newValue) { - this._updateValueCalendarSelectedDatesChange(newValue); + /** + * The user selected a new date in the calendar + * @param event + * @protected + */ + onSelectedDatesChange(event) { + const timestamp = event.detail.dates && event.detail.dates[0]; + const calendarDate = CalendarDate.fromTimestamp(timestamp * 1000, this._primaryCalendarType); + const newValue = this.getFormat().format(calendarDate.toUTCJSDate(), true); + this._updateValueAndFireEvents(newValue, true, ["change", "value-changed"]); - this._calendar.timestamp = newValue; - this._calendar.selectedDates = [...event.detail.dates]; this._focusInputAfterClose = true; - - if (this.isInValidRange(this._getTimeStampFromString(this.value))) { - this.valueState = ValueState.None; - } else { - this.valueState = ValueState.Error; - } - - return true; - } - - _updateValueCalendarSelectedDatesChange(newValue) { - this.value = this.getFormat().format( - new Date(CalendarDate.fromTimestamp( - newValue * 1000, - this._primaryCalendarType - ).valueOf()), - true - ); + this.closePicker(); } /** @@ -939,19 +669,12 @@ class DatePicker extends UI5Element { /** * Opens the picker. - * @param {object} options A JSON object with additional configuration.
    - * { focusInput: true } By default, the focus goes in the picker after opening it. - * Specify this option to focus the input field. * @public */ - async openPicker(options) { + async openPicker() { this._isPickerOpen = true; + this._calendarCurrentPicker = "day"; this.responsivePopover = await this._respPopover(); - this._changeCalendarSelection(); - - if (options && options.focusInput) { - this._focusInputAfterOpen = true; - } this.responsivePopover.open(this); } @@ -964,18 +687,6 @@ class DatePicker extends UI5Element { } } - _changeCalendarSelection() { - if (this._calendarDate.getYear() < 1) { - // 0 is a valid year, but we cannot display it - return; - } - - const timestamp = this._calendarDate.valueOf() / 1000; - this._calendar = Object.assign({}, this._calendar); - this._calendar.timestamp = timestamp; - this._calendar.selectedDates = this.value ? [timestamp] : []; - } - /** * Checks if the picker is open. * @returns {Boolean} true if the picker is open, false otherwise @@ -986,23 +697,7 @@ class DatePicker extends UI5Element { } /** - * Gets some semantic details about an event originated in the control. - * @param {*} event An event object - * @returns {Object} Semantic details - */ - getSemanticTargetInfo(event) { - const oDomTarget = getDomTarget(event); - let isInput = false; - - if (oDomTarget && oDomTarget.className.indexOf("ui5-input-inner") > -1) { - isInput = true; - } - - return { isInput }; - } - - /** - * Currently selected date represented as JavaScript Date instance. + * Currently selected date represented as a Local JavaScript Date instance. * * @readonly * @type { Date } @@ -1012,6 +707,10 @@ class DatePicker extends UI5Element { return this.getFormat().parse(this.value); } + get dateValueUTC() { + return this.getFormat().parse(this.value, true); + } + get styles() { return { main: { @@ -1033,30 +732,8 @@ class DatePicker extends UI5Element { Button, ]; } - - static async onDefine() { - await Promise.all([ - fetchCldr(getLocale().getLanguage(), getLocale().getRegion(), getLocale().getScript()), - fetchI18nBundle("@ui5/webcomponents"), - ]); - } } -const getDomTarget = event => { - let target, - composedPath; - - if (typeof event.composedPath === "function") { - composedPath = event.composedPath(); - } - - if (Array.isArray(composedPath) && composedPath.length) { - target = composedPath[0]; - } - - return target; -}; - DatePicker.define(); export default DatePicker; diff --git a/packages/main/src/DatePickerPopover.hbs b/packages/main/src/DatePickerPopover.hbs index 3fb8f95ea220..560fa827dd04 100644 --- a/packages/main/src/DatePickerPopover.hbs +++ b/packages/main/src/DatePickerPopover.hbs @@ -1,7 +1,7 @@ {{#if showHeader}} {{> header}} @@ -44,16 +41,17 @@ {{#*inline "content"}} {{/inline}} -{{#*inline "footer"}}{{/inline}} \ No newline at end of file +{{#*inline "footer"}}{{/inline}} diff --git a/packages/main/src/DateRangePicker.hbs b/packages/main/src/DateRangePicker.hbs deleted file mode 100644 index 18b8bed40b83..000000000000 --- a/packages/main/src/DateRangePicker.hbs +++ /dev/null @@ -1 +0,0 @@ -{{>include "./DatePicker.hbs"}} \ No newline at end of file diff --git a/packages/main/src/DateRangePicker.js b/packages/main/src/DateRangePicker.js index d708332b52d1..c2e9d8bb3447 100644 --- a/packages/main/src/DateRangePicker.js +++ b/packages/main/src/DateRangePicker.js @@ -1,10 +1,7 @@ -import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; -import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; -import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; import RenderScheduler from "@ui5/webcomponents-base/dist/RenderScheduler.js"; import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; -import CalendarSelection from "@ui5/webcomponents-base/dist/types/CalendarSelection.js"; -import DateRangePickerTemplate from "./generated/templates/DateRangePickerTemplate.lit.js"; +import modifyDateBy from "@ui5/webcomponents-localization/dist/dates/modifyDateBy.js"; +import getRoundedTimestamp from "@ui5/webcomponents-localization/dist/dates/getRoundedTimestamp.js"; // Styles import DateRangePickerCss from "./generated/themes/DateRangePicker.css.js"; @@ -18,37 +15,15 @@ const metadata = { properties: /** @lends sap.ui.webcomponents.main.DateRangePicker.prototype */ { /** * Determines the symbol which separates the dates. + * If not supplied, the default time interval delimiter for the current locale will be used. * * @type {string} - * @defaultvalue "-" * @public */ delimiter: { type: String, defaultValue: "-", }, - /** - * Defines the UNIX timestamp of the first date - seconds since 00:00:00 UTC on Jan 1, 1970. - * @type {number} - * @private - */ - _firstDateTimestamp: { - type: Integer, - }, - /** - * Defines the UNIX timestamp of the second date- seconds since 00:00:00 UTC on Jan 1, 1970. - * @type {number} - * @private - */ - _lastDateTimestamp: { - type: Integer, - }, - }, - slots: /** @lends sap.ui.webcomponents.main.DateRangePicker.prototype */ { - // - }, - events: /** @lends sap.ui.webcomponents.main.DateRangePicker.prototype */ { - // }, }; @@ -71,10 +46,8 @@ const metadata = { *
    * * When the ui5-daterange-picker input field is focused the user can - * increment or decrement the corresponding field of the JS date object referenced by _firstDateTimestamp propery - * if the caret symbol is before the delimiter character or _lastDateTimestamp property if the caret symbol is - * after the delimiter character. - * The following shortcuts are enabled: + * increment or decrement respectively the range start or end date, depending on where the cursor is. + * The following shortcuts are available: *
    *
      *
    • [PAGEDOWN] - Decrements the corresponding day of the month by one
    • @@ -98,100 +71,40 @@ class DateRangePicker extends DatePicker { return metadata; } - static get render() { - return litRender; - } - static get styles() { return [DatePicker.styles, DateRangePickerCss]; } - static get template() { - return DateRangePickerTemplate; + get _firstDateTimestamp() { + return this._extractFirstTimestamp(this.value); } - constructor() { - super(); - this._calendar.selection = CalendarSelection.Range; - } - - _splitValueByDelimiter(value) { - let returnValue = []; - - if (!value) { - return ["", ""]; - } - - if (this.delimiter) { - returnValue = String(value).split(this.delimiter); - } - - return returnValue; + get _lastDateTimestamp() { + return this._extractLastTimestamp(this.value); } - _setValue(value) { - const emptyValue = value === "", - isValid = emptyValue || this._checkValueValidity(value); - - if (value === this._prevValue) { - return this; - } - - if (!value) { - this.value = ""; - return; - } - - const dates = this._splitValueByDelimiter(value); - if (!isValid) { - this.valueState = ValueState.Error; - console.warn("Value can not be converted to a valid dates", this); // eslint-disable-line - return; - } - this.valueState = ValueState.None; - - let firstDate = this.getFormat().parse(dates[0]); - let lastDate; - - if (dates.length > 1) { - lastDate = this.getFormat().parse(dates[1]); - - if (firstDate > lastDate) { - const temp = firstDate; - firstDate = lastDate; - lastDate = temp; - } - this._lastDateTimestamp = CalendarDate.fromLocalJSDate(lastDate, this._primaryCalendarType).valueOf() / 1000; - } - this._firstDateTimestamp = CalendarDate.fromLocalJSDate(firstDate, this._primaryCalendarType).valueOf() / 1000; - - this.value = this._formatValue(firstDate, lastDate); - this._prevValue = this.value; + /** + * Required by DatePicker.js + * @override + */ + get _calendarSelectionMode() { + return "Range"; } - _changeCalendarSelection() { - if (this._calendarDate.getYear() < 1) { - // 0 is a valid year, but we cannot display it - return; - } - - const timestamp = this._calendarDate.valueOf() / 1000; - const dates = this._splitValueByDelimiter(this.value); - this._calendar = Object.assign({}, this._calendar); - this._calendar.timestamp = timestamp; - this._calendar.selectedDates = dates.map(date => this._getTimeStampFromString(date) / 1000); + /** + * Required by DatePicker.js - set the calendar focus on the first selected date (or today if not set) + * @override + */ + get _calendarTimestamp() { + return this._firstDateTimestamp || getRoundedTimestamp(); } - get _calendarDate() { - const dateStrings = this._splitValueByDelimiter(this.value), - value = Boolean(this.value) && this._checkValueValidity(this.value) ? dateStrings[0] : this.getFormat().format(new Date()), - millisecondsUTCFirstDate = value ? this.getFormat().parse(value, true).getTime() : this.getFormat().parse(this.validValue, true).getTime(), - oCalDateFirst = CalendarDate.fromTimestamp( - millisecondsUTCFirstDate - (millisecondsUTCFirstDate % (24 * 60 * 60 * 1000)), - this._primaryCalendarType - ); - - return oCalDateFirst; + /** + * Required by DatePicker.js + * @override + */ + get _calendarSelectedDates() { + return [this._firstDateTimestamp, this._lastDateTimestamp].filter(date => !!date); } /** @@ -202,8 +115,7 @@ class DateRangePicker extends DatePicker { * @public */ get firstDateValue() { - const dateValue = new Date(this._firstDateTimestamp * 1000); - return new Date(dateValue.getUTCFullYear(), dateValue.getUTCMonth(), dateValue.getUTCDate(), dateValue.getUTCHours()); + return CalendarDate.fromTimestamp(this._firstDateTimestamp * 1000).toLocalJSDate(); } /** @@ -214,205 +126,157 @@ class DateRangePicker extends DatePicker { * @public */ get lastDateValue() { - const dateValue = new Date(this._lastDateTimestamp * 1000); - return new Date(dateValue.getUTCFullYear(), dateValue.getUTCMonth(), dateValue.getUTCDate(), dateValue.getUTCHours()); + return CalendarDate.fromTimestamp(this._lastDateTimestamp * 1000).toLocalJSDate(); } + /** + * @override + */ get _placeholder() { - return this.placeholder !== undefined ? this.placeholder : this._displayFormat.concat(" ", this.delimiter, " ", this._displayFormat); - } - - async getDayPicker() { - this.responsivePopover = await this._respPopover(); - const calendar = this.responsivePopover.querySelector(`#${this._id}-calendar`); - return calendar.shadowRoot.querySelector(`#${calendar._id}-daypicker`); + return this.placeholder !== undefined ? this.placeholder : `${this._displayFormat} ${this._effectiveDelimiter} ${this._displayFormat}`; } - async _handleInputChange() { - const nextValue = await this._getInput().getInputValue(); - const emptyValue = nextValue === ""; - const isValid = emptyValue || this._checkValueValidity(nextValue); - - if (isValid) { - this._setValue(nextValue); - this.valueState = ValueState.None; - } else { - this.valueState = ValueState.Error; - } - - this.fireEvent("change", { value: nextValue, valid: isValid }); - // Angular two way data binding - this.fireEvent("value-changed", { value: nextValue, valid: isValid }); - } - - _checkValueValidity(value) { - return this.isValid(value) && this.isInValidRange(value); + /** + * @override + */ + async _onInputSubmit(event) { + const input = this._getInput(); + const caretPos = input.getCaretPosition(); + await RenderScheduler.whenFinished(); + input.setCaretPosition(caretPos); // Return the caret on the previous position after rendering } + /** + * @override + */ isValid(value) { - return this._splitValueByDelimiter(value) - .map(dateString => super.isValid(dateString)) - .every(valid => valid); + const parts = this._splitValueByDelimiter(value); + return parts.length <= 2 && parts.every(dateString => super.isValid(dateString)); // must be at most 2 dates and each must be valid } + /** + * @override + */ isInValidRange(value) { - return this._splitValueByDelimiter(value) - .map(dateString => super.isInValidRange(this._getTimeStampFromString(dateString))) - .every(valid => valid); + return this._splitValueByDelimiter(value).every(dateString => super.isInValidRange(dateString)); } - _handleCalendarChange(event) { - const selectedDates = event.detail.dates; - if (selectedDates.length === 2) { - this.closePicker(); - this._firstDateTimestamp = selectedDates[0] < selectedDates[1] ? selectedDates[0] : selectedDates[1]; - this._lastDateTimestamp = selectedDates[0] > selectedDates[1] ? selectedDates[0] : selectedDates[1]; - const fireChange = this._handleCalendarSelectedDatesChange(event, this._firstDateTimestamp); - - if (fireChange) { - this.fireEvent("change", { value: this.value, valid: true }); - // Angular two way data binding - this.fireEvent("value-changed", { value: this.value, valid: true }); - } - } else { - this._firstDateTimestamp = selectedDates[0]; - this._lastDateTimestamp = undefined; - this._calendar.timestamp = selectedDates[0]; - this._calendar.selectedDates = [...event.detail.dates]; - this.value = ""; - return false; + /** + * Extract both dates as timestamps, flip if necessary, and build (which will use the desired format so we enforce the format too) + * @override + */ + normalizeValue(value) { + const firstDateTimestamp = this._extractFirstTimestamp(value); + const lastDateTimestamp = this._extractLastTimestamp(value); + if (firstDateTimestamp && lastDateTimestamp && firstDateTimestamp > lastDateTimestamp) { // if both are timestamps (not undefined), flip if necessary + return this._buildValue(lastDateTimestamp, firstDateTimestamp); } + return this._buildValue(firstDateTimestamp, lastDateTimestamp); } /** - * Adds or extracts a given number of measuring units from the "dateValue" property value - * - * @param {boolean} forward if true indicates addition - * @param {boolean} years indicates that the measuring unit is in years - * @param {boolean} months indicates that the measuring unit is in months - * @param {boolean} days indicates that the measuring unit is in days - * @param {int} step number of measuring units to substract or add defaults ot 1 + * @override */ - async _changeDateValueWrapper(forward, years, months, days, step = 1) { - const emptyValue = this.value === ""; - const isValid = emptyValue || this._checkValueValidity(this.value); - - if (!isValid) { + onSelectedDatesChange(event) { + const selectedDates = event.detail.dates; + if (selectedDates.length !== 2) { // Do nothing until the user selects 2 dates, we don't change any state at all for one date return; } - const dates = this._splitValueByDelimiter(this.value); - const innerInput = this.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); - const caretPos = this._getCaretPosition(innerInput); - const first = dates[0] && caretPos <= dates[0].trim().length + 1; - const last = dates[1] && (caretPos >= this.value.length - dates[1].trim().length - 1 && caretPos <= this.value.length); - let firstDate = this.getFormat().parse(dates[0]); - let lastDate = this.getFormat().parse(dates[1]); - - if (first && firstDate) { - firstDate = this._changeDateValue(firstDate, forward, years, months, days, step); - } else if (last && lastDate) { - lastDate = this._changeDateValue(lastDate, forward, years, months, days, step); - } - - this.value = this._formatValue(firstDate, lastDate); - - await RenderScheduler.whenFinished(); - // Return the caret on the previous position after rendering - this._setCaretPosition(innerInput, caretPos); + const newValue = this._buildValue(...selectedDates); // the value will be normalized so we don't need to order them here + this._updateValueAndFireEvents(newValue, true, ["change", "value-changed"]); + this._focusInputAfterClose = true; + this.closePicker(); } /** - * This method is used in the derived classes + * @override */ - async _handleEnterPressed() { - const innerInput = this.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); - const caretPos = this._getCaretPosition(innerInput); + async _modifyDateValue(amount, unit) { + if (!this._lastDateTimestamp) { // If empty or only one date -> treat as datepicker entirely + return super._modifyDateValue(amount, unit); + } - this._setValue(this.value); + const input = this._getInput(); + let caretPos = input.getCaretPosition(); + let newValue; + + if (caretPos <= this.value.indexOf(this._effectiveDelimiter)) { // The user is focusing the first date -> change it and keep the seoond date + const firstDateModified = modifyDateBy(CalendarDate.fromTimestamp(this._firstDateTimestamp * 1000), amount, unit, this._minDate, this._maxDate); + const newFirstDateTimestamp = firstDateModified.valueOf() / 1000; + if (newFirstDateTimestamp > this._lastDateTimestamp) { // dates flipped -> move the caret to the same position but on the last date + caretPos += Math.ceil(this.value.length / 2); + } + newValue = this._buildValue(newFirstDateTimestamp, this._lastDateTimestamp); // the value will be normalized so we don't try to order them here + } else { + const lastDateModified = modifyDateBy(CalendarDate.fromTimestamp(this._lastDateTimestamp * 1000), amount, unit, this._minDate, this._maxDate); + const newLastDateTimestamp = lastDateModified.valueOf() / 1000; + newValue = this._buildValue(this._firstDateTimestamp, newLastDateTimestamp); // the value will be normalized so we don't try to order them here + if (newLastDateTimestamp < this._firstDateTimestamp) { // dates flipped -> move the caret to the same position but on the first date + caretPos -= Math.ceil(this.value.length / 2); + } + } + this._updateValueAndFireEvents(newValue, true, ["change", "value-changed"]); await RenderScheduler.whenFinished(); - // Return the caret on the previous position after rendering - this._setCaretPosition(innerInput, caretPos); + input.setCaretPosition(caretPos); // Return the caret to the previous (or the adjusted, if dates flipped) position after rendering } - _onfocusout() { - this._setValue(this.value); + get _effectiveDelimiter() { + return this.delimiter || this.constructor.getMetadata().getProperties().delimiter.defaultValue; // treat empty string as the default value + } + + _splitValueByDelimiter(value) { + return value.split(this._effectiveDelimiter).map(date => date.trim()); // just split by delimiter and trim spaces } /** - * Returns the caret (cursor) position of the specified text field (field). - * Return value range is 0-field.value.length. - */ - _getCaretPosition(field) { - // Initialize - let caretPos = 0; - - // IE Support - if (document.selection) { - // Set focus on the element - field.focus(); - - // To get cursor position, get empty selection range - const selection = document.selection.createRange(); - - // Move selection start to 0 position - selection.moveStart("character", -field.value.length); - - // The caret position is selection length - caretPos = selection.text.length; - } else if (field.selectionStart || field.selectionStart === "0") { // Firefox support - caretPos = field.selectionDirection === "backward" ? field.selectionStart : field.selectionEnd; + * Returns a UTC timestamp, representing the first date in the value string or undefined if the value is empty + * @private + */ + _extractFirstTimestamp(value) { + if (!value || !this._checkValueValidity(value)) { + return undefined; } - return caretPos; + const dateStrings = this._splitValueByDelimiter(value); // at least one item guaranteed due to the checks above (non-empty and valid) + return this.getFormat().parse(dateStrings[0], true).getTime() / 1000; } - _setCaretPosition(field, caretPos) { - if (field.createTextRange) { - const range = field.createTextRange(); - range.move("character", caretPos); - range.select(); - } else if (field.selectionStart) { - field.focus(); - field.setSelectionRange(caretPos, caretPos); - } else { - field.focus(); + /** + * Returns a UTC timestamp, representing the last date in the value string or undefined if the value is empty or there is just one date + * @private + */ + _extractLastTimestamp(value) { + if (!value || !this._checkValueValidity(value)) { + return undefined; } - } - _updateValueCalendarSelectedDatesChange() { - const calStartDate = CalendarDate.fromTimestamp(this._firstDateTimestamp * 1000, this._primaryCalendarType); - const calEndDate = CalendarDate.fromTimestamp(this._lastDateTimestamp * 1000, this._primaryCalendarType); + const dateStrings = this._splitValueByDelimiter(value); + if (dateStrings[1]) { + return this.getFormat().parse(dateStrings[1], true).getTime() / 1000; + } - // Collect both dates and merge them into one - this.value = this._formatValue(calStartDate.toLocalJSDate(), calEndDate.toLocalJSDate()); - this._prevValue = this.value; + return undefined; } /** - * Combines the start and end dates of a range into a formated string - * - * @param {int} firstDate locale start date - * @param {int} lastDate locale end date - * @returns {string} formated start to end date range + * Builds a string value out of two UTC timestamps - this method is the counterpart to _extractFirstTimestamp/_extractLastTimestamp + * @private */ - _formatValue(firstDate, lastDate) { - let value = ""; - const delimiter = this.delimiter, - format = this.getFormat(), - firstDateString = firstDate && format.format(firstDate), - lastDateString = lastDate && format.format(lastDate); - - if (firstDateString) { - if (delimiter && delimiter !== "" && lastDateString) { - value = firstDateString.concat(" ", delimiter, " ", lastDateString); - } else { - value = firstDateString; + _buildValue(firstDateTimestamp, lastDateTimestamp) { + if (firstDateTimestamp) { + const firstDateString = this._getStringFromTimestamp(firstDateTimestamp * 1000); + + if (!lastDateTimestamp) { + return firstDateString; } + + const lastDateString = this._getStringFromTimestamp(lastDateTimestamp * 1000); + return `${firstDateString} ${this._effectiveDelimiter} ${lastDateString}`; } - return value; + return ""; } } diff --git a/packages/main/src/DateTimePicker.js b/packages/main/src/DateTimePicker.js index aee390969d7b..dff30146ce6e 100644 --- a/packages/main/src/DateTimePicker.js +++ b/packages/main/src/DateTimePicker.js @@ -1,34 +1,18 @@ import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; +import modifyDateBy from "@ui5/webcomponents-localization/dist/dates/modifyDateBy.js"; import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import "@ui5/webcomponents-icons/dist/date-time.js"; -import { - isLeft, - isRight, -} from "@ui5/webcomponents-base/dist/Keys.js"; import Button from "./Button.js"; import ToggleButton from "./ToggleButton.js"; import SegmentedButton from "./SegmentedButton.js"; import Calendar from "./Calendar.js"; import DatePicker from "./DatePicker.js"; -import WheelSlider from "./WheelSlider.js"; - -// time functions -import { - getHours, - getMinutes, - getSeconds, - getHoursConfigByFormat, - getTimeControlsByFormat, -} from "./timepicker-utils/TimeSlider.js"; +import TimeSelection from "./TimeSelection.js"; // i18n texts import { - TIMEPICKER_HOURS_LABEL, - TIMEPICKER_MINUTES_LABEL, - TIMEPICKER_SECONDS_LABEL, - TIMEPICKER_PERIODS_LABEL, TIMEPICKER_SUBMIT_BUTTON, TIMEPICKER_CANCEL_BUTTON, DATETIME_PICKER_DATE_BUTTON, @@ -49,7 +33,6 @@ const PHONE_MODE_BREAKPOINT = 640; // px */ const metadata = { tag: "ui5-datetime-picker", - languageAware: true, properties: /** @lends sap.ui.webcomponents.main.DateTimePicker.prototype */ { /** @@ -81,13 +64,19 @@ const metadata = { }, /** - * Defines the state the hours slider - expanded by default. - * @type {boolean} - * @defaultvalue false + * Selected, but not yet confirmed date/time * @private */ - _hoursCollapsed: { - type: Boolean, + _previewValues: { + type: Object, + }, + + /** + * @private + */ + _currentTimeSlider: { + type: String, + defaultValue: "hours", }, }, }; @@ -190,40 +179,28 @@ class DateTimePicker extends DatePicker { Button, ToggleButton, SegmentedButton, - WheelSlider, + TimeSelection, ]; } constructor() { super(); - - this._calendarPreview = null; // preview of the calendar selection - - this._hoursConfig = { // hours configuration (12/24 hour format) - minHour: 0, - maxHour: 0, - isTwelveHoursFormat: false, - }; - - const superFn = this._respPopoverConfig.afterClose; - this._respPopoverConfig.afterClose = () => { - superFn(); - this._showTimeView = false; - this._calendarPreview = null; - }; - this._handleResizeBound = this._handleResize.bind(this); } /** - * LIFECYCLE METHODS + * @override */ - - onBeforeRendering() { - super.onBeforeRendering(); - this.updateHoursFormatConfig(); + onResponsivePopoverAfterClose() { + super.onResponsivePopoverAfterClose(); + this._showTimeView = false; + this._previewValues = {}; } + /** + * LIFECYCLE METHODS + */ + onEnterDOM() { ResizeHandler.register(document.body, this._handleResizeBound); } @@ -246,33 +223,8 @@ class DateTimePicker extends DatePicker { */ async openPicker(options) { await super.openPicker(options); - await this.setSlidersValue(); - this.expandHoursSlider(); - this.storePreviousValue(); - this._slidersDomRefs = await this.slidersDomRefs(); - } - - /** - * Closes the picker. - * @public - */ - closePicker() { - return super.closePicker(); // in order to be displayed in the DateTimePicker API reference - } - - /** - * Checks if a value is valid against the current date/time format. - * - * @param {string} value A value to be tested against the current date/time format - * @public - */ - isValid(value = "") { - return super.isValid(value); // in order to be displayed in the DateTimePicker API reference - } - - async slidersDomRefs() { - await this.getPicker(); - return this.responsivePopover.getElementsByClassName("ui5-dt-wheel"); + this._currentTimeSlider = "hours"; + this._previewValues.timeSelectionValue = this.value || this.getFormat().format(new Date()); } /** @@ -292,53 +244,29 @@ class DateTimePicker extends DatePicker { } get _formatPattern() { - return this.normalizePattern(this.formatPattern); - } + const hasHours = !!this.formatPattern.match(/H/i); + const fallback = !this.formatPattern || !hasHours; - get _calTimestamp() { - return this._calendarPreview ? this._calendarPreview.timestamp : this._calendar.timestamp; - } - - get _calDates() { - return this._calendarPreview ? this._calendarPreview.selectedDates : this._calendar.selectedDates; - } - - get secondsArray() { - return getSeconds(); + const localeData = getCachedLocaleDataInstance(getLocale()); + return fallback ? localeData.getCombinedDateTimePattern("medium", "medium", this._primaryCalendarType) : this.formatPattern; } - get minutesArray() { - return getMinutes(); + get _effectiveCalendarTimestamp() { + return this._previewValues.calendarTimestamp ? this._previewValues.calendarTimestamp : this._calendarTimestamp; } - get hoursArray() { - return getHours(this._hoursConfig); + get _effectiveCalendarSelectedDates() { + return this._previewValues.calendarSelectedDate ? [this._previewValues.calendarSelectedDate] : this._calendarSelectedDates; } - get periodsArray() { - return this.getFormat().aDayPeriods.map(x => x.toUpperCase()); + get _effectiveTimeValue() { + return this._previewValues.timeSelectionValue ? this._previewValues.timeSelectionValue : this.value; } get openIconName() { return "date-time"; } - get hoursLabel() { - return this.i18nBundle.getText(TIMEPICKER_HOURS_LABEL); - } - - get minutesLabel() { - return this.i18nBundle.getText(TIMEPICKER_MINUTES_LABEL); - } - - get secondsLabel() { - return this.i18nBundle.getText(TIMEPICKER_SECONDS_LABEL); - } - - get periodLabel() { - return this.i18nBundle.getText(TIMEPICKER_PERIODS_LABEL); - } - get btnOKLabel() { return this.i18nBundle.getText(TIMEPICKER_SUBMIT_BUTTON); } @@ -371,26 +299,6 @@ class DateTimePicker extends DatePicker { return super.phone || this._phoneMode; } - get shouldBuildHoursSlider() { - return this.isTimeControlContained()[0]; - } - - get shouldBuildMinutesSlider() { - return this.isTimeControlContained()[1]; - } - - get shouldBuildSecondsSlider() { - return this.isTimeControlContained()[2]; - } - - get shouldBuildPeriodsSlider() { - return this.isTimeControlContained()[3]; - } - - get _hoursExpanded() { - return !this._hoursCollapsed; - } - /** * Defines whether the dialog on mobile should have header * @private @@ -403,61 +311,61 @@ class DateTimePicker extends DatePicker { * EVENT HANDLERS */ - /** + /** * @override - * Overwrite the method to update the time sliders. */ - _handleInputLiveChange() { - super._handleInputLiveChange(); - this.setSlidersValue(); + onSelectedDatesChange(event) { + this._previewValues = { + ...this._previewValues, + calendarTimestamp: event.detail.timestamp, + calendarSelectedDate: event.detail.dates[0], + }; } - /** - * @override - */ - _handleCalendarChange(event) { - const newValue = event.detail.dates && event.detail.dates[0]; - super._handleCalendarSelectedDatesChange(event, newValue); - this.storeCalendarSelection(); + onTimeSelectionChange(event) { + this._previewValues = { + ...this._previewValues, + timeSelectionValue: event.detail.value, + }; } - /** - * @override - * Overwrite the method to avoid updating the value when the user clicks on the calendar. - * - * Note: the DateTimePicker should change and update the value - * after user presses the submit button. - */ - _updateValueCalendarSelectedDatesChange() {} + onTimeSliderChange(event) { + this._currentTimeSlider = event.detail.slider; + } /** * Handles document resize to switch between phoneMode and normal appearance. */ - async _handleResize() { + _handleResize() { const documentWidth = document.body.offsetWidth; const toPhoneMode = documentWidth <= PHONE_MODE_BREAKPOINT; const modeChange = (toPhoneMode && !this._phoneMode) || (!toPhoneMode && this._phoneMode); // XOR not allowed by lint if (modeChange) { this._phoneMode = toPhoneMode; - this.setSlidersValue(); } } + get _submitDisabled() { + return !this._effectiveCalendarSelectedDates || !this._effectiveCalendarSelectedDates.length; + } + /** * Handles clicking on the submit button, within the picker`s footer. */ - async _submitClick() { - const selectedDate = await this.getCurrentDateTime(); + _submitClick() { + const selectedDate = this.getSelectedDateTime(); - this.value = this.getFormat().format(selectedDate); - const valid = this.isValid(this.value); + const value = this.getFormat().format(selectedDate); + const valid = this.isValid(value); - if (this.value !== this.previousValue) { + if (this.value !== value) { + this.value = value; this.fireEvent("change", { value: this.value, valid }); this.fireEvent("value-changed", { value: this.value, valid }); } + this._focusInputAfterClose = true; this.closePicker(); } @@ -465,8 +373,7 @@ class DateTimePicker extends DatePicker { * Handles clicking on the cancel button, within the picker`s footer, * that would disregard the user selection. */ - async _cancelClick() { - this.value = this.previousValue; + _cancelClick() { this.closePicker(); } @@ -475,95 +382,30 @@ class DateTimePicker extends DatePicker { * between the date and time views. * @param {Event} event */ - async _dateTimeSwitchChange(event) { + _dateTimeSwitchChange(event) { this._showTimeView = event.target.getAttribute("key") === "Time"; - if (this._showTimeView) { - this.expandHoursSlider(); + this._currentTimeSlider = "hours"; } } - /** - * Handles clicking on "minutes", "seconds" and "periods" sliders. - * Note: not bound for "hours" click - * @param {Event} event - */ - _sliderClick() { - this.collapseHoursSlider(); - } /** - * PRIVATE METHODS - */ - - /** - * Stores a preview of the calendar selection to restore it - * when the user switches between the time and date view. - *

      - * Note: this is needed, because the value is not immediately updated on user interaction, - * but only after the user presses the sumbit button. - */ - storeCalendarSelection() { - this._calendarPreview = { - timestamp: this._calendar.timestamp, - dates: this._calendar.selectedDates, - }; - } - - /** - * Stores the value when the picker opens to compare with the value, - * selected by any user interaction and fire the change event, if they differ. - */ - storePreviousValue() { - this.previousValue = this.value; - } - - /** - * Normalizes the current formatPattern. - * - * Fallbacks to the default formatPattern according to the locale when: - * - no format is set at all - * - the format does not include hours - * - * @param {string} pattern The current formatPattern - * @returns {string} - */ - normalizePattern(pattern) { - const hasHours = !!pattern.match(/H/i); - const fallback = !pattern || !hasHours; - - const localeData = getCachedLocaleDataInstance(getLocale()); - return fallback ? localeData.getCombinedDateTimePattern("medium", "medium", this._primaryCalendarType) : pattern; - } - - /** - * Expands the "hours" time slider. - */ - expandHoursSlider() { - this._hoursCollapsed = false; - } - - /** - * Collapses the "hours" time slider. + * @override */ - collapseHoursSlider() { - this._hoursCollapsed = true; - } - - async getHoursSlider() { - return (await this.getPicker()).querySelector(".ui5-dt-hours-wheel"); - } - - async getMinutesSlider() { - return (await this.getPicker()).querySelector(".ui5-dt-minutes-wheel"); - } + _modifyDateValue(amount, unit) { + if (!this.dateValue) { + return; + } - async getSecondsSlider() { - return (await this.getPicker()).querySelector(".ui5-dt-seconds-wheel"); - } + const modifiedDate = modifyDateBy(CalendarDate.fromLocalJSDate(this.dateValue), amount, unit, this._minDate, this._maxDate); + const modifiedLocalDate = modifiedDate.toLocalJSDate(); + modifiedLocalDate.setHours(this.dateValue.getHours()); + modifiedLocalDate.setMinutes(this.dateValue.getMinutes()); + modifiedLocalDate.setSeconds(this.dateValue.getSeconds()); - async getPeriodsSlider() { - return (await this.getPicker()).querySelector(".ui5-dt-periods-wheel"); + const newValue = this.formatValue(modifiedLocalDate); + this._updateValueAndFireEvents(newValue, true, ["change", "value-changed"]); } async getPicker() { @@ -571,177 +413,15 @@ class DateTimePicker extends DatePicker { return staticAreaItem.querySelector("[ui5-responsive-popover]"); } - async getCurrentDateTime() { - // the time set in the timepicker - const selectedTime = new Date(); - const timeValues = await this.getTimePickerValues(); - - selectedTime.setHours(timeValues.hours); - selectedTime.setMinutes(timeValues.minutes); - selectedTime.setSeconds(timeValues.seconds); - - // the date set in the calendar - const currentCalendarValue = this.getFormat().format( - new Date(CalendarDate.fromTimestamp( - this._calTimestamp * 1000, - this._primaryCalendarType - ).valueOf()), - true - ); - - // merge both the date and time - const selectedDate = this.getFormat().parse(currentCalendarValue) || selectedTime; + getSelectedDateTime() { + const selectedDate = CalendarDate.fromTimestamp(this._effectiveCalendarSelectedDates[0] * 1000).toLocalJSDate(); + const selectedTime = this.getFormat().parse(this._effectiveTimeValue); selectedDate.setHours(selectedTime.getHours()); selectedDate.setMinutes(selectedTime.getMinutes()); selectedDate.setSeconds(selectedTime.getSeconds()); return selectedDate; } - - async getTimePickerValues() { - const secondsSlider = await this.getSecondsSlider(); - const minutesSlider = await this.getMinutesSlider(); - const hoursSlider = await this.getHoursSlider(); - const periodsSlider = await this.getPeriodsSlider(); - - let hours = hoursSlider ? hoursSlider.value : this._hoursConfig.minHour.toString(); - const minutes = minutesSlider ? minutesSlider.value : "0"; - const seconds = secondsSlider ? secondsSlider.value : "0"; - const period = periodsSlider ? periodsSlider.value : this.periodsArray[0]; - - if (period === this.periodsArray[0]) { // AM - hours = hours === "12" ? 0 : hours; - } - - if (period === this.periodsArray[1]) { // PM - hours = hours === "12" ? hours : hours * 1 + 12; - } - - return { - hours, - minutes, - seconds, - period, - }; - } - - /** - * Sets hours, minutes, seconds and period according to the current value - * or the current time if the value is not set. - */ - async setSlidersValue() { - const currentDate = this.value ? this.getFormat().parse(this.value) : new Date(); - - if (currentDate) { - await this.setHours(currentDate.getHours()); - await this.setMinutes(currentDate.getMinutes()); - await this.setSeconds(currentDate.getSeconds()); - await this.setPeriod(currentDate.getHours()); - } - } - - async setHours(value) { - let tempValue = ""; - const hoursSlider = await this.getHoursSlider(); - const config = this._hoursConfig; - - if (hoursSlider) { - if (config.isTwelveHoursFormat && value > config.maxHour) { - tempValue = value - 12; - } else if (config.isTwelveHoursFormat && value < config.minHour) { - tempValue = value + 12; - } else { - tempValue = value; - } - - hoursSlider.value = this.normalizeDigit(tempValue); - } - } - - async setMinutes(value) { - const minutesSlider = await this.getMinutesSlider(); - - if (minutesSlider) { - minutesSlider.value = this.normalizeDigit(value); - } - } - - async setSeconds(value) { - const secondsSlider = await this.getSecondsSlider(); - - if (secondsSlider) { - secondsSlider.value = this.normalizeDigit(value); - } - } - - async setPeriod(hours) { - const config = this._hoursConfig; - const periodsSlider = await this.getPeriodsSlider(); - - if (!periodsSlider) { - return; - } - - if (config.isTwelveHoursFormat) { - if (config.minHour === 1) { - periodsSlider.value = hours >= config.maxHour ? this.periodsArray[1] : this.periodsArray[0]; - } else { - periodsSlider.value = (hours > config.maxHour || hours === config.minHour) ? this.periodsArray[1] : this.periodsArray[0]; - } - } - } - - async _ontimekeydown(event) { - if (isLeft(event)) { - let expandedSliderIndex = 0; - for (let i = 0; i < this._slidersDomRefs.length; i++) { - if (this._slidersDomRefs[i]._expanded) { - expandedSliderIndex = i; - } - } - if (this._slidersDomRefs[expandedSliderIndex - 1]) { - this._slidersDomRefs[expandedSliderIndex - 1].focus(); - } else { - this._slidersDomRefs[this._slidersDomRefs.length - 1].focus(); - } - } else if (isRight(event)) { - let expandedSliderIndex = 0; - - for (let i = 0; i < this._slidersDomRefs.length; i++) { - if (this._slidersDomRefs[i]._expanded) { - expandedSliderIndex = i; - } - } - if (this._slidersDomRefs[expandedSliderIndex + 1]) { - this._slidersDomRefs[expandedSliderIndex + 1].focus(); - } else { - this._slidersDomRefs[0].focus(); - } - } - } - - normalizeDigit(value) { - const valueAsString = value.toString(); - return valueAsString.length === 1 ? `0${value}` : valueAsString; - } - - isTimeControlContained() { - const format = this.getFormat().aFormatArray; - return getTimeControlsByFormat(format, this._hoursConfig); - } - - updateHoursFormatConfig() { - const formatArray = this.getFormat().aFormatArray; - - if (formatArray.length < 7) { - return; // does not contain time data - } - - const config = getHoursConfigByFormat(formatArray[6].type); - this._hoursConfig.minHour = config.minHour; - this._hoursConfig.maxHour = config.maxHour; - this._hoursConfig.isTwelveHoursFormat = config.isTwelveHoursFormat; - } } DateTimePicker.define(); diff --git a/packages/main/src/DateTimePickerPopover.hbs b/packages/main/src/DateTimePickerPopover.hbs index 0150ebf5537a..1bd535a52a29 100644 --- a/packages/main/src/DateTimePickerPopover.hbs +++ b/packages/main/src/DateTimePickerPopover.hbs @@ -17,61 +17,31 @@ - + primary-calendar-type="{{_primaryCalendarType}}" + format-pattern="{{_formatPattern}}" + timestamp="{{_effectiveCalendarTimestamp}}" + .selectedDates={{_effectiveCalendarSelectedDates}} + .selectionMode="{{_calendarSelectionMode}}" + .minDate="{{minDate}}" + .maxDate="{{maxDate}}" + @ui5-selected-dates-change="{{onSelectedDatesChange}}" + ?hide-week-numbers="{{hideWeekNumbers}}" + ._currentPicker="{{_calendarCurrentPicker}}" + > {{#unless phone}} {{/unless}} -
      - {{#if shouldBuildHoursSlider}} - - {{/if}} - - {{#if shouldBuildMinutesSlider}} - - {{/if}} - - {{#if shouldBuildSecondsSlider}} - - {{/if}} - - {{#if shouldBuildPeriodsSlider}} - - {{/if}} -
      +
    {{/inline}} @@ -81,6 +51,7 @@ id="ok" class="ui5-dt-picker-action" design="Emphasized" + ?disabled="{{_submitDisabled}}" @click="{{_submitClick}}" > {{btnOKLabel}} diff --git a/packages/main/src/DayPicker.hbs b/packages/main/src/DayPicker.hbs index bd2c743d5ca8..f44eb8bb14b2 100644 --- a/packages/main/src/DayPicker.hbs +++ b/packages/main/src/DayPicker.hbs @@ -3,41 +3,41 @@ style="{{styles.wrapper}}" @keydown={{_onkeydown}} @keyup={{_onkeyup}} - @mousedown={{_onmousedown}} - @mouseup={{_onmouseup}} + @click={{_onclick}} + @mouseover={{_onmouseover}} + @focusin={{_onfocusin}} > - +
    {{#each _dayNames}}
    + class="{{this.classes}}" + > {{this.ultraShortName}}
    {{/each}}
    {{#each _weeks}} {{#if this.length}} -
    +
    {{#each this}} {{#if this.timestamp}}
    + > {{this.iDay}}
    diff --git a/packages/main/src/DayPicker.js b/packages/main/src/DayPicker.js index f69e7a4a3f95..2657a21fbad9 100644 --- a/packages/main/src/DayPicker.js +++ b/packages/main/src/DayPicker.js @@ -1,14 +1,17 @@ import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; import { getFirstDayOfWeek } from "@ui5/webcomponents-base/dist/config/FormatSettings.js"; import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; -import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js"; import { isSpace, + isSpaceShift, isEnter, + isEnterShift, isUp, isDown, isLeft, isRight, + isHome, + isEnd, isHomeCtrl, isEndCtrl, isPageUp, @@ -21,31 +24,18 @@ import { import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import calculateWeekNumber from "@ui5/webcomponents-localization/dist/dates/calculateWeekNumber.js"; import CalendarType from "@ui5/webcomponents-base/dist/types/CalendarType.js"; -import ItemNavigationBehavior from "@ui5/webcomponents-base/dist/types/ItemNavigationBehavior.js"; -import RenderScheduler from "@ui5/webcomponents-base/dist/RenderScheduler.js"; -import CalendarSelection from "@ui5/webcomponents-base/dist/types/CalendarSelection.js"; -import PickerBase from "./PickerBase.js"; +import CalendarSelectionMode from "./types/CalendarSelectionMode.js"; +import CalendarPart from "./CalendarPart.js"; import DayPickerTemplate from "./generated/templates/DayPickerTemplate.lit.js"; import { DAY_PICKER_WEEK_NUMBER_TEXT, DAY_PICKER_NON_WORKING_DAY, + DAY_PICKER_TODAY, } from "./generated/i18n/i18n-defaults.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,21 +43,20 @@ const metadata = { tag: "ui5-daypicker", properties: /** @lends sap.ui.webcomponents.main.DayPicker.prototype */ { /** - * Defines the type of selection used in the calendar component. - * The property takes as value an object of type CalendarSelection. + * Defines the type of selection used in the day picker component. * Accepted property values are:
    *
      - *
    • CalendarSelection.Single - enables a single date selection.(default value)
    • - *
    • CalendarSelection.Range - enables selection of a date range.
    • - *
    • CalendarSelection.Multiple - enables selection of multiple dates.
    • + *
    • CalendarSelectionMode.Single - enables a single date selection.(default value)
    • + *
    • CalendarSelectionMode.Range - enables selection of a date range.
    • + *
    • CalendarSelectionMode.Multiple - enables selection of multiple dates.
    • *
    - * @type {CalendarSelection} + * @type {CalendarSelectionMode} * @defaultvalue "Single" * @public */ - selection: { - type: CalendarSelection, - defaultValue: CalendarSelection.Single, + selectionMode: { + type: CalendarSelectionMode, + defaultValue: CalendarSelectionMode.Single, }, /** @@ -86,16 +75,6 @@ const metadata = { type: Boolean, }, - /** - * Defines the effective weeks numbers visibility, - * based on the primaryCalendarType and hideWeekNumbers property. - * @type {boolean} - * @private - */ - _hideWeekNumbers: { - type: Boolean, - }, - /** * @type {Object} * @private @@ -105,7 +84,13 @@ const metadata = { multiple: true, }, + _dayNames: { + type: Object, + multiple: true, + }, + /** + * When set, the component will skip all work in onBeforeRendering and will not automatically set the focus on itself * @type {boolean} * @private */ @@ -113,16 +98,24 @@ const metadata = { type: Boolean, noAttribute: true, }, + + /** + * When selectionMode="Range" and the first day in the range is selected, this is the currently hovered (when using mouse) or focused (when using keyboard) day by the user + * @private + */ + _secondTimestamp: { + type: String, + }, }, events: /** @lends sap.ui.webcomponents.main.DayPicker.prototype */ { /** - * Fired when the user selects a new Date on the Web Component. + * Fired when the selected date(s) change * @public * @event */ change: {}, /** - * Fired when month, year has changed due to item navigation. + * Fired when the timestamp changes (user navigates with the keyboard) or clicks with the mouse * @public * @event */ @@ -130,6 +123,10 @@ const metadata = { }, }; +const isBetween = (x, num1, num2) => x > Math.min(num1, num2) && x < Math.max(num1, num2); + +const DAYS_IN_WEEK = 7; + /** * @class * @@ -138,11 +135,11 @@ const metadata = { * @constructor * @author SAP SE * @alias sap.ui.webcomponents.main.DayPicker - * @extends sap.ui.webcomponents.main.PickerBase + * @extends CalendarPart * @tagname ui5-daypicker * @public */ -class DayPicker extends PickerBase { +class DayPicker extends CalendarPart { static get metadata() { return metadata; } @@ -155,152 +152,142 @@ class DayPicker extends PickerBase { return dayPickerCSS; } - constructor() { - super(); - this._itemNav = new ItemNavigation(this, { - rowSize: 7, - pageSize: 42, - behavior: ItemNavigationBehavior.Paging, - affectedPropertiesNames: ["_weeks"], - getItemsCallback: () => this.focusableDays, - hasNextPageCallback: this._hasNextMonth.bind(this), - hasPreviousPageCallback: this._hasPrevMonth.bind(this), - }); - - this._itemNav.attachEvent( - ItemNavigation.BORDER_REACH, - this._handleItemNavigationBorderReach.bind(this) - ); - - this._itemNav.attachEvent( - ItemNavigation.AFTER_FOCUS, - this._handleItemNavigationAfterFocus.bind(this) - ); - } - onBeforeRendering() { const localeData = getCachedLocaleDataInstance(getLocale()); + this._buildWeeks(localeData); + this._buildDayNames(localeData); + } - let oCalDate, - day, - timestamp, - lastWeekNumber = -1, - isDaySelected = false, - todayIndex = 0; + /** + * Builds the _weeks object that represents the month + * @param localeData + * @private + */ + _buildWeeks(localeData) { + if (this._hidden) { + return; // Optimization to not do any work unless the current picker + } - const _aVisibleDays = this._getVisibleDays(this._calendarDate); this._weeks = []; - let week = []; - this._weekNumbers = []; - let weekday; - const _monthsNameWide = localeData.getMonths("wide", this._calendarDate._oUDate.sCalendarType); - const visualizedSelectedDates = this._getVisualizedSelectedDates(); + const firstDayOfWeek = this._getFirstDayOfWeek(); + const monthsNames = localeData.getMonths("wide", this._primaryCalendarType); + const nonWorkingDayLabel = this.i18nBundle.getText(DAY_PICKER_NON_WORKING_DAY); + const todayLabel = this.i18nBundle.getText(DAY_PICKER_TODAY); + const tempDate = this._getFirstDay(); // date that will be changed by 1 day 42 times + const todayDate = CalendarDate.fromLocalJSDate(new Date(), this._primaryCalendarType); // current day date - calculate once + const calendarDate = this._calendarDate; // store the _calendarDate value as this getter is expensive and degrades IE11 perf + const minDate = this._minDate; // store the _minDate (expensive getter) + const maxDate = this._maxDate; // store the _maxDate (expensive getter) - /* eslint-disable no-loop-func */ - for (let i = 0; i < _aVisibleDays.length; i++) { - oCalDate = _aVisibleDays[i]; - timestamp = oCalDate.valueOf() / 1000; // no need to round because CalendarDate does it + let week = []; + for (let i = 0; i < DAYS_IN_WEEK * 6; i++) { // always show 6 weeks total, 42 days to avoid jumping + const timestamp = tempDate.valueOf() / 1000; // no need to round because CalendarDate does it - // day of the week - weekday = oCalDate.getDay() - this._getFirstDayOfWeek(); - if (weekday < 0) { - weekday += 7; + let dayOfTheWeek = tempDate.getDay() - firstDayOfWeek; + if (dayOfTheWeek < 0) { + dayOfTheWeek += DAYS_IN_WEEK; } - const nonWorkingAriaLabel = this._isWeekend(oCalDate) ? `${this._dayPickerNonWorkingDay} ` : ""; + const isFocused = tempDate.getMonth() === calendarDate.getMonth() && tempDate.getDate() === calendarDate.getDate(); + const isSelected = this._isDaySelected(timestamp); + const isSelectedBetween = this._isDayInsideSelectionRange(timestamp); + const isOtherMonth = tempDate.getMonth() !== calendarDate.getMonth(); + const isWeekend = this._isWeekend(tempDate); + const isDisabled = tempDate.valueOf() < minDate.valueOf() || tempDate.valueOf() > maxDate.valueOf(); + const isToday = tempDate.isSame(todayDate); + const isFirstDayOfWeek = tempDate.getDay() === firstDayOfWeek; + + const nonWorkingAriaLabel = isWeekend ? `${nonWorkingDayLabel} ` : ""; + const todayAriaLabel = isToday ? `${todayLabel} ` : ""; - day = { + const day = { timestamp: timestamp.toString(), - selected: visualizedSelectedDates.some(d => { - return d === timestamp; - }), - iDay: oCalDate.getDate(), - _index: i.toString(), - classes: `ui5-dp-item ui5-dp-wday${weekday}`, - ariaLabel: `${nonWorkingAriaLabel}${_monthsNameWide[oCalDate.getMonth()]} ${oCalDate.getDate()}, ${oCalDate.getYear()}`, + focusRef: isFocused, + _tabIndex: isFocused ? "0" : "-1", + selected: isSelected, + iDay: tempDate.getDate(), + classes: `ui5-dp-item ui5-dp-wday${dayOfTheWeek}`, + ariaLabel: `${todayAriaLabel}${nonWorkingAriaLabel}${monthsNames[tempDate.getMonth()]} ${tempDate.getDate()}, ${tempDate.getYear()}`, + ariaSelected: isSelected ? "true" : "false", + ariaDisabled: isOtherMonth ? "true" : undefined, + disabled: isDisabled, }; - const isToday = oCalDate.isSame(CalendarDate.fromLocalJSDate(new Date(), this._primaryCalendarType)); - - week.push(day); - - if (oCalDate.getDay() === this._getFirstDayOfWeek()) { + if (isFirstDayOfWeek) { day.classes += " ui5-dp-firstday"; } - if (day.selected) { + if (isSelected) { day.classes += " ui5-dp-item--selected"; - isDaySelected = true; + } + + if (isSelectedBetween) { + day.classes += " ui5-dp-item--selected-between"; } if (isToday) { day.classes += " ui5-dp-item--now"; - todayIndex = i; - day.ariaLabel = `today ${day.ariaLabel}`; } - if (oCalDate.getMonth() !== this._month) { + if (isOtherMonth) { day.classes += " ui5-dp-item--othermonth"; - day.ariaDisabled = "true"; } - day.id = `${this._id}-${timestamp}`; - - if (this._isWeekend(oCalDate)) { + if (isWeekend) { day.classes += " ui5-dp-item--weeekend"; } - if (this._isOutOfSelectableRange(oCalDate)) { + if (isDisabled) { day.classes += " ui5-dp-item--disabled"; - day.disabled = true; } - this._hideWeekNumbers = this.shouldHideWeekNumbers; - - if (day.classes.indexOf("ui5-dp-wday6") !== -1 - || _aVisibleDays.length - 1 === i) { - const weekNumber = calculateWeekNumber(getFirstDayOfWeek(), oCalDate.toUTCJSDate(), oCalDate.getYear(), getLocale(), localeData); - if (lastWeekNumber !== weekNumber) { - const weekNum = { - weekNum: weekNumber, - isHidden: this._hideWeekNumbers, - }; - week.unshift(weekNum); - lastWeekNumber = weekNumber; - } + week.push(day); + if (dayOfTheWeek === DAYS_IN_WEEK - 1) { // 0-indexed so 6 is the last day of the week + week.unshift({ + weekNum: calculateWeekNumber(getFirstDayOfWeek(), tempDate.toUTCJSDate(), tempDate.getYear(), getLocale(), localeData), + isHidden: this.shouldHideWeekNumbers, + }); + } + + if (week.length === DAYS_IN_WEEK + 1) { // 7 entries for each day + 1 for the week numbers this._weeks.push(week); week = []; } + + tempDate.setDate(tempDate.getDate() + 1); } - while (this._weeks.length < 6) { - this._weeks.push([]); - } - /* eslint-enable no-loop-func */ + } - if (!isDaySelected && todayIndex && this._itemNav.current === 0) { - this._itemNav.current = todayIndex; + /** + * Builds the dayNames object (header of the month) + * @param localeData + * @private + */ + _buildDayNames(localeData) { + if (this._hidden) { + return; // Optimization to not do any work unless the current picker } + let dayOfTheWeek; + const aDayNamesWide = localeData.getDays("wide", this._primaryCalendarType); const aDayNamesAbbreviated = localeData.getDays("abbreviated", this._primaryCalendarType); - const aUltraShortNames = aDayNamesAbbreviated.map(n => n); let dayName; this._dayNames = []; this._dayNames.push({ classes: "ui5-dp-dayname", - name: this._dayPickerWeekNumberText, + name: this.i18nBundle.getText(DAY_PICKER_WEEK_NUMBER_TEXT), }); - for (let i = 0; i < 7; i++) { - weekday = i + this._getFirstDayOfWeek(); - if (weekday > 6) { - weekday -= 7; + for (let i = 0; i < DAYS_IN_WEEK; i++) { + dayOfTheWeek = i + this._getFirstDayOfWeek(); + if (dayOfTheWeek > DAYS_IN_WEEK - 1) { // 0-indexed so index of 6 is the maximum allowed + dayOfTheWeek -= DAYS_IN_WEEK; } dayName = { - id: `${this._id}-WH${i.toString()}`, - name: aDayNamesWide[weekday], - ultraShortName: aUltraShortNames[weekday], + name: aDayNamesWide[dayOfTheWeek], + ultraShortName: aDayNamesAbbreviated[dayOfTheWeek], classes: "ui5-dp-dayname", }; @@ -311,387 +298,350 @@ class DayPicker extends PickerBase { } onAfterRendering() { - const visualizedDates = this._getVisualizedSelectedDates(); - if (this.selection === CalendarSelection.Range && visualizedDates.length > 0) { - const dayItems = this.getDomRef().querySelectorAll(".ui5-dp-item"); - const firstTimestamp = this.selectedDates[0]; - const lastTimestamp = (visualizedDates.length === 1) ? parseInt(dayItems[this._itemNav.currentIndex].dataset.sapTimestamp) : this.selectedDates[1]; - - this._updateSelectionBetween(dayItems, firstTimestamp, lastTimestamp); + if (this._autoFocus && !this._hidden) { + this.focus(); } } - _getVisualizedSelectedDates() { - switch (this.selection) { - case CalendarSelection.Single: - return [this.selectedDates[0]]; - case CalendarSelection.Multiple: - return [...this.selectedDates]; - case CalendarSelection.Range: - return this.selectedDates.slice(0, 2); - default: - return []; - } + _onfocusin() { + this._autoFocus = true; } - _onmousedown(event) { - const target = event.target; - const dayPressed = this._isDayPressed(target); - - if (dayPressed) { - const targetDate = this.getTimestampFromDom(target); - const selectedDay = this.focusableDays.find(day => parseInt(day.timestamp) === targetDate); - this._itemNav.update(selectedDay); - - this.targetDate = targetDate; + /** + * Tells if the day is selected (dark blue) + * @param timestamp + * @returns {boolean} + * @private + */ + _isDaySelected(timestamp) { + if (this.selectionMode === CalendarSelectionMode.Single) { + return timestamp === this.selectedDates[0]; } + + // Multiple, Range + return this.selectedDates.includes(timestamp); } - _onmouseup(event) { - const dayPressed = this._isDayPressed(event.target); - if (this.targetDate) { - this._modifySelectionAndNotifySubscribers(this.targetDate); - this.targetDate = null; + /** + * Tells if the day is inside a selection range (light blue) + * @param timestamp + * @returns {*} + * @private + */ + _isDayInsideSelectionRange(timestamp) { + // No selection at all (or not in range selection mode) + if (this.selectionMode !== CalendarSelectionMode.Range || !this.selectedDates.length) { + return false; } - if (!dayPressed) { - this._itemNav.focusCurrent(); + // Only one date selected - the user is hovering with the mouse or navigating with the keyboard to select the second one + if (this.selectedDates.length === 1 && this._secondTimestamp) { + return isBetween(timestamp, this.selectedDates[0], this._secondTimestamp); } - } - _onitemmouseover(event) { - const hoveredItem = event.target.classList.contains("ui5-dp-item") ? event.target : event.target.parentElement; - if (this.selectedDates.length === 1 && this.selection === CalendarSelection.Range && hoveredItem.classList.contains("ui5-dp-item")) { - const dayItems = this.getDomRef().querySelectorAll(".ui5-dp-item"); - const firstTimestamp = this.selectedDates[0]; - const lastTimestamp = parseInt(hoveredItem.dataset.sapTimestamp); - - this._updateSelectionBetween(dayItems, firstTimestamp, lastTimestamp); - } + // Two dates selected - stable range + return isBetween(timestamp, this.selectedDates[0], this.selectedDates[1]); } - _updateSelectionBetween(dayItems, firstTimestamp, lastTimestamp) { - dayItems.forEach(day => { - const dayTimestamp = parseInt(day.dataset.sapTimestamp); - - if ((dayTimestamp > firstTimestamp && dayTimestamp < lastTimestamp) || (dayTimestamp > lastTimestamp && dayTimestamp < firstTimestamp)) { - day.classList.add("ui5-dp-item--selected-between"); - } else { - day.classList.remove("ui5-dp-item--selected-between"); - } - }); - } - - _onkeydown(event) { - if (isEnter(event)) { - return this._handleEnter(event); - } + /** + * Selects/deselects a day + * @param event + * @param isShift true if the user did Click+Shift or Enter+Shift (but not Space+Shift) + * @private + */ + _selectDate(event, isShift) { + const target = event.target; - if (isSpace(event)) { - event.preventDefault(); + if (!this._isDayPressed(target)) { return; } - if (isHomeCtrl(event)) { - this._navToStartEndDayOfTheMonth(event, true); - } + const timestamp = this._getTimestampFromDom(target); - if (isEndCtrl(event)) { - this._navToStartEndDayOfTheMonth(event, false); - } + this._safelySetTimestamp(timestamp); + this._updateSecondTimestamp(); - if (isPageUpShift(event)) { - this._changeYears(event, false, 1); + if (this.selectionMode === CalendarSelectionMode.Single) { + this.selectedDates = [timestamp]; + } else if (this.selectionMode === CalendarSelectionMode.Multiple) { + if (this.selectedDates.length > 0 && isShift) { + this._multipleSelection(timestamp); + } else { + this._toggleTimestampInSelection(timestamp); + } + } else { + this.selectedDates = (this.selectedDates.length === 1) ? [...this.selectedDates, timestamp] : [timestamp]; } - if (isPageUpShiftCtrl(event)) { - this._changeYears(event, false, 10); - } + this.fireEvent("change", { + timestamp: this.timestamp, + dates: this.selectedDates, + }); + } - if (isPageDownShift(event)) { - this._changeYears(event, true, 1); - } + /** + * Selects/deselects the whole row (week) + * @param event + * @private + */ + _selectWeek(event) { + this._weeks.forEach(week => { + const dayInThisWeek = week.findIndex(item => { + const date = CalendarDate.fromTimestamp(parseInt(item.timestamp) * 1000); + return date.getMonth() === this._calendarDate.getMonth() && date.getDate() === this._calendarDate.getDate(); + }) !== -1; + if (dayInThisWeek) { // The current day is in this week + const notAllDaysOfThisWeekSelected = week.some(item => item.timestamp && !this.selectedDates.includes(parseInt(item.timestamp))); + if (notAllDaysOfThisWeekSelected) { // even if one day is not selected, select the whole week + week.filter(item => item.timestamp).forEach(item => { + this._addTimestampToSelection(parseInt(item.timestamp)); + }); + } else { // only if all days of this week are selected, deselect them + week.filter(item => item.timestamp).forEach(item => { + this._removeTimestampFromSelection(parseInt(item.timestamp)); + }); + } + } + }); - if (isPageDownShiftCtrl(event)) { - this._changeYears(event, true, 10); - } + this.fireEvent("change", { + timestamp: this.timestamp, + dates: this.selectedDates, + }); } - _onkeyup(event) { - if (isSpace(event)) { - this._handleSpace(event); + _toggleTimestampInSelection(timestamp) { + if (this.selectedDates.includes(timestamp)) { + this._removeTimestampFromSelection(timestamp); + } else { + this._addTimestampToSelection(timestamp); } } - _handleEnter(event) { - event.preventDefault(); - if (event.target.className.indexOf("ui5-dp-item") > -1) { - const targetDate = parseInt(event.target.getAttribute("data-sap-timestamp")); - this._modifySelectionAndNotifySubscribers(targetDate); - } - } - _handleSpace(event) { - event.preventDefault(); - if (event.target.className.indexOf("ui5-dp-item") > -1) { - const targetDate = parseInt(event.target.getAttribute("data-sap-timestamp")); - this._modifySelectionAndNotifySubscribers(targetDate); + _addTimestampToSelection(timestamp) { + if (!this.selectedDates.includes(timestamp)) { + this.selectedDates = [...this.selectedDates, timestamp]; } } - _navToStartEndDayOfTheMonth(event, start) { - event.preventDefault(); - - const currentItem = this._itemNav._getCurrentItem(); - let currentTimestamp = parseInt(currentItem.getAttribute("data-sap-timestamp")) * 1000; - let calDate = CalendarDate.fromTimestamp(currentTimestamp, this._primaryCalendarType); + _removeTimestampFromSelection(timestamp) { + this.selectedDates = this.selectedDates.filter(value => value !== timestamp); + } - if (currentItem.classList.contains("ui5-dp-item--othermonth")) { - return; + /** + * When at least one day is selected and the user pressed shift + * @param timestamp + * @private + */ + _multipleSelection(timestamp) { + const min = Math.min(...this.selectedDates); + const max = Math.max(...this.selectedDates); + let start; + let end; + let toggle = false; + + if (timestamp < min) { + start = timestamp; + end = min; + } else if (timestamp >= min && timestamp <= max) { // inside the current range - toggle all between the selected and focused + const distanceToMin = Math.abs(timestamp - min); + const distanceToMax = Math.abs(timestamp - max); + + if (distanceToMin < distanceToMax) { + start = timestamp; + end = max; + } else { + start = min; + end = timestamp; + } + toggle = true; + } else { + start = max; + end = timestamp; } - calDate.setDate(1); - if (!start) { - // set the day to be the last day of the current month - calDate.setMonth(calDate.getMonth() + 1, 0); - } + const startDate = CalendarDate.fromTimestamp(start * 1000); + const endDate = CalendarDate.fromTimestamp(end * 1000); - if (calDate.valueOf() < this._minDate) { - calDate = CalendarDate.fromLocalJSDate(new Date(this._minDate), this._primaryCalendarType); - } else if (calDate.valueOf() > this._maxDate) { - calDate = CalendarDate.fromLocalJSDate(new Date(this._maxDate), this._primaryCalendarType); + while (startDate.valueOf() <= endDate.valueOf()) { + this[toggle ? "_toggleTimestampInSelection" : "_addTimestampToSelection"](startDate.valueOf() / 1000); + startDate.setDate(startDate.getDate() + 1); } - - currentTimestamp = calDate.valueOf() / 1000; - const newItemIndex = this.focusableDays.findIndex(item => parseInt(item.timestamp) === currentTimestamp); - - this._itemNav.currentIndex = newItemIndex; - this._itemNav.focusCurrent(); } + /** - * Converts "timestamp" property value into a Java Script Date object and - * adds or extracts a given number of years from it - * - * @param {object} event used to prevent the default browser behavior - * @param {boolean} forward if true indicates addition - * @param {int} step for year number to substract or add + * Set the hovered day as the _secondTimestamp + * @param event + * @private */ - _changeYears(event, forward, step) { - const currentItem = this._itemNav._getCurrentItem(); - let currentTimestamp = parseInt(currentItem.getAttribute("data-sap-timestamp") * 1000); - const currentDate = CalendarDate.fromTimestamp(currentTimestamp, this._primaryCalendarType); - let newDate = new CalendarDate(currentDate, this._primaryCalendarType); - - if (forward) { - newDate.setYear(newDate.getYear() + step); - } else { - newDate.setYear(newDate.getYear() - step); + _onmouseover(event) { + const hoveredItem = event.target.closest(".ui5-dp-item"); + if (hoveredItem && this.selectionMode === CalendarSelectionMode.Range && this.selectedDates.length === 1) { + this._secondTimestamp = this._getTimestampFromDom(hoveredItem); } + } - if (currentDate.getMonth() !== newDate.getMonth()) { - newDate.setDate(0); - } + _onkeydown(event) { + let preventDefault = true; - if (newDate.valueOf() < this._minDate) { - newDate = CalendarDate.fromLocalJSDate(new Date(this._minDate), this._primaryCalendarType); - } else if (newDate.valueOf() > this._maxDate) { - newDate = CalendarDate.fromLocalJSDate(new Date(this._maxDate), this._primaryCalendarType); + if (isEnter(event) || isEnterShift(event)) { + this._selectDate(event, isEnterShift(event)); + } else if (isSpace(event) || isSpaceShift(event)) { + event.preventDefault(); + } else if (isLeft(event)) { + this._modifyTimestampBy(-1, "day"); + } else if (isRight(event)) { + this._modifyTimestampBy(1, "day"); + } else if (isUp(event)) { + this._modifyTimestampBy(-7, "day"); + } else if (isDown(event)) { + this._modifyTimestampBy(7, "day"); + } else if (isPageUp(event)) { + this._modifyTimestampBy(-1, "month"); + } else if (isPageDown(event)) { + this._modifyTimestampBy(1, "month"); + } else if (isPageUpShift(event)) { + this._modifyTimestampBy(-1, "year"); + } else if (isPageDownShift(event)) { + this._modifyTimestampBy(1, "year"); + } else if (isPageUpShiftCtrl(event)) { + this._modifyTimestampBy(-10, "year"); + } else if (isPageDownShiftCtrl(event)) { + this._modifyTimestampBy(10, "year"); + } else if (isHome(event) || isEnd(event)) { + this._onHomeOrEnd(isHome(event)); + } else if (isHomeCtrl(event)) { + const tempDate = new CalendarDate(this._calendarDate, this._primaryCalendarType); + tempDate.setDate(1); // Set the first day of the month + this._setTimestamp(tempDate.valueOf() / 1000); + } else if (isEndCtrl(event)) { + const tempDate = new CalendarDate(this._calendarDate, this._primaryCalendarType); + tempDate.setMonth(tempDate.getMonth() + 1); + tempDate.setDate(0); // Set the last day of the month (0th day of next month) + this._setTimestamp(tempDate.valueOf() / 1000); + } else { + preventDefault = false; } - currentTimestamp = (newDate.valueOf() / 1000); - - this._navigateAndWaitRerender(currentTimestamp); - - event.preventDefault(); + if (preventDefault) { + event.preventDefault(); + } } - get shouldHideWeekNumbers() { - if (this._primaryCalendarType !== CalendarType.Gregorian) { - return true; + _onkeyup(event) { + // Even if Space+Shift was pressed, ignore the shift unless in Multiple selection + if (isSpace(event) || (isSpaceShift(event) && this.selectionMode !== CalendarSelectionMode.Multiple)) { + this._selectDate(event, false); + } else if (isSpaceShift(event)) { + this._selectWeek(event); } - - return this.hideWeekNumbers; } - get _currentCalendarDate() { - return CalendarDate.fromTimestamp(new Date().getTime(), this._primaryCalendarType); + /** + * Click is the same as Enter: Click+Shift has the same effect as Enter+Shift + * @param event + * @private + */ + _onclick(event) { + this._selectDate(event, event.shiftKey); } - get focusableDays() { - const focusableDays = []; - - for (let i = 0; i < this._weeks.length; i++) { - const week = this._weeks[i].slice(1).filter(dayItem => !dayItem.disabled); - focusableDays.push(week); - } - - return [].concat(...focusableDays); + /** + * One Home or End, move the focus to the first or last item in the row + * @param homePressed + * @private + */ + _onHomeOrEnd(homePressed) { + this._weeks.forEach(week => { + const dayInThisWeek = week.findIndex(item => { + const date = CalendarDate.fromTimestamp(parseInt(item.timestamp) * 1000); + return date.getMonth() === this._calendarDate.getMonth() && date.getDate() === this._calendarDate.getDate(); + }) !== -1; + if (dayInThisWeek) { // The current day is in this week + const index = homePressed ? 1 : 7; // select the first (if Home) or last (if End) day of the week + this._setTimestamp(parseInt(week[index].timestamp)); + } + }); } - get _dayPickerWeekNumberText() { - return this.i18nBundle.getText(DAY_PICKER_WEEK_NUMBER_TEXT); + /** + * Called from Calendar.js + * @protected + */ + _hasPreviousPage() { + return !(this._calendarDate.getMonth() === this._minDate.getMonth() && this._calendarDate.getYear() === this._minDate.getYear()); } - get _dayPickerNonWorkingDay() { - return this.i18nBundle.getText(DAY_PICKER_NON_WORKING_DAY); + /** + * Called from Calendar.js + * @protected + */ + _hasNextPage() { + return !(this._calendarDate.getMonth() === this._maxDate.getMonth() && this._calendarDate.getYear() === this._maxDate.getYear()); } - _setCurrentItemTabIndex(index) { - const currentItem = this._itemNav._getCurrentItem(); - if (currentItem) { - currentItem.setAttribute("tabindex", index.toString()); - } + /** + * Called from Calendar.js + * Same as PageUp + * @protected + */ + _showPreviousPage() { + this._modifyTimestampBy(-1, "month"); } - _modifySelectionAndNotifySubscribers(timestamp) { - if (this.selection === CalendarSelection.Single) { - this.selectedDates = [timestamp]; - } else if (this.selection === CalendarSelection.Multiple) { - this.selectedDates = this.selectedDates.includes(timestamp) ? this.selectedDates.filter(value => value !== timestamp) : [...this.selectedDates, timestamp]; - } else { - this.selectedDates = (this.selectedDates.length === 1) ? [...this.selectedDates, timestamp] : [timestamp]; - } - - this.fireEvent("change", { dates: [...this.selectedDates] }); + /** + * Called from Calendar.js + * Same as PageDown + * @protected + */ + _showNextPage() { + this._modifyTimestampBy(1, "month"); } - _hasNextMonth() { - let newMonth = this._month + 1; - let newYear = this._year; - const maxCalendarYear = CalendarDate.fromTimestamp(this._getMaxCalendarDate(), this._primaryCalendarType).getYear(); - - if (newMonth > 11) { - newMonth = 0; - newYear++; - } - - if (newYear > maxCalendarYear && 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; - } + /** + * Modifies the timestamp by a certain amount of days/months/years + * @param amount + * @param unit + * @private + */ + _modifyTimestampBy(amount, unit) { + // Modify the current timestamp + this._safelyModifyTimestampBy(amount, unit); + this._updateSecondTimestamp(); - return true; + // Notify the calendar to update its timestamp + this.fireEvent("navigate", { timestamp: this.timestamp }); } - _hasPrevMonth() { - let newMonth = this._month - 1; - let newYear = this._year; - const minCalendarYear = CalendarDate.fromTimestamp(this._getMinCalendarDate(), this._primaryCalendarType).getYear(); - - if (newMonth < 0) { - newMonth = 11; - newYear--; - } - - if (newYear < minCalendarYear && 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; + /** + * Sets the timestamp to an absolute value + * @param value + * @private + */ + _setTimestamp(value) { + this._safelySetTimestamp(value); + this._updateSecondTimestamp(); + this.fireEvent("navigate", { timestamp: this.timestamp }); } - _handleItemNavigationBorderReach(event) { - const currentItem = this._itemNav._getCurrentItem(); - let newDate; - let currentDate; - let currentTimestamp; - - if (isUp(event.originalEvent) || isLeft(event.originalEvent)) { - currentTimestamp = this._weeks[0][event.offset + 1].timestamp * 1000; - newDate = CalendarDate.fromTimestamp(currentTimestamp, this._primaryCalendarType); - newDate.setDate(newDate.getDate() - 7); - } - - if (isDown(event.originalEvent) || isRight(event.originalEvent)) { - currentTimestamp = this._weeks[this._weeks.length - 1][event.offset + 1].timestamp * 1000; - newDate = CalendarDate.fromTimestamp(currentTimestamp, this._primaryCalendarType); - newDate.setDate(newDate.getDate() + 7); - } - - if (isPageUp(event.originalEvent)) { - currentTimestamp = parseInt(currentItem.getAttribute("data-sap-timestamp") * 1000); - currentDate = CalendarDate.fromTimestamp(currentTimestamp, this._primaryCalendarType); - newDate = new CalendarDate(currentDate, this._primaryCalendarType); - newDate.setMonth(newDate.getMonth() - 1); - if (currentDate.getMonth() === newDate.getMonth()) { - newDate.setDate(0); - } - } - - if (isPageDown(event.originalEvent)) { - currentTimestamp = parseInt(currentItem.getAttribute("data-sap-timestamp") * 1000); - currentDate = CalendarDate.fromTimestamp(currentTimestamp, this._primaryCalendarType); - newDate = new CalendarDate(currentDate, this._primaryCalendarType); - newDate.setMonth(newDate.getMonth() + 1); - if (newDate.getMonth() - currentDate.getMonth() > 1) { - newDate.setDate(0); - } - } - - if (!newDate) { - return; - } - - if (newDate.valueOf() < this._minDate) { - newDate = CalendarDate.fromLocalJSDate(new Date(this._minDate), this._primaryCalendarType); - } else if (newDate.valueOf() > this._maxDate) { - newDate = CalendarDate.fromLocalJSDate(new Date(this._maxDate), this._primaryCalendarType); + /** + * During range selection, when the user is navigating with the keyboard, the currently focused day is considered the "second day" + * @private + */ + _updateSecondTimestamp() { + if (this.selectionMode === CalendarSelectionMode.Range && this.selectedDates.length === 1) { + this._secondTimestamp = this.timestamp; } - - currentTimestamp = (newDate.valueOf() / 1000); - - this._navigateAndWaitRerender(currentTimestamp); } - _handleItemNavigationAfterFocus() { - const currentItem = this._itemNav._getCurrentItem(); - const currentTimestamp = parseInt(currentItem.getAttribute("data-sap-timestamp")); - - if (currentItem.classList.contains("ui5-dp-item--othermonth")) { - this._navigateAndWaitRerender(currentTimestamp); + get shouldHideWeekNumbers() { + if (this._primaryCalendarType !== CalendarType.Gregorian) { + return true; } - } - - async _navigateAndWaitRerender(timestamp) { - this.fireEvent("navigate", { timestamp }); - await RenderScheduler.whenFinished(); - const newItemIndex = this.focusableDays.findIndex(item => parseInt(item.timestamp) === timestamp); - this._itemNav.currentIndex = newItemIndex; - this._itemNav.focusCurrent(); + return this.hideWeekNumbers; } _isWeekend(oDate) { @@ -710,56 +660,24 @@ class DayPicker extends PickerBase { return (target.className.indexOf("ui5-dp-item") > -1) || (targetParent && targetParent.classList && targetParent.classList.contains("ui5-dp-item")); } - _isOutOfSelectableRange(date) { - return date.valueOf() < this._minDate || date.valueOf() > this._maxDate; - } - - _getVisibleDays(oStartDate, bIncludeBCDates) { - let oCalDate, - iDaysOldMonth, - iYear; + _getFirstDay() { + let daysFromPreviousMonth; - const minCalendarDateYear = CalendarDate.fromTimestamp(this._getMinCalendarDate(), this._primaryCalendarType).getYear(); - const maxCalendarDateYear = CalendarDate.fromTimestamp(this._getMaxCalendarDate(), this._primaryCalendarType).getYear(); - const _aVisibleDays = []; - - // If date passed generate days for new start date else return the current one - if (!oStartDate) { - return _aVisibleDays; - } - - const iFirstDayOfWeek = this._getFirstDayOfWeek(); + const firstDayOfWeek = this._getFirstDayOfWeek(); // determine weekday of first day in month - const oFirstDay = new CalendarDate(oStartDate, this._primaryCalendarType); - oFirstDay.setDate(1); - iDaysOldMonth = oFirstDay.getDay() - iFirstDayOfWeek; - if (iDaysOldMonth < 0) { - iDaysOldMonth = 7 + iDaysOldMonth; + const firstDay = new CalendarDate(this._calendarDate, this._primaryCalendarType); + firstDay.setDate(1); + daysFromPreviousMonth = firstDay.getDay() - firstDayOfWeek; + if (daysFromPreviousMonth < 0) { + daysFromPreviousMonth = 7 + daysFromPreviousMonth; } - if (iDaysOldMonth > 0) { - // determine first day for display - oFirstDay.setDate(1 - iDaysOldMonth); - } - - const oDay = new CalendarDate(oFirstDay); - for (let i = 0; i < 42; i++) { - iYear = oDay.getYear(); - oCalDate = new CalendarDate(oDay, this._primaryCalendarType); - if (bIncludeBCDates && iYear < minCalendarDateYear) { - // 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 >= minCalendarDateYear && iYear <= maxCalendarDateYear) { - // Days before 0001-01-01 or after 9999-12-31 should not be rendered. - _aVisibleDays.push(oCalDate); - } - oDay.setDate(oDay.getDate() + 1); + if (daysFromPreviousMonth > 0) { + firstDay.setDate(1 - daysFromPreviousMonth); } - return _aVisibleDays; + return firstDay; } _getFirstDayOfWeek() { diff --git a/packages/main/src/DurationPicker.hbs b/packages/main/src/DurationPicker.hbs deleted file mode 100644 index 85ab471a8656..000000000000 --- a/packages/main/src/DurationPicker.hbs +++ /dev/null @@ -1,29 +0,0 @@ -
    - - {{#unless readonly}} - - {{/unless}} - - {{#if valueStateMessage.length}} - - {{/if}} - -
    \ No newline at end of file diff --git a/packages/main/src/DurationPicker.js b/packages/main/src/DurationPicker.js index 9927d090ce23..88ef6b43a45f 100644 --- a/packages/main/src/DurationPicker.js +++ b/packages/main/src/DurationPicker.js @@ -1,50 +1,12 @@ -import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; -import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; -import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; -import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; -import { - isShow, - isLeft, - isRight, - isPageUp, - isPageDown, - isPageUpShift, - isPageDownShift, - isPageUpShiftCtrl, - isPageDownShiftCtrl, -} from "@ui5/webcomponents-base/dist/Keys.js"; -import { isPhone } from "@ui5/webcomponents-base/dist/Device.js"; import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; -import DurationPickerTemplate from "./generated/templates/DurationPickerTemplate.lit.js"; -import PopoverPlacementType from "./types/PopoverPlacementType.js"; -import PopoverHorizontalAlign from "./types/PopoverHorizontalAlign.js"; -import WheelSlider from "./WheelSlider.js"; -import ResponsivePopover from "./ResponsivePopover.js"; -import Input from "./Input.js"; -import Icon from "./Icon.js"; -import Button from "./Button.js"; import "@ui5/webcomponents-icons/dist/fob-watch.js"; -import DurationPickerPopoverTemplate from "./generated/templates/DurationPickerPopoverTemplate.lit.js"; -import { - TIMEPICKER_HOURS_LABEL, - TIMEPICKER_MINUTES_LABEL, - TIMEPICKER_SECONDS_LABEL, - TIMEPICKER_SUBMIT_BUTTON, - TIMEPICKER_CANCEL_BUTTON, -} from "./generated/i18n/i18n-defaults.js"; - -// Styles -import DurationPickerCss from "./generated/themes/DurationPicker.css.js"; -import ResponsivePopoverCommonCss from "./generated/themes/ResponsivePopoverCommon.css.js"; -import DurationPickerPopoverCss from "./generated/themes/DurationPickerPopover.css.js"; +import TimePickerBase from "./TimePickerBase.js"; /** * @public */ const metadata = { tag: "ui5-duration-picker", - languageAware: true, - managedSlots: true, properties: /** @lends sap.ui.webcomponents.main.DurationPicker.prototype */ { /** * Defines a formatted time value. @@ -125,93 +87,21 @@ const metadata = { hideHours: { type: Boolean, }, + }, +}; - /** - * Determines whether the ui5-duration-picker is displayed as disabled. - * - * @type {boolean} - * @defaultvalue false - * @public - */ - disabled: { - type: Boolean, - }, - - /** - * Determines whether the ui5-duration-picker is displayed as readonly. - * - * @type {boolean} - * @defaultvalue false - * @public - */ - readonly: { - type: Boolean, - }, - - /** - * Visualizes the validation state of the Web Component, for example - * Error, Warning and - * Success. - * - *
      - *
    • None
    • - *
    • Error
    • - *
    • Warning
    • - *
    • Success
    • - *
    • Information
    • - *
    - * - * @type {string} - * @defaultvalue "None" - * @public - */ - valueState: { - type: ValueState, - defaultValue: ValueState.None, - }, - - /** - * @private - */ - _isPickerOpen: { - type: Boolean, - }, +const getNearestValue = (x, step, max) => { + const down = Math.floor(x / step) * step; // closest value rounded down to the step + const up = Math.ceil(x / step) * step; // closest value rounded up to the step + if (up > max || x - down < up - x) { // if the rounded-up value is more than max, or x is closer to the rounded-down value, return down + return down; + } + return up; // x is closer to the rounded-up value and it is not +}; - /** - * @private - */ - _maxValue: { - type: String, - multiple: true, - }, - }, - slots: /** @lends sap.ui.webcomponents.main.DurationPicker.prototype */ { - /** - * Defines the value state message that will be displayed as pop up under the ui5-duration-picker. - *

    - * - * Note: If not specified, a default text (in the respective language) will be displayed. - *
    - * Note: The valueStateMessage would be displayed, - * when the ui5-duration-picker is in Information, Warning or Error value state. - * @type {HTMLElement} - * @since 1.0.0-rc.9 - * @slot - * @public - */ - valueStateMessage: { - type: HTMLElement, - }, - }, - events: /** @lends sap.ui.webcomponents.main.DurationPicker.prototype */ { - /** - * Fired when the input operation has finished by pressing Enter or on focusout. - * - * @event - * @public - */ - change: {}, - }, +const pad = number => { + number = parseInt(number); + return number < 9 ? `0${number}` : `${number}`; }; /** @@ -268,444 +158,133 @@ const metadata = { * @since 1.0.0-rc.7 * @author SAP SE * @alias sap.ui.webcomponents.main.DurationPicker - * @extends UI5Element + * @extends TimePickerBase * @tagname ui5-duration-picker * @public */ -class DurationPicker extends UI5Element { +class DurationPicker extends TimePickerBase { static get metadata() { return metadata; } - static get render() { - return litRender; - } - - static get styles() { - return DurationPickerCss; - } - - static get template() { - return DurationPickerTemplate; - } - - static get staticAreaTemplate() { - return DurationPickerPopoverTemplate; - } - - static get staticAreaStyles() { - return [ResponsivePopoverCommonCss, DurationPickerPopoverCss]; - } - - constructor() { - super(); - - this.i18nBundle = getI18nBundle("@ui5/webcomponents"); - - this._respPopover = { - placementType: PopoverPlacementType.Bottom, - horizontalAlign: PopoverHorizontalAlign.Left, - allowTargetOverlap: true, - stayOpenOnScroll: true, - _onAfterClose: () => { - this._isPickerOpen = false; - }, - }; - - this._slidersDomRefs = []; - } - - onBeforeRendering() { - this.checkValue(); - } - - checkValue() { - this._setValue("maxValue"); - this.setSelectedValues(); - this.normalizaValue(); - } - - normalizaValue() { - this.value = `${!this.hideHours ? this.selectedHours || "00" : ""}${!this.hideHours && !this.hideMinutes ? ":" : ""}${!this.hideMinutes ? this.selectedMinutes || "00" : ""}${!this.hideSeconds ? `:${this.selectedSeconds || "00"}` : ""}`; - } - /** - * reads string from format hh:mm:ss and returns an array which contains the hours, minutes and seconds - * @param {string} value string in formathh:mm:ss + * In order to keep the existing behavior (although not consistent with the other picker components), we enforce limits and step on each change and initially */ - readFormattedValue(value) { - value = value.replace(/\s/g, ""); // Remove spaces - return value.split(":"); - } - - getSecondsFromFormattedValue(destructuredValues) { - if (this.hideSeconds) { - return ""; - } - - if (this.hideHours && this.hideMinutes) { - return destructuredValues[0]; - } - - if (this.hideHours || this.hideMinutes) { - return destructuredValues[1]; - } - - return destructuredValues[2]; - } - - getMinutesFromFormattedValue(destructuredValues) { - if (this.hideMinutes) { - return ""; - } - - if (this.hideHours) { - return destructuredValues[0]; - } - - return destructuredValues[1]; - } - - setSelectedValues() { - const destructuredValues = this.readFormattedValue(this.value || ""); - let currentHours = this.hideHours ? "" : destructuredValues[0], - currentMinutes = this.getMinutesFromFormattedValue(destructuredValues), // this.hideHours && !this.hideMinutes ? destructuredValues[0] : "", - currentSeconds = this.getSecondsFromFormattedValue(destructuredValues); // this.hideHours && this.hideHours ? destructuredValues[0] : {}; - - if (currentHours > -1) { - if (parseInt(currentHours) > parseInt(this._maxValue[0])) { - currentHours = this._maxValue[0]; - } - - this.selectedHours = this._formatSelectedValue(currentHours, parseInt(this.readFormattedValue(this.maxValue))); - } - - if (currentMinutes > -1) { - if (currentMinutes && parseInt(currentMinutes) % this.minutesStep !== 0) { - currentMinutes = this.findNearestStep(currentMinutes, this.minutesStep); - } - if (this._maxValue[0] && this.selectedHours === this._maxValue[0]) { - currentMinutes = currentMinutes > this._maxValue[1] ? this._maxValue[1] : currentMinutes; - } else if (parseInt(currentMinutes) > parseInt(this._maxValue[1])) { - currentMinutes = this._maxValue[1]; - } - - this.selectedMinutes = this._formatSelectedValue(currentMinutes, 59); - } - - if (currentSeconds > -1) { - if (currentSeconds && parseInt(currentSeconds) % this.secondsStep !== 0) { - currentSeconds = this.findNearestStep(currentSeconds, this.secondsStep); - } - if (this._maxValue[0] && this._maxValue[1] && this.selectedHours >= this._maxValue[0] && this.selectedSeconds >= this._maxValue[1]) { - currentSeconds = currentSeconds > this._maxValue[2] ? this._maxValue[2] : currentSeconds; - } else if (parseInt(currentSeconds) > parseInt(this._maxValue[2])) { - currentSeconds = this._maxValue[2]; - } - - this.selectedSeconds = this._formatSelectedValue(currentSeconds, 59); - } - } - - _formatSelectedValue(currentValue, maximum = Infinity) { - if (currentValue.length === 1) { - return `0${currentValue}`; - } - - if (parseInt(currentValue) < 0 || parseInt(currentValue) > maximum) { - return "00"; + onBeforeRendering() { + const value = this.value; + if (this.isValid(value)) { + this.value = this.normalizeValue(value); } - - return currentValue; } /** - * Reads maxValue and stores it as array _maxValue - * @param {string} name the name of the property to read(could be used for _minValue e.g.) - * @private + * In order to keep the existing behavior (although not consistent with the other picker components), we do not update "value" on input, only fire event + * @override */ - _setValue(name) { - const _value = this[name]; - if (!_value) { - return; - } - const temp = this.readFormattedValue(_value); - this[`_${name}`] = temp; + async _handleInputLiveChange(event) { + const value = event.target.value; + const valid = this.isValid(value); + this._updateValueState(); // Change the value state to Error/None, but only if needed + this.fireEvent("input", { value, valid }); } - findNearestStep(currentValue, step) { - const curr = parseInt(currentValue); - const biggerClosest = this._getClosest(curr, step, true), - lowerClosest = this._getClosest(curr, step, false); - - const diffToBiggerClosest = biggerClosest - curr, - diffToLowerClosest = curr - lowerClosest; - - return diffToBiggerClosest > diffToLowerClosest ? lowerClosest.toString() : biggerClosest.toString(); + get _formatPattern() { + return "HH:mm:ss"; } /** - * Finds the nearest lower/bigger number to the givent curr - * @param {Integer} curr the starting number - * @param {Boolean} larger defines if we are searching for bigger or lower number + * The "value" property might be "02:03" (HH:ss) or just "12"(ss) but the ui5-time-selection component requires a value compliant with _formatPattern + * We split the value and shift up to 3 times, filling the values for the configured units (based on hideHours, hideMinutes, hideSeconds) + * @override */ - _getClosest(curr, step, larger = true) { - while (curr % step !== 0) { - curr = larger ? ++curr : --curr; - } - - return curr; - } - - async _handleContainerKeysDown(event) { - if (isLeft(event)) { - let expandedSliderIndex = 0; - for (let i = 0; i < this._slidersDomRefs.length; i++) { - if (this._slidersDomRefs[i]._expanded) { - expandedSliderIndex = i; - } - } - if (this._slidersDomRefs[expandedSliderIndex - 1]) { - this._slidersDomRefs[expandedSliderIndex - 1].focus(); - } else { - this._slidersDomRefs[this._slidersDomRefs.length - 1].focus(); - } - } else if (isRight(event)) { - let expandedSliderIndex = 0; - - for (let i = 0; i < this._slidersDomRefs.length; i++) { - if (this._slidersDomRefs[i]._expanded) { - expandedSliderIndex = i; - } - } - if (this._slidersDomRefs[expandedSliderIndex + 1]) { - this._slidersDomRefs[expandedSliderIndex + 1].focus(); - } else { - this._slidersDomRefs[0].focus(); - } - } + get _effectiveValue() { + return this.isValid(this.value) ? this._toFullFormat(this.value) : "00:00:00"; } - _onkeydown(event) { - if (isShow(event)) { - event.preventDefault(); - this.togglePicker(); - } - - if (isPageUpShiftCtrl(event)) { - event.preventDefault(); - this._incrementValue(true, false, false, true); - } else if (isPageUpShift(event)) { - event.preventDefault(); - this._incrementValue(true, false, true, false); - } else if (isPageUp(event)) { - event.preventDefault(); - this._incrementValue(true, true, false, false); - } - - if (isPageDownShiftCtrl(event)) { - event.preventDefault(); - this._incrementValue(false, false, false, true); - } else if (isPageDownShift(event)) { - event.preventDefault(); - this._incrementValue(false, false, true, false); - } else if (isPageDown(event)) { - event.preventDefault(); - this._incrementValue(false, true, false, false); - } - } - - _incrementValue(increment, hours, minutes, seconds) { - const values = this.readFormattedValue(this.value); - const incrementStep = increment ? 1 : -1; - - if (hours && !this.hideHours) { - values[0] = Number(values[0]) + incrementStep; - } else if (minutes && !this.hideMinutes) { - values[1] = Number(values[1]) + incrementStep; - } else if (seconds && !this.hideSeconds) { - values[2] = Number(values[2]) + incrementStep; - } else { - return; - } - - this.value = `${!this.hideHours ? values[0] : ""}${!this.hideHours && !this.hideMinutes ? ":" : ""}${!this.hideMinutes ? values[1] : ""}${!this.hideSeconds ? `:${values[2]}` : ""}`; - this.fireEvent("change", { value: this.value }); + get _timeSelectionValue() { + return this._effectiveValue; } - - generateTimeItemsArray(arrayLength, step = 1) { - const resultArray = []; - for (let i = 0; i < arrayLength; i++) { - let tempString = i.toString(); - if (tempString.length === 1) { - tempString = `0${tempString}`; - } - - if (tempString % step === 0) { - resultArray.push(tempString); - } - } - - return resultArray; + /** + * @override + */ + get openIconName() { + return "fob-watch"; } - submitPickers() { - const prevValue = this.value; - this.value = `${!this.hideHours ? this.hoursSlider.value : ""}${!this.hideHours && !this.hideMinutes ? ":" : ""}${!this.hideMinutes ? this.minutesSlider.value : ""}${!this.hideSeconds ? `:${this.secondsSlider.value}` : ""}`; - this.togglePicker(); - if (prevValue !== this.value) { - this.fireEvent("change", { value: this.value }); + /** + * Transforms the value to HH:mm:ss format to be compatible with time manipulation logic (keyboard handling, time selection component) + * @private + */ + _toFullFormat(value) { + let hours = "00", + minutes = "00", + seconds = "00"; + + const parts = value.split(":"); + if (parts.length && !this.hideHours) { + hours = parts.shift(); } - } - - _handleInputChange(event) { - const prevValue = this.value; - this.value = event.target.value.replace(/[^\d:]/g, ""); - this.checkValue(); - - if (prevValue !== this.value) { - this.fireEvent("change", { value: this.value }); + if (parts.length && !this.hideMinutes) { + minutes = parts.shift(); } - } - - _handleKeysDown(event) { - if (isShow(event)) { - event.preventDefault(); - this.togglePicker(); + if (parts.length && !this.hideSeconds) { + seconds = parts.shift(); } - } - async _handleInputLiveChange() { - await this._getResponsivePopover(); - - if (this.responsivePopover.opened) { - this.togglePicker(); - } + return `${hours}:${minutes}:${seconds}`; } - async togglePicker() { - await this._getResponsivePopover(); - - if (this.responsivePopover.opened) { - this._isPickerOpen = false; - this.responsivePopover.close(); - } else { - this._isPickerOpen = true; - this.responsivePopover.open(this); - this._slidersDomRefs = await this.slidersDomRefs(); + /** + * Transforms the value from HH:mm:ss format to the needed partial format (f.e. HH:ss or mm or ss) to be displayed in the input + * @private + */ + _toPartialFormat(value) { + const parts = value.split(":"); + const newParts = []; + if (!this.hideHours) { + newParts.push(parts[0]); } - } - - async _getResponsivePopover() { - if (this.responsivePopover) { - return this.responsivePopover; + if (!this.hideMinutes) { + newParts.push(parts[1]); } - - const staticAreaItem = await this.getStaticAreaItemDomRef(); - this.responsivePopover = staticAreaItem.querySelector("[ui5-responsive-popover]"); - return this.responsivePopover; - } - - async slidersDomRefs() { - await this._getResponsivePopover(); - return this.responsivePopover.default.length ? [...this.responsivePopover.default[0].children].filter(x => x.isUI5Element) : this.responsivePopover.default; - } - - - get hours() { - return this.selectedHours; - } - - get minutes() { - return this.selectedMinutes; - } - - get seconds() { - return this.selectedSeconds; - } - - get hoursArray() { - const _maxHours = parseInt(this.readFormattedValue(this.maxValue)[0]); - const _currHours = parseInt(this.selectedHours) + 1; - let hours; - - if (_maxHours) { - hours = _maxHours + 1; - } else if (_currHours < 24) { - hours = 24; - } else { - hours = _currHours; + if (!this.hideSeconds) { + newParts.push(parts[2]); } - - return this.generateTimeItemsArray(hours); - } - - get minutesArray() { - const currentMinutes = parseInt(this.readFormattedValue(this.maxValue)[1]); - const minutes = currentMinutes && currentMinutes > 0 && currentMinutes < 60 ? currentMinutes + 1 : 60; - return this.generateTimeItemsArray(minutes, this.minutesStep); - } - - get secondsArray() { - const currentSeconds = parseInt(this.readFormattedValue(this.maxValue)[2]); - const seconds = currentSeconds && currentSeconds > 0 && currentSeconds < 60 ? currentSeconds + 1 : 60; - return this.generateTimeItemsArray(seconds, this.secondsStep); - } - - get secondsSlider() { - return this.responsivePopover && this.responsivePopover.querySelector(".ui5-duration-picker-seconds-wheelslider"); - } - - get minutesSlider() { - return this.responsivePopover && this.responsivePopover.querySelector(".ui5-duration-picker-minutes-wheelslider"); + return newParts.join(":"); } - get hoursSlider() { - return this.responsivePopover && this.responsivePopover.querySelector(".ui5-duration-picker-hours-wheelslider"); - } + _enforceLimitsAndStep(fullFormatValue) { + let [hours, minutes, seconds] = fullFormatValue.split(":"); + hours = Math.min(hours, this.maxHours); + minutes = Math.min(minutes, this.maxMinutes); + seconds = Math.min(seconds, this.maxSeconds); - get hoursSliderTitle() { - return this.i18nBundle.getText(TIMEPICKER_HOURS_LABEL); - } + minutes = getNearestValue(minutes, this.minutesStep, this.maxMinutes); + seconds = getNearestValue(seconds, this.secondsStep, this.maxSeconds); - get minutesSliderTitle() { - return this.i18nBundle.getText(TIMEPICKER_MINUTES_LABEL); + return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; } - get secondsSliderTitle() { - return this.i18nBundle.getText(TIMEPICKER_SECONDS_LABEL); - } - - get submitButtonLabel() { - return this.i18nBundle.getText(TIMEPICKER_SUBMIT_BUTTON); - } - - get cancelButtonLabel() { - return this.i18nBundle.getText(TIMEPICKER_CANCEL_BUTTON); + /** + * @override + */ + normalizeValue(value) { + let fullFormatValue = this._toFullFormat(value); // transform to full format (HH:mm:ss) if not already in this format, in order to normalize the value + fullFormatValue = this._enforceLimitsAndStep(fullFormatValue); + return this._toPartialFormat(fullFormatValue); // finally transform back to the needed format for the input } - get classes() { - return { - container: { - "ui5-duration-picker-sliders-container": true, - "ui5-phone": isPhone(), - }, - }; + get maxHours() { + return parseInt(this.maxValue.split(":")[0]); } - static get dependencies() { - return [ - Icon, - WheelSlider, - ResponsivePopover, - Input, - Button, - ]; + get maxMinutes() { + return parseInt(this.maxValue.split(":")[1]); } - static async onDefine() { - await fetchI18nBundle("@ui5/webcomponents"); + get maxSeconds() { + return parseInt(this.maxValue.split(":")[2]); } } diff --git a/packages/main/src/DurationPickerPopover.hbs b/packages/main/src/DurationPickerPopover.hbs deleted file mode 100644 index abe7374243c6..000000000000 --- a/packages/main/src/DurationPickerPopover.hbs +++ /dev/null @@ -1,48 +0,0 @@ - -
    - {{#unless hideHours}} - - {{/unless}} - - {{#unless hideMinutes}} - - {{/unless}} - - {{#unless hideSeconds}} - - {{/unless}} -
    - - -
    \ No newline at end of file diff --git a/packages/main/src/Input.js b/packages/main/src/Input.js index 9ac396e469fd..23c7c2120bd9 100644 --- a/packages/main/src/Input.js +++ b/packages/main/src/Input.js @@ -16,6 +16,7 @@ import { import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AriaLabelHelper.js"; +import { getCaretPosition, setCaretPosition } from "@ui5/webcomponents-base/dist/util/Caret.js"; import "@ui5/webcomponents-icons/dist/decline.js"; import InputType from "./types/InputType.js"; import Popover from "./Popover.js"; @@ -939,6 +940,14 @@ class Input extends UI5Element { return this.Suggestions && this.Suggestions.responsivePopover.querySelector(".ui5-input-inner-phone"); } + return this.nativeInput; + } + + /** + * Returns a reference to the native input element + * @protected + */ + get nativeInput() { return this.getDomRef().querySelector(`input`); } @@ -1175,6 +1184,23 @@ class Input extends UI5Element { return isPhone(); } + /** + * Returns the caret position inside the native input + * @protected + */ + getCaretPosition() { + return getCaretPosition(this.nativeInput); + } + + /** + * Sets the caret to a certain position inside the native input + * @protected + * @param pos + */ + setCaretPosition(pos) { + setCaretPosition(this.nativeInput, pos); + } + static get dependencies() { const Suggestions = getFeature("InputSuggestions"); diff --git a/packages/main/src/MonthPicker.hbs b/packages/main/src/MonthPicker.hbs index d684c49aa75c..f2ecd4a3b238 100644 --- a/packages/main/src/MonthPicker.hbs +++ b/packages/main/src/MonthPicker.hbs @@ -3,22 +3,22 @@ role="grid" aria-readonly="false" aria-multiselectable="false" - style="{{styles.main}}" @keydown={{_onkeydown}} - @mousedown={{_onmousedown}} - @mouseup={{_onmouseup}} + @keyup={{_onkeyup}} + @click={{_selectMonth}} + @focusin={{_onfocusin}} > - {{#each _quarters}} + {{#each _months}}
    {{#each this}}
    {{this.name}}
    diff --git a/packages/main/src/MonthPicker.js b/packages/main/src/MonthPicker.js index 234111a9814b..5ac5cd3dabc3 100644 --- a/packages/main/src/MonthPicker.js +++ b/packages/main/src/MonthPicker.js @@ -1,12 +1,22 @@ import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; -import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js"; -import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js"; +import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; +import { + isEnter, + isSpace, + isDown, + isUp, + isLeft, + isRight, + isHome, + isEnd, + isHomeCtrl, + isEndCtrl, + isPageUp, + isPageDown, +} from "@ui5/webcomponents-base/dist/Keys.js"; import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; -import ItemNavigationBehavior from "@ui5/webcomponents-base/dist/types/ItemNavigationBehavior.js"; -import PickerBase from "./PickerBase.js"; +import CalendarPart from "./CalendarPart.js"; import MonthPickerTemplate from "./generated/templates/MonthPickerTemplate.lit.js"; - -// Styles import styles from "./generated/themes/MonthPicker.css.js"; /** * @public @@ -14,7 +24,7 @@ import styles from "./generated/themes/MonthPicker.css.js"; const metadata = { tag: "ui5-monthpicker", properties: /** @lends sap.ui.webcomponents.main.MonthPicker.prototype */ { - _quarters: { + _months: { type: Object, multiple: true, }, @@ -26,13 +36,13 @@ const metadata = { }, events: /** @lends sap.ui.webcomponents.main.MonthPicker.prototype */ { /** - * Fired when the user selects a new Date on the Web Component. + * Fired when the user selects a month (space/enter/click). * @public * @event */ change: {}, /** - * Fired when month, year has changed due to item navigation. + * Fired when the timestamp changes - the user navigates with the keyboard or clicks with the mouse. * @since 1.0.0-rc.9 * @public * @event @@ -41,6 +51,9 @@ const metadata = { }, }; +const PAGE_SIZE = 12; // Total months on a single page +const ROW_SIZE = 3; // Months per row (4 rows of 3 months each) + /** * Month picker component. * @@ -51,11 +64,11 @@ const metadata = { * @constructor * @author SAP SE * @alias sap.ui.webcomponents.main.MonthPicker - * @extends sap.ui.webcomponents.main.PickerBase + * @extends CalendarPart * @tagname ui5-monthpicker * @public */ -class MonthPicker extends PickerBase { +class MonthPicker extends CalendarPart { static get metadata() { return metadata; } @@ -68,141 +81,204 @@ class MonthPicker extends PickerBase { return styles; } - constructor() { - super(); - - this._itemNav = new ItemNavigation(this, { - pageSize: 12, - rowSize: 3, - behavior: ItemNavigationBehavior.Paging, - getItemsCallback: () => this.focusableMonths, - affectedPropertiesNames: ["_quarters"], - }); - - this._itemNav.attachEvent( - ItemNavigation.BORDER_REACH, - this._handleItemNavigationBorderReach.bind(this) - ); + onBeforeRendering() { + this._buildMonths(); } - onBeforeRendering() { + _buildMonths() { + if (this._hidden) { + return; + } + const localeData = getCachedLocaleDataInstance(getLocale()); + const monthsNames = localeData.getMonths("wide", this._primaryCalendarType); - const quarters = []; - const oCalDate = this._calendarDate; + const months = []; + const calendarDate = this._calendarDate; // store the value of the expensive getter + const minDate = this._minDate; // store the value of the expensive getter + const maxDate = this._maxDate; // store the value of the expensive getter + const tempDate = new CalendarDate(calendarDate, this._primaryCalendarType); let timestamp; /* eslint-disable no-loop-func */ for (let i = 0; i < 12; i++) { - oCalDate.setMonth(i); - timestamp = oCalDate.valueOf() / 1000; + tempDate.setMonth(i); + timestamp = tempDate.valueOf() / 1000; + + const isSelected = this.selectedDates.some(itemTimestamp => { + const date = CalendarDate.fromTimestamp(itemTimestamp * 1000, this._primaryCalendarType); + return date.getYear() === tempDate.getYear() && date.getMonth() === tempDate.getMonth(); + }); + const isFocused = tempDate.getMonth() === calendarDate.getMonth(); + const isDisabled = this._isOutOfSelectableRange(tempDate, minDate, maxDate); const month = { timestamp: timestamp.toString(), - id: `${this._id}-m${i}`, - selected: this.selectedDates.some(d => d === timestamp), - name: localeData.getMonths("wide", this._primaryCalendarType)[i], + focusRef: isFocused, + _tabIndex: isFocused ? "0" : "-1", + selected: isSelected, + ariaSelected: isSelected ? "true" : "false", + name: monthsNames[i], + disabled: isDisabled, classes: "ui5-mp-item", }; - if (month.selected) { + if (isSelected) { month.classes += " ui5-mp-item--selected"; } - if ((this.minDate || this.maxDate) && this._isOutOfSelectableRange(i)) { + if (isDisabled) { month.classes += " ui5-mp-item--disabled"; - month.disabled = true; } - const quarterIndex = parseInt(i / 3); + const quarterIndex = parseInt(i / ROW_SIZE); - if (quarters[quarterIndex]) { - quarters[quarterIndex].push(month); + if (months[quarterIndex]) { + months[quarterIndex].push(month); } else { - quarters[quarterIndex] = [month]; + months[quarterIndex] = [month]; } } - this._quarters = quarters; + this._months = months; } onAfterRendering() { - this._itemNav.focusCurrent(); + if (!this._hidden) { + this.focus(); + } } - _setCurrentItemTabIndex(index) { - const currentItem = this._itemNav._getCurrentItem(); - if (currentItem) { - currentItem.setAttribute("tabindex", index.toString()); + _onkeydown(event) { + let preventDefault = true; + + if (isEnter(event)) { + this._selectMonth(event); + } else if (isSpace(event)) { + event.preventDefault(); + } else if (isLeft(event)) { + this._modifyTimestampBy(-1); + } else if (isRight(event)) { + this._modifyTimestampBy(1); + } else if (isUp(event)) { + this._modifyTimestampBy(-ROW_SIZE); + } else if (isDown(event)) { + this._modifyTimestampBy(ROW_SIZE); + } else if (isPageUp(event)) { + this._modifyTimestampBy(-PAGE_SIZE); + } else if (isPageDown(event)) { + this._modifyTimestampBy(PAGE_SIZE); + } else if (isHome(event) || isEnd(event)) { + this._onHomeOrEnd(isHome(event)); + } else if (isHomeCtrl(event)) { + this._setTimestamp(parseInt(this._months[0][0].timestamp)); // first month of first row + } else if (isEndCtrl(event)) { + this._setTimestamp(parseInt(this._months[PAGE_SIZE / ROW_SIZE - 1][ROW_SIZE - 1].timestamp)); // last month of last row + } else { + preventDefault = false; } - } - _onmousedown(event) { - if (event.target.className.indexOf("ui5-mp-item") > -1) { - const targetTimestamp = this.getTimestampFromDom(event.target); - const focusedItem = this.focusableMonths.find(item => parseInt(item.timestamp) === targetTimestamp); - this._itemNav.update(focusedItem); + if (preventDefault) { + event.preventDefault(); } } - _onmouseup(event) { - if (event.target.className.indexOf("ui5-mp-item") > -1) { - const timestamp = this.getTimestampFromDom(event.target); - this.timestamp = timestamp; - this.fireEvent("change", { timestamp }); - } + _onHomeOrEnd(homePressed) { + this._months.forEach(row => { + const indexInRow = row.findIndex(item => CalendarDate.fromTimestamp(parseInt(item.timestamp) * 1000).getMonth() === this._calendarDate.getMonth()); + if (indexInRow !== -1) { // The current month is on this row + const index = homePressed ? 0 : ROW_SIZE - 1; // select the first (if Home) or last (if End) month on the row + this._setTimestamp(parseInt(row[index].timestamp)); + } + }); } - _onkeydown(event) { - if (isSpace(event) || isEnter(event)) { - this._activateMonth(event); + /** + * Sets the timestamp to an absolute value + * @param value + * @private + */ + _setTimestamp(value) { + this._safelySetTimestamp(value); + this.fireEvent("navigate", { timestamp: this.timestamp }); + } + + /** + * Modifies timestamp by a given amount of months and, if necessary, loads the prev/next page + * @param amount + * @private + */ + _modifyTimestampBy(amount) { + // Modify the current timestamp + this._safelyModifyTimestampBy(amount, "month"); + + // Notify the calendar to update its timestamp + this.fireEvent("navigate", { timestamp: this.timestamp }); + } + + _onkeyup(event) { + if (isSpace(event)) { + this._selectMonth(event); } } - _activateMonth(event) { + /** + * User clicked with the mouser or pressed Enter/Space + * @param event + * @private + */ + _selectMonth(event) { event.preventDefault(); if (event.target.className.indexOf("ui5-mp-item") > -1) { - const timestamp = this.getTimestampFromDom(event.target); - this.timestamp = timestamp; - this.fireEvent("change", { timestamp }); + const timestamp = this._getTimestampFromDom(event.target); + this._safelySetTimestamp(timestamp); + this.fireEvent("change", { timestamp: this.timestamp }); } } - _handleItemNavigationBorderReach(event) { - if (this._isOutOfSelectableRange(this._month)) { - return; - } - - this.fireEvent("navigate", event); + /** + * Called from Calendar.js + * @protected + */ + _hasPreviousPage() { + return this._calendarDate.getYear() !== this._minDate.getYear(); } - _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; + /** + * Called from Calendar.js + * @protected + */ + _hasNextPage() { + return this._calendarDate.getYear() !== this._maxDate.getYear(); } - get focusableMonths() { - const focusableMonths = []; - - for (let i = 0; i < this._quarters.length; i++) { - const quarter = this._quarters[i].filter(x => !x.disabled); - focusableMonths.push(quarter); - } + /** + * Called by Calendar.js + * User pressed the "<" button in the calendar header (same as PageUp) + * @protected + */ + _showPreviousPage() { + this._modifyTimestampBy(-PAGE_SIZE); + } - return [].concat(...focusableMonths); + /** + * Called by Calendar.js + * User pressed the ">" button in the calendar header (same as PageDown) + * @protected + */ + _showNextPage() { + this._modifyTimestampBy(PAGE_SIZE); } - get styles() { - return { - main: { - display: this._hidden ? "none" : "", - }, - }; + _isOutOfSelectableRange(date, minDate, maxDate) { + const month = date.getMonth(); + const year = date.getYear(); + const minYear = minDate.getYear(); + const minMonth = minDate.getMonth(); + const maxYear = maxDate.getYear(); + const maxMonth = maxDate.getMonth(); + + return year < minYear || (year === minYear && month < minMonth) || year > maxYear || (year === maxYear && month > maxMonth); } } diff --git a/packages/main/src/SuggestionListItem.js b/packages/main/src/SuggestionListItem.js index 0d9b98c56f17..28765899c882 100644 --- a/packages/main/src/SuggestionListItem.js +++ b/packages/main/src/SuggestionListItem.js @@ -34,7 +34,7 @@ const metadata = { * @constructor * @author SAP SE * @alias sap.ui.webcomponents.main.SuggestionListItem - * @extends UI5Element + * @extends StandardListItem * @tagname ui5-suggestion-item */ class SuggestionListItem extends StandardListItem { diff --git a/packages/main/src/TimePicker.hbs b/packages/main/src/TimePicker.hbs index 09ec081d999a..14dd65ecae99 100644 --- a/packages/main/src/TimePicker.hbs +++ b/packages/main/src/TimePicker.hbs @@ -1,4 +1,4 @@ -
    +
    {{/unless}} -
    \ No newline at end of file +
    diff --git a/packages/main/src/TimePicker.js b/packages/main/src/TimePicker.js index 61efd0519161..7ceb41dae2ee 100644 --- a/packages/main/src/TimePicker.js +++ b/packages/main/src/TimePicker.js @@ -1,57 +1,6 @@ -import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; -import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; -import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; -import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; -import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; -import "@ui5/webcomponents-localization/dist/features/calendar/Gregorian.js"; // default calendar for bundling -import { fetchCldr } from "@ui5/webcomponents-base/dist/asset-registries/LocaleData.js"; -import { isPhone } from "@ui5/webcomponents-base/dist/Device.js"; -import { - isLeft, - isRight, - isTabNext, - isTabPrevious, - isShow, - isPageUp, - isPageDown, - isPageUpShift, - isPageDownShift, - isPageUpShiftCtrl, - isPageDownShiftCtrl, -} from "@ui5/webcomponents-base/dist/Keys.js"; -import "@ui5/webcomponents-icons/dist/time-entry-request.js"; -import Icon from "./Icon.js"; -import PopoverHorizontalAlign from "./types/PopoverHorizontalAlign.js"; -import ResponsivePopover from "./ResponsivePopover.js"; -import PopoverPlacementType from "./types/PopoverPlacementType.js"; -import TimePickerTemplate from "./generated/templates/TimePickerTemplate.lit.js"; -import TimePickerPopoverTemplate from "./generated/templates/TimePickerPopoverTemplate.lit.js"; -import Input from "./Input.js"; -import Button from "./Button.js"; -import WheelSlider from "./WheelSlider.js"; -import { - getHours, - getMinutes, - getSeconds, - getHoursConfigByFormat, - getTimeControlsByFormat, -} from "./timepicker-utils/TimeSlider.js"; - -import { - TIMEPICKER_HOURS_LABEL, - TIMEPICKER_MINUTES_LABEL, - TIMEPICKER_SECONDS_LABEL, - TIMEPICKER_PERIODS_LABEL, - TIMEPICKER_SUBMIT_BUTTON, - TIMEPICKER_CANCEL_BUTTON, -} from "./generated/i18n/i18n-defaults.js"; - -// Styles -import TimePickerCss from "./generated/themes/TimePicker.css.js"; -import TimePickerPopoverCss from "./generated/themes/TimePickerPopover.css.js"; -import ResponsivePopoverCommonCss from "./generated/themes/ResponsivePopoverCommon.css.js"; +import TimePickerBase from "./TimePickerBase.js"; /** * @public @@ -59,21 +8,7 @@ import ResponsivePopoverCommonCss from "./generated/themes/ResponsivePopoverComm const metadata = { tag: "ui5-time-picker", altTag: "ui5-timepicker", - languageAware: true, - managedSlots: true, - properties: /** @lends sap.ui.webcomponents.main.TimePicker.prototype */ { - /** - * Defines a formatted time value. - * - * @type {string} - * @defaultvalue undefined - * @public - */ - value: { - type: String, - defaultValue: undefined, - }, - + properties: /** @lends sap.ui.webcomponents.main.TimePickerBase.prototype */ { /** * Defines a short hint, intended to aid the user with data entry when the * ui5-time-picker has no value. @@ -106,105 +41,6 @@ const metadata = { formatPattern: { type: String, }, - - /** - * Defines the value state of the ui5-time-picker. - *

    - * Available options are: - *
      - *
    • None
    • - *
    • Error
    • - *
    • Warning
    • - *
    • Success
    • - *
    • Information
    • - *
    - * - * @type {ValueState} - * @defaultvalue "None" - * @public - */ - valueState: { - type: ValueState, - defaultValue: ValueState.None, - }, - - /** - * Determines whether the ui5-time-picker is displayed as disabled. - * - * @type {boolean} - * @defaultvalue false - * @public - */ - disabled: { - type: Boolean, - }, - - /** - * Determines whether the ui5-time-picker is displayed as readonly. - * - * @type {boolean} - * @defaultvalue false - * @public - */ - readonly: { - type: Boolean, - }, - - _isPickerOpen: { - type: Boolean, - noAttribute: true, - }, - - _respPopover: { - type: Object, - }, - - _hours: { - type: String, - }, - - _minutes: { - type: String, - }, - - _seconds: { - type: String, - }, - }, - slots: /** @lends sap.ui.webcomponents.main.TimePicker.prototype */ { - /** - * Defines the value state message that will be displayed as pop up under the ui5-time-picker. - *

    - * - * Note: If not specified, a default text (in the respective language) will be displayed. - *
    - * Note: The valueStateMessage would be displayed, - * when the ui5-time-picker is in Information, Warning or Error value state. - * @type {HTMLElement} - * @since 1.0.0-rc.8 - * @slot - * @public - */ - valueStateMessage: { - type: HTMLElement, - }, - }, - events: /** @lends sap.ui.webcomponents.main.TimePicker.prototype */ { - /** - * Fired when the input operation has finished by clicking the "OK" button or - * when the text in the input field has changed and the focus leaves the input field. - * - * @event - * @public - */ - change: {}, - /** - * Fired when the value of the ui5-time-picker is changed at each key stroke. - * - * @event - * @public - */ - input: {}, }, }; @@ -268,347 +104,22 @@ const metadata = { * @constructor * @author SAP SE * @alias sap.ui.webcomponents.main.TimePicker - * @extends UI5Element + * @extends TimePickerBase * @tagname ui5-time-picker * @public * @since 1.0.0-rc.6 */ -class TimePicker extends UI5Element { +class TimePicker extends TimePickerBase { static get metadata() { return metadata; } - static get render() { - return litRender; - } - - static get styles() { - return TimePickerCss; - } - - static get staticAreaTemplate() { - return TimePickerPopoverTemplate; - } - - static get template() { - return TimePickerTemplate; - } - - static get dependencies() { - return [ - Icon, - ResponsivePopover, - WheelSlider, - Input, - Button, - ]; - } - - static async onDefine() { - await Promise.all([ - fetchCldr(getLocale().getLanguage(), getLocale().getRegion(), getLocale().getScript()), - fetchI18nBundle("@ui5/webcomponents"), - ]); - } - - static get staticAreaStyles() { - return [ResponsivePopoverCommonCss, TimePickerPopoverCss]; - } - - - constructor() { - super(); - - this.prevValue = null; - this._isPickerOpen = false; - this.i18nBundle = getI18nBundle("@ui5/webcomponents"); - - this._respPopover = { - placementType: PopoverPlacementType.Bottom, - horizontalAlign: PopoverHorizontalAlign.Left, - allowTargetOverlap: true, - stayOpenOnScroll: true, - afterClose: () => { - this._isPickerOpen = false; - this.closePicker(); - }, - }; - - this._hoursParameters = { - minHour: 0, - maxHour: 0, - isTwelveHoursFormat: false, - }; - - this._slidersDomRefs = []; - } - - onBeforeRendering() { - if (!this.formatPattern) { - const localeData = getCachedLocaleDataInstance(getLocale()); - this.formatPattern = localeData.getTimePattern(this.getFormat().oFormatOptions.style); - } - - if (this.value === undefined) { - this.value = this.getFormat().format(new Date()); - } - - this._initHoursFormatParameters(); - } - - async _handleInputClick() { - if (this._isPickerOpen) { - return; - } - - const inputField = await this._getInputField(); - - if (inputField) { - inputField.select(); - } - } - - async _handleInputChange() { - const nextValue = await this._getInput().getInputValue(); - const isValid = this.isValid(nextValue); - - this.setValue(nextValue); - this.fireEvent("change", { value: nextValue, valid: isValid }); - this.fireEvent("value-changed", { value: nextValue, valid: isValid }); - } - - async _handleInputLiveChange() { - const nextValue = await this._getInput().getInputValue(); - const isValid = this.isValid(nextValue); - - this.value = nextValue; - this.setSlidersValue(); - this.fireEvent("input", { value: nextValue, valid: isValid }); - } - - setSlidersValue() { - const currentDate = this._getInput() ? this.getFormat().parse(this._getInput().getAttribute("value")) : null, - secondsSlider = this.secondsSlider, - minutesSlider = this.minutesSlider, - hoursSlider = this.hoursSlider, - periodsSlider = this.periodsSlider; - - if (!currentDate) { - return; - } - if (hoursSlider) { - let tempValue = ""; - if (this._hoursParameters.isTwelveHoursFormat && currentDate.getHours() > this._hoursParameters.maxHour) { - tempValue = currentDate.getHours() - 12; - } else if (this._hoursParameters.isTwelveHoursFormat && currentDate.getHours() < this._hoursParameters.minHour) { - tempValue = currentDate.getHours() + 12; - } else { - tempValue = currentDate.getHours(); - } - if (tempValue.toString().length === 1) { - hoursSlider.value = `0${tempValue}`; - } else { - hoursSlider.value = tempValue.toString(); - } - } - if (minutesSlider) { - const tempValue = currentDate.getMinutes(); - if (tempValue.toString().length === 1) { - minutesSlider.value = `0${tempValue}`; - } else { - minutesSlider.value = tempValue.toString(); - } - } - if (secondsSlider) { - const tempValue = currentDate.getSeconds(); - if (tempValue.toString().length === 1) { - secondsSlider.value = `0${tempValue}`; - } else { - secondsSlider.value = tempValue.toString(); - } - } - if (this._hoursParameters.isTwelveHoursFormat && periodsSlider && this._hoursParameters.minHour === 1) { - periodsSlider.value = currentDate.getHours() >= this._hoursParameters.maxHour ? this.periodsArray[1] : this.periodsArray[0]; - } else if (this._hoursParameters.isTwelveHoursFormat && periodsSlider) { - periodsSlider.value = (currentDate.getHours() > this._hoursParameters.maxHour || currentDate.getHours() === this._hoursParameters.minHour) ? this.periodsArray[1] : this.periodsArray[0]; - } - } - - /** - * Closes the picker - * @public - */ - async closePicker() { - await this._getPopover(); - this.responsivePopover.close(); - this._isPickerOpen = false; - - for (let i = 0; i < this._slidersDomRefs.length; i++) { - this._slidersDomRefs[i].collapseSlider(); - } - } - - /** - * Opens the picker. - * { focusInput: true } By default, the focus goes in the picker after opening it. - * Specify this option to focus the input field. - * @public - */ - async openPicker() { - await this._getPopover(); - this.responsivePopover.open(this); - this._isPickerOpen = true; - this._slidersDomRefs = await this.slidersDomRefs(); - - this.setSlidersValue(); - - if (this._slidersDomRefs[0]) { - this._slidersDomRefs[0].focus(); - } - } - - togglePicker() { - if (this.isOpen()) { - this.closePicker(); - this._isPickerOpen = false; - } else if (this._canOpenPicker()) { - this.openPicker(); - this._isPickerOpen = true; - } - } - - /** - * Checks if a value is valid against the current date format of the TimePicker - * @param {string} value A value to be tested against the current date format - * @public - */ - isOpen() { - return !!this._isPickerOpen; - } - - _canOpenPicker() { - return !this.disabled && !this.readonly; - } - - async _getPopover() { - const staticAreaItem = await this.getStaticAreaItemDomRef(); - this.responsivePopover = staticAreaItem.querySelector("[ui5-responsive-popover]"); - return this.responsivePopover; - } - - get secondsArray() { - return getSeconds(); - } - - get minutesArray() { - return getMinutes(); - } - - get hoursArray() { - return getHours(this._hoursParameters); - } - - get periodsArray() { - return this.getFormat().aDayPeriods.map(x => x.toUpperCase()); - } - - async slidersDomRefs() { - await this._getPopover(); - return this.responsivePopover.default.length ? [...this.responsivePopover.default[0].children].filter(x => x.isUI5Element) : this.responsivePopover.default; - } - - _getInput() { - return this.shadowRoot.querySelector("[ui5-input]"); - } - - _getInputField() { - const input = this._getInput(); - return input && input.getInputDOMRef(); - } - - get secondsSlider() { - return this.responsivePopover && this.responsivePopover.querySelector(".ui5-time-picker-seconds-wheelslider"); - } - - get minutesSlider() { - return this.responsivePopover && this.responsivePopover.querySelector(".ui5-time-picker-minutes-wheelslider"); - } - - get hoursSlider() { - return this.responsivePopover && this.responsivePopover.querySelector(".ui5-time-picker-hours-wheelslider"); - } - - get periodsSlider() { - return this.responsivePopover && this.responsivePopover.querySelector(".ui5-time-picker-period-wheelslider"); - } - - submitPickers() { - const selectedDate = new Date(), - secondsSlider = this.secondsSlider, - minutesSlider = this.minutesSlider, - hoursSlider = this.hoursSlider, - periodsSlider = this.periodsSlider, - minutes = minutesSlider ? minutesSlider.getAttribute("value") : "0", - seconds = secondsSlider ? secondsSlider.getAttribute("value") : "0", - period = periodsSlider ? periodsSlider.getAttribute("value") : this.periodsArray[0], - isTwelveHoursFormat = this._hoursParameters.isTwelveHoursFormat; - - let hours = hoursSlider ? hoursSlider.getAttribute("value") : this._hoursParameters.minHour.toString(); - - if (isTwelveHoursFormat) { - if (period === this.periodsArray[0]) { // AM - hours = hours === "12" ? 0 : hours; - } - - if (period === this.periodsArray[1]) { // PM - hours = hours === "12" ? hours : hours * 1 + 12; - } - } - - selectedDate.setHours(hours); - selectedDate.setMinutes(minutes); - selectedDate.setSeconds(seconds); - - this.setPrevValue(this.value); - this.setValue(this.getFormat().format(selectedDate)); - - if (this.prevValue !== this.value) { - this.fireEvent("change", { value: this.value, valid: true }); - this.previousValue = this.value; - } - - this.closePicker(); - } - - /** - * Checks if a value is valid against the current format patternt of the TimePicker. - * - *

    - * Note: an empty string is considered as valid value. - * @param {string} value The value to be tested against the current date format - * @public - */ - isValid(value) { - if (value === "") { - return true; - } - return !!(value && this.getFormat().parse(value)); - } - - normalizeValue(value) { - if (value === "") { - return value; - } - - return this.getFormat().format(this.getFormat().parse(value)); - } - get _formatPattern() { - return this.formatPattern || "medium"; // get from config - } + const hasHours = !!this.formatPattern.match(/H/i); + const fallback = !this.formatPattern || !hasHours; - get _isPattern() { - return this._formatPattern !== "medium" && this._formatPattern !== "short" && this._formatPattern !== "long"; + const localeData = getCachedLocaleDataInstance(getLocale()); + return fallback ? localeData.getTimePattern("medium") : this.formatPattern; } get _displayFormat() { @@ -619,204 +130,6 @@ class TimePicker extends UI5Element { return this.placeholder !== undefined ? this.placeholder : this._displayFormat; } - handleSliderClicked(event) { - if (event.target._expanded) { - this.openSlider(event.target.label); - } - } - - openSlider(label) { - for (let i = 0; i < this._slidersDomRefs.length; i++) { - if (this._slidersDomRefs[i].label !== label) { - this._slidersDomRefs[i].collapseSlider(); - } - } - } - - async _onfocuscontainerin(e) { - if (e.target !== e.currentTarget) { - return; - } - let sliders = []; - if (this._slidersDomRefs.length) { - sliders = await this.slidersDomRefs(); - } else { - sliders = this._slidersDomRefs; - } - if (sliders[0]) { - sliders[0].focus(); - } - } - - async _oncontainerkeydown(e) { - if (isLeft(e)) { - let expandedSliderIndex = 0; - for (let i = 0; i < this._slidersDomRefs.length; i++) { - if (this._slidersDomRefs[i]._expanded) { - expandedSliderIndex = i; - } - } - if (this._slidersDomRefs[expandedSliderIndex - 1]) { - this._slidersDomRefs[expandedSliderIndex - 1].focus(); - } else { - this._slidersDomRefs[this._slidersDomRefs.length - 1].focus(); - } - } else if (isRight(e)) { - let expandedSliderIndex = 0; - - for (let i = 0; i < this._slidersDomRefs.length; i++) { - if (this._slidersDomRefs[i]._expanded) { - expandedSliderIndex = i; - } - } - if (this._slidersDomRefs[expandedSliderIndex + 1]) { - this._slidersDomRefs[expandedSliderIndex + 1].focus(); - } else { - this._slidersDomRefs[0].focus(); - } - } - - if (isTabNext(e) && e.target === this._slidersDomRefs[this._slidersDomRefs.length - 1]) { - const responsivePopover = await this._getPopover(); - e.preventDefault(); - responsivePopover.querySelector(".ui5-time-picker-footer").firstElementChild.focus(); - } else if (isTabPrevious(e) && e.target === this._slidersDomRefs[0]) { - const responsivePopover = await this._getPopover(); - e.preventDefault(); - responsivePopover.querySelector(`.ui5-time-picker-footer`).lastElementChild.focus(); - } - } - - _onfooterkeydown(e) { - if (isTabNext(e) && e.target === e.target.parentElement.lastElementChild) { - e.preventDefault(); - this._slidersDomRefs[0].focus(); - } - - if (isTabPrevious(e) && e.target === e.target.parentElement.firstElementChild) { - e.preventDefault(); - this._slidersDomRefs[this._slidersDomRefs.length - 1].focus(); - } - } - - _ontimepickerkeydown(e) { - this._handleTimepickerKeysDown(e); - } - - _ontimepickerpopoverkeydown(e) { - this._handleTimepickerKeysDown(e); - } - - _handleTimepickerKeysDown(e) { - if (isShow(e)) { - e.preventDefault(); - this.togglePicker(); - } - - if (this.isOpen()) { - return; - } - - if (isPageUpShiftCtrl(e)) { - e.preventDefault(); - this._incrementValue(true, false, false, true); - } else if (isPageUpShift(e)) { - e.preventDefault(); - this._incrementValue(true, false, true, false); - } else if (isPageUp(e)) { - e.preventDefault(); - this._incrementValue(true, true, false, false); - } - - if (isPageDownShiftCtrl(e)) { - e.preventDefault(); - this._incrementValue(false, false, false, true); - } else if (isPageDownShift(e)) { - e.preventDefault(); - this._incrementValue(false, false, true, false); - } else if (isPageDown(e)) { - e.preventDefault(); - this._incrementValue(false, true, false, false); - } - } - - _incrementValue(increment, hours, minutes, seconds) { - const date = this.dateValue; - const incrementStep = increment ? 1 : -1; - - if (hours && this.shouldBuildHoursSlider) { - date.setHours(date.getHours() + incrementStep); - } else if (minutes && this.shouldBuildMinutesSlider) { - date.setMinutes(date.getMinutes() + incrementStep); - } else if (seconds && this.shouldBuildSecondsSlider) { - date.setSeconds(date.getSeconds() + incrementStep); - } else { - return; - } - - this.setValue(this.formatValue(date)); - this.fireEvent("change", { value: this.value, valid: true }); - } - - _handleWheel(e) { - e.preventDefault(); - } - - getFormat() { - let dateFormat; - if (this._isPattern) { - dateFormat = DateFormat.getInstance({ - pattern: this._formatPattern, - }); - } else { - dateFormat = DateFormat.getInstance({ - style: this._formatPattern, - }); - } - - return dateFormat; - } - - setValue(value) { - if (this.isValid(value)) { - this.value = this.normalizeValue(value); - this.setSlidersValue(); - this.valueState = ValueState.None; - } else { - this.valueState = ValueState.Error; - } - } - - setPrevValue(value) { - if (this.isValid(value)) { - this.prevValue = this.normalizeValue(value); - } - } - - /** - * Formats a Java Script date object into a string representing a locale date and time - * according to the formatPattern property of the TimePicker instance - * @param {object} oDate A Java Script date object to be formatted as string - * @public - */ - formatValue(oDate) { - return this.getFormat().format(oDate); - } - - _getSlidersContained() { - const formatArray = this.getFormat().aFormatArray; - return getTimeControlsByFormat(formatArray, this._hoursParameters); - } - - _initHoursFormatParameters() { - const formatArray = this.getFormat().aFormatArray; - const config = getHoursConfigByFormat(formatArray[0].type); - - this._hoursParameters.minHour = config.minHour; - this._hoursParameters.maxHour = config.maxHour; - this._hoursParameters.isTwelveHoursFormat = config.isTwelveHoursFormat; - } - /** * Currently selected date represented as JavaScript Date instance * @@ -825,56 +138,7 @@ class TimePicker extends UI5Element { * @public */ get dateValue() { - return this.getFormat().parse(this.value); - } - - get shouldBuildHoursSlider() { - return this._getSlidersContained()[0]; - } - - get shouldBuildMinutesSlider() { - return this._getSlidersContained()[1]; - } - - get shouldBuildSecondsSlider() { - return this._getSlidersContained()[2]; - } - - get shouldBuildPeriodsSlider() { - return this._getSlidersContained()[3]; - } - - get hoursSliderTitle() { - return this.i18nBundle.getText(TIMEPICKER_HOURS_LABEL); - } - - get minutesSliderTitle() { - return this.i18nBundle.getText(TIMEPICKER_MINUTES_LABEL); - } - - get secondsSliderTitle() { - return this.i18nBundle.getText(TIMEPICKER_SECONDS_LABEL); - } - - get periodSliderTitle() { - return this.i18nBundle.getText(TIMEPICKER_PERIODS_LABEL); - } - - get submitButtonLabel() { - return this.i18nBundle.getText(TIMEPICKER_SUBMIT_BUTTON); - } - - get cancelButtonLabel() { - return this.i18nBundle.getText(TIMEPICKER_CANCEL_BUTTON); - } - - get classes() { - return { - container: { - "ui5-time-picker-sliders-container": true, - "ui5-phone": isPhone(), - }, - }; + return this.getFormat().parse(this._effectiveValue); } } diff --git a/packages/main/src/TimePickerBase.js b/packages/main/src/TimePickerBase.js new file mode 100644 index 000000000000..107288a7d39a --- /dev/null +++ b/packages/main/src/TimePickerBase.js @@ -0,0 +1,455 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; +import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; +import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; +import "@ui5/webcomponents-localization/dist/features/calendar/Gregorian.js"; // default calendar for bundling +import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; +import { fetchCldr } from "@ui5/webcomponents-base/dist/asset-registries/LocaleData.js"; +import { + isShow, + isPageUp, + isPageDown, + isPageUpShift, + isPageDownShift, + isPageUpShiftCtrl, + isPageDownShiftCtrl, +} from "@ui5/webcomponents-base/dist/Keys.js"; +import "@ui5/webcomponents-icons/dist/time-entry-request.js"; +import Icon from "./Icon.js"; +import ResponsivePopover from "./ResponsivePopover.js"; +import TimePickerTemplate from "./generated/templates/TimePickerTemplate.lit.js"; +import TimePickerPopoverTemplate from "./generated/templates/TimePickerPopoverTemplate.lit.js"; +import Input from "./Input.js"; +import Button from "./Button.js"; +import TimeSelection from "./TimeSelection.js"; + +import { + TIMEPICKER_SUBMIT_BUTTON, + TIMEPICKER_CANCEL_BUTTON, +} from "./generated/i18n/i18n-defaults.js"; + +// Styles +import TimePickerCss from "./generated/themes/TimePicker.css.js"; +import TimePickerPopoverCss from "./generated/themes/TimePickerPopover.css.js"; +import ResponsivePopoverCommonCss from "./generated/themes/ResponsivePopoverCommon.css.js"; + +/** + * @public + */ +const metadata = { + languageAware: true, + managedSlots: true, + properties: /** @lends sap.ui.webcomponents.main.TimePickerBase.prototype */ { + /** + * Defines a formatted time value. + * + * @type {string} + * @defaultvalue undefined + * @public + */ + value: { + type: String, + defaultValue: undefined, + }, + + /** + * Defines the value state of the ui5-time-picker. + *

    + * Available options are: + *
      + *
    • None
    • + *
    • Error
    • + *
    • Warning
    • + *
    • Success
    • + *
    • Information
    • + *
    + * + * @type {ValueState} + * @defaultvalue "None" + * @public + */ + valueState: { + type: ValueState, + defaultValue: ValueState.None, + }, + + /** + * Determines whether the ui5-time-picker is displayed as disabled. + * + * @type {boolean} + * @defaultvalue false + * @public + */ + disabled: { + type: Boolean, + }, + + /** + * Determines whether the ui5-time-picker is displayed as readonly. + * + * @type {boolean} + * @defaultvalue false + * @public + */ + readonly: { + type: Boolean, + }, + + /** + * @private + */ + _isPickerOpen: { + type: Boolean, + noAttribute: true, + }, + }, + slots: /** @lends sap.ui.webcomponents.main.TimePickerBase.prototype */ { + /** + * Defines the value state message that will be displayed as pop up under the ui5-time-picker. + *

    + * + * Note: If not specified, a default text (in the respective language) will be displayed. + *
    + * Note: The valueStateMessage would be displayed, + * when the ui5-time-picker is in Information, Warning or Error value state. + * @type {HTMLElement} + * @since 1.0.0-rc.8 + * @slot + * @public + */ + valueStateMessage: { + type: HTMLElement, + }, + }, + events: /** @lends sap.ui.webcomponents.main.TimePickerBase.prototype */ { + /** + * Fired when the input operation has finished by clicking the "OK" button or + * when the text in the input field has changed and the focus leaves the input field. + * + * @event + * @public + */ + change: {}, + /** + * Fired when the value of the ui5-time-picker is changed at each key stroke. + * + * @event + * @public + */ + input: {}, + }, +}; + +/** + * @class + * + * @constructor + * @author SAP SE + * @alias sap.ui.webcomponents.main.TimePickerBase + * @extends UI5Element + * @public + * @since 1.0.0-rc.6 + */ +class TimePickerBase extends UI5Element { + static get metadata() { + return metadata; + } + + static get render() { + return litRender; + } + + static get styles() { + return TimePickerCss; + } + + static get staticAreaTemplate() { + return TimePickerPopoverTemplate; + } + + static get template() { + return TimePickerTemplate; + } + + static get dependencies() { + return [ + Icon, + ResponsivePopover, + TimeSelection, + Input, + Button, + ]; + } + + static async onDefine() { + await Promise.all([ + fetchCldr(getLocale().getLanguage(), getLocale().getRegion(), getLocale().getScript()), + fetchI18nBundle("@ui5/webcomponents"), + ]); + } + + static get staticAreaStyles() { + return [ResponsivePopoverCommonCss, TimePickerPopoverCss]; + } + + constructor() { + super(); + this.i18nBundle = getI18nBundle("@ui5/webcomponents"); + } + + /** + * @abstract + * @protected + */ + get _placeholder() { + return undefined; + } + + /** + * @abstract + * @protected + */ + get _formatPattern() { + return undefined; + } + + get _effectiveValue() { + return this.value; + } + + get _timeSelectionValue() { + return this.tempValue; + } + + onTimeSelectionChange(event) { + this.tempValue = event.detail.value; // every time the user changes the sliders -> update tempValue + } + + submitPickers() { + this._updateValueAndFireEvents(this.tempValue, true, ["change", "value-changed"]); + this.closePicker(); + } + + onResponsivePopoverAfterClose() { + this._isPickerOpen = false; + } + + async _handleInputClick() { + if (this._isPickerOpen) { + return; + } + + const inputField = await this._getInputField(); + + if (inputField) { + inputField.select(); + } + } + + _updateValueAndFireEvents(value, normalizeValue, events) { + if (value === this.value) { + return; + } + + const valid = this.isValid(value); + if (valid && normalizeValue) { + value = this.normalizeValue(value); // transform valid values (in any format) to the correct format + } + + this.value = ""; // Do not remove! DurationPicker use case -> value is 05:10, user tries 05:12, after normalization value is changed back to 05:10 so no invalidation happens, but the input still shows 05:12. Thus we enforce invalidation with the "" + this.value = value; + this.tempValue = value; // if the picker is open, sync it + this._updateValueState(); // Change the value state to Error/None, but only if needed + events.forEach(event => { + this.fireEvent(event, { value, valid }); + }); + } + + _updateValueState() { + const isValid = this.isValid(this.value); + if (!isValid) { // If not valid - always set Error regardless of the current value state + this.valueState = ValueState.Error; + } else if (isValid && this.valueState === ValueState.Error) { // However if valid, change only Error (but not the others) to None + this.valueState = ValueState.None; + } + } + + async _handleInputChange(event) { + this._updateValueAndFireEvents(event.target.value, true, ["change", "value-changed"]); + } + + async _handleInputLiveChange(event) { + this._updateValueAndFireEvents(event.target.value, false, ["input"]); + } + + /** + * Closes the picker + * @public + */ + async closePicker() { + const responsivePopover = await this._getPopover(); + responsivePopover.close(); + this._isPickerOpen = false; + } + + /** + * Opens the picker. + * { focusInput: true } By default, the focus goes in the picker after opening it. + * Specify this option to focus the input field. + * @public + */ + async openPicker() { + this.tempValue = this.value && this.isValid(this.value) ? this.value : this.getFormat().format(new Date()); + const responsivePopover = await this._getPopover(); + responsivePopover.open(this); + this._isPickerOpen = true; + } + + togglePicker() { + if (this.isOpen()) { + this.closePicker(); + } else if (this._canOpenPicker()) { + this.openPicker(); + } + } + + /** + * Checks if a value is valid against the current date format of the TimePicker + * @public + */ + isOpen() { + return !!this._isPickerOpen; + } + + _canOpenPicker() { + return !this.disabled && !this.readonly; + } + + async _getPopover() { + const staticAreaItem = await this.getStaticAreaItemDomRef(); + return staticAreaItem.querySelector("[ui5-responsive-popover]"); + } + + _getInput() { + return this.shadowRoot.querySelector("[ui5-input]"); + } + + _getInputField() { + const input = this._getInput(); + return input && input.getInputDOMRef(); + } + + _onkeydown(e) { + if (isShow(e)) { + e.preventDefault(); + this.togglePicker(); + } else if (isPageUpShiftCtrl(e)) { + e.preventDefault(); + this._modifyValueBy(1, "second"); + } else if (isPageUpShift(e)) { + e.preventDefault(); + this._modifyValueBy(1, "minute"); + } else if (isPageUp(e)) { + e.preventDefault(); + this._modifyValueBy(1, "hour"); + } else if (isPageDownShiftCtrl(e)) { + e.preventDefault(); + this._modifyValueBy(-1, "second"); + } else if (isPageDownShift(e)) { + e.preventDefault(); + this._modifyValueBy(-1, "minute"); + } else if (isPageDown(e)) { + e.preventDefault(); + this._modifyValueBy(-1, "hour"); + } + } + + get _isPattern() { + return this._formatPattern !== "medium" && this._formatPattern !== "short" && this._formatPattern !== "long"; + } + + getFormat() { + let dateFormat; + if (this._isPattern) { + dateFormat = DateFormat.getInstance({ + pattern: this._formatPattern, + }); + } else { + dateFormat = DateFormat.getInstance({ + style: this._formatPattern, + }); + } + + return dateFormat; + } + + /** + * Formats a Java Script date object into a string representing a locale date and time + * according to the formatPattern property of the TimePicker instance + * @param {object} oDate A Java Script date object to be formatted as string + * @public + */ + formatValue(oDate) { + return this.getFormat().format(oDate); + } + + /** + * Checks if a value is valid against the current format patternt of the TimePicker. + * + *

    + * Note: an empty string is considered as valid value. + * @param {string} value The value to be tested against the current date format + * @public + */ + isValid(value) { + return value === "" || this.getFormat().parse(value); + } + + normalizeValue(value) { + if (value === "") { + return value; + } + + return this.getFormat().format(this.getFormat().parse(value)); + } + + _modifyValueBy(amount, unit) { + const date = this.getFormat().parse(this._effectiveValue); + if (!date) { + return; + } + + if (unit === "hour") { + date.setHours(date.getHours() + amount); + } else if (unit === "minute") { + date.setMinutes(date.getMinutes() + amount); + } else if (unit === "second") { + date.setSeconds(date.getSeconds() + amount); + } + + const newValue = this.formatValue(date); + this._updateValueAndFireEvents(newValue, true, ["change", "value-changed"]); + } + + _handleWheel(e) { + e.preventDefault(); + } + + get submitButtonLabel() { + return this.i18nBundle.getText(TIMEPICKER_SUBMIT_BUTTON); + } + + get cancelButtonLabel() { + return this.i18nBundle.getText(TIMEPICKER_CANCEL_BUTTON); + } + + /** + * @protected + */ + get openIconName() { + return "time-entry-request"; + } +} + +export default TimePickerBase; diff --git a/packages/main/src/TimePickerPopover.hbs b/packages/main/src/TimePickerPopover.hbs index 04043c292940..ef00adb0ae03 100644 --- a/packages/main/src/TimePickerPopover.hbs +++ b/packages/main/src/TimePickerPopover.hbs @@ -1,58 +1,34 @@ -
    - {{#if shouldBuildHoursSlider}} - - {{/if}} - {{#if shouldBuildMinutesSlider}} - - {{/if}} - {{#if shouldBuildSecondsSlider}} - - {{/if}} - {{#if shouldBuildPeriodsSlider}} - - {{/if}} + + + + - - \ No newline at end of file + diff --git a/packages/main/src/TimeSelection.hbs b/packages/main/src/TimeSelection.hbs new file mode 100644 index 000000000000..15f79063a557 --- /dev/null +++ b/packages/main/src/TimeSelection.hbs @@ -0,0 +1,60 @@ +
    + {{#if _hasHoursSlider}} + + {{/if}} + {{#if _hasMinutesSlider}} + + {{/if}} + {{#if _hasSecondsSlider}} + + {{/if}} + {{#if _hasPeriodsSlider}} + + {{/if}} +
    diff --git a/packages/main/src/TimeSelection.js b/packages/main/src/TimeSelection.js new file mode 100644 index 000000000000..151ab85fcbb8 --- /dev/null +++ b/packages/main/src/TimeSelection.js @@ -0,0 +1,494 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; +import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; +import { isPhone, isIE } from "@ui5/webcomponents-base/dist/Device.js"; +import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; +import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; +import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; +import "@ui5/webcomponents-localization/dist/features/calendar/Gregorian.js"; // default calendar for bundling +import { fetchCldr } from "@ui5/webcomponents-base/dist/asset-registries/LocaleData.js"; +import { + isLeft, + isRight, +} from "@ui5/webcomponents-base/dist/Keys.js"; +import "@ui5/webcomponents-icons/dist/time-entry-request.js"; +import timeSelectionTemplate from "./generated/templates/TimeSelectionTemplate.lit.js"; +import WheelSlider from "./WheelSlider.js"; +import { + getHours, + getMinutes, + getSeconds, + getHoursConfigByFormat, + getTimeControlsByFormat, +} from "./timepicker-utils/TimeSlider.js"; + +import { + TIMEPICKER_HOURS_LABEL, + TIMEPICKER_MINUTES_LABEL, + TIMEPICKER_SECONDS_LABEL, + TIMEPICKER_PERIODS_LABEL, +} from "./generated/i18n/i18n-defaults.js"; + +// Styles +import timeSelectionCss from "./generated/themes/TimeSelection.css.js"; + +const capitalizeFirst = str => str.substr(0, 1).toUpperCase() + str.substr(1); + +/** + * @public + */ +const metadata = { + tag: "ui5-time-selection", + languageAware: true, + managedSlots: true, + properties: /** @lends sap.ui.webcomponents.main.TimeSelection.prototype */ { + /** + * Defines a formatted time value. + * + * @type {string} + * @defaultvalue undefined + * @public + */ + value: { + type: String, + defaultValue: undefined, + }, + + /** + * Determines the format, displayed in the input field. + * + * Example: + * HH:mm:ss -> 11:42:35 + * hh:mm:ss a -> 2:23:15 PM + * mm:ss -> 12:04 (only minutes and seconds) + * + * @type {string} + * @defaultvalue "" + * @public + */ + formatPattern: { + type: String, + }, + + /** + * Hides the hours slider regardless of formatPattern + * This property is only needed for the duration picker use case which requires non-standard slider combinations + * @public + */ + hideHours: { + type: Boolean, + }, + + /** + * Hides the minutes slider regardless of formatPattern + * This property is only needed for the duration picker use case which requires non-standard slider combinations + * @public + */ + hideMinutes: { + type: Boolean, + }, + + /** + * Hides the seconds slider regardless of formatPattern + * This property is only needed for the duration picker use case which requires non-standard slider combinations + * @public + */ + hideSeconds: { + type: Boolean, + }, + + /** + * The maximum number of hours to be displayed for the hours slider (only needed for the duration picker use case) + * @public + */ + maxHours: { + type: Integer, + }, + + /** + * The maximum number of minutes to be displayed for the minutes slider (only needed for the duration picker use case) + * @public + */ + maxMinutes: { + type: Integer, + }, + + /** + * The maximum number of seconds to be displayed for the seconds slider (only needed for the duration picker use case) + * @public + */ + maxSeconds: { + type: Integer, + }, + + secondsStep: { + type: Integer, + defaultValue: 1, + }, + + minutesStep: { + type: Integer, + defaultValue: 1, + }, + + _currentSlider: { + type: String, + defaultValue: "hours", + }, + }, + events: /** @lends sap.ui.webcomponents.main.TimeSelection.prototype */ { + /** + * Fired when the value changes due to user interaction with the sliders + */ + change: {}, + + /** + * Fired when the expanded/collapsed slider changes (a new slider is expanded or the expanded slider is collapsed) + */ + sliderChange: {}, + }, +}; + +/** + * @class + * + * @constructor + * @author SAP SE + * @alias sap.ui.webcomponents.main.TimeSelection + * @extends UI5Element + * @tagname ui5-time-selection + * @private + * @since 1.0.0-rc.12 + */ +class TimeSelection extends UI5Element { + static get metadata() { + return metadata; + } + + static get render() { + return litRender; + } + + static get styles() { + return timeSelectionCss; + } + + static get template() { + return timeSelectionTemplate; + } + + static get dependencies() { + return [WheelSlider]; + } + + static async onDefine() { + await Promise.all([ + fetchCldr(getLocale().getLanguage(), getLocale().getRegion(), getLocale().getScript()), + fetchI18nBundle("@ui5/webcomponents"), + ]); + } + + constructor() { + super(); + this.i18nBundle = getI18nBundle("@ui5/webcomponents"); + } + + get _hoursConfiguration() { + const hourFormat = this.getFormat().aFormatArray.find(item => item.type.startsWith("hour")); // try to find an entry for the hours + return getHoursConfigByFormat(hourFormat ? hourFormat.type : "hour0_23"); + } + + get _neededSliders() { + const formatArray = this.getFormat().aFormatArray; + return getTimeControlsByFormat(formatArray, this._hoursConfiguration); + } + + get _hasHoursSlider() { + return this._neededSliders[0] && !this.hideHours; + } + + get _hasMinutesSlider() { + return this._neededSliders[1] && !this.hideMinutes; + } + + get _hasSecondsSlider() { + return this._neededSliders[2] && !this.hideSeconds; + } + + get _hasPeriodsSlider() { + return this._neededSliders[3]; + } + + get secondsArray() { + return getSeconds(this.maxSeconds ? this.maxSeconds + 1 : undefined, this.secondsStep); + } + + get minutesArray() { + return getMinutes(this.maxMinutes ? this.maxMinutes + 1 : undefined, this.minutesStep); + } + + get hoursArray() { + return getHours(this._hoursConfiguration, this.maxHours ? this.maxHours + 1 : undefined); + } + + get periodsArray() { + return this.getFormat().aDayPeriods.map(x => x.toUpperCase()); + } + + get _hoursSliderFocused() { + return this._currentSlider === "hours"; + } + + get _minutesSliderFocused() { + return this._currentSlider === "minutes"; + } + + get _secondsSliderFocused() { + return this._currentSlider === "seconds"; + } + + get _periodSliderFocused() { + return this._currentSlider === "period"; + } + + get _hours() { + let hours; + const dateValue = this.validDateValue; + if (this._hoursConfiguration.isTwelveHoursFormat && dateValue.getHours() > this._hoursConfiguration.maxHour) { + hours = dateValue.getHours() - 12; + } else if (this._hoursConfiguration.isTwelveHoursFormat && dateValue.getHours() < this._hoursConfiguration.minHour) { + hours = dateValue.getHours() + 12; + } else { + hours = dateValue.getHours(); + } + if (hours.toString().length === 1) { + hours = `0${hours}`; + } + return hours.toString(); + } + + get _minutes() { + const minutes = this.validDateValue.getMinutes().toString(); + return minutes.length === 1 ? `0${minutes}` : minutes; + } + + get _seconds() { + const seconds = this.validDateValue.getSeconds().toString(); + return seconds.length === 1 ? `0${seconds}` : seconds; + } + + get _period() { + if (!this._hoursConfiguration.isTwelveHoursFormat) { + return undefined; + } + + let period; + const dateValue = this.validDateValue; + if (this._hoursConfiguration.minHour === 1) { + period = dateValue.getHours() >= this._hoursConfiguration.maxHour ? this.periodsArray[1] : this.periodsArray[0]; + } else { + period = (dateValue.getHours() > this._hoursConfiguration.maxHour || dateValue.getHours() === this._hoursConfiguration.minHour) ? this.periodsArray[1] : this.periodsArray[0]; + } + return period; + } + + setValue(date) { + const value = this.formatValue(date); + if (this.isValid(value)) { + this.value = this.normalizeValue(value); + this.fireEvent("change", { value: this.value, valid: true }); + } + } + + onHoursChange(event) { + let hours = event.detail.value; + const isTwelveHoursFormat = this._hoursConfiguration.isTwelveHoursFormat; + + if (isTwelveHoursFormat) { + if (this._period === this.periodsArray[0]) { // AM + hours = hours === "12" ? 0 : hours; + } + + if (this._period === this.periodsArray[1]) { // PM + hours = hours === "12" ? hours : hours * 1 + 12; + } + } + + const date = this.validDateValue; + date.setHours(hours); + this.setValue(date); + } + + onMinutesChange(event) { + const minutes = event.detail.value; + const date = this.validDateValue; + date.setMinutes(minutes); + this.setValue(date); + } + + onSecondsChange(event) { + const seconds = event.detail.value; + const date = this.validDateValue; + date.setSeconds(seconds); + this.setValue(date); + } + + onPeriodChange(event) { + const period = event.detail.value; + const date = this.validDateValue; + if (period === this.periodsArray[0] && date.getHours() >= 12) { + date.setHours(date.getHours() - 12); + } if (period === this.periodsArray[1] && date.getHours() < 12) { + date.setHours(date.getHours() + 12); + } + this.setValue(date); + } + + isValid(value) { + return value === "" || this.getFormat().parse(value); + } + + normalizeValue(value) { + if (value === "") { + return value; + } + + return this.getFormat().format(this.getFormat().parse(value)); + } + + get _formatPattern() { + const pattern = this.formatPattern; + const hasHours = !!pattern.match(/H/i); + const fallback = !pattern || !hasHours; + + const localeData = getCachedLocaleDataInstance(getLocale()); + return fallback ? localeData.getCombinedDateTimePattern("medium", "medium", this._primaryCalendarType) : pattern; + } + + get _isPattern() { + return this._formatPattern !== "medium" && this._formatPattern !== "short" && this._formatPattern !== "long"; + } + + /** + * Event handler for the "click" and "focusin" events of the sliders + * @param event + */ + selectSlider(event) { + this._setCurrentSlider(event.target.closest("[ui5-wheelslider]").getAttribute("data-sap-slider")); + } + + _setCurrentSlider(slider) { + if (this._currentSlider === slider) { + return; + } + this._currentSlider = slider; + this.fireEvent("slider-change", { slider }); + } + + get _currentSliderDOM() { + return this.shadowRoot.querySelector(`[data-sap-slider="${this._currentSlider}"]`); + } + + get _activeSliders() { + return ["hours", "minutes", "seconds", "period"].filter(slider => this[`_has${capitalizeFirst(slider)}Slider`]); + } + + _onfocusin(event) { + if (!this._currentSlider) { + this._setCurrentSlider(this._activeSliders[0]); + } + + if (event.target === event.currentTarget) { + this._currentSliderDOM.focus(); + } + } + + _onfocusout(event) { + if (!this.shadowRoot.contains(event.relatedTarget)) { + this._setCurrentSlider(""); + } + } + + async _onkeydown(event) { + if (!(isLeft(event) || isRight(event))) { + return; + } + event.preventDefault(); + + const activeSliders = this._activeSliders; + const activeSlider = event.target.closest("[ui5-wheelslider]").getAttribute("data-sap-slider"); + let index = activeSliders.indexOf(activeSlider); + if (isLeft(event)) { + index = index === 0 ? activeSliders.length - 1 : index - 1; + } else if (isRight(event)) { + index = index === activeSliders.length - 1 ? 0 : index + 1; + } + this._setCurrentSlider(activeSliders[index]); + this._currentSliderDOM.focus(); + } + + _handleWheel(e) { + e.preventDefault(); + } + + getFormat() { + let dateFormat; + if (this._isPattern) { + dateFormat = DateFormat.getInstance({ + pattern: this._formatPattern, + }); + } else { + dateFormat = DateFormat.getInstance({ + style: this._formatPattern, + }); + } + + return dateFormat; + } + + formatValue(oDate) { + return this.getFormat().format(oDate); + } + + get dateValue() { + return this.value ? this.getFormat().parse(this.value) : new Date(); + } + + get validDateValue() { + return this.isValid(this.value) ? this.dateValue : new Date(); + } + + get hoursSliderTitle() { + return this.i18nBundle.getText(TIMEPICKER_HOURS_LABEL); + } + + get minutesSliderTitle() { + return this.i18nBundle.getText(TIMEPICKER_MINUTES_LABEL); + } + + get secondsSliderTitle() { + return this.i18nBundle.getText(TIMEPICKER_SECONDS_LABEL); + } + + get periodSliderTitle() { + return this.i18nBundle.getText(TIMEPICKER_PERIODS_LABEL); + } + + get _isCyclic() { + return !isIE(); + } + + get classes() { + return { + root: { + "ui5-time-selection-root": true, + "ui5-phone": isPhone(), + }, + }; + } +} + +TimeSelection.define(); + +export default TimeSelection; diff --git a/packages/main/src/WheelSlider.hbs b/packages/main/src/WheelSlider.hbs index f358bf164ad2..088e623fb2fa 100644 --- a/packages/main/src/WheelSlider.hbs +++ b/packages/main/src/WheelSlider.hbs @@ -3,13 +3,10 @@ ?disabled= "{{disabled}}" value = "{{value}}" label = "{{label}}" - ?expanded= "{{_expanded}}" @click = {{_onclick}} @keydown={{_onkeydown}} class = "{{classes.root}}" data-sap-focus-ref - @focusin="{{_onfocusin}}" - @focusout="{{_onfocusout}}" tabindex="0" @wheel="{{_handleWheel}}" > @@ -19,10 +16,9 @@
    -
    -
    +
    - {{#if _expanded}} + {{#if expanded}}
      {{#each _itemsToShow}}
    • {{this}}
    • diff --git a/packages/main/src/WheelSlider.js b/packages/main/src/WheelSlider.js index 2cd4763ff4ca..8c151083207e 100644 --- a/packages/main/src/WheelSlider.js +++ b/packages/main/src/WheelSlider.js @@ -62,15 +62,16 @@ const metadata = { * Indicates if the wheelslider is expanded. * @type {boolean} * @defaultvalue false - * @private + * @public */ - _expanded: { + expanded: { type: Boolean, }, _items: { type: String, multiple: true, + compareValues: true, }, _itemsToShow: { @@ -113,6 +114,9 @@ const metadata = { }, }; +const CELL_SIZE_COMPACT = 2; +const CELL_SIZE_COZY = 2.875; + /** * @class * @@ -154,7 +158,6 @@ class WheelSlider extends UI5Element { constructor() { super(); this._currentElementIndex = 0; - this._itemCellHeight = 0; this._itemsToShow = []; this._scroller = new ScrollEnablement(this); this._scroller.attachEvent("scroll", this._updateScrolling.bind(this)); @@ -163,7 +166,7 @@ class WheelSlider extends UI5Element { } onBeforeRendering() { - if (!this._expanded && this.cyclic) { + if (!this.expanded && this.cyclic) { const index = this._currentElementIndex % this._items.length; this._currentElementIndex = (this._timesMultipliedOnCyclic() / 2) * this._items.length + index; } @@ -173,7 +176,6 @@ class WheelSlider extends UI5Element { } this._buildItemsToShow(); - this._updateItemCellHeight(); } static get dependencies() { @@ -185,11 +187,11 @@ class WheelSlider extends UI5Element { this._scroller.scrollContainer = this.shadowRoot.querySelector(`#${this._id}--wrapper`); } - if (!this._expanded) { + if (!this.expanded) { this._scroller.scrollTo(0, 0); } - if (this._expanded) { + if (this.expanded) { const elements = this.shadowRoot.querySelectorAll(".ui5-wheelslider-item"); for (let i = 0; i < elements.length; i++) { if (elements[i].textContent === this.value) { @@ -212,27 +214,30 @@ class WheelSlider extends UI5Element { } expandSlider() { - this._expanded = true; + this.expanded = true; this.fireEvent("expand", {}); } collapseSlider() { - this._expanded = false; + this.expanded = false; this.fireEvent("collapse", {}); } - _updateItemCellHeight() { + get _itemCellHeight() { + const defaultSize = this.isCompact ? CELL_SIZE_COMPACT : CELL_SIZE_COZY; + if (this.shadowRoot.querySelectorAll(".ui5-wheelslider-item").length) { const itemComputedStyle = getComputedStyle(this.shadowRoot.querySelector(".ui5-wheelslider-item")); const itemHeightValue = itemComputedStyle.getPropertyValue("--_ui5_wheelslider_item_height"); const onlyDigitsValue = itemHeightValue.replace("rem", ""); - - this._itemCellHeight = Number(onlyDigitsValue); + return Number(onlyDigitsValue) || defaultSize; } + + return defaultSize; } _updateScrolling() { - const sizeOfOneElementInPixels = this._itemCellHeight * 16, + const cellSizeInPx = this._itemCellHeight * 16, scrollWhere = this._scroller.scrollContainer.scrollTop; let offsetIndex; @@ -240,7 +245,7 @@ class WheelSlider extends UI5Element { return; } - offsetIndex = Math.round(scrollWhere / sizeOfOneElementInPixels); + offsetIndex = Math.round(scrollWhere / cellSizeInPx); if (this.value === this._itemsToShow[offsetIndex]) { return; @@ -258,7 +263,7 @@ class WheelSlider extends UI5Element { } _handleScrollTouchEnd() { - if (this._expanded) { + if (this.expanded) { this._selectElementByIndex(this._currentElementIndex); } } @@ -281,18 +286,15 @@ class WheelSlider extends UI5Element { _selectElementByIndex(currentIndex) { let index = currentIndex; const itemsCount = this._itemsToShow.length; - const sizeOfCellInCompactInRem = 2; - const sizeOfCellInCozyInRem = 2.875; - const sizeOfCellInCompactInPixels = sizeOfCellInCompactInRem * 16; - const sizeOfCellInCozyInPixels = sizeOfCellInCozyInRem * 16; - const scrollBy = this.isCompact ? sizeOfCellInCompactInPixels * index : sizeOfCellInCozyInPixels * index; + const cellSizeInPx = this._itemCellHeight * 16; + const scrollBy = cellSizeInPx * index; if (this.cyclic) { index = this._handleArrayBorderReached(index); } if (index < itemsCount && index > -1) { - this._scroller.scrollTo(0, scrollBy); + this._scroller.scrollTo(0, scrollBy, 5, 100); // sometimes the container isn't painted yet so retry 5 times (although it succeeds on the 1st) this._currentElementIndex = index; this.value = this._items[index - (this._getCurrentRepetition() * this._items.length)]; this.fireEvent("select", { value: this.value }); @@ -340,7 +342,7 @@ class WheelSlider extends UI5Element { e.stopPropagation(); e.preventDefault(); - if (e.timeStamp === this._prevWheelTimestamp || !this._expanded) { + if (e.timeStamp === this._prevWheelTimestamp || !this.expanded) { return; } @@ -358,12 +360,12 @@ class WheelSlider extends UI5Element { return; } - if (this._expanded) { + if (this.expanded) { this.value = e.target.textContent; this._selectElement(e.target); this.fireEvent("select", { value: this.value }); } else { - this._expanded = true; + this.expanded = true; } } @@ -388,7 +390,7 @@ class WheelSlider extends UI5Element { } _onkeydown(е) { - if (!this._expanded) { + if (!this.expanded) { return; } @@ -418,16 +420,6 @@ class WheelSlider extends UI5Element { this._selectElementByIndex(intexIncrease); } } - - _onfocusin(e) { - e.preventDefault(); - this.expandSlider(); - } - - _onfocusout(e) { - e.preventDefault(); - this.collapseSlider(); - } } WheelSlider.define(); diff --git a/packages/main/src/YearPicker.hbs b/packages/main/src/YearPicker.hbs index 1b5de724ed03..d9d639c67ed8 100644 --- a/packages/main/src/YearPicker.hbs +++ b/packages/main/src/YearPicker.hbs @@ -3,20 +3,22 @@ role="grid" aria-readonly="false" aria-multiselectable="false" - style="{{styles.main}}" @keydown={{_onkeydown}} - @mousedown={{_onmousedown}} - @mouseup={{_onmouseup}} + @keyup={{_onkeyup}} + @click={{_selectYear}} + @focusin={{_onfocusin}} > - {{#each _yearIntervals}} + {{#each _years}}
      {{#each this}} -
      + aria-selected="{{this.ariaSelected}}" + > {{this.year}}
      {{/each}} diff --git a/packages/main/src/YearPicker.js b/packages/main/src/YearPicker.js index 13677a3a923b..827a581b7a20 100644 --- a/packages/main/src/YearPicker.js +++ b/packages/main/src/YearPicker.js @@ -1,14 +1,23 @@ import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; -import { isEnter, isSpace } from "@ui5/webcomponents-base/dist/Keys.js"; -import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js"; +import { + isEnter, + isSpace, + isDown, + isUp, + isLeft, + isRight, + isHome, + isEnd, + isHomeCtrl, + isEndCtrl, + isPageUp, + isPageDown, +} from "@ui5/webcomponents-base/dist/Keys.js"; import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; -import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; -import ItemNavigationBehavior from "@ui5/webcomponents-base/dist/types/ItemNavigationBehavior.js"; -import PickerBase from "./PickerBase.js"; +import { getMaxCalendarDate } from "@ui5/webcomponents-localization/dist/dates/ExtremeDates.js"; +import CalendarPart from "./CalendarPart.js"; import YearPickerTemplate from "./generated/templates/YearPickerTemplate.lit.js"; - -// Styles import styles from "./generated/themes/YearPicker.css.js"; /** @@ -17,12 +26,7 @@ import styles from "./generated/themes/YearPicker.css.js"; const metadata = { tag: "ui5-yearpicker", properties: /** @lends sap.ui.webcomponents.main.YearPicker.prototype */ { - _selectedYear: { - type: Integer, - noAttribute: true, - }, - - _yearIntervals: { + _years: { type: Object, multiple: true, }, @@ -34,13 +38,13 @@ const metadata = { }, events: /** @lends sap.ui.webcomponents.main.YearPicker.prototype */ { /** - * Fired when the user selects a new Date on the Web Component. + * Fired when the user selects a year (space/enter/click). * @public * @event */ change: {}, /** - * Fired when month, year has changed due to item navigation. + * Fired when the timestamp changes - the user navigates with the keyboard or clicks with the mouse. * @since 1.0.0-rc.9 * @public * @event @@ -49,6 +53,9 @@ const metadata = { }, }; +const PAGE_SIZE = 20; // Total years on a single page +const ROW_SIZE = 4; // Years per row (5 rows of 4 years each) + /** * @class * @@ -57,11 +64,11 @@ const metadata = { * @constructor * @author SAP SE * @alias sap.ui.webcomponents.main.YearPicker - * @extends sap.ui.webcomponents.main.PickerBase + * @extends CalendarPart * @tagname ui5-yearpicker * @public */ -class YearPicker extends PickerBase { +class YearPicker extends CalendarPart { static get metadata() { return metadata; } @@ -74,213 +81,231 @@ class YearPicker extends PickerBase { return YearPickerTemplate; } - constructor() { - super(); - - this._oLocale = getLocale(); + onBeforeRendering() { + this._buildYears(); + } - this._itemNav = new ItemNavigation(this, { - pageSize: 20, - rowSize: 4, - behavior: ItemNavigationBehavior.Paging, - getItemsCallback: () => this.focusableYears, - affectedPropertiesNames: ["_yearIntervals"], - }); + _buildYears() { + if (this._hidden) { + return; + } - this._itemNav.attachEvent( - ItemNavigation.BORDER_REACH, - this._handleItemNavigationBorderReach.bind(this) - ); + const oYearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._primaryCalendarType }, getLocale()); - this._yearIntervals = []; - } + this._calculateFirstYear(); - onBeforeRendering() { - const oYearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._primaryCalendarType }, this._oLocale); - const oCalDate = this._calendarDate; - const maxCalendarDateYear = CalendarDate.fromTimestamp(this._getMaxCalendarDate(), this._primaryCalendarType).getYear(); - const minCalendarDateYear = CalendarDate.fromTimestamp(this._getMinCalendarDate(), this._primaryCalendarType).getYear(); - - oCalDate.setMonth(0); - oCalDate.setDate(1); - if (oCalDate.getYear() - YearPicker._MIDDLE_ITEM_INDEX - 1 > maxCalendarDateYear - YearPicker._ITEMS_COUNT) { - oCalDate.setYear(maxCalendarDateYear - YearPicker._ITEMS_COUNT); - } else if (oCalDate.getYear() - YearPicker._MIDDLE_ITEM_INDEX - 1 < minCalendarDateYear) { - oCalDate.setYear(minCalendarDateYear - 1); - } else { - oCalDate.setYear(oCalDate.getYear() - YearPicker._MIDDLE_ITEM_INDEX - 1); - } + const calendarDate = this._calendarDate; // store the value of the expensive getter + const minDate = this._minDate; // store the value of the expensive getter + const maxDate = this._maxDate; // store the value of the expensive getter + const tempDate = new CalendarDate(calendarDate, this._primaryCalendarType); + tempDate.setYear(this._firstYear); const intervals = []; let timestamp; - if (this._selectedYear === undefined) { - this._selectedYear = this._year; - } - /* eslint-disable no-loop-func */ - for (let i = 0; i < YearPicker._ITEMS_COUNT; i++) { - const intervalIndex = parseInt(i / 4); - if (!intervals[intervalIndex]) { - intervals[intervalIndex] = []; - } + for (let i = 0; i < PAGE_SIZE; i++) { + timestamp = tempDate.valueOf() / 1000; - oCalDate.setYear(oCalDate.getYear() + 1); - - timestamp = oCalDate.valueOf() / 1000; + const isSelected = this.selectedDates.some(itemTimestamp => { + const date = CalendarDate.fromTimestamp(itemTimestamp * 1000, this._primaryCalendarType); + return date.getYear() === tempDate.getYear(); + }); + const isFocused = tempDate.getYear() === calendarDate.getYear(); + const isDisabled = tempDate.getYear() < minDate.getYear() || tempDate.getYear() > maxDate.getYear(); const year = { timestamp: timestamp.toString(), - id: `${this._id}-y${timestamp}`, - selected: this.selectedDates.some(itemTimestamp => { - const date = CalendarDate.fromTimestamp(itemTimestamp * 1000, this._primaryCalendarType); - return date.getYear() === oCalDate.getYear(); - }), - year: oYearFormat.format(oCalDate.toLocalJSDate()), + _tabIndex: isFocused ? "0" : "-1", + focusRef: isFocused, + selected: isSelected, + ariaSelected: isSelected ? "true" : "false", + year: oYearFormat.format(tempDate.toLocalJSDate()), + disabled: isDisabled, classes: "ui5-yp-item", }; - if (year.selected) { + if (isSelected) { year.classes += " ui5-yp-item--selected"; } - if ((this.minDate || this.maxDate) && this._isOutOfSelectableRange(oCalDate.getYear())) { + if (isDisabled) { year.classes += " ui5-yp-item--disabled"; - year.disabled = true; } + const intervalIndex = parseInt(i / ROW_SIZE); + if (intervals[intervalIndex]) { intervals[intervalIndex].push(year); + } else { + intervals[intervalIndex] = [year]; } + + tempDate.setYear(tempDate.getYear() + 1); } - this._yearIntervals = intervals; + this._years = intervals; } - onAfterRendering() { - this._itemNav.focusCurrent(); - } + _calculateFirstYear() { + const absoluteMaxYear = getMaxCalendarDate(this._primaryCalendarType).getYear(); // 9999 + const currentYear = this._calendarDate.getYear(); - _setCurrentItemTabIndex(index) { - const currentItem = this._itemNav._getCurrentItem(); - if (currentItem) { - currentItem.setAttribute("tabindex", index.toString()); + // 1. If first load - center the current year (set first year to be current year minus half page size) + if (!this._firstYear) { + this._firstYear = currentYear - PAGE_SIZE / 2; } - } - _onmousedown(event) { - if (event.target.className.indexOf("ui5-yp-item") > -1) { - const targetTimestamp = this.getTimestampFromDom(event.target); - const focusedItem = this.focusableYears.find(item => parseInt(item.timestamp) === targetTimestamp); - this._itemNav.update(focusedItem); + // 2. If out of range - change by a page (20) - do not center in order to keep the same position as the last page + if (currentYear < this._firstYear) { + this._firstYear -= PAGE_SIZE; + } else if (currentYear >= this._firstYear + PAGE_SIZE) { + this._firstYear += PAGE_SIZE; + } + + // 3. If the date was changed by more than 20 years - reset _firstYear completely + if (Math.abs(this._firstYear - currentYear) >= PAGE_SIZE) { + this._firstYear = currentYear - PAGE_SIZE / 2; + } + + // Keep it in the range between the min and max year + this._firstYear = Math.max(this._firstYear, this._minDate.getYear()); + this._firstYear = Math.min(this._firstYear, this._maxDate.getYear()); + + // If first year is > 9980, make it 9980 to not show any years beyond 9999 + if (this._firstYear > absoluteMaxYear - PAGE_SIZE + 1) { + this._firstYear = absoluteMaxYear - PAGE_SIZE + 1; } } - _onmouseup(event) { - if (event.target.className.indexOf("ui5-yp-item") > -1) { - const timestamp = this.getTimestampFromDom(event.target); - this.timestamp = timestamp; - this._selectedYear = this._year; - this.fireEvent("change", { timestamp }); + onAfterRendering() { + if (!this._hidden) { + this.focus(); } } _onkeydown(event) { + let preventDefault = true; + if (isEnter(event)) { - return this._handleEnter(event); + this._selectYear(event); + } else if (isSpace(event)) { + event.preventDefault(); + } else if (isLeft(event)) { + this._modifyTimestampBy(-1); + } else if (isRight(event)) { + this._modifyTimestampBy(1); + } else if (isUp(event)) { + this._modifyTimestampBy(-ROW_SIZE); + } else if (isDown(event)) { + this._modifyTimestampBy(ROW_SIZE); + } else if (isPageUp(event)) { + this._modifyTimestampBy(-PAGE_SIZE); + } else if (isPageDown(event)) { + this._modifyTimestampBy(PAGE_SIZE); + } else if (isHome(event) || isEnd(event)) { + this._onHomeOrEnd(isHome(event)); + } else if (isHomeCtrl(event)) { + this._setTimestamp(parseInt(this._years[0][0].timestamp)); // first year of first row + } else if (isEndCtrl(event)) { + this._setTimestamp(parseInt(this._years[PAGE_SIZE / ROW_SIZE - 1][ROW_SIZE - 1].timestamp)); // last year of last row + } else { + preventDefault = false; } - if (isSpace(event)) { - return this._handleSpace(event); + if (preventDefault) { + event.preventDefault(); } } - _handleEnter(event) { - event.preventDefault(); - if (event.target.className.indexOf("ui5-yp-item") > -1) { - const timestamp = this.getTimestampFromDom(event.target); - - this.timestamp = timestamp; - this._selectedYear = this._year; - this._itemNav.current = YearPicker._MIDDLE_ITEM_INDEX; - this.fireEvent("change", { timestamp }); - } + _onHomeOrEnd(homePressed) { + this._years.forEach(row => { + const indexInRow = row.findIndex(item => CalendarDate.fromTimestamp(parseInt(item.timestamp) * 1000).getYear() === this._calendarDate.getYear()); + if (indexInRow !== -1) { // The current year is on this row + const index = homePressed ? 0 : ROW_SIZE - 1; // select the first (if Home) or last (if End) year on the row + this._setTimestamp(parseInt(row[index].timestamp)); + } + }); } - _handleSpace(event) { - event.preventDefault(); - if (event.target.className.indexOf("ui5-yp-item") > -1) { - const timestamp = this.getTimestampFromDom(event.target); - - this._selectedYear = CalendarDate.fromTimestamp( - timestamp * 1000, - this._primaryCalendarType - ).getYear(); - } + /** + * Sets the timestamp to an absolute value + * @param value + * @private + */ + _setTimestamp(value) { + this._safelySetTimestamp(value); + this.fireEvent("navigate", { timestamp: this.timestamp }); } - _handleItemNavigationBorderReach(event) { - const oCalDate = this._calendarDate; - const maxCalendarDateYear = CalendarDate.fromTimestamp(this._getMaxCalendarDate(), this._primaryCalendarType).getYear(); - const minCalendarDateYear = CalendarDate.fromTimestamp(this._getMinCalendarDate(), this._primaryCalendarType).getYear(); - oCalDate.setMonth(0); - oCalDate.setDate(1); - - if (event.end) { - oCalDate.setYear(oCalDate.getYear() + YearPicker._ITEMS_COUNT); - } else if (event.start) { - if (oCalDate.getYear() - YearPicker._MIDDLE_ITEM_INDEX < minCalendarDateYear) { - return; - } - oCalDate.setYear(oCalDate.getYear() - YearPicker._ITEMS_COUNT); - } + /** + * Modifies timestamp by a given amount of years and, if necessary, loads the prev/next page + * @param amount + * @private + */ + _modifyTimestampBy(amount) { + // Modify the current timestamp + this._safelyModifyTimestampBy(amount, "year"); + + // Notify the calendar to update its timestamp + this.fireEvent("navigate", { timestamp: this.timestamp }); + } - if (oCalDate.getYear() - YearPicker._MIDDLE_ITEM_INDEX > maxCalendarDateYear) { - return; + _onkeyup(event) { + if (isSpace(event)) { + this._selectYear(event); } + } - if (this._isOutOfSelectableRange(oCalDate.getYear() - YearPicker._MIDDLE_ITEM_INDEX) - && this._isOutOfSelectableRange(oCalDate.getYear() + YearPicker._MIDDLE_ITEM_INDEX)) { - return; + /** + * User clicked with the mouser or pressed Enter/Space + * @param event + * @private + */ + _selectYear(event) { + event.preventDefault(); + if (event.target.className.indexOf("ui5-yp-item") > -1) { + const timestamp = this._getTimestampFromDom(event.target); + this._safelySetTimestamp(timestamp); + this.fireEvent("change", { timestamp: this.timestamp }); } - - this.timestamp = oCalDate.valueOf() / 1000; - - this.fireEvent("navigate", event); } - _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; + /** + * Called from Calendar.js + * @protected + */ + _hasPreviousPage() { + return this._firstYear > this._minDate.getYear(); } - get focusableYears() { - const focusableYears = []; - - for (let i = 0; i < this._yearIntervals.length; i++) { - const yearInterval = this._yearIntervals[i].filter(x => !x.disabled); - focusableYears.push(yearInterval); - } + /** + * Called from Calendar.js + * @protected + */ + _hasNextPage() { + return this._firstYear + PAGE_SIZE - 1 < this._maxDate.getYear(); + } - return [].concat(...focusableYears); + /** + * Called by Calendar.js + * User pressed the "<" button in the calendar header (same as PageUp) + * @protected + */ + _showPreviousPage() { + this._modifyTimestampBy(-PAGE_SIZE); } - get styles() { - return { - main: { - display: this._hidden ? "none" : "", - }, - }; + /** + * Called by Calendar.js + * User pressed the ">" button in the calendar header (same as PageDown) + * @protected + */ + _showNextPage() { + this._modifyTimestampBy(PAGE_SIZE); } } -YearPicker._ITEMS_COUNT = 20; -YearPicker._MIDDLE_ITEM_INDEX = 10; - YearPicker.define(); export default YearPicker; diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index edd8832197c7..d68db88e0112 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -381,3 +381,6 @@ DAY_PICKER_WEEK_NUMBER_TEXT = Week number #XBUT: Text for 'Non-Working Day' in the DayPicker DAY_PICKER_NON_WORKING_DAY = Non-Working Day + +#XBUT: Text for 'Today' in the DayPicker +DAY_PICKER_TODAY = Today diff --git a/packages/main/src/themes/Calendar.css b/packages/main/src/themes/Calendar.css index 58aec27cbbac..ceca93fd5f8d 100644 --- a/packages/main/src/themes/Calendar.css +++ b/packages/main/src/themes/Calendar.css @@ -1,19 +1,18 @@ -:host { - display: inline-block; -} - -.ui5-yearpicker--hidden, -.ui5-monthpicker--hidden, -.ui5-daypicker--hidden { - display: none; +:host(:not([hidden])) { + display: inline-block; } .ui5-cal-root { - background: var(--sapList_Background); + background: var(--sapList_Background); + box-sizing: border-box; + height: var(--_ui5_calendar_height); + width: var(--_ui5_calendar_width); + padding: var(--_ui5_calendar_padding); + display: flex; + flex-direction: column-reverse; + justify-content: flex-end; } -.ui5-cal-root [ui5-daypicker], -.ui5-cal-root [ui5-month-picker], -.ui5-cal-root [ui5-yearpicker] { - vertical-align: top; +.ui5-cal-root [ui5-calendar-header] { + height: var(--_ui5_calendar_header_height); } diff --git a/packages/main/src/themes/CalendarHeader.css b/packages/main/src/themes/CalendarHeader.css index d124e0b21ee8..ef442c01ce61 100644 --- a/packages/main/src/themes/CalendarHeader.css +++ b/packages/main/src/themes/CalendarHeader.css @@ -1,19 +1,16 @@ :host { - display: inline-block; + display: block; width: 100%; + height: 100%; } .ui5-calheader-root { display: flex; - height: var(--_ui5_calendar_header_height); + height: 100%; padding: var(--_ui5_calendar_header_padding); box-sizing: border-box; } -.ui5-calheader-root [ui5-button] { - height: 100%; -} - .ui5-calheader-arrowbtn { display: flex; justify-content: center; diff --git a/packages/main/src/themes/DateTimePickerPopover.css b/packages/main/src/themes/DateTimePickerPopover.css index b7e042dd7c4c..168826ccc6e0 100644 --- a/packages/main/src/themes/DateTimePickerPopover.css +++ b/packages/main/src/themes/DateTimePickerPopover.css @@ -14,12 +14,8 @@ } .ui5-dt-time { - display: flex; - justify-content: center; - align-items: center; width: 100%; min-width: var(--_ui5_datetime_timeview_width); - padding: var(--_ui5_datetime_timeview_padding); box-sizing: border-box; } @@ -28,10 +24,6 @@ display: none; } -.ui5-dt-wheel { - height: 100%; -} - .ui5-dt-picker-header { display: flex; justify-content: center; @@ -79,4 +71,4 @@ .ui5-dt-picker-content--phone .ui5-dt-time { min-width: var(--_ui5_datetime_timeview_phonemode_width); -} \ No newline at end of file +} diff --git a/packages/main/src/themes/DayPicker.css b/packages/main/src/themes/DayPicker.css index 1d87e99d333f..f5a798a913f0 100644 --- a/packages/main/src/themes/DayPicker.css +++ b/packages/main/src/themes/DayPicker.css @@ -1,6 +1,5 @@ - :host(:not([hidden])) { - display: inline-block; + display: block; } :host { diff --git a/packages/main/src/themes/DurationPicker.css b/packages/main/src/themes/DurationPicker.css deleted file mode 100644 index b562d508645b..000000000000 --- a/packages/main/src/themes/DurationPicker.css +++ /dev/null @@ -1,29 +0,0 @@ -:host(:not([hidden])) { - display: inline-block; -} - -:host { - color: var(--sapField_TextColor); - background-color: var(--sapField_Background); -} - -:host .ui5-duration-picker-input { - width: 100%; - color: inherit; - background-color: inherit; -} - -.ui5-duration-picker-input-icon-button:hover { - cursor: pointer; - background: var(--sapButton_Hover_Background); -} - -.ui5-duration-picker-input-icon-button:active { - background-color: var(--sapButton_Active_Background); - color: var(--sapButton_Active_TextColor); -} - -.ui5-duration-picker-input-icon-button[pressed] { - background-color: var(--sapButton_Active_Background); - color: var(--sapButton_Active_TextColor); -} \ No newline at end of file diff --git a/packages/main/src/themes/DurationPickerPopover.css b/packages/main/src/themes/DurationPickerPopover.css deleted file mode 100644 index df59b4eb749b..000000000000 --- a/packages/main/src/themes/DurationPickerPopover.css +++ /dev/null @@ -1,29 +0,0 @@ -.ui5-duration-picker-sliders-container { - display: flex; - justify-content: center; - align-items: stretch; - direction: ltr; - padding: 0.5rem; - min-width: 18rem; -} - -.ui5-duration-picker-sliders-container.ui5-phone{ - height: 90vh; -} - -.ui5-duration-picker-wheelslider { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - -.ui5-duration-picker-footer { - height: fit-content; - display: flex; - justify-content: flex-end; - width: 100%; -} - -.ui5-duration-picker-footer > [ui5-button] { - margin: 1%; - min-width: 20%; -} diff --git a/packages/main/src/themes/MonthPicker.css b/packages/main/src/themes/MonthPicker.css index 8dc5a8ad96be..32740e826ca6 100644 --- a/packages/main/src/themes/MonthPicker.css +++ b/packages/main/src/themes/MonthPicker.css @@ -1,5 +1,5 @@ :host(:not([hidden])) { - display: inline-block; + display: block; } :host { diff --git a/packages/main/src/themes/TimePickerPopover.css b/packages/main/src/themes/TimePickerPopover.css index 9ec332a9f7db..fad6f1e75e63 100644 --- a/packages/main/src/themes/TimePickerPopover.css +++ b/packages/main/src/themes/TimePickerPopover.css @@ -1,17 +1,3 @@ -.ui5-time-picker-sliders-container { - display: flex; - justify-content: center; - align-items: stretch; - direction: ltr; - padding: 0.5rem; - min-width: 18rem; - box-sizing: border-box; -} - -.ui5-time-picker-sliders-container.ui5-phone{ - height: 90vh; -} - .ui5-time-picker-footer { height: fit-content; display: flex; @@ -19,11 +5,6 @@ width: 100%; } -.ui5-time-picker-wheelslider { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - .ui5-time-picker-footer > [ui5-button] { margin: 1%; min-width: 20%; diff --git a/packages/main/src/themes/TimeSelection.css b/packages/main/src/themes/TimeSelection.css new file mode 100644 index 000000000000..8aecd408fa7c --- /dev/null +++ b/packages/main/src/themes/TimeSelection.css @@ -0,0 +1,23 @@ +:host(:not([hidden])) { + display: inline-block; + min-width: 18rem; +} + +.ui5-time-selection-root { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: stretch; + direction: ltr; + box-sizing: border-box; +} + +.ui5-time-selection-root.ui5-phone{ + height: 90vh; +} + +[ui5-wheelslider] { + padding-left: 0.25rem; + padding-right: 0.25rem; +} diff --git a/packages/main/src/themes/WheelSlider.css b/packages/main/src/themes/WheelSlider.css index 0a9369cfd84c..c35f77f1b8db 100644 --- a/packages/main/src/themes/WheelSlider.css +++ b/packages/main/src/themes/WheelSlider.css @@ -31,14 +31,14 @@ outline: none } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-arrow { +:host([expanded]) .ui5-wheelslider-arrow { visibility: var(--_ui5_wheelslider_arrows_visibility); box-sizing: border-box; border-color: transparent; cursor: pointer; } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-arrow:hover { +:host([expanded]) .ui5-wheelslider-arrow:hover { visibility: var(--_ui5_wheelslider_arrows_visibility); box-sizing: border-box; border-color: inherit; @@ -55,7 +55,7 @@ margin-top: 0rem; } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-inner { +:host([expanded]) .ui5-wheelslider-inner { margin-top: 0; } @@ -107,13 +107,13 @@ top: var(--_ui5_wheelslider_mobile_selection_frame_margin_top); } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-inner .ui5-wheelslider-wrapper > ul { +:host([expanded]) .ui5-wheelslider-inner .ui5-wheelslider-wrapper > ul { list-style-type: none; top: 0; padding-top: calc(var(--_ui5_wheelslider_item_height) * 2); } -.ui5-wheelslider-root.ui5-phone[expanded] .ui5-wheelslider-inner .ui5-wheelslider-wrapper > ul { +:host([expanded]) .ui5-wheelslider-root.ui5-phone .ui5-wheelslider-inner .ui5-wheelslider-wrapper > ul { list-style-type: none; top: 0; padding-top: calc(var(--_ui5_wheelslider_item_height) * 4); @@ -130,7 +130,7 @@ height: var(--_ui5_wheelslider_mobile_height); } -.ui5-wheelslider-root[expanded] { +:host([expanded]) { height: 100%; cursor: default; margin: 0; @@ -139,7 +139,7 @@ display: inline-flex; } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-label { +:host([expanded]) .ui5-wheelslider-label { display: block; visibility: visible; } @@ -153,7 +153,7 @@ height: var(--_ui5_wheelslider_mobile_height); } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-inner .ui5-wheelslider-item { +:host([expanded]) .ui5-wheelslider-inner .ui5-wheelslider-item { background: var(--_ui5_wheelslider_item_background_color); color: var(--_ui5_wheelslider_item_text_color); border: 1px solid var(--_ui5_wheelslider_item_border_color); @@ -162,22 +162,22 @@ cursor: auto; } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-inner .ui5-wheelslider-item:hover { +:host([expanded]) .ui5-wheelslider-inner .ui5-wheelslider-item:hover { background: var(--sapList_Hover_Background); border-color: var(--_ui5_wheelslider_item_hovered_border_color); } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-inner .ui5-wheelslider-item:active { +:host([expanded]) .ui5-wheelslider-inner .ui5-wheelslider-item:active { background: var(--_ui5_wheelslider_active_item_background_color); color: var(--_ui5_wheelslider_active_item_text_color); } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-inner .ui5-wheelslider-item:focus { +:host([expanded]) .ui5-wheelslider-inner .ui5-wheelslider-item:focus { outline: 1px dotted black; outline-offset: -3px; } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-inner .ui5-wheelslider-selection-frame { +:host([expanded]) .ui5-wheelslider-inner .ui5-wheelslider-selection-frame { visibility: visible; -webkit-box-shadow: inset 0px 0px 0px 2px var(--_ui5_wheelslider_selection_frame_color); -moz-box-shadow: inset 0px 0px 0px 2px var(--_ui5_wheelslider_selection_frame_color); @@ -186,14 +186,14 @@ margin-top: var(--_ui5_wheelslider_selection_frame_margin_top); } -.ui5-wheelslider-root.ui5-phone[expanded] .ui5-wheelslider-inner .ui5-wheelslider-selection-frame { +:host([expanded]) .ui5-wheelslider-root.ui5-phone .ui5-wheelslider-inner .ui5-wheelslider-selection-frame { margin-top: var(--_ui5_wheelslider_mobile_selection_frame_margin_top); } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-inner .ui5-wheelslider-selection-frame:hover + ul > li.ui5-wheelslider-item.ui5-wheelslider-itemSelected { +:host([expanded]) .ui5-wheelslider-inner .ui5-wheelslider-selection-frame:hover + ul > li.ui5-wheelslider-item.ui5-wheelslider-itemSelected { background: var(--_ui_wheelslider_item_hover_color); } -.ui5-wheelslider-root[expanded] .ui5-wheelslider-inner .ui5-wheelslider-selection-frame:active + ul > li.ui5-wheelslider-item.ui5-wheelslider-itemSelected { +:host([expanded]) .ui5-wheelslider-inner .ui5-wheelslider-selection-frame:active + ul > li.ui5-wheelslider-item.ui5-wheelslider-itemSelected { background: var(--_ui5_wheelslider_selected_item_background_color); color: lightgray; } diff --git a/packages/main/src/themes/YearPicker.css b/packages/main/src/themes/YearPicker.css index 86d41622a5a6..845393f7b96f 100644 --- a/packages/main/src/themes/YearPicker.css +++ b/packages/main/src/themes/YearPicker.css @@ -1,5 +1,5 @@ :host(:not([hidden])) { - display: inline-block; + display: block; } :host { diff --git a/packages/main/src/themes/base/sizes-parameters.css b/packages/main/src/themes/base/sizes-parameters.css index 055c40590b0b..aca2cf8f91da 100644 --- a/packages/main/src/themes/base/sizes-parameters.css +++ b/packages/main/src/themes/base/sizes-parameters.css @@ -1,7 +1,12 @@ :root { + /* Calendar */ + --_ui5_calendar_height: 24.5rem; + --_ui5_calendar_width: 20.5rem; + --_ui5_calendar_padding: 0.75rem; --_ui5_calendar_header_height: 3rem; --_ui5_calendar_header_arrow_button_width: 2.5rem; --_ui5_calendar_header_padding: 0.25rem 0; + --_ui5_checkbox_root_side_padding: .6875rem; --_ui5_checkbox_icon_size: 1rem; --_ui5_custom_list_item_height: 3rem; @@ -93,9 +98,14 @@ --_ui5_button_base_padding: 0.4375rem; --_ui5_button_base_min_width: 2rem; --_ui5_button_icon_font_size: 1rem; + + /* Calendar */ + --_ui5_calendar_height: 18rem; + --_ui5_calendar_width: 17.75rem; + --_ui5_calendar_padding: 0.5rem; --_ui5_calendar_header_height: 2rem; - --_ui5_calendar_header_padding: 0; --_ui5_calendar_header_arrow_button_width: 2rem; + --_ui5_calendar_header_padding: 0; /* CheckBox */ --_ui5_checkbox_root_side_padding: var(--_ui5_checkbox_wrapped_focus_padding); diff --git a/packages/main/src/timepicker-utils/TimeSlider.js b/packages/main/src/timepicker-utils/TimeSlider.js index 1087f2cc734a..a08868cdc5e0 100644 --- a/packages/main/src/timepicker-utils/TimeSlider.js +++ b/packages/main/src/timepicker-utils/TimeSlider.js @@ -1,24 +1,26 @@ -const generateTimeItemsArray = x => { +const generateTimeItemsArray = (x, step = 1) => { const array = []; for (let i = 0; i < x; i++) { - let tempString = i.toString(); - if (tempString.length === 1) { - tempString = `0${tempString}`; - } + if (i % step === 0) { + let tempString = i.toString(); + if (tempString.length === 1) { + tempString = `0${tempString}`; + } - array.push(tempString); + array.push(tempString); + } } return array; }; -const getHours = config => { +const getHours = (config, max) => { let hoursValueArray = []; if (config.isTwelveHoursFormat) { - hoursValueArray = generateTimeItemsArray(12); + hoursValueArray = generateTimeItemsArray(max || 12, 1); } else { - hoursValueArray = generateTimeItemsArray(24); + hoursValueArray = generateTimeItemsArray(max || 24, 1); } if (config.minHour === 1) { @@ -36,12 +38,12 @@ const getHours = config => { return hoursValueArray; }; -const getMinutes = () => { - return generateTimeItemsArray(60); +const getMinutes = (max, step) => { + return generateTimeItemsArray(max || 60, step); }; -const getSeconds = () => { - return generateTimeItemsArray(60); +const getSeconds = (max, step) => { + return generateTimeItemsArray(max || 60, step); }; const getHoursConfigByFormat = type => { diff --git a/packages/main/src/types/CalendarSelectionMode.js b/packages/main/src/types/CalendarSelectionMode.js new file mode 100644 index 000000000000..38359635a343 --- /dev/null +++ b/packages/main/src/types/CalendarSelectionMode.js @@ -0,0 +1,17 @@ +import DataType from "@ui5/webcomponents-base/dist/types/DataType.js"; + +const CalendarSelectionModes = { + Single: "Single", + Multiple: "Multiple", + Range: "Range", +}; + +class CalendarSelectionMode extends DataType { + static isValid(value) { + return !!CalendarSelectionModes[value]; + } +} + +CalendarSelectionMode.generateTypeAccessors(CalendarSelectionModes); + +export default CalendarSelectionMode; diff --git a/packages/main/src/util/DateTime.js b/packages/main/src/util/DateTime.js deleted file mode 100644 index 867dce0e702e..000000000000 --- a/packages/main/src/util/DateTime.js +++ /dev/null @@ -1,25 +0,0 @@ -import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; - -const getMinCalendarDate = primaryCalendarType => { - const minDate = new CalendarDate(1, 0, 1, primaryCalendarType); - minDate.setYear(1); - minDate.setMonth(0); - minDate.setDate(1); - return minDate.valueOf(); -}; - -const getMaxCalendarDate = primaryCalendarType => { - const maxDate = new CalendarDate(1, 0, 1, primaryCalendarType); - maxDate.setYear(9999); - maxDate.setMonth(11); - const tempDate = new CalendarDate(maxDate, primaryCalendarType); - tempDate.setDate(1); - tempDate.setMonth(tempDate.getMonth() + 1, 0); - maxDate.setDate(tempDate.getDate());// 31st for Gregorian Calendar - return maxDate.valueOf(); -}; - -export { - getMinCalendarDate, - getMaxCalendarDate, -}; diff --git a/packages/main/test/pageobjects/DatePickerTestPage.js b/packages/main/test/pageobjects/DatePickerTestPage.js index 6e162a2537d8..13cc8eee3e18 100644 --- a/packages/main/test/pageobjects/DatePickerTestPage.js +++ b/packages/main/test/pageobjects/DatePickerTestPage.js @@ -54,19 +54,19 @@ class DatePickerTestPage { } get btnPrev() { - return browser.$(`.${this.staticAreaItemClassName}`).shadow$("ui5-calendar").shadow$(`ui5-calendar-header`).shadow$(`div[data-sap-cal-head-button="Prev"]`); + return browser.$(`.${this.staticAreaItemClassName}`).shadow$("ui5-calendar").shadow$(`ui5-calendar-header`).shadow$(`div[data-ui5-cal-header-btn-prev]`); } get btnNext() { - return browser.$(`.${this.staticAreaItemClassName}`).shadow$("ui5-calendar").shadow$(`ui5-calendar-header`).shadow$(`div[data-sap-cal-head-button="Next"]`); + return browser.$(`.${this.staticAreaItemClassName}`).shadow$("ui5-calendar").shadow$(`ui5-calendar-header`).shadow$(`div[data-ui5-cal-header-btn-next]`); } get btnYear() { - return browser.$(`.${this.staticAreaItemClassName}`).shadow$("ui5-calendar").shadow$(`ui5-calendar-header`).shadow$(`div[data-sap-show-picker="Year"]`); + return browser.$(`.${this.staticAreaItemClassName}`).shadow$("ui5-calendar").shadow$(`ui5-calendar-header`).shadow$(`div[data-ui5-cal-header-btn-year]`); } get btnMonth() { - return browser.$(`.${this.staticAreaItemClassName}`).shadow$("ui5-calendar").shadow$(`ui5-calendar-header`).shadow$(`div[data-sap-show-picker="Month"]`); + return browser.$(`.${this.staticAreaItemClassName}`).shadow$("ui5-calendar").shadow$(`ui5-calendar-header`).shadow$(`div[data-ui5-cal-header-btn-month]`); } get dayPicker() { @@ -111,7 +111,7 @@ class DatePickerTestPage { const dayNames = Array.from(this.getDayPickerContent()); return dayNames[0].$$("div"); } - + getDayPickerDatesRow(index) { const data = Array.from(this.getDayPickerContent()); return data[index].$$("div"); diff --git a/packages/main/test/pages/Calendar.html b/packages/main/test/pages/Calendar.html index ada1075317fd..a80fec9ec263 100644 --- a/packages/main/test/pages/Calendar.html +++ b/packages/main/test/pages/Calendar.html @@ -21,6 +21,12 @@ + + @@ -39,6 +45,10 @@ +
      + +
      +
      @@ -55,7 +65,7 @@ const textArea = document.getElementById("textArea"); select.addEventListener("ui5-change", function(event) { - calendar1.setAttribute("selection", event.detail.selectedOption.textContent) + calendar1.setAttribute("selection-mode", event.detail.selectedOption.textContent) }); toggleButton.addEventListener("click", function(event) { diff --git a/packages/main/test/pages/DatePicker.html b/packages/main/test/pages/DatePicker.html index 293e4bcee67a..798cc604a399 100644 --- a/packages/main/test/pages/DatePicker.html +++ b/packages/main/test/pages/DatePicker.html @@ -127,6 +127,11 @@

      DatePicker in Compact

      +
      +

      DatePicker with properties given in the wrong format

      + +
      +
    - - - - - - - - - - - - - - - -
    - - - -
    - -
    - - - - - - - - - -
    - - - diff --git a/packages/main/test/pages/TimePicker.html b/packages/main/test/pages/TimePicker.html index 779d5a32ade9..b7502104b977 100644 --- a/packages/main/test/pages/TimePicker.html +++ b/packages/main/test/pages/TimePicker.html @@ -21,6 +21,7 @@ + @@ -74,7 +75,7 @@

    TimePicker in Compact

    inputResult.value = ++inputCounter; }); - var tp = document.getElementById('timepickerSetTime'); + var tp = document.getElementById('timepickerSetTime'); document.getElementById('setTimeButton').addEventListener("click", function(e) { tp.setAttribute("value", tp.formatValue(new Date(2020, 3, 13, 3, 16, 16))); }); diff --git a/packages/main/test/pages/TimeSelection.html b/packages/main/test/pages/TimeSelection.html new file mode 100644 index 000000000000..5022344cc3cf --- /dev/null +++ b/packages/main/test/pages/TimeSelection.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + +

    + +

    + +

    + +

    + + + + diff --git a/packages/main/test/samples/Calendar.sample.html b/packages/main/test/samples/Calendar.sample.html index 6c13ac41a24f..9be43516c087 100644 --- a/packages/main/test/samples/Calendar.sample.html +++ b/packages/main/test/samples/Calendar.sample.html @@ -49,11 +49,11 @@

    Calendar with hidden week numbers

    Calendar with selection type Multiple

    - +
    
    -<ui5-calendar selection="Multiple"></ui5-calendar>
    +<ui5-calendar selection-mode="Multiple"></ui5-calendar>
     	
    @@ -61,11 +61,11 @@

    Calendar with selection type Multiple

    Calendar with selection type Range

    - +
    
    -<ui5-calendar selection="Range"></ui5-calendar>
    +<ui5-calendar selection-mode="Range"></ui5-calendar>
     	
    diff --git a/packages/main/test/specs/Calendar.spec.js b/packages/main/test/specs/Calendar.spec.js index a517c53bb6d9..3fb53da0ae11 100644 --- a/packages/main/test/specs/Calendar.spec.js +++ b/packages/main/test/specs/Calendar.spec.js @@ -11,19 +11,13 @@ describe("Calendar general interaction", () => { it("Year is set in the header", () => { const calendarHeader = browser.$("#calendar1").shadow$("ui5-calendar-header"); - const headerText = parseInt(calendarHeader.getAttribute("year-text")); + const yearButton = calendarHeader.shadow$(`[data-ui5-cal-header-btn-year]`); + const headerText = parseInt(yearButton.getText()); const currentYear = new Date().getFullYear(); assert.equal(headerText, currentYear, "Year is set in the header"); }); - it("Default year is the current year", () => { - const calendarHeader = browser.$("#calendar1").shadow$("ui5-calendar-header"); - const calendarYear = parseInt(calendarHeader.getAttribute("year-text")); - - assert.strictEqual(calendarYear, new Date().getFullYear(), "Default year is correct"); - }); - it("Month is set in the header", () => { const monthMap = new Map(); ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"].forEach((month, index) => { @@ -31,30 +25,11 @@ describe("Calendar general interaction", () => { }); const calendarHeader = browser.$("#calendar1").shadow$("ui5-calendar-header"); - const monthText = calendarHeader.getAttribute("month-text"); + const monthButton = calendarHeader.shadow$(`[data-ui5-cal-header-btn-month]`); + const monthText = monthButton.getText(); const currentMonth = new Date().getMonth(); - assert.strictEqual(monthText.toString(), monthMap.get(currentMonth), "Month is set in the header"); - }); - - it("Default month is the current year", () => { - const calendarHeader = browser.$("#calendar1").shadow$("ui5-calendar-header"); - const monthMap = new Map(); - ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"].forEach((month, index) => { - monthMap.set(index, month); - }); - const calendarMonth = calendarHeader.getAttribute("month-text"); - - assert.strictEqual(calendarMonth, monthMap.get(new Date().getMonth()), "Default month is correct"); - }); - - it("timestamp is propagated to the content part", () => { - const calendar = browser.$("#calendar1"); - const TIMESTAMP = 1; - - calendar.setProperty("timestamp", TIMESTAMP); - - assert.strictEqual(calendar.getProperty("timestamp"), TIMESTAMP); + assert.strictEqual(monthText, monthMap.get(currentMonth), "Month is set in the header"); }); it("Focus goes into the current day item of the day picker", () => { @@ -64,8 +39,8 @@ describe("Calendar general interaction", () => { const dayPicker = calendar.shadow$("ui5-daypicker"); const header = calendar.shadow$("ui5-calendar-header"); const currentDayItem = dayPicker.shadow$(`div[data-sap-timestamp="974851200"]`); - const monthButton = header.shadow$(`[data-sap-show-picker="Month"]`); - const yearButton = header.shadow$(`[data-sap-show-picker="Year"]`); + const monthButton = header.shadow$(`[data-ui5-cal-header-btn-month]`); + const yearButton = header.shadow$(`[data-ui5-cal-header-btn-year]`); toggleButton.click(); toggleButton.click(); @@ -86,44 +61,46 @@ describe("Calendar general interaction", () => { assert.ok(currentDayItem.isFocusedDeep(), "Current calendar day item is focused"); }); - it("Calendar sets the selected year when yearpicker is opened", () => { + it("Calendar focuses the selected year when yearpicker is opened", () => { + browser.url("http://localhost:8080/test-resources/pages/Calendar.html"); const calendar = browser.$("#calendar1"); const yearPicker = calendar.shadow$("ui5-yearpicker"); const YEAR = 1997; calendar.setAttribute("timestamp", Date.UTC(YEAR) / 1000); - calendar.shadow$("ui5-calendar-header").shadow$(`div[data-sap-show-picker="Year"]`).click(); - assert.strictEqual(yearPicker.getProperty("_selectedYear"), YEAR, "Year is set"); - - calendar.shadow$("ui5-yearpicker").shadow$(`div[data-sap-timestamp="852076800"]`).click(); + calendar.shadow$("ui5-calendar-header").shadow$(`div[data-ui5-cal-header-btn-year]`).click(); + const focusedItemTimestamp = yearPicker.shadow$(`[tabindex="0"]`).getAttribute("data-sap-timestamp"); + assert.ok(new Date(parseInt(focusedItemTimestamp) * 1000).getUTCFullYear() === 1997, "The focused year is 1997"); }); it("Calendar doesn't mark year as selected when there are no selected dates", () => { + browser.url("http://localhost:8080/test-resources/pages/Calendar.html"); const calendar = browser.$("#calendar1"); calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 10, 1, 0, 0, 0)).valueOf() / 1000); - calendar.shadow$("ui5-calendar-header").shadow$(`div[data-sap-show-picker="Year"]`).click(); - const focusedItem = calendar.shadow$("ui5-yearpicker").shadow$(`[data-sap-timestamp="946684800"]`); + calendar.shadow$("ui5-calendar-header").shadow$(`div[data-ui5-cal-header-btn-year]`).click(); + const focusedItem = calendar.shadow$("ui5-yearpicker").shadow$(`[data-sap-timestamp="973036800"]`); - assert.ok(focusedItem.isFocusedDeep(), "Current year element is the acrive element"); - assert.notOk(focusedItem.hasClass("ui5-mp-item--selected"), "Current year is not selected"); - focusedItem.click(); + assert.ok(focusedItem.isFocusedDeep(), "Current year element is the active element"); + assert.notOk(focusedItem.hasClass("ui5-yp-item--selected"), "Current year is not selected"); }); it("Calendar doesn't mark month as selected when there are no selected dates", () => { + browser.url("http://localhost:8080/test-resources/pages/Calendar.html"); const calendar = browser.$("#calendar1"); calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 10, 1, 0, 0, 0)).valueOf() / 1000); - calendar.shadow$("ui5-calendar-header").shadow$(`div[data-sap-show-picker="Month"]`).click(); + calendar.shadow$("ui5-calendar-header").shadow$(`div[data-ui5-cal-header-btn-month]`).click(); const focusedItem = calendar.shadow$("ui5-monthpicker").shadow$(`[data-sap-timestamp="973036800"]`); - assert.ok(focusedItem.isFocusedDeep(), "Current month element is the acrive element"); + assert.ok(focusedItem.isFocusedDeep(), "Current month element is the active element"); assert.notOk(focusedItem.hasClass("ui5-mp-item--selected"), "Current month is not selected"); - focusedItem.click(); }); it("Page up/down increments/decrements the month value", () => { + browser.url("http://localhost:8080/test-resources/pages/Calendar.html"); const calendar = browser.$("#calendar1"); + calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 10, 1, 0, 0, 0)).valueOf() / 1000); - calendar.shadow$("ui5-daypicker").shadow$(".ui5-dp-days-names-container").click(); + calendar.shadow$("ui5-daypicker").shadow$(`[tabindex="0"]`).click(); browser.keys('PageUp'); assert.deepEqual(new Date(calendar.getProperty("timestamp") * 1000), new Date(Date.UTC(2000, 9, 1, 0, 0, 0))); @@ -137,7 +114,7 @@ describe("Calendar general interaction", () => { const calendar = browser.$("#calendar1"); calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 10, 1, 0, 0, 0)).valueOf() / 1000); - calendar.shadow$("ui5-daypicker").shadow$(".ui5-dp-days-names-container").click(); + calendar.shadow$("ui5-daypicker").shadow$(`[tabindex="0"]`).click(); browser.keys(['Shift', 'PageUp']); assert.deepEqual(new Date(calendar.getProperty("timestamp") * 1000), new Date(Date.UTC(1999, 10, 1, 0, 0, 0))); @@ -151,7 +128,7 @@ describe("Calendar general interaction", () => { const calendar = browser.$("#calendar1"); calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 10, 1, 0, 0, 0)).valueOf() / 1000); - calendar.shadow$("ui5-daypicker").shadow$(".ui5-dp-days-names-container").click(); + calendar.shadow$("ui5-daypicker").shadow$(`[tabindex="0"]`).click(); browser.keys(['Control', 'Shift', 'PageUp']); assert.deepEqual(new Date(calendar.getProperty("timestamp") * 1000), new Date(Date.UTC(1990, 10, 1, 0, 0, 0))); @@ -165,6 +142,7 @@ describe("Calendar general interaction", () => { const calendar = browser.$("#calendar1"); calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 9, 1, 0, 0, 0)).valueOf() / 1000); + calendar.shadow$("ui5-daypicker").shadow$(`[tabindex="0"]`).click(); browser.keys(["F4"]); browser.keys('PageUp'); @@ -176,9 +154,11 @@ describe("Calendar general interaction", () => { }); it("Page up/down increments/decrements the year range in the year picker", () => { + browser.url("http://localhost:8080/test-resources/pages/Calendar.html"); const calendar = browser.$("#calendar1"); calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 9, 1, 0, 0, 0)).valueOf() / 1000); + calendar.shadow$("ui5-daypicker").shadow$(`[tabindex="0"]`).click(); browser.keys(['Shift', 'F4']); browser.keys('PageUp'); @@ -190,26 +170,22 @@ describe("Calendar general interaction", () => { }); it("When month picker is shown the month button is hidden", () => { - const calendarHeader = browser.$("#calendar1").shadow$("ui5-calendar-header"); + browser.url("http://localhost:8080/test-resources/pages/Calendar.html"); + const calendar = browser.$("#calendar1"); + const calendarHeader = calendar.shadow$("ui5-calendar-header"); + calendar.shadow$("ui5-daypicker").shadow$(`[tabindex="0"]`).click(); browser.keys(["F4"]); browser.keys('PageUp'); - assert.ok(calendarHeader.shadow$(".ui5-calheader-middlebtn").getAttribute("hidden"), "The button for month is hidden"); + assert.ok(calendarHeader.shadow$("[data-ui5-cal-header-btn-month]").getAttribute("hidden"), "The button for month is hidden"); browser.keys("Space"); }); it("Calendar with 'Multiple' selection type", () => { + browser.url("http://localhost:8080/test-resources/pages/Calendar.html"); const calendar = browser.$("#calendar1"); - calendar.setAttribute("selection", "Multiple"); - let selectedDates = browser.execute(() => document.getElementById("calendar1").selectedDates ); - - // deselect previously selected dates - selectedDates.forEach(timestamp => { - const dateDOM = calendar.shadow$("ui5-daypicker").shadow$(`[data-sap-timestamp="${timestamp}"]`); - dateDOM.click(); - }); - + calendar.setAttribute("selection-mode", "Multiple"); calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 9, 10, 0, 0, 0)).valueOf() / 1000); const dates = [ @@ -223,15 +199,16 @@ describe("Calendar general interaction", () => { assert.ok(date.hasClass("ui5-dp-item--selected"), `${date.getAttribute("data-sap-timestamp")} is selected`); }); - selectedDates = browser.execute(() => document.getElementById("calendar1").selectedDates ); + const selectedDates = calendar.getProperty("selectedDates"); assert.deepEqual(selectedDates, [971136000, 971222400, 971308800], "Change event is fired with proper data"); }); it("Keyboard navigation works properly, when calendar selection type is set to 'Multiple'", () => { + browser.url("http://localhost:8080/test-resources/pages/Calendar.html"); const toggleButton = browser.$("#weekNumbersButton"); const calendar = browser.$("#calendar1"); - calendar.setAttribute("selection", "Multiple"); + calendar.setAttribute("selection-mode", "Multiple"); calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 9, 10, 0, 0, 0)).valueOf() / 1000); toggleButton.click(); @@ -248,9 +225,10 @@ describe("Calendar general interaction", () => { }); it("Calendar with 'Range' selection type", () => { + browser.url("http://localhost:8080/test-resources/pages/Calendar.html"); const calendar = browser.$("#calendar1"); calendar.setAttribute("timestamp", new Date(Date.UTC(2000, 9, 10, 0, 0, 0)).valueOf() / 1000); - calendar.setAttribute("selection", "Range"); + calendar.setAttribute("selection-mode", "Range"); const dates = [ calendar.shadow$("ui5-daypicker").shadow$(`[data-sap-timestamp="971740800"]`), @@ -265,7 +243,7 @@ describe("Calendar general interaction", () => { assert.ok(dates[1].hasClass("ui5-dp-item--selected-between"), `${dates[1].getAttribute("data-sap-timestamp")} is selected between`); assert.ok(dates[2].hasClass("ui5-dp-item--selected"), `${dates[2].getAttribute("data-sap-timestamp")} is selected`); - const selectedDates = browser.execute(() => document.getElementById("calendar1").selectedDates ); + const selectedDates = calendar.getProperty("selectedDates"); assert.deepEqual(selectedDates, [971740800, 971913600], "Change event is fired with proper data"); }); diff --git a/packages/main/test/specs/DatePicker-fg.spec.js b/packages/main/test/specs/DatePicker-fg.spec.js deleted file mode 100644 index 33ed004e86e0..000000000000 --- a/packages/main/test/specs/DatePicker-fg.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -const DatePickerFGPage = require("../pageobjects/DatePickerFGPage"); -const assert = require("chai").assert; - -describe('Date Picker Field Glass modifications', () => { - it('direct usage for comparison', () => { - browser.url('http://localhost:8080/test-resources/pages/DatePicker_fg.html'); - - const staticAreaItemClassName = browser.getStaticAreaItemClassName("#ui5-datepicker--startDate"); - let popoverContent = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover") - assert.ok(!popoverContent.getProperty("opened"), "popover is initially hidden"); - - const innerInput = browser.$("#ui5-datepicker--startDate").shadow$("ui5-input").shadow$("input"); - innerInput.click(); - assert.ok(popoverContent.getProperty("opened"), "popover is visible"); - - innerInput.keys("Tab"); - const dpEnd = browser.$("#ui5-datepicker--endDate"); - assert.ok(!popoverContent.getProperty("opened"), "popover is hidden"); - assert.ok(dpEnd.isFocused(), "focus is on end date"); - }); - - it('Input click and tab out', () => { - DatePickerFGPage.open(); - assert.ok(!DatePickerFGPage.startPopoverContent.getProperty("opened"), "popover is initially hidden"); - - DatePickerFGPage.startInnerInput.click(); - assert.ok(DatePickerFGPage.startPopoverContent.getProperty("opened"), "popover is visible after click"); - - DatePickerFGPage.startInnerInput.keys("Tab"); - assert.ok(!DatePickerFGPage.startPopoverContent.getProperty("opened"), "popover is hidden after tab out"); - assert.ok(DatePickerFGPage.dpEnd.isFocused(), "focus is on end date"); - }); -}); diff --git a/packages/main/test/specs/DatePicker.spec.js b/packages/main/test/specs/DatePicker.spec.js index 55543276a1ba..26ceeb62c7be 100644 --- a/packages/main/test/specs/DatePicker.spec.js +++ b/packages/main/test/specs/DatePicker.spec.js @@ -50,18 +50,9 @@ describe("Date Picker Tests", () => { assert.ok(contentWrapper.isDisplayedInViewport(), "content wrapper has error styles"); }); - it("Can focus the input after open", () => { - datepicker.id = "#dp1"; - datepicker.openPicker({ focusInput: true }); - const a = datepicker.innerInput.isFocusedDeep(); - - console.log(datepicker.innerInput.isFocusedDeep()); - assert.ok(a, "inner input is focused"); - }); - it("Value State Message", () => { datepicker.id = "#dp17" - datepicker.root.click(); + datepicker.input.click(); const inputStaticAreaItem = datepicker.inputStaticAreaItem; const popover = inputStaticAreaItem.shadow$("ui5-popover"); @@ -140,8 +131,8 @@ describe("Date Picker Tests", () => { it("focusout fires change", () => { datepicker.id = "#dp5"; - datepicker.root.click(); - datepicker.innerInput.setValue("Jan 6, 2015"); + datepicker.input.click(); + datepicker.root.keys("Jan 1, 1999"); browser.$("#dp1").shadow$("ui5-input").shadow$("input").click(); //click elsewhere to focusout assert.equal(browser.$("#lbl").getHTML(false), "1", 'change has fired once'); @@ -154,7 +145,7 @@ describe("Date Picker Tests", () => { const timestamp_8_Jan_2015 = timestamp_6_Jan_2015 + 2 * 24 * 60 * 60; //type in the input - datepicker.innerInput.setValue("Jan 6, 2015"); + datepicker.root.setProperty("value", "Jan 6, 2015"); //open picker datepicker.valueHelpIcon.click(); @@ -212,12 +203,12 @@ describe("Date Picker Tests", () => { browser.url("http://localhost:8080/test-resources/pages/DatePicker_test_page.html?sap-ui-language=bg"); datepicker.id = "#dp7_1"; - datepicker.innerInput.setValue("фев 6, 2019"); + datepicker.root.setProperty("value", "фев 6, 2019"); datepicker.valueHelpIcon.click(); const firstDisplayedDate = datepicker.getFirstDisplayedDate(); - assert.ok(firstDisplayedDate.getProperty("id").indexOf("1548633600") > -1, "28 Jan is the first displayed date for Feb 2019") + assert.ok(firstDisplayedDate.getAttribute("data-sap-timestamp").indexOf("1548633600") > -1, "28 Jan is the first displayed date for Feb 2019") const calendarDate_3_Feb_2019 = datepicker.getPickerDate(1549152000); @@ -229,14 +220,14 @@ describe("Date Picker Tests", () => { datepicker.id = "#dp7_2"; - datepicker.innerInput.setValue("Jan 30, 2019"); + datepicker.root.setProperty("value", "Jan 30, 2019"); datepicker.valueHelpIcon.click(); datepicker.btnNext.click(); const firstDisplayedDate = datepicker.getFirstDisplayedDate(); // first displayed date should be Jan 27, 2019, so this is February - assert.ok(firstDisplayedDate.getProperty("id").indexOf("1548547200") > -1, "Feb is the displayed month"); + assert.ok(firstDisplayedDate.getAttribute("data-sap-timestamp").indexOf("1548547200") > -1, "Feb is the displayed month"); }); it("picker stays open on input click", () => { @@ -363,7 +354,7 @@ describe("Date Picker Tests", () => { datepicker.valueHelpIcon.click() browser.keys("F4"); - assert.notOk(datepicker.calendar.getProperty("_monthPicker")._hidden, "Month picker is open"); + assert.notOk(datepicker.calendar.shadow$("ui5-monthpicker")._hidden, "Month picker is open"); datepicker.valueHelpIcon.click(); // close the datepicker }); @@ -373,7 +364,7 @@ describe("Date Picker Tests", () => { datepicker.valueHelpIcon.click() browser.keys(['Shift', 'F4']); - assert.notOk(datepicker.calendar.getProperty("_yearPicker")._hidden, "Year picker is open"); + assert.notOk(datepicker.calendar.shadow$("ui5-yearpicker")._hidden, "Year picker is open"); datepicker.valueHelpIcon.click(); // close the datepicker }); @@ -384,7 +375,7 @@ describe("Date Picker Tests", () => { browser.keys(['Shift', 'F4']); browser.keys('F4'); - assert.notOk(datepicker.calendar.getProperty("_monthPicker")._hidden, "Year picker is open"); + assert.notOk(datepicker.calendar.shadow$("ui5-monthpicker")._hidden, "Year picker is open"); datepicker.valueHelpIcon.click(); // close the datepicker }); @@ -396,7 +387,7 @@ describe("Date Picker Tests", () => { browser.keys('F4'); browser.keys(['Shift', 'F4']); - assert.notOk(datepicker.calendar.getProperty("_yearPicker")._hidden, "Year picker is open"); + assert.notOk(datepicker.calendar.shadow$("ui5-yearpicker")._hidden, "Year picker is open"); datepicker.valueHelpIcon.click(); // close the datepicker }); @@ -418,40 +409,36 @@ describe("Date Picker Tests", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Dec 31, 9999"); + datepicker.root.setProperty("value", "Dec 31, 9999"); datepicker.valueHelpIcon.click(); - assert.ok(datepicker.getFirstDisplayedDate().getProperty("id").indexOf(_28Nov9999) > -1, "28 Nov, 9999 is the first displayed date"); + assert.ok(datepicker.getFirstDisplayedDate().getAttribute("data-sap-timestamp").indexOf(_28Nov9999) > -1, "28 Nov, 9999 is the first displayed date"); }); it("daypicker extreme values min", () => { - var _1Jan0001 = "-62135596800"; + var _31Dec0000 = "-62135683200"; datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Jan 1, 0001"); + datepicker.root.setProperty("value", "Jan 1, 0001"); datepicker.valueHelpIcon.click(); - assert.ok(datepicker.getFirstDisplayedDate().getProperty("id").indexOf(_1Jan0001) > -1, "Jan 1, 0001 is the first displayed date"); + assert.ok(datepicker.getFirstDisplayedDate().getAttribute("data-sap-timestamp").indexOf(_31Dec0000) > -1, "Jan 1, 0001 is the second displayed date"); }); it("daypicker prev extreme values min", () => { - var _1Jan0001 = "-62135596800"; + var _31Dec0000 = "-62135683200"; datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Feb 1, 0001"); + datepicker.root.setProperty("value", "Feb 1, 0001"); datepicker.valueHelpIcon.click(); datepicker.btnPrev.click(); - assert.ok(datepicker.getFirstDisplayedDate().getProperty("id").indexOf(_1Jan0001) > -1, "Jan 1, 0001 is the first displayed date"); - - datepicker.btnPrev.click(); - - assert.ok(datepicker.getFirstDisplayedDate().getProperty("id").indexOf(_1Jan0001) > -1, "Jan 1, 0001 is the first displayed date"); + assert.ok(datepicker.getFirstDisplayedDate().getAttribute("data-sap-timestamp").indexOf(_31Dec0000) > -1, "Jan 1, 0001 is the second displayed date"); }); it("daypicker next extreme values max", () => { @@ -460,57 +447,45 @@ describe("Date Picker Tests", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Nov 31, 9999"); + datepicker.root.setProperty("value", "Nov 30, 9999"); datepicker.valueHelpIcon.click(); datepicker.btnNext.click(); - assert.ok(datepicker.getFirstDisplayedDate().getProperty("id").indexOf(_28Nov9999) > -1, "28 Nov, 9999 is the first displayed date"); - - datepicker.btnNext.click(); - - assert.ok(datepicker.getFirstDisplayedDate().getProperty("id").indexOf(_28Nov9999) > -1, "28 Nov, 9999 is the first displayed date"); + assert.ok(datepicker.getFirstDisplayedDate().getAttribute("data-sap-timestamp").indexOf(_28Nov9999) > -1, "28 Nov, 9999 is the first displayed date"); }); it("monthpicker next extreme values max", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Dec 31, 9998"); + datepicker.root.setProperty("value", "Dec 31, 9998"); datepicker.valueHelpIcon.click(); datepicker.btnMonth.click(); datepicker.btnNext.click(); assert.ok(datepicker.btnYear.getProperty("innerHTML").indexOf("9999") > -1, "year button's text is correct"); - - datepicker.btnNext.click(); - - assert.ok(datepicker.btnYear.getProperty("innerHTML").indexOf("9999") > -1, "year button's text is correct"); }); it("monthpicker prev extreme values min", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Jan 1, 0002"); + datepicker.root.setProperty("value", "Jan 1, 0002"); datepicker.valueHelpIcon.click(); datepicker.btnMonth.click(); datepicker.btnPrev.click(); assert.ok(datepicker.btnYear.getProperty("innerHTML").indexOf("0001") > -1, "year button's text is correct"); - - datepicker.btnPrev.click(); - - assert.ok(datepicker.btnYear.getProperty("innerHTML").indexOf("0001") > -1, "year button's text is correct"); }); it("yearpicker extreme values max", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Dec 31, 9995"); + datepicker.root.setProperty("value", "Dec 31, 9995"); datepicker.valueHelpIcon.click(); datepicker.btnYear.click(); @@ -522,7 +497,7 @@ describe("Date Picker Tests", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Jan 1, 0003"); + datepicker.root.setProperty("value", "Jan 1, 0003"); datepicker.valueHelpIcon.click(); datepicker.btnYear.click(); @@ -534,7 +509,7 @@ describe("Date Picker Tests", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Jan 1, 0012"); + datepicker.root.setProperty("value", "Jan 1, 0012"); datepicker.valueHelpIcon.click(); datepicker.btnYear.click(); @@ -544,17 +519,13 @@ describe("Date Picker Tests", () => { datepicker.btnPrev.click(); assert.ok(datepicker.getFirstDisplayedYear().getProperty("innerHTML").indexOf("0001") > -1, "First year in the year picker is correct"); - - datepicker.btnPrev.click(); - - assert.ok(datepicker.getFirstDisplayedYear().getProperty("innerHTML").indexOf("0001") > -1, "First year in the year picker is correct"); }); it("yearpicker next page extreme values max", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Dec 31, 9986"); + datepicker.root.setProperty("value", "Dec 31, 9986"); datepicker.valueHelpIcon.click(); datepicker.btnYear.click(); @@ -564,17 +535,13 @@ describe("Date Picker Tests", () => { datepicker.btnNext.click(); assert.ok(datepicker.getFirstDisplayedYear().getProperty("innerHTML").indexOf("9980") > -1, "First year in the year picker is correct"); - - datepicker.btnNext.click(); - - assert.ok(datepicker.getFirstDisplayedYear().getProperty("innerHTML").indexOf("9980") > -1, "First year in the year picker is correct"); }); it("yearpicker click extreme values max", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Dec 31, 9986"); + datepicker.root.setProperty("value", "Dec 31, 9986"); datepicker.valueHelpIcon.click(); datepicker.btnYear.click(); @@ -588,19 +555,26 @@ describe("Date Picker Tests", () => { assert.ok(datepicker.getFirstDisplayedYear().getProperty("innerHTML").indexOf("9976") > -1, "First year in the year picker is correct"); }); - it("yearpicker click extreme values min", () => { + it("yearpicker click extreme values min above 10", () => { datepicker.open(); datepicker.id = "#dp12"; - datepicker.innerInput.setValue("Jan 1, 0012"); + datepicker.root.setProperty("value", "Jan 1, 0012"); datepicker.valueHelpIcon.click(); datepicker.btnYear.click(); var thirdYear = datepicker.getDisplayedYear(2); assert.ok(thirdYear.getProperty("innerHTML").indexOf("0004") > -1, "Third year in the year picker is correct"); + }); + + it("yearpicker click extreme values min below 10", () => { + datepicker.open(); + datepicker.id = "#dp12"; + + datepicker.root.setProperty("value", "Jan 1, 0004"); + datepicker.valueHelpIcon.click(); - thirdYear.click(); datepicker.btnYear.click(); assert.ok(datepicker.getFirstDisplayedYear().getProperty("innerHTML").indexOf("0001") > -1, "First year in the year picker is correct"); @@ -633,7 +607,7 @@ describe("Date Picker Tests", () => { it("Going under the minimum date changes value state", () => { datepicker.id = "#dp33"; - datepicker.root.click(); + datepicker.input.click(); datepicker.root.keys("Jan 1, 1999"); datepicker.root.keys("Enter"); @@ -646,7 +620,7 @@ describe("Date Picker Tests", () => { it("Going over the maximum date changes value state", () => { datepicker.id = "#dp33"; - datepicker.root.click(); + datepicker.input.click(); while(datepicker.root.getValue() !== ""){ datepicker.root.keys("Backspace"); } @@ -663,7 +637,7 @@ describe("Date Picker Tests", () => { it("Maximum or minimum date changes value state to none", () => { datepicker.id = "#dp33"; - datepicker.root.click(); + datepicker.input.click(); while(datepicker.root.getValue() !== ""){ datepicker.root.keys("Backspace"); } @@ -673,7 +647,7 @@ describe("Date Picker Tests", () => { assert.equal(datepicker.input.getProperty("valueState"), "None", "value state of the input is valid"); - datepicker.root.click(); + datepicker.input.click(); while(datepicker.root.getValue() !== ""){ datepicker.root.keys("Backspace"); } @@ -690,25 +664,26 @@ describe("Date Picker Tests", () => { it("Years are disabled when out of range", () => { datepicker.id = "#dp33"; - datepicker.root.click(); + datepicker.input.click(); while(datepicker.root.getValue() !== ""){ datepicker.root.keys("Backspace"); } datepicker.root.keys("Jan 8, 2100"); datepicker.root.keys("Enter"); - datepicker.openPicker({ focusInput: false }); + datepicker.openPicker(); datepicker.btnYear.click(); assert.ok(datepicker.getDisplayedYear(11).hasClass("ui5-yp-item--disabled"), "Years out of range are disabled"); datepicker.root.keys("ArrowRight"); - assert.ok(datepicker.getDisplayedYear(0).isFocusedDeep(), "Years out of range can not be reached with keyboard"); + assert.ok(datepicker.getDisplayedYear(10).isFocusedDeep(), "Focus remained on year 2100"); + assert.ok(!datepicker.getDisplayedYear(11).isFocusedDeep(), "Years out of range (2101) can not be reached with keyboard"); }); it("Months are disabled when out of range", () => { datepicker.id = "#dp33"; - datepicker.openPicker({ focusInput: false }); + datepicker.openPicker(); datepicker.btnMonth.click(); assert.ok(datepicker.getDisplayedMonth(10).hasClass("ui5-mp-item--disabled"), "Months out of range are disabled"); @@ -721,7 +696,7 @@ describe("Date Picker Tests", () => { datepicker.id = "#dp33"; datepicker.root.keys("Escape"); - datepicker.openPicker({ focusInput: false }); + datepicker.openPicker(); assert.ok(datepicker.getDisplayedDay(15).hasClass("ui5-dp-item--disabled"), "Days out of range are disabled"); }); @@ -731,7 +706,7 @@ describe("Date Picker Tests", () => { datepicker.root.keys("Escape"); datepicker.id = "#dp34"; - datepicker.openPicker({ focusInput: false }); + datepicker.openPicker(); assert.ok(datepicker.getDisplayedDay(14).isFocusedDeep(), "Days out of range are disabled"); }); @@ -739,7 +714,7 @@ describe("Date Picker Tests", () => { datepicker.id = "#dp33"; datepicker.root.keys("Escape"); - datepicker.openPicker({ focusInput: false }); + datepicker.openPicker(); assert.equal(datepicker.getDisplayedDay(9).hasClass("ui5-dp-item--disabled"), false , "Min date is included"); assert.equal(datepicker.getDisplayedDay(11).hasClass("ui5-dp-item--disabled"), false, "Max date is included"); @@ -779,14 +754,14 @@ describe("Date Picker Tests", () => { assert.strictEqual(monthpickerContent.getAttribute("role"), "grid", "Calendar root have correct role attribute"); assert.strictEqual(monthpickerContent.getAttribute("aria-roledescription"), "Calendar", "Calendar root have correct roledescription") - + }); it("DayPicker content wrapped", ()=>{ datepicker.id = "#dp19"; datepicker.open(); let arr = datepicker.getDayPickerContent(); - + arr.forEach(function(el){ assert.strictEqual(el.getAttribute("role"), "row", "Content wrapper has correct role"); }); @@ -796,10 +771,10 @@ describe("Date Picker Tests", () => { // browser.url("http://localhost:8080/test-resources/pages/DatePicker_test_page.html?sap-ui-language=en"); // datepicker.root.setAttribute("primary-calendar-type", "Gregorian"); // datepicker.id = "#dp13"; - // datepicker.openPicker({ focusInput: true }); + // datepicker.openPicker(); // datepicker.root.keys("May 3, 2100"); // datepicker.root.keys("Enter"); - + // const content = Array.from(datepicker.getDayPickerDayNames()); // const dayName = ["Week number", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; // content.forEach((element,index) => { @@ -812,7 +787,7 @@ describe("Date Picker Tests", () => { browser.url("http://localhost:8080/test-resources/pages/DatePicker_test_page.html?sap-ui-language=en"); datepicker.root.setAttribute("primary-calendar-type", "Gregorian"); datepicker.id = "#dp13"; - datepicker.openPicker({ focusInput: true }); + datepicker.openPicker(); datepicker.root.keys("May 3, 2100"); datepicker.root.keys("Enter"); @@ -833,9 +808,10 @@ describe("Date Picker Tests", () => { browser.url("http://localhost:8080/test-resources/pages/DatePicker_test_page.html?sap-ui-language=en"); datepicker.root.setAttribute("primary-calendar-type", "Gregorian"); datepicker.id = "#dp13"; - datepicker.openPicker({ focusInput: true }); - datepicker.root.keys("May 3, 2100"); - datepicker.root.keys("Enter"); + datepicker.openPicker(); + datepicker.input.click(); + browser.keys("May 3, 2100"); + browser.keys("Enter"); const data = Array.from(datepicker.getDayPickerDatesRow(2)); assert.strictEqual(data[0].getAttribute("aria-label"), "Calendar Week 18", "First columnheader have Week number aria-label"); @@ -848,7 +824,7 @@ describe("Date Picker Tests", () => { const EXPECTED_ARIA_LABEL = "Hello World"; datepicker.id = "#dpAriaLabel"; - + assert.strictEqual(datepicker.innerInput.getAttribute("aria-label"), EXPECTED_ARIA_LABEL, "The aria-label is correct.") }); @@ -864,8 +840,8 @@ describe("Date Picker Tests", () => { it("Page up/down increments/decrements the day value", () => { datepicker.id = "#dp1"; - datepicker.innerInput.setValue("Jan 1, 2000"); - datepicker.root.click(); + datepicker.root.setProperty("value", "Jan 1, 2000"); + datepicker.input.click(); browser.keys('PageDown'); @@ -884,8 +860,8 @@ describe("Date Picker Tests", () => { it("Shift + Page up/down increments/decrements the month value", () => { datepicker.id = "#dp1"; - datepicker.innerInput.setValue("Jan 1, 2000"); - datepicker.root.click(); + datepicker.root.setProperty("value", "Jan 1, 2000"); + datepicker.input.click(); browser.keys(['Shift', 'PageDown']); @@ -904,8 +880,8 @@ describe("Date Picker Tests", () => { it("Ctrl + Shift + Page up/down increments/decrements the year value", () => { datepicker.id = "#dp1"; - datepicker.innerInput.setValue("Jan 1, 2000"); - datepicker.root.click(); + datepicker.root.setProperty("value", "Jan 1, 2000"); + datepicker.input.click(); browser.keys(['Control', 'Shift', 'PageDown']); diff --git a/packages/main/test/specs/DateTimePicker.spec.js b/packages/main/test/specs/DateTimePicker.spec.js index c41d68c9e69c..8b6804f26811 100644 --- a/packages/main/test/specs/DateTimePicker.spec.js +++ b/packages/main/test/specs/DateTimePicker.spec.js @@ -1,9 +1,11 @@ const assert = require("chai").assert; const openPickerById = (id, options) => { - return browser.execute((id, options) => { + const res = browser.execute((id, options) => { return document.querySelector(`#${id}`).openPicker(options); }, id, options); + browser.pause(1000); + return res; } const closePickerById = id => { @@ -37,7 +39,7 @@ const getTimeSlidersCount = id => { const picker = getPicker(id); return browser.execute( picker => { - return picker.querySelectorAll("ui5-wheelslider").length; + return picker.querySelector("ui5-time-selection").shadowRoot.querySelectorAll("ui5-wheelslider").length; }, picker); } @@ -75,13 +77,21 @@ describe("DateTimePicker general interaction", () => { // select the next day (the right from the selected) const selectedDay = picker.$("ui5-calendar").shadow$("ui5-daypicker").shadow$(".ui5-dp-item--selected"); selectedDay.click(); - selectedDay.keys("ArrowRight"); + browser.keys("ArrowRight"); browser.keys("Space"); // select new time - picker.$(".ui5-dt-hours-wheel").setProperty("value","01"); - picker.$(".ui5-dt-minutes-wheel").setProperty("value","02"); - picker.$(".ui5-dt-seconds-wheel").setProperty("value","03"); + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="hours"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown"); // select 01 + + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="minutes"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown"); // select 0 + browser.keys("ArrowDown"); browser.keys("ArrowDown"); // select 02 + + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="seconds"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown"); // select 0 + browser.keys("ArrowDown"); browser.keys("ArrowDown"); browser.keys("ArrowDown"); // select 03 + picker.$("#ok").click(); // assert @@ -93,10 +103,12 @@ describe("DateTimePicker general interaction", () => { // test submit from empty value to current date/time value openPickerById("dt1"); + const picker = getPicker("dt1"); const inputCounter = browser.$("#input1"); const submitBtn = getSubmitButton("dt1"); // act + picker.$("ui5-calendar").shadow$("ui5-daypicker").shadow$("[data-sap-focus-ref]").click(); // select a date to enable the OK button submitBtn.click(); // assert @@ -158,7 +170,7 @@ describe("DateTimePicker general interaction", () => { // assert const picker = getPicker("dt"); - const expanded = picker.$(".ui5-dt-hours-wheel").getProperty("_expanded"); + const expanded = picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="hours"]`).getProperty("expanded"); assert.strictEqual(expanded, true, "The hours slider is expanded."); closePickerById("dt"); @@ -171,10 +183,20 @@ describe("DateTimePicker general interaction", () => { openPickerById("dtTest12AM"); const picker = getPicker("dtTest12AM"); - picker.$(".ui5-dt-hours-wheel").setProperty("value","12"); - picker.$(".ui5-dt-minutes-wheel").setProperty("value","00"); - picker.$(".ui5-dt-seconds-wheel").setProperty("value","00"); - picker.$(".ui5-dt-periods-wheel").setProperty("value","AM"); + + // select new time + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="hours"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageUp"); // select 12 + + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="minutes"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown");// select 00 + + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="seconds"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown");// select 00 + + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="period"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown");// select AM + picker.$("#ok").click(); // assert @@ -189,10 +211,21 @@ describe("DateTimePicker general interaction", () => { openPickerById("dtTest12PM"); const picker = getPicker("dtTest12PM"); - picker.$(".ui5-dt-hours-wheel").setProperty("value","12"); - picker.$(".ui5-dt-minutes-wheel").setProperty("value","00"); - picker.$(".ui5-dt-seconds-wheel").setProperty("value","00"); - picker.$(".ui5-dt-periods-wheel").setProperty("value","PM"); + + // select new time + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="hours"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageUp"); // select 12 + + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="minutes"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown");// select 00 + + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="seconds"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown");// select 00 + + picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="period"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageUp");// select PM + + picker.$("#ok").click(); // assert diff --git a/packages/main/test/specs/DurationPicker.spec.js b/packages/main/test/specs/DurationPicker.spec.js index b976255df969..d8c8d80f484c 100644 --- a/packages/main/test/specs/DurationPicker.spec.js +++ b/packages/main/test/specs/DurationPicker.spec.js @@ -5,8 +5,8 @@ describe("Duration Picker general interaction", () => { browser.url("http://localhost:8080/test-resources/pages/DurationPicker.html"); it("Tests opening and closing of popover", () => { - const durationPicker = browser.$("#duration-picker1") - const duratationPickerIcon = durationPicker.shadow$(".ui5-duration-picker-input-icon-button"); + const durationPicker = browser.$("#duration-picker1"); + const duratationPickerIcon = durationPicker.shadow$(".ui5-time-picker-input-icon-button"); const staticAreaItemClassName = browser.getStaticAreaItemClassName("#duration-picker1"); const popover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); @@ -22,30 +22,31 @@ describe("Duration Picker general interaction", () => { }); it("Tests max-value", () => { - const durationPicker = browser.$("#duration-picker4") - const duratationPickerIcon = durationPicker.shadow$(".ui5-duration-picker-input-icon-button"); + const durationPicker = browser.$("#duration-picker4"); + const duratationPickerIcon = durationPicker.shadow$(".ui5-time-picker-input-icon-button"); // act duratationPickerIcon.click(); // assert - the custom max-value assert.strictEqual(durationPicker.getProperty("value"), durationPicker.getProperty("maxValue") , - "The value and the max-vaoue are equal."); - assert.strictEqual(durationPicker.getProperty("_maxValue")[0], "05", "max value is read correctly"); - assert.strictEqual(durationPicker.getProperty("_maxValue")[1], "10", "max value is read correctly"); - assert.strictEqual(durationPicker.getProperty("_maxValue")[2], "08", "max value is read correctly"); + "The value and the max-value are equal."); + assert.strictEqual(durationPicker.getProperty("maxHours"), 5, "max hours value is read correctly"); + assert.strictEqual(durationPicker.getProperty("maxMinutes"), 10, "max minutes value is read correctly"); + assert.strictEqual(durationPicker.getProperty("maxSeconds"), 8, "max seconds value is read correctly"); }); it("Tests seconds-step property", () => { const durationPicker = browser.$("#duration-picker6"); assert.strictEqual(durationPicker.getProperty("value"), "05:10:00", "The initial value is taking in consideration the seconds-step property"); - + durationPicker.click(); + durationPicker.click(); // On click it's all selected, so lose the selection not to delete everything with backspace durationPicker.keys("Backspace"); durationPicker.keys("2"); durationPicker.keys("Enter"); - + assert.strictEqual(durationPicker.getProperty("value"), "05:10:00", "Editing the value is taking in consideration the seconds-step property"); }); @@ -53,12 +54,13 @@ describe("Duration Picker general interaction", () => { const durationPicker = browser.$("#duration-picker7"); assert.strictEqual(durationPicker.getProperty("value"), "05:10", "The initial value is taking in consideration the minutes-step property"); - + durationPicker.click(); + durationPicker.click(); // On click it's all selected, so lose the selection not to delete everything with backspace durationPicker.keys("Backspace"); durationPicker.keys("2"); durationPicker.keys("Enter"); - + assert.strictEqual(durationPicker.getProperty("value"), "05:10", "Editing the value is taking in consideration the minutes-step property"); }); @@ -81,16 +83,16 @@ describe("Duration Picker general interaction", () => { }); it("Tests default max-value", () => { - const durationPicker = browser.$("#duration-default") - const duratationPickerIcon = durationPicker.shadow$(".ui5-duration-picker-input-icon-button"); + const durationPicker = browser.$("#duration-default"); + const duratationPickerIcon = durationPicker.shadow$(".ui5-time-picker-input-icon-button"); // act duratationPickerIcon.click(); // assert - the default max-value - assert.strictEqual(durationPicker.getProperty("_maxValue")[0], "23", "max value is read correctly"); - assert.strictEqual(durationPicker.getProperty("_maxValue")[1], "59", "max value is read correctly"); - assert.strictEqual(durationPicker.getProperty("_maxValue")[2], "59", "max value is read correctly"); + assert.strictEqual(durationPicker.getProperty("maxHours"), 23, "max value is read correctly"); + assert.strictEqual(durationPicker.getProperty("maxMinutes"), 59, "max value is read correctly"); + assert.strictEqual(durationPicker.getProperty("maxSeconds"), 59, "max value is read correctly"); // close picker duratationPickerIcon.click(); diff --git a/packages/main/test/specs/TimePicker.spec.js b/packages/main/test/specs/TimePicker.spec.js index ab984caf998e..fec8faf927de 100644 --- a/packages/main/test/specs/TimePicker.spec.js +++ b/packages/main/test/specs/TimePicker.spec.js @@ -21,10 +21,11 @@ describe("TimePicker general interaction", () => { // act timepicker.setProperty("value", "11:12:13"); timepicker.shadow$("ui5-input").$(".ui5-time-picker-input-icon-button").click(); + // browser.pause(500); - const hoursSliderValue = timepickerPopover.$(".ui5-time-picker-hours-wheelslider").getValue(); - const minutesSliderValue = timepickerPopover.$(".ui5-time-picker-minutes-wheelslider").getValue(); - const secondsSliderValue = timepickerPopover.$(".ui5-time-picker-seconds-wheelslider").getValue(); + const hoursSliderValue = timepickerPopover.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="hours"]`).getValue(); + const minutesSliderValue = timepickerPopover.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="minutes"]`).getValue(); + const secondsSliderValue = timepickerPopover.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="seconds"]`).getValue(); // assert assert.strictEqual(hoursSliderValue, "11", "Hours are equal"); @@ -35,14 +36,29 @@ describe("TimePicker general interaction", () => { it("tests sliders submit value", () => { const timepicker = browser.$("#timepicker"); const staticAreaItemClassName = browser.getStaticAreaItemClassName("#timepicker"); - const timepickerPopover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); + const picker = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); // act - timepickerPopover.setProperty("opened", true); - timepickerPopover.$(".ui5-time-picker-hours-wheelslider").setProperty("value","14"); - timepickerPopover.$(".ui5-time-picker-minutes-wheelslider").setProperty("value","15"); - timepickerPopover.$(".ui5-time-picker-seconds-wheelslider").setProperty("value","16"); - timepickerPopover.$("#submit").click(); + timepicker.shadow$("ui5-input").$(".ui5-time-picker-input-icon-button").click(); + browser.keys("Escape"); + timepicker.shadow$("ui5-input").$(".ui5-time-picker-input-icon-button").click(); + + // picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="hours"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown"); // select 00 + for (let i=1; i<= 14; i++) browser.keys("ArrowDown"); // Select 14 + + // picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="minutes"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("Tab"); + browser.keys("PageDown");// select 00 + for (let i=1; i<= 15; i++) browser.keys("ArrowDown"); // Select 15 + + // picker.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="seconds"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("Tab"); + browser.keys("PageDown");// select 00 + for (let i=1; i<= 16; i++) browser.keys("ArrowDown"); // Select 16 + + browser.keys("Tab"); // Move to submit + browser.keys("Enter"); // Enter on submit const textValue = timepicker.shadow$("ui5-input").getValue(); assert.strictEqual(textValue.substring(0,2), "14", "Hours are equal"); @@ -53,8 +69,8 @@ describe("TimePicker general interaction", () => { const timepicker = browser.$("#timepicker"); timepicker.click(); - timepicker.keys("123123123"); - timepicker.keys("Enter"); + browser.keys("123123123"); + browser.keys("Enter"); assert.strictEqual(timepicker.shadow$("ui5-input").getProperty("valueState"), "Error", "The value state is on error"); }); @@ -87,7 +103,11 @@ describe("TimePicker general interaction", () => { // act - submit value after changing time icon.click(); - timepickerPopover.$(".ui5-time-picker-hours-wheelslider").setProperty("value", "10"); + + timepickerPopover.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="hours"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("PageDown"); // select 00 + for (let i=1; i<= 10; i++) browser.keys("ArrowDown"); // Select 10 + timepickerPopover.$("#submit").click(); // assert @@ -102,8 +122,9 @@ describe("TimePicker general interaction", () => { // act - submit value after changing time icon.click(); - timepickerPopover.$(".ui5-time-picker-hours-wheelslider").setProperty("value", "11"); - timepickerPopover.$("#submit").click(); + timepickerPopover.$("ui5-time-selection").shadow$(`ui5-wheelslider[data-sap-slider="hours"]`).shadow$(`div[tabindex="0"]`).click(); + browser.keys("ArrowDown"); // select 11 + timepickerPopover.$("#submit").click(); // click submit (the other test tests Enter, this one tests click) // assert assert.strictEqual(changeResult.getProperty("value"), "2", "Change fired as expected"); @@ -116,7 +137,7 @@ describe("TimePicker general interaction", () => { // act timepicker.click(); while(timepicker.shadow$("ui5-input").getProperty("value") !== ""){ - timepicker.keys("Backspace"); + browser.keys("Backspace"); } button.click(); @@ -129,47 +150,47 @@ describe("TimePicker general interaction", () => { // act timepicker.click(); - timepicker.keys(['Shift', 'PageUp']); - timepicker.keys('Shift'); + browser.keys(['Shift', 'PageUp']); + browser.keys('Shift'); // assert assert.strictEqual(timepicker.shadow$("ui5-input").getProperty("value"), "12:01:01", "The value of minutes is +1"); // act timepicker.click(); - timepicker.keys(['Shift', 'PageDown']); - timepicker.keys('Shift'); + browser.keys(['Shift', 'PageDown']); + browser.keys('Shift'); // assert assert.strictEqual(timepicker.shadow$("ui5-input").getProperty("value"), "12:00:01", "The value of minutes is -1"); // act timepicker.click(); - timepicker.keys('PageUp'); + browser.keys('PageUp'); // assert assert.strictEqual(timepicker.shadow$("ui5-input").getProperty("value"), "01:00:01", "The value of hours is +1"); // act timepicker.click(); - timepicker.keys('PageDown'); + browser.keys('PageDown'); // assert assert.strictEqual(timepicker.shadow$("ui5-input").getProperty("value"), "12:00:01", "The value of hours is -1"); // act timepicker.click(); - timepicker.keys(['Shift', 'Control', 'PageUp']); - timepicker.keys('Shift'); - timepicker.keys('Control'); + browser.keys(['Shift', 'Control', 'PageUp']); + browser.keys('Shift'); + browser.keys('Control'); // assert assert.strictEqual(timepicker.shadow$("ui5-input").getProperty("value"), "12:00:02", "The value of seconds is +1"); // act timepicker.click(); - timepicker.keys(['Shift', 'Control', 'PageDown']); - timepicker.keys('Shift'); - timepicker.keys('Control'); + browser.keys(['Shift', 'Control', 'PageDown']); + browser.keys('Shift'); + browser.keys('Control'); // assert assert.strictEqual(timepicker.shadow$("ui5-input").getProperty("value"), "12:00:01", "The value of seconds is -1"); }); -}); \ No newline at end of file +}); diff --git a/packages/main/test/specs/WheelSlider.spec.js b/packages/main/test/specs/WheelSlider.spec.js index 8112b72a745e..6552f55539c9 100644 --- a/packages/main/test/specs/WheelSlider.spec.js +++ b/packages/main/test/specs/WheelSlider.spec.js @@ -6,7 +6,7 @@ describe("Wheel Slider general interaction", () => { before(() => { browser.$("#wheelslider").setProperty("_items",["1","2","3","4","5","6","7"]); browser.$("#wheelslider").setProperty("value","1"); - browser.$("#wheelslider").setProperty("_expanded",true); + browser.$("#wheelslider").setProperty("expanded",true); browser.$("body").setAttribute("class", "sapUiSizeCompact"); }); @@ -26,7 +26,7 @@ describe("Wheel Slider general interaction", () => { it("Arrow up button is working", () => { const slider = browser.$("#wheelslider"); - + const button = slider.shadow$$(".ui5-wheelslider-arrow")[0]; button.click(); @@ -58,4 +58,4 @@ describe("Wheel Slider general interaction", () => { assert.strictEqual(slider.getValue(), "4", "Wheel Slider pick elements with click works"); }); -}); \ No newline at end of file +}); diff --git a/packages/tools/lib/documentation/index.js b/packages/tools/lib/documentation/index.js index 10bc83660c5e..948a06c5f857 100644 --- a/packages/tools/lib/documentation/index.js +++ b/packages/tools/lib/documentation/index.js @@ -61,28 +61,40 @@ const appendCSSVarsAPI = entry => { return entry; } -const calculateAPI = entry => { - if (entriesAPI.indexOf(entry.basename) !== -1) { - return entry; - } - - let parent = getComponentByName(entry.extends) || {}; - - entry = appendCSSVarsAPI(entry); - parent = appendCSSVarsAPI(parent); - - parent = { ...{ properties: [], events: [], slots: [], cssVariables: [] }, ...parent }; +const componentHasEntityItem = (component, entity, name) => { + return component[entity].some(x => x && x.name === name); +}; +const removeEmpty = arr => arr.filter(x => x); - // extend component documentation - entry.properties = [...(entry.properties || []), ...(parent.properties || [])]; - entry.events = [...(entry.events || []), ...(parent.events || [])]; - entry.slots = [...(entry.slots || []), ...(parent.slots || [])]; - entry.cssVariables = [...(entry.cssVariables || []), ...(parent.cssVariables || [])]; +const calculateAPI = component => { + if (entriesAPI.indexOf(component.basename) !== -1) { + return component; + } + const entities = ["properties", "slots", "events", "methods", "cssVariables"]; + + // Initialize all entities with [] if necessary, and remove undefined things, and only leave public things + entities.forEach(entity => { + component[entity] = removeEmpty(component[entity] || []).filter(x => x.visibility === "public"); + }); + + component = appendCSSVarsAPI(component); + + let parent = getComponentByName(component.extends); + if (parent) { + let parentComponent = calculateAPI(parent); + entities.forEach(entity => { + parentComponent[entity].forEach( x => { + if (!componentHasEntityItem(component, entity, x.name)) { + component[entity].push(x); + } + }); + }); + } - entriesAPI.push(entry.basename); + entriesAPI.push(component.basename); - return entry; -} + return component; +}; const appendAdditionalEntriesAPI = entry => { if (entry.appenddocs) {