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

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

Issue 1862213002: Roll third_party/polymer. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Remove obsolete appearance_browsertest.js, result of a previous bad merge. Created 4 years, 8 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 efa2abf0e1294cc9664a6e53acb14ad99bb8278f..f720148cd7cce7b70f2b0063994c07beb45aa865 100644
--- a/chrome/browser/resources/md_downloads/crisper.js
+++ b/chrome/browser/resources/md_downloads/crisper.js
@@ -1,3 +1,75 @@
+// Copyright (c) 2013 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 Assertion support.
+ */
+
+/**
+ * Verify |condition| is truthy and return |condition| if so.
+ * @template T
+ * @param {T} condition A condition to check for truthiness. Note that this
+ * may be used to test whether a value is defined or not, and we don't want
+ * to force a cast to Boolean.
+ * @param {string=} opt_message A message to show on failure.
+ * @return {T} A non-null |condition|.
+ */
+function assert(condition, opt_message) {
+ if (!condition) {
+ var message = 'Assertion failed';
+ if (opt_message)
+ message = message + ': ' + opt_message;
+ var error = new Error(message);
+ var global = function() { return this; }();
+ if (global.traceAssertionsForTesting)
+ console.warn(error.stack);
+ throw error;
+ }
+ return condition;
+}
+
+/**
+ * Call this from places in the code that should never be reached.
+ *
+ * For example, handling all the values of enum with a switch() like this:
+ *
+ * function getValueFromEnum(enum) {
+ * switch (enum) {
+ * case ENUM_FIRST_OF_TWO:
+ * return first
+ * case ENUM_LAST_OF_TWO:
+ * return last;
+ * }
+ * assertNotReached();
+ * return document;
+ * }
+ *
+ * This code should only be hit in the case of serious programmer error or
+ * unexpected input.
+ *
+ * @param {string=} opt_message A message to show when this is hit.
+ */
+function assertNotReached(opt_message) {
+ assert(false, opt_message || 'Unreachable code hit');
+}
+
+/**
+ * @param {*} value The value to check.
+ * @param {function(new: T, ...)} type A user-defined constructor.
+ * @param {string=} opt_message A message to show when this is hit.
+ * @return {T}
+ * @template T
+ */
+function assertInstanceof(value, type, opt_message) {
+ // We don't use assert immediately here so that we avoid constructing an error
+ // message if we don't have to.
+ if (!(value instanceof type)) {
+ assertNotReached(opt_message || 'Value ' + value +
+ ' is not a[n] ' + (type.name || typeof type));
+ }
+ return value;
+};
// 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.
@@ -21,17 +93,31 @@
* @template T
*/
function PromiseResolver() {
- /** @type {function(T): void} */
- this.resolve;
+ /** @private {function(T): void} */
+ this.resolve_;
- /** @type {function(*=): void} */
- this.reject;
+ /** @private {function(*=): void} */
+ this.reject_;
- /** @type {!Promise<T>} */
- this.promise = new Promise(function(resolve, reject) {
- this.resolve = resolve;
- this.reject = reject;
+ /** @private {!Promise<T>} */
+ this.promise_ = new Promise(function(resolve, reject) {
+ this.resolve_ = resolve;
+ this.reject_ = reject;
}.bind(this));
+}
+
+PromiseResolver.prototype = {
+ /** @return {!Promise<T>} */
+ get promise() { return this.promise_; },
+ set promise(p) { assertNotReached(); },
+
+ /** @return {function(T): void} */
+ get resolve() { return this.resolve_; },
+ set resolve(r) { assertNotReached(); },
+
+ /** @return {function(*=): void} */
+ get reject() { return this.reject_; },
+ set reject(s) { assertNotReached(); },
};
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
@@ -2176,20 +2262,31 @@ function quoteString(str) {
/**
* Specifies the element that will handle the scroll event
- * on the behalf of the current element. This is typically a reference to an `Element`,
+ * on the behalf of the current element. This is typically a reference to an element,
* but there are a few more posibilities:
*
* ### Elements id
*
*```html
- * <div id="scrollable-element" style="overflow-y: auto;">
+ * <div id="scrollable-element" style="overflow: auto;">
* <x-element scroll-target="scrollable-element">
- * Content
+ * \x3c!-- Content--\x3e
* </x-element>
* </div>
*```
- * In this case, `scrollTarget` will point to the outer div element. Alternatively,
- * you can set the property programatically:
+ * In this case, the `scrollTarget` will point to the outer div element.
+ *
+ * ### Document scrolling
+ *
+ * For document scrolling, you can use the reserved word `document`:
+ *
+ *```html
+ * <x-element scroll-target="document">
+ * \x3c!-- Content --\x3e
+ * </x-element>
+ *```
+ *
+ * ### Elements reference
*
*```js
* appHeader.scrollTarget = document.querySelector('#scrollable-element');
@@ -2210,42 +2307,39 @@ function quoteString(str) {
],
_scrollTargetChanged: function(scrollTarget, isAttached) {
- // Remove lister to the current scroll target
+ var eventTarget;
+
if (this._oldScrollTarget) {
- if (this._oldScrollTarget === this._doc) {
- window.removeEventListener('scroll', this._boundScrollHandler);
- } else if (this._oldScrollTarget.removeEventListener) {
- this._oldScrollTarget.removeEventListener('scroll', this._boundScrollHandler);
- }
+ eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScrollTarget;
+ eventTarget.removeEventListener('scroll', this._boundScrollHandler);
this._oldScrollTarget = null;
}
- if (isAttached) {
- // Support element id references
- if (typeof scrollTarget === 'string') {
- var host = this.domHost;
- this.scrollTarget = host && host.$ ? host.$[scrollTarget] :
- Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
+ if (!isAttached) {
+ return;
+ }
+ // Support element id references
+ if (scrollTarget === 'document') {
- } else if (this._scrollHandler) {
+ this.scrollTarget = this._doc;
- this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler.bind(this);
- // Add a new listener
- if (scrollTarget === this._doc) {
- window.addEventListener('scroll', this._boundScrollHandler);
- if (this._scrollTop !== 0 || this._scrollLeft !== 0) {
- this._scrollHandler();
- }
- } else if (scrollTarget && scrollTarget.addEventListener) {
- scrollTarget.addEventListener('scroll', this._boundScrollHandler);
- }
- this._oldScrollTarget = scrollTarget;
- }
+ } else if (typeof scrollTarget === 'string') {
+
+ this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
+ Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
+
+ } else if (this._isValidScrollTarget()) {
+
+ eventTarget = scrollTarget === this._doc ? window : scrollTarget;
+ this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler.bind(this);
+ this._oldScrollTarget = scrollTarget;
+
+ eventTarget.addEventListener('scroll', this._boundScrollHandler);
}
},
/**
- * Runs on every scroll event. Consumer of this behavior may want to override this method.
+ * Runs on every scroll event. Consumer of this behavior may override this method.
*
* @protected
*/
@@ -2472,10 +2566,6 @@ function quoteString(str) {
Polymer.IronScrollTargetBehavior
],
- listeners: {
- 'iron-resize': '_resizeHandler'
- },
-
keyBindings: {
'up': '_didMoveUp',
'down': '_didMoveDown',
@@ -2779,10 +2869,14 @@ function quoteString(str) {
attached: function() {
this.updateViewportBoundaries();
this._render();
+ // `iron-resize` is fired when the list is attached if the event is added
+ // before attached causing unnecessary work.
+ this.listen(this, 'iron-resize', '_resizeHandler');
},
detached: function() {
this._itemsRendered = false;
+ this.unlisten(this, 'iron-resize', '_resizeHandler');
},
/**
@@ -2903,11 +2997,9 @@ function quoteString(str) {
}
if (recycledTiles === 0) {
- // If the list ever reach this case, the physical average is not significant enough
- // to create all the items needed to cover the entire viewport.
- // e.g. A few items have a height that differs from the average by serveral order of magnitude.
+ // Try to increase the pool if the list's client height isn't filled up with physical items
if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
- this.async(this._increasePool.bind(this, 1));
+ this._increasePoolIfNeeded();
}
} else {
this._virtualStart = this._virtualStart + recycledTiles;
@@ -2962,19 +3054,27 @@ function quoteString(str) {
/**
* Increases the pool of physical items only if needed.
- * This function will allocate additional physical items
- * if the physical size is shorter than `_optPhysicalSize`
+ *
+ * @return {boolean} True if the pool was increased.
*/
_increasePoolIfNeeded: function() {
- if (this._viewportSize === 0 || this._physicalSize >= this._optPhysicalSize) {
+ // Base case 1: the list has no size.
+ if (this._viewportSize === 0) {
+ return false;
+ }
+ // Base case 2: If the physical size is optimal and the list's client height is full
+ // with physical items, don't increase the pool.
+ var isClientHeightFull = this._physicalBottom >= this._scrollBottom && this._physicalTop <= this._scrollPosition;
+ if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
return false;
}
- // 0 <= `currentPage` <= `_maxPages`
+ // this value should range between [0 <= `currentPage` <= `_maxPages`]
var currentPage = Math.floor(this._physicalSize / this._viewportSize);
+
if (currentPage === 0) {
// fill the first page
this._debounceTemplate(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5)));
- } else if (this._lastPage !== currentPage) {
+ } else if (this._lastPage !== currentPage && isClientHeightFull) {
// paint the page and defer the next increase
// wait 16ms which is rough enough to get paint cycle.
Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increasePool.bind(this, 1), 16));
@@ -2982,7 +3082,9 @@ function quoteString(str) {
// fill the rest of the pages
this._debounceTemplate(this._increasePool.bind(this, 1));
}
+
this._lastPage = currentPage;
+
return true;
},
@@ -3028,7 +3130,6 @@ function quoteString(str) {
if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) {
this._lastPage = 0;
this._update();
- this._scrollHandler();
this._itemsRendered = true;
}
},
@@ -3123,7 +3224,7 @@ function quoteString(str) {
if (!el) {
return;
}
-
+
inst = el._templateInstance;
if (inst.__key__ !== key) {
@@ -3407,7 +3508,7 @@ function quoteString(str) {
// update the position of the items
this._positionItems();
// set the new scroll position
- this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop + 1);
+ this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop);
// increase the pool of physical items if needed
this._increasePoolIfNeeded();
// clear cached visible index
@@ -3432,14 +3533,19 @@ function quoteString(str) {
if (IOS && Math.abs(this._viewportSize - this._scrollTargetHeight) < 100) {
return;
}
- this._debounceTemplate(function() {
- this._render();
- if (this._itemsRendered && this._physicalItems && this._isVisible) {
- this._resetAverage();
- this.updateViewportBoundaries();
- this.scrollToIndex(this.firstVisibleIndex);
- }
- });
+ // In Desktop Safari 9.0.3, if the scroll bars are always shown,
+ // changing the scroll position from a resize handler would result in
+ // the scroll position being reset. Waiting 1ms fixes the issue.
+ Polymer.dom.addDebouncer(this.debounce('_debounceTemplate',
+ function() {
+ this._render();
+
+ if (this._itemsRendered && this._physicalItems && this._isVisible) {
+ this._resetAverage();
+ this.updateViewportBoundaries();
+ this.scrollToIndex(this.firstVisibleIndex);
+ }
+ }.bind(this), 1));
},
_getModelFromItem: function(item) {
@@ -3761,78 +3867,6 @@ function quoteString(str) {
});
})();
-// Copyright (c) 2013 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 Assertion support.
- */
-
-/**
- * Verify |condition| is truthy and return |condition| if so.
- * @template T
- * @param {T} condition A condition to check for truthiness. Note that this
- * may be used to test whether a value is defined or not, and we don't want
- * to force a cast to Boolean.
- * @param {string=} opt_message A message to show on failure.
- * @return {T} A non-null |condition|.
- */
-function assert(condition, opt_message) {
- if (!condition) {
- var message = 'Assertion failed';
- if (opt_message)
- message = message + ': ' + opt_message;
- var error = new Error(message);
- var global = function() { return this; }();
- if (global.traceAssertionsForTesting)
- console.warn(error.stack);
- throw error;
- }
- return condition;
-}
-
-/**
- * Call this from places in the code that should never be reached.
- *
- * For example, handling all the values of enum with a switch() like this:
- *
- * function getValueFromEnum(enum) {
- * switch (enum) {
- * case ENUM_FIRST_OF_TWO:
- * return first
- * case ENUM_LAST_OF_TWO:
- * return last;
- * }
- * assertNotReached();
- * return document;
- * }
- *
- * This code should only be hit in the case of serious programmer error or
- * unexpected input.
- *
- * @param {string=} opt_message A message to show when this is hit.
- */
-function assertNotReached(opt_message) {
- assert(false, opt_message || 'Unreachable code hit');
-}
-
-/**
- * @param {*} value The value to check.
- * @param {function(new: T, ...)} type A user-defined constructor.
- * @param {string=} opt_message A message to show when this is hit.
- * @return {T}
- * @template T
- */
-function assertInstanceof(value, type, opt_message) {
- // We don't use assert immediately here so that we avoid constructing an error
- // message if we don't have to.
- if (!(value instanceof type)) {
- assertNotReached(opt_message || 'Value ' + value +
- ' is not a[n] ' + (type.name || typeof type));
- }
- return value;
-};
// Copyright 2015 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.
@@ -6557,8 +6591,14 @@ Polymer({
properties: {
/**
- * If you want to use the attribute value of an element for `selected` instead of the index,
- * set this to the name of the attribute.
+ * If you want to use an attribute value or property of an element for
+ * `selected` instead of the index, set this to the name of the attribute
+ * or property. Hyphenated values are converted to camel case when used to
+ * look up the property of a selectable element. Camel cased values are
+ * *not* converted to hyphenated values for attribute lookup. It's
+ * recommended that you provide the hyphenated form of the name so that
+ * selection works in both cases. (Use `attr-or-property-name` instead of
+ * `attrOrPropertyName`.)
*/
attrForSelected: {
type: String,
@@ -6619,6 +6659,15 @@ Polymer({
},
/**
+ * Default fallback if the selection based on selected with `attrForSelected`
+ * is not found.
+ */
+ fallbackSelection: {
+ type: String,
+ value: null
+ },
+
+ /**
* The list of items from which a selection can be made.
*/
items: {
@@ -6648,7 +6697,8 @@ Polymer({
observers: [
'_updateAttrForSelected(attrForSelected)',
- '_updateSelected(selected)'
+ '_updateSelected(selected)',
+ '_checkFallback(fallbackSelection)'
],
created: function() {
@@ -6734,6 +6784,12 @@ Polymer({
return this.selected != null;
},
+ _checkFallback: function() {
+ if (this._shouldUpdateSelection) {
+ this._updateSelected();
+ }
+ },
+
_addListener: function(eventName) {
this.listen(this, eventName, '_activateHandler');
},
@@ -6755,7 +6811,7 @@ Polymer({
_updateAttrForSelected: function() {
if (this._shouldUpdateSelection) {
- this.selected = this._indexToValue(this.indexOf(this.selectedItem));
+ this.selected = this._indexToValue(this.indexOf(this.selectedItem));
}
},
@@ -6765,6 +6821,11 @@ Polymer({
_selectSelected: function(selected) {
this._selection.select(this._valueToItem(this.selected));
+ // Check for items, since this array is populated only when attached
+ // Since Number(0) is falsy, explicitly check for undefined
+ if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
+ this.selected = this.fallbackSelection;
+ }
},
_filterItem: function(node) {
@@ -6799,7 +6860,7 @@ Polymer({
},
_valueForItem: function(item) {
- var propValue = item[this.attrForSelected];
+ var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)];
return propValue != undefined ? propValue : item.getAttribute(this.attrForSelected);
},
@@ -6892,7 +6953,7 @@ Polymer({
},
observers: [
- '_updateSelected(selectedValues)'
+ '_updateSelected(selectedValues.splices)'
],
/**
@@ -6928,7 +6989,7 @@ Polymer({
Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
} else if (this._shouldUpdateSelection) {
this.selectedValues = this.selectedItems.map(function(selectedItem) {
- return this._indexToValue(this.indexOf(selectedItem));
+ return this._indexToValue(this.indexOf(selectedItem));
}, this).filter(function(unfilteredValue) {
return unfilteredValue != null;
}, this);
@@ -6952,6 +7013,13 @@ Polymer({
for (var i = 0; i < selectedItems.length; i++) {
this._selection.setItemSelected(selectedItems[i], true);
}
+ // Check for items, since this array is populated only when attached
+ if (this.fallbackSelection && this.items.length && !this._selection.get().length) {
+ var fallback = this._valueToItem(this.fallbackSelection);
+ if (fallback) {
+ this.selectedValues = [this.fallbackSelection];
+ }
+ }
} else {
this._selection.clear();
}
@@ -6975,7 +7043,6 @@ Polymer({
} else {
this.splice('selectedValues',i,1);
}
- this._selection.setItemSelected(this._valueToItem(value), unselected);
},
_valuesToItems: function(values) {
@@ -7053,6 +7120,8 @@ Polymer({
* @param {string|number} value the value to select.
*/
select: function(value) {
+ // Cancel automatically focusing a default item if the menu received focus
+ // through a user action selecting a particular item.
if (this._defaultFocusAsync) {
this.cancelAsync(this._defaultFocusAsync);
this._defaultFocusAsync = null;
@@ -7223,8 +7292,6 @@ Polymer({
return;
}
- this.blur();
-
// clear the cached focus item
this._defaultFocusAsync = this.async(function() {
// focus the selected item when the menu receives focus, or the first item
@@ -7235,11 +7302,10 @@ Polymer({
if (selectedItem) {
this._setFocusedItem(selectedItem);
- } else {
+ } else if (this.items[0]) {
this._setFocusedItem(this.items[0]);
}
- // async 1ms to wait for `select` to get called from `_itemActivate`
- }, 1);
+ });
},
/**
@@ -7569,195 +7635,356 @@ CSS properties | Action
/**
* @struct
* @constructor
+ * @private
*/
Polymer.IronOverlayManagerClass = function() {
+ /**
+ * Used to keep track of the opened overlays.
+ * @private {Array<Element>}
+ */
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.
+ * iframes have a default z-index of 100,
+ * so this default should be at least that.
* @private {number}
*/
this._minimumZ = 101;
- this._backdrops = [];
-
+ /**
+ * Memoized backdrop element.
+ * @private {Element|null}
+ */
this._backdropElement = null;
- Object.defineProperty(this, 'backdropElement', {
- get: function() {
- if (!this._backdropElement) {
- this._backdropElement = document.createElement('iron-overlay-backdrop');
- }
- return this._backdropElement;
- }.bind(this)
- });
+
+ // Listen to mousedown or touchstart to be sure to be the first to capture
+ // clicks outside the overlay.
+ var clickEvent = ('ontouchstart' in window) ? 'touchstart' : 'mousedown';
+ document.addEventListener(clickEvent, this._onCaptureClick.bind(this), true);
+ document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
+ document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
+ };
+
+ Polymer.IronOverlayManagerClass.prototype = {
+
+ constructor: Polymer.IronOverlayManagerClass,
+
+ /**
+ * The shared backdrop element.
+ * @type {Element} backdropElement
+ */
+ get backdropElement() {
+ if (!this._backdropElement) {
+ this._backdropElement = document.createElement('iron-overlay-backdrop');
+ }
+ return this._backdropElement;
+ },
/**
* 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)
- });
- };
+ * @type {Element} activeElement the active element
+ */
+ get deepActiveElement() {
+ // document.activeElement can be null
+ // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
+ // In case of null, default it to document.body.
+ var active = document.activeElement || document.body;
+ while (active.root && Polymer.dom(active.root).activeElement) {
+ active = Polymer.dom(active.root).activeElement;
+ }
+ return active;
+ },
- /**
- * 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;
+ /**
+ * Brings the overlay at the specified index to the front.
+ * @param {number} i
+ * @private
+ */
+ _bringOverlayAtIndexToFront: function(i) {
+ var overlay = this._overlays[i];
+ var lastI = this._overlays.length - 1;
+ // Ensure always-on-top overlay stays on top.
+ if (!overlay.alwaysOnTop && this._overlays[lastI].alwaysOnTop) {
+ lastI--;
+ }
+ // If already the top element, return.
+ if (!overlay || i >= lastI) {
+ return;
+ }
+ // Update z-index to be on top.
+ var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
+ if (this._getZ(overlay) <= minimumZ) {
+ this._applyOverlayZ(overlay, minimumZ);
}
- }
- return false;
- };
- Polymer.IronOverlayManagerClass.prototype._applyOverlayZ = function(overlay, aboveZ) {
- this._setZ(overlay, aboveZ + 2);
- };
+ // Shift other overlays behind the new on top.
+ while (i < lastI) {
+ this._overlays[i] = this._overlays[i + 1];
+ i++;
+ }
+ this._overlays[lastI] = overlay;
+ },
- Polymer.IronOverlayManagerClass.prototype._setZ = function(element, z) {
- element.style.zIndex = z;
- };
+ /**
+ * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
+ * Also updates the backdrop z-index.
+ * @param {Element} overlay
+ */
+ addOrRemoveOverlay: function(overlay) {
+ if (overlay.opened) {
+ this.addOverlay(overlay);
+ } else {
+ this.removeOverlay(overlay);
+ }
+ this.trackBackdrop();
+ },
- /**
- * track overlays for z-index and focus managemant
- */
- Polymer.IronOverlayManagerClass.prototype.addOverlay = function(overlay) {
- var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
- this._overlays.push(overlay);
- var newZ = this.currentOverlayZ();
- 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);
- };
+ /**
+ * Tracks overlays for z-index and focus management.
+ * Ensures the last added overlay with always-on-top remains on top.
+ * @param {Element} overlay
+ */
+ addOverlay: function(overlay) {
+ var i = this._overlays.indexOf(overlay);
+ if (i >= 0) {
+ this._bringOverlayAtIndexToFront(i);
+ return;
+ }
+ var insertionIndex = this._overlays.length;
+ var currentOverlay = this._overlays[insertionIndex - 1];
+ var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
+ var newZ = this._getZ(overlay);
- Polymer.IronOverlayManagerClass.prototype.removeOverlay = function(overlay) {
- var i = this._overlays.indexOf(overlay);
- if (i >= 0) {
- this._overlays.splice(i, 1);
- this._setZ(overlay, '');
+ // Ensure always-on-top overlay stays on top.
+ if (currentOverlay && currentOverlay.alwaysOnTop && !overlay.alwaysOnTop) {
+ // This bumps the z-index of +2.
+ this._applyOverlayZ(currentOverlay, minimumZ);
+ insertionIndex--;
+ // Update minimumZ to match previous overlay's z-index.
+ var previousOverlay = this._overlays[insertionIndex - 1];
+ minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
+ }
- 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();
+ // Update z-index and insert overlay.
+ if (newZ <= minimumZ) {
+ this._applyOverlayZ(overlay, minimumZ);
}
- this._lastFocusedNodes.splice(i, 1);
- }
- };
+ this._overlays.splice(insertionIndex, 0, overlay);
- Polymer.IronOverlayManagerClass.prototype.currentOverlay = function() {
- var i = this._overlays.length - 1;
- while (this._overlays[i] && !this._overlays[i].opened) {
- --i;
- }
- return this._overlays[i];
- };
+ // Get focused node.
+ var element = this.deepActiveElement;
+ overlay.restoreFocusNode = this._overlayParent(element) ? null : element;
+ },
- Polymer.IronOverlayManagerClass.prototype.currentOverlayZ = function() {
- return this._getOverlayZ(this.currentOverlay());
- };
+ /**
+ * @param {Element} overlay
+ */
+ removeOverlay: function(overlay) {
+ var i = this._overlays.indexOf(overlay);
+ if (i === -1) {
+ return;
+ }
+ this._overlays.splice(i, 1);
- /**
- * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
- * This does not effect the z-index of any existing overlays.
- *
- * @param {number} minimumZ
- */
- Polymer.IronOverlayManagerClass.prototype.ensureMinimumZ = function(minimumZ) {
- this._minimumZ = Math.max(this._minimumZ, minimumZ);
- };
+ var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null;
+ overlay.restoreFocusNode = null;
+ // Focus back only if still contained in document.body
+ if (node && Polymer.dom(document.body).deepContains(node)) {
+ node.focus();
+ }
+ },
- Polymer.IronOverlayManagerClass.prototype.focusOverlay = function() {
- var current = this.currentOverlay();
- // We have to be careful to focus the next overlay _after_ any current
- // transitions are complete (due to the state being toggled prior to the
- // transition). Otherwise, we risk infinite recursion when a transitioning
- // (closed) overlay becomes the current overlay.
- //
- // NOTE: We make the assumption that any overlay that completes a transition
- // will call into focusOverlay to kick the process back off. Currently:
- // transitionend -> _applyFocus -> focusOverlay.
- if (current && !current.transitioning) {
- current._applyFocus();
- }
- };
+ /**
+ * Returns the current overlay.
+ * @return {Element|undefined}
+ */
+ currentOverlay: function() {
+ var i = this._overlays.length - 1;
+ return this._overlays[i];
+ },
- Polymer.IronOverlayManagerClass.prototype.trackBackdrop = function(element) {
- // backdrops contains the overlays with a backdrop that are currently
- // visible
- var index = this._backdrops.indexOf(element);
- if (element.opened && element.withBackdrop) {
- // no duplicates
- if (index === -1) {
- this._backdrops.push(element);
- }
- } else if (index >= 0) {
- this._backdrops.splice(index, 1);
- }
- };
+ /**
+ * Returns the current overlay z-index.
+ * @return {number}
+ */
+ currentOverlayZ: function() {
+ return this._getZ(this.currentOverlay());
+ },
- Polymer.IronOverlayManagerClass.prototype.getBackdrops = function() {
- return this._backdrops;
- };
+ /**
+ * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
+ * This does not effect the z-index of any existing overlays.
+ * @param {number} minimumZ
+ */
+ ensureMinimumZ: function(minimumZ) {
+ this._minimumZ = Math.max(this._minimumZ, minimumZ);
+ },
- /**
- * Returns the z-index for the backdrop.
- */
- Polymer.IronOverlayManagerClass.prototype.backdropZ = function() {
- return this._getOverlayZ(this._overlayWithBackdrop()) - 1;
- };
+ focusOverlay: function() {
+ var current = /** @type {?} */ (this.currentOverlay());
+ // We have to be careful to focus the next overlay _after_ any current
+ // transitions are complete (due to the state being toggled prior to the
+ // transition). Otherwise, we risk infinite recursion when a transitioning
+ // (closed) overlay becomes the current overlay.
+ //
+ // NOTE: We make the assumption that any overlay that completes a transition
+ // will call into focusOverlay to kick the process back off. Currently:
+ // transitionend -> _applyFocus -> focusOverlay.
+ if (current && !current.transitioning) {
+ current._applyFocus();
+ }
+ },
- /**
- * Returns the first opened overlay that has a backdrop.
- */
- Polymer.IronOverlayManagerClass.prototype._overlayWithBackdrop = function() {
- for (var i = 0; i < this._overlays.length; i++) {
- if (this._overlays[i].opened && this._overlays[i].withBackdrop) {
- return this._overlays[i];
+ /**
+ * Updates the backdrop z-index.
+ */
+ trackBackdrop: function() {
+ this.backdropElement.style.zIndex = this.backdropZ();
+ },
+
+ /**
+ * @return {Array<Element>}
+ */
+ getBackdrops: function() {
+ var backdrops = [];
+ for (var i = 0; i < this._overlays.length; i++) {
+ if (this._overlays[i].withBackdrop) {
+ backdrops.push(this._overlays[i]);
+ }
}
- }
- };
+ return backdrops;
+ },
- /**
- * Calculates the minimum z-index for the overlay.
- */
- Polymer.IronOverlayManagerClass.prototype._getOverlayZ = function(overlay) {
- var z = this._minimumZ;
- if (overlay) {
- var z1 = Number(window.getComputedStyle(overlay).zIndex);
- // Check if is a number
- // Number.isNaN not supported in IE 10+
- if (z1 === z1) {
- z = z1;
+ /**
+ * Returns the z-index for the backdrop.
+ * @return {number}
+ */
+ backdropZ: function() {
+ return this._getZ(this._overlayWithBackdrop()) - 1;
+ },
+
+ /**
+ * Returns the first opened overlay that has a backdrop.
+ * @return {Element|undefined}
+ * @private
+ */
+ _overlayWithBackdrop: function() {
+ for (var i = 0; i < this._overlays.length; i++) {
+ if (this._overlays[i].withBackdrop) {
+ return this._overlays[i];
+ }
+ }
+ },
+
+ /**
+ * Calculates the minimum z-index for the overlay.
+ * @param {Element=} overlay
+ * @private
+ */
+ _getZ: function(overlay) {
+ var z = this._minimumZ;
+ if (overlay) {
+ var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
+ // Check if is a number
+ // Number.isNaN not supported in IE 10+
+ if (z1 === z1) {
+ z = z1;
+ }
+ }
+ return z;
+ },
+
+ /**
+ * @param {Element} element
+ * @param {number|string} z
+ * @private
+ */
+ _setZ: function(element, z) {
+ element.style.zIndex = z;
+ },
+
+ /**
+ * @param {Element} overlay
+ * @param {number} aboveZ
+ * @private
+ */
+ _applyOverlayZ: function(overlay, aboveZ) {
+ this._setZ(overlay, aboveZ + 2);
+ },
+
+ /**
+ * Returns the overlay containing the provided node. If the node is an overlay,
+ * it returns the node.
+ * @param {Element=} node
+ * @return {Element|undefined}
+ * @private
+ */
+ _overlayParent: function(node) {
+ while (node && node !== document.body) {
+ // Check if it is an overlay.
+ if (node._manager === this) {
+ return node;
+ }
+ // Use logical parentNode, or native ShadowRoot host.
+ node = Polymer.dom(node).parentNode || node.host;
+ }
+ },
+
+ /**
+ * Returns the deepest overlay in the path.
+ * @param {Array<Element>=} path
+ * @return {Element|undefined}
+ * @private
+ */
+ _overlayInPath: function(path) {
+ path = path || [];
+ for (var i = 0; i < path.length; i++) {
+ if (path[i]._manager === this) {
+ return path[i];
+ }
+ }
+ },
+
+ /**
+ * Ensures the click event is delegated to the right overlay.
+ * @param {!Event} event
+ * @private
+ */
+ _onCaptureClick: function(event) {
+ var overlay = /** @type {?} */ (this.currentOverlay());
+ // Check if clicked outside of top overlay.
+ if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
+ overlay._onCaptureClick(event);
+ }
+ },
+
+ /**
+ * Ensures the focus event is delegated to the right overlay.
+ * @param {!Event} event
+ * @private
+ */
+ _onCaptureFocus: function(event) {
+ var overlay = /** @type {?} */ (this.currentOverlay());
+ if (overlay) {
+ overlay._onCaptureFocus(event);
+ }
+ },
+
+ /**
+ * Ensures TAB and ESC keyboard events are delegated to the right overlay.
+ * @param {!Event} event
+ * @private
+ */
+ _onCaptureKeyDown: function(event) {
+ var overlay = /** @type {?} */ (this.currentOverlay());
+ if (overlay) {
+ if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
+ overlay._onCaptureEsc(event);
+ } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
+ overlay._onCaptureTab(event);
+ }
}
}
- return z;
};
Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
@@ -7794,8 +8021,6 @@ CSS properties | Action
* Appends the backdrop to document body and sets its `z-index` to be below the latest overlay.
*/
prepare: function() {
- // Always update z-index
- this.style.zIndex = this._manager.backdropZ();
if (!this.parentNode) {
Polymer.dom(document.body).appendChild(this);
}
@@ -7815,8 +8040,6 @@ CSS properties | Action
* Hides the backdrop if needed.
*/
close: function() {
- // Always update z-index
- this.style.zIndex = this._manager.backdropZ();
// close only if no element with backdrop is left
if (this._manager.getBackdrops().length === 0) {
// Read style before setting opened.
@@ -7849,6 +8072,10 @@ CSS properties | Action
});
})();
+// IIFE to help scripts concatenation.
+(function() {
+'use strict';
+
/**
Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
on top of other content. It includes an optional backdrop, and can be used to implement a variety
@@ -7952,15 +8179,6 @@ context. You should place this element as a child of `<body>` whenever possible.
},
/**
- * 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: {
@@ -7968,25 +8186,23 @@ context. You should place this element as a child of `<body>` whenever possible.
value: false
},
+ /**
+ * Set to true to keep overlay always on top.
+ */
+ alwaysOnTop: {
+ type: Boolean
+ },
+
+ /**
+ * Shortcut to access to the overlay manager.
+ * @private
+ * @type {Polymer.IronOverlayManagerClass}
+ */
_manager: {
type: Object,
value: Polymer.IronOverlayManager
},
- _boundOnCaptureClick: {
- type: Function,
- value: function() {
- return this._onCaptureClick.bind(this);
- }
- },
-
- _boundOnCaptureFocus: {
- type: Function,
- value: function() {
- return this._onCaptureFocus.bind(this);
- }
- },
-
/**
* The node being focused.
* @type {?Node}
@@ -7997,18 +8213,13 @@ context. You should place this element as a child of `<body>` whenever possible.
},
- keyBindings: {
- 'esc': '__onEsc',
- 'tab': '__onTab'
- },
-
listeners: {
'iron-resize': '_onIronResize'
},
/**
* The backdrop element.
- * @type {Node}
+ * @type {Element}
*/
get backdropElement() {
return this._manager.backdropElement;
@@ -8029,7 +8240,7 @@ context. You should place this element as a child of `<body>` whenever possible.
*
* 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]}
+ * @type {Array<Node>}
* @protected
*/
get _focusableNodes() {
@@ -8075,11 +8286,17 @@ context. You should place this element as a child of `<body>` whenever possible.
},
ready: function() {
+ // Used to skip calls to notifyResize and refit while the overlay is animating.
+ this.__isAnimating = false;
// 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;
+ // Used for requestAnimationFrame when opened changes.
+ this.__openChangedAsync = null;
+ // Used for requestAnimationFrame when iron-resize is fired.
+ this.__onIronResizeAsync = null;
this._ensureSetup();
},
@@ -8095,8 +8312,10 @@ context. You should place this element as a child of `<body>` whenever possible.
Polymer.dom(this).unobserveNodes(this._observer);
this._observer = null;
this.opened = false;
- this._manager.trackBackdrop(this);
- this._manager.removeOverlay(this);
+ if (this.withBackdrop) {
+ // Allow user interactions right away.
+ this.backdropElement.close();
+ }
},
/**
@@ -8125,7 +8344,7 @@ context. You should place this element as a child of `<body>` whenever possible.
/**
* Cancels the overlay.
- * @param {?Event} event The original event
+ * @param {Event=} event The original event
*/
cancel: function(event) {
var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
@@ -8158,31 +8377,26 @@ context. You should place this element as a child of `<body>` whenever possible.
return;
}
- this._manager.trackBackdrop(this);
+ this._manager.addOrRemoveOverlay(this);
- if (this.opened) {
- this._prepareRenderOpened();
- }
+ this.__isAnimating = true;
- if (this._openChangedAsync) {
- this.cancelAsync(this._openChangedAsync);
+ // requestAnimationFrame for non-blocking rendering
+ if (this.__openChangedAsync) {
+ cancelAnimationFrame(this.__openChangedAsync);
}
- // Async here to allow overlay layer to become visible, and to avoid
- // listeners to immediately close via a click.
- this._openChangedAsync = this.async(function() {
- // overlay becomes visible here
- this.style.display = '';
- // Force layout to ensure transition will go. Set offsetWidth to itself
- // so that compilers won't remove it.
- this.offsetWidth = this.offsetWidth;
- if (this.opened) {
- this._renderOpened();
- } else {
- this._renderClosed();
+ if (this.opened) {
+ if (this.withBackdrop) {
+ this.backdropElement.prepare();
}
- this._toggleListeners();
- this._openChangedAsync = null;
- }, 1);
+ this.__openChangedAsync = requestAnimationFrame(function() {
+ this.__openChangedAsync = null;
+ this._prepareRenderOpened();
+ this._renderOpened();
+ }.bind(this));
+ } else {
+ this._renderClosed();
+ }
},
_canceledChanged: function() {
@@ -8200,7 +8414,7 @@ context. You should place this element as a child of `<body>` whenever possible.
this.__shouldRemoveTabIndex = false;
}
if (this.opened) {
- this._manager.trackBackdrop(this);
+ this._manager.trackBackdrop();
if (this.withBackdrop) {
this.backdropElement.prepare();
// Give time to be added to document.
@@ -8213,42 +8427,18 @@ context. You should place this element as a child of `<body>` whenever possible.
}
},
- _toggleListener: function(enable, node, event, boundListener, capture) {
- if (enable) {
- // enable document-wide tap recognizer
- if (event === 'tap') {
- Polymer.Gestures.add(document, 'tap', null);
- }
- node.addEventListener(event, boundListener, capture);
- } else {
- // disable document-wide tap recognizer
- if (event === 'tap') {
- Polymer.Gestures.remove(document, 'tap', null);
- }
- node.removeEventListener(event, boundListener, capture);
- }
- },
-
- _toggleListeners: function() {
- this._toggleListener(this.opened, document, 'tap', this._boundOnCaptureClick, true);
- this._toggleListener(this.opened, document, 'focus', this._boundOnCaptureFocus, true);
- },
-
- // tasks which must occur before opening; e.g. making the element visible
+ /**
+ * tasks which must occur before opening; e.g. making the element visible.
+ * @protected
+ */
_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.refit();
this._finishPositioning();
- 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) {
@@ -8256,8 +8446,10 @@ context. You should place this element as a child of `<body>` whenever possible.
}
},
- // tasks which cause the overlay to actually open; typically play an
- // animation
+ /**
+ * Tasks which cause the overlay to actually open; typically play an animation.
+ * @protected
+ */
_renderOpened: function() {
if (this.withBackdrop) {
this.backdropElement.open();
@@ -8265,6 +8457,10 @@ context. You should place this element as a child of `<body>` whenever possible.
this._finishRenderOpened();
},
+ /**
+ * Tasks which cause the overlay to actually close; typically play an animation.
+ * @protected
+ */
_renderClosed: function() {
if (this.withBackdrop) {
this.backdropElement.close();
@@ -8272,25 +8468,33 @@ context. You should place this element as a child of `<body>` whenever possible.
this._finishRenderClosed();
},
+ /**
+ * Tasks to be performed at the end of open action. Will fire `iron-overlay-opened`.
+ * @protected
+ */
_finishRenderOpened: function() {
- // 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.notifyResize();
+ this.__isAnimating = false;
this.fire('iron-overlay-opened');
},
+ /**
+ * Tasks to be performed at the end of close action. Will fire `iron-overlay-closed`.
+ * @protected
+ */
_finishRenderClosed: function() {
// Hide the overlay and remove the backdrop.
- this.resetFit();
this.style.display = 'none';
- this._manager.removeOverlay(this);
+ // Reset z-index only at the end of the animation.
+ this.style.zIndex = '';
this._applyFocus();
- this.notifyResize();
+ this.notifyResize();
+ this.__isAnimating = false;
this.fire('iron-overlay-closed', this.closingReason);
},
@@ -8301,14 +8505,24 @@ context. You should place this element as a child of `<body>` whenever possible.
},
_finishPositioning: function() {
+ // First, make it invisible & reactivate animations.
this.style.display = 'none';
- this.style.transform = this.style.webkitTransform = '';
- // Force layout layout to avoid application of transform.
- // Set offsetWidth to itself so that compilers won't remove it.
- this.offsetWidth = this.offsetWidth;
+ // Force reflow before re-enabling animations so that they don't start.
+ // Set scrollTop to itself so that Closure Compiler doesn't remove this.
+ this.scrollTop = this.scrollTop;
this.style.transition = this.style.webkitTransition = '';
+ this.style.transform = this.style.webkitTransform = '';
+ // Now that animations are enabled, make it visible again
+ this.style.display = '';
+ // Force reflow, so that following animations are properly started.
+ // Set scrollTop to itself so that Closure Compiler doesn't remove this.
+ this.scrollTop = this.scrollTop;
},
+ /**
+ * Applies focus according to the opened state.
+ * @protected
+ */
_applyFocus: function() {
if (this.opened) {
if (!this.noAutoFocus) {
@@ -8321,68 +8535,56 @@ context. You should place this element as a child of `<body>` whenever possible.
}
},
+ /**
+ * Cancels (closes) the overlay. Call when click happens outside the overlay.
+ * @param {!Event} event
+ * @protected
+ */
_onCaptureClick: function(event) {
- if (this._manager.currentOverlay() === this &&
- Polymer.dom(event).path.indexOf(this) === -1) {
- if (this.noCancelOnOutsideClick) {
- this._applyFocus();
- } else {
- this.cancel(event);
- }
+ if (!this.noCancelOnOutsideClick) {
+ this.cancel(event);
}
},
+ /**
+ * Keeps track of the focused child. If withBackdrop, traps focus within overlay.
+ * @param {!Event} event
+ * @protected
+ */
_onCaptureFocus: function (event) {
- if (this._manager.currentOverlay() === this && this.withBackdrop) {
- var path = Polymer.dom(event).path;
- if (path.indexOf(this) === -1) {
- event.stopPropagation();
- this._applyFocus();
- } else {
- this._focusedChild = path[0];
- }
+ if (!this.withBackdrop) {
+ return;
}
- },
-
- _onIronResize: function() {
- if (this.opened) {
- this.refit();
+ var path = Polymer.dom(event).path;
+ if (path.indexOf(this) === -1) {
+ event.stopPropagation();
+ this._applyFocus();
+ } else {
+ this._focusedChild = path[0];
}
},
/**
+ * Handles the ESC key event and cancels (closes) the overlay.
+ * @param {!Event} event
* @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];
- },
-
- __onEsc: function(event) {
- // Not opened or not on top, so return.
- if (this._manager.currentOverlay() !== this) {
- return;
- }
+ _onCaptureEsc: function(event) {
if (!this.noCancelOnEscKey) {
this.cancel(event);
}
},
- __onTab: function(event) {
- // Not opened or not on top, so return.
- if (this._manager.currentOverlay() !== this) {
- return;
- }
+ /**
+ * Handles TAB key events to track focus changes.
+ * Will wrap focus for overlays withBackdrop.
+ * @param {!Event} event
+ * @protected
+ */
+ _onCaptureTab: function(event) {
// TAB wraps from last to first focusable.
// Shift + TAB wraps from first to last focusable.
- var shift = event.detail.keyboardEvent.shiftKey;
+ var shift = event.shiftKey;
var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusableNode;
var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNode;
if (this.withBackdrop && this._focusedChild === nodeToCheck) {
@@ -8390,11 +8592,43 @@ context. You should place this element as a child of `<body>` whenever possible.
// wrapping of the focus (the next event after tab is focus).
this._focusedChild = nodeToSet;
}
+ },
+
+ /**
+ * Refits if the overlay is opened and not animating.
+ * @protected
+ */
+ _onIronResize: function() {
+ if (this.__onIronResizeAsync) {
+ cancelAnimationFrame(this.__onIronResizeAsync);
+ this.__onIronResizeAsync = null;
+ }
+ if (this.opened && !this.__isAnimating) {
+ this.__onIronResizeAsync = requestAnimationFrame(function() {
+ this.__onIronResizeAsync = null;
+ this.refit();
+ }.bind(this));
+ }
+ },
+
+ /**
+ * Will call notifyResize if overlay is opened.
+ * Can be overridden in order to avoid multiple observers on the same node.
+ * @protected
+ */
+ _onNodesChange: function() {
+ if (this.opened && !this.__isAnimating) {
+ 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];
}
};
/** @polymerBehavior */
- Polymer.IronOverlayBehavior = [Polymer.IronA11yKeysBehavior, Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
+ Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
/**
* Fired after the `iron-overlay` opens.
@@ -8415,6 +8649,8 @@ context. You should place this element as a child of `<body>` whenever possible.
* @event iron-overlay-closed
* @param {{canceled: (boolean|undefined)}} closingReason Contains `canceled` (whether the overlay was canceled).
*/
+
+})();
/**
* Use `Polymer.NeonAnimationBehavior` to implement an animation.
* @polymerBehavior
@@ -8918,9 +9154,11 @@ Polymer({
},
_scrollInteractionHandler: function(event) {
+ var scrolledElement =
+ /** @type {HTMLElement} */(Polymer.dom(event).rootTarget);
if (Polymer
.IronDropdownScrollManager
- .elementIsScrollLocked(Polymer.dom(event).rootTarget)) {
+ .elementIsScrollLocked(scrolledElement)) {
if (event.type === 'keydown' &&
!Polymer.IronDropdownScrollManager._isScrollingKeypress(event)) {
return;
@@ -9050,8 +9288,7 @@ Polymer({
* it is opened.
*/
positionTarget: {
- type: Object,
- observer: '_positionTargetChanged'
+ type: Object
},
/**
@@ -9096,15 +9333,6 @@ Polymer({
allowOutsideScroll: {
type: Boolean,
value: false
- },
-
- /**
- * We memoize the positionTarget bounding rectangle so that we can
- * limit the number of times it is queried per resize / relayout.
- * @type {?Object}
- */
- _positionRectMemo: {
- type: Object
}
},
@@ -9113,13 +9341,13 @@ Polymer({
},
observers: [
- '_updateOverlayPosition(verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
+ '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
],
attached: function() {
- if (this.positionTarget === undefined) {
- this.positionTarget = this._defaultPositionTarget;
- }
+ this.positionTarget = this.positionTarget || this._defaultPositionTarget;
+ // Memoize this to avoid expensive calculations & relayouts.
+ this._isRTL = window.getComputedStyle(this).direction == 'rtl';
},
/**
@@ -9138,13 +9366,6 @@ Polymer({
},
/**
- * Whether the text direction is RTL
- */
- _isRTL: function() {
- return window.getComputedStyle(this).direction == 'rtl';
- },
-
- /**
* The element that should be used to position the dropdown when
* it opens, if no position target is configured.
*/
@@ -9159,31 +9380,32 @@ Polymer({
},
/**
- * The bounding rect of the position target.
+ * The horizontal align value, accounting for the RTL/LTR text direction.
*/
- get _positionRect() {
- if (!this._positionRectMemo && this.positionTarget) {
- this._positionRectMemo = this.positionTarget.getBoundingClientRect();
+ get _localeHorizontalAlign() {
+ // In RTL, "left" becomes "right".
+ if (this._isRTL) {
+ return this.horizontalAlign === 'right' ? 'left' : 'right';
+ } else {
+ return this.horizontalAlign;
}
-
- return this._positionRectMemo;
},
/**
* The horizontal offset value used to position the dropdown.
+ * @param {ClientRect} dropdownRect
+ * @param {ClientRect} positionRect
+ * @param {boolean=false} fromRight
+ * @return {number} pixels
+ * @private
*/
- get _horizontalAlignTargetValue() {
+ _horizontalAlignTargetValue: function(dropdownRect, positionRect, fromRight) {
var target;
-
- // In RTL, the direction flips, so what is "right" in LTR becomes "left".
- var isRTL = this._isRTL();
- if ((!isRTL && this.horizontalAlign === 'right') ||
- (isRTL && this.horizontalAlign === 'left')) {
- target = document.documentElement.clientWidth - this._positionRect.right;
+ if (fromRight) {
+ target = document.documentElement.clientWidth - positionRect.right - (this._fitWidth - dropdownRect.right);
} else {
- target = this._positionRect.left;
+ target = positionRect.left - dropdownRect.left;
}
-
target += this.horizontalOffset;
return Math.max(target, 0);
@@ -9191,34 +9413,25 @@ Polymer({
/**
* The vertical offset value used to position the dropdown.
+ * @param {ClientRect} dropdownRect
+ * @param {ClientRect} positionRect
+ * @param {boolean=false} fromBottom
+ * @return {number} pixels
+ * @private
*/
- get _verticalAlignTargetValue() {
+ _verticalAlignTargetValue: function(dropdownRect, positionRect, fromBottom) {
var target;
-
- if (this.verticalAlign === 'bottom') {
- target = document.documentElement.clientHeight - this._positionRect.bottom;
+ if (fromBottom) {
+ target = document.documentElement.clientHeight - positionRect.bottom - (this._fitHeight - dropdownRect.bottom);
} else {
- target = this._positionRect.top;
+ target = positionRect.top - dropdownRect.top;
}
-
target += this.verticalOffset;
return Math.max(target, 0);
},
/**
- * The horizontal align value, accounting for the RTL/LTR text direction.
- */
- get _localeHorizontalAlign() {
- // In RTL, "left" becomes "right".
- if (this._isRTL()) {
- return this.horizontalAlign === 'right' ? 'left' : 'right';
- } else {
- return this.horizontalAlign;
- }
- },
-
- /**
* Called when the value of `opened` changes.
*
* @param {boolean} opened True if the dropdown is opened.
@@ -9228,7 +9441,8 @@ Polymer({
this.cancel();
} else {
this.cancelAnimation();
- this._prepareDropdown();
+ this.sizingTarget = this.containedElement || this.sizingTarget;
+ this._updateAnimationConfig();
Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
}
},
@@ -9242,6 +9456,9 @@ Polymer({
}
if (!this.noAnimations && this.animationConfig && this.animationConfig.open) {
+ if (this.withBackdrop) {
+ this.backdropElement.open();
+ }
this.$.contentWrapper.classList.add('animating');
this.playAnimation('open');
} else {
@@ -9255,6 +9472,9 @@ Polymer({
_renderClosed: function() {
Polymer.IronDropdownScrollManager.removeScrollLock(this);
if (!this.noAnimations && this.animationConfig && this.animationConfig.close) {
+ if (this.withBackdrop) {
+ this.backdropElement.close();
+ }
this.$.contentWrapper.classList.add('animating');
this.playAnimation('close');
} else {
@@ -9271,45 +9491,13 @@ Polymer({
_onNeonAnimationFinish: function() {
this.$.contentWrapper.classList.remove('animating');
if (this.opened) {
- Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this);
+ Polymer.IronOverlayBehaviorImpl._finishRenderOpened.apply(this);
} else {
- Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this);
+ Polymer.IronOverlayBehaviorImpl._finishRenderClosed.apply(this);
}
},
/**
- * Called when an `iron-resize` event fires.
- */
- _onIronResize: function() {
- var containedElement = this.containedElement;
- var scrollTop;
- var scrollLeft;
-
- if (this.opened && containedElement) {
- scrollTop = containedElement.scrollTop;
- scrollLeft = containedElement.scrollLeft;
- }
-
- if (this.opened) {
- this._updateOverlayPosition();
- }
-
- Polymer.IronOverlayBehaviorImpl._onIronResize.apply(this, arguments);
-
- if (this.opened && containedElement) {
- containedElement.scrollTop = scrollTop;
- containedElement.scrollLeft = scrollLeft;
- }
- },
-
- /**
- * Called when the `positionTarget` property changes.
- */
- _positionTargetChanged: function() {
- this._updateOverlayPosition();
- },
-
- /**
* Constructs the final animation config from different properties used
* to configure specific parts of the opening and closing animations.
*/
@@ -9341,43 +9529,73 @@ Polymer({
},
/**
- * Prepares the dropdown for opening by updating measured layout
- * values.
+ * Updates the overlay position based on configured horizontal
+ * and vertical alignment.
*/
- _prepareDropdown: function() {
- this.sizingTarget = this.containedElement || this.sizingTarget;
- this._updateAnimationConfig();
- this._updateOverlayPosition();
+ _updateOverlayPosition: function() {
+ if (this.isAttached) {
+ // This triggers iron-resize, and iron-overlay-behavior will call refit if needed.
+ this.notifyResize();
+ }
},
/**
- * Updates the overlay position based on configured horizontal
- * and vertical alignment, and re-memoizes these values for the sake
- * of behavior in `IronFitBehavior`.
+ * Useful to call this after the element, the window, or the `fitInfo`
+ * element has been resized. Will maintain the scroll position.
*/
- _updateOverlayPosition: function() {
- this._positionRectMemo = null;
+ refit: function () {
+ if (!this.opened) {
+ return
+ }
+ var containedElement = this.containedElement;
+ var scrollTop;
+ var scrollLeft;
- if (!this.positionTarget) {
- return;
+ if (containedElement) {
+ scrollTop = containedElement.scrollTop;
+ scrollLeft = containedElement.scrollLeft;
+ }
+ Polymer.IronFitBehavior.refit.apply(this, arguments);
+
+ if (containedElement) {
+ containedElement.scrollTop = scrollTop;
+ containedElement.scrollLeft = scrollLeft;
}
+ },
+
+ /**
+ * Resets the target element's position and size constraints, and clear
+ * the memoized data.
+ */
+ resetFit: function() {
+ Polymer.IronFitBehavior.resetFit.apply(this, arguments);
- this.style[this._localeHorizontalAlign] =
- this._horizontalAlignTargetValue + 'px';
+ var hAlign = this._localeHorizontalAlign;
+ var vAlign = this.verticalAlign;
+ // Set to 0, 0 in order to discover any offset caused by parent stacking contexts.
+ this.style[hAlign] = this.style[vAlign] = '0px';
- this.style[this.verticalAlign] =
- this._verticalAlignTargetValue + 'px';
+ var dropdownRect = this.getBoundingClientRect();
+ var positionRect = this.positionTarget.getBoundingClientRect();
+ var horizontalValue = this._horizontalAlignTargetValue(dropdownRect, positionRect, hAlign === 'right');
+ var verticalValue = this._verticalAlignTargetValue(dropdownRect, positionRect, vAlign === 'bottom');
- // NOTE(cdata): We re-memoize inline styles here, otherwise
- // calling `refit` from `IronFitBehavior` will reset inline styles
- // to whatever they were when the dropdown first opened.
- if (this._fitInfo) {
- this._fitInfo.inlineStyle[this.horizontalAlign] =
- this.style[this.horizontalAlign];
+ this.style[hAlign] = horizontalValue + 'px';
+ this.style[vAlign] = verticalValue + 'px';
+ },
- this._fitInfo.inlineStyle[this.verticalAlign] =
- this.style[this.verticalAlign];
- }
+ /**
+ * Overridden from `IronFitBehavior`.
+ * Ensure positionedBy has correct values for horizontally & vertically.
+ */
+ _discoverInfo: function() {
+ Polymer.IronFitBehavior._discoverInfo.apply(this, arguments);
+ // Note(valdrin): in Firefox, an element with style `position: fixed; bottom: 90vh; height: 20vh`
+ // would have `getComputedStyle(element).top < 0` (instead of being `auto`) http://jsbin.com/cofired/3/edit?html,output
+ // This would cause IronFitBehavior's `constrain` to wrongly calculate sizes
+ // (it would use `top` instead of `bottom`), so we ensure we give the correct values.
+ this._fitInfo.positionedBy.horizontally = this._localeHorizontalAlign;
+ this._fitInfo.positionedBy.vertically = this.verticalAlign;
},
/**
@@ -9522,257 +9740,278 @@ Polymer({
}
});
(function() {
- 'use strict';
+ 'use strict';
- var PaperMenuButton = Polymer({
- is: 'paper-menu-button',
+ var PaperMenuButton = Polymer({
+ is: 'paper-menu-button',
- /**
- * Fired when the dropdown opens.
- *
- * @event paper-dropdown-open
- */
+ /**
+ * Fired when the dropdown opens.
+ *
+ * @event paper-dropdown-open
+ */
- /**
- * Fired when the dropdown closes.
- *
- * @event paper-dropdown-close
- */
+ /**
+ * Fired when the dropdown closes.
+ *
+ * @event paper-dropdown-close
+ */
- behaviors: [
- Polymer.IronA11yKeysBehavior,
- Polymer.IronControlState
- ],
+ behaviors: [
+ Polymer.IronA11yKeysBehavior,
+ Polymer.IronControlState
+ ],
- properties: {
+ properties: {
+ /**
+ * True if the content is currently displayed.
+ */
+ opened: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ observer: '_openedChanged'
+ },
- /**
- * True if the content is currently displayed.
- */
- opened: {
- type: Boolean,
- value: false,
- notify: true,
- observer: '_openedChanged'
+ /**
+ * The orientation against which to align the menu dropdown
+ * horizontally relative to the dropdown trigger.
+ */
+ horizontalAlign: {
+ type: String,
+ value: 'left',
+ reflectToAttribute: true
+ },
+
+ /**
+ * The orientation against which to align the menu dropdown
+ * vertically relative to the dropdown trigger.
+ */
+ verticalAlign: {
+ type: String,
+ value: 'top',
+ reflectToAttribute: true
+ },
+
+ /**
+ * A pixel value that will be added to the position calculated for the
+ * given `horizontalAlign`. Use a negative value to offset to the
+ * left, or a positive value to offset to the right.
+ */
+ horizontalOffset: {
+ type: Number,
+ value: 0,
+ notify: true
+ },
+
+ /**
+ * A pixel value that will be added to the position calculated for the
+ * given `verticalAlign`. Use a negative value to offset towards the
+ * top, or a positive value to offset towards the bottom.
+ */
+ verticalOffset: {
+ type: Number,
+ value: 0,
+ notify: true
+ },
+
+ /**
+ * Set to true to disable animations when opening and closing the
+ * dropdown.
+ */
+ noAnimations: {
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * Set to true to disable automatically closing the dropdown after
+ * a selection has been made.
+ */
+ ignoreSelect: {
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * An animation config. If provided, this will be used to animate the
+ * opening of the dropdown.
+ */
+ openAnimationConfig: {
+ type: Object,
+ value: function() {
+ return [{
+ name: 'fade-in-animation',
+ timing: {
+ delay: 100,
+ duration: 200
+ }
+ }, {
+ name: 'paper-menu-grow-width-animation',
+ timing: {
+ delay: 100,
+ duration: 150,
+ easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
+ }
+ }, {
+ name: 'paper-menu-grow-height-animation',
+ timing: {
+ delay: 100,
+ duration: 275,
+ easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
+ }
+ }];
+ }
+ },
+
+ /**
+ * An animation config. If provided, this will be used to animate the
+ * closing of the dropdown.
+ */
+ closeAnimationConfig: {
+ type: Object,
+ value: function() {
+ return [{
+ name: 'fade-out-animation',
+ timing: {
+ duration: 150
+ }
+ }, {
+ name: 'paper-menu-shrink-width-animation',
+ timing: {
+ delay: 100,
+ duration: 50,
+ easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
+ }
+ }, {
+ name: 'paper-menu-shrink-height-animation',
+ timing: {
+ duration: 200,
+ easing: 'ease-in'
+ }
+ }];
+ }
+ },
+
+ /**
+ * This is the element intended to be bound as the focus target
+ * for the `iron-dropdown` contained by `paper-menu-button`.
+ */
+ _dropdownContent: {
+ type: Object
+ }
},
- /**
- * The orientation against which to align the menu dropdown
- * horizontally relative to the dropdown trigger.
- */
- horizontalAlign: {
- type: String,
- value: 'left',
- reflectToAttribute: true
+ hostAttributes: {
+ role: 'group',
+ 'aria-haspopup': 'true'
},
- /**
- * The orientation against which to align the menu dropdown
- * vertically relative to the dropdown trigger.
- */
- verticalAlign: {
- type: String,
- value: 'top',
- reflectToAttribute: true
+ listeners: {
+ 'iron-select': '_onIronSelect'
},
/**
- * A pixel value that will be added to the position calculated for the
- * given `horizontalAlign`. Use a negative value to offset to the
- * left, or a positive value to offset to the right.
+ * The content element that is contained by the menu button, if any.
*/
- horizontalOffset: {
- type: Number,
- value: 0,
- notify: true
+ get contentElement() {
+ return Polymer.dom(this.$.content).getDistributedNodes()[0];
},
/**
- * A pixel value that will be added to the position calculated for the
- * given `verticalAlign`. Use a negative value to offset towards the
- * top, or a positive value to offset towards the bottom.
+ * Toggles the drowpdown content between opened and closed.
*/
- verticalOffset: {
- type: Number,
- value: 0,
- notify: true
+ toggle: function() {
+ if (this.opened) {
+ this.close();
+ } else {
+ this.open();
+ }
},
/**
- * Set to true to disable animations when opening and closing the
- * dropdown.
+ * Make the dropdown content appear as an overlay positioned relative
+ * to the dropdown trigger.
*/
- noAnimations: {
- type: Boolean,
- value: false
+ open: function() {
+ if (this.disabled) {
+ return;
+ }
+
+ this.$.dropdown.open();
},
/**
- * Set to true to disable automatically closing the dropdown after
- * a selection has been made.
+ * Hide the dropdown content.
*/
- ignoreSelect: {
- type: Boolean,
- value: false
+ close: function() {
+ this.$.dropdown.close();
},
/**
- * An animation config. If provided, this will be used to animate the
- * opening of the dropdown.
+ * When an `iron-select` event is received, the dropdown should
+ * automatically close on the assumption that a value has been chosen.
+ *
+ * @param {CustomEvent} event A CustomEvent instance with type
+ * set to `"iron-select"`.
*/
- openAnimationConfig: {
- type: Object,
- value: function() {
- return [{
- name: 'fade-in-animation',
- timing: {
- delay: 100,
- duration: 200
- }
- }, {
- name: 'paper-menu-grow-width-animation',
- timing: {
- delay: 100,
- duration: 150,
- easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
- }
- }, {
- name: 'paper-menu-grow-height-animation',
- timing: {
- delay: 100,
- duration: 275,
- easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
- }
- }];
+ _onIronSelect: function(event) {
+ if (!this.ignoreSelect) {
+ this.close();
}
},
/**
- * An animation config. If provided, this will be used to animate the
- * closing of the dropdown.
+ * When the dropdown opens, the `paper-menu-button` fires `paper-open`.
+ * When the dropdown closes, the `paper-menu-button` fires `paper-close`.
+ *
+ * @param {boolean} opened True if the dropdown is opened, otherwise false.
+ * @param {boolean} oldOpened The previous value of `opened`.
*/
- closeAnimationConfig: {
- type: Object,
- value: function() {
- return [{
- name: 'fade-out-animation',
- timing: {
- duration: 150
- }
- }, {
- name: 'paper-menu-shrink-width-animation',
- timing: {
- delay: 100,
- duration: 50,
- easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
- }
- }, {
- name: 'paper-menu-shrink-height-animation',
- timing: {
- duration: 200,
- easing: 'ease-in'
- }
- }];
+ _openedChanged: function(opened, oldOpened) {
+ if (opened) {
+ // TODO(cdata): Update this when we can measure changes in distributed
+ // children in an idiomatic way.
+ // We poke this property in case the element has changed. This will
+ // cause the focus target for the `iron-dropdown` to be updated as
+ // necessary:
+ this._dropdownContent = this.contentElement;
+ this.fire('paper-dropdown-open');
+ } else if (oldOpened != null) {
+ this.fire('paper-dropdown-close');
}
},
/**
- * This is the element intended to be bound as the focus target
- * for the `iron-dropdown` contained by `paper-menu-button`.
+ * If the dropdown is open when disabled becomes true, close the
+ * dropdown.
+ *
+ * @param {boolean} disabled True if disabled, otherwise false.
*/
- _dropdownContent: {
- type: Object
- }
- },
-
- hostAttributes: {
- role: 'group',
- 'aria-haspopup': 'true'
- },
-
- listeners: {
- 'iron-select': '_onIronSelect'
- },
-
- /**
- * The content element that is contained by the menu button, if any.
- */
- get contentElement() {
- return Polymer.dom(this.$.content).getDistributedNodes()[0];
- },
-
- /**
- * Make the dropdown content appear as an overlay positioned relative
- * to the dropdown trigger.
- */
- open: function() {
- if (this.disabled) {
- return;
- }
-
- this.$.dropdown.open();
- },
-
- /**
- * Hide the dropdown content.
- */
- close: function() {
- this.$.dropdown.close();
- },
-
- /**
- * When an `iron-select` event is received, the dropdown should
- * automatically close on the assumption that a value has been chosen.
- *
- * @param {CustomEvent} event A CustomEvent instance with type
- * set to `"iron-select"`.
- */
- _onIronSelect: function(event) {
- if (!this.ignoreSelect) {
- this.close();
- }
- },
+ _disabledChanged: function(disabled) {
+ Polymer.IronControlState._disabledChanged.apply(this, arguments);
+ if (disabled && this.opened) {
+ this.close();
+ }
+ },
- /**
- * When the dropdown opens, the `paper-menu-button` fires `paper-open`.
- * When the dropdown closes, the `paper-menu-button` fires `paper-close`.
- *
- * @param {boolean} opened True if the dropdown is opened, otherwise false.
- * @param {boolean} oldOpened The previous value of `opened`.
- */
- _openedChanged: function(opened, oldOpened) {
- if (opened) {
- // TODO(cdata): Update this when we can measure changes in distributed
- // children in an idiomatic way.
- // We poke this property in case the element has changed. This will
- // cause the focus target for the `iron-dropdown` to be updated as
- // necessary:
- this._dropdownContent = this.contentElement;
- this.fire('paper-dropdown-open');
- } else if (oldOpened != null) {
- this.fire('paper-dropdown-close');
- }
- },
+ __onIronOverlayCanceled: function(event) {
+ var uiEvent = event.detail;
+ var target = Polymer.dom(uiEvent).rootTarget;
+ var trigger = this.$.trigger;
+ var path = Polymer.dom(uiEvent).path;
- /**
- * If the dropdown is open when disabled becomes true, close the
- * dropdown.
- *
- * @param {boolean} disabled True if disabled, otherwise false.
- */
- _disabledChanged: function(disabled) {
- Polymer.IronControlState._disabledChanged.apply(this, arguments);
- if (disabled && this.opened) {
- this.close();
+ if (path.indexOf(trigger) > -1) {
+ event.preventDefault();
+ }
}
- }
- });
+ });
- PaperMenuButton.ANIMATION_CUBIC_BEZIER = 'cubic-bezier(.3,.95,.5,1)';
- PaperMenuButton.MAX_ANIMATION_TIME_MS = 400;
+ PaperMenuButton.ANIMATION_CUBIC_BEZIER = 'cubic-bezier(.3,.95,.5,1)';
+ PaperMenuButton.MAX_ANIMATION_TIME_MS = 400;
- Polymer.PaperMenuButton = PaperMenuButton;
- })();
+ Polymer.PaperMenuButton = PaperMenuButton;
+ })();
Polymer({
is: 'paper-icon-button',
@@ -9821,6 +10060,67 @@ Polymer({
}
}
});
+(function() {
+ 'use strict';
+
+ Polymer.IronA11yAnnouncer = Polymer({
+ is: 'iron-a11y-announcer',
+
+ properties: {
+
+ /**
+ * The value of mode is used to set the `aria-live` attribute
+ * for the element that will be announced. Valid values are: `off`,
+ * `polite` and `assertive`.
+ */
+ mode: {
+ type: String,
+ value: 'polite'
+ },
+
+ _text: {
+ type: String,
+ value: ''
+ }
+ },
+
+ created: function() {
+ if (!Polymer.IronA11yAnnouncer.instance) {
+ Polymer.IronA11yAnnouncer.instance = this;
+ }
+
+ document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(this));
+ },
+
+ /**
+ * Cause a text string to be announced by screen readers.
+ *
+ * @param {string} text The text that should be announced.
+ */
+ announce: function(text) {
+ this._text = '';
+ this.async(function() {
+ this._text = text;
+ }, 100);
+ },
+
+ _onIronAnnounce: function(event) {
+ if (event.detail && event.detail.text) {
+ this.announce(event.detail.text);
+ }
+ }
+ });
+
+ Polymer.IronA11yAnnouncer.instance = null;
+
+ Polymer.IronA11yAnnouncer.requestAvailability = function() {
+ if (!Polymer.IronA11yAnnouncer.instance) {
+ Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-announcer');
+ }
+
+ document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
+ };
+ })();
/**
* `Use Polymer.IronValidatableBehavior` to implement an element that validates user input.
* Use the related `Polymer.IronValidatorBehavior` to add custom validation logic to an iron-input.
@@ -9993,18 +10293,22 @@ is separate from validation, and `allowed-pattern` does not affect how the input
},
/**
- * Set to true to prevent the user from entering invalid input. The new input characters are
- * matched with `allowedPattern` if it is set, otherwise it will use the `type` attribute (only
- * supported for `type=number`).
+ * Set to true to prevent the user from entering invalid input. If `allowedPattern` is set,
+ * any character typed by the user will be matched against that pattern, and rejected if it's not a match.
+ * Pasted input will have each character checked individually; if any character
+ * doesn't match `allowedPattern`, the entire pasted string will be rejected.
+ * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
*/
preventInvalidInput: {
type: Boolean
},
/**
- * Regular expression expressing a set of characters to enforce the validity of input characters.
- * The recommended value should follow this format: `[a-ZA-Z0-9.+-!;:]` that list the characters
- * allowed as input.
+ * Regular expression that list the characters allowed as input.
+ * This pattern represents the allowed characters for the field; as the user inputs text,
+ * each individual character will be checked against the pattern (rather than checking
+ * the entire value as a whole). The recommended format should be a list of allowed characters;
+ * for example, `[a-zA-Z0-9.+-!;:]`
*/
allowedPattern: {
type: String,
@@ -10028,6 +10332,47 @@ is separate from validation, and `allowed-pattern` does not affect how the input
'keypress': '_onKeypress'
},
+ /** @suppress {checkTypes} */
+ registered: function() {
+ // Feature detect whether we need to patch dispatchEvent (i.e. on FF and IE).
+ if (!this._canDispatchEventOnDisabled()) {
+ this._origDispatchEvent = this.dispatchEvent;
+ this.dispatchEvent = this._dispatchEventFirefoxIE;
+ }
+ },
+
+ created: function() {
+ Polymer.IronA11yAnnouncer.requestAvailability();
+ },
+
+ _canDispatchEventOnDisabled: function() {
+ var input = document.createElement('input');
+ var canDispatch = false;
+ input.disabled = true;
+
+ input.addEventListener('feature-check-dispatch-event', function() {
+ canDispatch = true;
+ });
+
+ try {
+ input.dispatchEvent(new Event('feature-check-dispatch-event'));
+ } catch(e) {}
+
+ return canDispatch;
+ },
+
+ _dispatchEventFirefoxIE: function() {
+ // Due to Firefox bug, events fired on disabled form controls can throw
+ // errors; furthermore, neither IE nor Firefox will actually dispatch
+ // events from disabled form controls; as such, we toggle disable around
+ // the dispatch to allow notifying properties to notify
+ // See issue #47 for details
+ var disabled = this.disabled;
+ this.disabled = false;
+ this._origDispatchEvent.apply(this, arguments);
+ this.disabled = disabled;
+ },
+
get _patternRegExp() {
var pattern;
if (this.allowedPattern) {
@@ -10068,6 +10413,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
if (this.preventInvalidInput && !this._patternAlreadyChecked) {
var valid = this._checkPatternValidity();
if (!valid) {
+ this._announceInvalidCharacter('Invalid string of characters not entered.');
this.value = this._previousValidInput;
}
}
@@ -10128,6 +10474,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
var thisChar = String.fromCharCode(event.charCode);
if (this._isPrintable(event) && !regexp.test(thisChar)) {
event.preventDefault();
+ this._announceInvalidCharacter('Invalid character ' + thisChar + ' not entered.');
}
},
@@ -10150,23 +10497,29 @@ is separate from validation, and `allowed-pattern` does not affect how the input
* @return {boolean} True if the value is valid.
*/
validate: function() {
- // Empty, non-required input is valid.
- if (!this.required && this.value == '') {
- this.invalid = false;
- return true;
- }
+ // First, check what the browser thinks. Some inputs (like type=number)
+ // behave weirdly and will set the value to "" if something invalid is
+ // entered, but will set the validity correctly.
+ var valid = this.checkValidity();
- var valid;
- if (this.hasValidator()) {
- valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
- } else {
- valid = this.checkValidity();
- this.invalid = !valid;
+ // Only do extra checking if the browser thought this was valid.
+ if (valid) {
+ // Empty, required input is invalid
+ if (this.required && this.value === '') {
+ valid = false;
+ } else if (this.hasValidator()) {
+ valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
+ }
}
+
+ this.invalid = !valid;
this.fire('iron-input-validate');
return valid;
- }
+ },
+ _announceInvalidCharacter: function(message) {
+ this.fire('iron-announce', { text: message });
+ }
});
/*
@@ -10303,14 +10656,15 @@ Polymer({
}
this.addEventListener('focus', this._boundOnFocus, true);
this.addEventListener('blur', this._boundOnBlur, true);
+ },
+
+ attached: function() {
if (this.attrForValue) {
this._inputElement.addEventListener(this._valueChangedEvent, this._boundValueChanged);
} else {
this.addEventListener('input', this._onInput);
}
- },
- attached: function() {
// Only validate when attached if the input already has a value.
if (this._inputElementValue != '') {
this._handleValueAndAutoValidate(this._inputElement);
« 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