Merge "Introduce gr-dropdown-list element"
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index ab91fc5..21552d9 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -20,6 +20,7 @@
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <script src="../../../bower_components/moment/moment.js"></script>
+<script src="../../../scripts/util.js"></script>
 
 <dom-module id="gr-date-formatter">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
new file mode 100644
index 0000000..59f63fa
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -0,0 +1,174 @@
+<!--
+Copyright (C) 2017 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-dropdown/iron-dropdown.html">
+<link rel="import" href="../../../bower_components/paper-item/paper-item.html">
+<link rel="import" href="../../../bower_components/paper-listbox/paper-listbox.html">
+
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-select/gr-select.html">
+
+
+<dom-module id="gr-dropdown-list">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: inline-block;
+      }
+      #trigger {
+        -moz-user-select: text;
+        -ms-user-select: text;
+        -webkit-user-select: text;
+        user-select: text;
+      }
+      .downArrow {
+        display: inline-block;
+        font-size: .6em;
+        user-select: none;
+        vertical-align: middle;
+      }
+      .dropdown-trigger {
+        cursor: pointer;
+        padding: 0;
+      }
+      .dropdown-content {
+        background-color: #fff;
+        box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
+        max-height: 70vh;
+        margin-top: 1.5em;
+        width: 266px;
+      }
+      paper-listbox {
+        --paper-listbox: {
+          padding: 0;
+        }
+      }
+      paper-item {
+        cursor: pointer;
+        flex-direction: column;
+        --paper-item: {
+          min-height: 0;
+          padding: 10px 16px;
+        }
+        --paper-item-selected: {
+          background-color: rgba(161,194,250,.12);
+        }
+        --paper-item-focused-before: {
+          background-color: #f2f2f2;
+        }
+        --paper-item-focused: {
+          background-color: #f2f2f2;
+        }
+      }
+      paper-item:not(:last-of-type) {
+        border-bottom: 1px solid #ddd;
+      }
+      gr-button {
+        color: black;
+        font: inherit;
+        padding: .3em 0;
+        text-decoration: none;
+      }
+      .bottomContent {
+        color: rgba(0,0,0,.54);
+        font-size: .85em;
+        line-height: 16px;
+      }
+      .bottomContent,
+      .topContent {
+        display: flex;
+        line-height: 16px;
+        justify-content: space-between;
+        flex-direction: row;
+        width: 100%;
+      }
+      gr-date-formatter {
+        color: rgba(0,0,0,.54);
+        margin-left: 2em;
+      }
+      gr-select {
+        display: none;
+      }
+      @media only screen and (max-width: 50em) {
+        gr-select {
+          display: inline;
+        }
+        gr-button,
+        iron-dropdown {
+          display: none;
+        }
+        select {
+          max-width: 5.25em;
+        }
+      }
+    </style>
+    <gr-button
+        link
+        id="trigger"
+        class="dropdown-trigger"
+        on-tap="_showDropdownTapHandler">
+      <span>[[text]]</span>
+      <span
+          class="downArrow"
+          on-tap="_showDropdownTapHandler">&#9660;</span>
+    </gr-button>
+    <iron-dropdown
+        id="dropdown"
+        vertical-align="top"
+        allow-outside-scroll="true">
+      <paper-listbox
+          class="dropdown-content"
+          slot="dropdown-content"
+          attr-for-selected="value"
+          on-tap="_handleDropdownTap"
+          selected="{{value}}">
+        <template is="dom-repeat" items="[[items]]">
+            <paper-item
+                disabled="[[item.disabled]]"
+                value="[[item.value]]">
+              <div class="topContent">
+                <div>[[item.text]]</div>
+                <template is="dom-if" if="[[item.date]]">
+                    <gr-date-formatter
+                        date-str="[[item.date]]"></gr-date-formatter>
+                </template>
+              </div>
+              <template is="dom-if" if="[[item.bottomText]]">
+                <div class="bottomContent">
+                  <div>[[item.bottomText]]</div>
+                </div>
+              </template>
+          </paper-item>
+          </template>
+      </paper-listbox>
+    </iron-dropdown>
+    <gr-select bind-value="{{value}}">
+      <select>
+        <template is="dom-repeat" items="[[items]]">
+          <option
+              disabled$="[[item.disabled]]"
+              value="[[item.value]]">
+            [[_computeMobileText(item)]]
+          </option>
+        </template>
+      </select>
+    </gr-select>
+  </template>
+  <script src="gr-dropdown-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
new file mode 100644
index 0000000..27c6ba8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -0,0 +1,102 @@
+// Copyright (C) 2017 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';
+
+  /**
+   * fired when the selected value of the dropdown changes
+   *
+   * @event {change}
+   */
+
+  const Defs = {};
+
+  /**
+   * Requred values are text and value. mobileText and triggerText will
+   * fall back to text if not provided.
+   *
+   * If bottomText is not provided, nothing will display on the second
+   * line.
+   *
+   * If date is not provided, nothing will be displayed in its place.
+   *
+   * @typedef {{
+   *    text: string,
+   *    value: (string|number),
+   *    bottomText: (string|undefined),
+   *    triggerText: (string|undefined),
+   *    mobileText: (string|undefined),
+   *    date: (!Date|undefined),
+   * }}
+   */
+  Defs.item;
+
+  Polymer({
+    is: 'gr-dropdown-list',
+
+    properties: {
+      /** @type {!Array<!Defs.item>} */
+      items: Object,
+      text: String,
+      value: {
+        type: String,
+        notify: true,
+      },
+    },
+
+    observers: [
+      '_handleValueChange(value, items)',
+    ],
+
+    /**
+     * Handle a click on the iron-dropdown element.
+     * @param {!Event} e
+     */
+    _handleDropdownTap(e) {
+      // async is needed so that that the click event is fired before the
+      // dropdown closes (This was a bug for touch devices).
+      this.async(() => {
+        this.$.dropdown.close();
+      }, 1);
+    },
+
+    /**
+     * Handle a click on the button to open the dropdown.
+     * @param {!Event} e
+     */
+    _showDropdownTapHandler(e) {
+      this._open();
+    },
+
+    /**
+     * Open the dropdown.
+     */
+    _open() {
+      this.$.dropdown.open();
+    },
+
+    _computeMobileText(item) {
+      return item.mobileText ? item.mobileText : item.text;
+    },
+
+    _handleValueChange(value, items) {
+      if (!value) { return; }
+      const selectedObj = items.find(item => {
+        return item.value + '' === value + '';
+      });
+      this.text = selectedObj.triggerText? selectedObj.triggerText :
+          selectedObj.text;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
new file mode 100644
index 0000000..d3c6d83
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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-dropdown-list</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-dropdown-list.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-dropdown-list></gr-dropdown-list>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-dropdown-list tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getConfig() { return Promise.resolve({}); },
+      });
+      element = fixture('basic');
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('tap on trigger opens menu', () => {
+      sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
+      assert.isFalse(element.$.dropdown.opened);
+      MockInteractions.tap(element.$.trigger);
+      assert.isTrue(element.$.dropdown.opened);
+    });
+
+    test('_computeMobileText', () => {
+      const item = {
+        value: 1,
+        text: 'text',
+      };
+      assert.equal(element._computeMobileText(item), item.text);
+      item.mobileText = 'mobile text';
+      assert.equal(element._computeMobileText(item), item.mobileText);
+    });
+
+    test('options are selected and laid out correctly', () => {
+      element.value = 2;
+      element.items = [
+        {
+          value: 1,
+          text: 'Top Text 1',
+        },
+        {
+          value: 2,
+          bottomText: 'Bottom Text 2',
+          triggerText: 'Button Text 2',
+          text: 'Top Text 2',
+          mobileText: 'Mobile Text 2',
+        },
+        {
+          value: 3,
+          disabled: true,
+          bottomText: 'Bottom Text 3',
+          triggerText: 'Button Text 3',
+          date: '2017-08-18 23:11:42.569000000',
+          text: 'Top Text 3',
+          mobileText: 'Mobile Text 3',
+        },
+      ];
+      assert.equal(element.$$('paper-listbox').selected, element.value);
+      assert.equal(element.text, 'Button Text 2');
+      flushAsynchronousOperations();
+      const items = Polymer.dom(element.root).querySelectorAll('paper-item');
+      const mobileItems = Polymer.dom(element.root).querySelectorAll('option');
+      assert.equal(items.length, 3);
+      assert.equal(mobileItems.length, 3);
+
+      // First Item
+      // The first item should be disabled, has no bottom text, and no date.
+      assert.isFalse(!!items[0].disabled);
+      assert.isFalse(mobileItems[0].disabled);
+      assert.isFalse(items[0].classList.contains('iron-selected'));
+      assert.isFalse(mobileItems[0].selected);
+
+      assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter'));
+      assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent'));
+      assert.equal(items[0].value, element.items[0].value);
+      assert.equal(mobileItems[0].value, element.items[0].value);
+      assert.equal(Polymer.dom(items[0]).querySelector('.topContent div')
+          .innerText, element.items[0].text);
+
+      // Since no mobile specific text, it should fall back to text.
+      assert.equal(mobileItems[0].text, element.items[0].text);
+
+
+      // Second Item
+      // The second item should have top text, bottom text, and no date.
+      assert.isFalse(!!items[1].disabled);
+      assert.isFalse(mobileItems[1].disabled);
+      assert.isTrue(items[1].classList.contains('iron-selected'));
+      assert.isTrue(mobileItems[1].selected);
+
+      assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter'));
+      assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent'));
+      assert.equal(items[1].value, element.items[1].value);
+      assert.equal(mobileItems[1].value, element.items[1].value);
+      assert.equal(Polymer.dom(items[1]).querySelector('.topContent div')
+          .innerText, element.items[1].text);
+
+      // Since there is mobile specific text, it should that.
+      assert.equal(mobileItems[1].text, element.items[1].mobileText);
+
+      // Since this item is selected, and it has triggerText defined, that
+      // should be used.
+      assert.equal(element.text, element.items[1].triggerText);
+
+      // Third item
+      // The third item should be disabled, and have a date, and bottom content.
+      assert.isTrue(!!items[2].disabled);
+      assert.isTrue(mobileItems[2].disabled);
+      assert.isFalse(items[2].classList.contains('iron-selected'));
+      assert.isFalse(mobileItems[2].selected);
+
+      assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter'));
+      assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent'));
+      assert.equal(items[2].value, element.items[2].value);
+      assert.equal(mobileItems[2].value, element.items[2].value);
+      assert.equal(Polymer.dom(items[2]).querySelector('.topContent div')
+          .innerText, element.items[2].text);
+
+      // Since there is mobile specific text, it should that.
+      assert.equal(mobileItems[2].text, element.items[2].mobileText);
+
+      // Select a new item.
+      MockInteractions.tap(items[0]);
+      flushAsynchronousOperations();
+      assert.equal(element.value, 1);
+      assert.isTrue(items[0].classList.contains('iron-selected'));
+      assert.isTrue(mobileItems[0].selected);
+
+      // Since no triggerText, the fallback is used.
+      assert.equal(element.text, element.items[0].text);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 2dba4fc..25a053a 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -130,6 +130,7 @@
     'shared/gr-cursor-manager/gr-cursor-manager_test.html',
     'shared/gr-date-formatter/gr-date-formatter_test.html',
     'shared/gr-download-commands/gr-download-commands_test.html',
+    'shared/gr-dropdown-list/gr-dropdown-list_test.html',
     'shared/gr-editable-content/gr-editable-content_test.html',
     'shared/gr-editable-label/gr-editable-label_test.html',
     'shared/gr-formatted-text/gr-formatted-text_test.html',