From 84eb484a14a7fe5927d0bf7e50e34217989efb63 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Fri, 9 Oct 2020 09:58:56 +0300 Subject: [PATCH] feat(ui5-daterange-picker): enhance keyboard handling (#2179) Depending on the caret symbol position, the corresponding date gets incremented or decremented with one unit of measure by using the following keyboard combinations: [PAGEDOWN] - Decrements the corresponding day of the month by one [SHIFT] + [PAGEDOWN] - Decrements the corresponding month by one [SHIFT] + [CTRL] + [PAGEDOWN] - Decrements the corresponding year by one [PAGEUP] - Increments the corresponding day of the month by one [SHIFT] + [PAGEUP] - Increments the corresponding month by one [SHIFT] + [CTRL] + [PAGEUP] - Increments the corresponding year by one Fixes #1534 --- packages/main/src/Calendar.js | 4 +- packages/main/src/DatePicker.hbs | 1 + packages/main/src/DatePicker.js | 85 +++++---- packages/main/src/DateRangePicker.js | 162 +++++++++++++++++- packages/main/src/DayPicker.js | 21 ++- packages/main/src/MonthPicker.js | 4 +- packages/main/src/YearPicker.js | 4 +- packages/main/test/pages/DateRangePicker.html | 2 +- .../main/test/specs/DateRangePicker.spec.js | 141 +++++++++++++++ 9 files changed, 368 insertions(+), 56 deletions(-) diff --git a/packages/main/src/Calendar.js b/packages/main/src/Calendar.js index e8930586bf29..e64ee655857c 100644 --- a/packages/main/src/Calendar.js +++ b/packages/main/src/Calendar.js @@ -353,9 +353,7 @@ class Calendar extends UI5Element { _getTimeStampFromString(value) { const jsDate = this.getFormat().parse(value); if (jsDate) { - const jsDateTimeNow = Date.UTC(jsDate.getFullYear(), jsDate.getMonth(), jsDate.getDate()); - const calDate = CalendarDate.fromTimestamp(jsDateTimeNow, this._primaryCalendarType); - return calDate.valueOf(); + return CalendarDate.fromLocalJSDate(jsDate, this._primaryCalendarType).toUTCJSDate().valueOf(); } return undefined; } diff --git a/packages/main/src/DatePicker.hbs b/packages/main/src/DatePicker.hbs index f489a3f56dfa..4c30167a1175 100644 --- a/packages/main/src/DatePicker.hbs +++ b/packages/main/src/DatePicker.hbs @@ -2,6 +2,7 @@ class="ui5-date-picker-root" style="{{styles.main}}" @keydown={{_onkeydown}} + @focusout="{{_onfocusout}}" > this._maxDate) { - date = new Date(this._maxDate); + 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); } - this.value = this.formatValue(date); - this.fireEvent("change", { value: this.value, valid: true }); + return calDate.toLocalJSDate(); } _toggleAndFocusInput() { diff --git a/packages/main/src/DateRangePicker.js b/packages/main/src/DateRangePicker.js index 20bbc70490c3..1e8d8810706d 100644 --- a/packages/main/src/DateRangePicker.js +++ b/packages/main/src/DateRangePicker.js @@ -3,6 +3,7 @@ import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import DateRangePickerTemplate from "./generated/templates/DateRangePickerTemplate.lit.js"; +import RenderScheduler from "../../base/src/RenderScheduler.js"; // Styles import DateRangePickerCss from "./generated/themes/DateRangePicker.css.js"; @@ -64,6 +65,25 @@ const metadata = { * * import @ui5/webcomponents/dist/DateRangePicker.js"; * + *

Keyboard Handling

+ * The ui5-daterange-picker provides advanced keyboard handling. + *
+ * + * 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: + *
+ * + * * @constructor * @author SAP SE * @alias sap.ui.webcomponents.main.DateRangePicker @@ -204,7 +224,8 @@ class DateRangePicker extends DatePicker { } this._calendar.selectedDates = this.dateIntervalArrayBuilder(this._firstDateTimestamp * 1000, this._lastDateTimestamp * 1000); - this.value = this._formatValue(this._firstDateTimestamp, this._lastDateTimestamp); + + this.value = this._formatValue(firstDate.valueOf() / 1000, secondDate.valueOf() / 1000); this.realValue = this.value; this._prevValue = this.realValue; } @@ -378,6 +399,125 @@ class DateRangePicker extends DatePicker { } } + /** + * 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 + */ + async _changeDateValueWrapper(forward, years, months, days, step = 1) { + const emptyValue = this.value === ""; + const isValid = emptyValue || this._checkValueValidity(this.value); + + if (!isValid) { + 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.valueOf() / 1000, lastDate.valueOf() / 1000); + + await RenderScheduler.whenFinished(); + // Return the caret on the previous position after rendering + this._setCaretPosition(innerInput, caretPos); + } + + /** + * This method is used in the derived classes + */ + async _handleEnterPressed() { + const innerInput = this.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + const caretPos = this._getCaretPosition(innerInput); + + this._confirmInput(); + + await RenderScheduler.whenFinished(); + // Return the caret on the previous position after rendering + this._setCaretPosition(innerInput, caretPos); + } + + _onfocusout() { + this._confirmInput(); + } + + _confirmInput() { + const emptyValue = this.value === ""; + + if (emptyValue) { + return; + } + + const dates = this._splitValueByDelimiter(this.value); + let firstDate = this.getFormat().parse(dates[0]); + let lastDate = this.getFormat().parse(dates[1]); + + if (firstDate > lastDate) { + const temp = firstDate; + firstDate = lastDate; + lastDate = temp; + } + + const newValue = this._formatValue(firstDate.valueOf() / 1000, lastDate.valueOf() / 1000); + + this._setValue(newValue); + } + + /** + * 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; + } + + return caretPos; + } + + _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(); + } + } + _handleCalendarSelectedDatesChange() { this._updateValueCalendarSelectedDatesChange(); this._cleanHoveredAttributeFromVisibleItems(); @@ -409,23 +549,31 @@ class DateRangePicker extends DatePicker { } _updateValueCalendarSelectedDatesChange() { + const calStartDate = CalendarDate.fromTimestamp(this._firstDateTimestamp * 1000, this._primaryCalendarType); + const calEndDate = CalendarDate.fromTimestamp(this._lastDateTimestamp * 1000, this._primaryCalendarType); + // Collect both dates and merge them into one if (this._firstDateTimestamp !== this._lastDateTimestamp || this._oneTimeStampSelected) { - this.value = this._formatValue(this._firstDateTimestamp, this._lastDateTimestamp); + this.value = this._formatValue(calStartDate.toLocalJSDate().valueOf() / 1000, calEndDate.toLocalJSDate().valueOf() / 1000); } - this.realValue = this._formatValue(this._firstDateTimestamp, this._lastDateTimestamp); + this.realValue = this._formatValue(calStartDate.toLocalJSDate().valueOf() / 1000, calEndDate.toLocalJSDate().valueOf() / 1000); this._prevValue = this.realValue; } + /** + * Combines the start and end dates of a range into a formated string + * + * @param {int} firstDateValue locale start date timestamp + * @param {int} lastDateValue locale end date timestamp + * @returns {string} formated start to end date range + */ _formatValue(firstDateValue, lastDateValue) { let value = ""; const delimiter = this.delimiter, format = this.getFormat(), - firstDate = new Date(firstDateValue * 1000), - lastDate = new Date(lastDateValue * 1000), - firstDateString = format.format(new Date(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate(), firstDate.getUTCHours())), - lastDateString = format.format(new Date(lastDate.getUTCFullYear(), lastDate.getUTCMonth(), lastDate.getUTCDate(), lastDate.getUTCHours())); + firstDateString = format.format(new Date(firstDateValue * 1000)), + lastDateString = format.format(new Date(lastDateValue * 1000)); if (firstDateValue) { if (delimiter && delimiter !== "" && lastDateString) { diff --git a/packages/main/src/DayPicker.js b/packages/main/src/DayPicker.js index 45b2458dbc02..6b8e9bb993ab 100644 --- a/packages/main/src/DayPicker.js +++ b/packages/main/src/DayPicker.js @@ -391,9 +391,7 @@ class DayPicker extends UI5Element { } onAfterRendering() { - if (this.selectedDates.length === 1) { - this.fireEvent("daypickerrendered", { focusedItemIndex: this._itemNav.currentIndex }); - } + this._fireDayPickerRendered(); } _onmousedown(event) { @@ -788,8 +786,14 @@ class DayPicker extends UI5Element { const newItemIndex = this._itemNav._getItems().findIndex(item => parseInt(item.timestamp) === timestamp); this._itemNav.currentIndex = newItemIndex; - this._itemNav.focusCurrent(); + this._fireDayPickerRendered(); + } + + _fireDayPickerRendered() { + if (this.selectedDates.length === 1) { + this.fireEvent("daypickerrendered", { focusedItemIndex: this._itemNav.currentIndex }); + } } _isWeekend(oDate) { @@ -829,9 +833,8 @@ class DayPicker extends UI5Element { _getTimeStampFromString(value) { const jsDate = this.getFormat().parse(value); if (jsDate) { - const jsDateTimeNow = Date.UTC(jsDate.getFullYear(), jsDate.getMonth(), jsDate.getDate()); - const calDate = CalendarDate.fromTimestamp(jsDateTimeNow, this._primaryCalendarType); - return calDate.valueOf(); + const calDate = CalendarDate.fromLocalJSDate(jsDate, this._primaryCalendarType); + return calDate.toUTCJSDate().valueOf(); } return undefined; } @@ -841,7 +844,7 @@ class DayPicker extends UI5Element { minDate.setYear(1); minDate.setMonth(0); minDate.setDate(1); - return minDate.valueOf(); + return minDate.toUTCJSDate().valueOf(); } _getMaxCalendarDate() { @@ -852,7 +855,7 @@ class DayPicker extends UI5Element { tempDate.setDate(1); tempDate.setMonth(tempDate.getMonth() + 1, 0); maxDate.setDate(tempDate.getDate());// 31st for Gregorian Calendar - return maxDate.valueOf(); + return maxDate.toUTCJSDate().valueOf(); } getFormat() { diff --git a/packages/main/src/MonthPicker.js b/packages/main/src/MonthPicker.js index 10fa2f968abb..4d51d8ec6d9b 100644 --- a/packages/main/src/MonthPicker.js +++ b/packages/main/src/MonthPicker.js @@ -289,9 +289,7 @@ class MonthPicker extends UI5Element { _getTimeStampFromString(value) { const jsDate = this.getFormat().parse(value); if (jsDate) { - const jsDateTimeNow = Date.UTC(jsDate.getFullYear(), jsDate.getMonth(), jsDate.getDate()); - const calDate = CalendarDate.fromTimestamp(jsDateTimeNow, this._primaryCalendarType); - return calDate.valueOf(); + return CalendarDate.fromLocalJSDate(jsDate, this._primaryCalendarType).toUTCJSDate().valueOf(); } return undefined; } diff --git a/packages/main/src/YearPicker.js b/packages/main/src/YearPicker.js index 69195d0fb414..55b1f6f1974e 100644 --- a/packages/main/src/YearPicker.js +++ b/packages/main/src/YearPicker.js @@ -362,9 +362,7 @@ class YearPicker extends UI5Element { _getTimeStampFromString(value) { const jsDate = this.getFormat().parse(value); if (jsDate) { - const jsDateTimeNow = Date.UTC(jsDate.getFullYear(), jsDate.getMonth(), jsDate.getDate()); - const calDate = CalendarDate.fromTimestamp(jsDateTimeNow, this._primaryCalendarType); - return calDate.valueOf(); + return CalendarDate.fromLocalJSDate(jsDate, this._primaryCalendarType).toUTCJSDate().valueOf(); } return undefined; } diff --git a/packages/main/test/pages/DateRangePicker.html b/packages/main/test/pages/DateRangePicker.html index e901e3a6ef3a..9495f010a2fd 100644 --- a/packages/main/test/pages/DateRangePicker.html +++ b/packages/main/test/pages/DateRangePicker.html @@ -44,7 +44,7 @@

daterange-picker with minDate 01/09/2019 and maxDate 01/11/2019

daterange-picker in Compact

- +
diff --git a/packages/main/test/specs/DateRangePicker.spec.js b/packages/main/test/specs/DateRangePicker.spec.js index c9693fac3821..dc3b883f4170 100644 --- a/packages/main/test/specs/DateRangePicker.spec.js +++ b/packages/main/test/specs/DateRangePicker.spec.js @@ -87,4 +87,145 @@ describe("DateRangePicker general interaction", () => { assert.strictEqual(browser.$("#labelChange").getHTML(false), "1", "The change event was fired once"); }); + + it("Page up/down increments/decrements day value", () => { + const dateRange = browser.$("#daterange-picker5"); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + dateRange._setValue("Jul 16, 2020 @ Jul 29, 2020"); + innerInput.click(); + dateRange._setCaretPosition(innerInput, 15); + }); + + browser.keys('PageDown'); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2020 @ Jul 28, 2020"); + + browser.keys('PageUp'); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2020 @ Jul 29, 2020"); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + dateRange._setCaretPosition(innerInput, 5); + }); + + browser.keys('PageDown'); + assert.strictEqual(dateRange.getProperty("value"), "Jul 15, 2020 @ Jul 29, 2020"); + + browser.keys('PageUp'); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2020 @ Jul 29, 2020"); + }); + + it("Page up/down increments/decrements month value", () => { + const dateRange = browser.$("#daterange-picker5"); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + dateRange._setValue("Jul 16, 2020 @ Jul 29, 2020"); + innerInput.click(); + dateRange._setCaretPosition(innerInput, 15); + }); + + browser.keys(['Shift', 'PageUp']); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2020 @ Aug 29, 2020"); + + browser.keys(['Shift', 'PageDown']); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2020 @ Jul 29, 2020"); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + dateRange._setCaretPosition(innerInput, 5); + }); + + browser.keys(['Shift', 'PageDown']); + assert.strictEqual(dateRange.getProperty("value"), "Jun 16, 2020 @ Jul 29, 2020"); + + browser.keys(['Shift', 'PageUp']); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2020 @ Jul 29, 2020"); + }); + + it("Page up/down increments/decrements year value", () => { + const dateRange = browser.$("#daterange-picker5"); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + dateRange._setValue("Jul 16, 2020 @ Jul 29, 2020"); + innerInput.click(); + dateRange._setCaretPosition(innerInput, 15); + }); + + browser.keys(['Control', 'Shift', 'PageUp']); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2020 @ Jul 29, 2021"); + + browser.keys(['Control', 'Shift', 'PageDown']); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2020 @ Jul 29, 2020"); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + dateRange._setCaretPosition(innerInput, 5); + }); + + browser.keys(['Control', 'Shift', 'PageDown']); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2019 @ Jul 29, 2020"); + + browser.keys(['Control', 'Shift', 'PageUp']); + assert.strictEqual(dateRange.getProperty("value"), "Jul 16, 2020 @ Jul 29, 2020"); + }); + + it("Enter keyboard key confirms the date range in the input field", () => { + const dateRange = browser.$("#daterange-picker5"); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + dateRange._setValue("Jul 16, 2020 @ Jul 16, 2020"); + }); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + innerInput.click(); + dateRange._setCaretPosition(innerInput, 0); + }); + + browser.keys('PageUp'); + assert.strictEqual(dateRange.getAttribute("value"), "Jul 17, 2020 @ Jul 16, 2020"); + + browser.keys('Enter'); + assert.strictEqual(dateRange.getAttribute("value"), "Jul 16, 2020 @ Jul 17, 2020"); + }); + + it("Focus out of the input field confirms the date range", () => { + const dateRange = browser.$("#daterange-picker5"); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + dateRange._setValue("Jul 16, 2020 @ Jul 16, 2020"); + }); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker5"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + innerInput.click(); + dateRange._setCaretPosition(innerInput, 0); + }); + + browser.keys('PageUp'); + assert.strictEqual(dateRange.getAttribute("value"), "Jul 17, 2020 @ Jul 16, 2020"); + + browser.execute(() => { + const dateRange = document.getElementById("daterange-picker4"); + const innerInput = dateRange.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner"); + innerInput.click(); + dateRange._setCaretPosition(innerInput, 0); + }); + assert.strictEqual(dateRange.getAttribute("value"), "Jul 16, 2020 @ Jul 17, 2020"); + }); }); \ No newline at end of file