blob: 63599e61e853ac166d93d56a4cbd5a0475be0b25 [file] [log] [blame]
// Copyright 2017 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.
/** @implements {cr.ui.FocusRow.Delegate} */
class FocusRowDelegate {
/** @param {{lastFocused: Object}} listItem */
constructor(listItem) {
/** @private */
this.listItem_ = listItem;
}
/**
* This function gets called when the [focus-row-control] element receives
* the focus event.
* @override
* @param {!cr.ui.FocusRow} row
* @param {!Event} e
*/
onFocus(row, e) {
this.listItem_.lastFocused = e.path[0];
this.listItem_.tabIndex = -1;
}
/**
* @override
* @param {!cr.ui.FocusRow} row The row that detected a keydown.
* @param {!Event} e
* @return {boolean} Whether the event was handled.
*/
onKeydown(row, e) {
// Prevent iron-list from changing the focus on enter.
if (e.key == 'Enter')
e.stopPropagation();
return false;
}
}
/** @extends {cr.ui.FocusRow} */
class VirtualFocusRow extends cr.ui.FocusRow {
/**
* @param {!Element} root
* @param {cr.ui.FocusRow.Delegate} delegate
*/
constructor(root, delegate) {
super(root, /* boundary */ null, delegate);
}
}
/**
* Any element that is being used as an iron-list row item can extend this
* behavior, which encapsulates focus controls of mouse and keyboards.
* To use this behavior:
* - The parent element should pass a "last-focused" attribute double-bound
* to the row items, to track the last-focused element across rows.
* - There must be a container in the extending element with the
* [focus-row-container] attribute that contains all focusable controls.
* - On each of the focusable controls, there must be a [focus-row-control]
* attribute, and a [focus-type=] attribute unique for each control.
*
* @polymerBehavior
*/
const FocusRowBehavior = {
properties: {
/** @private {VirtualFocusRow} */
row_: Object,
/** @private {boolean} */
mouseFocused_: Boolean,
/** @type {Element} */
lastFocused: {
type: Object,
notify: true,
},
/**
* This is different from tabIndex, since the template only does a one-way
* binding on both attributes, and the behavior actually make use of this
* fact. For example, when a control within a row is focused, it will have
* tabIndex = -1 and ironListTabIndex = 0.
* @type {number}
*/
ironListTabIndex: {
type: Number,
observer: 'ironListTabIndexChanged_',
},
},
/** @override */
attached: function() {
this.classList.add('no-outline');
Polymer.RenderStatus.afterNextRender(this, function() {
const rowContainer = this.root.querySelector('[focus-row-container]');
assert(!!rowContainer);
this.row_ = new VirtualFocusRow(rowContainer, new FocusRowDelegate(this));
this.ironListTabIndexChanged_();
this.addItems_();
// Adding listeners asynchronously to reduce blocking time, since this
// behavior will be used by items in potentially long lists.
this.listen(this, 'focus', 'onFocus_');
this.listen(this, 'dom-change', 'addItems_');
this.listen(this, 'mousedown', 'onMouseDown_');
this.listen(this, 'blur', 'onBlur_');
});
},
/** @override */
detached: function() {
this.unlisten(this, 'focus', 'onFocus_');
this.unlisten(this, 'dom-change', 'addItems_');
this.unlisten(this, 'mousedown', 'onMouseDown_');
this.unlisten(this, 'blur', 'onBlur_');
if (this.row_)
this.row_.destroy();
},
/** @private */
addItems_: function() {
if (this.row_) {
this.row_.destroy();
const controls = this.root.querySelectorAll('[focus-row-control]');
for (let i = 0; i < controls.length; i++) {
this.row_.addItem(
controls[i].getAttribute('focus-type'),
/** @type {HTMLElement} */ (controls[i]));
}
}
},
/**
* This function gets called when the row itself receives the focus event.
* @private
*/
onFocus_: function() {
if (this.mouseFocused_) {
this.mouseFocused_ = false; // Consume and reset flag.
return;
}
if (this.lastFocused) {
this.row_.getEquivalentElement(this.lastFocused).focus();
} else {
const firstFocusable = assert(this.row_.getFirstFocusable());
firstFocusable.focus();
}
this.tabIndex = -1;
},
/** @private */
ironListTabIndexChanged_: function() {
if (this.row_)
this.row_.makeActive(this.ironListTabIndex == 0);
},
/** @private */
onMouseDown_: function() {
this.mouseFocused_ = true; // Set flag to not do any control-focusing.
},
/** @private */
onBlur_: function() {
this.mouseFocused_ = false; // Reset flag since it's not active anymore.
}
};