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">▼</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',