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