Skip to content

Commit 7ec3c43

Browse files
feat(ui5-duration-picker): implement keyboard handling support (#2095)
Fixed one keyboard handling bug and added new features: PageUp - Hours + 1 PageDown - Hours - 1 PageUp + Shift - Minutes + 1 PageDown + Shift - Minutes - 1 PageUp + Shift + Ctrl - Seconds + 1 PageDown + Shift + Ctrl - Seconds - 1 Related to: #1534
1 parent 7a1c3eb commit 7ec3c43

File tree

3 files changed

+177
-2
lines changed

3 files changed

+177
-2
lines changed

packages/main/src/DurationPicker.js

+127-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
22
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
33
import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
44
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
5-
import { isShow } from "@ui5/webcomponents-base/dist/Keys.js";
5+
import {
6+
isShow,
7+
isLeft,
8+
isRight,
9+
isPageUp,
10+
isPageDown,
11+
isPageUpShift,
12+
isPageDownShift,
13+
isPageUpShiftCtrl,
14+
isPageDownShiftCtrl,
15+
} from "@ui5/webcomponents-base/dist/Keys.js";
616
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
717
import Integer from "@ui5/webcomponents-base/dist/types/Integer.js";
818
import DurationPickerTemplate from "./generated/templates/DurationPickerTemplate.lit.js";
@@ -227,6 +237,24 @@ const metadata = {
227237
* When the user directly triggers the sliders display, the actual time is displayed.
228238
*
229239
* For the <code>ui5-duration-picker</code>
240+
*
241+
* <h3>Keyboard handling</h3>
242+
* [F4], [ALT]+[UP], [ALT]+[DOWN] Open/Close picker dialog and move focus to it.
243+
* When closed:
244+
* [PAGEUP] - Increments hours by 1. If 12 am is reached, increment hours to 1 pm and vice versa.
245+
* [PAGEDOWN] - Decrements the corresponding field by 1. If 1 pm is reached, decrement hours to 12 am and vice versa.
246+
* [SHIFT]+[PAGEUP] Increments minutes by 1.
247+
* [SHIFT]+ [PAGEDOWN] Decrements minutes by 1.
248+
* [SHIFT]+[CTRL]+[PAGEUP] Increments seconds by 1.
249+
* [SHIFT]+[CTRL]+ [PAGEDOWN] Decrements seconds by 1.
250+
* When opened:
251+
* [UP] If focus is on one of the selection lists: Select the value which is above the current value. If the first value is selected, select the last value in the list. Exception: AM/ PM List: stay on the first item.
252+
* [DOWN] If focus is on one of the selection lists: Select the value which is below the current value. If the last value is selected, select the first value in the list. Exception: AM/ PM List: stay on the last item.
253+
* [LEFT] If focus is on one of the selection lists: Move focus to the selection list which is left of the current selection list. If focus is at the first selection list, move focus to the last selection list.
254+
* [RIGHT] If focus is on one of the selection lists: Move focus to the selection list which is right of the current selection list. When focus is at the last selection list, move focus to the first selection list.
255+
* [PAGEUP] If focus is on one of the selection lists: Move focus to the first entry of this list.
256+
* [PAGEDOWN] If focus is on one of the selection lists: Move focus to the last entry of this list.
257+
*
230258
* <h3>ES6 Module Import</h3>
231259
*
232260
* <code>import @ui5/webcomponents/dist/DurationPicker.js";</code>
@@ -278,6 +306,8 @@ class DurationPicker extends UI5Element {
278306
this._isPickerOpen = false;
279307
},
280308
};
309+
310+
this._slidersDomRefs = [];
281311
}
282312

