Skip to content

Commit

Permalink
feat(ui5-daterange-picker): enhance keyboard handling (#2179)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
unazko committed Oct 9, 2020
1 parent f9b9ead commit 84eb484
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 56 deletions.
4 changes: 1 addition & 3 deletions packages/main/src/Calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/DatePicker.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
class="ui5-date-picker-root"
style="{{styles.main}}"
@keydown={{_onkeydown}}
@focusout="{{_onfocusout}}"
>
<!-- INPUT -->
<ui5-input
Expand Down
85 changes: 55 additions & 30 deletions packages/main/src/DatePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDat
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,
Expand Down Expand Up @@ -503,9 +504,7 @@ class DatePicker 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;
}
Expand All @@ -530,82 +529,108 @@ class DatePicker extends UI5Element {
return;
}

if (isEnter(event)) {
this._handleEnterPressed();
}

if (isPageUpShiftCtrl(event)) {
event.preventDefault();
this._changeDateValue(true, true, false, false);
this._changeDateValueWrapper(true, true, false, false);
} else if (isPageUpShift(event)) {
event.preventDefault();
this._changeDateValue(true, false, true, false);
this._changeDateValueWrapper(true, false, true, false);
} else if (isPageUp(event)) {
event.preventDefault();
this._changeDateValue(true, false, false, true);
this._changeDateValueWrapper(true, false, false, true);
}

if (isPageDownShiftCtrl(event)) {
event.preventDefault();
this._changeDateValue(false, true, false, false);
this._changeDateValueWrapper(false, true, false, false);
} else if (isPageDownShift(event)) {
event.preventDefault();
this._changeDateValue(false, false, true, false);
this._changeDateValueWrapper(false, false, true, false);
} else if (isPageDown(event)) {
event.preventDefault();
this._changeDateValue(false, false, false, true);
this._changeDateValueWrapper(false, false, false, true);
}
}

/**
* 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 {boolean} forward if true indicates addition
* @param {int} step number of measuring units to substract or add defaults to 1
*/
_changeDateValue(forward, years, months, days, step = 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
*/
_changeDateValue(date, forward, years, months, days, step = 1) {
if (!date) {
return;
}

const oldDate = new Date(date.getTime());
let calDate = CalendarDate.fromLocalJSDate(date, this._primaryCalendarType);
const oldCalDate = new CalendarDate(calDate, this._primaryCalendarType);
const incrementStep = forward ? step : -step;

if (incrementStep === 0) {
if (incrementStep === 0 || (!days && !months && !years)) {
return;
}

if (days) {
date.setDate(date.getDate() + incrementStep);
calDate.setDate(calDate.getDate() + incrementStep);
} else if (months) {
date.setMonth(date.getMonth() + incrementStep);
const monthDiff = (date.getFullYear() - oldDate.getFullYear()) * 12 + (date.getMonth() - oldDate.getMonth());
calDate.setMonth(calDate.getMonth() + incrementStep);
const monthDiff = (calDate.getYear() - oldCalDate.getYear()) * 12 + (calDate.getMonth() - oldCalDate.getMonth());

if (date.getMonth() === oldDate.getMonth() || monthDiff !== incrementStep) {
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
date.setDate(0);
calDate.setDate(0);
}
} else if (years) {
date.setFullYear(date.getFullYear() + incrementStep);
calDate.setYear(calDate.getYear() + incrementStep);

if (date.getMonth() !== oldDate.getMonth()) {
if (calDate.getMonth() !== oldCalDate.getMonth()) {
// day doesn't exist in this month (February 29th)
date.setDate(0);
calDate.setDate(0);
}
} else {
return;
}

if (date.valueOf() < this._minDate) {
date = new Date(this._minDate);
} else if (date.valueOf() > 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() {
Expand Down
162 changes: 155 additions & 7 deletions packages/main/src/DateRangePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -64,6 +65,25 @@ const metadata = {
*
* <code>import @ui5/webcomponents/dist/DateRangePicker.js";</code>
*
* <h3>Keyboard Handling</h3>
* The <code>ui5-daterange-picker</code> provides advanced keyboard handling.
* <br>
*
* When the <code>ui5-daterange-picker</code> input field is focused the user can
* increment or decrement the corresponding field of the JS date object referenced by <code>_firstDateTimestamp</code> propery
* if the caret symbol is before the delimiter character or <code>_lastDateTimestamp</code> property if the caret symbol is
* after the delimiter character.
* The following shortcuts are enabled:
* <br>
* <ul>
* <li>[PAGEDOWN] - Decrements the corresponding day of the month by one</li>
* <li>[SHIFT] + [PAGEDOWN] - Decrements the corresponding month by one</li>
* <li>[SHIFT] + [CTRL] + [PAGEDOWN] - Decrements the corresponding year by one</li>
* <li>[PAGEUP] - Increments the corresponding day of the month by one</li>
* <li>[SHIFT] + [PAGEUP] - Increments the corresponding month by one</li>
* <li>[SHIFT] + [CTRL] + [PAGEUP] - Increments the corresponding year by one</li>
* </ul>
*
* @constructor
* @author SAP SE
* @alias sap.ui.webcomponents.main.DateRangePicker
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 84eb484

Please sign in to comment.