Update gr-editable-label to new design

Uses a gr-dropdown and paper-input.

Change-Id: I34bdfecc5fbe53432aa4175cdc6151d8f85b2ac7
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 22126fd..595559d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -222,6 +222,7 @@
           </template>
           <template is="dom-if" if="[[!change.topic]]">
             <gr-editable-label
+                label-text="Add a topic"
                 value="[[change.topic]]"
                 placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
                 read-only="[[_topicReadOnly]]"
@@ -246,6 +247,7 @@
               </gr-linked-chip>
             </template>
             <gr-editable-label
+                label-text="Add a hashtag"
                 value="{{_newHashtag}}"
                 placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]"
                 read-only="[[_hashtagReadOnly]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index 5c2422d..252ea71 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -140,6 +140,7 @@
             <gr-editable-label
                 id="descriptionLabel"
                 class="descriptionLabel"
+                label-text="Add patchset description"
                 value="[[_computePatchSetDescription(change, patchRange.patchNum)]]"
                 placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
                 read-only="[[_descriptionReadOnly]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
index 08ecf6f..79d69f5 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -13,9 +13,11 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <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/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="../../../bower_components/paper-input/paper-input.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-editable-label">
@@ -44,17 +46,55 @@
         cursor: pointer;
         text-decoration: underline;
       }
+      #dropdown {
+        box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
+      }
+      .inputContainer {
+        padding: .8em;
+        background-color: #fff;
+      }
+      .buttons {
+        display: flex;
+        padding-top: 1.2em;
+        justify-content: flex-end;
+        width: 100%;
+      }
+      .buttons gr-button {
+        margin-left: .5em;
+      }
+      paper-input {
+        --paper-input-container: {
+          padding: 0;
+          min-width: 15em;
+        }
+        --paper-input-container-input: {
+          font-size: 1em;
+        }
+      }
     </style>
