• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
3This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
4The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
5The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
6Code distributed by Google as part of the polymer project is also
7subject 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<!--
17Material 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
20hovers over an element with the cursor or with the keyboard. It will be centered
21to an anchor element specified in the `for` attribute, or, if that doesn't exist,
22centered to the parent node containing it.
23
24Example:
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
36The tooltip can be positioned on the top|bottom|left|right of the anchor using
37the `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
44The following custom properties and mixins are available for styling:
45
46Custom 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