Skip to content

Commit

Permalink
feat(ui5-multiinput, ui5-multi-combobox): implement keyboard handling (
Browse files Browse the repository at this point in the history
…#2166)

Navigation between the tokens is now possible with the Arrow keys both in MultiComboBox and MultiInput components.
  • Loading branch information
ivoplashkov authored Sep 4, 2020
1 parent 904da0e commit dc2ae6d
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 25 deletions.
7 changes: 6 additions & 1 deletion packages/main/src/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,10 +625,15 @@ class Input extends UI5Element {
}

async _onfocusin(event) {
const inputDomRef = await this.getInputDOMRef();

if (event.target !== inputDomRef) {
return;
}

this.focused = true; // invalidating property
this.previousValue = this.value;

await this.getInputDOMRef();
this._inputIconFocused = event.target && event.target === this.querySelector("[ui5-icon]");
}

Expand Down
1 change: 1 addition & 0 deletions packages/main/src/MultiComboBox.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
@ui5-token-delete="{{_tokenDelete}}"
@focusout="{{_tokenizerFocusOut}}"
@click={{_click}}
@keydown="{{_onTokenizerKeydown}}"
?expanded="{{_tokenizerExpanded}}"
>
{{#each items}}
Expand Down
58 changes: 46 additions & 12 deletions packages/main/src/MultiComboBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
import {
isShow, isDown, isBackSpace, isSpace,
isShow,
isDown,
isBackSpace,
isSpace,
isLeft,
isRight,
} from "@ui5/webcomponents-base/dist/Keys.js";
import "@ui5/webcomponents-icons/dist/icons/slim-arrow-down.js";
import { isIE, isPhone } from "@ui5/webcomponents-base/dist/Device.js";
Expand Down Expand Up @@ -444,13 +449,33 @@ class MultiComboBox extends UI5Element {
this.fireSelectionChange();
}

_tokenizerFocusOut() {
_handleLeft() {
const cursorPosition = this.getDomRef().querySelector(`input`).selectionStart;

if (cursorPosition === 0) {
this._focusLastToken();
}
}

_focusLastToken() {
const lastTokenIndex = this._tokenizer.tokens.length - 1;

if (lastTokenIndex < 0) {
return;
}

this._tokenizer.tokens[lastTokenIndex].focus();
this._tokenizer._itemNav.currentIndex = lastTokenIndex;
}

_tokenizerFocusOut(event) {
const tokenizer = this.shadowRoot.querySelector("[ui5-tokenizer]");
const tokensCount = tokenizer.tokens.length - 1;

tokenizer.tokens.forEach(token => { token.selected = false; });

this._tokenizer.scrollToStart();
if (!event.relatedTarget || event.relatedTarget.localName !== "ui5-token") {
this._tokenizer.tokens.forEach(token => { token.selected = false; });
this._tokenizer.scrollToStart();
}

if (tokensCount === 0 && this._deleting) {
setTimeout(() => {
Expand All @@ -468,6 +493,10 @@ class MultiComboBox extends UI5Element {
}

async _onkeydown(event) {
if (isLeft(event)) {
this._handleLeft(event);
}

if (isShow(event) && !this.readonly && !this.disabled) {
event.preventDefault();
this._toggleRespPopover();
Expand All @@ -483,17 +512,22 @@ class MultiComboBox extends UI5Element {
if (isBackSpace(event) && event.target.value === "") {
event.preventDefault();

this._focusLastToken();
}

this._keyDown = true;
}

_onTokenizerKeydown(event) {
if (isRight(event)) {
const lastTokenIndex = this._tokenizer.tokens.length - 1;

if (lastTokenIndex < 0) {
return;
if (this._tokenizer.tokens[lastTokenIndex] === document.activeElement.shadowRoot.activeElement) {
setTimeout(() => {
this.shadowRoot.querySelector("input").focus();
}, 0);
}

this._tokenizer.tokens[lastTokenIndex].focus();
this._tokenizer._itemNav.currentIndex = lastTokenIndex;
}

this._keyDown = true;
}

_filterItems(value) {
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/MultiInput.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.popoverMinWidth={{_inputWidth}}
?expanded="{{expandedTokenizer}}"
show-more
@keydown="{{_onTokenizerKeydown}}"
@show-more-items-press={{showMorePress}}
@token-delete={{tokenDelete}}
@focusout="{{_tokenizerFocusOut}}"
Expand Down
61 changes: 59 additions & 2 deletions packages/main/src/MultiInput.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import { isShow } from "@ui5/webcomponents-base/dist/Keys.js";
import {
isShow,
isBackSpace,
isLeft,
isRight,
} from "@ui5/webcomponents-base/dist/Keys.js";
import Input from "./Input.js";
import MultiInputTemplate from "./generated/templates/MultiInputTemplate.lit.js";
import styles from "./generated/themes/MultiInput.css.js";
Expand Down Expand Up @@ -120,6 +125,13 @@ class MultiInput extends Input {
return [Input.styles, styles];
}

constructor() {
super();

// Prevent suggestions' opening.
this._skipOpenSuggestions = false;
}

valueHelpPress(event) {
this.closePopover();
this.fireEvent("value-help-trigger", {});
Expand Down Expand Up @@ -147,6 +159,7 @@ class MultiInput extends Input {

_tokenizerFocusOut(event) {
if (!this.contains(event.relatedTarget)) {
this.tokenizer._tokens.forEach(token => { token.selected = false; });
this.tokenizer.scrollToStart();
}
}
Expand All @@ -164,11 +177,55 @@ class MultiInput extends Input {
_onkeydown(event) {
super._onkeydown(event);

if (isLeft(event)) {
this._skipOpenSuggestions = true; // Prevent input focus when navigating through the tokens.

return this._handleLeft(event);
}

this._skipOpenSuggestions = false;
if (isBackSpace(event) && event.target.value === "") {
event.preventDefault();

this._focusLastToken();
}

if (isShow(event)) {
this.valueHelpPress();
}
}

_onTokenizerKeydown(event) {
if (isRight(event)) {
const lastTokenIndex = this.tokenizer._tokens.length - 1;

if (this.tokenizer._tokens[lastTokenIndex] === document.activeElement) {
setTimeout(() => {
this.focus();
}, 0);
}
}
}

_handleLeft() {
const cursorPosition = this.getDomRef().querySelector(`input`).selectionStart;

if (cursorPosition === 0) {
this._focusLastToken();
}
}

_focusLastToken() {
const lastTokenIndex = this.tokenizer._tokens.length - 1;

if (lastTokenIndex < 0) {
return;
}

this.tokenizer._itemNav.currentIndex = lastTokenIndex;
this.tokenizer._tokens[lastTokenIndex].focus();
}

_onfocusout(event) {
super._onfocusout(event);
const relatedTarget = event.relatedTarget;
Expand All @@ -185,7 +242,7 @@ class MultiInput extends Input {
const valueHelpPressed = this._valueHelpIconPressed;
const nonEmptyValue = this.value !== "";

return parent && nonEmptyValue && !valueHelpPressed;
return parent && nonEmptyValue && !valueHelpPressed && !this._skipOpenSuggestions;
}

lastItemDeleted() {
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/Token.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div
tabindex="{{_tabIndex}}"
@click="{{_select}}"
@click="{{_handleSelect}}"
@keydown="{{_keydown}}"
class="ui5-token--wrapper"
dir="{{effectiveDir}}"
Expand Down
36 changes: 29 additions & 7 deletions packages/main/src/Token.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import { getTheme } from "@ui5/webcomponents-base/dist/config/Theme.js";
import {
isBackSpace,
isEnter,
isSpace,
isDelete,
} from "@ui5/webcomponents-base/dist/Keys.js";
Expand Down Expand Up @@ -52,6 +51,20 @@ const metadata = {
* @private
*/
overflows: { type: Boolean },

/** Defines whether the <code>ui5-token</code> is selected or not.
*
* @type {boolean}
* @public
*/
selected: { type: Boolean },

/**
* Defines the tabIndex of the component.
* @type {string}
* @private
*/
_tabIndex: { type: String, defaultValue: "-1", noAttribute: true },
},

events: /** @lends sap.ui.webcomponents.main.Token.prototype */ {
Expand All @@ -70,6 +83,14 @@ const metadata = {
"delete": { type: Boolean },
},
},

/**
* Fired when the a <code>ui5-token</code> is selected by user interaction with mouse or clicking space.
*
* @event
* @public
*/
select: {},
},
};

Expand Down Expand Up @@ -114,10 +135,10 @@ class Token extends UI5Element {
this.i18nBundle = getI18nBundle("@ui5/webcomponents");
}

_select() {
_handleSelect() {
this.selected = !this.selected;
this.fireEvent("select");
this.selected = true;
}
}

_delete() {
this.fireEvent("delete");
Expand All @@ -136,9 +157,10 @@ class Token extends UI5Element {
});
}

if (isEnter(event) || isSpace(event)) {
this.fireEvent("select", {});
this.selected = true;
if (isSpace(event)) {
event.preventDefault();

this._handleSelect();
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/main/src/Tokenizer.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
class="{{classes.wrapper}}"
>
<span id="{{_id}}-hiddenText" class="ui5-hidden-text">{{tokenizerLabel}}</span>

<div
class="{{classes.content}}"
@ui5-delete="{{_tokenDelete}}"
@click="{{_click}}"
@mousedown="{{_onmousedown}}"
@keydown="{{_onkeydown}}"
role="listbox"
aria-labelledby="{{_id}}-hiddenText"
>
Expand Down
29 changes: 28 additions & 1 deletion packages/main/src/Tokenizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation
import ScrollEnablement from "@ui5/webcomponents-base/dist/delegate/ScrollEnablement.js";
import Integer from "@ui5/webcomponents-base/dist/types/Integer.js";
import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
import { isSpace } from "@ui5/webcomponents-base/dist/Keys.js";
import ResponsivePopover from "./ResponsivePopover.js";
import List from "./List.js";
import StandardListItem from "./StandardListItem.js";
Expand Down Expand Up @@ -108,7 +109,7 @@ class Tokenizer extends UI5Element {
super();

this._resizeHandler = this._handleResize.bind(this);
this._itemNav = new ItemNavigation(this);
this._itemNav = new ItemNavigation(this, { currentIndex: "-1" });
this._itemNav.getItemsCallback = this._getVisibleTokens.bind(this);
this._scrollEnablement = new ScrollEnablement(this);
this.i18nBundle = getI18nBundle("@ui5/webcomponents");
Expand Down Expand Up @@ -181,6 +182,32 @@ class Tokenizer extends UI5Element {
this.fireEvent("token-delete", { ref: token });
}

_onkeydown(event) {
if (isSpace(event)) {
event.preventDefault();

this._handleTokenSelection(event);
}
}

_click(event) {
this._handleTokenSelection(event);
}

_onmousedown(event) {
this._itemNav.update(event.target);
}

_handleTokenSelection(event) {
if (event.target.localName === "ui5-token") {
this._tokens.forEach(token => {
if (token !== event.target) {
token.selected = false;
}
});
}
}

/* Keyboard handling */

_updateAndFocus() {
Expand Down

0 comments on commit dc2ae6d

Please sign in to comment.