Commit 584a22f4 authored by Gavin Williams's avatar Gavin Williams Committed by Commit Bot

Create print preview dropdown element

- Use iron-dropdown instead of native <select> to support displaying
  printer status icons inside the dropdown. Using iron-dropdown is a
  temporary workaround and will need to be replaced with another
  dropdown element in the future.

Bug: 1059607
Change-Id: I7598600ebb21e7d604ba416eb0a8938bd45e584c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2177274
Commit-Queue: Gavin Williams <gavinwill@chromium.org>
Reviewed-by: default avatarRebekah Potter <rbpotter@chromium.org>
Reviewed-by: default avatarBailey Berro <baileyberro@chromium.org>
Cr-Commit-Position: refs/heads/master@{#770830}
parent a398ea7d
......@@ -49,6 +49,9 @@
<include name="IDR_PRINT_PREVIEW_UI_DESTINATION_SELECT_CROS_JS"
file="${root_gen_dir}/chrome/browser/resources/print_preview/ui/destination_select_cros.js"
use_base_dir="false" compress="false" type="BINDATA"/>
<include name="IDR_PRINT_PREVIEW_UI_PRINT_PREVIEW_DESTINATION_DROPDOWN_CROS_JS"
file="${root_gen_dir}/chrome/browser/resources/print_preview/ui/destination_dropdown_cros.js"
use_base_dir="false" compress="false" type="BINDATA"/>
</if>
<include name="IDR_PRINT_PREVIEW_UI_DESTINATION_SELECT_CSS_JS"
file="${root_gen_dir}/chrome/browser/resources/print_preview/ui/destination_select_css.js"
......
......@@ -53,6 +53,7 @@ js_type_check("closure_compile_module") {
if (is_chromeos) {
deps += [
":destination_dropdown_cros",
":destination_select_cros",
":pin_settings",
]
......@@ -160,6 +161,7 @@ js_library("destination_settings") {
if (is_chromeos) {
js_library("destination_select_cros") {
deps = [
":destination_dropdown_cros",
":select_behavior",
"..:print_preview_utils",
"../data:destination",
......@@ -178,6 +180,15 @@ if (is_chromeos) {
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
}
js_library("destination_dropdown_cros") {
deps = [
"..:print_preview_utils",
"//third_party/polymer/v3_0/components-chromium/iron-dropdown:iron-dropdown",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/cr_elements/cr_input:cr_input.m",
]
}
} else {
js_library("destination_select") {
deps = [
......@@ -544,6 +555,7 @@ html_to_js("web_components") {
]
if (is_chromeos) {
js_files += [
"destination_dropdown_cros.js",
"destination_select_cros.js",
"pin_settings.js",
]
......
<style include="cr-shared-style cr-hidden-style md-select">
:host([opened_]) cr-input {
--cr-input-border-radius: 4px 4px 0 0;
}
iron-dropdown,
cr-input {
width: var(--md-select-width);
}
cr-input {
--cr-input-padding-start: 8px;
}
iron-dropdown {
max-height: 270px;
}
iron-dropdown [slot='dropdown-content'] {
background-color: white;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 6px var(--google-grey-600);
min-width: var(--md-select-width);
padding: 8px 0;
}
#input-overlay {
border-radius: 4px;
height: 100%;
left: 0;
overflow: hidden;
pointer-events: none;
position: absolute;
top: 0;
width: 100%;
}
#dropdown-icon {
--iron-icon-height: 20px;
--iron-icon-width: 20px;
margin-top: -10px;
padding-inline-end: 6px;
position: absolute;
right: 0;
top: 50%;
}
:host-context([dir='rtl']) #dropdown-icon {
left: 0;
right: unset;
}
cr-input:focus-within #dropdown-icon {
--iron-icon-fill-color: var(--google-blue-600);
}
#input-box {
height: 100%;
left: 0;
pointer-events: none;
top: 0;
width: 100%;
}
#dropdown-box {
pointer-events: initial;
width: 100%;
}
.list-item {
background: none;
border: none;
box-sizing: border-box;
color: var(--google-grey-600);
font: inherit;
min-height: 32px;
padding: 0 8px;
text-align: start;
width: 100%;
}
.list-item[selected_] {
background-color: rgba(0, 0, 0, .04);
outline: none;
}
.list-item:active {
background-color: rgba(0, 0, 0, .12);
outline: none;
}
</style>
<cr-input id="dropdownInput" on-keydown="onKeyDown_"
value="[[value.displayName]]" readonly>
<div id="input-overlay" slot="suffix">
<div id="input-box">
<iron-icon id="dropdown-icon" icon="cr:arrow-drop-down"></iron-icon>
</div>
<div id="dropdown-box">
<iron-dropdown horizontal-align="left" vertical-align="top"
vertical-offset="0" no-cancel-on-outside-click
no-cancel-on-esc-key>
<div slot="dropdown-content">
<template is="dom-repeat" items="[[itemList]]">
<button class="list-item" on-click="onSelect_" tabindex="-1"
value="[[item.key]]">
[[item.displayName]]
</button>
</template>
</div>
</iron-dropdown>
</div>
</div>
</cr-input>
\ No newline at end of file
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import 'chrome://resources/cr_elements/hidden_style_css.m.js';
import 'chrome://resources/cr_elements/md_select_css.m.js';
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
// TODO(gavinwill): Remove iron-dropdown dependency https://crbug.com/1082587.
import 'chrome://resources/polymer/v3_0/iron-dropdown/iron-dropdown.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {Destination} from '../data/destination.js';
Polymer({
is: 'print-preview-destination-dropdown-cros',
_template: html`{__html_template__}`,
properties: {
/** @type {!Destination} */
value: Object,
/** @type {!Array<!Destination>} */
itemList: {
type: Array,
observer: 'enqueueDropdownRefit_',
},
},
listeners: {
'mousemove': 'onMouseMove_',
},
/** @override */
attached() {
this.pointerDownListener_ = event => this.onPointerDown_(event);
document.addEventListener('pointerdown', this.pointerDownListener_);
},
/** @override */
detached() {
document.removeEventListener('pointerdown', this.pointerDownListener_);
},
/**
* Enqueues a task to refit the iron-dropdown if it is open.
* @private
*/
enqueueDropdownRefit_() {
const dropdown = this.$$('iron-dropdown');
if (!this.dropdownRefitPending_ && dropdown.opened) {
this.dropdownRefitPending_ = true;
setTimeout(() => {
dropdown.refit();
this.dropdownRefitPending_ = false;
}, 0);
}
},
/** @private */
openDropdown_() {
this.$$('iron-dropdown').open();
this.opened_ = true;
},
/** @private */
closeDropdown_() {
this.$$('iron-dropdown').close();
this.opened_ = false;
const selectedItem = this.findSelectedItem_();
if (selectedItem) {
selectedItem.removeAttribute('selected_');
}
},
/**
* @param {!Event} event
* @private
*/
onMouseMove_(event) {
const item = event.composedPath().find(
elm => elm.classList && elm.classList.contains('list-item'));
if (!item) {
return;
}
// Select the item the mouse is hovering over. If the user uses the
// keyboard, the selection will shift. But once the user moves the mouse,
// selection should be updated based on the location of the mouse cursor.
const selectedItem = this.findSelectedItem_();
if (item === selectedItem) {
return;
}
if (selectedItem) {
selectedItem.removeAttribute('selected_');
}
item.setAttribute('selected_', '');
},
/**
* @param {!Event} event
* @private
*/
onPointerDown_(event) {
const paths = event.composedPath();
const dropdown =
/** @type {!IronDropdownElement} */ (this.$$('iron-dropdown'));
const dropdownInput =
/** @type {!CrInputElement} */ (this.$$('#dropdownInput'));
// Exit if path includes |dropdown| because event will be handled by
// onSelect_.
if (paths.includes(dropdown)) {
return;
}
if (!paths.includes(dropdownInput) || dropdown.opened) {
this.closeDropdown_();
return;
}
this.openDropdown_();
},
/** @private */
onSelect_() {
const selectedItem = this.findSelectedItem_();
this.closeDropdown_();
this.fire('dropdown-value-selected', selectedItem);
},
/**
* @param {!Event} event
* @private
*/
onKeyDown_(event) {
event.stopPropagation();
const dropdown = this.$$('iron-dropdown');
switch (event.code) {
case 'Tab':
this.closeDropdown_();
break;
case 'ArrowUp':
case 'ArrowDown': {
const items = dropdown.getElementsByClassName('list-item');
if (items.length === 0) {
break;
}
this.updateSelected_(event.code === 'ArrowDown');
break;
}
case 'Enter': {
if (dropdown.opened) {
this.onSelect_();
break;
}
this.openDropdown_();
break;
}
case 'Escape': {
if (dropdown.opened) {
this.closeDropdown_();
event.preventDefault();
}
break;
}
}
},
/**
* Updates the currently selected element based on keyboard up/down movement.
* @param {boolean} moveDown
* @private
*/
updateSelected_(moveDown) {
const items = this.getButtonListFromDropdown_();
const numItems = items.length;
if (numItems === 0) {
return;
}
let nextIndex = 0;
const currentIndex = this.findSelectedItemIndex_();
if (currentIndex === -1) {
nextIndex = moveDown ? 0 : numItems - 1;
} else {
const delta = moveDown ? 1 : -1;
nextIndex = (numItems + currentIndex + delta) % numItems;
items[currentIndex].removeAttribute('selected_');
}
items[nextIndex].setAttribute('selected_', '');
// The newly selected item might not be visible because the dropdown needs
// to be scrolled. So scroll the dropdown if necessary.
items[nextIndex].scrollIntoViewIfNeeded();
},
/**
* Finds the currently selected dropdown item.
* @return {Element|undefined} Currently selected dropdown item, or undefined
* if no item is selected.
* @private
*/
findSelectedItem_() {
const items = this.getButtonListFromDropdown_();
return items.find(item => item.hasAttribute('selected_'));
},
/**
* Finds the index of currently selected dropdown item.
* @return {number} Index of the currently selected dropdown item, or -1 if
* no item is selected.
* @private
*/
findSelectedItemIndex_() {
const items = this.getButtonListFromDropdown_();
return items.findIndex(item => item.hasAttribute('selected_'));
},
/**
* Returns list of all the visible items in the dropdown.
* @return {!Array<!Element>}
* @private
*/
getButtonListFromDropdown_() {
const dropdown = this.$$('iron-dropdown');
return Array.from(dropdown.getElementsByClassName('list-item'))
.filter(item => !item.hidden);
},
});
......@@ -7,8 +7,8 @@
<div class="throbber"></div>
</div>
<template is="dom-if" if="[[!printerStatusFlagEnabled_]]">
<select class="md-select" aria-label$="[[i18n(destinationLabel)]]"
hidden$="[[!loaded]]"
<select id="dropdown" class="md-select"
aria-label$="[[i18n(destinationLabel)]]" hidden$="[[!loaded]]"
style="background-image:[[backgroundImages_]];"
disabled$="[[disabled]]"
value="{{selectedValue::change}}">
......@@ -33,7 +33,10 @@
</select>
</template>
<template is="dom-if" if="[[printerStatusFlagEnabled_]]">
<div>print-preview-destination-select-cros</div>
<print-preview-destination-dropdown-cros id="dropdown"
value="[[destination]]" item-list="[[recentDestinationList]]"
on-dropdown-value-selected="onDropdownValueSelected_">
</print-preview-destination-dropdown-cros>
</template>
</div>
</print-preview-settings-section>
......
......@@ -7,13 +7,16 @@ import 'chrome://resources/cr_elements/shared_vars_css.m.js';
import 'chrome://resources/cr_elements/md_select_css.m.js';
import 'chrome://resources/js/util.m.js';
import 'chrome://resources/polymer/v3_0/iron-iconset-svg/iron-iconset-svg.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/iron-meta/iron-meta.js';
import './destination_dropdown_cros.js';
import './destination_select_css.js';
import './icons.js';
import './print_preview_shared_css.js';
import './throbber_css.js';
import '../strings.m.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {I18nBehavior} from 'chrome://resources/js/i18n_behavior.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {Base, html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
......@@ -85,7 +88,7 @@ Polymer({
Base.create('iron-meta', {type: 'iconset'})),
focus() {
this.$$('.md-select').focus();
this.$$('#dropdown').focus();
},
/** Sets the select to the current value of |destination|. */
......@@ -173,4 +176,19 @@ Polymer({
this.i18n('noLongerSupportedFragment') :
this.destination.connectionStatusText;
},
/**
* @param {!Event} e
* @private
*/
onDropdownValueSelected_(e) {
assert(this.printerStatusFlagEnabled_);
const selectedItem = e.detail;
if (!selectedItem || selectedItem.value === this.destination.key) {
return;
}
this.fire('selected-option-change', selectedItem.value);
},
});
......@@ -73,6 +73,10 @@ js_type_check("closure_compile") {
#":system_dialog_browsertest",
#":user_manager_test",
]
if (is_chromeos) {
deps += [ ":destination_dropdown_cros_test" ]
}
}
js_library("advanced_dialog_test") {
......@@ -128,3 +132,14 @@ js_library("destination_settings_test") {
]
externs_list = [ "$externs_path/mocha-2.5.js" ]
}
if (is_chromeos) {
js_library("destination_dropdown_cros_test") {
deps = [
":print_preview_test_utils",
"..:chai_assert",
"//chrome/browser/resources/print_preview:print_preview",
"//ui/webui/resources/js:assert.m",
]
}
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {Destination, DestinationConnectionStatus, DestinationOrigin, DestinationType} from 'chrome://print/print_preview.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {keyDownOn, move} from 'chrome://resources/polymer/v3_0/iron-test-helpers/mock-interactions.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertTrue} from '../chai_assert.js';
window.destination_dropdown_cros_test = {};
const destination_dropdown_cros_test = window.destination_dropdown_cros_test;
destination_dropdown_cros_test.suiteName =
'PrintPreviewDestinationDropdownCrosTest';
/** @enum {string} */
destination_dropdown_cros_test.TestNames = {
CorrectListItems: 'correct list items',
ClickRemovesSelected: 'click removes selected',
ClickCloses: 'click closes dropdown',
TabCloses: 'tab closes dropdown',
SelectedAfterUpDown: 'selected after keyboard press up and down',
EnterOpensCloses: 'enter opens and closes dropdown',
SelectedFollowsMouse: 'selected follows mouse',
};
suite(destination_dropdown_cros_test.suiteName, function() {
/** @type {!PrintPreviewDestinationDropdownCrosElement} */
let dropdown;
/** @param {!Array<!Destination>} items */
function setItemList(items) {
dropdown.itemList = items;
flush();
}
/** @return {!NodeList} */
function getList() {
return dropdown.shadowRoot.querySelectorAll('.list-item');
}
/** @param {?Element} element */
function pointerDown(element) {
element.dispatchEvent(new PointerEvent('pointerdown', {
bubbles: true,
cancelable: true,
composed: true,
buttons: 1,
}));
}
function down() {
keyDownOn(dropdown.$$('#dropdownInput'), 'ArrowDown', [], 'ArrowDown');
}
function up() {
keyDownOn(dropdown.$$('#dropdownInput'), 'ArrowUp', [], 'ArrowUp');
}
function enter() {
keyDownOn(dropdown.$$('#dropdownInput'), 'Enter', [], 'Enter');
}
function tab() {
keyDownOn(dropdown.$$('#dropdownInput'), 'Tab', [], 'Tab');
}
/** @return {?Element} */
function getSelectedElement() {
return dropdown.$$('[selected_]');
}
/** @return {string} */
function getSelectedElementText() {
return getSelectedElement().textContent.trim();
}
/**
* @param {string} displayName
* @return {!Destination}
*/
function createDestination(displayName) {
return new Destination(
displayName, DestinationType.LOCAL, DestinationOrigin.LOCAL,
displayName, DestinationConnectionStatus.ONLINE);
}
/** @override */
setup(function() {
document.body.innerHTML = '';
dropdown =
/** @type {!PrintPreviewDestinationDropdownCrosElement} */
(document.createElement('print-preview-destination-dropdown-cros'));
document.body.appendChild(dropdown);
});
test(
assert(destination_dropdown_cros_test.TestNames.CorrectListItems),
function() {
setItemList([
createDestination('One'), createDestination('Two'),
createDestination('Three')
]);
const itemList = getList();
assertEquals(3, itemList.length);
assertEquals('One', itemList[0].textContent.trim());
assertEquals('Two', itemList[1].textContent.trim());
assertEquals('Three', itemList[2].textContent.trim());
});
test(
assert(destination_dropdown_cros_test.TestNames.ClickRemovesSelected),
function() {
setItemList([createDestination('One'), createDestination('Two')]);
dropdown.destination = createDestination('One');
getList()[1].setAttribute('selected_', '');
assertTrue(getList()[1].hasAttribute('selected_'));
getList()[1].click();
assertFalse(getList()[1].hasAttribute('selected_'));
});
test(
assert(destination_dropdown_cros_test.TestNames.ClickCloses), function() {
setItemList([createDestination('One')]);
const ironDropdown = dropdown.$$('iron-dropdown');
pointerDown(dropdown.$$('#dropdownInput'));
assertTrue(ironDropdown.opened);
getList()[0].click();
assertFalse(ironDropdown.opened);
pointerDown(dropdown.$$('#dropdownInput'));
assertTrue(ironDropdown.opened);
// Click outside dropdown to close the dropdown.
pointerDown(document.body);
assertFalse(ironDropdown.opened);
});
test(assert(destination_dropdown_cros_test.TestNames.TabCloses), function() {
setItemList([createDestination('One')]);
const ironDropdown = dropdown.$$('iron-dropdown');
pointerDown(dropdown.$$('#dropdownInput'));
assertTrue(ironDropdown.opened);
tab();
assertFalse(ironDropdown.opened);
});
test(
assert(destination_dropdown_cros_test.TestNames.SelectedAfterUpDown),
function() {
setItemList([
createDestination('One'), createDestination('Two'),
createDestination('Three')
]);
pointerDown(dropdown.$$('#dropdownInput'));
down();
assertEquals('One', getSelectedElementText());
down();
assertEquals('Two', getSelectedElementText());
down();
assertEquals('Three', getSelectedElementText());
down();
assertEquals('One', getSelectedElementText());
up();
assertEquals('Three', getSelectedElementText());
up();
assertEquals('Two', getSelectedElementText());
up();
assertEquals('One', getSelectedElementText());
up();
assertEquals('Three', getSelectedElementText());
});
test(
assert(destination_dropdown_cros_test.TestNames.EnterOpensCloses),
function() {
setItemList([createDestination('One')]);
assertFalse(dropdown.$$('iron-dropdown').opened);
enter();
assertTrue(dropdown.$$('iron-dropdown').opened);
enter();
assertFalse(dropdown.$$('iron-dropdown').opened);
});
test(
assert(destination_dropdown_cros_test.TestNames.SelectedFollowsMouse),
function() {
setItemList([
createDestination('One'), createDestination('Two'),
createDestination('Three')
]);
pointerDown(dropdown.$$('#dropdownInput'));
move(getList()[1], {x: 0, y: 0}, {x: 0, y: 0}, 1);
assertEquals('Two', getSelectedElementText());
move(getList()[2], {x: 0, y: 0}, {x: 0, y: 0}, 1);
assertEquals('Three', getSelectedElementText());
// Interacting with the keyboard should update the selected element.
up();
assertEquals('Two', getSelectedElementText());
// When the user moves the mouse again, the selected element should
// change.
move(getList()[0], {x: 0, y: 0}, {x: 0, y: 0}, 1);
assertEquals('One', getSelectedElementText());
});
});
// Copyright 2019 The Chromium Authors. All rights reserved.
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
......
......@@ -1082,6 +1082,60 @@ TEST_F('PrintPreviewDestinationSelectTestCrOS', 'EulaIsDisplayed', function() {
this.runMochaTest(destination_select_test_cros.TestNames.EulaIsDisplayed);
});
// eslint-disable-next-line no-var
var PrintPreviewDestinationDropdownCrosTest = class extends PrintPreviewTest {
/** @override */
get browsePreload() {
return 'chrome://print/test_loader.html?module=print_preview/destination_dropdown_cros_test.js';
}
/** @override */
get suiteName() {
return destination_dropdown_cros_test.suiteName;
}
};
TEST_F(
'PrintPreviewDestinationDropdownCrosTest', 'CorrectListItems', function() {
this.runMochaTest(
destination_dropdown_cros_test.TestNames.CorrectListItems);
});
TEST_F(
'PrintPreviewDestinationDropdownCrosTest', 'ClickRemovesSelected',
function() {
this.runMochaTest(
destination_dropdown_cros_test.TestNames.ClickRemovesSelected);
});
TEST_F('PrintPreviewDestinationDropdownCrosTest', 'ClickCloses', function() {
this.runMochaTest(destination_dropdown_cros_test.TestNames.ClickCloses);
});
TEST_F('PrintPreviewDestinationDropdownCrosTest', 'TabCloses', function() {
this.runMochaTest(destination_dropdown_cros_test.TestNames.TabCloses);
});
TEST_F(
'PrintPreviewDestinationDropdownCrosTest', 'SelectedAfterUpDown',
function() {
this.runMochaTest(
destination_dropdown_cros_test.TestNames.SelectedAfterUpDown);
});
TEST_F(
'PrintPreviewDestinationDropdownCrosTest', 'EnterOpensCloses', function() {
this.runMochaTest(
destination_dropdown_cros_test.TestNames.EnterOpensCloses);
});
TEST_F(
'PrintPreviewDestinationDropdownCrosTest', 'SelectedFollowsMouse',
function() {
this.runMochaTest(
destination_dropdown_cros_test.TestNames.SelectedFollowsMouse);
});
GEN('#else');
// eslint-disable-next-line no-var
var PrintPreviewDestinationSelectTest = class extends PrintPreviewTest {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment