• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2@license
3Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
9-->
10
11<link rel="import" href="../polymer/polymer.html">
12<link rel="import" href="../iron-fit-behavior/iron-fit-behavior.html">
13<link rel="import" href="../iron-resizable-behavior/iron-resizable-behavior.html">
14<link rel="import" href="iron-overlay-manager.html">
15<link rel="import" href="iron-focusables-helper.html">
16
17<script>
18(function() {
19  'use strict';
20
21  /** @polymerBehavior */
22  Polymer.IronOverlayBehaviorImpl = {
23
24    properties: {
25
26      /**
27       * True if the overlay is currently displayed.
28       */
29      opened: {
30        observer: '_openedChanged',
31        type: Boolean,
32        value: false,
33        notify: true
34      },
35
36      /**
37       * True if the overlay was canceled when it was last closed.
38       */
39      canceled: {
40        observer: '_canceledChanged',
41        readOnly: true,
42        type: Boolean,
43        value: false
44      },
45
46      /**
47       * Set to true to display a backdrop behind the overlay. It traps the focus
48       * within the light DOM of the overlay.
49       */
50      withBackdrop: {
51        observer: '_withBackdropChanged',
52        type: Boolean
53      },
54
55      /**
56       * Set to true to disable auto-focusing the overlay or child nodes with
57       * the `autofocus` attribute` when the overlay is opened.
58       */
59      noAutoFocus: {
60        type: Boolean,
61        value: false
62      },
63
64      /**
65       * Set to true to disable canceling the overlay with the ESC key.
66       */
67      noCancelOnEscKey: {
68        type: Boolean,
69        value: false
70      },
71
72      /**
73       * Set to true to disable canceling the overlay by clicking outside it.
74       */
75      noCancelOnOutsideClick: {
76        type: Boolean,
77        value: false
78      },
79
80      /**
81       * Contains the reason(s) this overlay was last closed (see `iron-overlay-closed`).
82       * `IronOverlayBehavior` provides the `canceled` reason; implementers of the
83       * behavior can provide other reasons in addition to `canceled`.
84       */
85      closingReason: {
86        // was a getter before, but needs to be a property so other
87        // behaviors can override this.
88        type: Object
89      },
90
91      /**
92       * Set to true to enable restoring of focus when overlay is closed.
93       */
94      restoreFocusOnClose: {
95        type: Boolean,
96        value: false
97      },
98
99      /**
100       * Set to true to keep overlay always on top.
101       */
102      alwaysOnTop: {
103        type: Boolean
104      },
105
106      /**
107       * Shortcut to access to the overlay manager.
108       * @private
109       * @type {Polymer.IronOverlayManagerClass}
110       */
111      _manager: {
112        type: Object,
113        value: Polymer.IronOverlayManager
114      },
115
116      /**
117       * The node being focused.
118       * @type {?Node}
119       */
120      _focusedChild: {
121        type: Object
122      }
123
124    },
125
126    listeners: {
127      'iron-resize': '_onIronResize'
128    },
129
130    /**
131     * The backdrop element.
132     * @type {Element}
133     */
134    get backdropElement() {
135      return this._manager.backdropElement;
136    },
137
138    /**
139     * Returns the node to give focus to.
140     * @type {Node}
141     */
142    get _focusNode() {
143      return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]') || this;
144    },
145
146    /**
147     * Array of nodes that can receive focus (overlay included), ordered by `tabindex`.
148     * This is used to retrieve which is the first and last focusable nodes in order
149     * to wrap the focus for overlays `with-backdrop`.
150     *
151     * If you know what is your content (specifically the first and last focusable children),
152     * you can override this method to return only `[firstFocusable, lastFocusable];`
153     * @type {Array<Node>}
154     * @protected
155     */
156    get _focusableNodes() {
157      return Polymer.IronFocusablesHelper.getTabbableNodes(this);
158    },
159
160    ready: function() {
161      // Used to skip calls to notifyResize and refit while the overlay is animating.
162      this.__isAnimating = false;
163      // with-backdrop needs tabindex to be set in order to trap the focus.
164      // If it is not set, IronOverlayBehavior will set it, and remove it if with-backdrop = false.
165      this.__shouldRemoveTabIndex = false;
166      // Used for wrapping the focus on TAB / Shift+TAB.
167      this.__firstFocusableNode = this.__lastFocusableNode = null;
168      // Used by __onNextAnimationFrame to cancel any previous callback.
169      this.__raf = null;
170      // Focused node before overlay gets opened. Can be restored on close.
171      this.__restoreFocusNode = null;
172      this._ensureSetup();
173    },
174
175    attached: function() {
176      // Call _openedChanged here so that position can be computed correctly.
177      if (this.opened) {
178        this._openedChanged(this.opened);
179      }
180      this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
181    },
182
183    detached: function() {
184      Polymer.dom(this).unobserveNodes(this._observer);
185      this._observer = null;
186      if (this.__raf) {
187        window.cancelAnimationFrame(this.__raf);
188        this.__raf = null;
189      }
190      this._manager.removeOverlay(this);
191    },
192
193    /**
194     * Toggle the opened state of the overlay.
195     */
196    toggle: function() {
197      this._setCanceled(false);
198      this.opened = !this.opened;
199    },
200
201    /**
202     * Open the overlay.
203     */
204    open: function() {
205      this._setCanceled(false);
206      this.opened = true;
207    },
208
209    /**
210     * Close the overlay.
211     */
212    close: function() {
213      this._setCanceled(false);
214      this.opened = false;
215    },
216
217    /**
218     * Cancels the overlay.
219     * @param {Event=} event The original event
220     */
221    cancel: function(event) {
222      var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
223      if (cancelEvent.defaultPrevented) {
224        return;
225      }
226
227      this._setCanceled(true);
228      this.opened = false;
229    },
230
231    /**
232     * Invalidates the cached tabbable nodes. To be called when any of the focusable
233     * content changes (e.g. a button is disabled).
234     */
235    invalidateTabbables: function() {
236      this.__firstFocusableNode = this.__lastFocusableNode = null;
237    },
238
239    _ensureSetup: function() {
240      if (this._overlaySetup) {
241        return;
242      }
243      this._overlaySetup = true;
244      this.style.outline = 'none';
245      this.style.display = 'none';
246    },
247
248    /**
249     * Called when `opened` changes.
250     * @param {boolean=} opened
251     * @protected
252     */
253    _openedChanged: function(opened) {
254      if (opened) {
255        this.removeAttribute('aria-hidden');
256      } else {
257        this.setAttribute('aria-hidden', 'true');
258      }
259
260      // Defer any animation-related code on attached
261      // (_openedChanged gets called again on attached).
262      if (!this.isAttached) {
263        return;
264      }
265
266      this.__isAnimating = true;
267
268      // Use requestAnimationFrame for non-blocking rendering.
269      this.__onNextAnimationFrame(this.__openedChanged);
270    },
271
272    _canceledChanged: function() {
273      this.closingReason = this.closingReason || {};
274      this.closingReason.canceled = this.canceled;
275    },
276
277    _withBackdropChanged: function() {
278      // If tabindex is already set, no need to override it.
279      if (this.withBackdrop && !this.hasAttribute('tabindex')) {
280        this.setAttribute('tabindex', '-1');
281        this.__shouldRemoveTabIndex = true;
282      } else if (this.__shouldRemoveTabIndex) {
283        this.removeAttribute('tabindex');
284        this.__shouldRemoveTabIndex = false;
285      }
286      if (this.opened && this.isAttached) {
287        this._manager.trackBackdrop();
288      }
289    },
290
291    /**
292     * tasks which must occur before opening; e.g. making the element visible.
293     * @protected
294     */
295    _prepareRenderOpened: function() {
296      // Store focused node.
297      this.__restoreFocusNode = this._manager.deepActiveElement;
298
299      // Needed to calculate the size of the overlay so that transitions on its size
300      // will have the correct starting points.
301      this._preparePositioning();
302      this.refit();
303      this._finishPositioning();
304
305      // Safari will apply the focus to the autofocus element when displayed
306      // for the first time, so we make sure to return the focus where it was.
307      if (this.noAutoFocus && document.activeElement === this._focusNode) {
308        this._focusNode.blur();
309        this.__restoreFocusNode.focus();
310      }
311    },
312
313    /**
314     * Tasks which cause the overlay to actually open; typically play an animation.
315     * @protected
316     */
317    _renderOpened: function() {
318      this._finishRenderOpened();
319    },
320
321    /**
322     * Tasks which cause the overlay to actually close; typically play an animation.
323     * @protected
324     */
325    _renderClosed: function() {
326      this._finishRenderClosed();
327    },
328
329    /**
330     * Tasks to be performed at the end of open action. Will fire `iron-overlay-opened`.
331     * @protected
332     */
333    _finishRenderOpened: function() {
334      this.notifyResize();
335      this.__isAnimating = false;
336
337      this.fire('iron-overlay-opened');
338    },
339
340    /**
341     * Tasks to be performed at the end of close action. Will fire `iron-overlay-closed`.
342     * @protected
343     */
344    _finishRenderClosed: function() {
345      // Hide the overlay.
346      this.style.display = 'none';
347      // Reset z-index only at the end of the animation.
348      this.style.zIndex = '';
349      this.notifyResize();
350      this.__isAnimating = false;
351      this.fire('iron-overlay-closed', this.closingReason);
352    },
353
354    _preparePositioning: function() {
355      this.style.transition = this.style.webkitTransition = 'none';
356      this.style.transform = this.style.webkitTransform = 'none';
357      this.style.display = '';
358    },
359
360    _finishPositioning: function() {
361      // First, make it invisible & reactivate animations.
362      this.style.display = 'none';
363      // Force reflow before re-enabling animations so that they don't start.
364      // Set scrollTop to itself so that Closure Compiler doesn't remove this.
365      this.scrollTop = this.scrollTop;
366      this.style.transition = this.style.webkitTransition = '';
367      this.style.transform = this.style.webkitTransform = '';
368      // Now that animations are enabled, make it visible again
369      this.style.display = '';
370      // Force reflow, so that following animations are properly started.
371      // Set scrollTop to itself so that Closure Compiler doesn't remove this.
372      this.scrollTop = this.scrollTop;
373    },
374
375    /**
376     * Applies focus according to the opened state.
377     * @protected
378     */
379    _applyFocus: function() {
380      if (this.opened) {
381        if (!this.noAutoFocus) {
382          this._focusNode.focus();
383        }
384      }
385      else {
386        this._focusNode.blur();
387        this._focusedChild = null;
388        // Restore focus.
389        if (this.restoreFocusOnClose && this.__restoreFocusNode) {
390          this.__restoreFocusNode.focus();
391        }
392        this.__restoreFocusNode = null;
393        // If many overlays get closed at the same time, one of them would still
394        // be the currentOverlay even if already closed, and would call _applyFocus
395        // infinitely, so we check for this not to be the current overlay.
396        var currentOverlay = this._manager.currentOverlay();
397        if (currentOverlay && this !== currentOverlay) {
398          currentOverlay._applyFocus();
399        }
400      }
401    },
402
403    /**
404     * Cancels (closes) the overlay. Call when click happens outside the overlay.
405     * @param {!Event} event
406     * @protected
407     */
408    _onCaptureClick: function(event) {
409      if (!this.noCancelOnOutsideClick) {
410        this.cancel(event);
411      }
412    },
413
414    /**
415     * Keeps track of the focused child. If withBackdrop, traps focus within overlay.
416     * @param {!Event} event
417     * @protected
418     */
419    _onCaptureFocus: function (event) {
420      if (!this.withBackdrop) {
421        return;
422      }
423      var path = Polymer.dom(event).path;
424      if (path.indexOf(this) === -1) {
425        event.stopPropagation();
426        this._applyFocus();
427      } else {
428        this._focusedChild = path[0];
429      }
430    },
431
432    /**
433     * Handles the ESC key event and cancels (closes) the overlay.
434     * @param {!Event} event
435     * @protected
436     */
437    _onCaptureEsc: function(event) {
438      if (!this.noCancelOnEscKey) {
439        this.cancel(event);
440      }
441    },
442
443    /**
444     * Handles TAB key events to track focus changes.
445     * Will wrap focus for overlays withBackdrop.
446     * @param {!Event} event
447     * @protected
448     */
449    _onCaptureTab: function(event) {
450      if (!this.withBackdrop) {
451        return;
452      }
453      this.__ensureFirstLastFocusables();
454      // TAB wraps from last to first focusable.
455      // Shift + TAB wraps from first to last focusable.
456      var shift = event.shiftKey;
457      var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusableNode;
458      var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNode;
459      var shouldWrap = false;
460      if (nodeToCheck === nodeToSet) {
461        // If nodeToCheck is the same as nodeToSet, it means we have an overlay
462        // with 0 or 1 focusables; in either case we still need to trap the
463        // focus within the overlay.
464        shouldWrap = true;
465      } else {
466        // In dom=shadow, the manager will receive focus changes on the main
467        // root but not the ones within other shadow roots, so we can't rely on
468        // _focusedChild, but we should check the deepest active element.
469        var focusedNode = this._manager.deepActiveElement;
470        // If the active element is not the nodeToCheck but the overlay itself,
471        // it means the focus is about to go outside the overlay, hence we
472        // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
473        shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
474      }
475
476      if (shouldWrap) {
477        // When the overlay contains the last focusable element of the document
478        // and it's already focused, pressing TAB would move the focus outside
479        // the document (e.g. to the browser search bar). Similarly, when the
480        // overlay contains the first focusable element of the document and it's
481        // already focused, pressing Shift+TAB would move the focus outside the
482        // document (e.g. to the browser search bar).
483        // In both cases, we would not receive a focus event, but only a blur.
484        // In order to achieve focus wrapping, we prevent this TAB event and
485        // force the focus. This will also prevent the focus to temporarily move
486        // outside the overlay, which might cause scrolling.
487        event.preventDefault();
488        this._focusedChild = nodeToSet;
489        this._applyFocus();
490      }
491    },
492
493    /**
494     * Refits if the overlay is opened and not animating.
495     * @protected
496     */
497    _onIronResize: function() {
498      if (this.opened && !this.__isAnimating) {
499        this.__onNextAnimationFrame(this.refit);
500      }
501    },
502
503    /**
504     * Will call notifyResize if overlay is opened.
505     * Can be overridden in order to avoid multiple observers on the same node.
506     * @protected
507     */
508    _onNodesChange: function() {
509      if (this.opened && !this.__isAnimating) {
510        // It might have added focusable nodes, so invalidate cached values.
511        this.invalidateTabbables();
512        this.notifyResize();
513      }
514    },
515
516    /**
517     * Will set first and last focusable nodes if any of them is not set.
518     * @private
519     */
520    __ensureFirstLastFocusables: function() {
521      if (!this.__firstFocusableNode || !this.__lastFocusableNode) {
522        var focusableNodes = this._focusableNodes;
523        this.__firstFocusableNode = focusableNodes[0];
524        this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
525      }
526    },
527
528    /**
529     * Tasks executed when opened changes: prepare for the opening, move the
530     * focus, update the manager, render opened/closed.
531     * @private
532     */
533    __openedChanged: function() {
534      if (this.opened) {
535        // Make overlay visible, then add it to the manager.
536        this._prepareRenderOpened();
537        this._manager.addOverlay(this);
538        // Move the focus to the child node with [autofocus].
539        this._applyFocus();
540
541        this._renderOpened();
542      } else {
543        // Remove overlay, then restore the focus before actually closing.
544        this._manager.removeOverlay(this);
545        this._applyFocus();
546
547        this._renderClosed();
548      }
549    },
550
551    /**
552     * Executes a callback on the next animation frame, overriding any previous
553     * callback awaiting for the next animation frame. e.g.
554     * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`;
555     * `callback1` will never be invoked.
556     * @param {!Function} callback Its `this` parameter is the overlay itself.
557     * @private
558     */
559    __onNextAnimationFrame: function(callback) {
560      if (this.__raf) {
561        window.cancelAnimationFrame(this.__raf);
562      }
563      var self = this;
564      this.__raf = window.requestAnimationFrame(function nextAnimationFrame() {
565        self.__raf = null;
566        callback.call(self);
567      });
568    }
569
570  };
571
572  /**
573  Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
574  on top of other content. It includes an optional backdrop, and can be used to implement a variety
575  of UI controls including dialogs and drop downs. Multiple overlays may be displayed at once.
576
577  See the [demo source code](https://github.com/PolymerElements/iron-overlay-behavior/blob/master/demo/simple-overlay.html)
578  for an example.
579
580  ### Closing and canceling
581
582  An overlay may be hidden by closing or canceling. The difference between close and cancel is user
583  intent. Closing generally implies that the user acknowledged the content on the overlay. By default,
584  it will cancel whenever the user taps outside it or presses the escape key. This behavior is
585  configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click` properties.
586  `close()` should be called explicitly by the implementer when the user interacts with a control
587  in the overlay element. When the dialog is canceled, the overlay fires an 'iron-overlay-canceled'
588  event. Call `preventDefault` on this event to prevent the overlay from closing.
589
590  ### Positioning
591
592  By default the element is sized and positioned to fit and centered inside the window. You can
593  position and size it manually using CSS. See `Polymer.IronFitBehavior`.
594
595  ### Backdrop
596
597  Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is
598  appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page for styling
599  options.
600
601  In addition, `with-backdrop` will wrap the focus within the content in the light DOM.
602  Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_focusableNodes)
603  to achieve a different behavior.
604
605  ### Limitations
606
607  The element is styled to appear on top of other content by setting its `z-index` property. You
608  must ensure no element has a stacking context with a higher `z-index` than its parent stacking
609  context. You should place this element as a child of `<body>` whenever possible.
610
611  @demo demo/index.html
612  @polymerBehavior
613  */
614  Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
615
616  /**
617   * Fired after the overlay opens.
618   * @event iron-overlay-opened
619   */
620
621  /**
622   * Fired when the overlay is canceled, but before it is closed.
623   * @event iron-overlay-canceled
624   * @param {Event} event The closing of the overlay can be prevented
625   * by calling `event.preventDefault()`. The `event.detail` is the original event that
626   * originated the canceling (e.g. ESC keyboard event or click event outside the overlay).
627   */
628
629  /**
630   * Fired after the overlay closes.
631   * @event iron-overlay-closed
632   * @param {Event} event The `event.detail` is the `closingReason` property
633   * (contains `canceled`, whether the overlay was canceled).
634   */
635
636})();
637</script>
638