Home | History | Annotate | Download | only in iron-overlay-behavior
      1 <!--
      2 @license
      3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
      4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      7 Code distributed by Google as part of the polymer project is also
      8 subject 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-a11y-keys-behavior/iron-a11y-keys-behavior.html">
     13 <link rel="import" href="iron-overlay-backdrop.html">
     14 
     15 <script>
     16 
     17   /**
     18    * @struct
     19    * @constructor
     20    * @private
     21    */
     22   Polymer.IronOverlayManagerClass = function() {
     23     /**
     24      * Used to keep track of the opened overlays.
     25      * @private {Array<Element>}
     26      */
     27     this._overlays = [];
     28 
     29     /**
     30      * iframes have a default z-index of 100,
     31      * so this default should be at least that.
     32      * @private {number}
     33      */
     34     this._minimumZ = 101;
     35 
     36     /**
     37      * Memoized backdrop element.
     38      * @private {Element|null}
     39      */
     40     this._backdropElement = null;
     41 
     42     // Enable document-wide tap recognizer.
     43     Polymer.Gestures.add(document, 'tap', null);
     44     // Need to have useCapture=true, Polymer.Gestures doesn't offer that.
     45     document.addEventListener('tap', this._onCaptureClick.bind(this), true);
     46     document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
     47     document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
     48   };
     49 
     50   Polymer.IronOverlayManagerClass.prototype = {
     51 
     52     constructor: Polymer.IronOverlayManagerClass,
     53 
     54     /**
     55      * The shared backdrop element.
     56      * @type {!Element} backdropElement
     57      */
     58     get backdropElement() {
     59       if (!this._backdropElement) {
     60         this._backdropElement = document.createElement('iron-overlay-backdrop');
     61       }
     62       return this._backdropElement;
     63     },
     64 
     65     /**
     66      * The deepest active element.
     67      * @type {!Element} activeElement the active element
     68      */
     69     get deepActiveElement() {
     70       // document.activeElement can be null
     71       // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
     72       // In case of null, default it to document.body.
     73       var active = document.activeElement || document.body;
     74       while (active.root && Polymer.dom(active.root).activeElement) {
     75         active = Polymer.dom(active.root).activeElement;
     76       }
     77       return active;
     78     },
     79 
     80     /**
     81      * Brings the overlay at the specified index to the front.
     82      * @param {number} i
     83      * @private
     84      */
     85     _bringOverlayAtIndexToFront: function(i) {
     86       var overlay = this._overlays[i];
     87       if (!overlay) {
     88         return;
     89       }
     90       var lastI = this._overlays.length - 1;
     91       var currentOverlay = this._overlays[lastI];
     92       // Ensure always-on-top overlay stays on top.
     93       if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
     94         lastI--;
     95       }
     96       // If already the top element, return.
     97       if (i >= lastI) {
     98         return;
     99       }
    100       // Update z-index to be on top.
    101       var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
    102       if (this._getZ(overlay) <= minimumZ) {
    103         this._applyOverlayZ(overlay, minimumZ);
    104       }
    105 
    106       // Shift other overlays behind the new on top.
    107       while (i < lastI) {
    108         this._overlays[i] = this._overlays[i + 1];
    109         i++;
    110       }
    111       this._overlays[lastI] = overlay;
    112     },
    113 
    114     /**
    115      * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
    116      * Also updates the backdrop z-index.
    117      * @param {!Element} overlay
    118      */
    119     addOrRemoveOverlay: function(overlay) {
    120       if (overlay.opened) {
    121         this.addOverlay(overlay);
    122       } else {
    123         this.removeOverlay(overlay);
    124       }
    125     },
    126 
    127     /**
    128      * Tracks overlays for z-index and focus management.
    129      * Ensures the last added overlay with always-on-top remains on top.
    130      * @param {!Element} overlay
    131      */
    132     addOverlay: function(overlay) {
    133       var i = this._overlays.indexOf(overlay);
    134       if (i >= 0) {
    135         this._bringOverlayAtIndexToFront(i);
    136         this.trackBackdrop();
    137         return;
    138       }
    139       var insertionIndex = this._overlays.length;
    140       var currentOverlay = this._overlays[insertionIndex - 1];
    141       var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
    142       var newZ = this._getZ(overlay);
    143 
    144       // Ensure always-on-top overlay stays on top.
    145       if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
    146         // This bumps the z-index of +2.
    147         this._applyOverlayZ(currentOverlay, minimumZ);
    148         insertionIndex--;
    149         // Update minimumZ to match previous overlay's z-index.
    150         var previousOverlay = this._overlays[insertionIndex - 1];
    151         minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
    152       }
    153 
    154       // Update z-index and insert overlay.
    155       if (newZ <= minimumZ) {
    156         this._applyOverlayZ(overlay, minimumZ);
    157       }
    158       this._overlays.splice(insertionIndex, 0, overlay);
    159 
    160       // Get focused node.
    161       var element = this.deepActiveElement;
    162       overlay.restoreFocusNode = this._overlayParent(element) ? null : element;
    163       this.trackBackdrop();
    164     },
    165 
    166     /**
    167      * @param {!Element} overlay
    168      */
    169     removeOverlay: function(overlay) {
    170       var i = this._overlays.indexOf(overlay);
    171       if (i === -1) {
    172         return;
    173       }
    174       this._overlays.splice(i, 1);
    175 
    176       var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null;
    177       overlay.restoreFocusNode = null;
    178       // Focus back only if still contained in document.body
    179       if (node && Polymer.dom(document.body).deepContains(node)) {
    180         node.focus();
    181       }
    182       this.trackBackdrop();
    183     },
    184 
    185     /**
    186      * Returns the current overlay.
    187      * @return {Element|undefined}
    188      */
    189     currentOverlay: function() {
    190       var i = this._overlays.length - 1;
    191       return this._overlays[i];
    192     },
    193 
    194     /**
    195      * Returns the current overlay z-index.
    196      * @return {number}
    197      */
    198     currentOverlayZ: function() {
    199       return this._getZ(this.currentOverlay());
    200     },
    201 
    202     /**
    203      * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
    204      * This does not effect the z-index of any existing overlays.
    205      * @param {number} minimumZ
    206      */
    207     ensureMinimumZ: function(minimumZ) {
    208       this._minimumZ = Math.max(this._minimumZ, minimumZ);
    209     },
    210 
    211     focusOverlay: function() {
    212       var current = /** @type {?} */ (this.currentOverlay());
    213       // We have to be careful to focus the next overlay _after_ any current
    214       // transitions are complete (due to the state being toggled prior to the
    215       // transition). Otherwise, we risk infinite recursion when a transitioning
    216       // (closed) overlay becomes the current overlay.
    217       //
    218       // NOTE: We make the assumption that any overlay that completes a transition
    219       // will call into focusOverlay to kick the process back off. Currently:
    220       // transitionend -> _applyFocus -> focusOverlay.
    221       if (current && !current.transitioning) {
    222         current._applyFocus();
    223       }
    224     },
    225 
    226     /**
    227      * Updates the backdrop z-index.
    228      */
    229     trackBackdrop: function() {
    230       var overlay = this._overlayWithBackdrop();
    231       // Avoid creating the backdrop if there is no overlay with backdrop.
    232       if (!overlay && !this._backdropElement) {
    233         return;
    234       }
    235       this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
    236       this.backdropElement.opened = !!overlay;
    237     },
    238 
    239     /**
    240      * @return {Array<Element>}
    241      */
    242     getBackdrops: function() {
    243       var backdrops = [];
    244       for (var i = 0; i < this._overlays.length; i++) {
    245         if (this._overlays[i].withBackdrop) {
    246           backdrops.push(this._overlays[i]);
    247         }
    248       }
    249       return backdrops;
    250     },
    251 
    252     /**
    253      * Returns the z-index for the backdrop.
    254      * @return {number}
    255      */
    256     backdropZ: function() {
    257       return this._getZ(this._overlayWithBackdrop()) - 1;
    258     },
    259 
    260     /**
    261      * Returns the first opened overlay that has a backdrop.
    262      * @return {Element|undefined}
    263      * @private
    264      */
    265     _overlayWithBackdrop: function() {
    266       for (var i = 0; i < this._overlays.length; i++) {
    267         if (this._overlays[i].withBackdrop) {
    268           return this._overlays[i];
    269         }
    270       }
    271     },
    272 
    273     /**
    274      * Calculates the minimum z-index for the overlay.
    275      * @param {Element=} overlay
    276      * @private
    277      */
    278     _getZ: function(overlay) {
    279       var z = this._minimumZ;
    280       if (overlay) {
    281         var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
    282         // Check if is a number
    283         // Number.isNaN not supported in IE 10+
    284         if (z1 === z1) {
    285           z = z1;
    286         }
    287       }
    288       return z;
    289     },
    290 
    291     /**
    292      * @param {!Element} element
    293      * @param {number|string} z
    294      * @private
    295      */
    296     _setZ: function(element, z) {
    297       element.style.zIndex = z;
    298     },
    299 
    300     /**
    301      * @param {!Element} overlay
    302      * @param {number} aboveZ
    303      * @private
    304      */
    305     _applyOverlayZ: function(overlay, aboveZ) {
    306       this._setZ(overlay, aboveZ + 2);
    307     },
    308 
    309     /**
    310      * Returns the overlay containing the provided node. If the node is an overlay,
    311      * it returns the node.
    312      * @param {Element=} node
    313      * @return {Element|undefined}
    314      * @private
    315      */
    316     _overlayParent: function(node) {
    317       while (node && node !== document.body) {
    318         // Check if it is an overlay.
    319         if (node._manager === this) {
    320           return node;
    321         }
    322         // Use logical parentNode, or native ShadowRoot host.
    323         node = Polymer.dom(node).parentNode || node.host;
    324       }
    325     },
    326 
    327     /**
    328      * Returns the deepest overlay in the path.
    329      * @param {Array<Element>=} path
    330      * @return {Element|undefined}
    331      * @suppress {missingProperties}
    332      * @private
    333      */
    334     _overlayInPath: function(path) {
    335       path = path || [];
    336       for (var i = 0; i < path.length; i++) {
    337         if (path[i]._manager === this) {
    338           return path[i];
    339         }
    340       }
    341     },
    342 
    343     /**
    344      * Ensures the click event is delegated to the right overlay.
    345      * @param {!Event} event
    346      * @private
    347      */
    348     _onCaptureClick: function(event) {
    349       var overlay = /** @type {?} */ (this.currentOverlay());
    350       // Check if clicked outside of top overlay.
    351       if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
    352         overlay._onCaptureClick(event);
    353       }
    354     },
    355 
    356     /**
    357      * Ensures the focus event is delegated to the right overlay.
    358      * @param {!Event} event
    359      * @private
    360      */
    361     _onCaptureFocus: function(event) {
    362       var overlay = /** @type {?} */ (this.currentOverlay());
    363       if (overlay) {
    364         overlay._onCaptureFocus(event);
    365       }
    366     },
    367 
    368     /**
    369      * Ensures TAB and ESC keyboard events are delegated to the right overlay.
    370      * @param {!Event} event
    371      * @private
    372      */
    373     _onCaptureKeyDown: function(event) {
    374       var overlay = /** @type {?} */ (this.currentOverlay());
    375       if (overlay) {
    376         if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
    377           overlay._onCaptureEsc(event);
    378         } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
    379           overlay._onCaptureTab(event);
    380         }
    381       }
    382     },
    383 
    384     /**
    385      * Returns if the overlay1 should be behind overlay2.
    386      * @param {!Element} overlay1
    387      * @param {!Element} overlay2
    388      * @return {boolean}
    389      * @suppress {missingProperties}
    390      * @private
    391      */
    392     _shouldBeBehindOverlay: function(overlay1, overlay2) {
    393       return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
    394     }
    395   };
    396 
    397   Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
    398 </script>
    399