Home | History | Annotate | Download | only in paper-tooltip
      1 <!--
      2 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
      3 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      4 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      5 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      6 Code distributed by Google as part of the polymer project is also
      7 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
      8 -->
      9 
     10 
     11 <link rel="import" href="../polymer/polymer.html">
     12 <link rel="import" href="../neon-animation/neon-animation-runner-behavior.html">
     13 <link rel="import" href="../neon-animation/animations/fade-in-animation.html">
     14 <link rel="import" href="../neon-animation/animations/fade-out-animation.html">
     15 
     16 <!--
     17 Material design: [Tooltips](https://www.google.com/design/spec/components/tooltips.html)
     18 
     19 `<paper-tooltip>` is a label that appears on hover and focus when the user
     20 hovers over an element with the cursor or with the keyboard. It will be centered
     21 to an anchor element specified in the `for` attribute, or, if that doesn't exist,
     22 centered to the parent node containing it.
     23 
     24 Example:
     25 
     26     <div style="display:inline-block">
     27       <button>Click me!</button>
     28       <paper-tooltip>Tooltip text</paper-tooltip>
     29     </div>
     30 
     31     <div>
     32       <button id="btn">Click me!</button>
     33       <paper-tooltip for="btn">Tooltip text</paper-tooltip>
     34     </div>
     35 
     36 The tooltip can be positioned on the top|bottom|left|right of the anchor using
     37 the `position` attribute. The default position is bottom.
     38 
     39     <paper-tooltip for="btn" position="left">Tooltip text</paper-tooltip>
     40     <paper-tooltip for="btn" position="top">Tooltip text</paper-tooltip>
     41 
     42 ### Styling
     43 
     44 The following custom properties and mixins are available for styling:
     45 
     46 Custom property | Description | Default
     47 ----------------|-------------|----------
     48 `--paper-tooltip-background` | The background color of the tooltip | `#616161`
     49 `--paper-tooltip-opacity` | The opacity of the tooltip | `0.9`
     50 `--paper-tooltip-text-color` | The text color of the tooltip | `white`
     51 `--paper-tooltip` | Mixin applied to the tooltip | `{}`
     52 
     53 @group Paper Elements
     54 @element paper-tooltip
     55 @demo demo/index.html
     56 -->
     57 
     58 <dom-module id="paper-tooltip">
     59   <template>
     60     <style>
     61       :host {
     62         display: block;
     63         position: absolute;
     64         outline: none;
     65         z-index: 1002;
     66         -moz-user-select: none;
     67         -ms-user-select: none;
     68         -webkit-user-select: none;
     69         user-select: none;
     70         cursor: default;
     71       }
     72 
     73       #tooltip {
     74         display: block;
     75         outline: none;
     76         @apply(--paper-font-common-base);
     77         font-size: 10px;
     78         line-height: 1;
     79 
     80         background-color: var(--paper-tooltip-background, #616161);
     81         opacity: var(--paper-tooltip-opacity, 0.9);
     82         color: var(--paper-tooltip-text-color, white);
     83 
     84         padding: 8px;
     85         border-radius: 2px;
     86 
     87         @apply(--paper-tooltip);
     88       }
     89 
     90       /* Thanks IE 10. */
     91       .hidden {
     92         display: none !important;
     93       }
     94     </style>
     95 
     96     <div id="tooltip" class="hidden">
     97       <content></content>
     98     </div>
     99   </template>
    100 
    101   <script>
    102     Polymer({
    103       is: 'paper-tooltip',
    104 
    105       hostAttributes: {
    106         role: 'tooltip',
    107         tabindex: -1
    108       },
    109 
    110       behaviors: [
    111         Polymer.NeonAnimationRunnerBehavior
    112       ],
    113 
    114       properties: {
    115         /**
    116          * The id of the element that the tooltip is anchored to. This element
    117          * must be a sibling of the tooltip.
    118          */
    119         for: {
    120           type: String,
    121           observer: '_forChanged'
    122         },
    123 
    124         /**
    125          * Set this to true if you want to manually control when the tooltip
    126          * is shown or hidden.
    127          */
    128         manualMode: {
    129           type: Boolean,
    130           value: false
    131         },
    132 
    133         /**
    134          * Positions the tooltip to the top, right, bottom, left of its content.
    135          */
    136         position: {
    137           type: String,
    138           value: 'bottom'
    139         },
    140 
    141         /**
    142          * If true, no parts of the tooltip will ever be shown offscreen.
    143          */
    144         fitToVisibleBounds: {
    145           type: Boolean,
    146           value: false
    147         },
    148 
    149         /**
    150          * The spacing between the top of the tooltip and the element it is
    151          * anchored to.
    152          */
    153         offset: {
    154           type: Number,
    155           value: 14
    156         },
    157 
    158         /**
    159          * This property is deprecated, but left over so that it doesn't
    160          * break exiting code. Please use `offset` instead. If both `offset` and
    161          * `marginTop` are provided, `marginTop` will be ignored.
    162          * @deprecated since version 1.0.3
    163          */
    164         marginTop: {
    165           type: Number,
    166           value: 14
    167         },
    168 
    169         /**
    170          * The delay that will be applied before the `entry` animation is
    171          * played when showing the tooltip.
    172          */
    173         animationDelay: {
    174           type: Number,
    175           value: 500
    176         },
    177 
    178         /**
    179          * The entry and exit animations that will be played when showing and
    180          * hiding the tooltip. If you want to override this, you must ensure
    181          * that your animationConfig has the exact format below.
    182          */
    183         animationConfig: {
    184           type: Object,
    185           value: function() {
    186             return {
    187               'entry': [{
    188                 name: 'fade-in-animation',
    189                 node: this,
    190                 timing: {delay: 0}
    191               }],
    192               'exit': [{
    193                 name: 'fade-out-animation',
    194                 node: this
    195               }]
    196             }
    197           }
    198         },
    199 
    200         _showing: {
    201           type: Boolean,
    202           value: false
    203         }
    204       },
    205 
    206       listeners: {
    207         'neon-animation-finish': '_onAnimationFinish',
    208         'mouseenter': 'hide'
    209       },
    210 
    211       /**
    212        * Returns the target element that this tooltip is anchored to. It is
    213        * either the element given by the `for` attribute, or the immediate parent
    214        * of the tooltip.
    215        */
    216       get target () {
    217         var parentNode = Polymer.dom(this).parentNode;
    218         // If the parentNode is a document fragment, then we need to use the host.
    219         var ownerRoot = Polymer.dom(this).getOwnerRoot();
    220 
    221         var target;
    222         if (this.for) {
    223           target = Polymer.dom(ownerRoot).querySelector('#' + this.for);
    224         } else {
    225           target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
    226               ownerRoot.host : parentNode;
    227         }
    228 
    229         return target;
    230       },
    231 
    232       attached: function() {
    233         this._target = this.target;
    234 
    235         if (this.manualMode)
    236           return;
    237 
    238         this.listen(this._target, 'mouseenter', 'show');
    239         this.listen(this._target, 'focus', 'show');
    240         this.listen(this._target, 'mouseleave', 'hide');
    241         this.listen(this._target, 'blur', 'hide');
    242         this.listen(this._target, 'tap', 'hide');
    243       },
    244 
    245       detached: function() {
    246         if (this._target && !this.manualMode) {
    247           this.unlisten(this._target, 'mouseenter', 'show');
    248           this.unlisten(this._target, 'focus', 'show');
    249           this.unlisten(this._target, 'mouseleave', 'hide');
    250           this.unlisten(this._target, 'blur', 'hide');
    251           this.unlisten(this._target, 'tap', 'hide');
    252         }
    253       },
    254 
    255       show: function() {
    256         // If the tooltip is already showing, there's nothing to do.
    257         if (this._showing)
    258           return;
    259 
    260         if (Polymer.dom(this).textContent.trim() === '')
    261           return;
    262 
    263 
    264         this.cancelAnimation();
    265         this._showing = true;
    266         this.toggleClass('hidden', false, this.$.tooltip);
    267         this.updatePosition();
    268 
    269         this.animationConfig.entry[0].timing.delay = this.animationDelay;
    270         this._animationPlaying = true;
    271         this.playAnimation('entry');
    272       },
    273 
    274       hide: function() {
    275         // If the tooltip is already hidden, there's nothing to do.
    276         if (!this._showing) {
    277           return;
    278         }
    279 
    280         // If the entry animation is still playing, don't try to play the exit
    281         // animation since this will reset the opacity to 1. Just end the animation.
    282         if (this._animationPlaying) {
    283           this.cancelAnimation();
    284           this._showing = false;
    285           this._onAnimationFinish();
    286           return;
    287         }
    288 
    289         this._showing = false;
    290         this._animationPlaying = true;
    291         this.playAnimation('exit');
    292       },
    293 
    294       _forChanged: function() {
    295         this._target = this.target;
    296       },
    297 
    298       updatePosition: function() {
    299         if (!this._target || !this.offsetParent)
    300           return;
    301 
    302         var offset = this.offset;
    303         // If a marginTop has been provided by the user (pre 1.0.3), use it.
    304         if (this.marginTop != 14 && this.offset == 14)
    305           offset = this.marginTop;
    306 
    307         var parentRect = this.offsetParent.getBoundingClientRect();
    308         var targetRect = this._target.getBoundingClientRect();
    309         var thisRect = this.getBoundingClientRect();
    310 
    311         var horizontalCenterOffset = (targetRect.width - thisRect.width) / 2;
    312         var verticalCenterOffset = (targetRect.height - thisRect.height) / 2;
    313 
    314         var targetLeft = targetRect.left - parentRect.left;
    315         var targetTop = targetRect.top - parentRect.top;
    316 
    317         var tooltipLeft, tooltipTop;
    318 
    319         switch (this.position) {
    320           case 'top':
    321             tooltipLeft = targetLeft + horizontalCenterOffset;
    322             tooltipTop = targetTop - thisRect.height - offset;
    323             break;
    324           case 'bottom':
    325             tooltipLeft = targetLeft + horizontalCenterOffset;
    326             tooltipTop = targetTop + targetRect.height + offset;
    327             break;
    328           case 'left':
    329             tooltipLeft = targetLeft - thisRect.width - offset;
    330             tooltipTop = targetTop + verticalCenterOffset;
    331             break;
    332           case 'right':
    333             tooltipLeft = targetLeft + targetRect.width + offset;
    334             tooltipTop = targetTop + verticalCenterOffset;
    335             break;
    336         }
    337 
    338         // TODO(noms): This should use IronFitBehavior if possible.
    339         if (this.fitToVisibleBounds) {
    340           // Clip the left/right side.
    341           if (tooltipLeft + thisRect.width > window.innerWidth) {
    342             this.style.right = '0px';
    343             this.style.left = 'auto';
    344           } else {
    345             this.style.left = Math.max(0, tooltipLeft) + 'px';
    346             this.style.right = 'auto';
    347           }
    348 
    349           // Clip the top/bottom side.
    350           if (tooltipTop + thisRect.height > window.innerHeight) {
    351             this.style.bottom = '0px';
    352             this.style.top = 'auto';
    353           } else {
    354             this.style.top = Math.max(0, tooltipTop) + 'px';
    355             this.style.bottom = 'auto';
    356           }
    357         } else {
    358           this.style.left = tooltipLeft + 'px';
    359           this.style.top = tooltipTop + 'px';
    360         }
    361 
    362       },
    363 
    364       _onAnimationFinish: function() {
    365         this._animationPlaying = false;
    366         if (!this._showing) {
    367           this.toggleClass('hidden', true, this.$.tooltip);
    368         }
    369       },
    370     });
    371   </script>
    372 </dom-module>
    373