-    <input
-        is="iron-input"
-        id="input"
-        hidden$="[[!editing]]"
-        bind-value="{{_inputText}}">
-    <label
-        hidden$="[[editing]]"
-        class$="[[_computeLabelClass(readOnly, value, placeholder)]]"
-        title$="[[_computeLabel(value, placeholder)]]"
-        on-tap="_open">[[_computeLabel(value, placeholder)]]</label>
+      <label
+          class$="[[_computeLabelClass(readOnly, value, placeholder)]]"
+          title$="[[_computeLabel(value, placeholder)]]"
+          on-tap="_showDropdown">[[_computeLabel(value, placeholder)]]</label>
+      <iron-dropdown id="dropdown"
+          vertical-align="auto"
+          horizontal-align="auto"
+          vertical-offset="[[_verticalOffset]]"
+          allow-outside-scroll="true"
+          on-iron-overlay-canceled="_cancel">
+        <div class="dropdown-content" slot="dropdown-content">
+          <div class="inputContainer">
+            <paper-input
+                id="input"
+                label="[[labelText]]"
+                value="{{_inputText}}"></paper-input>
+            <div class="buttons">
+              <gr-button id="cancelBtn" on-tap="_cancel">cancel</gr-button>
+              <gr-button id="saveBtn" on-tap="_save">save</gr-button>
+            </div>
+          </div>
+        </div>
+    </iron-dropdown>
   </template>
   <script src="gr-editable-label.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index a78e94c..5f970b8 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -14,6 +14,9 @@
 (function() {
   'use strict';
 
+  const AWAIT_MAX_ITERS = 10;
+  const AWAIT_STEP = 5;
+
   Polymer({
     is: 'gr-editable-label',
 
@@ -24,6 +27,7 @@
      */
 
     properties: {
+      labelText: String,
       editing: {
         type: Boolean,
         value: false,
@@ -43,6 +47,14 @@
         value: false,
       },
       _inputText: String,
+      // This is used to push the iron-input element up on the page, so
+      // the input is placed in approximately the same position as the
+      // trigger.
+      _verticalOffset: {
+        type: Number,
+        readOnly: true,
+        value: 30,
+      },
     },
 
     behaviors: [
@@ -69,21 +81,51 @@
       return value;
     },
 
-    _open() {
+    _showDropdown() {
       if (this.readOnly || this.editing) { return; }
+      this._open().then(() => {
+        this.$.input.$.input.focus();
+        if (!this.$.input.value) { return; }
+        this.$.input.$.input.setSelectionRange(0, this.$.input.value.length);
+      });
+    },
 
+    _open(...args) {
+      this.$.dropdown.open();
       this._inputText = this.value;
       this.editing = true;
 
-      this.async(() => {
-        this.$.input.focus();
-        this.$.input.setSelectionRange(0, this.$.input.value.length);
+      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);
+    },
+
+    _id() {
+      return this.getAttribute('id') || 'global';
+    },
+
     _save() {
       if (!this.editing) { return; }
-
+      this.$.dropdown.close();
       this.value = this._inputText;
       this.editing = false;
       this.fire('changed', this.value);
@@ -91,7 +133,7 @@
 
     _cancel() {
       if (!this.editing) { return; }
-
+      this.$.dropdown.close();
       this.editing = false;
       this._inputText = this.value;
     },
@@ -104,7 +146,7 @@
     _handleEnter(e) {
       e = this.getKeyboardEvent(e);
       const target = Polymer.dom(e).rootTarget;
-      if (target === this.$.input) {
+      if (target === this.$.input.$.input) {
         e.preventDefault();
         this._save();
       }
@@ -118,7 +160,7 @@
     _handleEsc(e) {
       e = this.getKeyboardEvent(e);
       const target = Polymer.dom(e).rootTarget;
-      if (target === this.$.input) {
+      if (target === this.$.input.$.input) {
         e.preventDefault();
         this._cancel();
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
index dc1a5aa..74f1bdd 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -54,7 +54,7 @@
     setup(() => {
       element = fixture('basic');
 
-      input = element.$$('input');
+      input = element.$.input.$.input;
       label = element.$$('label');
       sandbox = sinon.sandbox.create();
     });
@@ -64,33 +64,30 @@
     });
 
     test('element render', () => {
-      // The input is hidden and the label is visible:
-      assert.isNotNull(input.getAttribute('hidden'));
-      assert.isNull(label.getAttribute('hidden'));
-
+      // The dropdown is closed and the label is visible:
+      assert.isFalse(element.$.dropdown.opened);
       assert.isTrue(label.classList.contains('editable'));
-
       assert.equal(label.textContent, 'value text');
 
       MockInteractions.tap(label);
 
       Polymer.dom.flush();
 
-      // The input is visible and the label is hidden:
-      assert.isNull(input.getAttribute('hidden'));
-      assert.isNotNull(label.getAttribute('hidden'));
-
+      // The dropdown is open (which covers up the label):
+      assert.isTrue(element.$.dropdown.opened);
       assert.equal(input.value, 'value text');
     });
 
     test('edit value', done => {
       const editedStub = sandbox.stub();
       element.addEventListener('changed', editedStub);
+      assert.isFalse(element.editing);
 
       MockInteractions.tap(label);
 
       Polymer.dom.flush();
 
+      assert.isTrue(element.editing);
       element._inputText = 'new text';
 
       assert.isFalse(editedStub.called);
@@ -98,38 +95,111 @@
       element.async(() => {
         assert.isTrue(editedStub.called);
         assert.equal(input.value, 'new text');
+        assert.isFalse(element.editing);
         done();
       });
 
       // Press enter:
       MockInteractions.keyDownOn(input, 13);
     });
-  });
 
-  suite('gr-editable-label read-only tests', () => {
-    let element;
-    let input;
-    let label;
-
-    setup(() => {
-      element = fixture('read-only');
-
-      input = element.$$('input');
-      label = element.$$('label');
-    });
-
-    test('disallows edit when read-only', () => {
-      // The input is hidden and the label is visible:
-      assert.isNotNull(input.getAttribute('hidden'));
-      assert.isNull(label.getAttribute('hidden'));
+    test('save button', done => {
+      const editedStub = sandbox.stub();
+      element.addEventListener('changed', editedStub);
+      assert.isFalse(element.editing);
 
       MockInteractions.tap(label);
 
       Polymer.dom.flush();
 
-      // The input is still hidden and the label is still visible:
-      assert.isNotNull(input.getAttribute('hidden'));
-      assert.isNull(label.getAttribute('hidden'));
+      assert.isTrue(element.editing);
+      element._inputText = 'new text';
+
+      assert.isFalse(editedStub.called);
+
+      element.async(() => {
+        assert.isTrue(editedStub.called);
+        assert.equal(input.value, 'new text');
+        assert.isFalse(element.editing);
+        done();
+      });
+
+      // Press enter:
+      MockInteractions.tap(element.$.saveBtn, 13);
+    });
+
+
+    test('edit and then escape key', done => {
+      const editedStub = sandbox.stub();
+      element.addEventListener('changed', editedStub);
+      assert.isFalse(element.editing);
+
+      MockInteractions.tap(label);
+
+      Polymer.dom.flush();
+
+      assert.isTrue(element.editing);
+      element._inputText = 'new text';
+
+      assert.isFalse(editedStub.called);
+
+      element.async(() => {
+        assert.isFalse(editedStub.called);
+        // Text changes sould be discarded.
+        assert.equal(input.value, 'value text');
+        assert.isFalse(element.editing);
+        done();
+      });
+
+      // Press escape:
+      MockInteractions.keyDownOn(input, 27);
+    });
+
+    test('cancel button', done => {
+      const editedStub = sandbox.stub();
+      element.addEventListener('changed', editedStub);
+      assert.isFalse(element.editing);
+
+      MockInteractions.tap(label);
+
+      Polymer.dom.flush();
+
+      assert.isTrue(element.editing);
+      element._inputText = 'new text';
+
+      assert.isFalse(editedStub.called);
+
+      element.async(() => {
+        assert.isFalse(editedStub.called);
+        // Text changes sould be discarded.
+        assert.equal(input.value, 'value text');
+        assert.isFalse(element.editing);
+        done();
+      });
+
+      // Press escape:
+      MockInteractions.tap(element.$.cancelBtn);
+    });
+  });
+
+  suite('gr-editable-label read-only tests', () => {
+    let element;
+    let label;
+
+    setup(() => {
+      element = fixture('read-only');
+      label = element.$$('label');
+    });
+
+    test('disallows edit when read-only', () => {
+      // The dropdown is closed.
+      assert.isFalse(element.$.dropdown.opened);
+      MockInteractions.tap(label);
+
+      Polymer.dom.flush();
+
+      // The dropdown is still closed.
+      assert.isFalse(element.$.dropdown.opened);
     });
 
     test('label is not marked as editable', () => {