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&lt;style is="custom-style">
63  .link {
64    &#64;apply(--layout-horizontal);
65    &#64;apply(--layout-center-center);
66  }
67&lt;/style>
68
69&lt;paper-tabs selected="0">
70  &lt;paper-tab link>
71    &lt;a href="#link1" class="link" tabindex="-1">TAB ONE&lt;/a>
72  &lt;/paper-tab>
73  &lt;paper-tab link>
74    &lt;a href="#link2" class="link" tabindex="-1">TAB TWO&lt;/a>
75  &lt;/paper-tab>
76  &lt;paper-tab link>
77    &lt;a href="#link3" class="link" tabindex="-1">TAB THREE&lt;/a>
78  &lt;/paper-tab>
79&lt;/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