Converts gr-reviewer-list to use a new, generic autocomplete
Introduces gr-autocomplete, which is a generic component for
autocompleted text inputs. The reviewer-specific autocomplete box found
in gr-reviewer-list is converted to use gr-autocomplete.
Change-Id: I47426740d3372ac0fed08e269aff280d444fe0f4
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 3fee424..e3489c3 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -16,9 +16,9 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-reviewer-list">
@@ -45,21 +45,6 @@
gr-account-chip {
margin-top: .3em;
}
- .dropdown {
- background-color: #fff;
- box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
- position: absolute;
- left: 0;
- top: 100%;
- z-index: 1000;
- }
- .dropdown .reviewer {
- cursor: pointer;
- padding: .5em .75em;
- }
- .dropdown .reviewer[selected] {
- background-color: #ccc;
- }
.remove,
.cancel {
color: #999;
@@ -89,29 +74,24 @@
<div class="controlsContainer" hidden$="[[!mutable]]">
<div class="autocompleteContainer" hidden$="[[!_showInput]]">
<div class="inputContainer">
- <input is="iron-input" id="input"
- bind-value="{{_inputVal}}" disabled$="[[disabled]]">
- <gr-button link class="cancel" on-tap="_handleCancelTap">×</gr-button>
- </div>
- <div class="dropdown" hidden$="[[_hideAutocomplete]]">
- <template is="dom-repeat" items="[[_autocompleteData]]" as="reviewer">
- <div class="reviewer"
- data-index$="[[index]]"
- on-mouseenter="_handleMouseEnterItem"
- on-tap="_handleItemTap"
- selected$="[[_computeSelected(index, _selectedIndex)]]">
- <template is="dom-if" if="[[reviewer.account]]">
- <gr-account-label
- account="[[reviewer.account]]" show-email></gr-account-label>
- </template>
- <template is="dom-if" if="[[reviewer.group]]">
- <span>[[reviewer.group.name]] (group)</span>
- </template>
- </div>
- </template>
+ <gr-autocomplete
+ id="input"
+ threshold="3"
+ query="[[_query]]"
+ disabled="[[disabled]]"
+ on-commit="_sendAddRequest"
+ on-cancel="_handleCancelTap"></gr-autocomplete>
+ <gr-button
+ link
+ class="cancel"
+ on-tap="_handleCancelTap">×</gr-button>
</div>
</div>
- <gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap"
+ <gr-button
+ link
+ id="addReviewer"
+ class="addReviewer"
+ on-tap="_handleAddTap"
hidden$="[[_showInput]]">Add reviewer</gr-button>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index de99039..84c15b1 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -37,38 +37,15 @@
type: Array,
value: function() { return []; },
},
- _autocompleteData: {
- type: Array,
- value: function() { return []; },
- observer: '_autocompleteDataChanged',
- },
- _inputVal: {
- type: String,
- value: '',
- observer: '_inputValChanged',
- },
- _inputRequestHandle: Number,
- _inputRequestTimeout: {
- type: Number,
- value: 250,
- },
_showInput: {
type: Boolean,
value: false,
},
- _hideAutocomplete: {
- type: Boolean,
- value: true,
- observer: '_hideAutocompleteChanged',
- },
- _selectedIndex: {
- type: Number,
- value: 0,
- },
- _boundBodyClickHandler: {
+
+ _query: {
type: Function,
value: function() {
- return this._handleBodyClick.bind(this);
+ return this._getReviewerSuggestions.bind(this);
},
},
@@ -77,25 +54,10 @@
_xhrPromise: Object,
},
- behaviors: [
- Gerrit.KeyboardShortcutBehavior,
- ],
-
observers: [
'_reviewersChanged(change.reviewers.*, change.owner)',
],
- detached: function() {
- this._clearInputRequestHandle();
- },
-
- _clearInputRequestHandle: function() {
- if (this._inputRequestHandle != null) {
- this.cancelAsync(this._inputRequestHandle);
- this._inputRequestHandle = null;
- }
- },
-
_reviewersChanged: function(changeRecord, owner) {
var result = [];
var reviewers = changeRecord.base;
@@ -121,21 +83,6 @@
return false;
},
- _computeSelected: function(index, selectedIndex) {
- return index == selectedIndex;
- },
-
- _handleBodyClick: function(e) {
- var eventPath = Polymer.dom(e).path;
- for (var i = 0; i < eventPath.length; i++) {
- if (eventPath[i] == this) {
- return;
- }
- }
- this._selectedIndex = -1;
- this._autocompleteData = [];
- },
-
_handleRemove: function(e) {
e.preventDefault();
var target = Polymer.dom(e).rootTarget;
@@ -170,139 +117,27 @@
_handleCancelTap: function(e) {
e.preventDefault();
+ this.$.input.clear();
this._cancel();
},
- _handleMouseEnterItem: function(e) {
- this._selectedIndex =
- parseInt(Polymer.dom(e).rootTarget.getAttribute('data-index'), 10);
- },
-
- _handleItemTap: function(e) {
- var reviewerEl;
- var eventPath = Polymer.dom(e).path;
- for (var i = 0; i < eventPath.length; i++) {
- var el = eventPath[i];
- if (el.classList && el.classList.contains('reviewer')) {
- reviewerEl = el;
- break;
- }
- }
- this._selectedIndex =
- parseInt(reviewerEl.getAttribute('data-index'), 10);
- this._sendAddRequest();
- },
-
- _autocompleteDataChanged: function(data) {
- this._hideAutocomplete = data.length == 0;
- },
-
- _hideAutocompleteChanged: function(hidden) {
- if (hidden) {
- document.body.removeEventListener('click',
- this._boundBodyClickHandler);
- this._selectedIndex = -1;
- } else {
- document.body.addEventListener('click', this._boundBodyClickHandler);
- this._selectedIndex = 0;
- }
- },
-
- _inputValChanged: function(val) {
- var sendRequest = function() {
- if (this.disabled || val == null || val.trim().length == 0) {
- return;
- }
- if (val.length < this.suggestFrom) {
- this._clearInputRequestHandle();
- this._hideAutocomplete = true;
- this._selectedIndex = -1;
- return;
- }
- this._lastAutocompleteRequest =
- this._getSuggestedReviewers(this.change._number, val).then(
- this._handleReviewersResponse.bind(this));
- }.bind(this);
-
- this._clearInputRequestHandle();
- if (this._inputRequestTimeout == 0) {
- sendRequest();
- } else {
- this._inputRequestHandle =
- this.async(sendRequest, this._inputRequestTimeout);
- }
- },
-
- _handleReviewersResponse: function(response) {
- this._autocompleteData = response.filter(function(reviewer) {
- var account = reviewer.account;
- if (!account) { return true; }
- for (var i = 0; i < this._reviewers.length; i++) {
- if (account._account_id == this.change.owner._account_id ||
- account._account_id == this._reviewers[i]._account_id) {
- return false;
- }
- }
- return true;
- }, this);
- },
-
- _getSuggestedReviewers: function(changeNum, inputVal) {
- return this.$.restAPI.getChangeSuggestedReviewers(changeNum, inputVal);
- },
-
- _handleKey: function(e) {
- if (this._hideAutocomplete) {
- if (e.keyCode == 27) { // 'esc'
- e.preventDefault();
- this._cancel();
- }
- return;
- }
-
- switch (e.keyCode) {
- case 38: // 'up':
- e.preventDefault();
- this._selectedIndex = Math.max(this._selectedIndex - 1, 0);
- break;
- case 40: // 'down'
- e.preventDefault();
- this._selectedIndex = Math.min(this._selectedIndex + 1,
- this._autocompleteData.length - 1);
- break;
- case 27: // 'esc'
- e.preventDefault();
- this._hideAutocomplete = true;
- break;
- case 13: // 'enter'
- e.preventDefault();
- this._sendAddRequest();
- break;
- }
- },
-
_cancel: function() {
this._showInput = false;
- this._selectedIndex = 0;
- this._inputVal = '';
- this._autocompleteData = [];
+ this.$.input.clear();
this.$.addReviewer.focus();
},
- _sendAddRequest: function() {
- this._clearInputRequestHandle();
-
+ _sendAddRequest: function(e, reviewer) {
var reviewerID;
- var reviewer = this._autocompleteData[this._selectedIndex];
if (reviewer.account) {
reviewerID = reviewer.account._account_id;
} else if (reviewer.group) {
reviewerID = reviewer.group.id;
}
- this._autocompleteData = [];
+
this.disabled = true;
this._xhrPromise = this._addReviewer(reviewerID).then(function(response) {
- this.change.reviewers['CC'] = this.change.reviewers['CC'] || [];
+ this.change.reviewers.CC = this.change.reviewers.CC || [];
this.disabled = false;
if (!response.ok) { return response; }
@@ -311,7 +146,6 @@
this.push('change.removable_reviewers', r);
this.push('change.reviewers.CC', r);
}, this);
- this._inputVal = '';
this.$.input.focus();
}.bind(this));
}.bind(this)).catch(function(err) {
@@ -327,5 +161,47 @@
_removeReviewer: function(id) {
return this.$.restAPI.removeChangeReviewer(this.change._number, id);
},
+
+ _notInList: function(reviewer) {
+ var account = reviewer.account;
+ if (!account) { return true; }
+ if (account._account_id === this.change.owner._account_id) {
+ return false;
+ }
+ for (var i = 0; i < this._reviewers.length; i++) {
+ if (account._account_id === this._reviewers[i]._account_id) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ _makeSuggestion: function(reviewer) {
+ if (reviewer.account) {
+ return {
+ name: reviewer.account.name + ' (' + reviewer.account.email + ')',
+ value: reviewer,
+ };
+ } else if (reviewer.group) {
+ return {
+ name: reviewer.group.name,
+ value: reviewer,
+ };
+ }
+ },
+
+ _getReviewerSuggestions: function(input) {
+ var xhr = this.$.restAPI.getChangeSuggestedReviewers(
+ this.change._number, input);
+
+ this._lastAutocompleteRequest = xhr;
+
+ return xhr.then(function(reviewers) {
+ if (!reviewers) { return []; }
+ return reviewers
+ .filter(this._notInList.bind(this))
+ .map(this._makeSuggestion);
+ }.bind(this));
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 9311e91..b89bc4a 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -34,9 +34,11 @@
<script>
suite('gr-reviewer-list tests', function() {
var element;
+ var autocompleteInput;
setup(function() {
element = fixture('basic');
+ autocompleteInput = element.$.input.$.input;
stub('gr-rest-api-interface', {
getChangeSuggestedReviewers: function() {
return Promise.resolve([
@@ -112,7 +114,9 @@
assert.isFalse(
element.$$('.autocompleteContainer').hasAttribute('hidden'));
assert.equal(getActiveElement().id, 'input');
- MockInteractions.pressAndReleaseKeyOn(element, 27); // 'esc'
+
+ MockInteractions.pressAndReleaseKeyOn(autocompleteInput, 27); // 'esc'
+
assert.isFalse(element.$$('.addReviewer').hasAttribute('hidden'));
assert.isTrue(
element.$$('.autocompleteContainer').hasAttribute('hidden'));
@@ -178,15 +182,13 @@
test('autocomplete starts at >= 3 chars', function() {
element._inputRequestTimeout = 0;
element._mutable = true;
- var requestStub = sinon.stub(element, '_getSuggestedReviewers',
- function() {
- assert(false, '_getSuggestedReviewers should not be called for ' +
- 'input lengths of less than 3 chars');
- }
- );
- element._inputVal = 'fo';
+ element.change = {_number: 123};
+
+ element.$.input.text = 'fo';
+
flushAsynchronousOperations();
- requestStub.restore();
+
+ assert.isFalse(element.$.restAPI.getChangeSuggestedReviewers.called);
});
test('add/remove reviewer flow', function(done) {
@@ -200,35 +202,21 @@
element._mutable = true;
MockInteractions.tap(element.$$('.addReviewer'));
flushAsynchronousOperations();
- element._inputVal = 'andy';
+ element.$.input.text = 'andy';
element._lastAutocompleteRequest.then(function() {
flushAsynchronousOperations();
- assert.isFalse(element.$$('.dropdown').hasAttribute('hidden'));
- var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer');
- assert.equal(itemEls.length, 3);
- assert.isTrue(itemEls[0].hasAttribute('selected'));
- assert.isFalse(itemEls[1].hasAttribute('selected'));
- MockInteractions.pressAndReleaseKeyOn(element, 40); // 'down'
- assert.isFalse(itemEls[0].hasAttribute('selected'));
- assert.isTrue(itemEls[1].hasAttribute('selected'));
+ MockInteractions.pressAndReleaseKeyOn(autocompleteInput, 27); // 'esc'
+ assert.isTrue(element.$$('.autocompleteContainer')
+ .hasAttribute('hidden'));
- MockInteractions.pressAndReleaseKeyOn(element, 38); // 'up'
- assert.isTrue(itemEls[0].hasAttribute('selected'));
- assert.isFalse(itemEls[1].hasAttribute('selected'));
+ MockInteractions.tap(element.$$('.addReviewer'));
- MockInteractions.pressAndReleaseKeyOn(element, 27); // 'esc'
- assert.isTrue(element.$$('.dropdown').hasAttribute('hidden'));
-
- element._inputVal = 'andyb';
+ element.$.input.text = 'andyb';
element._lastAutocompleteRequest.then(function() {
- assert.isFalse(element.$$('.dropdown').hasAttribute('hidden'));
- var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer');
- assert.equal(itemEls.length, 3);
- assert.isTrue(itemEls[0].hasAttribute('selected'));
- assert.isFalse(itemEls[1].hasAttribute('selected'));
- MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+
+ MockInteractions.pressAndReleaseKeyOn(autocompleteInput, 13); // 'enter'
assert.isTrue(element.disabled);
element._xhrPromise.then(function() {
@@ -253,5 +241,63 @@
});
});
});
+
+ test('_makeSuggestion', function() {
+ var account = {
+ _account_id: 123456,
+ name: 'name',
+ email: 'email'
+ };
+ var group = {
+ id: '123456',
+ name: 'name',
+ };
+
+ var suggestion = element._makeSuggestion({account: account});
+
+ assert.deepEqual(suggestion, {
+ name: 'name (email)',
+ value: {account: account},
+ });
+
+ suggestion = element._makeSuggestion({group: group});
+
+ assert.deepEqual(suggestion, {
+ name: 'name',
+ value: {group: group},
+ });
+ });
+
+ test('_notInList', function() {
+ var group = {
+ id: '123456',
+ name: 'name',
+ };
+ var account = {
+ _account_id: 123456,
+ name: 'name',
+ email: 'email',
+ };
+
+ element.change = {owner: {_account_id: 123456}};
+
+ // Is true when passing a group.
+ assert.isTrue(element._notInList({group: group}));
+
+ // Is false when passing the change owner.
+ assert.isFalse(element._notInList({account: account}));
+
+ element.change.owner._account_id = 789;
+
+ // Is true when passing a different user than the change owner, and is not
+ // in the reviewer list.
+ assert.isTrue(element._notInList({account: account}));
+
+ element._reviewers = [{_account_id: 123456}];
+
+ // Is false when passing a different user than the change owner, but *is*
+ // the reviewer list.
+ assert.isFalse(element._notInList({account: account}));
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
new file mode 100644
index 0000000..d5c1140
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -0,0 +1,68 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
+
+<dom-module id="gr-autocomplete">
+ <template>
+ <style>
+ input {
+ font-size: 1em;
+ }
+ #suggestions {
+ background-color: #fff;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+ position: absolute;
+ z-index: 10;
+ }
+ ul {
+ list-style: none;
+ }
+ li {
+ cursor: pointer;
+ padding: .5em .75em;
+ }
+ li.selected {
+ background-color: #eee;
+ }
+ </style>
+ <input
+ id="input"
+ is="iron-input"
+ disabled$="[[disabled]]"
+ bind-value="{{text}}"
+ on-keydown="_handleInputKeydown"
+ on-focus="_updateSuggestions" />
+ <div
+ id="suggestions"
+ hidden$="[[_computeSuggestionsHidden(_suggestions)]]">
+ <ul>
+ <template is="dom-repeat" items="[[_suggestions]]">
+ <li
+ data-index$="[[index]]"
+ on-tap="_handleSuggestionTap">[[item.name]]</li>
+ </template>
+ </ul>
+ </div>
+ <gr-cursor-manager
+ id="cursor"
+ index="{{_index}}"
+ cursor-target-class="selected"
+ stops="[[_getSuggestionElems(_suggestions)]]"></gr-cursor-manager>
+ </template>
+ <script src="gr-autocomplete.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
new file mode 100644
index 0000000..dfe0575
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -0,0 +1,171 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-autocomplete',
+
+ /**
+ * Fired when a value is chosen.
+ *
+ * @event commit
+ */
+
+ /**
+ * Fired when the user cancels.
+ *
+ * @event cancel
+ */
+
+ properties: {
+
+ /**
+ * Query for requesting autocomplete suggestions. The function should
+ * accept the input as a string parameter and return a promise. The
+ * promise should yield an array of suggestion objects with "name" and
+ * "value" properties. The "name" property will be displayed in the
+ * suggestion entry. The "value" property will be emitted if that
+ * suggestion is selected.
+ *
+ * @type {function(String): Promise<Array<Object>>}
+ */
+ query: {
+ type: Function,
+ value: function() {
+ return function() {
+ return Promise.resolve([]);
+ };
+ },
+ },
+
+ /**
+ * The number of characters that must be typed before suggestions are
+ * made.
+ */
+ threshold: {
+ type: Number,
+ value: 1,
+ },
+
+ disabled: Boolean,
+
+ text: {
+ type: String,
+ observer: '_updateSuggestions',
+ },
+
+ _value: {
+ type: Object,
+ computed: '_getValue(_suggestions, _index)'
+ },
+
+ _suggestions: {
+ type: Array,
+ value: function() { return []; },
+ },
+
+ _index: Number,
+ },
+
+ attached: function() {
+ this.listen(document.body, 'click', '_handleBodyClick');
+ },
+
+ detached: function() {
+ this.unlisten(document.body, 'click', '_handleBodyClick');
+ },
+
+ focus: function() {
+ this.$.input.focus();
+ },
+
+ clear: function() {
+ this.text = '';
+ },
+
+ _updateSuggestions: function() {
+ if (this.text.length < this.threshold) {
+ this._suggestions = [];
+ return;
+ }
+
+ this.query(this.text).then(function(suggestions) {
+ this._suggestions = suggestions;
+ this.$.cursor.moveToStart();
+ }.bind(this));
+ },
+
+ _computeSuggestionsHidden: function(suggestions) {
+ return !suggestions.length;
+ },
+
+ _getSuggestionElems: function() {
+ Polymer.dom.flush();
+ return this.$.suggestions.querySelectorAll('li');
+ },
+
+ _handleInputKeydown: function(e) {
+ switch (e.keyCode) {
+ case 38: // Up
+ e.preventDefault();
+ this.$.cursor.previous();
+ break;
+ case 40: // Down
+ e.preventDefault();
+ this.$.cursor.next();
+ break;
+ case 27: // Escape
+ e.preventDefault();
+ this._cancel();
+ break;
+ case 13: // Enter
+ e.preventDefault();
+ this._commit();
+ this._suggestions = [];
+ break;
+ }
+ },
+
+ _cancel: function() {
+ this._suggestions = [];
+ this.fire('cancel');
+ },
+
+ _getValue: function(suggestions, index) {
+ if (!suggestions.length || index === -1) { return null; }
+ return suggestions[index].value;
+ },
+
+ _handleBodyClick: function(e) {
+ var eventPath = Polymer.dom(e).path;
+ for (var i = 0; i < eventPath.length; i++) {
+ if (eventPath[i] == this) {
+ return;
+ }
+ }
+ this._suggestions = [];
+ },
+
+ _handleSuggestionTap: function(e) {
+ this.$.cursor.setCursor(e.target);
+ this._commit();
+ },
+
+ _commit: function() {
+ this.fire('commit', this._value);
+ this.clear();
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
new file mode 100644
index 0000000..6a8f62c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -0,0 +1,174 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-reviewer-list</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-autocomplete.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-autocomplete></gr-autocomplete>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-autocomplete tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('renders', function(done) {
+ var promise;
+ var queryStub = sinon.spy(function(input) {
+ return promise = Promise.resolve([
+ {name: input + ' 0', value: 0},
+ {name: input + ' 1', value: 1},
+ {name: input + ' 2', value: 2},
+ {name: input + ' 3', value: 3},
+ {name: input + ' 4', value: 4},
+ ]);
+ });
+ element.query = queryStub;
+
+ assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+ assert.equal(element.$.cursor.index, -1);
+
+ element.text = 'blah';
+
+ assert.isTrue(queryStub.called);
+
+ promise.then(function() {
+ assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
+
+ var suggestions = element.$.suggestions.querySelectorAll('li');
+ assert.equal(suggestions.length, 5);
+
+ for (var i = 0; i < 5; i++) {
+ assert.equal(suggestions[i].textContent, 'blah ' + i);
+ }
+
+ assert.notEqual(element.$.cursor.index, -1);
+
+ done();
+ });
+ });
+
+ test('emits cancel', function(done) {
+ var promise;
+ var queryStub = sinon.spy(function() {
+ return promise = Promise.resolve([
+ {name: 'blah', value: 123},
+ ]);
+ });
+ element.query = queryStub;
+
+ assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+
+ element.text = 'blah';
+
+ promise.then(function() {
+ assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
+
+ var cancelHandler = sinon.spy();
+ element.addEventListener('cancel', cancelHandler);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 27); // Esc
+
+ assert.isTrue(cancelHandler.called);
+ assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+
+ done();
+ });
+ });
+
+ test('emits commit and handles cursor movement', function(done) {
+ var promise;
+ var queryStub = sinon.spy(function(input) {
+ return promise = Promise.resolve([
+ {name: input + ' 0', value: 0},
+ {name: input + ' 1', value: 1},
+ {name: input + ' 2', value: 2},
+ {name: input + ' 3', value: 3},
+ {name: input + ' 4', value: 4},
+ ]);
+ });
+ element.query = queryStub;
+
+ assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+ assert.equal(element.$.cursor.index, -1);
+
+ element.text = 'blah';
+
+ promise.then(function() {
+ assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
+
+ var commitHandler = sinon.spy();
+ element.addEventListener('commit', commitHandler);
+
+ assert.equal(element.$.cursor.index, 0);
+ assert.equal(element._value, 0);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down
+
+ assert.equal(element.$.cursor.index, 1);
+ assert.equal(element._value, 1);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down
+
+ assert.equal(element.$.cursor.index, 2);
+ assert.equal(element._value, 2);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 38); // Up
+
+ assert.equal(element.$.cursor.index, 1);
+ assert.equal(element._value, 1);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
+
+ assert.isTrue(commitHandler.called);
+ assert.equal(commitHandler.getCall(0).args[0].detail, 1);
+ assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+
+ done();
+ });
+ });
+
+ test('threshold guards the query', function() {
+ var queryStub = sinon.spy(function() {
+ return Promise.resolve([]);
+ });
+ element.query = queryStub;
+
+ element.threshold = 2;
+
+ element.text = 'a';
+
+ assert.isFalse(queryStub.called);
+
+ element.text = 'ab';
+
+ assert.isTrue(queryStub.called);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index e1990c4..7fff3d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -43,6 +43,7 @@
index: {
type: Number,
value: -1,
+ notify: true,
},
/**
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 073d7e1..32b7536 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -55,6 +55,7 @@
'diff/gr-selection-action-box/gr-selection-action-box_test.html',
'settings/gr-menu-editor/gr-menu-editor_test.html',
'settings/gr-settings-view/gr-settings-view_test.html',
+ 'shared/gr-autocomplete/gr-autocomplete_test.html',
'shared/gr-account-label/gr-account-label_test.html',
'shared/gr-account-link/gr-account-link_test.html',
'shared/gr-alert/gr-alert_test.html',