Chromium Code Reviews| Index: ui/webui/resources/js/cr/ui/focus_row.js |
| diff --git a/ui/webui/resources/js/cr/ui/focus_row.js b/ui/webui/resources/js/cr/ui/focus_row.js |
| index 26c4c5567e2b673f24b19574c2b35afe6c69263f..5a8426141aa455be595e5319e9a008b8fd07a30f 100644 |
| --- a/ui/webui/resources/js/cr/ui/focus_row.js |
| +++ b/ui/webui/resources/js/cr/ui/focus_row.js |
| @@ -26,25 +26,21 @@ cr.define('cr.ui', function() { |
| * changes to a node inside |this.boundary_|. If opt_boundary isn't |
| * specified, any focus change deactivates the row. |
| * |
| - * @param {!Array.<!Element>|!NodeList} items Elements to track focus of. |
| * @param {Node=} opt_boundary Focus events are ignored outside of this node. |
| * @param {FocusRow.Delegate=} opt_delegate A delegate to handle key events. |
| * @param {FocusRow.Observer=} opt_observer An observer that's notified if |
| * this focus row is added to or removed from the focus order. |
| * @constructor |
| */ |
| - function FocusRow(items, opt_boundary, opt_delegate, opt_observer) { |
| - /** @type {!Array.<!Element>} */ |
| - this.items = Array.prototype.slice.call(items); |
| - assert(this.items.length > 0); |
| - |
| + function FocusRow(opt_boundary, opt_delegate, opt_observer) { |
| /** @type {!Node} */ |
| this.boundary_ = opt_boundary || document; |
| /** @private {cr.ui.FocusRow.Delegate|undefined} */ |
| this.delegate_ = opt_delegate; |
| - /** @private {cr.ui.FocusRow.Observer|undefined} */ |
| + /** @private {cr.ui.FocusRow.Observer} */ |
| + assert(opt_observer); |
| this.observer_ = opt_observer; |
| /** @private {!EventTracker} */ |
| @@ -52,19 +48,11 @@ cr.define('cr.ui', function() { |
| this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this)); |
| this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this)); |
| - this.items.forEach(function(item) { |
| - if (item != document.activeElement) |
| - item.tabIndex = -1; |
| - |
| - this.eventTracker_.add(item, 'mousedown', this.onMousedown_.bind(this)); |
| - }, this); |
| + /** @type {Array<string>} */ |
| + this.elementIds = []; |
| - /** |
| - * The index that should be actively participating in the page tab order. |
| - * @type {number} |
| - * @private |
| - */ |
| - this.activeIndex_ = this.items.indexOf(document.activeElement); |
| + /** @private {Element} */ |
| + this.rowElement_ = null; |
| } |
| /** @interface */ |
| @@ -72,8 +60,8 @@ cr.define('cr.ui', function() { |
| FocusRow.Delegate.prototype = { |
| /** |
| - * Called when a key is pressed while an item in |this.items| is focused. If |
| - * |e|'s default is prevented, further processing is skipped. |
| + * Called when a key is pressed while an item in |this.getItems()| is |
| + * focused. If |e|'s default is prevented, further processing is skipped. |
| * @param {cr.ui.FocusRow} row The row that detected a keydown. |
| * @param {Event} e |
| * @return {boolean} Whether the event was handled. |
| @@ -103,43 +91,208 @@ cr.define('cr.ui', function() { |
| * @param {cr.ui.FocusRow} row The row removed from the focus order. |
| */ |
| onDeactivate: assertNotReached, |
| + |
| + /** |
| + * Called when adding rowItems to the FocusRow to determine the element that |
| + * represents the row. This should return the same element regardless of the |
| + * rowItem it it called on for a specific row. |
| + * @param {Element} rowItem The item to find a row element for. |
| + * @return {Element} |rowItem|'s row element. |
| + */ |
| + getRowElement: assertNotReached, |
| + |
| + /** |
| + * Called whenever there is a change in rowElement focus and the elementId |
| + * is not in the FocusRow. |
| + * @param {cr.ui.FocusRow} row The row that is being focused. |
| + * @param {string} expectedId The id that was not found. |
| + * @return {string} The id in |row| that should be focused. |
| + */ |
| + onElementIdMiss: assertNotReached, |
|
Evan Stade
2015/01/15 23:38:09
this name is super confusing to me. It should be s
hcarmona
2015/01/16 21:39:06
Done
|
| }; |
| FocusRow.prototype = { |
| - get activeIndex() { |
| - return this.activeIndex_; |
| + /** |
| + * @param {Element} element The element whose id is needed. |
| + * @return {?string} |element|'s elementId. null if element is not in this |
| + * FocusRow. |
| + */ |
| + getElementId: function(element) { |
| + if (!this.rowElement_.contains(element)) |
| + return null; |
| + return element.getAttribute("focus-row-element-id"); |
| }, |
| - set activeIndex(index) { |
| - var wasActive = this.items[this.activeIndex_]; |
| - if (wasActive) |
| - wasActive.tabIndex = -1; |
| - this.items.forEach(function(item) { assert(item.tabIndex == -1); }); |
| - this.activeIndex_ = index; |
| + /** |
| + * @param {string} elementId |
| + * @return {?Element} The element in this FocusRow with elementId. null if |
| + * not in this FocusRow. |
| + */ |
| + getElement: function (elementId) { |
|
Evan Stade
2015/01/15 23:38:09
nit: extra space
hcarmona
2015/01/16 21:39:06
Done.
|
| + var element = this.rowElement_.querySelector('[focus-row-element-id="' + |
| + elementId + '"]'); |
| - if (this.items[index]) |
| - this.items[index].tabIndex = 0; |
| + // Special case when the column is the row. |
| + if (!element && elementId && |
| + this.rowElement_.getAttribute('focus-row-element-id') == elementId) |
| + return this.rowElement_; |
| - if (!this.observer_) |
| + return element; |
| + }, |
| + |
| + /** |
| + * @return {Element} The row element that contains all focusable row items. |
| + */ |
| + getRowElement: function() { |
| + return this.rowElement_; |
| + }, |
| + |
| + /** |
| + * @return {[Element]} An array with all row items in this row. Empty array |
| + * if nothing is focusable. |
| + */ |
| + getItems: function() { |
| + assert(this.rowElement_); |
| + var items = this.rowElement_.querySelectorAll('[focus-row-element-id]'); |
| + |
| + // Special case when the column is the row. |
| + if (items.length == 0 && |
| + this.rowElement_.hasAttribute('focus-row-element-id')) |
| + return [ this.rowElement_ ]; |
| + |
| + return items; |
| + }, |
| + |
| + /** |
| + * Add an element to this FocusRow with the given elementId. No-op if either |
| + * |element| or |elementId| is not provided. |
| + * @param {Element} element The element that should be added. |
| + * @param {string} elementId The elementId that should be used to find |
| + * similar elements in the FocusRow. This MUST be unique for each row. |
| + */ |
| + setFocusableElementId: function(element, elementId) { |
| + if (!element || !elementId) |
| return; |
| - var isActive = index >= 0 && index < this.items.length; |
| - if (isActive == !!wasActive) |
| + assert(this.elementIds.indexOf(elementId) == -1); |
| + element.setAttribute('focus-row-element-id', elementId); |
| + this.elementIds.push(elementId); |
| + this.eventTracker_.add(element, 'mousedown', |
| + this.onMousedown_.bind(this)); |
| + |
| + if (!this.rowElement_) |
| + this.rowElement_ = this.observer_.getRowElement(element); |
| + else |
| + assert(this.rowElement_ == this.observer_.getRowElement(element)); |
| + }, |
| + |
| + /** |
| + * @param {bool} focused Whether the initial focus for this row is enabled |
| + * or disabled. |
| + */ |
| + setInitialFocus: function(focused) { |
| + if (focused) |
| + this.onFocusIdChange(this.elementIds[0]); |
| + else |
| + this.enableRowTab(false); |
| + }, |
| + |
| + /** |
| + * Called when focus changes to activate/deactivate the row. Focus is |
| + * removed from the row when |elementId| is not in the FocusRow. |
| + * @param {string} elementId The elementId that has focus. |
| + */ |
| + onFocusIdChange: function(elementId) { |
| + var element = this.getElement(elementId); |
| + var rowClasses = this.rowElement_.classList; |
| + var wasActive = rowClasses.contains('focus-row-active'); |
| + |
| + if (element) { |
| + // Verify that the focus hasn't changed. This allows the FocusGrid to go |
| + // back to the same focused element on a miss. |
| + this.focusChanged_ = this.focusChanged_ || |
| + elementId != this.lastFocusedElementId_; |
| + // Keep track of the last elementId that was focused. |
| + this.lastFocusedElementId_ = elementId; |
| + |
| + this.enableRowTab(true); |
| + } else if (wasActive) |
| + this.enableRowTab(false); |
| + |
| + // Only send events if the active state is different for the row. |
| + if (!!element == wasActive) |
| return; |
| - if (isActive) |
| + if (element) { |
| + rowClasses.add('focus-row-active'); |
| this.observer_.onActivate(this); |
| - else |
| + } else { |
| + rowClasses.remove('focus-row-active'); |
| this.observer_.onDeactivate(this); |
| + } |
| + }, |
| + |
| + /** |
| + * Enables/disables the tabIndex of the focusable elements in the FocusRow. |
| + * tabIndex can be set properly. |
| + * @param {bool} allow True if tab is allowed for this row. |
| + */ |
| + enableRowTab: function(allow) { |
|
dmazzoni
2015/01/15 19:38:19
I'd call this something like makeRowFocusable or a
hcarmona
2015/01/16 21:39:06
Done.
|
| + var items = this.getItems(); |
| + for (var i = 0; i < items.length; ++i) |
| + items[i].tabIndex = allow ? 0 : -1; |
| }, |
| /** |
| - * Focuses the item at |index|. |
| - * @param {number} index An index to focus. Must be between 0 and |
| - * this.items.length - 1. |
| + * Will choose an appropriate element to focus. |
| + * @param {string} elementId The element id that should be focused. |
| + * @return {Element} A focusable element that best matches |elementId|. |
|
Evan Stade
2015/01/15 23:38:09
I am confused how you could "best match" an ID. ID
hcarmona
2015/01/16 21:39:06
Done.
|
| */ |
| - focusIndex: function(index) { |
| - this.items[index].focus(); |
| + getFocusableElement: function(elementId) { |
| + if (!elementId) |
| + return null; |
| + |
| + /** Priority for focus is: |
| + * 1. Focusable element with same elementId |
| + * 2. Let the delegate decide what should be focused |
| + * 3. Focus the first focusable element |
| + */ |
| + return this.getElement(elementId) || |
| + this.getElement(this.observer_.onElementIdMiss(this, elementId)) || |
| + this.getElement(this.elementIds[0]); |
|
dmazzoni
2015/01/15 19:38:19
Just checking, this works fine on an empty list /
hcarmona
2015/01/16 21:39:06
An empty grid would have no rows, so it should be
|
| + }, |
| + |
| + /** |
| + * Called to set focus to a given row item based on the elementId. Will |
| + * choose an appropriate element if |elementId| is not in the FocusRow. |
| + * @param {string} elementId The element that should be focused. |
| + */ |
| + setFocusId: function(elementId) { |
| + var element = this.getFocusableElement(elementId); |
| + if (element) |
| + element.focus(); |
| + }, |
| + |
| + /** @private {string} */ |
| + lastFocusedElementId_: null, |
| + |
| + /** @private {bool} */ |
| + focusChanged_: false, |
| + |
| + /** |
| + * Will reset the private focusChanged_ variable so that a change in focus |
| + * can be tracked. |
| + */ |
| + trackFocus: function() { |
| + this.focusChanged_ = false; |
| + }, |
| + |
| + /** |
| + * @return {bool} Whether the column focus changed in this row since it was |
| + * last asked to trackFocus(). |
| + */ |
| + focusChanged: function() { |
| + return this.focusChanged_; |
| }, |
| /** Call this to clean up event handling before dereferencing. */ |
| @@ -153,7 +306,7 @@ cr.define('cr.ui', function() { |
| */ |
| onFocusin_: function(e) { |
| if (this.boundary_.contains(assertInstanceof(e.target, Node))) |
| - this.activeIndex = this.items.indexOf(e.target); |
| + this.onFocusIdChange(this.getElementId(e.target)); |
| }, |
| /** |
| @@ -161,29 +314,30 @@ cr.define('cr.ui', function() { |
| * @private |
| */ |
| onKeydown_: function(e) { |
| - var item = this.items.indexOf(e.target); |
| - if (item < 0) |
| + if (!this.rowElement_.contains(e.target)) |
| return; |
| if (this.delegate_ && this.delegate_.onKeydown(this, e)) |
| return; |
| + var focusId = this.getElementId(e.target); |
| + var elementIndex = this.elementIds.indexOf(focusId); |
| var index = -1; |
| if (e.keyIdentifier == 'Left') |
| - index = item + (isRTL() ? 1 : -1); |
| + index = elementIndex + (isRTL() ? 1 : -1); |
| else if (e.keyIdentifier == 'Right') |
| - index = item + (isRTL() ? -1 : 1); |
| + index = elementIndex + (isRTL() ? -1 : 1); |
| else if (e.keyIdentifier == 'Home') |
| index = 0; |
| else if (e.keyIdentifier == 'End') |
| - index = this.items.length - 1; |
| + index = this.elementIds.length - 1; |
| - if (!this.items[index]) |
| - return; |
| - |
| - this.focusIndex(index); |
| - e.preventDefault(); |
| + focusId = this.elementIds[index]; |
| + if (focusId) { |
| + this.setFocusId(focusId); |
| + e.preventDefault(); |
| + } |
| }, |
| /** |
| @@ -191,11 +345,18 @@ cr.define('cr.ui', function() { |
| * @private |
| */ |
| onMousedown_: function(e) { |
| + if (!this.rowElement_.contains(e.target)) |
| + return; |
| + |
| if (this.delegate_ && this.delegate_.onMousedown(this, e)) |
| return; |
| - if (!e.button) |
| - this.activeIndex = this.items.indexOf(e.currentTarget); |
| + // Only accept the left mouse click. |
| + if (!e.button) { |
| + // Focus this row if the target is one of the elements in this row. |
| + this.onFocusIdChange(this.getElementId(e.target)); |
| + e.preventDefault(); |
| + } |
| }, |
| }; |