Commit 73f43db1 authored by Roman Arora's avatar Roman Arora Committed by Josip Sokcevic

Tab Search WebUI: components and styling changes

Tab Search UI related changes:
- Custom tab-search-item component that represents each tab item
- Show only hostname of tab url on tab-search-item
- Close tab icon for certain list-item states
- Keyboard navigation for tab-search-items

Bug: 1099917
Change-Id: Idae91276a66b5b8e2efd7914266ea73107c58d34
Reviewed-on: https://chrome-internal-review.googlesource.com/c/chrome/browser/resources/tab_search/+/3154044Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Reviewed-by: default avatarJohn Lee <johntlee@chromium.org>
Reviewed-by: default avatarTom Lukaszewicz <tluk@chromium.org>
Cr-Commit-Position: refs/heads/master@{#819570}
parent 4a2af52e
...@@ -16,6 +16,7 @@ js_type_check("closure_compile") { ...@@ -16,6 +16,7 @@ js_type_check("closure_compile") {
js_library("app") { js_library("app") {
deps = [ deps = [
":tab_search_api_proxy", ":tab_search_api_proxy",
":tab_search_item",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled", "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
] ]
} }
...@@ -27,7 +28,16 @@ js_library("tab_search_api_proxy") { ...@@ -27,7 +28,16 @@ js_library("tab_search_api_proxy") {
] ]
} }
html_to_js("web_components") { js_library("tab_search_item") {
js_files = [ "app.js" ] deps = [
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/cr_elements/cr_icon_button:cr_icon_button.m",
]
} }
html_to_js("web_components") {
js_files = [
"app.js",
"tab_search_item.js",
]
}
<style>
::-webkit-scrollbar-thumb {
background: var(--google-grey-refresh-100);
}
::-webkit-scrollbar-thumb:hover {
background: var(--cr-focused-item-color);
}
::-webkit-scrollbar {
width: 4px;
}
#tabs {
max-height: 280px;
overflow-x: hidden;
overflow-y: auto;
}
</style>
<div id="searchBar"> <div id="searchBar">
<iron-icon id="searchIcon" icon="cr:search"></iron-icon> <iron-icon id="searchIcon" icon="cr:search"></iron-icon>
<input id="searchInput" type="text" value="[[searchText_]]" <input id="searchInput" type="text" value="[[searchText_]]"
on-input="onSearchInput_"> on-input="onSearchInput_" on-keydown="onKeyDown_" autofocus>
</div> </div>
<div id="content"> <div id="tabs">
<template is="dom-repeat" items="[[filteredOpenTabs_]]"> <iron-selector id="selector" selected="{{selectedIndex_}}" selected-class="selected">
<div class="row" id="[[item.tabId]]" on-click="onItemClick_"> <template id="tabs-list" is="dom-repeat" items="[[filteredOpenTabs_]]">
<div> <tab-search-item id="[[item.tabId]]" data="[[item]]"
<img src="[[item.favIconUrl]]"> on-click="onItemClick_" tabindex="-1" >
</div> </tab-search-item>
<div> </template>
<div>[[item.title]]</div> </iron-selector>
<div>[[item.url]]</div>
</div>
</template>
</div> </div>
...@@ -3,7 +3,13 @@ ...@@ -3,7 +3,13 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'chrome://resources/cr_elements/icons.m.js'; import 'chrome://resources/cr_elements/icons.m.js';
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js'; import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
import './tab_search_item.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {TabSearchApiProxy, TabSearchApiProxyImpl} from './tab_search_api_proxy.js'; import {TabSearchApiProxy, TabSearchApiProxyImpl} from './tab_search_api_proxy.js';
...@@ -34,6 +40,12 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -34,6 +40,12 @@ export class TabSearchAppElement extends PolymerElement {
value: '', value: '',
}, },
/**
* The seleted item's index, or -1 if no item selected.
* @private {number}
*/
selectedIndex_: {type: Number, value: -1},
/** @private {?Array<!tabSearch.mojom.WindowTabs>} */ /** @private {?Array<!tabSearch.mojom.WindowTabs>} */
openTabs_: Array, openTabs_: Array,
...@@ -47,6 +59,7 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -47,6 +59,7 @@ export class TabSearchAppElement extends PolymerElement {
constructor() { constructor() {
super(); super();
/** @private {!TabSearchApiProxy} */ /** @private {!TabSearchApiProxy} */
this.apiProxy_ = TabSearchApiProxyImpl.getInstance(); this.apiProxy_ = TabSearchApiProxyImpl.getInstance();
} }
...@@ -62,12 +75,27 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -62,12 +75,27 @@ export class TabSearchAppElement extends PolymerElement {
}); });
} }
/** @return {number} */
getSelectedIndex() {
return this.selectedIndex_;
}
/** /**
* @param {!Event} e * @param {?Array<!tabSearch.mojom.WindowTabs>} windows
* @param {string} searchText
* @return {!Array<!tabSearch.mojom.Tab>}
* @private * @private
*/ */
onSearchInput_(e) { getFilteredTabs_(windows, searchText) {
this.searchText_ = e.target.value; const result = [];
if (windows) {
windows.forEach(window => {
result.push(...window.tabs.filter(filterFunc.bind(null, searchText)));
});
}
this.selectedIndex_ = result.length > 0 ? 0 : -1;
return result;
} }
/** /**
...@@ -75,24 +103,79 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -75,24 +103,79 @@ export class TabSearchAppElement extends PolymerElement {
* @private * @private
*/ */
onItemClick_(e) { onItemClick_(e) {
const tabId = parseInt(e.currentTarget.id, 10); const tabId = Number.parseInt(e.currentTarget.id, 10);
this.apiProxy_.switchToTab({tabId}); this.apiProxy_.switchToTab({tabId});
} }
/** /**
* @param {?Array<!tabSearch.mojom.WindowTabs>} windows * @param {number} index A valid index for an element present in the
* @param {string} searchText * filteredOpenTabs_ array.
* @return {!Array<!tabSearch.mojom.Tab>} * @return {?HTMLElement}
* @private * @private
*/ */
getFilteredTabs_(windows, searchText) { getTabSearchItem_(index) {
const result = []; const tabItemId = assert(this.filteredOpenTabs_[index]).tabId;
if (windows) { return this.shadowRoot.getElementById(tabItemId.toString());
windows.forEach(window => { }
result.push(...window.tabs.filter(filterFunc.bind(null, searchText)));
}); /**
* @param {!KeyboardEvent} e
* @private
*/
onKeyDown_(e) {
if (this.selectedIndex_ !== -1) {
switch (e.key) {
case 'ArrowUp':
this.selectItem_(-1);
break;
case 'ArrowDown':
this.selectItem_(1);
break;
case 'Home':
this.selectItem_(-this.selectedIndex_);
break;
case 'End':
this.selectItem_(
this.filteredOpenTabs_.length - 1 - this.selectedIndex_);
break;
case 'Enter':
const selectedItem = this.filteredOpenTabs_[this.selectedIndex_];
this.apiProxy_.switchToTab({tabId: selectedItem.tabId});
break;
}
} }
return result;
e.stopPropagation();
}
/**
* @param {!Event} e
* @private
*/
onSearchInput_(e) {
this.searchText_ = e.target.value;
}
/**
* @param {number} offset Distance from the desired item to select and the
* currently selected item.
* @private
*/
selectItem_(offset) {
const length = assert(this.filteredOpenTabs_.length);
this.selectedIndex_ = (this.selectedIndex_ + offset + length) % length;
// Ensure the scroll view can fully display a preceding or following tab
// item if existing. Use Math.sign to identify any such preceding or
// following item.
const scrollToIndex =
(this.selectedIndex_ === 0 ||
this.selectedIndex_ === this.filteredOpenTabs_.length - 1) ?
this.selectedIndex_ :
this.selectedIndex_ + Math.sign(offset);
this.getTabSearchItem_(scrollToIndex)
.scrollIntoView({behavior: 'smooth', block: 'nearest'});
} }
/** /**
......
<style>
/* TODO(crbug.com/1110109): Add dark mode support. */
:host {
--horizontal-margin: 16px;
--icon-size: 16px;
--vertical-margin: 12px;
align-items: center;
background-color: white;
display: flex;
padding: var(--vertical-margin) var(--horizontal-margin);
}
:host-context(.selected, :host:hover) {
background-color: var(--cr-focused-item-color);
}
:host-context(.selected, :host:hover) .button-container {
display: flex;
}
.button-container {
display: none;
}
img {
height: var(--icon-size);
margin-inline-end: var(--horizontal-margin);
width: var(--icon-size);
}
.text-container {
flex-grow: 1;
overflow: hidden;
}
.primary-text,
.secondary-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.primary-text {
color: var(--cr-primary-text-color);
font-size: 13px;
margin-bottom: 3px;
}
.secondary-text {
color: var(--cr-secondary-text-color);
font-size: 12px;
}
cr-icon-button {
--cr-icon-button-margin-end: calc(var(--icon-size) / 4);
--cr-icon-button-margin-start: calc(var(--icon-size) / 4);
--cr-icon-button-size: var(--icon-size);
}
</style>
<img src="[[data.favIconUrl]]">
<div class="text-container">
<div class="primary-text">[[data.title]]</div>
<div class="secondary-text">[[urlHostname_(data.url)]]</div>
</div>
<div class="button-container">
<cr-icon-button aria-labelledby="label" iron-icon="cr:cancel"
on-click="onItemCancel_" title="$i18n{close}">
</cr-icon-button>
</div>
// 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_icon_button/cr_icon_button.m.js';
import 'chrome://resources/cr_elements/cr_icons_css.m.js';
import 'chrome://resources/cr_elements/icons.m.js';
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
export class TabSearchItem extends PolymerElement {
static get is() {
return 'tab-search-item';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/** @type {!tabSearch.mojom.Tab} */
data: Object,
};
}
/**
* @param {!Event} e
* @private
*/
onItemCancel_(e) {
this.dispatchEvent(new CustomEvent('close'));
e.stopPropagation();
}
/**
* @param {string} url
* @return {string}
* @private
*/
urlHostname_(url) {
return new URL(url).hostname;
}
}
customElements.define(TabSearchItem.is, TabSearchItem);
...@@ -3,6 +3,18 @@ ...@@ -3,6 +3,18 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Tab Search</title> <title>Tab Search</title>
<link rel="stylesheet" href="chrome://resources/css/text_defaults_md.css">
<style>
html,
body {
height: 100%;
width: 320px;
}
body {
margin: 0;
}
</style>
</head> </head>
<body> <body>
<tab-search-app></tab-search-app> <tab-search-app></tab-search-app>
......
...@@ -22,10 +22,14 @@ ...@@ -22,10 +22,14 @@
<include name="IDR_TAB_SEARCH_PAGE_HTML" <include name="IDR_TAB_SEARCH_PAGE_HTML"
file="tab_search_page.html" file="tab_search_page.html"
type="BINDATA" /> type="BINDATA" />
<include name="IDR_TAB_SEARCH_ITEM"
file="${root_gen_dir}/chrome/browser/resources/tab_search/tab_search_item.js"
type="BINDATA"
use_base_dir="false"/>
<include name="IDR_TAB_SEARCH_MOJO_LITE_JS" <include name="IDR_TAB_SEARCH_MOJO_LITE_JS"
file="${root_gen_dir}/chrome/browser/ui/webui/tab_search/tab_search.mojom-lite.js" file="${root_gen_dir}/chrome/browser/ui/webui/tab_search/tab_search.mojom-lite.js"
use_base_dir="false" type="BINDATA"
type="BINDATA" /> use_base_dir="false"/>
</includes> </includes>
</release> </release>
</grit> </grit>
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {keyDownOn} from 'chrome://resources/polymer/v3_0/iron-test-helpers/mock-interactions.js';
import {TabSearchAppElement} from 'chrome://tab-search/app.js'; import {TabSearchAppElement} from 'chrome://tab-search/app.js';
import {TabSearchApiProxy, TabSearchApiProxyImpl} from 'chrome://tab-search/tab_search_api_proxy.js' import {TabSearchApiProxy, TabSearchApiProxyImpl} from 'chrome://tab-search/tab_search_api_proxy.js'
import {assertEquals} from '../../chai_assert.js'; import {assertEquals, assertTrue} from '../../chai_assert.js';
import {flushTasks} from '../../test_util.m.js'; import {flushTasks} from '../../test_util.m.js';
import {TestTabSearchApiProxy} from './test_tab_search_api_proxy.js'; import {TestTabSearchApiProxy} from './test_tab_search_api_proxy.js';
...@@ -31,21 +32,37 @@ suite('TabSearchAppTest', () => { ...@@ -31,21 +32,37 @@ suite('TabSearchAppTest', () => {
* @return {!NodeList<!Element>} * @return {!NodeList<!Element>}
*/ */
function queryRows() { function queryRows() {
return tabSearchApp.shadowRoot.querySelectorAll('.row'); return tabSearchApp.shadowRoot.querySelectorAll('tab-search-item');
} }
function setupData() { function sampleData() {
const profileTabs = { const profileTabs = {
windows: [ windows: [
{ {
active: true, active: true,
tabs: [{ tabs: [
index: 0, {
tabId: 1, index: 0,
favIconUrl: '', tabId: 1,
title: 'Google', favIconUrl: '',
url: 'https://www.google.com', title: 'Google',
}], url: 'https://www.google.com',
},
{
index: 1,
tabId: 5,
favIconUrl: '',
title: 'Amazon',
url: 'https://www.amazon.com',
},
{
index: 2,
tabId: 6,
favIconUrl: '',
title: 'Apple',
url: 'https://www.apple.com',
}
],
}, },
{ {
active: false, active: false,
...@@ -76,27 +93,84 @@ suite('TabSearchAppTest', () => { ...@@ -76,27 +93,84 @@ suite('TabSearchAppTest', () => {
] ]
}; };
testProxy.setProfileTabs(profileTabs); return profileTabs;
} }
setup(() => { async function setupTest(sampleData) {
testProxy = new TestTabSearchApiProxy(); testProxy = new TestTabSearchApiProxy();
testProxy.setProfileTabs(sampleData);
TabSearchApiProxyImpl.instance_ = testProxy; TabSearchApiProxyImpl.instance_ = testProxy;
setupData();
tabSearchApp = /** @type {!TabSearchAppElement} */ tabSearchApp = /** @type {!TabSearchAppElement} */
(document.createElement('tab-search-app')); (document.createElement('tab-search-app'));
document.body.innerHTML = '';
document.body.appendChild(tabSearchApp); document.body.appendChild(tabSearchApp);
}); await flushTasks();
}
test('return all tabs', async () => { test('return all tabs', async () => {
await flushTasks(); await setupTest(sampleData());
verifyTabIds(queryRows(), [1, 2, 3, 4]); verifyTabIds(queryRows(), [1, 5, 6, 2, 3, 4]);
}); });
test('return filtered tabs', async () => { test('return filtered tabs', async () => {
await setupTest(sampleData());
tabSearchApp.setSearchText('bing'); tabSearchApp.setSearchText('bing');
await flushTasks(); await flushTasks();
verifyTabIds(queryRows(), [2]); verifyTabIds(queryRows(), [2]);
}); });
test('Default tab selection when data is present', async () => {
await setupTest(sampleData());
assertTrue(
tabSearchApp.getSelectedIndex() != -1,
'No default selection in the precense of data');
});
test('Keyboard navigation on an empty list', async () => {
await setupTest({windows: [{active: true, tabs: []}]});
const searchInput = /** @type {!HTMLInputElement} */ (
tabSearchApp.shadowRoot.getElementById('searchInput'));
keyDownOn(searchInput, 0, [], 'ArrowUp');
assertEquals(-1, tabSearchApp.getSelectedIndex());
keyDownOn(searchInput, 0, [], 'ArrowDown');
assertEquals(-1, tabSearchApp.getSelectedIndex());
keyDownOn(searchInput, 0, [], 'Home');
assertEquals(-1, tabSearchApp.getSelectedIndex());
keyDownOn(searchInput, 0, [], 'End');
assertEquals(-1, tabSearchApp.getSelectedIndex());
});
test('Keyboard navigation abides by item list range boundaries', async () => {
await setupTest(sampleData());
const numTabs =
sampleData().windows.reduce((total, w) => total + w.tabs.length, 0);
const searchInput = /** @type {!HTMLInputElement} */ (
tabSearchApp.shadowRoot.getElementById('searchInput'));
keyDownOn(searchInput, 0, [], 'ArrowUp');
assertEquals(numTabs - 1, tabSearchApp.getSelectedIndex());
keyDownOn(searchInput, 0, [], 'ArrowDown');
assertEquals(0, tabSearchApp.getSelectedIndex());
keyDownOn(searchInput, 0, [], 'ArrowDown');
assertEquals(1, tabSearchApp.getSelectedIndex());
keyDownOn(searchInput, 0, [], 'ArrowUp');
assertEquals(0, tabSearchApp.getSelectedIndex());
keyDownOn(searchInput, 0, [], 'End');
assertEquals(numTabs - 1, tabSearchApp.getSelectedIndex());
keyDownOn(searchInput, 0, [], 'Home');
assertEquals(0, tabSearchApp.getSelectedIndex());
});
}); });
...@@ -25,7 +25,7 @@ export class TestTabSearchApiProxy extends TestBrowserProxy { ...@@ -25,7 +25,7 @@ export class TestTabSearchApiProxy extends TestBrowserProxy {
/** override */ /** override */
switchToTab(tabInfo) { switchToTab(tabInfo) {
this.methodCalled('swtichToTab'); this.methodCalled('switchToTab');
} }
/** @param {tabSearch.mojom.ProfileTabs} profileTabs */ /** @param {tabSearch.mojom.ProfileTabs} profileTabs */
......
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