` and is of type ``. 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 `

51ºÚÁÏ

` whenever possible. @demo demo/index.html @polymerBehavior Polymer.IronOverlayBehavior */ 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} * @protected */ get _focusableNodes() { // Elements that can be focused even if they have [disabled] attribute. var FOCUSABLE_WITH_DISABLED = [ 'a[href]', 'area[href]', 'iframe', '[tabindex]', '[contentEditable=true]' ]; // Elements that cannot be focused if they have [disabled] attribute. var FOCUSABLE_WITHOUT_DISABLED = [ 'input', 'select', 'textarea', 'button' ]; // Discard elements with tabindex=-1 (makes them not focusable). var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + ':not([tabindex="-1"]),' + FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),') + ':not([disabled]):not([tabindex="-1"])'; var focusables = Polymer.dom(this).querySelectorAll(selector); if (this.tabIndex >= 0) { // Insert at the beginning because we might have all elements with tabIndex = 0, // and the overlay should be the first of the list. focusables.splice(0, 0, this); } // Sort by tabindex. return focusables.sort(function (a, b) { if (a.tabIndex === b.tabIndex) { return 0; } if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { return 1; } return -1; }); }, ready: function() { // 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); }); } }; /** @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). */ })();