Commit a3172608 authored by Evan Stade's avatar Evan Stade Committed by Commit Bot

Revert "Tab Search: Infinite list component"

This reverts commit 74dbe648.

Reason for revert: suspected of making TabSearchAppTest.All flaky
see crbug.com/1148009

Original change's description:
> Tab Search: Infinite list component
>
> A list component that renders DOM items on demand based on the viewable
> scroll area and user navigation and scrolling interactions. Provides a
> performance gain for use cases where users have several dozens of items
> that should be available while scrolling the list by deferring scripting
> and rendering costs until necessary to fill the current scroll view.
>
> Bug: 1099917
> Change-Id: Ib22c0edf5ebc5febe3d8d6d4f7d8ac319e9bd0a2
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2503401
> Commit-Queue: Roman Arora <romanarora@google.com>
> Reviewed-by: Thomas Lukaszewicz <tluk@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#826287}

TBR=tluk@chromium.org,romanarora@chromium.org,romanarora@google.com

Change-Id: I5bb4c3d348ad739d2d503522dc84d8654c21f117
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 1099917
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2532987Reviewed-by: default avatarEvan Stade <estade@chromium.org>
Commit-Queue: Evan Stade <estade@chromium.org>
Cr-Commit-Position: refs/heads/master@{#826439}
parent 15c9a651
...@@ -54,7 +54,6 @@ preprocess_grit("preprocess_web_components") { ...@@ -54,7 +54,6 @@ preprocess_grit("preprocess_web_components") {
out_manifest = "$target_gen_dir/$preprocess_web_components_manifest" out_manifest = "$target_gen_dir/$preprocess_web_components_manifest"
in_files = [ in_files = [
"app.js", "app.js",
"infinite_list.js",
"tab_search_item.js", "tab_search_item.js",
"tab_search_search_field.js", "tab_search_search_field.js",
] ]
...@@ -104,7 +103,6 @@ js_type_check("closure_compile") { ...@@ -104,7 +103,6 @@ js_type_check("closure_compile") {
deps = [ deps = [
":app", ":app",
":fuzzy_search", ":fuzzy_search",
":infinite_list",
":tab_data", ":tab_data",
":tab_search_api_proxy", ":tab_search_api_proxy",
":tab_search_item", ":tab_search_item",
...@@ -120,6 +118,7 @@ js_library("app") { ...@@ -120,6 +118,7 @@ js_library("app") {
":tab_search_item", ":tab_search_item",
":tab_search_search_field", ":tab_search_search_field",
"//third_party/polymer/v3_0/components-chromium/iron-a11y-announcer", "//third_party/polymer/v3_0/components-chromium/iron-a11y-announcer",
"//third_party/polymer/v3_0/components-chromium/iron-selector:iron-selector",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled", "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/js:cr.m", "//ui/webui/resources/js:cr.m",
"//ui/webui/resources/js:load_time_data.m", "//ui/webui/resources/js:load_time_data.m",
...@@ -135,13 +134,6 @@ js_library("fuzzy_search") { ...@@ -135,13 +134,6 @@ js_library("fuzzy_search") {
] ]
} }
js_library("infinite_list") {
deps = [
"//third_party/polymer/v3_0/components-chromium/iron-selector:iron-selector",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
}
js_library("tab_data") { js_library("tab_data") {
deps = [] deps = []
} }
...@@ -174,7 +166,6 @@ js_library("tab_search_search_field") { ...@@ -174,7 +166,6 @@ js_library("tab_search_search_field") {
html_to_js("web_components") { html_to_js("web_components") {
js_files = [ js_files = [
"app.js", "app.js",
"infinite_list.js",
"tab_search_item.js", "tab_search_item.js",
"tab_search_search_field.js", "tab_search_search_field.js",
] ]
......
<style include="mwb-shared-style"> <style include="mwb-shared-style">
#tabsList { #tabs {
--list-max-height: 280px; max-height: 280px;
overflow-x: hidden;
overflow-y: auto;
position: relative;
}
#tabsContainer {
height: calc(var(--mwb-item-height) * var(--item-count, 0));
} }
#no-results { #no-results {
...@@ -55,17 +62,20 @@ ...@@ -55,17 +62,20 @@
label="$i18n{searchTabs}" on-focus="onSearchFocus_" label="$i18n{searchTabs}" on-focus="onSearchFocus_"
on-keydown="onSearchKeyDown_" on-search-changed="onSearchChanged_"> on-keydown="onSearchKeyDown_" on-search-changed="onSearchChanged_">
</tab-search-search-field> </tab-search-search-field>
<div hidden="[[!filteredOpenTabs_.length]]"> <div id="tabs">
<infinite-list id="tabsList" items="[[filteredOpenTabs_]]" > <div id="tabsContainer">
<template> <iron-selector id="selector" on-keydown="onItemKeyDown_"
<tab-search-item id="[[item.tab.tabId]]" aria-label="[[ariaLabel_(item)]]" selected="{{selectedIndex_}}" selected-class="selected" role="listbox">
class="mwb-list-item" data="[[item]]" <template id="tabsList" is="dom-repeat" items="[[filteredOpenTabs_]]"
on-click="onItemClick_" on-close="onItemClose_" initial-count="[[chunkingItemCount_]]">
on-focus="onItemFocus_" on-keydown="onItemKeyDown_" tabindex="0" <tab-search-item id="[[item.tab.tabId]]" aria-label="[[ariaLabel_(item)]]"
role="option"> class="mwb-list-item" data="[[item]]"
</tab-search-item> on-click="onItemClick_" on-close="onItemClose_"
</template> on-focus="onItemFocus_" tabindex="0" role="option">
</infinite-list> </tab-search-item>
</template>
</iron-selector>
</div>
</div> </div>
<div id="no-results" hidden="[[filteredOpenTabs_.length]]"> <div id="no-results" hidden="[[filteredOpenTabs_.length]]">
$i18n{noResultsFound} $i18n{noResultsFound}
......
...@@ -8,22 +8,24 @@ import 'chrome://resources/cr_elements/mwb_shared_style.js'; ...@@ -8,22 +8,24 @@ import 'chrome://resources/cr_elements/mwb_shared_style.js';
import 'chrome://resources/cr_elements/mwb_shared_vars.js'; import 'chrome://resources/cr_elements/mwb_shared_vars.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-iconset-svg/iron-iconset-svg.js'; import 'chrome://resources/polymer/v3_0/iron-iconset-svg/iron-iconset-svg.js';
import './infinite_list.js'; import 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
import './tab_search_item.js'; import './tab_search_item.js';
import './tab_search_search_field.js'; import './tab_search_search_field.js';
import './strings.js'; import './strings.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {listenOnce} from 'chrome://resources/js/util.m.js'; import {listenOnce} from 'chrome://resources/js/util.m.js';
import {IronA11yAnnouncer} from 'chrome://resources/polymer/v3_0/iron-a11y-announcer/iron-a11y-announcer.js'; import {IronA11yAnnouncer} from 'chrome://resources/polymer/v3_0/iron-a11y-announcer/iron-a11y-announcer.js';
import {afterNextRender, 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 {fuzzySearch} from './fuzzy_search.js'; import {fuzzySearch} from './fuzzy_search.js';
import {InfiniteList, NO_SELECTION, selectorNavigationKeys} from './infinite_list.js';
import {TabData} from './tab_data.js'; import {TabData} from './tab_data.js';
import {Tab, WindowTabs} from './tab_search.mojom-webui.js'; import {Tab, WindowTabs} from './tab_search.mojom-webui.js';
import {TabSearchApiProxy, TabSearchApiProxyImpl} from './tab_search_api_proxy.js'; import {TabSearchApiProxy, TabSearchApiProxyImpl} from './tab_search_api_proxy.js';
const selectorNavigationKeys = ['ArrowUp', 'ArrowDown', 'Home', 'End'];
export class TabSearchAppElement extends PolymerElement { export class TabSearchAppElement extends PolymerElement {
static get is() { static get is() {
return 'tab-search-app'; return 'tab-search-app';
...@@ -53,6 +55,16 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -53,6 +55,16 @@ export class TabSearchAppElement extends PolymerElement {
value: [], value: [],
}, },
/**
* Controls the number of tab search list items initially rendered in
* dom-repeat's chunked rendering mode.
* @private {number}
*/
chunkingItemCount_: {
type: Number,
value: 10,
},
/** /**
* Options for fuzzy search. * Options for fuzzy search.
* @private {!Object} * @private {!Object}
...@@ -154,13 +166,26 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -154,13 +166,26 @@ export class TabSearchAppElement extends PolymerElement {
// Prior to the first load |this.openTabs_| has not been set. Record the // Prior to the first load |this.openTabs_| has not been set. Record the
// time it takes for the initial list of tabs to render. // time it takes for the initial list of tabs to render.
if (!this.openTabs_) { if (!this.openTabs_) {
listenOnce(this.$.tabsList, 'dom-change', () => { listenOnce(this.$.tabsList, 'rendered-item-count-changed', e => {
afterNextRender(this, () => { const event = /** @type {!CustomEvent<!{value: number}>} */ (e);
// The initial rendered tab list must be non-zero.
assert(event.detail.value > 0);
// Chunking is used to bound the time to interactive for users
// irrespective of the number of tabs they have open. This is no longer
// needed after the initial list render and can cause flickering on
// updates so disable it here.
// TODO(tluk): Investigate a more efficient way to handle this.
this.chunkingItemCount_ = 0;
// Push showUI() to the event loop to allow reflow to occur following
// the DOM update.
setTimeout(() => {
this.apiProxy_.showUI(); this.apiProxy_.showUI();
chrome.metricsPrivate.recordTime( chrome.metricsPrivate.recordTime(
'Tabs.TabSearch.WebUI.InitialTabsRenderTime', 'Tabs.TabSearch.WebUI.InitialTabsRenderTime',
Math.round(window.performance.now())); Math.round(window.performance.now()));
}); }, 0);
}); });
} }
this.openTabs_ = profileTabs.windows; this.openTabs_ = profileTabs.windows;
...@@ -209,7 +234,10 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -209,7 +234,10 @@ export class TabSearchAppElement extends PolymerElement {
* @return {number} * @return {number}
*/ */
getSelectedIndex() { getSelectedIndex() {
return /** @type {!InfiniteList} */ (this.$.tabsList).selected; const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
return selector.selected !== undefined ?
/** @type {number} */ (selector.selected) :
-1;
} }
/** /**
...@@ -221,8 +249,9 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -221,8 +249,9 @@ export class TabSearchAppElement extends PolymerElement {
this.updateFilteredTabs_(this.openTabs_ || []); this.updateFilteredTabs_(this.openTabs_ || []);
// Reset the selected item whenever a search query is provided. // Reset the selected item whenever a search query is provided.
/** @type {!InfiniteList} */ (this.$.tabsList).selected = this.$.selector.selected =
this.filteredOpenTabs_.length > 0 ? 0 : NO_SELECTION; this.filteredOpenTabs_.length > 0 ? 0 : undefined;
this.$.tabs.scrollTop = 0;
const length = this.filteredOpenTabs_.length; const length = this.filteredOpenTabs_.length;
let text; let text;
...@@ -244,7 +273,7 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -244,7 +273,7 @@ export class TabSearchAppElement extends PolymerElement {
/** @private */ /** @private */
onFeedbackFocus_() { onFeedbackFocus_() {
/** @type {!InfiniteList} */ (this.$.tabsList).selected = NO_SELECTION; this.$.selector.selected = undefined;
} }
/** /**
...@@ -265,27 +294,11 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -265,27 +294,11 @@ export class TabSearchAppElement extends PolymerElement {
const tabId = Number.parseInt(e.currentTarget.id, 10); const tabId = Number.parseInt(e.currentTarget.id, 10);
this.apiProxy_.closeTab(tabId); this.apiProxy_.closeTab(tabId);
this.announceA11y_(loadTimeData.getString('a11yTabClosed')); this.announceA11y_(loadTimeData.getString('a11yTabClosed'));
listenOnce(this.$.tabsList, 'iron-items-changed', () => { listenOnce(this.$.tabsList, 'rendered-item-count-changed', () => {
performance.mark('close_tab:benchmark_end'); performance.mark('close_tab:benchmark_end');
}); });
} }
/**
* @param {!Event} e
* @private
*/
onItemKeyDown_(e) {
if (e.key !== 'Enter' && e.key !== ' ') {
return;
}
e.stopPropagation();
e.preventDefault();
this.apiProxy_.switchToTab(
{tabId: this.getSelectedTab_().tabId}, !!this.searchText_);
}
/** /**
* @param {!Array<!WindowTabs>} newOpenTabs * @param {!Array<!WindowTabs>} newOpenTabs
* @private * @private
...@@ -297,10 +310,9 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -297,10 +310,9 @@ export class TabSearchAppElement extends PolymerElement {
// selected; else retain the currently selected index. If the list // selected; else retain the currently selected index. If the list
// shrunk above the selected index, select the last index in the list. // shrunk above the selected index, select the last index in the list.
// If there are no matching results, set the selected index value to none. // If there are no matching results, set the selected index value to none.
const tabsList = /** @type {!InfiniteList} */ (this.$.tabsList); this.$.selector.selectIndex(Math.min(
tabsList.selected = Math.min(
Math.max(this.getSelectedIndex(), 0), Math.max(this.getSelectedIndex(), 0),
this.filteredOpenTabs_.length - 1); this.filteredOpenTabs_.length - 1));
} }
/** /**
...@@ -308,18 +320,67 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -308,18 +320,67 @@ export class TabSearchAppElement extends PolymerElement {
* @private * @private
*/ */
onItemFocus_(e) { onItemFocus_(e) {
// Ensure that when a TabSearchItem receives focus, it becomes the selected this.$.selector.selected =
// item in the list.
/** @type {!InfiniteList} */ (this.$.tabsList).selected =
e.currentTarget.parentNode.indexOf(e.currentTarget); e.currentTarget.parentNode.indexOf(e.currentTarget);
this.updateScrollView_();
}
/**
* @param {string} key Keyboard event key value.
* @private
*/
selectorNavigate_(key) {
const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
switch (key) {
case 'ArrowUp':
selector.selectPrevious();
break;
case 'ArrowDown':
selector.selectNext();
break;
case 'Home':
selector.selected = 0;
break;
case 'End':
selector.selected = this.filteredOpenTabs_.length - 1;
break;
}
}
/**
* Handles key events when list item elements have focus.
* @param {!KeyboardEvent} e
* @private
*/
onItemKeyDown_(e) {
if (e.shiftKey) {
return;
}
if (this.getSelectedIndex() === -1) {
// No tabs matching the search text criteria.
return;
}
if (selectorNavigationKeys.includes(e.key)) {
this.selectorNavigate_(e.key);
/** @type {!HTMLElement} */ (this.$.selector.selectedItem).focus({
preventScroll: true
});
e.stopPropagation();
e.preventDefault();
} else if (e.key === 'Enter' || e.key === ' ') {
this.apiProxy_.switchToTab(
{tabId: this.getSelectedTab_().tabId}, !!this.searchText_);
e.stopPropagation();
}
} }
/** @private */ /** @private */
onSearchFocus_() { onSearchFocus_() {
const tabsList = /** @type {!InfiniteList} */ (this.$.tabsList); if (this.$.selector.selected === undefined &&
if (tabsList.selected === NO_SELECTION &&
this.filteredOpenTabs_.length > 0) { this.filteredOpenTabs_.length > 0) {
tabsList.selected = 0; this.$.selector.selectIndex(0);
} }
} }
...@@ -352,8 +413,8 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -352,8 +413,8 @@ export class TabSearchAppElement extends PolymerElement {
} }
if (selectorNavigationKeys.includes(e.key)) { if (selectorNavigationKeys.includes(e.key)) {
/** @type {!InfiniteList} */ (this.$.tabsList).navigate(e.key); this.selectorNavigate_(e.key);
this.updateScrollView_();
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
...@@ -416,6 +477,42 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -416,6 +477,42 @@ export class TabSearchAppElement extends PolymerElement {
}); });
this.filteredOpenTabs_ = this.filteredOpenTabs_ =
fuzzySearch(this.searchText_, result, this.fuzzySearchOptions_); fuzzySearch(this.searchText_, result, this.fuzzySearchOptions_);
// Update the item count in css so that the css rule can calculate the final
// height of the tabsContainer. This prevents the scrolling height from
// changing as list items are added to the dom incrementally via chunking
// mode.
this.$.tabsContainer.style
.setProperty("--item-count", this.filteredOpenTabs_.length.toString());
}
/**
* Ensure the scroll view can fully display a preceding or following tab item
* if existing.
* @private
*/
updateScrollView_() {
const selectedIndex = this.getSelectedIndex();
if (selectedIndex === 0 ||
selectedIndex === this.filteredOpenTabs_.length - 1) {
/** @type {!Element} */ (this.$.selector.selectedItem).scrollIntoView({
behavior: 'smooth'
});
} else {
const previousItem = this.$.selector.items[this.$.selector.selected - 1];
if (previousItem.offsetTop < this.$.tabs.scrollTop) {
/** @type {!Element} */ (previousItem)
.scrollIntoView({behavior: 'smooth', block: 'nearest'});
return;
}
const nextItem = this.$.selector.items[this.$.selector.selected + 1];
if (nextItem.offsetTop + nextItem.offsetHeight >
this.$.tabs.scrollTop + this.$.tabs.offsetHeight) {
/** @type {!Element} */ (nextItem).scrollIntoView(
{behavior: 'smooth', block: 'nearest'});
}
}
} }
/** return {!Tab} */ /** return {!Tab} */
......
<style include="mwb-shared-style">
:host {
display: block;
}
#container {
max-height: var(--list-max-height);
overflow-x: hidden;
overflow-y: auto;
position: relative;
}
</style>
<div id="container" on-scroll="onScroll_">
<div id="items">
<iron-selector id="selector" on-keydown="onKeyDown_"
on-iron-items-changed="updateScrollerSize_"
on-iron-select="onSelectedChanged_" role="listbox"
selected-class="selected">
</iron-selector>
</div>
</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.
/**
* @fileoverview 'infinite-list' is a component optimized for showing a
* list of items that overflows the view and requires scrolling. For performance
* reasons, The DOM items are added incrementally to the view as the user
* scrolls through the list. The template inside this element represents the DOM
* to create for each list item. The `items` property specifies an array of list
* item data. The component leverages an <iron-selector> to manage item
* selection and styling and a <dom-repeat> which renders the provided template.
*
* Note that the component expects a '--list-max-height' variable to be defined
* in order to determine its maximum height. Additionally, it expects the
* `chunkItemCount` property to be a number of DOM items that is large enough to
* fill the view.
*/
/* TODO(crbug.com/1147535): Decouple MWB style from component */
import 'chrome://resources/cr_elements/mwb_shared_style.js';
import 'chrome://resources/cr_elements/mwb_shared_vars.js';
import 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {afterNextRender, DomRepeat, html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
/** @type {number} */
export const NO_SELECTION = -1;
/** @type {!Array<string>} */
export const selectorNavigationKeys = ['ArrowUp', 'ArrowDown', 'Home', 'End'];
export class InfiniteList extends PolymerElement {
static get is() {
return 'infinite-list';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/**
* Controls the number of list items rendered initially, and added on
* demand as the component is scrolled.
*/
chunkItemCount: {
type: Number,
value: 10,
},
/**
* Controls the number of scrolled items in the currently fetched item
* chunk after which a new chunk of items should be added on demand.
*/
chunkItemThreshold: {
type: Number,
value: 5,
},
/** @type {?Array<!Object>} */
items: {
type: Array,
observer: 'onItemsChanged_',
},
};
}
constructor() {
super();
/**
* An instance of DomRepeat in charge of stamping DOM item elements.
* For performance reasons, the items property of this instance is
* modified on scroll events so that it has enough items to render
* the current scroll view.
* @private {?DomRepeat}
*/
this.domRepeat_ = null;
}
/** @override */
ready() {
super.ready();
this.ensureTemplatized_();
}
/** @private */
getDomItems_() {
const selectorChildren = this.$.selector.children;
return Array.prototype.slice.call(
selectorChildren, 0, selectorChildren.length - 1);
}
/**
* @param {number} idx
* @private
*/
isDomItemAtIndexAvailable_(idx) {
return idx < this.domRepeat_.items.length;
}
/**
* Ensure we have the required DOM items to fill the current view starting
* at the specified index.
*
* @param {number} idx
* @private
*/
ensureDomItemsAvailableStartingAt_(idx) {
if (this.domRepeat_.items.length === this.items.length) {
return;
}
const newItems = this.items.slice(
this.domRepeat_.items.length,
Math.min(idx + this.chunkItemCount, this.items.length));
if (newItems.length > 0) {
this.domRepeat_.push('items', ...newItems);
}
}
/**
* Verifies a light-dom template has been provided and initializes a DomRepeat
* component with the given template.
* @private
*/
ensureTemplatized_() {
// The user provided light-dom template to use when stamping DOM items.
const template =
/** @type {!HTMLTemplateElement} */ (this.querySelector('template'));
assert(
template,
'infinite-list requires a template to be provided in light-dom');
this.domRepeat_ = new DomRepeat();
this.domRepeat_.appendChild(template);
this.$.selector.appendChild(this.domRepeat_);
}
/**
* Adds additional DOM items as needed to fill the view based on user scroll
* interactions.
* @private
*/
onScroll_() {
if (this.$.container.scrollTop > 0 &&
this.domRepeat_.items.length !== this.items.length) {
const aboveScrollTopItemCount =
Math.round(this.$.container.scrollTop / this.domItemAverageHeight_());
// Ensure we have sufficient items to fill the current scroll position and
// a full view following our current position.
if (aboveScrollTopItemCount + this.chunkItemThreshold >
this.domRepeat_.items.length) {
this.ensureDomItemsAvailableStartingAt_(aboveScrollTopItemCount);
}
}
}
/**
* Handles key events when list item elements have focus.
* @param {!KeyboardEvent} e
* @private
*/
onKeyDown_(e) {
// Do not interfere with any parent component that manages 'shift' related
// key events.
if (e.shiftKey) {
return;
}
const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
if (selector.selected === undefined) {
// No tabs matching the search text criteria.
return;
}
if (selectorNavigationKeys.includes(e.key)) {
this.navigate(e.key, true);
e.stopPropagation();
e.preventDefault();
}
}
/**
* @return {number}
* @private
*/
domItemAverageHeight_() {
if (!this.$.selector.items || this.$.selector.items.length === 0) {
return 0;
}
const domItemCount = this.$.selector.items.length;
const lastDomItem = this.$.selector.items[domItemCount - 1];
return (lastDomItem.offsetTop + lastDomItem.offsetHeight) / domItemCount;
}
/**
* Ensures that when the items property changes, only a chunk of the items
* needed to fill the current scroll position view are added to the DOM, thus
* improving rendering performance.
* @private
*/
onItemsChanged_() {
if (this.domRepeat_ && this.items) {
const domItemAvgHeight = this.domItemAverageHeight_();
const aboveScrollTopItemCount = domItemAvgHeight !== 0 ?
Math.round(this.$.container.scrollTop / domItemAvgHeight) :
0;
this.domRepeat_.set('items', []);
this.ensureDomItemsAvailableStartingAt_(aboveScrollTopItemCount);
this.updateScrollerSize_();
}
}
/**
* Sets the scroll height of the component based on an estimated average
* DOM item height and the total number of items.
* @private
*/
updateScrollerSize_() {
if (this.$.selector.items.length !== 0) {
const estScrollHeight = this.items.length * this.domItemAverageHeight_();
this.$.items.style.height = estScrollHeight + 'px';
}
}
/**
* Ensure the scroll view can fully display a preceding or following list item
* to the one selected, if existing.
* @private
*/
onSelectedChanged_() {
const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
if (selector.selected === undefined) {
return;
}
const selectedIndex = /** @type{number} */ (selector.selected);
if (selectedIndex === 0 || selectedIndex === this.items.length - 1) {
/** @type {!Element} */ (selector.selectedItem).scrollIntoView({
behavior: 'smooth'
});
} else {
// If the following DOM item to the currently selected item has not yet
// been rendered, ensure it is by waiting for the next render frame
// before we scroll it into the view.
if (!this.isDomItemAtIndexAvailable_(selectedIndex + 1)) {
this.ensureDomItemsAvailableStartingAt_(selectedIndex + 1);
afterNextRender(this, this.onSelectedChanged_);
return;
}
const previousItem = selector.items[selector.selected - 1];
if (previousItem.offsetTop < this.$.container.scrollTop) {
/** @type {!Element} */ (previousItem)
.scrollIntoView({behavior: 'smooth', block: 'nearest'});
return;
}
const nextItem =
selector.items[/** @type {number} */ (selector.selected) + 1];
if (nextItem.offsetTop + nextItem.offsetHeight >
this.$.container.scrollTop + this.offsetHeight) {
/** @type {!Element} */ (nextItem).scrollIntoView(
{behavior: 'smooth', block: 'nearest'});
}
}
}
/**
* @param {string} key Keyboard event key value.
* @param {boolean=} focusItem Whether to focus the selected item.
*/
navigate(key, focusItem) {
const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
if ((key === 'ArrowUp' && selector.selected === 0) || key === 'End') {
// If the DOM item to be selected has not yet been rendered, ensure it is
// by waiting for the next render frame.
const lastItemIndex = this.items.length - 1;
if (!this.isDomItemAtIndexAvailable_(lastItemIndex)) {
this.ensureDomItemsAvailableStartingAt_(lastItemIndex);
afterNextRender(
this, /** @type {function(...*)} */ (this.navigate),
[key, focusItem]);
return;
}
}
switch (key) {
case 'ArrowUp':
selector.selectPrevious();
break;
case 'ArrowDown':
selector.selectNext();
break;
case 'Home':
selector.selected = 0;
break;
case 'End':
this.$.selector.selected = this.items.length - 1;
break;
}
if (focusItem) {
selector.selectedItem.focus({preventScroll: true});
}
}
/** @param {number} index */
set selected(index) {
const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
if (index !== selector.selected) {
selector.selected = index;
if (index !== NO_SELECTION) {
assert(index < this.items.length);
this.ensureDomItemsAvailableStartingAt_(index);
}
}
}
/** @return {number} The selected index or -1 if none selected. */
get selected() {
return this.$.selector.selected !== undefined ? this.$.selector.selected :
NO_SELECTION;
}
}
customElements.define(InfiniteList.is, InfiniteList);
...@@ -61,7 +61,6 @@ class TabSearchUIBrowserTest : public InProcessBrowserTest { ...@@ -61,7 +61,6 @@ class TabSearchUIBrowserTest : public InProcessBrowserTest {
IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, InitialTabItemsListed) { IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, InitialTabItemsListed) {
const std::string tab_items_js = const std::string tab_items_js =
"const tabItems = document.querySelector('tab-search-app').shadowRoot" "const tabItems = document.querySelector('tab-search-app').shadowRoot"
" .getElementById('tabsList').shadowRoot"
" .querySelectorAll('tab-search-item');"; " .querySelectorAll('tab-search-item');";
int tab_item_count = int tab_item_count =
content::EvalJs(webui_contents_.get(), tab_items_js + "tabItems.length", content::EvalJs(webui_contents_.get(), tab_items_js + "tabItems.length",
...@@ -82,7 +81,7 @@ IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, SwitchToTabAction) { ...@@ -82,7 +81,7 @@ IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, SwitchToTabAction) {
const std::string tab_item_js = base::StringPrintf( const std::string tab_item_js = base::StringPrintf(
"document.querySelector('tab-search-app').shadowRoot" "document.querySelector('tab-search-app').shadowRoot"
" .getElementById('tabsList').shadowRoot.getElementById('%s')", " .getElementById('%s')",
base::NumberToString(tab_id).c_str()); base::NumberToString(tab_id).c_str());
ASSERT_TRUE(content::ExecJs(webui_contents_.get(), tab_item_js + ".click()", ASSERT_TRUE(content::ExecJs(webui_contents_.get(), tab_item_js + ".click()",
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS, content::EXECUTE_SCRIPT_DEFAULT_OPTIONS,
...@@ -97,8 +96,7 @@ IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, CloseTabAction) { ...@@ -97,8 +96,7 @@ IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, CloseTabAction) {
browser()->tab_strip_model()->GetWebContentsAt(0)); browser()->tab_strip_model()->GetWebContentsAt(0));
const std::string tab_item_button_js = base::StringPrintf( const std::string tab_item_button_js = base::StringPrintf(
"document.querySelector('tab-search-app').shadowRoot" "document.querySelector('tab-search-app').shadowRoot.getElementById('%s')"
" .getElementById('tabsList').shadowRoot.getElementById('%s')"
" .shadowRoot.getElementById('closeButton')", " .shadowRoot.getElementById('closeButton')",
base::NumberToString(tab_id).c_str()); base::NumberToString(tab_id).c_str());
ASSERT_TRUE(content::ExecJs(webui_contents_.get(), ASSERT_TRUE(content::ExecJs(webui_contents_.get(),
......
...@@ -21,7 +21,6 @@ js_type_check("closure_compile") { ...@@ -21,7 +21,6 @@ js_type_check("closure_compile") {
] ]
deps = [ deps = [
":fuzzy_search_test", ":fuzzy_search_test",
":infinite_list_test",
":tab_search_app_focus_test", ":tab_search_app_focus_test",
":tab_search_app_test", ":tab_search_app_test",
":tab_search_item_test", ":tab_search_item_test",
...@@ -36,17 +35,11 @@ js_library("fuzzy_search_test") { ...@@ -36,17 +35,11 @@ js_library("fuzzy_search_test") {
externs_list = [ "$externs_path/mocha-2.5.js" ] externs_list = [ "$externs_path/mocha-2.5.js" ]
} }
js_library("infinite_list_test") {
deps = [ "..:chai_assert" ]
externs_list = [ "$externs_path/mocha-2.5.js" ]
}
js_library("tab_search_app_test") { js_library("tab_search_app_test") {
deps = [ deps = [
":test_tab_search_api_proxy", ":test_tab_search_api_proxy",
"..:chai_assert", "..:chai_assert",
"//chrome/browser/resources/tab_search:app", "//chrome/browser/resources/tab_search:app",
"//chrome/browser/resources/tab_search:infinite_list",
] ]
externs_list = [ "$externs_path/mocha-2.5.js" ] externs_list = [ "$externs_path/mocha-2.5.js" ]
} }
......
// 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 {InfiniteList} from 'chrome://tab-search/infinite_list.js';
import {TabSearchItem} from 'chrome://tab-search/tab_search_item.js';
import {assertEquals, assertGT, assertNotEquals, assertTrue} from '../../chai_assert.js';
import {flushTasks, waitAfterNextRender} from '../../test_util.m.js';
import {generateSampleTabsFromSiteNames, sampleSiteNames} from './tab_search_test_data.js';
import {assertTabItemAndNeighborsInViewBounds, disableScrollIntoViewAnimations} from './tab_search_test_helper.js';
suite('InfniteListTest', () => {
const CHUNK_ITEM_COUNT = 7;
/** @type {!InfiniteList} */
let infiniteList;
disableScrollIntoViewAnimations(TabSearchItem);
/**
* @param {!Array<!tabSearch.mojom.Tab>} sampleData
*/
async function setupTest(sampleData) {
document.head.insertAdjacentHTML(
'beforeend', `<style>html{--list-max-height: 280px}</style>`);
document.body.innerHTML = `
<infinite-list id="list" chunk-item-count="${CHUNK_ITEM_COUNT}">
<template>
<tab-search-item id="[[item.tab.tabId]]" class="mwb-list-item"
data="[[item]]" tabindex="0" role="option">
</tab-search-item>
</template>
</infinite-list>`;
infiniteList =
/** @type {!InfiniteList} */ (document.querySelector('#list'));
infiniteList.items = sampleData;
await flushTasks();
}
/**
* @return {!NodeList<!HTMLElement>}
*/
function queryRows() {
return /** @type {!NodeList<!HTMLElement>} */ (
infiniteList.shadowRoot.querySelectorAll('tab-search-item'));
}
/**
* @param {!Array<string>} siteNames
* @return {!Array}
*/
function sampleTabItems(siteNames) {
return generateSampleTabsFromSiteNames(siteNames).map(tab => {
return {hostname: new URL(tab.url).hostname, tab};
});
}
test('ScrollHeight', async () => {
const tabItems = sampleTabItems(sampleSiteNames());
await setupTest(tabItems);
const container = infiniteList.shadowRoot.getElementById('container');
assertEquals(0, container.scrollTop);
const itemHeightStyle =
getComputedStyle(document.head).getPropertyValue('--mwb-item-height');
assertTrue(itemHeightStyle.endsWith('px'));
const tabItemHeight = Number.parseInt(
itemHeightStyle.substring(0, itemHeightStyle.length - 2), 10);
assertEquals(tabItemHeight * tabItems.length, container.scrollHeight);
});
test('SelectedIndex', async () => {
const siteNames = Array.from({length: 50}, (_, i) => 'site' + (i + 1));
const tabItems = sampleTabItems(siteNames);
await setupTest(tabItems);
const container = infiniteList.shadowRoot.getElementById('container');
assertEquals(0, container.scrollTop);
assertEquals(CHUNK_ITEM_COUNT, queryRows().length);
// Assert that upon changing the selected index to a non previously rendered
// item, this one is rendered on the view.
infiniteList.selected = CHUNK_ITEM_COUNT;
assertEquals(CHUNK_ITEM_COUNT, queryRows().length);
await waitAfterNextRender(infiniteList);
let domTabItems = queryRows();
const selectedTabItem = domTabItems[infiniteList.selected];
assertNotEquals(null, selectedTabItem);
assertEquals(2 * CHUNK_ITEM_COUNT, domTabItems.length);
// Assert that the view scrolled to show the selected item.
const afterSelectionScrollTop = container.scrollTop;
assertNotEquals(0, afterSelectionScrollTop);
// Assert that on replacing the list items, the currently selected index
// value is still rendered on the view.
infiniteList.items = sampleTabItems(siteNames);
await waitAfterNextRender(infiniteList);
domTabItems = queryRows();
const theSelectedTabItem = domTabItems[infiniteList.selected];
assertNotEquals(null, theSelectedTabItem);
// Assert the selected item is still visible in the view.
assertEquals(afterSelectionScrollTop, container.scrollTop);
});
test('NavigateDownShowsPreviousAndFollowingListItems', async () => {
const tabItems = sampleTabItems(sampleSiteNames());
await setupTest(tabItems);
const tabsDiv = /** @type {!HTMLElement} */
(infiniteList.shadowRoot.querySelector('#container'));
// Assert that the tabs are in a overflowing state.
assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight);
infiniteList.selected = 0;
for (let i = 0; i < tabItems.length; i++) {
infiniteList.navigate('ArrowDown');
await waitAfterNextRender(infiniteList);
const selectedIndex = ((i + 1) % tabItems.length);
assertEquals(selectedIndex, infiniteList.selected);
assertTabItemAndNeighborsInViewBounds(
tabsDiv, queryRows(), selectedIndex);
}
});
test('NavigateUpShowsPreviousAndFollowingListItems', async () => {
const tabItems = sampleTabItems(sampleSiteNames());
await setupTest(tabItems);
const tabsDiv = /** @type {!HTMLElement} */
(infiniteList.shadowRoot.querySelector('#container'));
// Assert that the tabs are in a overflowing state.
assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight);
infiniteList.selected = 0;
for (let i = tabItems.length; i > 0; i--) {
infiniteList.navigate('ArrowUp');
await waitAfterNextRender(infiniteList);
const selectIndex = (i - 1 + tabItems.length) % tabItems.length;
assertEquals(selectIndex, infiniteList.selected);
assertTabItemAndNeighborsInViewBounds(tabsDiv, queryRows(), selectIndex);
}
});
});
...@@ -55,8 +55,7 @@ suite('TabSearchAppFocusTest', () => { ...@@ -55,8 +55,7 @@ suite('TabSearchAppFocusTest', () => {
assertEquals(searchInput, getDeepActiveElement()); assertEquals(searchInput, getDeepActiveElement());
const tabSearchItems = /** @type {!NodeList<!HTMLElement>} */ const tabSearchItems = /** @type {!NodeList<!HTMLElement>} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelectorAll('tab-search-item'));
.shadowRoot.querySelectorAll('tab-search-item'));
tabSearchItems[0].focus(); tabSearchItems[0].focus();
// Once an item is focused, arrow keys should change focus too. // Once an item is focused, arrow keys should change focus too.
...@@ -89,8 +88,7 @@ suite('TabSearchAppFocusTest', () => { ...@@ -89,8 +88,7 @@ suite('TabSearchAppFocusTest', () => {
await setupTest(sampleData()); await setupTest(sampleData());
const tabSearchItem = /** @type {!HTMLElement} */ const tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item'));
.shadowRoot.querySelector('tab-search-item'));
tabSearchItem.focus(); tabSearchItem.focus();
keyDownOn(tabSearchItem, 0, [], 'Enter'); keyDownOn(tabSearchItem, 0, [], 'Enter');
...@@ -107,14 +105,12 @@ suite('TabSearchAppFocusTest', () => { ...@@ -107,14 +105,12 @@ suite('TabSearchAppFocusTest', () => {
await setupTest(generateSampleDataFromSiteNames(sampleSiteNames())); await setupTest(generateSampleDataFromSiteNames(sampleSiteNames()));
const tabsDiv = /** @type {!HTMLElement} */ const tabsDiv = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('#tabs'));
.shadowRoot.querySelector('#container'));
// Assert that the tabs are in a overflowing state. // Assert that the tabs are in a overflowing state.
assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight); assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight);
const tabItems = /** @type {!NodeList<HTMLElement>} */ const tabItems = /** @type {!NodeList<HTMLElement>} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelectorAll('tab-search-item'));
.shadowRoot.querySelectorAll('tab-search-item'));
for (let i = 0; i < tabItems.length; i++) { for (let i = 0; i < tabItems.length; i++) {
tabItems[i].focus(); tabItems[i].focus();
......
...@@ -9,11 +9,11 @@ import {TabSearchApiProxy, TabSearchApiProxyImpl} from 'chrome://tab-search/tab_ ...@@ -9,11 +9,11 @@ import {TabSearchApiProxy, TabSearchApiProxyImpl} from 'chrome://tab-search/tab_
import {TabSearchItem} from 'chrome://tab-search/tab_search_item.js'; import {TabSearchItem} from 'chrome://tab-search/tab_search_item.js';
import {TabSearchSearchField} from 'chrome://tab-search/tab_search_search_field.js'; import {TabSearchSearchField} from 'chrome://tab-search/tab_search_search_field.js';
import {assertEquals, assertFalse, assertNotEquals, assertTrue} from '../../chai_assert.js'; import {assertEquals, assertFalse, assertGT, assertNotEquals, assertTrue} from '../../chai_assert.js';
import {flushTasks, waitAfterNextRender} from '../../test_util.m.js'; import {flushTasks, waitAfterNextRender} from '../../test_util.m.js';
import {sampleData} from './tab_search_test_data.js'; import {generateSampleDataFromSiteNames, sampleData, sampleSiteNames} from './tab_search_test_data.js';
import {initLoadTimeDataWithDefaults} from './tab_search_test_helper.js'; import {assertTabItemAndNeighborsInViewBounds, assertTabItemInViewBounds, disableScrollIntoViewAnimations, initLoadTimeDataWithDefaults} from './tab_search_test_helper.js';
import {TestTabSearchApiProxy} from './test_tab_search_api_proxy.js'; import {TestTabSearchApiProxy} from './test_tab_search_api_proxy.js';
suite('TabSearchAppTest', () => { suite('TabSearchAppTest', () => {
...@@ -22,6 +22,8 @@ suite('TabSearchAppTest', () => { ...@@ -22,6 +22,8 @@ suite('TabSearchAppTest', () => {
/** @type {!TestTabSearchApiProxy} */ /** @type {!TestTabSearchApiProxy} */
let testProxy; let testProxy;
disableScrollIntoViewAnimations(TabSearchItem);
/** /**
* @param {!NodeList<!Element>} rows * @param {!NodeList<!Element>} rows
* @param {!Array<number>} ids * @param {!Array<number>} ids
...@@ -37,8 +39,7 @@ suite('TabSearchAppTest', () => { ...@@ -37,8 +39,7 @@ suite('TabSearchAppTest', () => {
* @return {!NodeList<!Element>} * @return {!NodeList<!Element>}
*/ */
function queryRows() { function queryRows() {
return tabSearchApp.shadowRoot.querySelector('#tabsList') return tabSearchApp.shadowRoot.querySelectorAll('tab-search-item');
.shadowRoot.querySelectorAll('tab-search-item');
} }
/** /**
...@@ -101,8 +102,7 @@ suite('TabSearchAppTest', () => { ...@@ -101,8 +102,7 @@ suite('TabSearchAppTest', () => {
await setupTest({windows: [{active: true, tabs: [tabData]}]}); await setupTest({windows: [{active: true, tabs: [tabData]}]});
const tabSearchItem = /** @type {!HTMLElement} */ const tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item'));
.shadowRoot.querySelector('tab-search-item'));
tabSearchItem.click(); tabSearchItem.click();
const [tabInfo] = await testProxy.whenCalled('switchToTab'); const [tabInfo] = await testProxy.whenCalled('switchToTab');
assertEquals(tabData.tabId, tabInfo.tabId); assertEquals(tabData.tabId, tabInfo.tabId);
...@@ -208,8 +208,7 @@ suite('TabSearchAppTest', () => { ...@@ -208,8 +208,7 @@ suite('TabSearchAppTest', () => {
await setupTest(sampleData()); await setupTest(sampleData());
verifyTabIds(queryRows(), [1, 5, 6, 2, 3, 4]); verifyTabIds(queryRows(), [1, 5, 6, 2, 3, 4]);
let tabSearchItem = /** @type {!HTMLElement} */ let tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item[id="1"]'));
.shadowRoot.querySelector('tab-search-item[id="1"]'));
assertEquals('Google', tabSearchItem.data.tab.title); assertEquals('Google', tabSearchItem.data.tab.title);
assertEquals('https://www.google.com', tabSearchItem.data.tab.url); assertEquals('https://www.google.com', tabSearchItem.data.tab.url);
const updatedTab = /** @type {!Tab} */ ({ const updatedTab = /** @type {!Tab} */ ({
...@@ -224,8 +223,7 @@ suite('TabSearchAppTest', () => { ...@@ -224,8 +223,7 @@ suite('TabSearchAppTest', () => {
// tabIds are not changed after tab updated. // tabIds are not changed after tab updated.
verifyTabIds(queryRows(), [1, 5, 6, 2, 3, 4]); verifyTabIds(queryRows(), [1, 5, 6, 2, 3, 4]);
tabSearchItem = /** @type {!HTMLElement} */ tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item[id="1"]'));
.shadowRoot.querySelector('tab-search-item[id="1"]'));
assertEquals(updatedTab.title, tabSearchItem.data.tab.title); assertEquals(updatedTab.title, tabSearchItem.data.tab.title);
assertEquals(updatedTab.url, tabSearchItem.data.tab.url); assertEquals(updatedTab.url, tabSearchItem.data.tab.url);
assertEquals('example.com', tabSearchItem.data.hostname); assertEquals('example.com', tabSearchItem.data.hostname);
...@@ -283,8 +281,7 @@ suite('TabSearchAppTest', () => { ...@@ -283,8 +281,7 @@ suite('TabSearchAppTest', () => {
// Click the first element with tabId 1. // Click the first element with tabId 1.
let tabSearchItem = /** @type {!HTMLElement} */ let tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item[id="1"]'));
.shadowRoot.querySelector('tab-search-item[id="1"]'));
tabSearchItem.click(); tabSearchItem.click();
// Assert switchToTab() was called appropriately for an unfiltered tab list. // Assert switchToTab() was called appropriately for an unfiltered tab list.
...@@ -305,8 +302,7 @@ suite('TabSearchAppTest', () => { ...@@ -305,8 +302,7 @@ suite('TabSearchAppTest', () => {
testProxy.reset(); testProxy.reset();
// Click the only remaining element with tabId 2. // Click the only remaining element with tabId 2.
tabSearchItem = /** @type {!HTMLElement} */ tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item[id="2"]'));
.shadowRoot.querySelector('tab-search-item[id="2"]'));
tabSearchItem.click(); tabSearchItem.click();
// Assert switchToTab() was called appropriately for a tab list fitlered by // Assert switchToTab() was called appropriately for a tab list fitlered by
...@@ -399,9 +395,8 @@ suite('TabSearchAppTest', () => { ...@@ -399,9 +395,8 @@ suite('TabSearchAppTest', () => {
const elements = [ const elements = [
tabSearchApp.shadowRoot.querySelector('#searchField'), tabSearchApp.shadowRoot.querySelector('#searchField'),
tabSearchApp.shadowRoot.querySelector('#tabsList'), tabSearchApp.shadowRoot.querySelector('#tabs'),
tabSearchApp.shadowRoot.querySelector('#tabsList') tabSearchApp.shadowRoot.querySelector('tab-search-item'),
.shadowRoot.querySelector('tab-search-item'),
tabSearchApp.shadowRoot.querySelector('#feedback-footer'), tabSearchApp.shadowRoot.querySelector('#feedback-footer'),
]; ];
...@@ -411,4 +406,39 @@ suite('TabSearchAppTest', () => { ...@@ -411,4 +406,39 @@ suite('TabSearchAppTest', () => {
assertEquals(4, testProxy.getCallCount('closeUI')); assertEquals(4, testProxy.getCallCount('closeUI'));
}); });
test('Scrollbar updates show previous and following list items', async () => {
await setupTest(generateSampleDataFromSiteNames(sampleSiteNames()));
const tabsDiv = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabs'));
// Assert that the tabs are in a overflowing state.
assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight);
const tabItems = /** @type {!NodeList<!HTMLElement>}*/ (queryRows());
const searchField = /** @type {!TabSearchSearchField} */
(tabSearchApp.shadowRoot.querySelector('#searchField'));
for (let i = 0; i < tabItems.length; i++) {
keyDownOn(searchField, 0, [], 'ArrowDown');
const selectedIndex = ((i + 1) % tabItems.length);
assertEquals(selectedIndex, tabSearchApp.getSelectedIndex());
assertTabItemAndNeighborsInViewBounds(tabsDiv, tabItems, selectedIndex);
}
keyDownOn(searchField, 0, [], 'End');
assertTabItemInViewBounds(tabsDiv, tabItems[tabItems.length - 1]);
for (let i = tabItems.length - 1; i >= 0; i--) {
keyDownOn(searchField, 0, [], 'ArrowUp');
const selectedIndex = (i - 1 + tabItems.length) % tabItems.length;
assertEquals(selectedIndex, tabSearchApp.getSelectedIndex());
assertTabItemAndNeighborsInViewBounds(tabsDiv, tabItems, selectedIndex);
}
keyDownOn(searchField, 0, [], 'Home');
assertTabItemInViewBounds(tabsDiv, tabItems[0]);
});
}); });
...@@ -56,18 +56,6 @@ TEST_F('FuzzySearchTest', 'All', function() { ...@@ -56,18 +56,6 @@ TEST_F('FuzzySearchTest', 'All', function() {
mocha.run(); mocha.run();
}); });
// eslint-disable-next-line no-var
var InfiniteListTest = class extends TabSearchBrowserTest {
/** @override */
get browsePreload() {
return 'chrome://tab-search/test_loader.html?module=tab_search/infinite_list_test.js';
}
};
TEST_F('InfiniteListTest', 'All', function() {
mocha.run();
});
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var TabSearchItemTest = class extends TabSearchBrowserTest { var TabSearchItemTest = class extends TabSearchBrowserTest {
/** @override */ /** @override */
......
...@@ -57,9 +57,6 @@ export function sampleData() { ...@@ -57,9 +57,6 @@ export function sampleData() {
return profileTabs; return profileTabs;
} }
/**
* @return {!Array<string>}
*/
export function sampleSiteNames() { export function sampleSiteNames() {
return [ return [
'Google', 'Google',
...@@ -76,12 +73,11 @@ export function sampleSiteNames() { ...@@ -76,12 +73,11 @@ export function sampleSiteNames() {
} }
/** /**
* Generates sample tabs based on some given site names. * Generates profile data for a window with a series of tabs.
* @param {!Array} siteNames * @param {!Array} siteNames
* @return {!Array}
*/ */
export function generateSampleTabsFromSiteNames(siteNames) { export function generateSampleDataFromSiteNames(siteNames) {
return siteNames.map((siteName, i) => { const tabs = siteNames.map((siteName, i) => {
return { return {
index: i, index: i,
tabId: i + 1, tabId: i + 1,
...@@ -89,15 +85,6 @@ export function generateSampleTabsFromSiteNames(siteNames) { ...@@ -89,15 +85,6 @@ export function generateSampleTabsFromSiteNames(siteNames) {
url: 'https://www.' + siteName.toLowerCase() + '.com', url: 'https://www.' + siteName.toLowerCase() + '.com',
}; };
}); });
}
/** return {windows: [{active: true, tabs: tabs}]};
* Generates profile data for a window with a series of tabs.
* @param {!Array} siteNames
* @return {!Object}
*/
export function generateSampleDataFromSiteNames(siteNames) {
return {
windows: [{active: true, tabs: generateSampleTabsFromSiteNames(siteNames)}]
};
} }
...@@ -174,8 +174,7 @@ class TabSearchStoryTop100(TabSearchStory): ...@@ -174,8 +174,7 @@ class TabSearchStoryTop100(TabSearchStory):
SCROLL_ELEMENT_FUNCTION = ''' SCROLL_ELEMENT_FUNCTION = '''
document.querySelector('tab-search-app').shadowRoot.getElementById('tabsList') document.querySelector('tab-search-app').shadowRoot.querySelector('#tabs')
.shadowRoot.getElementById('container')
''' '''
MEASURE_FRAME_TIME_SCRIPT = ''' MEASURE_FRAME_TIME_SCRIPT = '''
......
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