Introduce gr-dropdown-list element
This element will replace some existing gr-select (beginning with the patch range selector) and most likely the file dropdown in gr-diff-view. On wider/desktop screens, a custom dropdown appears, and on smaller/mobile devices, a native select appears. This element handles determining which to display and when. This element requires an array of 'items'. Items must contain a value and text. They can also include the following: - bottomText: (a second, more subtle row of text) - date: (a date to get displayed/formatted on the top row/right side) - triggerText: The text to display on the dropdown trigger when the particular option is selected. - mobileText: the text to use as the option text on mobile devices. When mobileText or triggerText are not set, they fall back to text. Change-Id: Ie8842d8833a1ecf7c260b88c389d3f3a4f93944b
This commit is contained in:
@@ -20,6 +20,7 @@ limitations under the License.
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -130,6 +130,7 @@ limitations under the License.
|
||||
'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',
|
||||
|
||||
Reference in New Issue
Block a user