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