aboutsummaryrefslogtreecommitdiff
path: root/catapult/third_party/polymer/components/iron-overlay-behavior/iron-overlay-behavior.html
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/third_party/polymer/components/iron-overlay-behavior/iron-overlay-behavior.html')
-rw-r--r--catapult/third_party/polymer/components/iron-overlay-behavior/iron-overlay-behavior.html637
1 files changed, 637 insertions, 0 deletions
diff --git a/catapult/third_party/polymer/components/iron-overlay-behavior/iron-overlay-behavior.html b/catapult/third_party/polymer/components/iron-overlay-behavior/iron-overlay-behavior.html
new file mode 100644
index 00000000..ae085c53
--- /dev/null
+++ b/catapult/third_party/polymer/components/iron-overlay-behavior/iron-overlay-behavior.html
@@ -0,0 +1,637 @@
+<!--
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
+The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
+The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
+Code distributed by Google as part of the polymer project is also
+subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
+-->
+
+<link rel="import" href="../polymer/polymer.html">
+<link rel="import" href="../iron-fit-behavior/iron-fit-behavior.html">
+<link rel="import" href="../iron-resizable-behavior/iron-resizable-behavior.html">
+<link rel="import" href="iron-overlay-manager.html">
+<link rel="import" href="iron-focusables-helper.html">
+
+<script>
+(function() {
+ 'use strict';
+
+ /** @polymerBehavior */
+ Polymer.IronOverlayBehaviorImpl = {
+
+ properties: {
+
+ /**
+ * True if the overlay is currently displayed.
+ */
+ opened: {
+ observer: '_openedChanged',
+ type: Boolean,
+ value: false,
+ notify: true
+ },
+
+ /**
+ * True if the overlay was canceled when it was last closed.
+ */
+ canceled: {
+ observer: '_canceledChanged',
+ readOnly: true,
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * Set to true to display a backdrop behind the overlay. It traps the focus
+ * within the light DOM of the overlay.
+ */
+ withBackdrop: {
+ observer: '_withBackdropChanged',
+ type: Boolean
+ },
+
+ /**
+ * Set to true to disable auto-focusing the overlay or child nodes with
+ * the `autofocus` attribute` when the overlay is opened.
+ */
+ noAutoFocus: {
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * Set to true to disable canceling the overlay with the ESC key.
+ */
+ noCancelOnEscKey: {
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * Set to true to disable canceling the overlay by clicking outside it.
+ */
+ noCancelOnOutsideClick: {
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * Contains the reason(s) this overlay was last closed (see `iron-overlay-closed`).
+ * `IronOverlayBehavior` provides the `canceled` reason; implementers of the
+ * behavior can provide other reasons in addition to `canceled`.
+ */
+ closingReason: {
+ // was a getter before, but needs to be a property so other
+ // behaviors can override this.
+ type: Object
+ },
+
+ /**
+ * Set to true to enable restoring of focus when overlay is closed.
+ */
+ restoreFocusOnClose: {
+ type: Boolean,
+ 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
+ },
+
+ /**
+ * The node being focused.
+ * @type {?Node}
+ */
+ _focusedChild: {
+ type: Object
+ }
+
+ },
+
+ listeners: {
+ 'iron-resize': '_onIronResize'
+ },
+
+ /**
+ * The backdrop element.
+ * @type {Element}
+ */
+ 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 {Array<Node>}
+ * @protected
+ */
+ get _focusableNodes() {
+ return Polymer.IronFocusablesHelper.getTabbableNodes(this);
+ },
+
+ 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 by __onNextAnimationFrame to cancel any previous callback.
+ this.__raf = null;
+ // Focused node before overlay gets opened. Can be restored on close.
+ this.__restoreFocusNode = null;
+ this._ensureSetup();
+ },
+
+ attached: function() {
+ // Call _openedChanged here so that position can be computed correctly.
+ if (this.opened) {
+ this._openedChanged(this.opened);
+ }
+ this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
+ },
+
+ detached: function() {
+ Polymer.dom(this).unobserveNodes(this._observer);
+ this._observer = null;
+ if (this.__raf) {
+ window.cancelAnimationFrame(this.__raf);
+ this.__raf = null;
+ }
+ this._manager.removeOverlay(this);
+ },
+
+ /**
+ * Toggle the opened state of the overlay.
+ */
+ toggle: function() {
+ this._setCanceled(false);
+ this.opened = !this.opened;
+ },
+
+ /**
+ * Open the overlay.
+ */
+ open: function() {
+ this._setCanceled(false);
+ this.opened = true;
+ },
+
+ /**
+ * Close the overlay.
+ */
+ close: function() {
+ this._setCanceled(false);
+ this.opened = false;
+ },
+
+ /**
+ * Cancels the overlay.
+ * @param {Event=} event The original event
+ */
+ cancel: function(event) {
+ var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
+ if (cancelEvent.defaultPrevented) {
+ return;
+ }
+
+ this._setCanceled(true);
+ this.opened = false;
+ },
+
+ /**
+ * Invalidates the cached tabbable nodes. To be called when any of the focusable
+ * content changes (e.g. a button is disabled).
+ */
+ invalidateTabbables: function() {
+ this.__firstFocusableNode = this.__lastFocusableNode = null;
+ },
+
+ _ensureSetup: function() {
+ if (this._overlaySetup) {
+ return;
+ }
+ this._overlaySetup = true;
+ this.style.outline = 'none';
+ this.style.display = 'none';
+ },
+
+ /**
+ * Called when `opened` changes.
+ * @param {boolean=} opened
+ * @protected
+ */
+ _openedChanged: function(opened) {
+ if (opened) {
+ this.removeAttribute('aria-hidden');
+ } else {
+ this.setAttribute('aria-hidden', 'true');
+ }
+
+ // Defer any animation-related code on attached
+ // (_openedChanged gets called again on attached).
+ if (!this.isAttached) {
+ return;
+ }
+
+ this.__isAnimating = true;
+
+ // Use requestAnimationFrame for non-blocking rendering.
+ this.__onNextAnimationFrame(this.__openedChanged);
+ },
+
+ _canceledChanged: function() {
+ this.closingReason = this.closingReason || {};
+ this.closingReason.canceled = this.canceled;
+ },
+
+ _withBackdropChanged: function() {
+ // If tabindex is already set, no need to override it.
+ if (this.withBackdrop && !this.hasAttribute('tabindex')) {
+ this.setAttribute('tabindex', '-1');
+ this.__shouldRemoveTabIndex = true;
+ } else if (this.__shouldRemoveTabIndex) {
+ this.removeAttribute('tabindex');
+ this.__shouldRemoveTabIndex = false;
+ }
+ if (this.opened && this.isAttached) {
+ this._manager.trackBackdrop();
+ }
+ },
+
+ /**
+ * tasks which must occur before opening; e.g. making the element visible.
+ * @protected
+ */
+ _prepareRenderOpened: function() {
+ // Store focused node.
+ this.__restoreFocusNode = this._manager.deepActiveElement;
+
+ // Needed to calculate the size of the overlay so that transitions on its size
+ // will have the correct starting points.
+ this._preparePositioning();
+ this.refit();
+ this._finishPositioning();
+
+ // Safari will apply the focus to the autofocus element when displayed
+ // for the first time, so we make sure to return the focus where it was.
+ if (this.noAutoFocus && document.activeElement === this._focusNode) {
+ this._focusNode.blur();
+ this.__restoreFocusNode.focus();
+ }
+ },
+
+ /**
+ * Tasks which cause the overlay to actually open; typically play an animation.
+ * @protected
+ */
+ _renderOpened: function() {
+ this._finishRenderOpened();
+ },
+
+ /**
+ * Tasks which cause the overlay to actually close; typically play an animation.
+ * @protected
+ */
+ _renderClosed: function() {
+ this._finishRenderClosed();
+ },
+
+ /**
+ * Tasks to be performed at the end of open action. Will fire `iron-overlay-opened`.
+ * @protected
+ */
+ _finishRenderOpened: function() {
+ 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.
+ this.style.display = 'none';
+ // Reset z-index only at the end of the animation.
+ this.style.zIndex = '';
+ this.notifyResize();
+ this.__isAnimating = false;
+ this.fire('iron-overlay-closed', this.closingReason);
+ },
+
+ _preparePositioning: function() {
+ this.style.transition = this.style.webkitTransition = 'none';
+ this.style.transform = this.style.webkitTransform = 'none';
+ this.style.display = '';
+ },
+
+ _finishPositioning: function() {
+ // First, make it invisible & reactivate animations.
+ this.style.display = 'none';
+ // 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) {
+ this._focusNode.focus();
+ }
+ }
+ else {
+ this._focusNode.blur();
+ this._focusedChild = null;
+ // Restore focus.
+ if (this.restoreFocusOnClose && this.__restoreFocusNode) {
+ this.__restoreFocusNode.focus();
+ }
+ this.__restoreFocusNode = null;
+ // If many overlays get closed at the same time, one of them would still
+ // be the currentOverlay even if already closed, and would call _applyFocus
+ // infinitely, so we check for this not to be the current overlay.
+ var currentOverlay = this._manager.currentOverlay();
+ if (currentOverlay && this !== currentOverlay) {
+ currentOverlay._applyFocus();
+ }
+ }
+ },
+
+ /**
+ * Cancels (closes) the overlay. Call when click happens outside the overlay.
+ * @param {!Event} event
+ * @protected
+ */
+ _onCaptureClick: function(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.withBackdrop) {
+ return;
+ }
+ 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
+ */
+ _onCaptureEsc: function(event) {
+ if (!this.noCancelOnEscKey) {
+ this.cancel(event);
+ }
+ },
+
+ /**
+ * Handles TAB key events to track focus changes.
+ * Will wrap focus for overlays withBackdrop.
+ * @param {!Event} event
+ * @protected
+ */
+ _onCaptureTab: function(event) {
+ if (!this.withBackdrop) {
+ return;
+ }
+ this.__ensureFirstLastFocusables();
+ // TAB wraps from last to first focusable.
+ // Shift + TAB wraps from first to last focusable.
+ var shift = event.shiftKey;
+ var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusableNode;
+ var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNode;
+ var shouldWrap = false;
+ if (nodeToCheck === nodeToSet) {
+ // If nodeToCheck is the same as nodeToSet, it means we have an overlay
+ // with 0 or 1 focusables; in either case we still need to trap the
+ // focus within the overlay.
+ shouldWrap = true;
+ } else {
+ // In dom=shadow, the manager will receive focus changes on the main
+ // root but not the ones within other shadow roots, so we can't rely on
+ // _focusedChild, but we should check the deepest active element.
+ var focusedNode = this._manager.deepActiveElement;
+ // If the active element is not the nodeToCheck but the overlay itself,
+ // it means the focus is about to go outside the overlay, hence we
+ // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
+ shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
+ }
+
+ if (shouldWrap) {
+ // When the overlay contains the last focusable element of the document
+ // and it's already focused, pressing TAB would move the focus outside
+ // the document (e.g. to the browser search bar). Similarly, when the
+ // overlay contains the first focusable element of the document and it's
+ // already focused, pressing Shift+TAB would move the focus outside the
+ // document (e.g. to the browser search bar).
+ // In both cases, we would not receive a focus event, but only a blur.
+ // In order to achieve focus wrapping, we prevent this TAB event and
+ // force the focus. This will also prevent the focus to temporarily move
+ // outside the overlay, which might cause scrolling.
+ event.preventDefault();
+ this._focusedChild = nodeToSet;
+ this._applyFocus();
+ }
+ },
+
+ /**
+ * Refits if the overlay is opened and not animating.
+ * @protected
+ */
+ _onIronResize: function() {
+ if (this.opened && !this.__isAnimating) {
+ this.__onNextAnimationFrame(this.refit);
+ }
+ },
+
+ /**
+ * 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) {
+ // It might have added focusable nodes, so invalidate cached values.
+ this.invalidateTabbables();
+ this.notifyResize();
+ }
+ },
+
+ /**
+ * Will set first and last focusable nodes if any of them is not set.
+ * @private
+ */
+ __ensureFirstLastFocusables: function() {
+ if (!this.__firstFocusableNode || !this.__lastFocusableNode) {
+ var focusableNodes = this._focusableNodes;
+ this.__firstFocusableNode = focusableNodes[0];
+ this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
+ }
+ },
+
+ /**
+ * Tasks executed when opened changes: prepare for the opening, move the
+ * focus, update the manager, render opened/closed.
+ * @private
+ */
+ __openedChanged: function() {
+ if (this.opened) {
+ // Make overlay visible, then add it to the manager.
+ this._prepareRenderOpened();
+ this._manager.addOverlay(this);
+ // Move the focus to the child node with [autofocus].
+ this._applyFocus();
+
+ this._renderOpened();
+ } else {
+ // Remove overlay, then restore the focus before actually closing.
+ this._manager.removeOverlay(this);
+ this._applyFocus();
+
+ this._renderClosed();
+ }
+ },
+
+ /**
+ * Executes a callback on the next animation frame, overriding any previous
+ * callback awaiting for the next animation frame. e.g.
+ * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`;
+ * `callback1` will never be invoked.
+ * @param {!Function} callback Its `this` parameter is the overlay itself.
+ * @private
+ */
+ __onNextAnimationFrame: function(callback) {
+ if (this.__raf) {
+ window.cancelAnimationFrame(this.__raf);
+ }
+ var self = this;
+ this.__raf = window.requestAnimationFrame(function nextAnimationFrame() {
+ self.__raf = null;
+ callback.call(self);
+ });
+ }
+
+ };
+
+ /**
+ 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
+ of UI controls including dialogs and drop downs. Multiple overlays may be displayed at once.
+
+ See the [demo source code](https://github.com/PolymerElements/iron-overlay-behavior/blob/master/demo/simple-overlay.html)
+ for an example.
+
+ ### Closing and canceling
+
+ An overlay may be hidden by closing or canceling. The difference between close and cancel is user
+ intent. Closing generally implies that the user acknowledged the content on the overlay. By default,
+ it will cancel whenever the user taps outside it or presses the escape key. This behavior is
+ configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click` properties.
+ `close()` should be called explicitly by the implementer when the user interacts with a control
+ in the overlay element. When the dialog is canceled, the overlay fires an 'iron-overlay-canceled'
+ event. Call `preventDefault` on this event to prevent the overlay from closing.
+
+ ### Positioning
+
+ By default the element is sized and positioned to fit and centered inside the window. You can
+ position and size it manually using CSS. See `Polymer.IronFitBehavior`.
+
+ ### Backdrop
+
+ Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is
+ appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page for styling
+ options.
+
+ In addition, `with-backdrop` will wrap the focus within the content in the light DOM.
+ Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_focusableNodes)
+ to achieve a different behavior.
+
+ ### Limitations
+
+ The element is styled to appear on top of other content by setting its `z-index` property. You
+ must ensure no element has a stacking context with a higher `z-index` than its parent stacking
+ context. You should place this element as a child of `<body>` whenever possible.
+
+ @demo demo/index.html
+ @polymerBehavior
+ */
+ Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
+
+ /**
+ * Fired after the overlay opens.
+ * @event iron-overlay-opened
+ */
+
+ /**
+ * Fired when the overlay is canceled, but before it is closed.
+ * @event iron-overlay-canceled
+ * @param {Event} event The closing of the 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 overlay).
+ */
+
+ /**
+ * Fired after the overlay closes.
+ * @event iron-overlay-closed
+ * @param {Event} event The `event.detail` is the `closingReason` property
+ * (contains `canceled`, whether the overlay was canceled).
+ */
+
+})();
+</script>