Skip to content

Commit 84eb484

Browse files
authored
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
1 parent f9b9ead commit 84eb484

9 files changed

+368
-56
lines changed

packages/main/src/Calendar.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,7 @@ class Calendar extends UI5Element {
353353
_getTimeStampFromString(value) {
354354
const jsDate = this.getFormat().parse(value);
355355
if (jsDate) {
356-
const jsDateTimeNow = Date.UTC(jsDate.getFullYear(), jsDate.getMonth(), jsDate.getDate());
357-
const calDate = CalendarDate.fromTimestamp(jsDateTimeNow, this._primaryCalendarType);
358-
return calDate.valueOf();
356+
return CalendarDate.fromLocalJSDate(jsDate, this._primaryCalendarType).toUTCJSDate().valueOf();
359357
}
360358
return undefined;
361359
}

packages/main/src/DatePicker.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
class="ui5-date-picker-root"
33
style="{{styles.main}}"
44
@keydown={{_onkeydown}}
5+
@focusout="{{_onfocusout}}"
56
>
67
<!-- INPUT -->
78
<ui5-input

packages/main/src/DatePicker.js

+55-30
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDat
1111
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
1212
import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AriaLabelHelper.js";
1313
import {
14+
isEnter,
1415
isPageUp,
1516
isPageDown,
1617
isPageUpShift,
@@ -503,9 +504,7 @@ class DatePicker extends UI5Element {
503504
_getTimeStampFromString(value) {
504505
const jsDate = this.getFormat().parse(value);
505506
if (jsDate) {
506-
const jsDateTimeNow = Date.UTC(jsDate.getFullYear(), jsDate.getMonth(), jsDate.getDate());
507-
const calDate = CalendarDate.fromTimestamp(jsDateTimeNow, this._primaryCalendarType);
508-
return calDate.valueOf();
507+
return CalendarDate.fromLocalJSDate(jsDate, this._primaryCalendarType).toUTCJSDate().valueOf();
509508
}
510509
return undefined;
511510
}
@@ -530,82 +529,108 @@ class DatePicker extends UI5Element {
530529
return;
531530
}
532531

532+
if (isEnter(event)) {
533+
this._handleEnterPressed();
534+
}
535+
533536
if (isPageUpShiftCtrl(event)) {
534537
event.preventDefault();
535-
this._changeDateValue(true, true, false, false);
538+
this._changeDateValueWrapper(true, true, false, false);
536539
} else if (isPageUpShift(event)) {
537540
event.preventDefault();
538-
this._changeDateValue(true, false, true, false);
541+
this._changeDateValueWrapper(true, false, true, false);
539542
} else if (isPageUp(event)) {
540543
event.preventDefault();
541-
this._changeDateValue(true, false, false, true);
544+
this._changeDateValueWrapper(true, false, false, true);
542545
}
543546

544547
if (isPageDownShiftCtrl(event)) {
545548
event.preventDefault();
546-
this._changeDateValue(false, true, false, false);
549+
this._changeDateValueWrapper(false, true, false, false);
547550
} else if (isPageDownShift(event)) {
548551
event.preventDefault();
549-
this._changeDateValue(false, false, true, false);
552+
this._changeDateValueWrapper(false, false, true, false);
550553
} else if (isPageDown(event)) {
551554
event.preventDefault();
552-
this._changeDateValue(false, false, false, true);
555+
this._changeDateValueWrapper(false, false, false, true);
553556
}
554557
}
555558

559+
/**
560+
* This method is used in the derived classes
561+
*/
562+
_handleEnterPressed() {}
563+
564+
/**
565+
* This method is used in the derived classes
566+
*/
567+
_onfocusout() {}
568+
556569
/**
557570
* Adds or extracts a given number of measuring units from the "dateValue" property value
558-
*
571+
* @param {boolean} forward if true indicates addition
559572
* @param {boolean} years indicates that the measuring unit is in years
560573
* @param {boolean} months indicates that the measuring unit is in months
561574
* @param {boolean} days indicates that the measuring unit is in days
562-
* @param {boolean} forward if true indicates addition
563575
* @param {int} step number of measuring units to substract or add defaults to 1
564576
*/
565-
_changeDateValue(forward, years, months, days, step = 1) {
577+
_changeDateValueWrapper(forward, years, months, days, step = 1) {
566578
let date = this.dateValue;
579+
date = this._changeDateValue(date, forward, years, months, days, step);
580+
this.value = this.formatValue(date);
581+
}
567582

583+
/**
584+
* Adds or extracts a given number of measuring units from the "dateValue" property value
585+
*
586+
* @param {boolean} date js date object to be changed
587+
* @param {boolean} years indicates that the measuring unit is in years
588+
* @param {boolean} months indicates that the measuring unit is in months
589+
* @param {boolean} days indicates that the measuring unit is in days
590+
* @param {boolean} forward if true indicates addition
591+
* @param {int} step number of measuring units to substract or add defaults ot 1
592+
* @returns {Object} JS date object
593+
*/
594+
_changeDateValue(date, forward, years, months, days, step = 1) {
568595
if (!date) {
569596
return;
570597
}
571598

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

575-
if (incrementStep === 0) {
603+
if (incrementStep === 0 || (!days && !months && !years)) {
576604
return;
577605
}
578606

579607
if (days) {
580-
date.setDate(date.getDate() + incrementStep);
608+
calDate.setDate(calDate.getDate() + incrementStep);
581609
} else if (months) {
582-
date.setMonth(date.getMonth() + incrementStep);
583-
const monthDiff = (date.getFullYear() - oldDate.getFullYear()) * 12 + (date.getMonth() - oldDate.getMonth());
610+
calDate.setMonth(calDate.getMonth() + incrementStep);
611+
const monthDiff = (calDate.getYear() - oldCalDate.getYear()) * 12 + (calDate.getMonth() - oldCalDate.getMonth());
584612

585-
if (date.getMonth() === oldDate.getMonth() || monthDiff !== incrementStep) {
613+
if (calDate.getMonth() === oldCalDate.getMonth() || monthDiff !== incrementStep) {
586614
// first condition example: 31th of March increment month with -1 results in 2th of March
587615
// second condition example: 31th of January increment month with +1 results in 2th of March
588-
date.setDate(0);
616+
calDate.setDate(0);
589617
}
590618
} else if (years) {
591-
date.setFullYear(date.getFullYear() + incrementStep);
619+
calDate.setYear(calDate.getYear() + incrementStep);
592620

593-
if (date.getMonth() !== oldDate.getMonth()) {
621+
if (calDate.getMonth() !== oldCalDate.getMonth()) {
594622
// day doesn't exist in this month (February 29th)
595-
date.setDate(0);
623+
calDate.setDate(0);
596624
}
597-
} else {
598-
return;
599625
}
600626

601-
if (date.valueOf() < this._minDate) {
602-
date = new Date(this._minDate);
603-
} else if (date.valueOf() > this._maxDate) {
604-
date = new Date(this._maxDate);
627+
if (calDate.valueOf() < this._minDate) {
628+
calDate = CalendarDate.fromTimestamp(this._minDate, this._primaryCalendarType);
629+
} else if (calDate.valueOf() > this._maxDate) {
630+
calDate = CalendarDate.fromTimestamp(this._maxDate, this._primaryCalendarType);
605631
}
606632

607-
this.value = this.formatValue(date);
608-
this.fireEvent("change", { value: this.value, valid: true });
633+
return calDate.toLocalJSDate();
609634
}
610635

611636
_toggleAndFocusInput() {

packages/main/src/DateRangePicker.js

+155-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Integer from "@ui5/webcomponents-base/dist/types/Integer.js";
33
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
44
import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js";
55
import DateRangePickerTemplate from "./generated/templates/DateRangePickerTemplate.lit.js";
6+
import RenderScheduler from "../../base/src/RenderScheduler.js";
67

78
// Styles
89
import DateRangePickerCss from "./generated/themes/DateRangePicker.css.js";
@@ -64,6 +65,25 @@ const metadata = {
6465
*
6566
* <code>import @ui5/webcomponents/dist/DateRangePicker.js";</code>
6667
*
68+
* <h3>Keyboard Handling</h3>
69+
* The <code>ui5-daterange-picker</code> provides advanced keyboard handling.
70+
* <br>
71+
*
72+
* When the <code>ui5-daterange-picker</code> input field is focused the user can
73+
* increment or decrement the corresponding field of the JS date object referenced by <code>_firstDateTimestamp</code> propery
74+
* if the caret symbol is before the delimiter character or <code>_lastDateTimestamp</code> property if the caret symbol is
75+
* after the delimiter character.
76+
* The following shortcuts are enabled:
77+
* <br>
78+
* <ul>
79+
* <li>[PAGEDOWN] - Decrements the corresponding day of the month by one</li>
80+
* <li>[SHIFT] + [PAGEDOWN] - Decrements the corresponding month by one</li>
81+
* <li>[SHIFT] + [CTRL] + [PAGEDOWN] - Decrements the corresponding year by one</li>
82+
* <li>[PAGEUP] - Increments the corresponding day of the month by one</li>
83+
* <li>[SHIFT] + [PAGEUP] - Increments the corresponding month by one</li>
84+
* <li>[SHIFT] + [CTRL] + [PAGEUP] - Increments the corresponding year by one</li>
85+
* </ul>
86+
*
6787
* @constructor
6888
* @author SAP SE
6989
* @alias sap.ui.webcomponents.main.DateRangePicker
@@ -204,7 +224,8 @@ class DateRangePicker extends DatePicker {
204224
}
205225

206226
this._calendar.selectedDates = this.dateIntervalArrayBuilder(this._firstDateTimestamp * 1000, this._lastDateTimestamp * 1000);
207-
this.value = this._formatValue(this._firstDateTimestamp, this._lastDateTimestamp);
227+
228+
this.value = this._formatValue(firstDate.valueOf() / 1000, secondDate.valueOf() / 1000);
208229
this.realValue = this.value;
209230
this._prevValue = this.realValue;
210231
}
@@ -378,6 +399,125 @@ class DateRangePicker extends DatePicker {
378399
}
379400
}
380401

402+
/**
403+
* Adds or extracts a given number of measuring units from the "dateValue" property value
404+
*
405+
* @param {boolean} forward if true indicates addition
406+
* @param {boolean} years indicates that the measuring unit is in years
407+
* @param {boolean} months indicates that the measuring unit is in months
408+
* @param {boolean} days indicates that the measuring unit is in days
409+
* @param {int} step number of measuring units to substract or add defaults ot 1
410+
*/
411+
async _changeDateValueWrapper(forward, years, months, days, step = 1) {
412+
const emptyValue = this.value === "";
413+
const isValid = emptyValue || this._checkValueValidity(this.value);
414+
415+
if (!isValid) {
416+
return;
417+
}
418+
419+
const dates = this._splitValueByDelimiter(this.value);
420+
const innerInput = this.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner");
421+
const caretPos = this._getCaretPosition(innerInput);
422+
const first = dates[0] && caretPos <= dates[0].trim().length + 1;
423+
const last = dates[1] && (caretPos >= this.value.length - dates[1].trim().length - 1 && caretPos <= this.value.length);
424+
let firstDate = this.getFormat().parse(dates[0]);
425+
let lastDate = this.getFormat().parse(dates[1]);
426+
427+
if (first && firstDate) {
428+
firstDate = this._changeDateValue(firstDate, forward, years, months, days, step);
429+
} else if (last && lastDate) {
430+
lastDate = this._changeDateValue(lastDate, forward, years, months, days, step);
431+
}
432+
433+
this.value = this._formatValue(firstDate.valueOf() / 1000, lastDate.valueOf() / 1000);
434+
435+
await RenderScheduler.whenFinished();
436+
// Return the caret on the previous position after rendering
437+
this._setCaretPosition(innerInput, caretPos);
438+
}
439+
440+
/**
441+
* This method is used in the derived classes
442+
*/
443+
async _handleEnterPressed() {
444+
const innerInput = this.shadowRoot.querySelector("ui5-input").shadowRoot.querySelector(".ui5-input-inner");
445+
const caretPos = this._getCaretPosition(innerInput);
446+
447+
this._confirmInput();
448+
449+
await RenderScheduler.whenFinished();
450+
// Return the caret on the previous position after rendering
451+
this._setCaretPosition(innerInput, caretPos);
452+
}
453+
454+
_onfocusout() {
455+
this._confirmInput();
456+
}
457+
458+
_confirmInput() {
459+
const emptyValue = this.value === "";
460+
461+
if (emptyValue) {
462+
return;
463+
}
464+
465+
const dates = this._splitValueByDelimiter(this.value);
466+
let firstDate = this.getFormat().parse(dates[0]);
467+
let lastDate = this.getFormat().parse(dates[1]);
468+
469+
if (firstDate > lastDate) {
470+
const temp = firstDate;
471+
firstDate = lastDate;
472+
lastDate = temp;
473+
}
474+
475+
const newValue = this._formatValue(firstDate.valueOf() / 1000, lastDate.valueOf() / 1000);
476+
477+
this._setValue(newValue);
478+
}
479+
480+
/**
481+
* Returns the caret (cursor) position of the specified text field (field).
482+
* Return value range is 0-field.value.length.
483+
*/
484+
_getCaretPosition(field) {
485+
// Initialize
486+
let caretPos = 0;
487+
488+
// IE Support
489+
if (document.selection) {
490+
// Set focus on the element
491+
field.focus();
492+
493+
// To get cursor position, get empty selection range
494+
const selection = document.selection.createRange();
495+
496+
// Move selection start to 0 position
497+
selection.moveStart("character", -field.value.length);
498+
499+
// The caret position is selection length
500+
caretPos = selection.text.length;
501+
} else if (field.selectionStart || field.selectionStart === "0") { // Firefox support
502+
caretPos = field.selectionDirection === "backward" ? field.selectionStart : field.selectionEnd;
503+
}
504+
505+
return caretPos;
506+
}
507+
508+
_setCaretPosition(field, caretPos) {
509+
if (field.createTextRange) {
510+
const range = field.createTextRange();
511+
range.move("character", caretPos);
512+
range.select();
513+
} else if (field.selectionStart) {
514+
field.focus();
515+
field.setSelectionRange(caretPos, caretPos);
516+
} else {
517+
field.focus();
518+
}
519+
}
520+
381521
_handleCalendarSelectedDatesChange() {
382522
this._updateValueCalendarSelectedDatesChange();
383523
this._cleanHoveredAttributeFromVisibleItems();
@@ -409,23 +549,31 @@ class DateRangePicker extends DatePicker {
409549
}
410550

411551
_updateValueCalendarSelectedDatesChange() {
552+
const calStartDate = CalendarDate.fromTimestamp(this._firstDateTimestamp * 1000, this._primaryCalendarType);
553+
const calEndDate = CalendarDate.fromTimestamp(this._lastDateTimestamp * 1000, this._primaryCalendarType);
554+
412555
// Collect both dates and merge them into one
413556
if (this._firstDateTimestamp !== this._lastDateTimestamp || this._oneTimeStampSelected) {
414-
this.value = this._formatValue(this._firstDateTimestamp, this._lastDateTimestamp);
557+
this.value = this._formatValue(calStartDate.toLocalJSDate().valueOf() / 1000, calEndDate.toLocalJSDate().valueOf() / 1000);
415558
}
416559

417-
this.realValue = this._formatValue(this._firstDateTimestamp, this._lastDateTimestamp);
560+
this.realValue = this._formatValue(calStartDate.toLocalJSDate().valueOf() / 1000, calEndDate.toLocalJSDate().valueOf() / 1000);
418561
this._prevValue = this.realValue;
419562
}
420563

564+
/**
565+
* Combines the start and end dates of a range into a formated string
566+
*
567+
* @param {int} firstDateValue locale start date timestamp
568+
* @param {int} lastDateValue locale end date timestamp
569+
* @returns {string} formated start to end date range
570+
*/
421571
_formatValue(firstDateValue, lastDateValue) {
422572
let value = "";
423573
const delimiter = this.delimiter,
424574
format = this.getFormat(),
425-
firstDate = new Date(firstDateValue * 1000),
426-
lastDate = new Date(lastDateValue * 1000),
427-
firstDateString = format.format(new Date(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate(), firstDate.getUTCHours())),
428-
lastDateString = format.format(new Date(lastDate.getUTCFullYear(), lastDate.getUTCMonth(), lastDate.getUTCDate(), lastDate.getUTCHours()));
575+
firstDateString = format.format(new Date(firstDateValue * 1000)),
576+
lastDateString = format.format(new Date(lastDateValue * 1000));
429577

430578
if (firstDateValue) {
431579
if (delimiter && delimiter !== "" && lastDateString) {

0 commit comments

Comments
 (0)