| // 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. |
| } |
| }; |