1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 cr.define('cr.ui', function() {
7 * A class to manage focus between given horizontally arranged elements.
8 * For example, given the page:
10 * <input type="checkbox"> <label>Check me!</label> <button>X</button>
12 * One could create a FocusRow by doing:
14 * new cr.ui.FocusRow([checkboxEl, labelEl, buttonEl])
16 * if there are references to each node or querying them from the DOM like so:
18 * new cr.ui.FocusRow(dialog.querySelectorAll('list input[type=checkbox]'))
20 * Pressing left cycles backward and pressing right cycles forward in item
21 * order. Pressing Home goes to the beginning of the list and End goes to the
24 * If an item in this row is focused, it'll stay active (accessible via tab).
25 * If no items in this row are focused, the row can stay active until focus
26 * changes to a node inside |this.boundary_|. If opt_boundary isn't
27 * specified, any focus change deactivates the row.
29 * @param {!Array.<!Element>|!NodeList} items Elements to track focus of.
30 * @param {Node=} opt_boundary Focus events are ignored outside of this node.
31 * @param {FocusRow.Delegate=} opt_delegate A delegate to handle key events.
32 * @param {FocusRow.Observer=} opt_observer An observer that's notified if
33 * this focus row is added to or removed from the focus order.
36 function FocusRow(items, opt_boundary, opt_delegate, opt_observer) {
37 /** @type {!Array.<!Element>} */
38 this.items = Array.prototype.slice.call(items);
39 assert(this.items.length > 0);
42 this.boundary_ = opt_boundary || document;
44 /** @private {cr.ui.FocusRow.Delegate|undefined} */
45 this.delegate_ = opt_delegate;
47 /** @private {cr.ui.FocusRow.Observer|undefined} */
48 this.observer_ = opt_observer;
50 /** @private {!EventTracker} */
51 this.eventTracker_ = new EventTracker;
52 this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this));
53 this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this));
55 this.items.forEach(function(item) {
56 if (item != document.activeElement)
59 this.eventTracker_.add(item, 'mousedown', this.onMousedown_.bind(this));
63 * The index that should be actively participating in the page tab order.
67 this.activeIndex_ = this.items.indexOf(document.activeElement);
71 FocusRow.Delegate = function() {};
73 FocusRow.Delegate.prototype = {
75 * Called when a key is pressed while an item in |this.items| is focused. If
76 * |e|'s default is prevented, further processing is skipped.
77 * @param {cr.ui.FocusRow} row The row that detected a keydown.
79 * @return {boolean} Whether the event was handled.
81 onKeydown: assertNotReached,
84 * @param {cr.ui.FocusRow} row The row that detected the mouse going down.
86 * @return {boolean} Whether the event was handled.
88 onMousedown: assertNotReached,
92 FocusRow.Observer = function() {};
94 FocusRow.Observer.prototype = {
96 * Called when the row is activated (added to the focus order).
97 * @param {cr.ui.FocusRow} row The row added to the focus order.
99 onActivate: assertNotReached,
102 * Called when the row is deactivated (removed from the focus order).
103 * @param {cr.ui.FocusRow} row The row removed from the focus order.
105 onDeactivate: assertNotReached,
108 FocusRow.prototype = {
110 return this.activeIndex_;
112 set activeIndex(index) {
113 var wasActive = this.items[this.activeIndex_];
115 wasActive.tabIndex = -1;
117 this.items.forEach(function(item) { assert(item.tabIndex == -1); });
118 this.activeIndex_ = index;
120 if (this.items[index])
121 this.items[index].tabIndex = 0;
126 var isActive = index >= 0 && index < this.items.length;
127 if (isActive == !!wasActive)
131 this.observer_.onActivate(this);
133 this.observer_.onDeactivate(this);
137 * Focuses the item at |index|.
138 * @param {number} index An index to focus. Must be between 0 and
139 * this.items.length - 1.
141 focusIndex: function(index) {
142 this.items[index].focus();
145 /** Call this to clean up event handling before dereferencing. */
146 destroy: function() {
147 this.eventTracker_.removeAll();
151 * @param {Event} e The focusin event.
154 onFocusin_: function(e) {
155 if (this.boundary_.contains(assertInstanceof(e.target, Node)))
156 this.activeIndex = this.items.indexOf(e.target);
160 * @param {Event} e A focus event.
163 onKeydown_: function(e) {
164 var item = this.items.indexOf(e.target);
168 if (this.delegate_ && this.delegate_.onKeydown(this, e))
173 if (e.keyIdentifier == 'Left')
174 index = item + (isRTL() ? 1 : -1);
175 else if (e.keyIdentifier == 'Right')
176 index = item + (isRTL() ? -1 : 1);
177 else if (e.keyIdentifier == 'Home')
179 else if (e.keyIdentifier == 'End')
180 index = this.items.length - 1;
182 if (!this.items[index])
185 this.focusIndex(index);
190 * @param {Event} e A click event.
193 onMousedown_: function(e) {
194 if (this.delegate_ && this.delegate_.onMousedown(this, e))
198 this.activeIndex = this.items.indexOf(e.currentTarget);