Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(5615)

Unified Diff: chrome/browser/resources/md_downloads/crisper.js

Issue 1824963002: MD Downloads: re-vulcanize to pick up new changes (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@downloads-double
Patch Set: Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | chrome/browser/resources/md_downloads/vulcanized.html » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: chrome/browser/resources/md_downloads/crisper.js
diff --git a/chrome/browser/resources/md_downloads/crisper.js b/chrome/browser/resources/md_downloads/crisper.js
index 9ff07deae87c16151f4ad3c13d97404a42d4fa20..352b5bf7f50cd2f7d5ad20653ec41e7da06002c0 100644
--- a/chrome/browser/resources/md_downloads/crisper.js
+++ b/chrome/browser/resources/md_downloads/crisper.js
@@ -1,3 +1,38 @@
+// Copyright 2016 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.
+
+/**
+ * @fileoverview PromiseResolver is a helper class that allows creating a
+ * Promise that will be fulfilled (resolved or rejected) some time later.
+ *
+ * Example:
+ * var resolver = new PromiseResolver();
+ * resolver.promise.then(function(result) {
+ * console.log('resolved with', result);
+ * });
+ * ...
+ * ...
+ * resolver.resolve({hello: 'world'});
+ */
+
+/**
+ * @constructor @struct
+ * @template T
+ */
+function PromiseResolver() {
+ /** @type {function(T): void} */
+ this.resolve;
+
+ /** @type {function(*=): void} */
+ this.reject;
+
+ /** @type {!Promise<T>} */
+ this.promise = new Promise(function(resolve, reject) {
+ this.resolve = resolve;
+ this.reject = reject;
+ }.bind(this));
+};
// Copyright (c) 2012 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.
@@ -13,7 +48,7 @@ var global = this;
var WebUIListener;
/** Platform, package, object property, and Event support. **/
-var cr = function() {
+var cr = cr || function() {
'use strict';
/**
@@ -25,6 +60,8 @@ var cr = function() {
* @param {*=} opt_object The object to expose at the end of the path.
* @param {Object=} opt_objectToExportTo The object to add the path to;
* default is {@code global}.
+ * @return {!Object} The last object exported (i.e. exportPath('cr.ui')
+ * returns a reference to the ui property of window.cr).
* @private
*/
function exportPath(name, opt_object, opt_objectToExportTo) {
@@ -42,7 +79,7 @@ var cr = function() {
}
}
return cur;
- };
+ }
/**
* Fires a property change event on the target.
@@ -318,9 +355,9 @@ var cr = function() {
/**
* The mapping used by the sendWithPromise mechanism to tie the Promise
* returned to callers with the corresponding WebUI response. The mapping is
- * from ID to the Promise resolver function; the ID is generated by
+ * from ID to the PromiseResolver helper; the ID is generated by
* sendWithPromise and is unique across all invocations of said method.
- * @type {!Object<!Function>}
+ * @type {!Object<!PromiseResolver>}
*/
var chromeSendResolverMap = {};
@@ -334,12 +371,17 @@ var cr = function() {
* supply any number of other arguments that will be included in the response.
* @param {string} id The unique ID identifying the Promise this response is
* tied to.
+ * @param {boolean} isSuccess Whether the request was successful.
* @param {*} response The response as sent from C++.
*/
- function webUIResponse(id, response) {
- var resolverFn = chromeSendResolverMap[id];
+ function webUIResponse(id, isSuccess, response) {
+ var resolver = chromeSendResolverMap[id];
delete chromeSendResolverMap[id];
- resolverFn(response);
+
+ if (isSuccess)
+ resolver.resolve(response);
+ else
+ resolver.reject(response);
}
/**
@@ -352,11 +394,11 @@ var cr = function() {
*/
function sendWithPromise(methodName, var_args) {
var args = Array.prototype.slice.call(arguments, 1);
- return new Promise(function(resolve, reject) {
- var id = methodName + '_' + createUid();
- chromeSendResolverMap[id] = resolve;
- chrome.send(methodName, [id].concat(args));
- });
+ var promiseResolver = new PromiseResolver();
+ var id = methodName + '_' + createUid();
+ chromeSendResolverMap[id] = promiseResolver;
+ chrome.send(methodName, [id].concat(args));
+ return promiseResolver.promise;
}
/**
@@ -1470,6 +1512,15 @@ function elide(original, maxLength) {
if (original.length <= maxLength)
return original;
return original.substring(0, maxLength - 1) + '\u2026';
+}
+
+/**
+ * Quote a string so it can be used in a regular expression.
+ * @param {string} str The source string.
+ * @return {string} The escaped string.
+ */
+function quoteString(str) {
+ return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
};
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
@@ -1840,11 +1891,13 @@ i18nTemplate.process(document, loadTimeData);
* size or hidden state of their children) and "resizables" (elements that need to be
* notified when they are resized or un-hidden by their parents in order to take
* action on their new measurements).
+ *
* Elements that perform measurement should add the `IronResizableBehavior` behavior to
* their element definition and listen for the `iron-resize` event on themselves.
* This event will be fired when they become showing after having been hidden,
* when they are resized explicitly by another resizable, or when the window has been
* resized.
+ *
* Note, the `iron-resize` event is non-bubbling.
*
* @polymerBehavior Polymer.IronResizableBehavior
@@ -2508,9 +2561,9 @@ i18nTemplate.process(document, loadTimeData);
// Support element id references
if (typeof scrollTarget === 'string') {
- var ownerRoot = Polymer.dom(this).getOwnerRoot();
- this.scrollTarget = (ownerRoot && ownerRoot.$) ?
- ownerRoot.$[scrollTarget] : Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
+ var host = this.domHost;
+ this.scrollTarget = host && host.$ ? host.$[scrollTarget] :
+ Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
} else if (this._scrollHandler) {
@@ -2609,15 +2662,15 @@ i18nTemplate.process(document, loadTimeData);
* Scrolls the content to a particular place.
*
* @method scroll
- * @param {number} top The top position
* @param {number} left The left position
+ * @param {number} top The top position
*/
- scroll: function(top, left) {
+ scroll: function(left, top) {
if (this.scrollTarget === this._doc) {
- window.scrollTo(top, left);
+ window.scrollTo(left, top);
} else if (this._isValidScrollTarget()) {
- this.scrollTarget.scrollTop = top;
this.scrollTarget.scrollLeft = left;
+ this.scrollTarget.scrollTop = top;
}
},
@@ -2774,7 +2827,7 @@ i18nTemplate.process(document, loadTimeData);
_ratio: 0.5,
/**
- * The padding-top value of the `scroller` element
+ * The padding-top value for the list.
*/
_scrollerPaddingTop: 0,
@@ -2784,21 +2837,6 @@ i18nTemplate.process(document, loadTimeData);
_scrollPosition: 0,
/**
- * The number of tiles in the DOM.
- */
- _physicalCount: 0,
-
- /**
- * The k-th tile that is at the top of the scrolling list.
- */
- _physicalStart: 0,
-
- /**
- * The k-th tile that is at the bottom of the scrolling list.
- */
- _physicalEnd: 0,
-
- /**
* The sum of the heights of all the tiles in the DOM.
*/
_physicalSize: 0,
@@ -2825,11 +2863,6 @@ i18nTemplate.process(document, loadTimeData);
_virtualCount: 0,
/**
- * The n-th item rendered in the `_physicalStart` tile.
- */
- _virtualStartVal: 0,
-
- /**
* A map between an item key and its physical item index
*/
_physicalIndexForKey: null,
@@ -2875,7 +2908,6 @@ i18nTemplate.process(document, loadTimeData);
*/
_lastVisibleIndexVal: null,
-
/**
* A Polymer collection for the items.
* @type {?Polymer.Collection}
@@ -2899,9 +2931,14 @@ i18nTemplate.process(document, loadTimeData);
_maxPages: 3,
/**
- * The currently focused item index.
+ * The currently focused physical item.
*/
- _focusedIndex: 0,
+ _focusedItem: null,
+
+ /**
+ * The index of the `_focusedItem`.
+ */
+ _focusedIndex: -1,
/**
* The the item that is focused if it is moved offscreen.
@@ -2937,6 +2974,20 @@ i18nTemplate.process(document, loadTimeData);
},
/**
+ * The height of the physical content that isn't on the screen.
+ */
+ get _hiddenContentSize() {
+ return this._physicalSize - this._viewportSize;
+ },
+
+ /**
+ * The maximum scroll top value.
+ */
+ get _maxScrollTop() {
+ return this._estScrollHeight - this._viewportSize + this._scrollerPaddingTop;
+ },
+
+ /**
* The lowest n-th value for an item such that it can be rendered in `_physicalStart`.
*/
_minVirtualStart: 0,
@@ -2949,41 +3000,54 @@ i18nTemplate.process(document, loadTimeData);
},
/**
- * The height of the physical content that isn't on the screen.
+ * The n-th item rendered in the `_physicalStart` tile.
*/
- get _hiddenContentSize() {
- return this._physicalSize - this._viewportSize;
+ _virtualStartVal: 0,
+
+ set _virtualStart(val) {
+ this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
},
- /**
- * The maximum scroll top value.
- */
- get _maxScrollTop() {
- return this._estScrollHeight - this._viewportSize;
+ get _virtualStart() {
+ return this._virtualStartVal || 0;
},
/**
- * Sets the n-th item rendered in `_physicalStart`
+ * The k-th tile that is at the top of the scrolling list.
*/
- set _virtualStart(val) {
- // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart
- this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
- if (this._physicalCount === 0) {
- this._physicalStart = 0;
- this._physicalEnd = 0;
- } else {
- this._physicalStart = this._virtualStartVal % this._physicalCount;
- this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
+ _physicalStartVal: 0,
+
+ set _physicalStart(val) {
+ this._physicalStartVal = val % this._physicalCount;
+ if (this._physicalStartVal < 0) {
+ this._physicalStartVal = this._physicalCount + this._physicalStartVal;
}
+ this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
+ },
+
+ get _physicalStart() {
+ return this._physicalStartVal || 0;
},
/**
- * Gets the n-th item rendered in `_physicalStart`
+ * The number of tiles in the DOM.
*/
- get _virtualStart() {
- return this._virtualStartVal;
+ _physicalCountVal: 0,
+
+ set _physicalCount(val) {
+ this._physicalCountVal = val;
+ this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
},
+ get _physicalCount() {
+ return this._physicalCountVal;
+ },
+
+ /**
+ * The k-th tile that is at the bottom of the scrolling list.
+ */
+ _physicalEnd: 0,
+
/**
* An optimal physical size such that we will have enough physical items
* to fill up the viewport and recycle when the user scrolls.
@@ -3009,12 +3073,11 @@ i18nTemplate.process(document, loadTimeData);
*/
get firstVisibleIndex() {
if (this._firstVisibleIndexVal === null) {
- var physicalOffset = this._physicalTop;
+ var physicalOffset = this._physicalTop + this._scrollerPaddingTop;
this._firstVisibleIndexVal = this._iterateItems(
function(pidx, vidx) {
physicalOffset += this._physicalSizes[pidx];
-
if (physicalOffset > this._scrollPosition) {
return vidx;
}
@@ -3035,14 +3098,18 @@ i18nTemplate.process(document, loadTimeData);
this._iterateItems(function(pidx, vidx) {
physicalOffset += this._physicalSizes[pidx];
- if(physicalOffset <= this._scrollBottom) {
- this._lastVisibleIndexVal = vidx;
+ if (physicalOffset <= this._scrollBottom) {
+ this._lastVisibleIndexVal = vidx;
}
});
}
return this._lastVisibleIndexVal;
},
+ get _defaultScrollTarget() {
+ return this;
+ },
+
ready: function() {
this.addEventListener('focus', this._didFocus.bind(this), true);
},
@@ -3056,10 +3123,6 @@ i18nTemplate.process(document, loadTimeData);
this._itemsRendered = false;
},
- get _defaultScrollTarget() {
- return this;
- },
-
/**
* Set the overflow property if this element has its own scrolling region
*/
@@ -3075,8 +3138,9 @@ i18nTemplate.process(document, loadTimeData);
* @method updateViewportBoundaries
*/
updateViewportBoundaries: function() {
- var scrollerStyle = window.getComputedStyle(this.scrollTarget);
- this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10);
+ this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
+ parseInt(window.getComputedStyle(this)['padding-top'], 10);
+
this._viewportSize = this._scrollTargetHeight;
},
@@ -3086,12 +3150,10 @@ i18nTemplate.process(document, loadTimeData);
*/
_scrollHandler: function() {
// clamp the `scrollTop` value
- // IE 10|11 scrollTop may go above `_maxScrollTop`
- // iOS `scrollTop` may go below 0 and above `_maxScrollTop`
var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop));
+ var delta = scrollTop - this._scrollPosition;
var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBottom;
var ratio = this._ratio;
- var delta = scrollTop - this._scrollPosition;
var recycledTiles = 0;
var hiddenContentSize = this._hiddenContentSize;
var currentRatio = ratio;
@@ -3100,7 +3162,7 @@ i18nTemplate.process(document, loadTimeData);
// track the last `scrollTop`
this._scrollPosition = scrollTop;
- // clear cached visible index
+ // clear cached visible indexes
this._firstVisibleIndexVal = null;
this._lastVisibleIndexVal = null;
@@ -3187,6 +3249,7 @@ i18nTemplate.process(document, loadTimeData);
}
} else {
this._virtualStart = this._virtualStart + recycledTiles;
+ this._physicalStart = this._physicalStart + recycledTiles;
this._update(recycledTileSet, movingUp);
}
},
@@ -3198,11 +3261,7 @@ i18nTemplate.process(document, loadTimeData);
*/
_update: function(itemSet, movingUp) {
// manage focus
- if (this._isIndexRendered(this._focusedIndex)) {
- this._restoreFocusedItem();
- } else {
- this._createFocusBackfillItem();
- }
+ this._manageFocus();
// update models
this._assignModels(itemSet);
// measure heights
@@ -3269,7 +3328,6 @@ i18nTemplate.process(document, loadTimeData);
* Increases the pool size.
*/
_increasePool: function(missingItems) {
- // limit the size
var nextPhysicalCount = Math.min(
this._physicalCount + missingItems,
this._virtualCount - this._virtualStart,
@@ -3278,14 +3336,24 @@ i18nTemplate.process(document, loadTimeData);
var prevPhysicalCount = this._physicalCount;
var delta = nextPhysicalCount - prevPhysicalCount;
- if (delta > 0) {
- [].push.apply(this._physicalItems, this._createPool(delta));
- [].push.apply(this._physicalSizes, new Array(delta));
+ if (delta <= 0) {
+ return;
+ }
+
+ [].push.apply(this._physicalItems, this._createPool(delta));
+ [].push.apply(this._physicalSizes, new Array(delta));
- this._physicalCount = prevPhysicalCount + delta;
- // tail call
- return this._update();
+ this._physicalCount = prevPhysicalCount + delta;
+
+ // update the physical start if we need to preserve the model of the focused item.
+ // In this situation, the focused item is currently rendered and its model would
+ // have changed after increasing the pool if the physical start remained unchanged.
+ if (this._physicalStart > this._physicalEnd &&
+ this._isIndexRendered(this._focusedIndex) &&
+ this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
+ this._physicalStart = this._physicalStart + delta;
}
+ this._update();
},
/**
@@ -3374,27 +3442,36 @@ i18nTemplate.process(document, loadTimeData);
/**
* Called as a side effect of a host items.<key>.<path> path change,
- * responsible for notifying item.<path> changes to row for key.
+ * responsible for notifying item.<path> changes.
*/
_forwardItemPath: function(path, value) {
- if (this._physicalIndexForKey) {
- var dot = path.indexOf('.');
- var key = path.substring(0, dot < 0 ? path.length : dot);
- var idx = this._physicalIndexForKey[key];
- var row = this._physicalItems[idx];
-
- if (idx === this._focusedIndex && this._offscreenFocusedItem) {
- row = this._offscreenFocusedItem;
- }
- if (row) {
- var inst = row._templateInstance;
- if (dot >= 0) {
- path = this.as + '.' + path.substring(dot+1);
- inst.notifyPath(path, value, true);
- } else {
- inst[this.as] = value;
- }
- }
+ if (!this._physicalIndexForKey) {
+ return;
+ }
+ var inst;
+ var dot = path.indexOf('.');
+ var key = path.substring(0, dot < 0 ? path.length : dot);
+ var idx = this._physicalIndexForKey[key];
+ var el = this._physicalItems[idx];
+
+
+ if (idx === this._focusedIndex && this._offscreenFocusedItem) {
+ el = this._offscreenFocusedItem;
+ }
+ if (!el) {
+ return;
+ }
+
+ inst = el._templateInstance;
+
+ if (inst.__key__ !== key) {
+ return;
+ }
+ if (dot >= 0) {
+ path = this.as + '.' + path.substring(dot+1);
+ inst.notifyPath(path, value, true);
+ } else {
+ inst[this.as] = value;
}
},
@@ -3404,19 +3481,15 @@ i18nTemplate.process(document, loadTimeData);
*/
_itemsChanged: function(change) {
if (change.path === 'items') {
-
- this._restoreFocusedItem();
- // render the new set
- this._itemsRendered = false;
- // update the whole set
+ // reset items
this._virtualStart = 0;
this._physicalTop = 0;
this._virtualCount = this.items ? this.items.length : 0;
- this._focusedIndex = 0;
this._collection = this.items ? Polymer.Collection.get(this.items) : null;
this._physicalIndexForKey = {};
this._resetScrollPosition(0);
+ this._removeFocusedItem();
// create the initial physical items
if (!this._physicalItems) {
@@ -3424,47 +3497,50 @@ i18nTemplate.process(document, loadTimeData);
this._physicalItems = this._createPool(this._physicalCount);
this._physicalSizes = new Array(this._physicalCount);
}
- this._debounceTemplate(this._render);
+
+ this._physicalStart = 0;
} else if (change.path === 'items.splices') {
- // render the new set
- this._itemsRendered = false;
this._adjustVirtualIndex(change.value.indexSplices);
this._virtualCount = this.items ? this.items.length : 0;
- this._debounceTemplate(this._render);
-
- if (this._focusedIndex < 0 || this._focusedIndex >= this._virtualCount) {
- this._focusedIndex = 0;
- }
- this._debounceTemplate(this._render);
-
} else {
// update a single item
this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.value);
+ return;
}
+
+ this._itemsRendered = false;
+ this._debounceTemplate(this._render);
},
/**
* @param {!Array<!PolymerSplice>} splices
*/
_adjustVirtualIndex: function(splices) {
- var i, splice, idx;
-
- for (i = 0; i < splices.length; i++) {
- splice = splices[i];
-
+ splices.forEach(function(splice) {
// deselect removed items
- splice.removed.forEach(this.$.selector.deselect, this.$.selector);
-
- idx = splice.index;
+ splice.removed.forEach(this._removeItem, this);
// We only need to care about changes happening above the current position
- if (idx >= this._virtualStart) {
- break;
+ if (splice.index < this._virtualStart) {
+ var delta = Math.max(
+ splice.addedCount - splice.removed.length,
+ splice.index - this._virtualStart);
+
+ this._virtualStart = this._virtualStart + delta;
+
+ if (this._focusedIndex >= 0) {
+ this._focusedIndex = this._focusedIndex + delta;
+ }
}
+ }, this);
+ },
- this._virtualStart = this._virtualStart +
- Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStart);
+ _removeItem: function(item) {
+ this.$.selector.deselect(item);
+ // remove the current focused item
+ if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
+ this._removeFocusedItem();
}
},
@@ -3517,19 +3593,18 @@ i18nTemplate.process(document, loadTimeData);
var inst = el._templateInstance;
var item = this.items && this.items[vidx];
- if (item !== undefined && item !== null) {
+ if (item != null) {
inst[this.as] = item;
inst.__key__ = this._collection.getKey(item);
inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item);
inst[this.indexAs] = vidx;
- inst.tabIndex = vidx === this._focusedIndex ? 0 : -1;
- el.removeAttribute('hidden');
+ inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
this._physicalIndexForKey[inst.__key__] = pidx;
+ el.removeAttribute('hidden');
} else {
inst.__key__ = null;
el.setAttribute('hidden', '');
}
-
}, itemSet);
},
@@ -3577,10 +3652,8 @@ i18nTemplate.process(document, loadTimeData);
var y = this._physicalTop;
this._iterateItems(function(pidx) {
-
this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
y += this._physicalSizes[pidx];
-
});
},
@@ -3628,7 +3701,6 @@ i18nTemplate.process(document, loadTimeData);
this._scrollHeight = this._estScrollHeight;
}
},
-
/**
* Scroll to a specific item in the virtual list regardless
* of the physical items in the DOM tree.
@@ -3643,12 +3715,13 @@ i18nTemplate.process(document, loadTimeData);
Polymer.dom.flush();
- var firstVisible = this.firstVisibleIndex;
idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
-
- // start at the previous virtual item
- // so we have a item above the first visible item
- this._virtualStart = idx - 1;
+ // update the virtual start only when needed
+ if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
+ this._virtualStart = idx - 1;
+ }
+ // manage focus
+ this._manageFocus();
// assign new models
this._assignModels();
// measure the new sizes
@@ -3711,7 +3784,7 @@ i18nTemplate.process(document, loadTimeData);
var key = this._collection.getKey(item);
var pidx = this._physicalIndexForKey[key];
- if (pidx !== undefined) {
+ if (pidx != null) {
return this._physicalItems[pidx]._templateInstance;
}
return null;
@@ -3849,139 +3922,179 @@ i18nTemplate.process(document, loadTimeData);
var key = this._collection.getKey(item);
var pidx = this._physicalIndexForKey[key];
- if (pidx !== undefined) {
+ if (pidx != null) {
this._updateMetrics([pidx]);
this._positionItems();
}
},
+ /**
+ * Creates a temporary backfill item in the rendered pool of physical items
+ * to replace the main focused item. The focused item has tabIndex = 0
+ * and might be currently focused by the user.
+ *
+ * This dynamic replacement helps to preserve the focus state.
+ */
+ _manageFocus: function() {
+ var fidx = this._focusedIndex;
+
+ if (fidx >= 0 && fidx < this._virtualCount) {
+ // if it's a valid index, check if that index is rendered
+ // in a physical item.
+ if (this._isIndexRendered(fidx)) {
+ this._restoreFocusedItem();
+ } else {
+ this._createFocusBackfillItem();
+ }
+ } else if (this._virtualCount > 0 && this._physicalCount > 0) {
+ // otherwise, assign the initial focused index.
+ this._focusedIndex = this._virtualStart;
+ this._focusedItem = this._physicalItems[this._physicalStart];
+ }
+ },
+
_isIndexRendered: function(idx) {
return idx >= this._virtualStart && idx <= this._virtualEnd;
},
- _getPhysicalItemForIndex: function(idx, force) {
- if (!this._collection) {
- return null;
- }
- if (!this._isIndexRendered(idx)) {
- if (force) {
- this.scrollToIndex(idx);
- return this._getPhysicalItemForIndex(idx, false);
- }
- return null;
- }
- var item = this._getNormalizedItem(idx);
- var physicalItem = this._physicalItems[this._physicalIndexForKey[this._collection.getKey(item)]];
+ _isIndexVisible: function(idx) {
+ return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
+ },
- return physicalItem || null;
+ _getPhysicalIndex: function(idx) {
+ return this._physicalIndexForKey[this._collection.getKey(this._getNormalizedItem(idx))];
},
_focusPhysicalItem: function(idx) {
- this._restoreFocusedItem();
-
- var physicalItem = this._getPhysicalItemForIndex(idx, true);
- if (!physicalItem) {
+ if (idx < 0 || idx >= this._virtualCount) {
return;
}
+ this._restoreFocusedItem();
+ // scroll to index to make sure it's rendered
+ if (!this._isIndexRendered(idx)) {
+ this.scrollToIndex(idx);
+ }
+
+ var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
var SECRET = ~(Math.random() * 100);
var model = physicalItem._templateInstance;
var focusable;
+ // set a secret tab index
model.tabIndex = SECRET;
- // the focusable element could be the entire physical item
+ // check if focusable element is the physical item
if (physicalItem.tabIndex === SECRET) {
focusable = physicalItem;
}
- // the focusable element could be somewhere within the physical item
+ // search for the element which tabindex is bound to the secret tab index
if (!focusable) {
focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECRET + '"]');
}
// restore the tab index
model.tabIndex = 0;
+ // focus the focusable element
+ this._focusedIndex = idx;
focusable && focusable.focus();
},
- _restoreFocusedItem: function() {
- if (!this._offscreenFocusedItem) {
- return;
- }
- var item = this._getNormalizedItem(this._focusedIndex);
- var pidx = this._physicalIndexForKey[this._collection.getKey(item)];
-
- if (pidx !== undefined) {
- this.translate3d(0, HIDDEN_Y, 0, this._physicalItems[pidx]);
- this._physicalItems[pidx] = this._offscreenFocusedItem;
- }
- this._offscreenFocusedItem = null;
- },
-
_removeFocusedItem: function() {
- if (!this._offscreenFocusedItem) {
- return;
+ if (this._offscreenFocusedItem) {
+ Polymer.dom(this).removeChild(this._offscreenFocusedItem);
}
- Polymer.dom(this).removeChild(this._offscreenFocusedItem);
this._offscreenFocusedItem = null;
this._focusBackfillItem = null;
+ this._focusedItem = null;
+ this._focusedIndex = -1;
},
_createFocusBackfillItem: function() {
- if (this._offscreenFocusedItem) {
+ var pidx, fidx = this._focusedIndex;
+ if (this._offscreenFocusedItem || fidx < 0) {
return;
}
- var item = this._getNormalizedItem(this._focusedIndex);
- var pidx = this._physicalIndexForKey[this._collection.getKey(item)];
-
- this._offscreenFocusedItem = this._physicalItems[pidx];
- this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
-
if (!this._focusBackfillItem) {
+ // create a physical item, so that it backfills the focused item.
var stampedTemplate = this.stamp(null);
this._focusBackfillItem = stampedTemplate.root.querySelector('*');
Polymer.dom(this).appendChild(stampedTemplate.root);
}
- this._physicalItems[pidx] = this._focusBackfillItem;
+ // get the physical index for the focused index
+ pidx = this._getPhysicalIndex(fidx);
+
+ if (pidx != null) {
+ // set the offcreen focused physical item
+ this._offscreenFocusedItem = this._physicalItems[pidx];
+ // backfill the focused physical item
+ this._physicalItems[pidx] = this._focusBackfillItem;
+ // hide the focused physical
+ this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
+ }
+ },
+
+ _restoreFocusedItem: function() {
+ var pidx, fidx = this._focusedIndex;
+
+ if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
+ return;
+ }
+ // assign models to the focused index
+ this._assignModels();
+ // get the new physical index for the focused index
+ pidx = this._getPhysicalIndex(fidx);
+
+ if (pidx != null) {
+ // flip the focus backfill
+ this._focusBackfillItem = this._physicalItems[pidx];
+ // restore the focused physical item
+ this._physicalItems[pidx] = this._offscreenFocusedItem;
+ // reset the offscreen focused item
+ this._offscreenFocusedItem = null;
+ // hide the physical item that backfills
+ this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
+ }
},
_didFocus: function(e) {
var targetModel = this.modelForElement(e.target);
+ var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
+ var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
var fidx = this._focusedIndex;
- if (!targetModel) {
+ if (!targetModel || !focusedModel) {
return;
}
- this._restoreFocusedItem();
-
- if (this.modelForElement(this._offscreenFocusedItem) === targetModel) {
- this.scrollToIndex(fidx);
+ if (focusedModel === targetModel) {
+ // if the user focused the same item, then bring it into view if it's not visible
+ if (!this._isIndexVisible(fidx)) {
+ this.scrollToIndex(fidx);
+ }
} else {
+ this._restoreFocusedItem();
// restore tabIndex for the currently focused item
- this._getModelFromItem(this._getNormalizedItem(fidx)).tabIndex = -1;
+ focusedModel.tabIndex = -1;
// set the tabIndex for the next focused item
targetModel.tabIndex = 0;
- fidx = /** @type {{index: number}} */(targetModel).index;
+ fidx = targetModel[this.indexAs];
this._focusedIndex = fidx;
- // bring the item into view
- if (fidx < this.firstVisibleIndex || fidx > this.lastVisibleIndex) {
- this.scrollToIndex(fidx);
- } else {
+ this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
+
+ if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
this._update();
}
}
},
_didMoveUp: function() {
- this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1));
+ this._focusPhysicalItem(this._focusedIndex - 1);
},
_didMoveDown: function() {
- this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex + 1));
+ this._focusPhysicalItem(this._focusedIndex + 1);
},
_didEnter: function(e) {
- // focus the currently focused physical item
this._focusPhysicalItem(this._focusedIndex);
- // toggle selection
- this._selectionHandler(/** @type {{keyboardEvent: Event}} */(e.detail).keyboardEvent);
+ this._selectionHandler(e.detail.keyboardEvent);
}
});
@@ -4301,7 +4414,8 @@ Polymer({
* @type {!Polymer.IronMeta}
*/
_meta: {
- value: Polymer.Base.create('iron-meta', {type: 'iconset'})
+ value: Polymer.Base.create('iron-meta', {type: 'iconset'}),
+ observer: '_updateIcon'
}
},
@@ -4326,7 +4440,14 @@ Polymer({
/** @suppress {visibility} */
_updateIcon: function() {
if (this._usesIconset()) {
- if (this._iconsetName) {
+ if (this._img && this._img.parentNode) {
+ Polymer.dom(this.root).removeChild(this._img);
+ }
+ if (this._iconName === "") {
+ if (this._iconset) {
+ this._iconset.removeIcon(this);
+ }
+ } else if (this._iconsetName && this._meta) {
this._iconset = /** @type {?Polymer.Iconset} */ (
this._meta.byKey(this._iconsetName));
if (this._iconset) {
@@ -4337,6 +4458,9 @@ Polymer({
}
}
} else {
+ if (this._iconset) {
+ this._iconset.removeIcon(this);
+ }
if (!this._img) {
this._img = document.createElement('img');
this._img.style.width = '100%';
@@ -4605,6 +4729,7 @@ Polymer({
this._oldTabIndex = this.tabIndex;
this.focused = false;
this.tabIndex = -1;
+ this.blur();
} else if (this._oldTabIndex !== undefined) {
this.tabIndex = this._oldTabIndex;
}
@@ -6335,16 +6460,19 @@ Polymer({
*/
setItemSelected: function(item, isSelected) {
if (item != null) {
- if (isSelected) {
- this.selection.push(item);
- } else {
- var i = this.selection.indexOf(item);
- if (i >= 0) {
- this.selection.splice(i, 1);
+ if (isSelected !== this.isSelected(item)) {
+ // proceed to update selection only if requested state differs from current
+ if (isSelected) {
+ this.selection.push(item);
+ } else {
+ var i = this.selection.indexOf(item);
+ if (i >= 0) {
+ this.selection.splice(i, 1);
+ }
+ }
+ if (this.selectCallback) {
+ this.selectCallback(item, isSelected);
}
- }
- if (this.selectCallback) {
- this.selectCallback(item, isSelected);
}
}
},
@@ -6501,7 +6629,8 @@ Polymer({
},
observers: [
- '_updateSelected(attrForSelected, selected)'
+ '_updateAttrForSelected(attrForSelected)',
+ '_updateSelected(selected)'
],
created: function() {
@@ -6513,7 +6642,7 @@ Polymer({
this._observer = this._observeItems(this);
this._updateItems();
if (!this._shouldUpdateSelection) {
- this._updateSelected(this.attrForSelected,this.selected)
+ this._updateSelected();
}
this._addListener(this.activateEvent);
},
@@ -6606,6 +6735,12 @@ Polymer({
this._setItems(nodes);
},
+ _updateAttrForSelected: function() {
+ if (this._shouldUpdateSelection) {
+ this.selected = this._indexToValue(this.indexOf(this.selectedItem));
+ }
+ },
+
_updateSelected: function() {
this._selectSelected(this.selected);
},
@@ -6675,7 +6810,7 @@ Polymer({
}
// Let other interested parties know about the change so that
- // we don't have to recreate mutation observers everywher.
+ // we don't have to recreate mutation observers everywhere.
this.fire('iron-items-changed', mutations, {
bubbles: false,
cancelable: false
@@ -6739,7 +6874,7 @@ Polymer({
},
observers: [
- '_updateSelected(attrForSelected, selectedValues)'
+ '_updateSelected(selectedValues)'
],
/**
@@ -6770,6 +6905,18 @@ Polymer({
(this.selectedValues != null && this.selectedValues.length);
},
+ _updateAttrForSelected: function() {
+ if (!this.multi) {
+ Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
+ } else if (this._shouldUpdateSelection) {
+ this.selectedValues = this.selectedItems.map(function(selectedItem) {
+ return this._indexToValue(this.indexOf(selectedItem));
+ }, this).filter(function(unfilteredValue) {
+ return unfilteredValue != null;
+ }, this);
+ }
+ },
+
_updateSelected: function() {
if (this.multi) {
this._selectMulti(this.selectedValues);
@@ -6779,11 +6926,16 @@ Polymer({
},
_selectMulti: function(values) {
- this._selection.clear();
if (values) {
- for (var i = 0; i < values.length; i++) {
- this._selection.setItemSelected(this._valueToItem(values[i]), true);
+ var selectedItems = this._valuesToItems(values);
+ // clear all but the current selected items
+ this._selection.clear(selectedItems);
+ // select only those not selected yet
+ for (var i = 0; i < selectedItems.length; i++) {
+ this._selection.setItemSelected(selectedItems[i], true);
}
+ } else {
+ this._selection.clear();
}
},
@@ -6806,6 +6958,12 @@ Polymer({
this.splice('selectedValues',i,1);
}
this._selection.setItemSelected(this._valueToItem(value), unselected);
+ },
+
+ _valuesToItems: function(values) {
+ return (values == null) ? null : values.map(function(value) {
+ return this._valueToItem(value);
+ }, this);
}
};
@@ -7039,6 +7197,14 @@ Polymer({
return;
}
+ // Do not focus the selected tab if the deepest target is part of the
+ // menu element's local DOM and is focusable.
+ var rootTarget = /** @type {?HTMLElement} */(
+ Polymer.dom(event).rootTarget);
+ if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !this.isLightDescendant(rootTarget)) {
+ return;
+ }
+
this.blur();
// clear the cached focus item
@@ -7066,6 +7232,7 @@ Polymer({
_onUpKey: function(event) {
// up and down arrows moves the focus
this._focusPrevious();
+ event.detail.keyboardEvent.preventDefault();
},
/**
@@ -7075,6 +7242,7 @@ Polymer({
*/
_onDownKey: function(event) {
this._focusNext();
+ event.detail.keyboardEvent.preventDefault();
},
/**
@@ -7283,14 +7451,14 @@ CSS properties | Action
* the memoized data.
*/
resetFit: function() {
- if (!this._fitInfo || !this._fitInfo.sizedBy.height) {
- this.sizingTarget.style.maxHeight = '';
- this.style.top = this._fitInfo ? this._fitInfo.inlineStyle.top : '';
- }
if (!this._fitInfo || !this._fitInfo.sizedBy.width) {
this.sizingTarget.style.maxWidth = '';
- this.style.left = this._fitInfo ? this._fitInfo.inlineStyle.left : '';
}
+ if (!this._fitInfo || !this._fitInfo.sizedBy.height) {
+ this.sizingTarget.style.maxHeight = '';
+ }
+ this.style.top = this._fitInfo ? this._fitInfo.inlineStyle.top : '';
+ this.style.left = this._fitInfo ? this._fitInfo.inlineStyle.left : '';
if (this._fitInfo) {
this.style.position = this._fitInfo.positionedBy.css;
}
@@ -7351,18 +7519,30 @@ CSS properties | Action
* `position:fixed`.
*/
center: function() {
- if (!this._fitInfo.positionedBy.vertically || !this._fitInfo.positionedBy.horizontally) {
- // need position:fixed to center
- this.style.position = 'fixed';
+ var positionedBy = this._fitInfo.positionedBy;
+ if (positionedBy.vertically && positionedBy.horizontally) {
+ // Already positioned.
+ return;
}
- if (!this._fitInfo.positionedBy.vertically) {
- var top = (this._fitHeight - this.offsetHeight) / 2 + this._fitTop;
- top -= this._fitInfo.margin.top;
+ // Need position:fixed to center
+ this.style.position = 'fixed';
+ // Take into account the offset caused by parents that create stacking
+ // contexts (e.g. with transform: translate3d). Translate to 0,0 and
+ // measure the bounding rect.
+ if (!positionedBy.vertically) {
+ this.style.top = '0px';
+ }
+ if (!positionedBy.horizontally) {
+ this.style.left = '0px';
+ }
+ // It will take in consideration margins and transforms
+ var rect = this.getBoundingClientRect();
+ if (!positionedBy.vertically) {
+ var top = this._fitTop - rect.top + (this._fitHeight - rect.height) / 2;
this.style.top = top + 'px';
}
- if (!this._fitInfo.positionedBy.horizontally) {
- var left = (this._fitWidth - this.offsetWidth) / 2 + this._fitLeft;
- left -= this._fitInfo.margin.left;
+ if (!positionedBy.horizontally) {
+ var left = this._fitLeft - rect.left + (this._fitWidth - rect.width) / 2;
this.style.left = left + 'px';
}
}
@@ -7374,6 +7554,9 @@ CSS properties | Action
*/
Polymer.IronOverlayManagerClass = function() {
this._overlays = [];
+ // Used to keep track of the last focused node before an overlay gets opened.
+ this._lastFocusedNodes = [];
+
/**
* iframes have a default z-index of 100, so this default should be at least
* that.
@@ -7382,7 +7565,52 @@ CSS properties | Action
this._minimumZ = 101;
this._backdrops = [];
- }
+
+ this._backdropElement = null;
+ Object.defineProperty(this, 'backdropElement', {
+ get: function() {
+ if (!this._backdropElement) {
+ this._backdropElement = document.createElement('iron-overlay-backdrop');
+ }
+ return this._backdropElement;
+ }.bind(this)
+ });
+
+ /**
+ * The deepest active element.
+ * returns {?Node} element the active element
+ */
+ this.deepActiveElement = null;
+ Object.defineProperty(this, 'deepActiveElement', {
+ get: function() {
+ var active = document.activeElement;
+ // document.activeElement can be null
+ // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
+ while (active && active.root && Polymer.dom(active.root).activeElement) {
+ active = Polymer.dom(active.root).activeElement;
+ }
+ return active;
+ }.bind(this)
+ });
+ };
+
+ /**
+ * If a node is contained in an overlay.
+ * @private
+ * @param {Node} node
+ * @returns {Boolean}
+ */
+ Polymer.IronOverlayManagerClass.prototype._isChildOfOverlay = function(node) {
+ while (node && node !== document.body) {
+ // Use logical parentNode, or native ShadowRoot host.
+ node = Polymer.dom(node).parentNode || node.host;
+ // Check if it is an overlay.
+ if (node && node.behaviors && node.behaviors.indexOf(Polymer.IronOverlayBehaviorImpl) !== -1) {
+ return true;
+ }
+ }
+ return false;
+ };
Polymer.IronOverlayManagerClass.prototype._applyOverlayZ = function(overlay, aboveZ) {
this._setZ(overlay, aboveZ + 2);
@@ -7402,6 +7630,12 @@ CSS properties | Action
if (newZ <= minimumZ) {
this._applyOverlayZ(overlay, minimumZ);
}
+ var element = this.deepActiveElement;
+ // If already in other overlay, don't reset focus there.
+ if (this._isChildOfOverlay(element)) {
+ element = null;
+ }
+ this._lastFocusedNodes.push(element);
};
Polymer.IronOverlayManagerClass.prototype.removeOverlay = function(overlay) {
@@ -7409,6 +7643,13 @@ CSS properties | Action
if (i >= 0) {
this._overlays.splice(i, 1);
this._setZ(overlay, '');
+
+ var node = this._lastFocusedNodes[i];
+ // Focus only if still contained in document.body
+ if (overlay.restoreFocusOnClose && node && Polymer.dom(document.body).deepContains(node)) {
+ node.focus();
+ }
+ this._lastFocusedNodes.splice(i, 1);
}
};
@@ -7463,15 +7704,6 @@ CSS properties | Action
}
};
- Object.defineProperty(Polymer.IronOverlayManagerClass.prototype, "backdropElement", {
- get: function() {
- if (!this._backdropElement) {
- this._backdropElement = document.createElement('iron-overlay-backdrop');
- }
- return this._backdropElement;
- }
- });
-
Polymer.IronOverlayManagerClass.prototype.getBackdrops = function() {
return this._backdrops;
};
@@ -7701,6 +7933,23 @@ context. You should place this element as a child of `<body>` whenever possible.
type: Object
},
+ /**
+ * The HTMLElement that will be firing relevant KeyboardEvents.
+ * Used for capturing esc and tab. Overridden from `IronA11yKeysBehavior`.
+ */
+ keyEventTarget: {
+ type: Object,
+ value: document
+ },
+
+ /**
+ * Set to true to enable restoring of focus when overlay is closed.
+ */
+ restoreFocusOnClose: {
+ type: Boolean,
+ value: false
+ },
+
_manager: {
type: Object,
value: Polymer.IronOverlayManager
@@ -7713,13 +7962,6 @@ context. You should place this element as a child of `<body>` whenever possible.
}
},
- _boundOnCaptureKeydown: {
- type: Function,
- value: function() {
- return this._onCaptureKeydown.bind(this);
- }
- },
-
_boundOnCaptureFocus: {
type: Function,
value: function() {
@@ -7727,44 +7969,113 @@ context. You should place this element as a child of `<body>` whenever possible.
}
},
- /** @type {?Node} */
+ /**
+ * The node being focused.
+ * @type {?Node}
+ */
_focusedChild: {
type: Object
}
},
+ keyBindings: {
+ 'esc': '__onEsc',
+ 'tab': '__onTab'
+ },
+
listeners: {
'iron-resize': '_onIronResize'
},
/**
* The backdrop element.
- * @type Node
+ * @type {Node}
*/
get backdropElement() {
return this._manager.backdropElement;
},
+ /**
+ * Returns the node to give focus to.
+ * @type {Node}
+ */
get _focusNode() {
return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]') || this;
},
+ /**
+ * Array of nodes that can receive focus (overlay included), ordered by `tabindex`.
+ * This is used to retrieve which is the first and last focusable nodes in order
+ * to wrap the focus for overlays `with-backdrop`.
+ *
+ * If you know what is your content (specifically the first and last focusable children),
+ * you can override this method to return only `[firstFocusable, lastFocusable];`
+ * @type {[Node]}
+ * @protected
+ */
+ get _focusableNodes() {
+ // Elements that can be focused even if they have [disabled] attribute.
+ var FOCUSABLE_WITH_DISABLED = [
+ 'a[href]',
+ 'area[href]',
+ 'iframe',
+ '[tabindex]',
+ '[contentEditable=true]'
+ ];
+
+ // Elements that cannot be focused if they have [disabled] attribute.
+ var FOCUSABLE_WITHOUT_DISABLED = [
+ 'input',
+ 'select',
+ 'textarea',
+ 'button'
+ ];
+
+ // Discard elements with tabindex=-1 (makes them not focusable).
+ var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') +
+ ':not([tabindex="-1"]),' +
+ FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),') +
+ ':not([disabled]):not([tabindex="-1"])';
+
+ var focusables = Polymer.dom(this).querySelectorAll(selector);
+ if (this.tabIndex >= 0) {
+ // Insert at the beginning because we might have all elements with tabIndex = 0,
+ // and the overlay should be the first of the list.
+ focusables.splice(0, 0, this);
+ }
+ // Sort by tabindex.
+ return focusables.sort(function (a, b) {
+ if (a.tabIndex === b.tabIndex) {
+ return 0;
+ }
+ if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) {
+ return 1;
+ }
+ return -1;
+ });
+ },
+
ready: function() {
- // with-backdrop need tabindex to be set in order to trap the focus.
+ // with-backdrop needs tabindex to be set in order to trap the focus.
// If it is not set, IronOverlayBehavior will set it, and remove it if with-backdrop = false.
this.__shouldRemoveTabIndex = false;
+ // Used for wrapping the focus on TAB / Shift+TAB.
+ this.__firstFocusableNode = this.__lastFocusableNode = null;
this._ensureSetup();
},
attached: function() {
// Call _openedChanged here so that position can be computed correctly.
- if (this._callOpenedWhenReady) {
+ if (this.opened) {
this._openedChanged();
}
+ this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
},
detached: function() {
+ Polymer.dom(this).unobserveNodes(this._observer);
+ this._observer = null;
this.opened = false;
this._manager.trackBackdrop(this);
this._manager.removeOverlay(this);
@@ -7796,9 +8107,10 @@ context. You should place this element as a child of `<body>` whenever possible.
/**
* Cancels the overlay.
+ * @param {?Event} event The original event
*/
- cancel: function() {
- var cancelEvent = this.fire('iron-overlay-canceled', undefined, {cancelable: true});
+ cancel: function(event) {
+ var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
if (cancelEvent.defaultPrevented) {
return;
}
@@ -7821,12 +8133,10 @@ context. You should place this element as a child of `<body>` whenever possible.
this.removeAttribute('aria-hidden');
} else {
this.setAttribute('aria-hidden', 'true');
- Polymer.dom(this).unobserveNodes(this._observer);
}
// wait to call after ready only if we're initially open
if (!this._overlaySetup) {
- this._callOpenedWhenReady = this.opened;
return;
}
@@ -7901,16 +8211,18 @@ context. You should place this element as a child of `<body>` whenever possible.
}
},
- _toggleListeners: function () {
+ _toggleListeners: function() {
this._toggleListener(this.opened, document, 'tap', this._boundOnCaptureClick, true);
- this._toggleListener(this.opened, document, 'keydown', this._boundOnCaptureKeydown, true);
this._toggleListener(this.opened, document, 'focus', this._boundOnCaptureFocus, true);
},
// tasks which must occur before opening; e.g. making the element visible
_prepareRenderOpened: function() {
+
this._manager.addOverlay(this);
+ // Needed to calculate the size of the overlay so that transitions on its size
+ // will have the correct starting points.
this._preparePositioning();
this.fit();
this._finishPositioning();
@@ -7918,6 +8230,12 @@ context. You should place this element as a child of `<body>` whenever possible.
if (this.withBackdrop) {
this.backdropElement.prepare();
}
+
+ // Safari will apply the focus to the autofocus element when displayed for the first time,
+ // so we blur it. Later, _applyFocus will set the focus if necessary.
+ if (this.noAutoFocus && document.activeElement === this._focusNode) {
+ this._focusNode.blur();
+ }
},
// tasks which cause the overlay to actually open; typically play an
@@ -7937,23 +8255,24 @@ context. You should place this element as a child of `<body>` whenever possible.
},
_finishRenderOpened: function() {
- // focus the child node with [autofocus]
+ // This ensures the overlay is visible before we set the focus
+ // (by calling _onIronResize -> refit).
+ this.notifyResize();
+ // Focus the child node with [autofocus]
this._applyFocus();
- this._observer = Polymer.dom(this).observeNodes(this.notifyResize);
this.fire('iron-overlay-opened');
},
_finishRenderClosed: function() {
- // hide the overlay and remove the backdrop
+ // Hide the overlay and remove the backdrop.
this.resetFit();
this.style.display = 'none';
this._manager.removeOverlay(this);
- this._focusedChild = null;
this._applyFocus();
-
this.notifyResize();
+
this.fire('iron-overlay-closed', this.closingReason);
},
@@ -7966,8 +8285,9 @@ context. You should place this element as a child of `<body>` whenever possible.
_finishPositioning: function() {
this.style.display = 'none';
this.style.transform = this.style.webkitTransform = '';
- // force layout to avoid application of transform
- /** @suppress {suspiciousCode} */ this.offsetWidth;
+ // Force layout layout to avoid application of transform.
+ // Set offsetWidth to itself so that compilers won't remove it.
+ this.offsetWidth = this.offsetWidth;
this.style.transition = this.style.webkitTransition = '';
},
@@ -7978,6 +8298,7 @@ context. You should place this element as a child of `<body>` whenever possible.
}
} else {
this._focusNode.blur();
+ this._focusedChild = null;
this._manager.focusOverlay();
}
},
@@ -7988,23 +8309,13 @@ context. You should place this element as a child of `<body>` whenever possible.
if (this.noCancelOnOutsideClick) {
this._applyFocus();
} else {
- this.cancel();
+ this.cancel(event);
}
}
},
- _onCaptureKeydown: function(event) {
- var ESC = 27;
- if (this._manager.currentOverlay() === this &&
- !this.noCancelOnEscKey &&
- event.keyCode === ESC) {
- this.cancel();
- }
- },
-
_onCaptureFocus: function (event) {
- if (this._manager.currentOverlay() === this &&
- this.withBackdrop) {
+ if (this._manager.currentOverlay() === this && this.withBackdrop) {
var path = Polymer.dom(event).path;
if (path.indexOf(this) === -1) {
event.stopPropagation();
@@ -8019,28 +8330,73 @@ context. You should place this element as a child of `<body>` whenever possible.
if (this.opened) {
this.refit();
}
- }
+ },
-/**
- * Fired after the `iron-overlay` opens.
- * @event iron-overlay-opened
- */
+ /**
+ * @protected
+ * Will call notifyResize if overlay is opened.
+ * Can be overridden in order to avoid multiple observers on the same node.
+ */
+ _onNodesChange: function() {
+ if (this.opened) {
+ this.notifyResize();
+ }
+ // Store it so we don't query too much.
+ var focusableNodes = this._focusableNodes;
+ this.__firstFocusableNode = focusableNodes[0];
+ this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
+ },
-/**
- * Fired when the `iron-overlay` is canceled, but before it is closed.
- * Cancel the event to prevent the `iron-overlay` from closing.
- * @event iron-overlay-canceled
- */
+ __onEsc: function(event) {
+ // Not opened or not on top, so return.
+ if (this._manager.currentOverlay() !== this) {
+ return;
+ }
+ if (!this.noCancelOnEscKey) {
+ this.cancel(event);
+ }
+ },
-/**
- * Fired after the `iron-overlay` closes.
- * @event iron-overlay-closed
- * @param {{canceled: (boolean|undefined)}} set to the `closingReason` attribute
- */
+ __onTab: function(event) {
+ // Not opened or not on top, so return.
+ if (this._manager.currentOverlay() !== this) {
+ return;
+ }
+ // TAB wraps from last to first focusable.
+ // Shift + TAB wraps from first to last focusable.
+ var shift = event.detail.keyboardEvent.shiftKey;
+ var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusableNode;
+ var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNode;
+ if (this.withBackdrop && this._focusedChild === nodeToCheck) {
+ // We set here the _focusedChild so that _onCaptureFocus will handle the
+ // wrapping of the focus (the next event after tab is focus).
+ this._focusedChild = nodeToSet;
+ }
+ }
};
/** @polymerBehavior */
- Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
+ Polymer.IronOverlayBehavior = [Polymer.IronA11yKeysBehavior, Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
+
+ /**
+ * Fired after the `iron-overlay` opens.
+ * @event iron-overlay-opened
+ */
+
+ /**
+ * Fired when the `iron-overlay` is canceled, but before it is closed.
+ * Cancel the event to prevent the `iron-overlay` from closing.
+ * @event iron-overlay-canceled
+ * @param {Event} event The closing of the `iron-overlay` can be prevented
+ * by calling `event.preventDefault()`. The `event.detail` is the original event that originated
+ * the canceling (e.g. ESC keyboard event or click event outside the `iron-overlay`).
+ */
+
+ /**
+ * Fired after the `iron-overlay` closes.
+ * @event iron-overlay-closed
+ * @param {{canceled: (boolean|undefined)}} closingReason Contains `canceled` (whether the overlay was canceled).
+ */
/**
* Use `Polymer.NeonAnimationBehavior` to implement an animation.
* @polymerBehavior
« no previous file with comments | « no previous file | chrome/browser/resources/md_downloads/vulcanized.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698