283313
onBeforeRendering() {
@@ -422,13 +452,103 @@ class DurationPicker extends UI5Element {
422452
return curr;
423453
}
424454

455+
async _handleContainerKeysDown(event) {
456+
if (isLeft(event)) {
457+
let expandedSliderIndex = 0;
458+
for (let i = 0; i < this._slidersDomRefs.length; i++) {
459+
if (this._slidersDomRefs[i]._expanded) {
460+
expandedSliderIndex = i;
461+
}
462+
}
463+
if (this._slidersDomRefs[expandedSliderIndex - 1]) {
464+
this._slidersDomRefs[expandedSliderIndex - 1].focus();
465+
} else {
466+
this._slidersDomRefs[this._slidersDomRefs.length - 1].focus();
467+
}
468+
} else if (isRight(event)) {
469+
let expandedSliderIndex = 0;
470+
471+
for (let i = 0; i < this._slidersDomRefs.length; i++) {
472+
if (this._slidersDomRefs[i]._expanded) {
473+
expandedSliderIndex = i;
474+
}
475+
}
476+
if (this._slidersDomRefs[expandedSliderIndex + 1]) {
477+
this._slidersDomRefs[expandedSliderIndex + 1].focus();
478+
} else {
479+
this._slidersDomRefs[0].focus();
480+
}
481+
}
482+
483+
if (isPageDown(event)) {
484+
this._selectLimitCell(event, false);
485+
} else if (isPageUp(event)) {
486+
this._selectLimitCell(event, true);
487+
}
488+
}
489+
490+
_selectLimitCell(event, isMax) {
491+
event.preventDefault();
492+
if (event.target === this.hoursSlider) {
493+
const hoursArray = this.hoursArray;
494+
event.target.value = isMax ? hoursArray[hoursArray.length - 1] : hoursArray[0];
495+
} else if (event.target === this.minutesSlider) {
496+
const minutesArray = this.minutesArray;
497+
event.target.value = isMax ? minutesArray[minutesArray.length - 1] : minutesArray[0];
498+
} else if (event.target === this.secondsSlider) {
499+
const secondsArray = this.secondsArray;
500+
event.target.value = isMax ? secondsArray[secondsArray.length - 1] : secondsArray[0];
501+
}
502+
}
503+
425504
_onkeydown(event) {
426505
if (isShow(event)) {
427506
event.preventDefault();
428507
this.togglePicker();
429508
}
509+
510+
if (isPageUpShiftCtrl(event)) {
511+
event.preventDefault();
512+
this._incrementValue(true, false, false, true);
513+
} else if (isPageUpShift(event)) {
514+
event.preventDefault();
515+
this._incrementValue(true, false, true, false);
516+
} else if (isPageUp(event)) {
517+
event.preventDefault();
518+
this._incrementValue(true, true, false, false);
519+
}
520+
521+
if (isPageDownShiftCtrl(event)) {
522+
event.preventDefault();
523+
this._incrementValue(false, false, false, true);
524+
} else if (isPageDownShift(event)) {
525+
event.preventDefault();
526+
this._incrementValue(false, false, true, false);
527+
} else if (isPageDown(event)) {
528+
event.preventDefault();
529+
this._incrementValue(false, true, false, false);
530+
}
430531
}
431532

533+
_incrementValue(increment, hours, minutes, seconds) {
534+
const values = this.readFormattedValue(this.value);
535+
const incrementStep = increment ? 1 : -1;
536+
537+
if (hours && !this.hideHours) {
538+
values[0] = Number(values[0]) + incrementStep;
539+
} else if (minutes && !this.hideMinutes) {
540+
values[1] = Number(values[1]) + incrementStep;
541+
} else if (seconds && !this.hideSeconds) {
542+
values[2] = Number(values[2]) + incrementStep;
543+
} else {
544+
return;
545+
}
546+
547+
this.value = `${!this.hideHours ? values[0] : ""}${!this.hideHours && !this.hideMinutes ? ":" : ""}${!this.hideMinutes ? values[1] : ""}${!this.hideSeconds ? `:${values[2]}` : ""}`;
548+
this.fireEvent("change", { value: this.value });
549+
}
550+
551+
432552
generateTimeItemsArray(arrayLength, step = 1) {
433553
const resultArray = [];
434554
for (let i = 0; i < arrayLength; i++) {
@@ -488,6 +608,7 @@ class DurationPicker extends UI5Element {
488608
} else {
489609
this._isPickerOpen = true;
490610
this.responsivePopover.open(this);
611+
this._slidersDomRefs = await this.slidersDomRefs();
491612
}
492613
}
493614

@@ -501,6 +622,11 @@ class DurationPicker extends UI5Element {
501622
return this.responsivePopover;
502623
}
503624

625+
async slidersDomRefs() {
626+
await this._getResponsivePopover();
627+
return this.responsivePopover.default.length ? [...this.responsivePopover.default[0].children].filter(x => x.isUI5Element) : this.responsivePopover.default;
628+
}
629+
504630

505631
get hours() {
506632
return this.selectedHours;

packages/main/src/DurationPickerPopover.hbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
@ui5-after-close="{{_respPopover._onAfterClose}}"
1010
@keydown="{{_handleKeysDown}}"
1111
>
12-
<div class="{{classes.container}}">
12+
<div class="{{classes.container}}" @keydown="{{_handleContainerKeysDown}}">
1313
{{#unless hideHours}}
1414
<ui5-wheelslider
1515
cyclic="true"

packages/main/test/specs/DurationPicker.spec.js

+49
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,55 @@ describe("Duration Picker general interaction", () => {
9696
duratationPickerIcon.click();
9797
});
9898

99+
it("Tests Keyboard handling", () => {
100+
const durationPicker = browser.$("#duration-default")
101+
102+
// act
103+
durationPicker.click();
104+
durationPicker.keys(['Shift', 'PageUp']);
105+
durationPicker.keys('Shift');
106+
107+
// assert
108+
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:01:00", "The value of minutes is +1");
109+
// act
110+
durationPicker.click();
111+
durationPicker.keys(['Shift', 'PageDown']);
112+
durationPicker.keys('Shift');
113+
114+
// assert
115+
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:00:00", "The value of minutes is -1");
116+
117+
// act
118+
durationPicker.click();
119+
durationPicker.keys('PageUp');
120+
121+
// assert
122+
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "01:00:00", "The value of hours is +1");
123+
// act
124+
durationPicker.click();
125+
durationPicker.keys('PageDown');
126+
127+
// assert
128+
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:00:00", "The value of hours is -1");
129+
130+
// act
131+
durationPicker.click();
132+
durationPicker.keys(['Shift', 'Control', 'PageUp']);
133+
durationPicker.keys('Control');
134+
durationPicker.keys('Shift');
135+
136+
// assert
137+
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:00:01", "The value of seconds is +1");
138+
// act
139+
durationPicker.click();
140+
durationPicker.keys(['Shift', 'Control', 'PageDown']);
141+
durationPicker.keys('Shift');
142+
durationPicker.keys('Control');
143+
144+
// assert
145+
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:00:00", "The value of seconds is +1");
146+
});
147+
99148
it("tests valueStateMessage slot", () => {
100149
const picker = browser.$("#pickerValueStateMessage");
101150

0 commit comments

Comments
 (0)