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<link rel="import" href="../polymer/polymer.html"> 12<link rel="import" href="../iron-flex-layout/iron-flex-layout.html"> 13<link rel="import" href="../iron-icon/iron-icon.html"> 14<link rel="import" href="../iron-menu-behavior/iron-menubar-behavior.html"> 15<link rel="import" href="../iron-resizable-behavior/iron-resizable-behavior.html"> 16<link rel="import" href="../paper-icon-button/paper-icon-button.html"> 17<link rel="import" href="../paper-styles/color.html"> 18<link rel="import" href="paper-tabs-icons.html"> 19<link rel="import" href="paper-tab.html"> 20 21<!-- 22Material design: [Tabs](https://www.google.com/design/spec/components/tabs.html) 23 24`paper-tabs` makes it easy to explore and switch between different views or functional aspects of 25an app, or to browse categorized data sets. 26 27Use `selected` property to get or set the selected tab. 28 29Example: 30 31 <paper-tabs selected="0"> 32 <paper-tab>TAB 1</paper-tab> 33 <paper-tab>TAB 2</paper-tab> 34 <paper-tab>TAB 3</paper-tab> 35 </paper-tabs> 36 37See <a href="?active=paper-tab">paper-tab</a> for more information about 38`paper-tab`. 39 40A common usage for `paper-tabs` is to use it along with `iron-pages` to switch 41between different views. 42 43 <paper-tabs selected="{{selected}}"> 44 <paper-tab>Tab 1</paper-tab> 45 <paper-tab>Tab 2</paper-tab> 46 <paper-tab>Tab 3</paper-tab> 47 </paper-tabs> 48 49 <iron-pages selected="{{selected}}"> 50 <div>Page 1</div> 51 <div>Page 2</div> 52 <div>Page 3</div> 53 </iron-pages> 54 55 56To use links in tabs, add `link` attribute to `paper-tab` and put an `<a>` 57element in `paper-tab` with a `tabindex` of -1. 58 59Example: 60 61<pre><code> 62<style is="custom-style"> 63 .link { 64 @apply(--layout-horizontal); 65 @apply(--layout-center-center); 66 } 67</style> 68 69<paper-tabs selected="0"> 70 <paper-tab link> 71 <a href="#link1" class="link" tabindex="-1">TAB ONE</a> 72 </paper-tab> 73 <paper-tab link> 74 <a href="#link2" class="link" tabindex="-1">TAB TWO</a> 75 </paper-tab> 76 <paper-tab link> 77 <a href="#link3" class="link" tabindex="-1">TAB THREE</a> 78 </paper-tab> 79</paper-tabs> 80</code></pre> 81 82### Styling 83 84The following custom properties and mixins are available for styling: 85 86Custom property | Description | Default 87----------------|-------------|---------- 88`--paper-tabs-selection-bar-color` | Color for the selection bar | `--paper-yellow-a100` 89`--paper-tabs-selection-bar` | Mixin applied to the selection bar | `{}` 90`--paper-tabs` | Mixin applied to the tabs | `{}` 91`--paper-tabs-content` | Mixin applied to the content container of tabs | `{}` 92`--paper-tabs-container` | Mixin applied to the layout container of tabs | `{}` 93 94@hero hero.svg 95@demo demo/index.html 96--> 97 98<dom-module id="paper-tabs"> 99 <template> 100 <style> 101 :host { 102 @apply(--layout); 103 @apply(--layout-center); 104 105 height: 48px; 106 font-size: 14px; 107 font-weight: 500; 108 overflow: hidden; 109 -moz-user-select: none; 110 -ms-user-select: none; 111 -webkit-user-select: none; 112 user-select: none; 113 114 /* NOTE: Both values are needed, since some phones require the value to be `transparent`. */ 115 -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 116 -webkit-tap-highlight-color: transparent; 117 118 @apply(--paper-tabs); 119 } 120 121 :host-context([dir=rtl]) { 122 @apply(--layout-horizontal-reverse); 123 } 124 125 #tabsContainer { 126 position: relative; 127 height: 100%; 128 white-space: nowrap; 129 overflow: hidden; 130 @apply(--layout-flex-auto); 131 @apply(--paper-tabs-container); 132 } 133 134 #tabsContent { 135 height: 100%; 136 -moz-flex-basis: auto; 137 -ms-flex-basis: auto; 138 flex-basis: auto; 139 @apply(--paper-tabs-content); 140 } 141 142 #tabsContent.scrollable { 143 position: absolute; 144 white-space: nowrap; 145 } 146 147 #tabsContent:not(.scrollable), 148 #tabsContent.scrollable.fit-container { 149 @apply(--layout-horizontal); 150 } 151 152 #tabsContent.scrollable.fit-container { 153 min-width: 100%; 154 } 155 156 #tabsContent.scrollable.fit-container > ::content > * { 157 /* IE - prevent tabs from compressing when they should scroll. */ 158 -ms-flex: 1 0 auto; 159 -webkit-flex: 1 0 auto; 160 flex: 1 0 auto; 161 } 162 163 .hidden { 164 display: none; 165 } 166 167 .not-visible { 168 opacity: 0; 169 cursor: default; 170 } 171 172 paper-icon-button { 173 width: 48px; 174 height: 48px; 175 padding: 12px; 176 margin: 0 4px; 177 } 178 179 #selectionBar { 180 position: absolute; 181 height: 0; 182 bottom: 0; 183 left: 0; 184 right: 0; 185 border-bottom: 2px solid var(--paper-tabs-selection-bar-color, --paper-yellow-a100); 186 -webkit-transform: scale(0); 187 transform: scale(0); 188 -webkit-transform-origin: left center; 189 transform-origin: left center; 190 transition: -webkit-transform; 191 transition: transform; 192 193 @apply(--paper-tabs-selection-bar); 194 } 195 196 #selectionBar.align-bottom { 197 top: 0; 198 bottom: auto; 199 } 200 201 #selectionBar.expand { 202 transition-duration: 0.15s; 203 transition-timing-function: cubic-bezier(0.4, 0.0, 1, 1); 204 } 205 206 #selectionBar.contract { 207 transition-duration: 0.18s; 208 transition-timing-function: cubic-bezier(0.0, 0.0, 0.2, 1); 209 } 210 211 #tabsContent > ::content > *:not(#selectionBar) { 212 height: 100%; 213 } 214 </style> 215 216 <paper-icon-button icon="paper-tabs:chevron-left" class$="[[_computeScrollButtonClass(_leftHidden, scrollable, hideScrollButtons)]]" on-up="_onScrollButtonUp" on-down="_onLeftScrollButtonDown" tabindex="-1"></paper-icon-button> 217 218 <div id="tabsContainer" on-track="_scroll" on-down="_down"> 219 <div id="tabsContent" class$="[[_computeTabsContentClass(scrollable, fitContainer)]]"> 220 <div id="selectionBar" class$="[[_computeSelectionBarClass(noBar, alignBottom)]]" 221 on-transitionend="_onBarTransitionEnd"></div> 222 <content select="*"></content> 223 </div> 224 </div> 225 226 <paper-icon-button icon="paper-tabs:chevron-right" class$="[[_computeScrollButtonClass(_rightHidden, scrollable, hideScrollButtons)]]" on-up="_onScrollButtonUp" on-down="_onRightScrollButtonDown" tabindex="-1"></paper-icon-button> 227 228 </template> 229 230 <script> 231 Polymer({ 232 is: 'paper-tabs', 233 234 behaviors: [ 235 Polymer.IronResizableBehavior, 236 Polymer.IronMenubarBehavior 237 ], 238 239 properties: { 240 /** 241 * If true, ink ripple effect is disabled. When this property is changed, 242 * all descendant `<paper-tab>` elements have their `noink` property 243 * changed to the new value as well. 244 */ 245 noink: { 246 type: Boolean, 247 value: false, 248 observer: '_noinkChanged' 249 }, 250 251 /** 252 * If true, the bottom bar to indicate the selected tab will not be shown. 253 */ 254 noBar: { 255 type: Boolean, 256 value: false 257 }, 258 259 /** 260 * If true, the slide effect for the bottom bar is disabled. 261 */ 262 noSlide: { 263 type: Boolean, 264 value: false 265 }, 266 267 /** 268 * If true, tabs are scrollable and the tab width is based on the label width. 269 */ 270 scrollable: { 271 type: Boolean, 272 value: false 273 }, 274 275 /** 276 * If true, tabs expand to fit their container. This currently only applies when 277 * scrollable is true. 278 */ 279 fitContainer: { 280 type: Boolean, 281 value: false 282 }, 283 284 /** 285 * If true, dragging on the tabs to scroll is disabled. 286 */ 287 disableDrag: { 288 type: Boolean, 289 value: false 290 }, 291 292 /** 293 * If true, scroll buttons (left/right arrow) will be hidden for scrollable tabs. 294 */ 295 hideScrollButtons: { 296 type: Boolean, 297 value: false 298 }, 299 300 /** 301 * If true, the tabs are aligned to bottom (the selection bar appears at the top). 302 */ 303 alignBottom: { 304 type: Boolean, 305 value: false 306 }, 307 308 selectable: { 309 type: String, 310 value: 'paper-tab' 311 }, 312 313 /** 314 * If true, tabs are automatically selected when focused using the 315 * keyboard. 316 */ 317 autoselect: { 318 type: Boolean, 319 value: false 320 }, 321 322 /** 323 * The delay (in milliseconds) between when the user stops interacting 324 * with the tabs through the keyboard and when the focused item is 325 * automatically selected (if `autoselect` is true). 326 */ 327 autoselectDelay: { 328 type: Number, 329 value: 0 330 }, 331 332 _step: { 333 type: Number, 334 value: 10 335 }, 336 337 _holdDelay: { 338 type: Number, 339 value: 1 340 }, 341 342 _leftHidden: { 343 type: Boolean, 344 value: false 345 }, 346 347 _rightHidden: { 348 type: Boolean, 349 value: false 350 }, 351 352 _previousTab: { 353 type: Object 354 } 355 }, 356 357 hostAttributes: { 358 role: 'tablist' 359 }, 360 361 listeners: { 362 'iron-resize': '_onTabSizingChanged', 363 'iron-items-changed': '_onTabSizingChanged', 364 'iron-select': '_onIronSelect', 365 'iron-deselect': '_onIronDeselect' 366 }, 367 368 keyBindings: { 369 'left:keyup right:keyup': '_onArrowKeyup' 370 }, 371 372 created: function() { 373 this._holdJob = null; 374 this._pendingActivationItem = undefined; 375 this._pendingActivationTimeout = undefined; 376 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind(this); 377 this.addEventListener('blur', this._onBlurCapture.bind(this), true); 378 }, 379 380 ready: function() { 381 this.setScrollDirection('y', this.$.tabsContainer); 382 }, 383 384 detached: function() { 385 this._cancelPendingActivation(); 386 }, 387 388 _noinkChanged: function(noink) { 389 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab'); 390 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAttribute); 391 }, 392 393 _setNoinkAttribute: function(element) { 394 element.setAttribute('noink', ''); 395 }, 396 397 _removeNoinkAttribute: function(element) { 398 element.removeAttribute('noink'); 399 }, 400 401 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScrollButtons) { 402 if (!scrollable || hideScrollButtons) { 403 return 'hidden'; 404 } 405 406 if (hideThisButton) { 407 return 'not-visible'; 408 } 409 410 return ''; 411 }, 412 413 _computeTabsContentClass: function(scrollable, fitContainer) { 414 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : '') : ' fit-container'; 415 }, 416 417 _computeSelectionBarClass: function(noBar, alignBottom) { 418 if (noBar) { 419 return 'hidden'; 420 } else if (alignBottom) { 421 return 'align-bottom'; 422 } 423 424 return ''; 425 }, 426 427 // TODO(cdata): Add `track` response back in when gesture lands. 428 429 _onTabSizingChanged: function() { 430 this.debounce('_onTabSizingChanged', function() { 431 this._scroll(); 432 this._tabChanged(this.selectedItem); 433 }, 10); 434 }, 435 436 _onIronSelect: function(event) { 437 this._tabChanged(event.detail.item, this._previousTab); 438 this._previousTab = event.detail.item; 439 this.cancelDebouncer('tab-changed'); 440 }, 441 442 _onIronDeselect: function(event) { 443 this.debounce('tab-changed', function() { 444 this._tabChanged(null, this._previousTab); 445 this._previousTab = null; 446 // See polymer/polymer#1305 447 }, 1); 448 }, 449 450 _activateHandler: function() { 451 // Cancel item activations scheduled by keyboard events when any other 452 // action causes an item to be activated (e.g. clicks). 453 this._cancelPendingActivation(); 454 455 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments); 456 }, 457 458 /** 459 * Activates an item after a delay (in milliseconds). 460 */ 461 _scheduleActivation: function(item, delay) { 462 this._pendingActivationItem = item; 463 this._pendingActivationTimeout = this.async( 464 this._bindDelayedActivationHandler, delay); 465 }, 466 467 /** 468 * Activates the last item given to `_scheduleActivation`. 469 */ 470 _delayedActivationHandler: function() { 471 var item = this._pendingActivationItem; 472 this._pendingActivationItem = undefined; 473 this._pendingActivationTimeout = undefined; 474 item.fire(this.activateEvent, null, { 475 bubbles: true, 476 cancelable: true 477 }); 478 }, 479 480 /** 481 * Cancels a previously scheduled item activation made with 482 * `_scheduleActivation`. 483 */ 484 _cancelPendingActivation: function() { 485 if (this._pendingActivationTimeout !== undefined) { 486 this.cancelAsync(this._pendingActivationTimeout); 487 this._pendingActivationItem = undefined; 488 this._pendingActivationTimeout = undefined; 489 } 490 }, 491 492 _onArrowKeyup: function(event) { 493 if (this.autoselect) { 494 this._scheduleActivation(this.focusedItem, this.autoselectDelay); 495 } 496 }, 497 498 _onBlurCapture: function(event) { 499 // Cancel a scheduled item activation (if any) when that item is 500 // blurred. 501 if (event.target === this._pendingActivationItem) { 502 this._cancelPendingActivation(); 503 } 504 }, 505 506 get _tabContainerScrollSize () { 507 return Math.max( 508 0, 509 this.$.tabsContainer.scrollWidth - 510 this.$.tabsContainer.offsetWidth 511 ); 512 }, 513 514 _scroll: function(e, detail) { 515 if (!this.scrollable) { 516 return; 517 } 518 519 var ddx = (detail && -detail.ddx) || 0; 520 this._affectScroll(ddx); 521 }, 522 523 _down: function(e) { 524 // go one beat async to defeat IronMenuBehavior 525 // autorefocus-on-no-selection timeout 526 this.async(function() { 527 if (this._defaultFocusAsync) { 528 this.cancelAsync(this._defaultFocusAsync); 529 this._defaultFocusAsync = null; 530 } 531 }, 1); 532 }, 533 534 _affectScroll: function(dx) { 535 this.$.tabsContainer.scrollLeft += dx; 536 537 var scrollLeft = this.$.tabsContainer.scrollLeft; 538 539 this._leftHidden = scrollLeft === 0; 540 this._rightHidden = scrollLeft === this._tabContainerScrollSize; 541 }, 542 543 _onLeftScrollButtonDown: function() { 544 this._scrollToLeft(); 545 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDelay); 546 }, 547 548 _onRightScrollButtonDown: function() { 549 this._scrollToRight(); 550 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDelay); 551 }, 552 553 _onScrollButtonUp: function() { 554 clearInterval(this._holdJob); 555 this._holdJob = null; 556 }, 557 558 _scrollToLeft: function() { 559 this._affectScroll(-this._step); 560 }, 561 562 _scrollToRight: function() { 563 this._affectScroll(this._step); 564 }, 565 566 _tabChanged: function(tab, old) { 567 if (!tab) { 568 // Remove the bar without animation. 569 this.$.selectionBar.classList.remove('expand'); 570 this.$.selectionBar.classList.remove('contract'); 571 this._positionBar(0, 0); 572 return; 573 } 574 575 var r = this.$.tabsContent.getBoundingClientRect(); 576 var w = r.width; 577 var tabRect = tab.getBoundingClientRect(); 578 var tabOffsetLeft = tabRect.left - r.left; 579 580 this._pos = { 581 width: this._calcPercent(tabRect.width, w), 582 left: this._calcPercent(tabOffsetLeft, w) 583 }; 584 585 if (this.noSlide || old == null) { 586 // Position the bar without animation. 587 this.$.selectionBar.classList.remove('expand'); 588 this.$.selectionBar.classList.remove('contract'); 589 this._positionBar(this._pos.width, this._pos.left); 590 return; 591 } 592 593 var oldRect = old.getBoundingClientRect(); 594 var oldIndex = this.items.indexOf(old); 595 var index = this.items.indexOf(tab); 596 var m = 5; 597 598 // bar animation: expand 599 this.$.selectionBar.classList.add('expand'); 600 601 var moveRight = oldIndex < index; 602 var isRTL = this._isRTL; 603 if (isRTL) { 604 moveRight = !moveRight; 605 } 606 607 if (moveRight) { 608 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - oldRect.left, w) - m, 609 this._left); 610 } else { 611 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tabRect.left, w) - m, 612 this._calcPercent(tabOffsetLeft, w) + m); 613 } 614 615 if (this.scrollable) { 616 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft); 617 } 618 }, 619 620 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) { 621 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft; 622 if (l < 0) { 623 this.$.tabsContainer.scrollLeft += l; 624 } else { 625 l += (tabWidth - this.$.tabsContainer.offsetWidth); 626 if (l > 0) { 627 this.$.tabsContainer.scrollLeft += l; 628 } 629 } 630 }, 631 632 _calcPercent: function(w, w0) { 633 return 100 * w / w0; 634 }, 635 636 _positionBar: function(width, left) { 637 width = width || 0; 638 left = left || 0; 639 640 this._width = width; 641 this._left = left; 642 this.transform( 643 'translateX(' + left + '%) scaleX(' + (width / 100) + ')', 644 this.$.selectionBar); 645 }, 646 647 _onBarTransitionEnd: function(e) { 648 var cl = this.$.selectionBar.classList; 649 // bar animation: expand -> contract 650 if (cl.contains('expand')) { 651 cl.remove('expand'); 652 cl.add('contract'); 653 this._positionBar(this._pos.width, this._pos.left); 654 // bar animation done 655 } else if (cl.contains('contract')) { 656 cl.remove('contract'); 657 } 658 } 659 }); 660 </script> 661</dom-module> 662