Commit a1bc7bb7 authored by calamity's avatar calamity Committed by Commit bot

[MD History] Add keyboard navigation to the main history list.

This CL adds arrow-key navigation to the main history list and makes a
single history item tab-navigable by using cr.ui.FocusRow and
iron-list's tab index property.

BUG=623011
CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.linux:closure_compilation

Review-Url: https://codereview.chromium.org/2334553002
Cr-Commit-Position: refs/heads/master@{#419956}
parent eedb95b1
...@@ -3701,6 +3701,7 @@ button.more-vert-button div { ...@@ -3701,6 +3701,7 @@ button.more-vert-button div {
:host { :host {
--checked-color: rgb(68, 136, 255); --checked-color: rgb(68, 136, 255);
display: block; display: block;
outline: none;
} }
:host(:not([embedded])) #main-container { :host(:not([embedded])) #main-container {
...@@ -4250,7 +4251,7 @@ history-item { ...@@ -4250,7 +4251,7 @@ history-item {
</div> </div>
<iron-list items="{{historyData_}}" as="item" id="infinite-list" hidden$="[[!hasResults(historyData_.length)]]"> <iron-list items="{{historyData_}}" as="item" id="infinite-list" hidden$="[[!hasResults(historyData_.length)]]">
<template> <template>
<history-item item="[[item]]" selected="{{item.selected}}" is-card-start="[[isCardStart_(item, index, historyData_.length)]]" is-card-end="[[isCardEnd_(item, index, historyData_.length)]]" has-time-gap="[[needsTimeGap_(item, index, historyData_.length)]]" search-term="[[searchedTerm]]" number-of-items="[[historyData_.length]]" path="[[pathForItem_(index)]]" index="[[index]]"> <history-item tabindex$="[[tabIndex]]" item="[[item]]" selected="{{item.selected}}" is-card-start="[[isCardStart_(item, index, historyData_.length)]]" is-card-end="[[isCardEnd_(item, index, historyData_.length)]]" has-time-gap="[[needsTimeGap_(item, index, historyData_.length)]]" search-term="[[searchedTerm]]" number-of-items="[[historyData_.length]]" path="[[pathForItem_(index)]]" index="[[index]]" iron-list-tab-index="[[tabIndex]]" last-focused="{{lastFocused_}}">
</history-item> </history-item>
</template> </template>
</iron-list> </iron-list>
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:icon', '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:icon',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data', '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:util', '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:util',
'<(DEPTH)/ui/webui/resources/js/cr/ui/compiled_resources2.gyp:focus_row',
'constants', 'constants',
'browser_service', 'browser_service',
'../history/compiled_resources2.gyp:externs', '../history/compiled_resources2.gyp:externs',
......
<link rel="import" href="chrome://resources/cr_elements/icons.html"> <link rel="import" href="chrome://resources/cr_elements/icons.html">
<link rel="import" href="chrome://resources/html/cr/ui/focus_row.html">
<link rel="import" href="chrome://resources/html/polymer.html"> <link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html"> <link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html"> <link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
:host { :host {
--checked-color: rgb(68, 136, 255); --checked-color: rgb(68, 136, 255);
display: block; display: block;
outline: none;
} }
:host(:not([embedded])) #main-container { :host(:not([embedded])) #main-container {
......
...@@ -2,7 +2,79 @@ ...@@ -2,7 +2,79 @@
// 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.
/**
* @param {!Element} root
* @param {?Element} boundary
* @param {cr.ui.FocusRow.Delegate} delegate
* @constructor
* @extends {cr.ui.FocusRow}
*/
function HistoryFocusRow(root, boundary, delegate) {
cr.ui.FocusRow.call(this, root, boundary, delegate);
this.addItems();
}
HistoryFocusRow.prototype = {
__proto__: cr.ui.FocusRow.prototype,
/** @override */
getCustomEquivalent: function(sampleElement) {
var equivalent;
if (this.getTypeForElement(sampleElement) == 'star')
equivalent = this.getFirstFocusable('title');
return equivalent ||
cr.ui.FocusRow.prototype.getCustomEquivalent.call(
this, sampleElement);
},
addItems: function() {
this.destroy();
assert(this.addItem('checkbox', '#checkbox'));
assert(this.addItem('title', '#title'));
assert(this.addItem('menu-button', '#menu-button'));
this.addItem('star', '#bookmark-star');
},
};
cr.define('md_history', function() { cr.define('md_history', function() {
/**
* @param {{lastFocused: Object}} historyItemElement
* @constructor
* @implements {cr.ui.FocusRow.Delegate}
*/
function FocusRowDelegate(historyItemElement) {
this.historyItemElement = historyItemElement;
}
FocusRowDelegate.prototype = {
/**
* @override
* @param {!cr.ui.FocusRow} row
* @param {!Event} e
*/
onFocus: function(row, e) {
this.historyItemElement.lastFocused = e.path[0];
},
/**
* @override
* @param {!cr.ui.FocusRow} row The row that detected a keydown.
* @param {!Event} e
* @return {boolean} Whether the event was handled.
*/
onKeydown: function(row, e) {
// Prevent iron-list from changing the focus on enter.
if (e.key == 'Enter')
e.stopPropagation();
return false;
},
};
var HistoryItem = Polymer({ var HistoryItem = Polymer({
is: 'history-item', is: 'history-item',
...@@ -32,6 +104,67 @@ cr.define('md_history', function() { ...@@ -32,6 +104,67 @@ cr.define('md_history', function() {
path: String, path: String,
index: Number, index: Number,
/** @type {Element} */
lastFocused: {
type: Object,
notify: true,
},
ironListTabIndex: {
type: Number,
observer: 'ironListTabIndexChanged_',
},
},
/** @private {?HistoryFocusRow} */
row_: null,
/** @override */
attached: function() {
Polymer.RenderStatus.afterNextRender(this, function() {
this.row_ = new HistoryFocusRow(
this.$['sizing-container'], null, new FocusRowDelegate(this));
this.row_.makeActive(this.ironListTabIndex == 0);
this.listen(this, 'focus', 'onFocus_');
this.listen(this, 'dom-change', 'onDomChange_');
});
},
/** @override */
detached: function() {
this.unlisten(this, 'focus', 'onFocus_');
this.unlisten(this, 'dom-change', 'onDomChange_');
if (this.row_)
this.row_.destroy();
},
/**
* @private
*/
onFocus_: function() {
if (this.lastFocused)
this.row_.getEquivalentElement(this.lastFocused).focus();
else
this.row_.getFirstFocusable().focus();
this.tabIndex = -1;
},
/**
* @private
*/
ironListTabIndexChanged_: function() {
if (this.row_)
this.row_.makeActive(this.ironListTabIndex == 0);
},
/**
* @private
*/
onDomChange_: function() {
if (this.row_)
this.row_.addItems();
}, },
/** /**
......
...@@ -27,7 +27,8 @@ ...@@ -27,7 +27,8 @@
<iron-list items="{{historyData_}}" as="item" id="infinite-list" <iron-list items="{{historyData_}}" as="item" id="infinite-list"
hidden$="[[!hasResults(historyData_.length)]]"> hidden$="[[!hasResults(historyData_.length)]]">
<template> <template>
<history-item item="[[item]]" <history-item tabindex$="[[tabIndex]]"
item="[[item]]"
selected="{{item.selected}}" selected="{{item.selected}}"
is-card-start="[[isCardStart_(item, index, historyData_.length)]]" is-card-start="[[isCardStart_(item, index, historyData_.length)]]"
is-card-end="[[isCardEnd_(item, index, historyData_.length)]]" is-card-end="[[isCardEnd_(item, index, historyData_.length)]]"
...@@ -35,7 +36,9 @@ ...@@ -35,7 +36,9 @@
search-term="[[searchedTerm]]" search-term="[[searchedTerm]]"
number-of-items="[[historyData_.length]]" number-of-items="[[historyData_.length]]"
path="[[pathForItem_(index)]]" path="[[pathForItem_(index)]]"
index="[[index]]"> index="[[index]]"
iron-list-tab-index="[[tabIndex]]"
last-focused="{{lastFocused_}}">
</history-item> </history-item>
</template> </template>
</iron-list> </iron-list>
......
...@@ -23,6 +23,8 @@ Polymer({ ...@@ -23,6 +23,8 @@ Polymer({
type: Boolean, type: Boolean,
value: false, value: false,
}, },
lastFocused_: Object,
}, },
listeners: { listeners: {
......
...@@ -22,6 +22,7 @@ cr.define('md_history.history_list_test', function() { ...@@ -22,6 +22,7 @@ cr.define('md_history.history_list_test', function() {
createHistoryEntry('2016-03-14 9:00', 'https://www.google.com'), createHistoryEntry('2016-03-14 9:00', 'https://www.google.com'),
createHistoryEntry('2016-03-13', 'https://en.wikipedia.org') createHistoryEntry('2016-03-13', 'https://en.wikipedia.org')
]; ];
TEST_HISTORY_RESULTS[2].starred = true;
ADDITIONAL_RESULTS = [ ADDITIONAL_RESULTS = [
createHistoryEntry('2016-03-13 10:00', 'https://en.wikipedia.org'), createHistoryEntry('2016-03-13 10:00', 'https://en.wikipedia.org'),
...@@ -503,6 +504,52 @@ cr.define('md_history.history_list_test', function() { ...@@ -503,6 +504,52 @@ cr.define('md_history.history_list_test', function() {
}); });
}); });
test('focus and keyboard nav', function() {
app.historyResult(createHistoryInfo(), TEST_HISTORY_RESULTS);
return flush().then(function() {
var items = polymerSelectAll(element, 'history-item');
var focused = items[2].$.checkbox;
focused.focus();
MockInteractions.pressAndReleaseKeyOn(focused, 39, [], 'ArrowRight');
focused = items[2].$.title;
assertEquals(focused, element.lastFocused_);
assertTrue(items[2].row_.isActive());
assertFalse(items[3].row_.isActive());
MockInteractions.pressAndReleaseKeyOn(focused, 40, [], 'ArrowDown');
focused = items[3].$.title;
assertEquals(focused, element.lastFocused_);
assertFalse(items[2].row_.isActive());
assertTrue(items[3].row_.isActive());
MockInteractions.pressAndReleaseKeyOn(focused, 39, [], 'ArrowRight');
focused = items[3].$['menu-button'];
assertEquals(focused, element.lastFocused_);
assertFalse(items[2].row_.isActive());
assertTrue(items[3].row_.isActive());
MockInteractions.pressAndReleaseKeyOn(focused, 38, [], 'ArrowUp');
focused = items[2].$['menu-button'];
assertEquals(focused, element.lastFocused_);
assertTrue(items[2].row_.isActive());
assertFalse(items[3].row_.isActive());
MockInteractions.pressAndReleaseKeyOn(focused, 37, [], 'ArrowLeft');
focused = items[2].$$('#bookmark-star');
assertEquals(focused, element.lastFocused_);
assertTrue(items[2].row_.isActive());
assertFalse(items[3].row_.isActive());
MockInteractions.pressAndReleaseKeyOn(focused, 40, [], 'ArrowDown');
focused = items[3].$.title;
assertEquals(focused, element.lastFocused_);
assertFalse(items[2].row_.isActive());
assertTrue(items[3].row_.isActive());
});
});
teardown(function() { teardown(function() {
element.historyData_ = []; element.historyData_ = [];
registerMessageCallback('removeVisits', this, undefined); registerMessageCallback('removeVisits', this, undefined);
......
...@@ -132,7 +132,7 @@ cr.define('cr.ui', function() { ...@@ -132,7 +132,7 @@ cr.define('cr.ui', function() {
}, },
/** /**
* @param {Element} sampleElement An element for to find an equivalent for. * @param {!Element} sampleElement An element for to find an equivalent for.
* @return {!Element} An equivalent element to focus for |sampleElement|. * @return {!Element} An equivalent element to focus for |sampleElement|.
* @protected * @protected
*/ */
......
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