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">
|
||||
<template>
|
||||
<style include="shared-styles">
|
||||
/* This must be set here vs. the container component because in some cases
|
||||
the element is moved in the DOM to a base element and is no longer a
|
||||
child of its original parent. */
|
||||
:host(.fixed){
|
||||
position: fixed;
|
||||
:host {
|
||||
z-index: 100;
|
||||
}
|
||||
:host([is-hidden]) {
|
||||
display: none;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
@@ -49,29 +49,22 @@ limitations under the License.
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
|
||||
}
|
||||
</style>
|
||||
<iron-dropdown
|
||||
id="dropdown"
|
||||
allow-outside-scroll="true"
|
||||
vertical-align="top"
|
||||
horizontal-align="auto"
|
||||
vertical-offset="[[verticalOffset]]">
|
||||
<div
|
||||
class="dropdown-content"
|
||||
slot="dropdown-content"
|
||||
id="suggestions"
|
||||
role="listbox">
|
||||
<ul>
|
||||
<template is="dom-repeat" items="[[suggestions]]">
|
||||
<li data-index$="[[index]]"
|
||||
data-value$="[[item.dataValue]]"
|
||||
tabindex="-1"
|
||||
aria-label$="[[item.name]]"
|
||||
role="option"
|
||||
on-tap="_handleTapItem">[[item.text]]</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</iron-dropdown>
|
||||
<div
|
||||
class="dropdown-content"
|
||||
slot="dropdown-content"
|
||||
id="suggestions"
|
||||
role="listbox">
|
||||
<ul>
|
||||
<template is="dom-repeat" items="[[suggestions]]">
|
||||
<li data-index$="[[index]]"
|
||||
data-value$="[[item.dataValue]]"
|
||||
tabindex="-1"
|
||||
aria-label$="[[item.name]]"
|
||||
role="option"
|
||||
on-tap="_handleTapItem">[[item.text]]</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<gr-cursor-manager
|
||||
id="cursor"
|
||||
index="{{index}}"
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const AWAIT_MAX_ITERS = 10;
|
||||
const AWAIT_STEP = 5;
|
||||
|
||||
Polymer({
|
||||
is: 'gr-autocomplete-dropdown',
|
||||
|
||||
@@ -34,6 +31,10 @@
|
||||
|
||||
properties: {
|
||||
index: Number,
|
||||
isHidden: {
|
||||
value: true,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
verticalOffset: {
|
||||
type: Number,
|
||||
value: null,
|
||||
@@ -53,6 +54,7 @@
|
||||
},
|
||||
|
||||
behaviors: [
|
||||
Polymer.IronFitBehavior,
|
||||
Gerrit.KeyboardShortcutBehavior,
|
||||
],
|
||||
|
||||
@@ -65,46 +67,14 @@
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$.dropdown.close();
|
||||
this.isHidden = true;
|
||||
},
|
||||
|
||||
open() {
|
||||
this._open().then(() => {
|
||||
this._resetCursorStops();
|
||||
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;
|
||||
this.isHidden = false;
|
||||
this.refit();
|
||||
this._resetCursorStops();
|
||||
this._resetCursorIndex();
|
||||
},
|
||||
|
||||
getCurrentText() {
|
||||
@@ -112,7 +82,7 @@
|
||||
},
|
||||
|
||||
_handleUp(e) {
|
||||
if (!this.hidden) {
|
||||
if (!this.isHidden) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.cursorUp();
|
||||
@@ -120,7 +90,7 @@
|
||||
},
|
||||
|
||||
_handleDown(e) {
|
||||
if (!this.hidden) {
|
||||
if (!this.isHidden) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.cursorDown();
|
||||
@@ -128,13 +98,13 @@
|
||||
},
|
||||
|
||||
cursorDown() {
|
||||
if (!this.hidden) {
|
||||
if (!this.isHidden) {
|
||||
this.$.cursor.next();
|
||||
}
|
||||
},
|
||||
|
||||
cursorUp() {
|
||||
if (!this.hidden) {
|
||||
if (!this.isHidden) {
|
||||
this.$.cursor.previous();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -52,7 +52,7 @@ limitations under the License.
|
||||
});
|
||||
|
||||
test('escape key', done => {
|
||||
const closeSpy = sandbox.spy(element.$.dropdown, 'close');
|
||||
const closeSpy = sandbox.spy(element, 'close');
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 27);
|
||||
flushAsynchronousOperations();
|
||||
assert.isTrue(closeSpy.called);
|
||||
@@ -87,24 +87,24 @@ limitations under the License.
|
||||
});
|
||||
|
||||
test('down key', () => {
|
||||
element.hidden = true;
|
||||
element.isHidden = true;
|
||||
const nextSpy = sandbox.spy(element.$.cursor, 'next');
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 40);
|
||||
assert.isFalse(nextSpy.called);
|
||||
assert.equal(element.$.cursor.index, 0);
|
||||
element.hidden = false;
|
||||
element.isHidden = false;
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 40);
|
||||
assert.isTrue(nextSpy.called);
|
||||
assert.equal(element.$.cursor.index, 1);
|
||||
});
|
||||
|
||||
test('up key', () => {
|
||||
element.hidden = true;
|
||||
element.isHidden = true;
|
||||
const prevSpy = sandbox.spy(element.$.cursor, 'previous');
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 38);
|
||||
assert.isFalse(prevSpy.called);
|
||||
assert.equal(element.$.cursor.index, 0);
|
||||
element.hidden = false;
|
||||
element.isHidden = false;
|
||||
element.$.cursor.setCursorAtIndex(1);
|
||||
assert.equal(element.$.cursor.index, 1);
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 38);
|
||||
|
||||
@@ -38,26 +38,29 @@ limitations under the License.
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
<input
|
||||
id="input"
|
||||
class$="[[_computeClass(borderless)]]"
|
||||
is="iron-input"
|
||||
disabled$="[[disabled]]"
|
||||
bind-value="{{text}}"
|
||||
placeholder="[[placeholder]]"
|
||||
on-keydown="_handleKeydown"
|
||||
on-focus="_onInputFocus"
|
||||
on-blur="_onInputBlur"
|
||||
autocomplete="off"/>
|
||||
<!-- This container is needed for Safari and Firefox -->
|
||||
<div id="suggestionContainer">
|
||||
<div>
|
||||
<input
|
||||
id="input"
|
||||
class$="[[_computeClass(borderless)]]"
|
||||
is="iron-input"
|
||||
disabled$="[[disabled]]"
|
||||
bind-value="{{text}}"
|
||||
placeholder="[[placeholder]]"
|
||||
on-keydown="_handleKeydown"
|
||||
on-focus="_onInputFocus"
|
||||
on-blur="_onInputBlur"
|
||||
autocomplete="off"/>
|
||||
<gr-autocomplete-dropdown
|
||||
vertical-align="top"
|
||||
vertical-offset="20"
|
||||
horizontal-align="auto"
|
||||
id="suggestions"
|
||||
on-item-selected="_handleItemSelect"
|
||||
on-keydown="_handleKeydown"
|
||||
suggestions="[[_suggestions]]"
|
||||
role="listbox"
|
||||
index="[[_index]]">
|
||||
index="[[_index]]"
|
||||
position-target="[[_inputElement]]">
|
||||
</gr-autocomplete-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -185,6 +185,10 @@
|
||||
this._commit();
|
||||
},
|
||||
|
||||
get _inputElement() {
|
||||
return this.$.input;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the text of the input without triggering the suggestion dropdown.
|
||||
* @param {string} text The new text for the input.
|
||||
|
||||
@@ -67,12 +67,19 @@ limitations under the License.
|
||||
}
|
||||
</style>
|
||||
<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
|
||||
vertical-align="top"
|
||||
horizontal-align="left"
|
||||
dynamic-align
|
||||
id="emojiSuggestions"
|
||||
suggestions="[[_suggestions]]"
|
||||
index="[[_index]]"
|
||||
vertical-offset="[[_verticalOffset]]"
|
||||
on-dropdown-closed="_resetAndFocus"
|
||||
on-dropdown-closed="_resetEmojiDropdown"
|
||||
on-item-selected="_handleEmojiSelect">
|
||||
</gr-autocomplete-dropdown>
|
||||
<iron-autogrow-textarea
|
||||
|
||||
@@ -125,7 +125,6 @@
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
closeDropdown() {
|
||||
return this.$.emojiSuggestions.close();
|
||||
},
|
||||
@@ -148,10 +147,6 @@
|
||||
if (this._hideAutocomplete) { return; }
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._resetAndFocus();
|
||||
},
|
||||
|
||||
_resetAndFocus() {
|
||||
this._resetEmojiDropdown();
|
||||
},
|
||||
|
||||
@@ -190,15 +185,19 @@
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
* element is added to the end so that they are correctly positioned by the
|
||||
* end of the last character entered.
|
||||
* the text up until the point of interest. Then caratSpan element is added
|
||||
* to the end and is set to be the positionTarget for the dropdown. Together
|
||||
* this allows the dropdown to appear near where the user is typing.
|
||||
*/
|
||||
_updateCaratPosition() {
|
||||
this._hideAutocomplete = false;
|
||||
this.$.hiddenText.textContent = this.$.textarea.value.substr(0,
|
||||
this.$.textarea.selectionStart);
|
||||
|
||||
this.$.hiddenText.appendChild(this.$.emojiSuggestions);
|
||||
const caratSpan = this.$.caratSpan;
|
||||
this.$.hiddenText.appendChild(caratSpan);
|
||||
this.$.emojiSuggestions.positionTarget = caratSpan;
|
||||
this._openEmojiDropdown();
|
||||
},
|
||||
|
||||
_getFontSize() {
|
||||
@@ -250,8 +249,6 @@
|
||||
// Otherwise open the dropdown and set the position to be just below the
|
||||
// cursor.
|
||||
} else if (this.$.emojiSuggestions.isHidden) {
|
||||
this._hideAutocomplete = false;
|
||||
this._openEmojiDropdown();
|
||||
this._updateCaratPosition();
|
||||
}
|
||||
this.$.textarea.textarea.focus();
|
||||
@@ -268,7 +265,7 @@
|
||||
suggestion.text = suggestion.value + ' ' + suggestion.match;
|
||||
suggestions.push(suggestion);
|
||||
}
|
||||
this._suggestions = suggestions;
|
||||
this.set('_suggestions', suggestions);
|
||||
},
|
||||
|
||||
_determineSuggestions(emojiText) {
|
||||
|
||||
@@ -171,11 +171,11 @@ limitations under the License.
|
||||
element.text = 'test';
|
||||
element._updateCaratPosition();
|
||||
assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
|
||||
element.$.emojiSuggestions.outerHTML);
|
||||
element.$.caratSpan.outerHTML);
|
||||
});
|
||||
|
||||
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');
|
||||
assert.isTrue(resetSpy.called);
|
||||
});
|
||||
@@ -190,10 +190,6 @@ limitations under the License.
|
||||
|
||||
suite('keyboard shortcuts', () => {
|
||||
function setupDropdown(callback) {
|
||||
element.$.emojiSuggestions.addEventListener('open-complete', () => {
|
||||
callback();
|
||||
});
|
||||
flushAsynchronousOperations();
|
||||
MockInteractions.focus(element.$.textarea);
|
||||
element.$.textarea.selectionStart = 1;
|
||||
element.$.textarea.selectionEnd = 1;
|
||||
@@ -201,55 +197,48 @@ limitations under the License.
|
||||
element.$.textarea.selectionStart = 1;
|
||||
element.$.textarea.selectionEnd = 2;
|
||||
element.text = ':1';
|
||||
flushAsynchronousOperations();
|
||||
}
|
||||
|
||||
test('escape key', done => {
|
||||
const resestSpy = sandbox.spy(element, '_resetAndFocus');
|
||||
test('escape key', () => {
|
||||
const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
|
||||
assert.isFalse(resestSpy.called);
|
||||
setupDropdown(() => {
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
|
||||
assert.isTrue(resestSpy.called);
|
||||
assert.isFalse(!element.$.emojiSuggestions.isHidden);
|
||||
done();
|
||||
});
|
||||
assert.isFalse(resetSpy.called);
|
||||
setupDropdown();
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
|
||||
assert.isTrue(resetSpy.called);
|
||||
assert.isFalse(!element.$.emojiSuggestions.isHidden);
|
||||
});
|
||||
|
||||
test('up key', done => {
|
||||
test('up key', () => {
|
||||
const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
|
||||
assert.isFalse(upSpy.called);
|
||||
setupDropdown(() => {
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
|
||||
assert.isTrue(upSpy.called);
|
||||
done();
|
||||
});
|
||||
setupDropdown();
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
|
||||
assert.isTrue(upSpy.called);
|
||||
});
|
||||
|
||||
test('down key', done => {
|
||||
test('down key', () => {
|
||||
const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
|
||||
assert.isFalse(downSpy.called);
|
||||
setupDropdown(() => {
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
|
||||
assert.isTrue(downSpy.called);
|
||||
done();
|
||||
});
|
||||
setupDropdown();
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
|
||||
assert.isTrue(downSpy.called);
|
||||
});
|
||||
|
||||
test('enter key', done => {
|
||||
test('enter key', () => {
|
||||
const enterSpy = sandbox.spy(element.$.emojiSuggestions,
|
||||
'getCursorTarget');
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
|
||||
assert.isFalse(enterSpy.called);
|
||||
setupDropdown(() => {
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
|
||||
assert.isTrue(enterSpy.called);
|
||||
flushAsynchronousOperations();
|
||||
// A space is automatically added at the end.
|
||||
assert.equal(element.text, '💯 ');
|
||||
done();
|
||||
});
|
||||
setupDropdown();
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
|
||||
assert.isTrue(enterSpy.called);
|
||||
flushAsynchronousOperations();
|
||||
// A space is automatically added at the end.
|
||||
assert.equal(element.text, '💯 ');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user