Commit 189ee109 authored by Hector Carmona's avatar Hector Carmona Committed by Commit Bot

Polymer: Update v3 iron-list patch with changes from v2.

This has the changes needed to preserve focus.
This change adds both v2 and v3 tests to validate the patch.

Bug: 1020664
Change-Id: I7b4f62f14d6aa8a370b4ebb7c30494eacb48fd74
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1893547
Commit-Queue: Hector Carmona <hcarmona@chromium.org>
Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Reviewed-by: default avatarDan Beam <dbeam@chromium.org>
Cr-Commit-Position: refs/heads/master@{#715055}
parent c22cf1f1
......@@ -42,6 +42,7 @@ js2gtest("interactive_ui_tests_js_webui") {
"$root_gen_dir/chrome/test/data/webui/cr_elements/cr_input_test.m.js",
"$root_gen_dir/chrome/test/data/webui/cr_elements/cr_tabs_test.m.js",
"$root_gen_dir/chrome/test/data/webui/cr_elements/cr_toggle_test.m.js",
"$root_gen_dir/chrome/test/data/webui/cr_elements/iron_list_focus_test.m.js",
"$root_gen_dir/chrome/test/data/webui/test_browser_proxy.m.js",
"$root_gen_dir/chrome/test/data/webui/mock_controller.m.js",
"$root_gen_dir/chrome/test/data/webui/test_util.m.js",
......
......@@ -29,6 +29,7 @@ js_modulizer("modulize") {
"cr_toast_manager_test.js",
"cr_toggle_test.js",
"cr_view_manager_test.js",
"iron_list_focus_test.js",
]
namespace_rewrites = [
"cr.isMac|isMac",
......
......@@ -201,3 +201,24 @@ CrElementsTabsTest.prototype = {
TEST_F('CrElementsTabsTest', 'All', function() {
mocha.run();
});
// eslint-disable-next-line no-var
var IronListFocusTest = class extends CrElementsFocusTest {
/** @override */
get browsePreload() {
return 'chrome://resources/polymer/v1_0/iron-list/iron-list.html';
}
/** @override */
get extraLibraries() {
return [
...PolymerTest.prototype.extraLibraries,
'../test_util.js',
'iron_list_focus_test.js',
];
}
};
TEST_F('IronListFocusTest', 'All', function() {
mocha.run();
});
......@@ -117,3 +117,15 @@ var CrElementsToggleV3Test = class extends CrElementsV3FocusTest {
TEST_F('CrElementsToggleV3Test', 'All', function() {
mocha.run();
});
// eslint-disable-next-line no-var
var IronListFocusV3Test = class extends CrElementsV3FocusTest {
/** @override */
get browsePreload() {
return 'chrome://test?module=cr_elements/iron_list_focus_test.m.js';
}
};
TEST_F('IronListFocusV3Test', 'All', function() {
mocha.run();
});
// Copyright 2019 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.
// clang-format off
// #import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
// #import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
// clang-format on
// test-iron-focusable is a native custom element in order to maintain
// compatibility between Polymer v2 and Polymer v3.
customElements.define('test-iron-focusable', class extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
}
set text(value) {
const button = this.shadowRoot.querySelector('button');
assertTrue(!!button);
button.textContent = value;
}
get text() {
const button = this.shadowRoot.querySelector('button');
assertTrue(!!button);
return button.textContent;
}
// Pass focus to child in shadowRoot b/c iron-list expects that.
focus() {
const button = this.shadowRoot.querySelector('button');
assertTrue(!!button);
button.focus();
}
connectedCallback() {
const button = document.createElement('button');
this.shadowRoot.appendChild(button);
}
});
suite('iron-list-focus-test', function() {
let testDiv = null;
let testIronList = null;
setup(function() {
document.body.innerHTML += `
<div id="testDiv">
<iron-list>
<template>
<test-iron-focusable text="[[item]]" tabindex$='[[tabIndex]]'>
</test-iron-focusable>
</template>
</iron-list>
</div>`;
testDiv = document.querySelector('#testDiv');
testIronList = document.querySelector('iron-list');
assertTrue(!!testDiv);
assertTrue(!!testIronList);
testIronList.items = Array(15).fill('item').map((v, i) => `${v}${i}`);
Polymer.dom.flush();
});
teardown(function() {
testDiv.remove();
testDiv = null;
testIronList = null;
});
test('test focus is NOT preserved', function() {
const initialFocus = testIronList.querySelector('[tabindex="0"]');
initialFocus.focus();
Polymer.dom.flush();
assertEquals('item0', initialFocus.text);
assertEquals(initialFocus, document.activeElement);
testIronList.splice('items', 0, 1); // Remove the item from the list.
Polymer.dom.flush();
const newFocus = testIronList.querySelector('[tabindex="0"]');
assertEquals('item1', newFocus.text);
assertNotEquals(newFocus, document.activeElement);
newFocus.focus();
Polymer.dom.flush();
testIronList.splice('items', 5, 1); // Remove a different item.
Polymer.dom.flush();
const sameFocus = testIronList.querySelector('[tabindex="0"]');
assertEquals('item1', sameFocus.text);
assertNotEquals(sameFocus, document.activeElement);
});
test('test focus is preserved', function() {
testIronList.preserveFocus = true;
const initialFocus = testIronList.querySelector('[tabindex="0"]');
initialFocus.focus();
Polymer.dom.flush();
assertEquals('item0', initialFocus.text);
assertEquals(initialFocus, document.activeElement);
testIronList.splice('items', 0, 1); // Remove the item from the list.
Polymer.dom.flush();
const newFocus = testIronList.querySelector('[tabindex="0"]');
assertEquals('item1', newFocus.text);
assertEquals(newFocus, document.activeElement);
testIronList.splice('items', 5, 1); // Remove a different item.
Polymer.dom.flush();
const sameFocus = testIronList.querySelector('[tabindex="0"]');
assertEquals('item1', sameFocus.text);
assertEquals(sameFocus, document.activeElement);
});
});
......@@ -81,3 +81,104 @@ index 21fa65c0208d..bd591b6dd341 100644
this._parseTemplateContent(template, templateInfo, {parent: null});
}
return template._templateInfo;
diff --git a/components-chromium/iron-list/iron-list.js b/components-chromium/iron-list/iron-list.js
index 43c59653a39b..26652936735c 100644
--- a/components-chromium/iron-list/iron-list.js
+++ b/components-chromium/iron-list/iron-list.js
@@ -376,7 +376,15 @@
* there's some offset between the scrolling element and the list. For
* example: a header is placed above the list.
*/
- scrollOffset: {type: Number, value: 0}
+ scrollOffset: {type: Number, value: 0},
+
+ /**
+ * If set to true, focus on an element will be preserved after rerender.
+ */
+ preserveFocus: {
+ type: Boolean,
+ value: false
+ }
},
observers: [
@@ -1067,10 +1075,52 @@
},
/**
+ * Finds and returns the focused element (both within self and children's
+ * Shadow DOM).
+ * @return {?HTMLElement}
+ */
+ _getFocusedElement: function() {
+ function doSearch(node, query) {
+ let result = null;
+ let type = node.nodeType;
+ if (type == Node.ELEMENT_NODE || type == Node.DOCUMENT_FRAGMENT_NODE)
+ result = node.querySelector(query);
+ if (result)
+ return result;
+
+ let child = node.firstChild;
+ while (child !== null && result === null) {
+ result = doSearch(child, query);
+ child = child.nextSibling;
+ }
+ if (result)
+ return result;
+
+ const shadowRoot = node.shadowRoot;
+ return shadowRoot ? doSearch(shadowRoot, query) : null;
+ }
+
+ // Find out if any of the items are focused first, and only search
+ // recursively in the item that contains focus, to avoid a slow
+ // search of the entire list.
+ const focusWithin = doSearch(this, ':focus-within');
+ return focusWithin ? doSearch(focusWithin, ':focus') : null;
+ },
+
+ /**
* Called when the items have changed. That is, reassignments
* to `items`, splices or updates to a single item.
*/
_itemsChanged: function(change) {
+ var rendering = /^items(\.splices){0,1}$/.test(change.path);
+ var lastFocusedIndex, focusedElement;
+ if (rendering && this.preserveFocus) {
+ lastFocusedIndex = this._focusedVirtualIndex;
+ focusedElement = this._getFocusedElement();
+ }
+
+ var preservingFocus = rendering && this.preserveFocus && focusedElement;
+
if (change.path === 'items') {
this._virtualStart = 0;
this._physicalTop = 0;
@@ -1082,7 +1132,7 @@
this._physicalItems = this._physicalItems || [];
this._physicalSizes = this._physicalSizes || [];
this._physicalStart = 0;
- if (this._scrollTop > this._scrollOffset) {
+ if (this._scrollTop > this._scrollOffset && !preservingFocus) {
this._resetScrollPosition(0);
}
this._removeFocusedItem();
@@ -1114,6 +1164,17 @@
} else if (change.path !== 'items.length') {
this._forwardItemPath(change.path, change.value);
}
+
+ // If the list was in focus when updated, preserve the focus on item.
+ if (preservingFocus) {
+ flush();
+ focusedElement.blur(); // paper- elements breaks when focused twice.
+ this._focusPhysicalItem(
+ Math.min(this.items.length - 1, lastFocusedIndex));
+ if (!this._isIndexVisible(this._focusedVirtualIndex)) {
+ this.scrollToIndex(this._focusedVirtualIndex);
+ }
+ }
},
_forwardItemPath: function(path, value) {
......@@ -376,7 +376,15 @@ Polymer({
* there's some offset between the scrolling element and the list. For
* example: a header is placed above the list.
*/
scrollOffset: {type: Number, value: 0}
scrollOffset: {type: Number, value: 0},
/**
* If set to true, focus on an element will be preserved after rerender.
*/
preserveFocus: {
type: Boolean,
value: false
}
},
observers: [
......@@ -1066,11 +1074,53 @@ Polymer({
newGrid && this._updateGridMetrics();
},
/**
* Finds and returns the focused element (both within self and children's
* Shadow DOM).
* @return {?HTMLElement}
*/
_getFocusedElement: function() {
function doSearch(node, query) {
let result = null;
let type = node.nodeType;
if (type == Node.ELEMENT_NODE || type == Node.DOCUMENT_FRAGMENT_NODE)
result = node.querySelector(query);
if (result)
return result;
let child = node.firstChild;
while (child !== null && result === null) {
result = doSearch(child, query);
child = child.nextSibling;
}
if (result)
return result;
const shadowRoot = node.shadowRoot;
return shadowRoot ? doSearch(shadowRoot, query) : null;
}
// Find out if any of the items are focused first, and only search
// recursively in the item that contains focus, to avoid a slow
// search of the entire list.
const focusWithin = doSearch(this, ':focus-within');
return focusWithin ? doSearch(focusWithin, ':focus') : null;
},
/**
* Called when the items have changed. That is, reassignments
* to `items`, splices or updates to a single item.
*/
_itemsChanged: function(change) {
var rendering = /^items(\.splices){0,1}$/.test(change.path);
var lastFocusedIndex, focusedElement;
if (rendering && this.preserveFocus) {
lastFocusedIndex = this._focusedVirtualIndex;
focusedElement = this._getFocusedElement();
}
var preservingFocus = rendering && this.preserveFocus && focusedElement;
if (change.path === 'items') {
this._virtualStart = 0;
this._physicalTop = 0;
......@@ -1082,7 +1132,7 @@ Polymer({
this._physicalItems = this._physicalItems || [];
this._physicalSizes = this._physicalSizes || [];
this._physicalStart = 0;
if (this._scrollTop > this._scrollOffset) {
if (this._scrollTop > this._scrollOffset && !preservingFocus) {
this._resetScrollPosition(0);
}
this._removeFocusedItem();
......@@ -1114,6 +1164,17 @@ Polymer({
} else if (change.path !== 'items.length') {
this._forwardItemPath(change.path, change.value);
}
// If the list was in focus when updated, preserve the focus on item.
if (preservingFocus) {
flush();
focusedElement.blur(); // paper- elements breaks when focused twice.
this._focusPhysicalItem(
Math.min(this.items.length - 1, lastFocusedIndex));
if (!this._isIndexVisible(this._focusedVirtualIndex)) {
this.scrollToIndex(this._focusedVirtualIndex);
}
}
},
_forwardItemPath: function(path, value) {
......
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