diff options
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.html | 637 |
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> |