Update gr-autocomplete-dropdown to use iron-fit-behavior
This is instead of iron-dropdown, because there were some focus issues with iron-dropdown in which the iron-dropdown controlled focus when we did not want it to. Bug: Issue 7334 Change-Id: I2da8e0892eecb950f35d7d2b7664a1c7ff5f111a
This commit is contained in:
@@ -25,11 +25,11 @@ limitations under the License.
|
|||||||
<dom-module id="gr-autocomplete-dropdown">
|
<dom-module id="gr-autocomplete-dropdown">
|
||||||
<template>
|
<template>
|
||||||
<style include="shared-styles">
|
<style include="shared-styles">
|
||||||
/* This must be set here vs. the container component because in some cases
|
:host {
|
||||||
the element is moved in the DOM to a base element and is no longer a
|
z-index: 100;
|
||||||
child of its original parent. */
|
}
|
||||||
:host(.fixed){
|
:host([is-hidden]) {
|
||||||
position: fixed;
|
display: none;
|
||||||
}
|
}
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -49,12 +49,6 @@ limitations under the License.
|
|||||||
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
|
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<iron-dropdown
|
|
||||||
id="dropdown"
|
|
||||||
allow-outside-scroll="true"
|
|
||||||
vertical-align="top"
|
|
||||||
horizontal-align="auto"
|
|
||||||
vertical-offset="[[verticalOffset]]">
|
|
||||||
<div
|
<div
|
||||||
class="dropdown-content"
|
class="dropdown-content"
|
||||||
slot="dropdown-content"
|
slot="dropdown-content"
|
||||||
@@ -71,7 +65,6 @@ limitations under the License.
|
|||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</iron-dropdown>
|
|
||||||
<gr-cursor-manager
|
<gr-cursor-manager
|
||||||
id="cursor"
|
id="cursor"
|
||||||
index="{{index}}"
|
index="{{index}}"
|
||||||
|
|||||||
@@ -14,9 +14,6 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const AWAIT_MAX_ITERS = 10;
|
|
||||||
const AWAIT_STEP = 5;
|
|
||||||
|
|
||||||
Polymer({
|
Polymer({
|
||||||
is: 'gr-autocomplete-dropdown',
|
is: 'gr-autocomplete-dropdown',
|
||||||
|
|
||||||
@@ -34,6 +31,10 @@
|
|||||||
|
|
||||||
properties: {
|
properties: {
|
||||||
index: Number,
|
index: Number,
|
||||||
|
isHidden: {
|
||||||
|
value: true,
|
||||||
|
reflectToAttribute: true,
|
||||||
|
},
|
||||||
verticalOffset: {
|
verticalOffset: {
|
||||||
type: Number,
|
type: Number,
|
||||||
value: null,
|
value: null,
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
behaviors: [
|
behaviors: [
|
||||||
|
Polymer.IronFitBehavior,
|
||||||
Gerrit.KeyboardShortcutBehavior,
|
Gerrit.KeyboardShortcutBehavior,
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -65,46 +67,14 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.$.dropdown.close();
|
this.isHidden = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
this._open().then(() => {
|
this.isHidden = false;
|
||||||
|
this.refit();
|
||||||
this._resetCursorStops();
|
this._resetCursorStops();
|
||||||
this._resetCursorIndex();
|
this._resetCursorIndex();
|
||||||
this.fire('open-complete');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO (beckysiegel) look into making this a behavior since it's used
|
|
||||||
// 3 times now.
|
|
||||||
_open(...args) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
Polymer.IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args);
|
|
||||||
this._awaitOpen(resolve);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
|
|
||||||
* opening. Eventually replace with a direct way to listen to the overlay.
|
|
||||||
*/
|
|
||||||
_awaitOpen(fn) {
|
|
||||||
let iters = 0;
|
|
||||||
const step = () => {
|
|
||||||
this.async(() => {
|
|
||||||
if (this.style.display !== 'none') {
|
|
||||||
fn.call(this);
|
|
||||||
} else if (iters++ < AWAIT_MAX_ITERS) {
|
|
||||||
step.call(this);
|
|
||||||
}
|
|
||||||
}, AWAIT_STEP);
|
|
||||||
};
|
|
||||||
step.call(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
get isHidden() {
|
|
||||||
return !this.$.dropdown.opened;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrentText() {
|
getCurrentText() {
|
||||||
@@ -112,7 +82,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
_handleUp(e) {
|
_handleUp(e) {
|
||||||
if (!this.hidden) {
|
if (!this.isHidden) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.cursorUp();
|
this.cursorUp();
|
||||||
@@ -120,7 +90,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
_handleDown(e) {
|
_handleDown(e) {
|
||||||
if (!this.hidden) {
|
if (!this.isHidden) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.cursorDown();
|
this.cursorDown();
|
||||||
@@ -128,13 +98,13 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
cursorDown() {
|
cursorDown() {
|
||||||
if (!this.hidden) {
|
if (!this.isHidden) {
|
||||||
this.$.cursor.next();
|
this.$.cursor.next();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cursorUp() {
|
cursorUp() {
|
||||||
if (!this.hidden) {
|
if (!this.isHidden) {
|
||||||
this.$.cursor.previous();
|
this.$.cursor.previous();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ limitations under the License.
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('escape key', done => {
|
test('escape key', done => {
|
||||||
const closeSpy = sandbox.spy(element.$.dropdown, 'close');
|
const closeSpy = sandbox.spy(element, 'close');
|
||||||
MockInteractions.pressAndReleaseKeyOn(element, 27);
|
MockInteractions.pressAndReleaseKeyOn(element, 27);
|
||||||
flushAsynchronousOperations();
|
flushAsynchronousOperations();
|
||||||
assert.isTrue(closeSpy.called);
|
assert.isTrue(closeSpy.called);
|
||||||
@@ -87,24 +87,24 @@ limitations under the License.
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('down key', () => {
|
test('down key', () => {
|
||||||
element.hidden = true;
|
element.isHidden = true;
|
||||||
const nextSpy = sandbox.spy(element.$.cursor, 'next');
|
const nextSpy = sandbox.spy(element.$.cursor, 'next');
|
||||||
MockInteractions.pressAndReleaseKeyOn(element, 40);
|
MockInteractions.pressAndReleaseKeyOn(element, 40);
|
||||||
assert.isFalse(nextSpy.called);
|
assert.isFalse(nextSpy.called);
|
||||||
assert.equal(element.$.cursor.index, 0);
|
assert.equal(element.$.cursor.index, 0);
|
||||||
element.hidden = false;
|
element.isHidden = false;
|
||||||
MockInteractions.pressAndReleaseKeyOn(element, 40);
|
MockInteractions.pressAndReleaseKeyOn(element, 40);
|
||||||
assert.isTrue(nextSpy.called);
|
assert.isTrue(nextSpy.called);
|
||||||
assert.equal(element.$.cursor.index, 1);
|
assert.equal(element.$.cursor.index, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('up key', () => {
|
test('up key', () => {
|
||||||
element.hidden = true;
|
element.isHidden = true;
|
||||||
const prevSpy = sandbox.spy(element.$.cursor, 'previous');
|
const prevSpy = sandbox.spy(element.$.cursor, 'previous');
|
||||||
MockInteractions.pressAndReleaseKeyOn(element, 38);
|
MockInteractions.pressAndReleaseKeyOn(element, 38);
|
||||||
assert.isFalse(prevSpy.called);
|
assert.isFalse(prevSpy.called);
|
||||||
assert.equal(element.$.cursor.index, 0);
|
assert.equal(element.$.cursor.index, 0);
|
||||||
element.hidden = false;
|
element.isHidden = false;
|
||||||
element.$.cursor.setCursorAtIndex(1);
|
element.$.cursor.setCursorAtIndex(1);
|
||||||
assert.equal(element.$.cursor.index, 1);
|
assert.equal(element.$.cursor.index, 1);
|
||||||
MockInteractions.pressAndReleaseKeyOn(element, 38);
|
MockInteractions.pressAndReleaseKeyOn(element, 38);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ limitations under the License.
|
|||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<div>
|
||||||
<input
|
<input
|
||||||
id="input"
|
id="input"
|
||||||
class$="[[_computeClass(borderless)]]"
|
class$="[[_computeClass(borderless)]]"
|
||||||
@@ -49,15 +50,17 @@ limitations under the License.
|
|||||||
on-focus="_onInputFocus"
|
on-focus="_onInputFocus"
|
||||||
on-blur="_onInputBlur"
|
on-blur="_onInputBlur"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
<!-- This container is needed for Safari and Firefox -->
|
|
||||||
<div id="suggestionContainer">
|
|
||||||
<gr-autocomplete-dropdown
|
<gr-autocomplete-dropdown
|
||||||
|
vertical-align="top"
|
||||||
|
vertical-offset="20"
|
||||||
|
horizontal-align="auto"
|
||||||
id="suggestions"
|
id="suggestions"
|
||||||
on-item-selected="_handleItemSelect"
|
on-item-selected="_handleItemSelect"
|
||||||
on-keydown="_handleKeydown"
|
on-keydown="_handleKeydown"
|
||||||
suggestions="[[_suggestions]]"
|
suggestions="[[_suggestions]]"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
index="[[_index]]">
|
index="[[_index]]"
|
||||||
|
position-target="[[_inputElement]]">
|
||||||
</gr-autocomplete-dropdown>
|
</gr-autocomplete-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -185,6 +185,10 @@
|
|||||||
this._commit();
|
this._commit();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get _inputElement() {
|
||||||
|
return this.$.input;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the text of the input without triggering the suggestion dropdown.
|
* Set the text of the input without triggering the suggestion dropdown.
|
||||||
* @param {string} text The new text for the input.
|
* @param {string} text The new text for the input.
|
||||||
|
|||||||
@@ -67,12 +67,19 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div id="hiddenText"></div>
|
<div id="hiddenText"></div>
|
||||||
|
<!-- When the autocomplete is open, the span is moved at the end of
|
||||||
|
hiddenText in order to correctly position the dropdown. After being moved,
|
||||||
|
it is set as the positionTarget for the emojiSuggestions dropdown. -->
|
||||||
|
<span id="caratSpan"></span>
|
||||||
<gr-autocomplete-dropdown
|
<gr-autocomplete-dropdown
|
||||||
|
vertical-align="top"
|
||||||
|
horizontal-align="left"
|
||||||
|
dynamic-align
|
||||||
id="emojiSuggestions"
|
id="emojiSuggestions"
|
||||||
suggestions="[[_suggestions]]"
|
suggestions="[[_suggestions]]"
|
||||||
index="[[_index]]"
|
index="[[_index]]"
|
||||||
vertical-offset="[[_verticalOffset]]"
|
vertical-offset="[[_verticalOffset]]"
|
||||||
on-dropdown-closed="_resetAndFocus"
|
on-dropdown-closed="_resetEmojiDropdown"
|
||||||
on-item-selected="_handleEmojiSelect">
|
on-item-selected="_handleEmojiSelect">
|
||||||
</gr-autocomplete-dropdown>
|
</gr-autocomplete-dropdown>
|
||||||
<iron-autogrow-textarea
|
<iron-autogrow-textarea
|
||||||
|
|||||||
@@ -125,7 +125,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
closeDropdown() {
|
closeDropdown() {
|
||||||
return this.$.emojiSuggestions.close();
|
return this.$.emojiSuggestions.close();
|
||||||
},
|
},
|
||||||
@@ -148,10 +147,6 @@
|
|||||||
if (this._hideAutocomplete) { return; }
|
if (this._hideAutocomplete) { return; }
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this._resetAndFocus();
|
|
||||||
},
|
|
||||||
|
|
||||||
_resetAndFocus() {
|
|
||||||
this._resetEmojiDropdown();
|
this._resetEmojiDropdown();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -190,15 +185,19 @@
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Uses a hidden element with the same width and styling of the textarea and
|
* Uses a hidden element with the same width and styling of the textarea and
|
||||||
* the text up until the point of interest. Then the emoji selection
|
* the text up until the point of interest. Then caratSpan element is added
|
||||||
* element is added to the end so that they are correctly positioned by the
|
* to the end and is set to be the positionTarget for the dropdown. Together
|
||||||
* end of the last character entered.
|
* this allows the dropdown to appear near where the user is typing.
|
||||||
*/
|
*/
|
||||||
_updateCaratPosition() {
|
_updateCaratPosition() {
|
||||||
|
this._hideAutocomplete = false;
|
||||||
this.$.hiddenText.textContent = this.$.textarea.value.substr(0,
|
this.$.hiddenText.textContent = this.$.textarea.value.substr(0,
|
||||||
this.$.textarea.selectionStart);
|
this.$.textarea.selectionStart);
|
||||||
|
|
||||||
this.$.hiddenText.appendChild(this.$.emojiSuggestions);
|
const caratSpan = this.$.caratSpan;
|
||||||
|
this.$.hiddenText.appendChild(caratSpan);
|
||||||
|
this.$.emojiSuggestions.positionTarget = caratSpan;
|
||||||
|
this._openEmojiDropdown();
|
||||||
},
|
},
|
||||||
|
|
||||||
_getFontSize() {
|
_getFontSize() {
|
||||||
@@ -250,8 +249,6 @@
|
|||||||
// Otherwise open the dropdown and set the position to be just below the
|
// Otherwise open the dropdown and set the position to be just below the
|
||||||
// cursor.
|
// cursor.
|
||||||
} else if (this.$.emojiSuggestions.isHidden) {
|
} else if (this.$.emojiSuggestions.isHidden) {
|
||||||
this._hideAutocomplete = false;
|
|
||||||
this._openEmojiDropdown();
|
|
||||||
this._updateCaratPosition();
|
this._updateCaratPosition();
|
||||||
}
|
}
|
||||||
this.$.textarea.textarea.focus();
|
this.$.textarea.textarea.focus();
|
||||||
@@ -268,7 +265,7 @@
|
|||||||
suggestion.text = suggestion.value + ' ' + suggestion.match;
|
suggestion.text = suggestion.value + ' ' + suggestion.match;
|
||||||
suggestions.push(suggestion);
|
suggestions.push(suggestion);
|
||||||
}
|
}
|
||||||
this._suggestions = suggestions;
|
this.set('_suggestions', suggestions);
|
||||||
},
|
},
|
||||||
|
|
||||||
_determineSuggestions(emojiText) {
|
_determineSuggestions(emojiText) {
|
||||||
|
|||||||
@@ -171,11 +171,11 @@ limitations under the License.
|
|||||||
element.text = 'test';
|
element.text = 'test';
|
||||||
element._updateCaratPosition();
|
element._updateCaratPosition();
|
||||||
assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
|
assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
|
||||||
element.$.emojiSuggestions.outerHTML);
|
element.$.caratSpan.outerHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
|
test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
|
||||||
const resetSpy = sandbox.spy(element, '_resetAndFocus');
|
const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
|
||||||
element.$.emojiSuggestions.fire('dropdown-closed');
|
element.$.emojiSuggestions.fire('dropdown-closed');
|
||||||
assert.isTrue(resetSpy.called);
|
assert.isTrue(resetSpy.called);
|
||||||
});
|
});
|
||||||
@@ -190,10 +190,6 @@ limitations under the License.
|
|||||||
|
|
||||||
suite('keyboard shortcuts', () => {
|
suite('keyboard shortcuts', () => {
|
||||||
function setupDropdown(callback) {
|
function setupDropdown(callback) {
|
||||||
element.$.emojiSuggestions.addEventListener('open-complete', () => {
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
flushAsynchronousOperations();
|
|
||||||
MockInteractions.focus(element.$.textarea);
|
MockInteractions.focus(element.$.textarea);
|
||||||
element.$.textarea.selectionStart = 1;
|
element.$.textarea.selectionStart = 1;
|
||||||
element.$.textarea.selectionEnd = 1;
|
element.$.textarea.selectionEnd = 1;
|
||||||
@@ -201,55 +197,48 @@ limitations under the License.
|
|||||||
element.$.textarea.selectionStart = 1;
|
element.$.textarea.selectionStart = 1;
|
||||||
element.$.textarea.selectionEnd = 2;
|
element.$.textarea.selectionEnd = 2;
|
||||||
element.text = ':1';
|
element.text = ':1';
|
||||||
|
flushAsynchronousOperations();
|
||||||
}
|
}
|
||||||
|
|
||||||
test('escape key', done => {
|
test('escape key', () => {
|
||||||
const resestSpy = sandbox.spy(element, '_resetAndFocus');
|
const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
|
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
|
||||||
assert.isFalse(resestSpy.called);
|
assert.isFalse(resetSpy.called);
|
||||||
setupDropdown(() => {
|
setupDropdown();
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
|
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
|
||||||
assert.isTrue(resestSpy.called);
|
assert.isTrue(resetSpy.called);
|
||||||
assert.isFalse(!element.$.emojiSuggestions.isHidden);
|
assert.isFalse(!element.$.emojiSuggestions.isHidden);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('up key', done => {
|
test('up key', () => {
|
||||||
const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
|
const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
|
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
|
||||||
assert.isFalse(upSpy.called);
|
assert.isFalse(upSpy.called);
|
||||||
setupDropdown(() => {
|
setupDropdown();
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
|
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
|
||||||
assert.isTrue(upSpy.called);
|
assert.isTrue(upSpy.called);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('down key', done => {
|
test('down key', () => {
|
||||||
const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
|
const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
|
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
|
||||||
assert.isFalse(downSpy.called);
|
assert.isFalse(downSpy.called);
|
||||||
setupDropdown(() => {
|
setupDropdown();
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
|
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
|
||||||
assert.isTrue(downSpy.called);
|
assert.isTrue(downSpy.called);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('enter key', done => {
|
test('enter key', () => {
|
||||||
const enterSpy = sandbox.spy(element.$.emojiSuggestions,
|
const enterSpy = sandbox.spy(element.$.emojiSuggestions,
|
||||||
'getCursorTarget');
|
'getCursorTarget');
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
|
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
|
||||||
assert.isFalse(enterSpy.called);
|
assert.isFalse(enterSpy.called);
|
||||||
setupDropdown(() => {
|
setupDropdown();
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
|
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
|
||||||
assert.isTrue(enterSpy.called);
|
assert.isTrue(enterSpy.called);
|
||||||
flushAsynchronousOperations();
|
flushAsynchronousOperations();
|
||||||
// A space is automatically added at the end.
|
// A space is automatically added at the end.
|
||||||
assert.equal(element.text, '💯 ');
|
assert.equal(element.text, '💯 ');
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user