diff --git a/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.css b/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.css
index b25726a6c6d3..e7f8daa5cd3b 100644
--- a/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.css
+++ b/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.css
@@ -1,3 +1,4 @@
.example-action-button {
- margin-bottom: 8px;
+ margin-top: 8px;
+ margin-right: 8px;
}
diff --git a/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.html b/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.html
index 538f335c1b8d..104c32507d3c 100644
--- a/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.html
+++ b/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.html
@@ -1,12 +1,13 @@
-
-
+
+
+
diff --git a/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.ts b/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.ts
index 330f3d54ff04..12886bf2b915 100644
--- a/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.ts
+++ b/src/material-examples/tab-nav-bar-basic/tab-nav-bar-basic-example.ts
@@ -16,4 +16,8 @@ export class TabNavBarBasicExample {
toggleBackground() {
this.background = this.background ? '' : 'primary';
}
+
+ addLink() {
+ this.links.push(`Link ${this.links.length + 1}`);
+ }
}
diff --git a/src/material/tabs/_tabs-common.scss b/src/material/tabs/_tabs-common.scss
index a395cddb1271..8a5067d1adc5 100644
--- a/src/material/tabs/_tabs-common.scss
+++ b/src/material/tabs/_tabs-common.scss
@@ -1,5 +1,6 @@
@import '../core/style/variables';
@import '../core/style/noop-animation';
+@import '../core/style/vendor-prefixes';
@import '../../cdk/a11y/a11y';
$mat-tab-bar-height: 48px !default;
@@ -51,14 +52,7 @@ $mat-tab-animation-duration: 500ms !default;
}
}
-// Mixin styles for the top section of the view; contains the tab labels.
-@mixin tab-header {
- overflow: hidden;
- position: relative;
- flex-shrink: 0;
-}
-
-// Mixin styles for the ink bar that displays near the active tab in the header.
+// The ink bar that displays under the active tab label
@mixin ink-bar {
$height: 2px;
@@ -78,3 +72,85 @@ $mat-tab-animation-duration: 500ms !default;
height: 0;
}
}
+
+// Structural styles for a tab header. Used by both `mat-tab-header` and `mat-tab-nav-bar`.
+@mixin paginated-tab-header {
+ .mat-tab-header {
+ display: flex;
+ overflow: hidden;
+ position: relative;
+ flex-shrink: 0;
+ }
+
+ .mat-tab-header-pagination {
+ @include user-select(none);
+ position: relative;
+ display: none;
+ justify-content: center;
+ align-items: center;
+ min-width: 32px;
+ cursor: pointer;
+ z-index: 2;
+ -webkit-tap-highlight-color: transparent;
+ touch-action: none;
+
+ .mat-tab-header-pagination-controls-enabled & {
+ display: flex;
+ }
+ }
+
+ // The pagination control that is displayed on the left side of the tab header.
+ .mat-tab-header-pagination-before, .mat-tab-header-rtl .mat-tab-header-pagination-after {
+ padding-left: 4px;
+ .mat-tab-header-pagination-chevron {
+ transform: rotate(-135deg);
+ }
+ }
+
+ // The pagination control that is displayed on the right side of the tab header.
+ .mat-tab-header-rtl .mat-tab-header-pagination-before, .mat-tab-header-pagination-after {
+ padding-right: 4px;
+ .mat-tab-header-pagination-chevron {
+ transform: rotate(45deg);
+ }
+ }
+
+ .mat-tab-header-pagination-chevron {
+ border-style: solid;
+ border-width: 2px 2px 0 0;
+ content: '';
+ height: 8px;
+ width: 8px;
+ }
+
+ .mat-tab-header-pagination-disabled {
+ box-shadow: none;
+ cursor: default;
+ }
+
+ .mat-tab-list {
+ flex-grow: 1;
+ position: relative;
+ transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1);
+ }
+}
+
+// Structural styles for the element that wraps the paginated header items.
+@mixin paginated-tab-header-item-wrapper {
+ display: flex;
+
+ [mat-align-tabs='center'] & {
+ justify-content: center;
+ }
+
+ [mat-align-tabs='end'] & {
+ justify-content: flex-end;
+ }
+}
+
+@mixin paginated-tab-header-container {
+ display: flex;
+ flex-grow: 1;
+ overflow: hidden;
+ z-index: 1;
+}
diff --git a/src/material/tabs/_tabs-theme.scss b/src/material/tabs/_tabs-theme.scss
index 65afdc75c188..15d1d4ddf865 100644
--- a/src/material/tabs/_tabs-theme.scss
+++ b/src/material/tabs/_tabs-theme.scss
@@ -96,7 +96,7 @@
@mixin _mat-tabs-background($background-color) {
// Set background color for the tab group
- .mat-tab-header, .mat-tab-links {
+ .mat-tab-header, .mat-tab-links, .mat-tab-header-pagination {
background-color: mat-color($background-color);
}
diff --git a/src/material/tabs/paginated-tab-header.ts b/src/material/tabs/paginated-tab-header.ts
new file mode 100644
index 000000000000..186a30da0e29
--- /dev/null
+++ b/src/material/tabs/paginated-tab-header.ts
@@ -0,0 +1,556 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ ChangeDetectorRef,
+ ElementRef,
+ NgZone,
+ Optional,
+ QueryList,
+ EventEmitter,
+ AfterContentChecked,
+ AfterContentInit,
+ AfterViewInit,
+ OnDestroy,
+} from '@angular/core';
+import {Direction, Directionality} from '@angular/cdk/bidi';
+import {coerceNumberProperty} from '@angular/cdk/coercion';
+import {ViewportRuler} from '@angular/cdk/scrolling';
+import {FocusKeyManager, FocusableOption} from '@angular/cdk/a11y';
+import {END, ENTER, HOME, SPACE, hasModifierKey} from '@angular/cdk/keycodes';
+import {merge, of as observableOf, Subject, timer, fromEvent} from 'rxjs';
+import {takeUntil} from 'rxjs/operators';
+import {MatInkBar} from './ink-bar';
+import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform';
+
+
+/** Config used to bind passive event listeners */
+const passiveEventListenerOptions =
+ normalizePassiveListenerOptions({passive: true}) as EventListenerOptions;
+
+/**
+ * The directions that scrolling can go in when the header's tabs exceed the header width. 'After'
+ * will scroll the header towards the end of the tabs list and 'before' will scroll towards the
+ * beginning of the list.
+ */
+export type ScrollDirection = 'after' | 'before';
+
+/**
+ * The distance in pixels that will be overshot when scrolling a tab label into view. This helps
+ * provide a small affordance to the label next to it.
+ */
+const EXAGGERATED_OVERSCROLL = 60;
+
+/**
+ * Amount of milliseconds to wait before starting to scroll the header automatically.
+ * Set a little conservatively in order to handle fake events dispatched on touch devices.
+ */
+const HEADER_SCROLL_DELAY = 650;
+
+/**
+ * Interval in milliseconds at which to scroll the header
+ * while the user is holding their pointer.
+ */
+const HEADER_SCROLL_INTERVAL = 100;
+
+/** Item inside a paginated tab header. */
+type MatPaginatedTabHeaderItem = FocusableOption & {elementRef: ElementRef};
+
+/**
+ * Base class for a tab header that supported pagination.
+ */
+export abstract class MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit,
+ AfterViewInit, OnDestroy {
+ abstract _items: QueryList;
+ abstract _inkBar: MatInkBar;
+ abstract _tabListContainer: ElementRef;
+ abstract _tabList: ElementRef;
+ abstract _nextPaginator: ElementRef;
+ abstract _previousPaginator: ElementRef;
+
+ /** The distance in pixels that the tab labels should be translated to the left. */
+ private _scrollDistance = 0;
+
+ /** Whether the header should scroll to the selected index after the view has been checked. */
+ private _selectedIndexChanged = false;
+
+ /** Emits when the component is destroyed. */
+ private readonly _destroyed = new Subject();
+
+ /** Whether the controls for pagination should be displayed */
+ _showPaginationControls = false;
+
+ /** Whether the tab list can be scrolled more towards the end of the tab label list. */
+ _disableScrollAfter = true;
+
+ /** Whether the tab list can be scrolled more towards the beginning of the tab label list. */
+ _disableScrollBefore = true;
+
+ /**
+ * The number of tab labels that are displayed on the header. When this changes, the header
+ * should re-evaluate the scroll position.
+ */
+ private _tabLabelCount: number;
+
+ /** Whether the scroll distance has changed and should be applied after the view is checked. */
+ private _scrollDistanceChanged: boolean;
+
+ /** Used to manage focus between the tabs. */
+ private _keyManager: FocusKeyManager;
+
+ /** Cached text content of the header. */
+ private _currentTextContent: string;
+
+ /** Stream that will stop the automated scrolling. */
+ private _stopScrolling = new Subject();
+
+ /** The index of the active tab. */
+ get selectedIndex(): number { return this._selectedIndex; }
+ set selectedIndex(value: number) {
+ value = coerceNumberProperty(value);
+
+ if (this._selectedIndex != value) {
+ this._selectedIndexChanged = true;
+ this._selectedIndex = value;
+
+ if (this._keyManager) {
+ this._keyManager.updateActiveItemIndex(value);
+ }
+ }
+ }
+ private _selectedIndex: number = 0;
+
+ /** Event emitted when the option is selected. */
+ readonly selectFocusedIndex: EventEmitter = new EventEmitter();
+
+ /** Event emitted when a label is focused. */
+ readonly indexFocused: EventEmitter = new EventEmitter();
+
+ constructor(protected _elementRef: ElementRef,
+ protected _changeDetectorRef: ChangeDetectorRef,
+ private _viewportRuler: ViewportRuler,
+ @Optional() private _dir: Directionality,
+ private _ngZone: NgZone,
+ /**
+ * @deprecated @breaking-change 9.0.0 `_platform` and `_animationMode`
+ * parameters to become required.
+ */
+ private _platform?: Platform,
+ public _animationMode?: string) {
+
+ // Bind the `mouseleave` event on the outside since it doesn't change anything in the view.
+ _ngZone.runOutsideAngular(() => {
+ fromEvent(_elementRef.nativeElement, 'mouseleave')
+ .pipe(takeUntil(this._destroyed))
+ .subscribe(() => {
+ this._stopInterval();
+ });
+ });
+ }
+
+ /** Called when the user has selected an item via the keyboard. */
+ protected abstract _itemSelected(event: KeyboardEvent): void;
+
+ ngAfterViewInit() {
+ // We need to handle these events manually, because we want to bind passive event listeners.
+ fromEvent(this._previousPaginator.nativeElement, 'touchstart', passiveEventListenerOptions)
+ .pipe(takeUntil(this._destroyed))
+ .subscribe(() => {
+ this._handlePaginatorPress('before');
+ });
+
+ fromEvent(this._nextPaginator.nativeElement, 'touchstart', passiveEventListenerOptions)
+ .pipe(takeUntil(this._destroyed))
+ .subscribe(() => {
+ this._handlePaginatorPress('after');
+ });
+ }
+
+ ngAfterContentInit() {
+ const dirChange = this._dir ? this._dir.change : observableOf(null);
+ const resize = this._viewportRuler.change(150);
+ const realign = () => {
+ this.updatePagination();
+ this._alignInkBarToSelectedTab();
+ };
+
+ this._keyManager = new FocusKeyManager(this._items)
+ .withHorizontalOrientation(this._getLayoutDirection())
+ .withWrap();
+
+ this._keyManager.updateActiveItem(0);
+
+ // Defer the first call in order to allow for slower browsers to lay out the elements.
+ // This helps in cases where the user lands directly on a page with paginated tabs.
+ typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame(realign) : realign();
+
+ // On dir change or window resize, realign the ink bar and update the orientation of
+ // the key manager if the direction has changed.
+ merge(dirChange, resize, this._items.changes).pipe(takeUntil(this._destroyed)).subscribe(() => {
+ realign();
+ this._keyManager.withHorizontalOrientation(this._getLayoutDirection());
+ });
+
+ // If there is a change in the focus key manager we need to emit the `indexFocused`
+ // event in order to provide a public event that notifies about focus changes. Also we realign
+ // the tabs container by scrolling the new focused tab into the visible section.
+ this._keyManager.change.pipe(takeUntil(this._destroyed)).subscribe(newFocusIndex => {
+ this.indexFocused.emit(newFocusIndex);
+ this._setTabFocus(newFocusIndex);
+ });
+ }
+
+ ngAfterContentChecked(): void {
+ // If the number of tab labels have changed, check if scrolling should be enabled
+ if (this._tabLabelCount != this._items.length) {
+ this.updatePagination();
+ this._tabLabelCount = this._items.length;
+ this._changeDetectorRef.markForCheck();
+ }
+
+ // If the selected index has changed, scroll to the label and check if the scrolling controls
+ // should be disabled.
+ if (this._selectedIndexChanged) {
+ this._scrollToLabel(this._selectedIndex);
+ this._checkScrollingControls();
+ this._alignInkBarToSelectedTab();
+ this._selectedIndexChanged = false;
+ this._changeDetectorRef.markForCheck();
+ }
+
+ // If the scroll distance has been changed (tab selected, focused, scroll controls activated),
+ // then translate the header to reflect this.
+ if (this._scrollDistanceChanged) {
+ this._updateTabScrollPosition();
+ this._scrollDistanceChanged = false;
+ this._changeDetectorRef.markForCheck();
+ }
+ }
+
+ ngOnDestroy() {
+ this._destroyed.next();
+ this._destroyed.complete();
+ this._stopScrolling.complete();
+ }
+
+ /** Handles keyboard events on the header. */
+ _handleKeydown(event: KeyboardEvent) {
+ // We don't handle any key bindings with a modifier key.
+ if (hasModifierKey(event)) {
+ return;
+ }
+
+ switch (event.keyCode) {
+ case HOME:
+ this._keyManager.setFirstItemActive();
+ event.preventDefault();
+ break;
+ case END:
+ this._keyManager.setLastItemActive();
+ event.preventDefault();
+ break;
+ case ENTER:
+ case SPACE:
+ this.selectFocusedIndex.emit(this.focusIndex);
+ this._itemSelected(event);
+ break;
+ default:
+ this._keyManager.onKeydown(event);
+ }
+ }
+
+ /**
+ * Callback for when the MutationObserver detects that the content has changed.
+ */
+ _onContentChanges() {
+ const textContent = this._elementRef.nativeElement.textContent;
+
+ // We need to diff the text content of the header, because the MutationObserver callback
+ // will fire even if the text content didn't change which is inefficient and is prone
+ // to infinite loops if a poorly constructed expression is passed in (see #14249).
+ if (textContent !== this._currentTextContent) {
+ this._currentTextContent = textContent || '';
+
+ // The content observer runs outside the `NgZone` by default, which
+ // means that we need to bring the callback back in ourselves.
+ this._ngZone.run(() => {
+ this.updatePagination();
+ this._alignInkBarToSelectedTab();
+ this._changeDetectorRef.markForCheck();
+ });
+ }
+ }
+
+ /**
+ * Updates the view whether pagination should be enabled or not.
+ *
+ * WARNING: Calling this method can be very costly in terms of performance. It should be called
+ * as infrequently as possible from outside of the Tabs component as it causes a reflow of the
+ * page.
+ */
+ updatePagination() {
+ this._checkPaginationEnabled();
+ this._checkScrollingControls();
+ this._updateTabScrollPosition();
+ }
+
+ /** Tracks which element has focus; used for keyboard navigation */
+ get focusIndex(): number {
+ return this._keyManager ? this._keyManager.activeItemIndex! : 0;
+ }
+
+ /** When the focus index is set, we must manually send focus to the correct label */
+ set focusIndex(value: number) {
+ if (!this._isValidIndex(value) || this.focusIndex === value || !this._keyManager) {
+ return;
+ }
+
+ this._keyManager.setActiveItem(value);
+ }
+
+ /**
+ * Determines if an index is valid. If the tabs are not ready yet, we assume that the user is
+ * providing a valid index and return true.
+ */
+ _isValidIndex(index: number): boolean {
+ if (!this._items) { return true; }
+
+ const tab = this._items ? this._items.toArray()[index] : null;
+ return !!tab && !tab.disabled;
+ }
+
+ /**
+ * Sets focus on the HTML element for the label wrapper and scrolls it into the view if
+ * scrolling is enabled.
+ */
+ _setTabFocus(tabIndex: number) {
+ if (this._showPaginationControls) {
+ this._scrollToLabel(tabIndex);
+ }
+
+ if (this._items && this._items.length) {
+ this._items.toArray()[tabIndex].focus();
+
+ // Do not let the browser manage scrolling to focus the element, this will be handled
+ // by using translation. In LTR, the scroll left should be 0. In RTL, the scroll width
+ // should be the full width minus the offset width.
+ const containerEl = this._tabListContainer.nativeElement;
+ const dir = this._getLayoutDirection();
+
+ if (dir == 'ltr') {
+ containerEl.scrollLeft = 0;
+ } else {
+ containerEl.scrollLeft = containerEl.scrollWidth - containerEl.offsetWidth;
+ }
+ }
+ }
+
+ /** The layout direction of the containing app. */
+ _getLayoutDirection(): Direction {
+ return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
+ }
+
+ /** Performs the CSS transformation on the tab list that will cause the list to scroll. */
+ _updateTabScrollPosition() {
+ const scrollDistance = this.scrollDistance;
+ const platform = this._platform;
+ const translateX = this._getLayoutDirection() === 'ltr' ? -scrollDistance : scrollDistance;
+
+ // Don't use `translate3d` here because we don't want to create a new layer. A new layer
+ // seems to cause flickering and overflow in Internet Explorer. For example, the ink bar
+ // and ripples will exceed the boundaries of the visible tab bar.
+ // See: https://github.com/angular/components/issues/10276
+ // We round the `transform` here, because transforms with sub-pixel precision cause some
+ // browsers to blur the content of the element.
+ this._tabList.nativeElement.style.transform = `translateX(${Math.round(translateX)}px)`;
+
+ // Setting the `transform` on IE will change the scroll offset of the parent, causing the
+ // position to be thrown off in some cases. We have to reset it ourselves to ensure that
+ // it doesn't get thrown off. Note that we scope it only to IE and Edge, because messing
+ // with the scroll position throws off Chrome 71+ in RTL mode (see #14689).
+ // @breaking-change 9.0.0 Remove null check for `platform` after it can no longer be undefined.
+ if (platform && (platform.TRIDENT || platform.EDGE)) {
+ this._tabListContainer.nativeElement.scrollLeft = 0;
+ }
+ }
+
+ /** Sets the distance in pixels that the tab header should be transformed in the X-axis. */
+ get scrollDistance(): number { return this._scrollDistance; }
+ set scrollDistance(value: number) {
+ this._scrollTo(value);
+ }
+
+ /**
+ * Moves the tab list in the 'before' or 'after' direction (towards the beginning of the list or
+ * the end of the list, respectively). The distance to scroll is computed to be a third of the
+ * length of the tab list view window.
+ *
+ * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
+ * should be called sparingly.
+ */
+ _scrollHeader(direction: ScrollDirection) {
+ const viewLength = this._tabListContainer.nativeElement.offsetWidth;
+
+ // Move the scroll distance one-third the length of the tab list's viewport.
+ const scrollAmount = (direction == 'before' ? -1 : 1) * viewLength / 3;
+
+ return this._scrollTo(this._scrollDistance + scrollAmount);
+ }
+
+ /** Handles click events on the pagination arrows. */
+ _handlePaginatorClick(direction: ScrollDirection) {
+ this._stopInterval();
+ this._scrollHeader(direction);
+ }
+
+ /**
+ * Moves the tab list such that the desired tab label (marked by index) is moved into view.
+ *
+ * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
+ * should be called sparingly.
+ */
+ _scrollToLabel(labelIndex: number) {
+ const selectedLabel = this._items ? this._items.toArray()[labelIndex] : null;
+
+ if (!selectedLabel) { return; }
+
+ // The view length is the visible width of the tab labels.
+ const viewLength = this._tabListContainer.nativeElement.offsetWidth;
+ const {offsetLeft, offsetWidth} = selectedLabel.elementRef.nativeElement;
+
+ let labelBeforePos: number, labelAfterPos: number;
+ if (this._getLayoutDirection() == 'ltr') {
+ labelBeforePos = offsetLeft;
+ labelAfterPos = labelBeforePos + offsetWidth;
+ } else {
+ labelAfterPos = this._tabList.nativeElement.offsetWidth - offsetLeft;
+ labelBeforePos = labelAfterPos - offsetWidth;
+ }
+
+ const beforeVisiblePos = this.scrollDistance;
+ const afterVisiblePos = this.scrollDistance + viewLength;
+
+ if (labelBeforePos < beforeVisiblePos) {
+ // Scroll header to move label to the before direction
+ this.scrollDistance -= beforeVisiblePos - labelBeforePos + EXAGGERATED_OVERSCROLL;
+ } else if (labelAfterPos > afterVisiblePos) {
+ // Scroll header to move label to the after direction
+ this.scrollDistance += labelAfterPos - afterVisiblePos + EXAGGERATED_OVERSCROLL;
+ }
+ }
+
+ /**
+ * Evaluate whether the pagination controls should be displayed. If the scroll width of the
+ * tab list is wider than the size of the header container, then the pagination controls should
+ * be shown.
+ *
+ * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
+ * should be called sparingly.
+ */
+ _checkPaginationEnabled() {
+ const isEnabled =
+ this._tabList.nativeElement.scrollWidth > this._elementRef.nativeElement.offsetWidth;
+
+ if (!isEnabled) {
+ this.scrollDistance = 0;
+ }
+
+ if (isEnabled !== this._showPaginationControls) {
+ this._changeDetectorRef.markForCheck();
+ }
+
+ this._showPaginationControls = isEnabled;
+ }
+
+ /**
+ * Evaluate whether the before and after controls should be enabled or disabled.
+ * If the header is at the beginning of the list (scroll distance is equal to 0) then disable the
+ * before button. If the header is at the end of the list (scroll distance is equal to the
+ * maximum distance we can scroll), then disable the after button.
+ *
+ * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
+ * should be called sparingly.
+ */
+ _checkScrollingControls() {
+ // Check if the pagination arrows should be activated.
+ this._disableScrollBefore = this.scrollDistance == 0;
+ this._disableScrollAfter = this.scrollDistance == this._getMaxScrollDistance();
+ this._changeDetectorRef.markForCheck();
+ }
+
+ /**
+ * Determines what is the maximum length in pixels that can be set for the scroll distance. This
+ * is equal to the difference in width between the tab list container and tab header container.
+ *
+ * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
+ * should be called sparingly.
+ */
+ _getMaxScrollDistance(): number {
+ const lengthOfTabList = this._tabList.nativeElement.scrollWidth;
+ const viewLength = this._tabListContainer.nativeElement.offsetWidth;
+ return (lengthOfTabList - viewLength) || 0;
+ }
+
+ /** Tells the ink-bar to align itself to the current label wrapper */
+ _alignInkBarToSelectedTab(): void {
+ const selectedItem = this._items && this._items.length ?
+ this._items.toArray()[this.selectedIndex] : null;
+ const selectedLabelWrapper = selectedItem ? selectedItem.elementRef.nativeElement : null;
+
+ if (selectedLabelWrapper) {
+ this._inkBar.alignToElement(selectedLabelWrapper);
+ } else {
+ this._inkBar.hide();
+ }
+ }
+
+ /** Stops the currently-running paginator interval. */
+ _stopInterval() {
+ this._stopScrolling.next();
+ }
+
+ /**
+ * Handles the user pressing down on one of the paginators.
+ * Starts scrolling the header after a certain amount of time.
+ * @param direction In which direction the paginator should be scrolled.
+ */
+ _handlePaginatorPress(direction: ScrollDirection) {
+ // Avoid overlapping timers.
+ this._stopInterval();
+
+ // Start a timer after the delay and keep firing based on the interval.
+ timer(HEADER_SCROLL_DELAY, HEADER_SCROLL_INTERVAL)
+ // Keep the timer going until something tells it to stop or the component is destroyed.
+ .pipe(takeUntil(merge(this._stopScrolling, this._destroyed)))
+ .subscribe(() => {
+ const {maxScrollDistance, distance} = this._scrollHeader(direction);
+
+ // Stop the timer if we've reached the start or the end.
+ if (distance === 0 || distance >= maxScrollDistance) {
+ this._stopInterval();
+ }
+ });
+ }
+
+ /**
+ * Scrolls the header to a given position.
+ * @param position Position to which to scroll.
+ * @returns Information on the current scroll distance and the maximum.
+ */
+ private _scrollTo(position: number) {
+ const maxScrollDistance = this._getMaxScrollDistance();
+ this._scrollDistance = Math.max(0, Math.min(maxScrollDistance, position));
+
+ // Mark that the scroll distance has changed so that after the view is checked, the CSS
+ // transformation can move the header.
+ this._scrollDistanceChanged = true;
+ this._checkScrollingControls();
+
+ return {maxScrollDistance, distance: this._scrollDistance};
+ }
+}
diff --git a/src/material/tabs/public-api.ts b/src/material/tabs/public-api.ts
index 60b7fb6a9221..b8865e5539a7 100644
--- a/src/material/tabs/public-api.ts
+++ b/src/material/tabs/public-api.ts
@@ -15,10 +15,11 @@ export {
MatTabBodyPositionState,
MatTabBodyPortal
} from './tab-body';
-export {MatTabHeader, ScrollDirection} from './tab-header';
+export {MatTabHeader} from './tab-header';
export {MatTabLabelWrapper} from './tab-label-wrapper';
export {MatTab} from './tab';
export {MatTabLabel} from './tab-label';
export {MatTabNav, MatTabLink} from './tab-nav-bar/index';
export {MatTabContent} from './tab-content';
+export {ScrollDirection} from './paginated-tab-header';
export * from './tabs-animations';
diff --git a/src/material/tabs/tab-header.html b/src/material/tabs/tab-header.html
index bc64b0c0bae8..39abcf94cd2c 100644
--- a/src/material/tabs/tab-header.html
+++ b/src/material/tabs/tab-header.html
@@ -9,12 +9,11 @@
-
+
diff --git a/src/material/tabs/tab-header.scss b/src/material/tabs/tab-header.scss
index 2ff126ff163e..be368ccd25a4 100644
--- a/src/material/tabs/tab-header.scss
+++ b/src/material/tabs/tab-header.scss
@@ -1,99 +1,32 @@
-@import '../core/style/variables';
-@import '../core/style/layout-common';
-@import '../core/style/vendor-prefixes';
@import '../core/style/noop-animation';
@import './tabs-common';
-.mat-tab-header {
- display: flex;
- @include tab-header;
-}
-
-// Wraps each tab label
-.mat-tab-label {
- @include tab-label;
- position: relative;
-}
-
-@media ($mat-xsmall) {
- .mat-tab-label {
- min-width: 72px;
- }
-}
+@include paginated-tab-header;
-// The ink bar that displays under the active tab label
.mat-ink-bar {
@include ink-bar;
}
-.mat-tab-header-pagination {
- @include user-select(none);
- position: relative;
- display: none;
- justify-content: center;
- align-items: center;
- min-width: 32px;
- cursor: pointer;
- z-index: 2;
- -webkit-tap-highlight-color: transparent;
- touch-action: none;
-
- .mat-tab-header-pagination-controls-enabled & {
- display: flex;
- }
-}
-
-// The pagination control that is displayed on the left side of the tab header.
-.mat-tab-header-pagination-before, .mat-tab-header-rtl .mat-tab-header-pagination-after {
- padding-left: 4px;
- .mat-tab-header-pagination-chevron {
- transform: rotate(-135deg);
- }
-}
-
-// The pagination control that is displayed on the right side of the tab header.
-.mat-tab-header-rtl .mat-tab-header-pagination-before, .mat-tab-header-pagination-after {
- padding-right: 4px;
- .mat-tab-header-pagination-chevron {
- transform: rotate(45deg);
- }
-}
-
-.mat-tab-header-pagination-chevron {
- border-style: solid;
- border-width: 2px 2px 0 0;
- content: '';
- height: 8px;
- width: 8px;
-}
-
-.mat-tab-header-pagination-disabled {
- box-shadow: none;
- cursor: default;
+.mat-tab-labels {
+ @include paginated-tab-header-item-wrapper;
}
.mat-tab-label-container {
- display: flex;
- flex-grow: 1;
- overflow: hidden;
- z-index: 1;
+ @include paginated-tab-header-container;
}
.mat-tab-list {
@include _noop-animation();
- flex-grow: 1;
- position: relative;
- transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1);
}
-.mat-tab-labels {
- display: flex;
-
- [mat-align-tabs='center'] & {
- justify-content: center;
- }
+// Wraps each tab label
+.mat-tab-label {
+ @include tab-label;
+ position: relative;
+}
- [mat-align-tabs='end'] & {
- justify-content: flex-end;
+@media ($mat-xsmall) {
+ .mat-tab-label {
+ min-width: 72px;
}
}
diff --git a/src/material/tabs/tab-header.ts b/src/material/tabs/tab-header.ts
index f242fa2076c6..52676efdd59f 100644
--- a/src/material/tabs/tab-header.ts
+++ b/src/material/tabs/tab-header.ts
@@ -6,9 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Direction, Directionality} from '@angular/cdk/bidi';
-import {coerceNumberProperty} from '@angular/cdk/coercion';
-import {END, ENTER, HOME, SPACE, hasModifierKey} from '@angular/cdk/keycodes';
+import {Directionality} from '@angular/cdk/bidi';
import {ViewportRuler} from '@angular/cdk/scrolling';
import {
AfterContentChecked,
@@ -18,62 +16,22 @@ import {
Component,
ContentChildren,
ElementRef,
- EventEmitter,
- Input,
NgZone,
- Inject,
OnDestroy,
Optional,
- Output,
QueryList,
ViewChild,
ViewEncapsulation,
AfterViewInit,
+ Input,
+ Inject,
} from '@angular/core';
-import {CanDisableRipple, CanDisableRippleCtor, mixinDisableRipple} from '@angular/material/core';
-import {merge, of as observableOf, Subject, timer, fromEvent} from 'rxjs';
-import {takeUntil} from 'rxjs/operators';
+import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
+import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {MatInkBar} from './ink-bar';
import {MatTabLabelWrapper} from './tab-label-wrapper';
-import {FocusKeyManager} from '@angular/cdk/a11y';
-import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform';
-import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
-
-
-/** Config used to bind passive event listeners */
-const passiveEventListenerOptions =
- normalizePassiveListenerOptions({passive: true}) as EventListenerOptions;
-
-/**
- * The directions that scrolling can go in when the header's tabs exceed the header width. 'After'
- * will scroll the header towards the end of the tabs list and 'before' will scroll towards the
- * beginning of the list.
- */
-export type ScrollDirection = 'after' | 'before';
-
-/**
- * The distance in pixels that will be overshot when scrolling a tab label into view. This helps
- * provide a small affordance to the label next to it.
- */
-const EXAGGERATED_OVERSCROLL = 60;
-
-/**
- * Amount of milliseconds to wait before starting to scroll the header automatically.
- * Set a little conservatively in order to handle fake events dispatched on touch devices.
- */
-const HEADER_SCROLL_DELAY = 650;
-
-/**
- * Interval in milliseconds at which to scroll the header
- * while the user is holding their pointer.
- */
-const HEADER_SCROLL_INTERVAL = 100;
-
-// Boilerplate for applying mixins to MatTabHeader.
-/** @docs-private */
-class MatTabHeaderBase {}
-const _MatTabHeaderMixinBase: CanDisableRippleCtor & typeof MatTabHeaderBase =
- mixinDisableRipple(MatTabHeaderBase);
+import {Platform} from '@angular/cdk/platform';
+import {MatPaginatedTabHeader} from './paginated-tab-header';
/**
* The header of the tab group which displays a list of all the tabs in the tab group. Includes
@@ -87,7 +45,8 @@ const _MatTabHeaderMixinBase: CanDisableRippleCtor & typeof MatTabHeaderBase =
selector: 'mat-tab-header',
templateUrl: 'tab-header.html',
styleUrls: ['tab-header.css'],
- inputs: ['disableRipple'],
+ inputs: ['selectedIndex'],
+ outputs: ['selectFocusedIndex', 'indexFocused'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
@@ -96,484 +55,34 @@ const _MatTabHeaderMixinBase: CanDisableRippleCtor & typeof MatTabHeaderBase =
'[class.mat-tab-header-rtl]': "_getLayoutDirection() == 'rtl'",
},
})
-export class MatTabHeader extends _MatTabHeaderMixinBase
- implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy, CanDisableRipple {
+export class MatTabHeader extends MatPaginatedTabHeader implements AfterContentChecked,
+ AfterContentInit, AfterViewInit, OnDestroy {
- @ContentChildren(MatTabLabelWrapper) _labelWrappers: QueryList;
+ @ContentChildren(MatTabLabelWrapper) _items: QueryList;
@ViewChild(MatInkBar, {static: true}) _inkBar: MatInkBar;
@ViewChild('tabListContainer', {static: true}) _tabListContainer: ElementRef;
@ViewChild('tabList', {static: true}) _tabList: ElementRef;
@ViewChild('nextPaginator', {static: false}) _nextPaginator: ElementRef;
@ViewChild('previousPaginator', {static: false}) _previousPaginator: ElementRef;
- /** The distance in pixels that the tab labels should be translated to the left. */
- private _scrollDistance = 0;
-
- /** Whether the header should scroll to the selected index after the view has been checked. */
- private _selectedIndexChanged = false;
-
- /** Emits when the component is destroyed. */
- private readonly _destroyed = new Subject();
-
- /** Whether the controls for pagination should be displayed */
- _showPaginationControls = false;
-
- /** Whether the tab list can be scrolled more towards the end of the tab label list. */
- _disableScrollAfter = true;
-
- /** Whether the tab list can be scrolled more towards the beginning of the tab label list. */
- _disableScrollBefore = true;
-
- /**
- * The number of tab labels that are displayed on the header. When this changes, the header
- * should re-evaluate the scroll position.
- */
- private _tabLabelCount: number;
-
- /** Whether the scroll distance has changed and should be applied after the view is checked. */
- private _scrollDistanceChanged: boolean;
-
- /** Used to manage focus between the tabs. */
- private _keyManager: FocusKeyManager;
-
- /** Cached text content of the header. */
- private _currentTextContent: string;
-
- /** Stream that will stop the automated scrolling. */
- private _stopScrolling = new Subject();
-
- /** The index of the active tab. */
+ /** Whether the ripple effect is disabled or not. */
@Input()
- get selectedIndex(): number { return this._selectedIndex; }
- set selectedIndex(value: number) {
- value = coerceNumberProperty(value);
- this._selectedIndexChanged = this._selectedIndex != value;
- this._selectedIndex = value;
-
- if (this._keyManager) {
- this._keyManager.updateActiveItemIndex(value);
- }
- }
- private _selectedIndex: number = 0;
-
- /** Event emitted when the option is selected. */
- @Output() readonly selectFocusedIndex: EventEmitter = new EventEmitter();
-
- /** Event emitted when a label is focused. */
- @Output() readonly indexFocused: EventEmitter = new EventEmitter();
-
- constructor(private _elementRef: ElementRef,
- private _changeDetectorRef: ChangeDetectorRef,
- private _viewportRuler: ViewportRuler,
- @Optional() private _dir: Directionality,
- private _ngZone: NgZone,
- private _platform: Platform,
+ get disableRipple() { return this._disableRipple; }
+ set disableRipple(value: any) { this._disableRipple = coerceBooleanProperty(value); }
+ private _disableRipple: boolean = false;
+
+ constructor(elementRef: ElementRef,
+ changeDetectorRef: ChangeDetectorRef,
+ viewportRuler: ViewportRuler,
+ @Optional() dir: Directionality,
+ ngZone: NgZone,
+ platform: Platform,
// @breaking-change 9.0.0 `_animationMode` parameter to be made required.
- @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {
- super();
-
- // Bind the `mouseleave` event on the outside since it doesn't change anything in the view.
- _ngZone.runOutsideAngular(() => {
- fromEvent(_elementRef.nativeElement, 'mouseleave')
- .pipe(takeUntil(this._destroyed))
- .subscribe(() => {
- this._stopInterval();
- });
- });
- }
-
- ngAfterContentChecked(): void {
- // If the number of tab labels have changed, check if scrolling should be enabled
- if (this._tabLabelCount != this._labelWrappers.length) {
- this.updatePagination();
- this._tabLabelCount = this._labelWrappers.length;
- this._changeDetectorRef.markForCheck();
- }
-
- // If the selected index has changed, scroll to the label and check if the scrolling controls
- // should be disabled.
- if (this._selectedIndexChanged) {
- this._scrollToLabel(this._selectedIndex);
- this._checkScrollingControls();
- this._alignInkBarToSelectedTab();
- this._selectedIndexChanged = false;
- this._changeDetectorRef.markForCheck();
- }
-
- // If the scroll distance has been changed (tab selected, focused, scroll controls activated),
- // then translate the header to reflect this.
- if (this._scrollDistanceChanged) {
- this._updateTabScrollPosition();
- this._scrollDistanceChanged = false;
- this._changeDetectorRef.markForCheck();
- }
- }
-
- /** Handles keyboard events on the header. */
- _handleKeydown(event: KeyboardEvent) {
- // We don't handle any key bindings with a modifier key.
- if (hasModifierKey(event)) {
- return;
- }
-
- switch (event.keyCode) {
- case HOME:
- this._keyManager.setFirstItemActive();
- event.preventDefault();
- break;
- case END:
- this._keyManager.setLastItemActive();
- event.preventDefault();
- break;
- case ENTER:
- case SPACE:
- this.selectFocusedIndex.emit(this.focusIndex);
- event.preventDefault();
- break;
- default:
- this._keyManager.onKeydown(event);
- }
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
+ super(elementRef, changeDetectorRef, viewportRuler, dir, ngZone, platform, animationMode);
}
- /**
- * Aligns the ink bar to the selected tab on load.
- */
- ngAfterContentInit() {
- const dirChange = this._dir ? this._dir.change : observableOf(null);
- const resize = this._viewportRuler.change(150);
- const realign = () => {
- this.updatePagination();
- this._alignInkBarToSelectedTab();
- };
-
- this._keyManager = new FocusKeyManager(this._labelWrappers)
- .withHorizontalOrientation(this._getLayoutDirection())
- .withWrap();
-
- this._keyManager.updateActiveItem(0);
-
- // Defer the first call in order to allow for slower browsers to lay out the elements.
- // This helps in cases where the user lands directly on a page with paginated tabs.
- typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame(realign) : realign();
-
- // On dir change or window resize, realign the ink bar and update the orientation of
- // the key manager if the direction has changed.
- merge(dirChange, resize).pipe(takeUntil(this._destroyed)).subscribe(() => {
- realign();
- this._keyManager.withHorizontalOrientation(this._getLayoutDirection());
- });
-
- // If there is a change in the focus key manager we need to emit the `indexFocused`
- // event in order to provide a public event that notifies about focus changes. Also we realign
- // the tabs container by scrolling the new focused tab into the visible section.
- this._keyManager.change.pipe(takeUntil(this._destroyed)).subscribe(newFocusIndex => {
- this.indexFocused.emit(newFocusIndex);
- this._setTabFocus(newFocusIndex);
- });
- }
-
- ngAfterViewInit() {
- // We need to handle these events manually, because we want to bind passive event listeners.
- fromEvent(this._previousPaginator.nativeElement, 'touchstart', passiveEventListenerOptions)
- .pipe(takeUntil(this._destroyed))
- .subscribe(() => {
- this._handlePaginatorPress('before');
- });
-
- fromEvent(this._nextPaginator.nativeElement, 'touchstart', passiveEventListenerOptions)
- .pipe(takeUntil(this._destroyed))
- .subscribe(() => {
- this._handlePaginatorPress('after');
- });
- }
-
- ngOnDestroy() {
- this._destroyed.next();
- this._destroyed.complete();
- this._stopScrolling.complete();
- }
-
- /**
- * Callback for when the MutationObserver detects that the content has changed.
- */
- _onContentChanges() {
- const textContent = this._elementRef.nativeElement.textContent;
-
- // We need to diff the text content of the header, because the MutationObserver callback
- // will fire even if the text content didn't change which is inefficient and is prone
- // to infinite loops if a poorly constructed expression is passed in (see #14249).
- if (textContent !== this._currentTextContent) {
- this._currentTextContent = textContent;
-
- // The content observer runs outside the `NgZone` by default, which
- // means that we need to bring the callback back in ourselves.
- this._ngZone.run(() => {
- this.updatePagination();
- this._alignInkBarToSelectedTab();
- this._changeDetectorRef.markForCheck();
- });
- }
- }
-
- /**
- * Updates the view whether pagination should be enabled or not.
- *
- * WARNING: Calling this method can be very costly in terms of performance. It should be called
- * as infrequently as possible from outside of the Tabs component as it causes a reflow of the
- * page.
- */
- updatePagination() {
- this._checkPaginationEnabled();
- this._checkScrollingControls();
- this._updateTabScrollPosition();
- }
-
- /** Tracks which element has focus; used for keyboard navigation */
- get focusIndex(): number {
- return this._keyManager ? this._keyManager.activeItemIndex! : 0;
- }
-
- /** When the focus index is set, we must manually send focus to the correct label */
- set focusIndex(value: number) {
- if (!this._isValidIndex(value) || this.focusIndex === value || !this._keyManager) {
- return;
- }
-
- this._keyManager.setActiveItem(value);
- }
-
- /**
- * Determines if an index is valid. If the tabs are not ready yet, we assume that the user is
- * providing a valid index and return true.
- */
- _isValidIndex(index: number): boolean {
- if (!this._labelWrappers) { return true; }
-
- const tab = this._labelWrappers ? this._labelWrappers.toArray()[index] : null;
- return !!tab && !tab.disabled;
- }
-
- /**
- * Sets focus on the HTML element for the label wrapper and scrolls it into the view if
- * scrolling is enabled.
- */
- _setTabFocus(tabIndex: number) {
- if (this._showPaginationControls) {
- this._scrollToLabel(tabIndex);
- }
-
- if (this._labelWrappers && this._labelWrappers.length) {
- this._labelWrappers.toArray()[tabIndex].focus();
-
- // Do not let the browser manage scrolling to focus the element, this will be handled
- // by using translation. In LTR, the scroll left should be 0. In RTL, the scroll width
- // should be the full width minus the offset width.
- const containerEl = this._tabListContainer.nativeElement;
- const dir = this._getLayoutDirection();
-
- if (dir == 'ltr') {
- containerEl.scrollLeft = 0;
- } else {
- containerEl.scrollLeft = containerEl.scrollWidth - containerEl.offsetWidth;
- }
- }
- }
-
- /** The layout direction of the containing app. */
- _getLayoutDirection(): Direction {
- return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
- }
-
- /** Performs the CSS transformation on the tab list that will cause the list to scroll. */
- _updateTabScrollPosition() {
- const scrollDistance = this.scrollDistance;
- const platform = this._platform;
- const translateX = this._getLayoutDirection() === 'ltr' ? -scrollDistance : scrollDistance;
-
- // Don't use `translate3d` here because we don't want to create a new layer. A new layer
- // seems to cause flickering and overflow in Internet Explorer. For example, the ink bar
- // and ripples will exceed the boundaries of the visible tab bar.
- // See: https://github.com/angular/components/issues/10276
- // We round the `transform` here, because transforms with sub-pixel precision cause some
- // browsers to blur the content of the element.
- this._tabList.nativeElement.style.transform = `translateX(${Math.round(translateX)}px)`;
-
- // Setting the `transform` on IE will change the scroll offset of the parent, causing the
- // position to be thrown off in some cases. We have to reset it ourselves to ensure that
- // it doesn't get thrown off. Note that we scope it only to IE and Edge, because messing
- // with the scroll position throws off Chrome 71+ in RTL mode (see #14689).
- if (platform.TRIDENT || platform.EDGE) {
- this._tabListContainer.nativeElement.scrollLeft = 0;
- }
- }
-
- /** Sets the distance in pixels that the tab header should be transformed in the X-axis. */
- get scrollDistance(): number { return this._scrollDistance; }
- set scrollDistance(value: number) {
- this._scrollTo(value);
- }
-
- /**
- * Moves the tab list in the 'before' or 'after' direction (towards the beginning of the list or
- * the end of the list, respectively). The distance to scroll is computed to be a third of the
- * length of the tab list view window.
- *
- * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
- * should be called sparingly.
- */
- _scrollHeader(direction: ScrollDirection) {
- const viewLength = this._tabListContainer.nativeElement.offsetWidth;
-
- // Move the scroll distance one-third the length of the tab list's viewport.
- const scrollAmount = (direction == 'before' ? -1 : 1) * viewLength / 3;
-
- return this._scrollTo(this._scrollDistance + scrollAmount);
- }
-
- /** Handles click events on the pagination arrows. */
- _handlePaginatorClick(direction: ScrollDirection) {
- this._stopInterval();
- this._scrollHeader(direction);
- }
-
- /**
- * Moves the tab list such that the desired tab label (marked by index) is moved into view.
- *
- * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
- * should be called sparingly.
- */
- _scrollToLabel(labelIndex: number) {
- const selectedLabel = this._labelWrappers ? this._labelWrappers.toArray()[labelIndex] : null;
-
- if (!selectedLabel) { return; }
-
- // The view length is the visible width of the tab labels.
- const viewLength = this._tabListContainer.nativeElement.offsetWidth;
-
- let labelBeforePos: number, labelAfterPos: number;
- if (this._getLayoutDirection() == 'ltr') {
- labelBeforePos = selectedLabel.getOffsetLeft();
- labelAfterPos = labelBeforePos + selectedLabel.getOffsetWidth();
- } else {
- labelAfterPos = this._tabList.nativeElement.offsetWidth - selectedLabel.getOffsetLeft();
- labelBeforePos = labelAfterPos - selectedLabel.getOffsetWidth();
- }
-
- const beforeVisiblePos = this.scrollDistance;
- const afterVisiblePos = this.scrollDistance + viewLength;
-
- if (labelBeforePos < beforeVisiblePos) {
- // Scroll header to move label to the before direction
- this.scrollDistance -= beforeVisiblePos - labelBeforePos + EXAGGERATED_OVERSCROLL;
- } else if (labelAfterPos > afterVisiblePos) {
- // Scroll header to move label to the after direction
- this.scrollDistance += labelAfterPos - afterVisiblePos + EXAGGERATED_OVERSCROLL;
- }
- }
-
- /**
- * Evaluate whether the pagination controls should be displayed. If the scroll width of the
- * tab list is wider than the size of the header container, then the pagination controls should
- * be shown.
- *
- * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
- * should be called sparingly.
- */
- _checkPaginationEnabled() {
- const isEnabled =
- this._tabList.nativeElement.scrollWidth > this._elementRef.nativeElement.offsetWidth;
-
- if (!isEnabled) {
- this.scrollDistance = 0;
- }
-
- if (isEnabled !== this._showPaginationControls) {
- this._changeDetectorRef.markForCheck();
- }
-
- this._showPaginationControls = isEnabled;
- }
-
- /**
- * Evaluate whether the before and after controls should be enabled or disabled.
- * If the header is at the beginning of the list (scroll distance is equal to 0) then disable the
- * before button. If the header is at the end of the list (scroll distance is equal to the
- * maximum distance we can scroll), then disable the after button.
- *
- * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
- * should be called sparingly.
- */
- _checkScrollingControls() {
- // Check if the pagination arrows should be activated.
- this._disableScrollBefore = this.scrollDistance == 0;
- this._disableScrollAfter = this.scrollDistance == this._getMaxScrollDistance();
- this._changeDetectorRef.markForCheck();
- }
-
- /**
- * Determines what is the maximum length in pixels that can be set for the scroll distance. This
- * is equal to the difference in width between the tab list container and tab header container.
- *
- * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
- * should be called sparingly.
- */
- _getMaxScrollDistance(): number {
- const lengthOfTabList = this._tabList.nativeElement.scrollWidth;
- const viewLength = this._tabListContainer.nativeElement.offsetWidth;
- return (lengthOfTabList - viewLength) || 0;
- }
-
- /** Tells the ink-bar to align itself to the current label wrapper */
- _alignInkBarToSelectedTab(): void {
- const selectedLabelWrapper = this._labelWrappers && this._labelWrappers.length ?
- this._labelWrappers.toArray()[this.selectedIndex].elementRef.nativeElement :
- null;
-
- this._inkBar.alignToElement(selectedLabelWrapper!);
- }
-
- /** Stops the currently-running paginator interval. */
- _stopInterval() {
- this._stopScrolling.next();
- }
-
- /**
- * Handles the user pressing down on one of the paginators.
- * Starts scrolling the header after a certain amount of time.
- * @param direction In which direction the paginator should be scrolled.
- */
- _handlePaginatorPress(direction: ScrollDirection) {
- // Avoid overlapping timers.
- this._stopInterval();
-
- // Start a timer after the delay and keep firing based on the interval.
- timer(HEADER_SCROLL_DELAY, HEADER_SCROLL_INTERVAL)
- // Keep the timer going until something tells it to stop or the component is destroyed.
- .pipe(takeUntil(merge(this._stopScrolling, this._destroyed)))
- .subscribe(() => {
- const {maxScrollDistance, distance} = this._scrollHeader(direction);
-
- // Stop the timer if we've reached the start or the end.
- if (distance === 0 || distance >= maxScrollDistance) {
- this._stopInterval();
- }
- });
- }
-
- /**
- * Scrolls the header to a given position.
- * @param position Position to which to scroll.
- * @returns Information on the current scroll distance and the maximum.
- */
- private _scrollTo(position: number) {
- const maxScrollDistance = this._getMaxScrollDistance();
- this._scrollDistance = Math.max(0, Math.min(maxScrollDistance, position));
-
- // Mark that the scroll distance has changed so that after the view is checked, the CSS
- // transformation can move the header.
- this._scrollDistanceChanged = true;
- this._checkScrollingControls();
-
- return {maxScrollDistance, distance: this._scrollDistance};
+ protected _itemSelected(event: KeyboardEvent) {
+ event.preventDefault();
}
}
diff --git a/src/material/tabs/tab-nav-bar/tab-nav-bar.html b/src/material/tabs/tab-nav-bar/tab-nav-bar.html
index d842ff7060db..8d65b9139400 100644
--- a/src/material/tabs/tab-nav-bar/tab-nav-bar.html
+++ b/src/material/tabs/tab-nav-bar/tab-nav-bar.html
@@ -1,5 +1,30 @@
-