• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5cr.define('cr.ui', function() {
6  /**
7   * A class to manage focus between given horizontally arranged elements.
8   * For example, given the page:
9   *
10   *   <input type="checkbox"> <label>Check me!</label> <button>X</button>
11   *
12   * One could create a FocusRow by doing:
13   *
14   *   new cr.ui.FocusRow([checkboxEl, labelEl, buttonEl])
15   *
16   * if there are references to each node or querying them from the DOM like so:
17   *
18   *   new cr.ui.FocusRow(dialog.querySelectorAll('list input[type=checkbox]'))
19   *
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
22   * end of the list.
23   *
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.
28   *
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.
34   * @constructor
35   */
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);
40
41    /** @type {!Node} */
42    this.boundary_ = opt_boundary || document;
43
44    /** @private {cr.ui.FocusRow.Delegate|undefined} */
45    this.delegate_ = opt_delegate;
46
47    /** @private {cr.ui.FocusRow.Observer|undefined} */
48    this.observer_ = opt_observer;
49
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));
54
55    this.items.forEach(function(item) {
56      if (item != document.activeElement)
57        item.tabIndex = -1;
58
59      this.eventTracker_.add(item, 'click', this.onClick_.bind(this));
60    }, this);
61
62    /**
63     * The index that should be actively participating in the page tab order.
64     * @type {number}
65     * @private
66     */
67    this.activeIndex_ = this.items.indexOf(document.activeElement);
68  }
69
70  /** @interface */
71  FocusRow.Delegate = function() {};
72
73  FocusRow.Delegate.prototype = {
74    /**
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.
78     * @param {Event} e The keydown event.
79     */
80    onKeydown: assertNotReached,
81  };
82
83  /** @interface */
84  FocusRow.Observer = function() {};
85
86  FocusRow.Observer.prototype = {
87    /**
88     * Called when the row is activated (added to the focus order).
89     * @param {cr.ui.FocusRow} row The row added to the focus order.
90     */
91    onActivate: assertNotReached,
92
93    /**
94     * Called when the row is deactivated (removed from the focus order).
95     * @param {cr.ui.FocusRow} row The row removed from the focus order.
96     */
97    onDeactivate: assertNotReached,
98  };
99
100  FocusRow.prototype = {
101    get activeIndex() {
102      return this.activeIndex_;
103    },
104    set activeIndex(index) {
105      var wasActive = this.items[this.activeIndex_];
106      if (wasActive)
107        wasActive.tabIndex = -1;
108
109      this.items.forEach(function(item) { assert(item.tabIndex == -1); });
110      this.activeIndex_ = index;
111
112      if (this.items[index])
113        this.items[index].tabIndex = 0;
114
115      if (!this.observer_)
116        return;
117
118      var isActive = index >= 0 && index < this.items.length;
119      if (isActive == !!wasActive)
120        return;
121
122      if (isActive)
123        this.observer_.onActivate(this);
124      else
125        this.observer_.onDeactivate(this);
126    },
127
128    /**
129     * Focuses the item at |index|.
130     * @param {number} index An index to focus. Must be between 0 and
131     *     this.items.length - 1.
132     */
133    focusIndex: function(index) {
134      this.items[index].focus();
135    },
136
137    /** Call this to clean up event handling before dereferencing. */
138    destroy: function() {
139      this.eventTracker_.removeAll();
140    },
141
142    /**
143     * @param {Event} e The focusin event.
144     * @private
145     */
146    onFocusin_: function(e) {
147      if (this.boundary_.contains(e.target))
148        this.activeIndex = this.items.indexOf(e.target);
149    },
150
151    /**
152     * @param {Event} e A focus event.
153     * @private
154     */
155    onKeydown_: function(e) {
156      var item = this.items.indexOf(e.target);
157      if (item < 0)
158        return;
159
160      if (this.delegate_)
161        this.delegate_.onKeydown(this, e);
162
163      if (e.defaultPrevented)
164        return;
165
166      var index = -1;
167
168      if (e.keyIdentifier == 'Left')
169        index = item + (isRTL() ? 1 : -1);
170      else if (e.keyIdentifier == 'Right')
171        index = item + (isRTL() ? -1 : 1);
172      else if (e.keyIdentifier == 'Home')
173        index = 0;
174      else if (e.keyIdentifier == 'End')
175        index = this.items.length - 1;
176
177      if (!this.items[index])
178        return;
179
180      this.focusIndex(index);
181      e.preventDefault();
182    },
183
184    /**
185     * @param {Event} e A click event.
186     * @private
187     */
188    onClick_: function(e) {
189      if (!e.button)
190        this.activeIndex = this.items.indexOf(e.currentTarget);
191    },
192  };
193
194  return {
195    FocusRow: FocusRow,
196  };
197});
198