diff --git a/packages/main/src/Select.js b/packages/main/src/Select.js index 337263895ce0..728c15c6c761 100644 --- a/packages/main/src/Select.js +++ b/packages/main/src/Select.js @@ -316,6 +316,8 @@ class Select extends UI5Element { this._selectedIndexBeforeOpen = -1; this._escapePressed = false; this._lastSelectedOption = null; + this._typedChars = ""; + this._typingTimeoutID = -1; this.i18nBundle = getI18nBundle("@ui5/webcomponents"); } @@ -458,11 +460,64 @@ class Select extends UI5Element { this._handleEndKey(event); } else if (isEnter(event)) { this._handleSelectionChange(); - } else { + } else if (isUp(event) || isDown(event)) { this._handleArrowNavigation(event); + } else { + this._handleKeyboardNavigation(event); + } + } + + _handleKeyboardNavigation(event) { + // Waiting for the actual symbol to trigger the keydown event + if (event.shiftKey && event.key === "Shift") { + return; + } + + const typedCharacter = event.key.toLowerCase(); + + this._typedChars += typedCharacter; + + // We check if we have more than one characters and they are all duplicate, we set the + // text to be the last input character (typedCharacter). If not, we set the text to be + // the whole input string. + + const text = (/^(.)\1+$/i).test(this._typedChars) ? typedCharacter : this._typedChars; + + clearTimeout(this._typingTimeoutID); + + this._typingTimeoutID = setTimeout(() => { + this._typedChars = ""; + this._typingTimeoutID = -1; + }, 1000); + + this._selectTypedItem(text); + } + + _selectTypedItem(text) { + const currentIndex = this._selectedIndex; + const itemToSelect = this._searchNextItemByText(text); + + if (itemToSelect) { + const nextIndex = this._getSelectedItemIndex(itemToSelect); + + this._changeSelectedItem(this._selectedIndex, nextIndex); + + if (currentIndex !== this._selectedIndex) { + this.itemSelectionAnnounce(); + } } } + _searchNextItemByText(text) { + let orderedOptions = this.options.slice(0); + const optionsAfterSelected = orderedOptions.splice(this._selectedIndex + 1, orderedOptions.length - this._selectedIndex); + const optionsBeforeSelected = orderedOptions.splice(0, orderedOptions.length - 1); + + orderedOptions = optionsAfterSelected.concat(optionsBeforeSelected); + + return orderedOptions.find(option => option.textContent.toLowerCase().startsWith(text)); + } + _handleHomeKey(event) { event.preventDefault(); this._changeSelectedItem(this._selectedIndex, 0); @@ -530,24 +585,21 @@ class Select extends UI5Element { let nextIndex = -1; const currentIndex = this._selectedIndex; const isDownKey = isDown(event); - const isUpKey = isUp(event); - if (isDownKey || isUpKey) { - event.preventDefault(); - if (isDownKey) { - nextIndex = this._getNextOptionIndex(); - } else { - nextIndex = this._getPreviousOptionIndex(); - } + event.preventDefault(); + if (isDownKey) { + nextIndex = this._getNextOptionIndex(); + } else { + nextIndex = this._getPreviousOptionIndex(); + } - this._changeSelectedItem(this._selectedIndex, nextIndex); + this._changeSelectedItem(this._selectedIndex, nextIndex); - if (currentIndex !== this._selectedIndex) { - // Announce new item even if picker is opened. - // The aria-activedescendents attribute can't be used, - // because listitem elements are in different shadow dom - this.itemSelectionAnnounce(); - } + if (currentIndex !== this._selectedIndex) { + // Announce new item even if picker is opened. + // The aria-activedescendents attribute can't be used, + // because listitem elements are in different shadow dom + this.itemSelectionAnnounce(); } } diff --git a/packages/main/test/specs/Select.spec.js b/packages/main/test/specs/Select.spec.js index e437bb305881..c591c3e9f6ca 100644 --- a/packages/main/test/specs/Select.spec.js +++ b/packages/main/test/specs/Select.spec.js @@ -111,13 +111,13 @@ describe("Select general interaction", () => { select.keys("ArrowDown"); assert.ok(selectText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT2), "Arrow Down should change selected item"); assert.strictEqual(selectionText.getHTML(false), EXPECTED_SELECTION_TEXT2, "Selection announcement text should be equalt to the current selected item's text"); - + // change previewed item with picker opened select.click(); select.keys("ArrowUp"); assert.strictEqual(selectionText.getHTML(false), EXPECTED_SELECTION_TEXT1, "Selection announcement text should be equalt to the current selected item's text"); select.keys("Escape"); - + // change selection with picker opened select.click(); select.keys("ArrowUp"); @@ -205,6 +205,31 @@ describe("Select general interaction", () => { select.keys("Escape"); }); + it("changes selection with typing single letter", () => { + const select = browser.$("#keyboardHandling"); + const EXPECTED_SELECTION_TEXT = "Banana"; + + select.click(); // Open select + select.keys("b"); + + const selectText = select.shadow$(".ui5-select-label-root"); + + assert.ok(selectText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT) > -1, "Typing letter should change selection"); + }); + + it("changes selection with typing more letters", () => { + const select = browser.$("#mySelect3"); + const EXPECTED_SELECTION_TEXT = "Brazil"; + + select.click(); // Open select + select.keys("b"); + select.keys("r"); + + const selectText = select.shadow$(".ui5-select-label-root"); + + assert.ok(selectText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT) > -1, "Typing text should change selection"); + }); + it("opens upon space", () => { browser.url(`http://localhost:${PORT}/test-resources/pages/Select.html`);