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', () => {