Skip to content

Commit

Permalink
feat(ui5-duration-picker): implement keyboard handling support (#2095)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tsanislavgatev committed Aug 21, 2020
1 parent 7a1c3eb commit 7ec3c43
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 2 deletions.
128 changes: 127 additions & 1 deletion packages/main/src/DurationPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ 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 } from "@ui5/webcomponents-base/dist/Keys.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";
Expand Down Expand Up @@ -227,6 +237,24 @@ const metadata = {
* When the user directly triggers the sliders display, the actual time is displayed.
*
* For the <code>ui5-duration-picker</code>
*
* <h3>Keyboard handling</h3>
* [F4], [ALT]+[UP], [ALT]+[DOWN] Open/Close picker dialog and move focus to it.
* When closed:
* [PAGEUP] - Increments hours by 1. If 12 am is reached, increment hours to 1 pm and vice versa.
* [PAGEDOWN] - Decrements the corresponding field by 1. If 1 pm is reached, decrement hours to 12 am and vice versa.
* [SHIFT]+[PAGEUP] Increments minutes by 1.
* [SHIFT]+ [PAGEDOWN] Decrements minutes by 1.
* [SHIFT]+[CTRL]+[PAGEUP] Increments seconds by 1.
* [SHIFT]+[CTRL]+ [PAGEDOWN] Decrements seconds by 1.
* When opened:
* [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.
* [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.
* [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.
* [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.
* [PAGEUP] If focus is on one of the selection lists: Move focus to the first entry of this list.
* [PAGEDOWN] If focus is on one of the selection lists: Move focus to the last entry of this list.
*
* <h3>ES6 Module Import</h3>
*
* <code>import @ui5/webcomponents/dist/DurationPicker.js";</code>
Expand Down Expand Up @@ -278,6 +306,8 @@ class DurationPicker extends UI5Element {
this._isPickerOpen = false;
},
};

this._slidersDomRefs = [];
}

onBeforeRendering() {
Expand Down Expand Up @@ -422,13 +452,103 @@ class DurationPicker extends UI5Element {
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();
}
}

if (isPageDown(event)) {
this._selectLimitCell(event, false);
} else if (isPageUp(event)) {
this._selectLimitCell(event, true);
}
}

_selectLimitCell(event, isMax) {
event.preventDefault();
if (event.target === this.hoursSlider) {
const hoursArray = this.hoursArray;
event.target.value = isMax ? hoursArray[hoursArray.length - 1] : hoursArray[0];
} else if (event.target === this.minutesSlider) {
const minutesArray = this.minutesArray;
event.target.value = isMax ? minutesArray[minutesArray.length - 1] : minutesArray[0];
} else if (event.target === this.secondsSlider) {
const secondsArray = this.secondsArray;
event.target.value = isMax ? secondsArray[secondsArray.length - 1] : secondsArray[0];
}
}

_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 });
}


generateTimeItemsArray(arrayLength, step = 1) {
const resultArray = [];
for (let i = 0; i < arrayLength; i++) {
Expand Down Expand Up @@ -488,6 +608,7 @@ class DurationPicker extends UI5Element {
} else {
this._isPickerOpen = true;
this.responsivePopover.open(this);
this._slidersDomRefs = await this.slidersDomRefs();
}
}

Expand All @@ -501,6 +622,11 @@ class DurationPicker extends UI5Element {
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;
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/DurationPickerPopover.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@ui5-after-close="{{_respPopover._onAfterClose}}"
@keydown="{{_handleKeysDown}}"
>
<div class="{{classes.container}}">
<div class="{{classes.container}}" @keydown="{{_handleContainerKeysDown}}">
{{#unless hideHours}}
<ui5-wheelslider
cyclic="true"
Expand Down
49 changes: 49 additions & 0 deletions packages/main/test/specs/DurationPicker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,55 @@ describe("Duration Picker general interaction", () => {
duratationPickerIcon.click();
});

it("Tests Keyboard handling", () => {
const durationPicker = browser.$("#duration-default")

// act
durationPicker.click();
durationPicker.keys(['Shift', 'PageUp']);
durationPicker.keys('Shift');

// assert
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:01:00", "The value of minutes is +1");
// act
durationPicker.click();
durationPicker.keys(['Shift', 'PageDown']);
durationPicker.keys('Shift');

// assert
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:00:00", "The value of minutes is -1");

// act
durationPicker.click();
durationPicker.keys('PageUp');

// assert
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "01:00:00", "The value of hours is +1");
// act
durationPicker.click();
durationPicker.keys('PageDown');

// assert
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:00:00", "The value of hours is -1");

// act
durationPicker.click();
durationPicker.keys(['Shift', 'Control', 'PageUp']);
durationPicker.keys('Control');
durationPicker.keys('Shift');

// assert
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:00:01", "The value of seconds is +1");
// act
durationPicker.click();
durationPicker.keys(['Shift', 'Control', 'PageDown']);
durationPicker.keys('Shift');
durationPicker.keys('Control');

// assert
assert.strictEqual(durationPicker.shadow$("ui5-input").getProperty("value"), "00:00:00", "The value of seconds is +1");
});

it("tests valueStateMessage slot", () => {
const picker = browser.$("#pickerValueStateMessage");

Expand Down

0 comments on commit 7ec3c43

Please sign in to comment.