mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(tabs): support pagination in nav bar (#16055)
Adds support for pagination to `mat-tab-nav-bar` by moving some code around so that it can be reused from the `mat-tab-group`. Fixes #2177.
This commit is contained in:
committed by
Andrew Seguin
parent
a461f32e95
commit
aa2236820d
@@ -1,3 +1,4 @@
|
||||
.example-action-button {
|
||||
margin-bottom: 8px;
|
||||
margin-top: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<button mat-raised-button
|
||||
class="example-action-button"
|
||||
(click)="toggleBackground()">
|
||||
Toggle background
|
||||
</button>
|
||||
|
||||
<nav mat-tab-nav-bar [backgroundColor]="background">
|
||||
<a mat-tab-link *ngFor="let link of links"
|
||||
(click)="activeLink = link"
|
||||
[active]="activeLink == link"> {{link}} </a>
|
||||
<a mat-tab-link disabled>Disabled Link</a>
|
||||
</nav>
|
||||
|
||||
<button mat-raised-button class="example-action-button" (click)="toggleBackground()">
|
||||
Toggle background
|
||||
</button>
|
||||
<button mat-raised-button class="example-action-button" (click)="addLink()">
|
||||
Add link
|
||||
</button>
|
||||
|
||||
@@ -16,4 +16,8 @@ export class TabNavBarBasicExample {
|
||||
toggleBackground() {
|
||||
this.background = this.background ? '' : 'primary';
|
||||
}
|
||||
|
||||
addLink() {
|
||||
this.links.push(`Link ${this.links.length + 1}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MatPaginatedTabHeaderItem>;
|
||||
abstract _inkBar: MatInkBar;
|
||||
abstract _tabListContainer: ElementRef<HTMLElement>;
|
||||
abstract _tabList: ElementRef<HTMLElement>;
|
||||
abstract _nextPaginator: ElementRef<HTMLElement>;
|
||||
abstract _previousPaginator: ElementRef<HTMLElement>;
|
||||
|
||||
/** 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<void>();
|
||||
|
||||
/** 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<MatPaginatedTabHeaderItem>;
|
||||
|
||||
/** Cached text content of the header. */
|
||||
private _currentTextContent: string;
|
||||
|
||||
/** Stream that will stop the automated scrolling. */
|
||||
private _stopScrolling = new Subject<void>();
|
||||
|
||||
/** 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<number> = new EventEmitter<number>();
|
||||
|
||||
/** Event emitted when a label is focused. */
|
||||
readonly indexFocused: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
constructor(protected _elementRef: ElementRef<HTMLElement>,
|
||||
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<MatPaginatedTabHeaderItem>(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};
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -9,12 +9,11 @@
|
||||
<div class="mat-tab-header-pagination-chevron"></div>
|
||||
</div>
|
||||
|
||||
<div class="mat-tab-label-container" #tabListContainer
|
||||
(keydown)="_handleKeydown($event)">
|
||||
<div class="mat-tab-label-container" #tabListContainer (keydown)="_handleKeydown($event)">
|
||||
<div
|
||||
#tabList
|
||||
class="mat-tab-list"
|
||||
[class._mat-animation-noopable]="_animationMode === 'NoopAnimations'"
|
||||
#tabList
|
||||
role="tablist"
|
||||
(cdkObserveContent)="_onContentChanges()">
|
||||
<div class="mat-tab-labels">
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
@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;
|
||||
@include paginated-tab-header;
|
||||
|
||||
.mat-ink-bar {
|
||||
@include ink-bar;
|
||||
}
|
||||
|
||||
.mat-tab-labels {
|
||||
@include paginated-tab-header-item-wrapper;
|
||||
}
|
||||
|
||||
.mat-tab-label-container {
|
||||
@include paginated-tab-header-container;
|
||||
}
|
||||
|
||||
.mat-tab-list {
|
||||
@include _noop-animation();
|
||||
}
|
||||
|
||||
// Wraps each tab label
|
||||
@@ -20,80 +30,3 @@
|
||||
min-width: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
// 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-label-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
[mat-align-tabs='end'] & {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-517
@@ -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<MatTabLabelWrapper>;
|
||||
@ContentChildren(MatTabLabelWrapper) _items: QueryList<MatTabLabelWrapper>;
|
||||
@ViewChild(MatInkBar, {static: true}) _inkBar: MatInkBar;
|
||||
@ViewChild('tabListContainer', {static: true}) _tabListContainer: ElementRef;
|
||||
@ViewChild('tabList', {static: true}) _tabList: ElementRef;
|
||||
@ViewChild('nextPaginator', {static: false}) _nextPaginator: ElementRef<HTMLElement>;
|
||||
@ViewChild('previousPaginator', {static: false}) _previousPaginator: ElementRef<HTMLElement>;
|
||||
|
||||
/** 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<void>();
|
||||
|
||||
/** 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<MatTabLabelWrapper>;
|
||||
|
||||
/** Cached text content of the header. */
|
||||
private _currentTextContent: string;
|
||||
|
||||
/** Stream that will stop the automated scrolling. */
|
||||
private _stopScrolling = new Subject<void>();
|
||||
|
||||
/** 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;
|
||||
get disableRipple() { return this._disableRipple; }
|
||||
set disableRipple(value: any) { this._disableRipple = coerceBooleanProperty(value); }
|
||||
private _disableRipple: boolean = false;
|
||||
|
||||
if (this._keyManager) {
|
||||
this._keyManager.updateActiveItemIndex(value);
|
||||
}
|
||||
}
|
||||
private _selectedIndex: number = 0;
|
||||
|
||||
/** Event emitted when the option is selected. */
|
||||
@Output() readonly selectFocusedIndex: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
/** Event emitted when a label is focused. */
|
||||
@Output() readonly indexFocused: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
constructor(private _elementRef: ElementRef,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _viewportRuler: ViewportRuler,
|
||||
@Optional() private _dir: Directionality,
|
||||
private _ngZone: NgZone,
|
||||
private _platform: Platform,
|
||||
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();
|
||||
});
|
||||
});
|
||||
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
|
||||
super(elementRef, changeDetectorRef, viewportRuler, dir, ngZone, platform, animationMode);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
<div class="mat-tab-links" (cdkObserveContent)="_alignInkBar()">
|
||||
<ng-content></ng-content>
|
||||
<mat-ink-bar></mat-ink-bar>
|
||||
<div class="mat-tab-header-pagination mat-tab-header-pagination-before mat-elevation-z4"
|
||||
#previousPaginator
|
||||
aria-hidden="true"
|
||||
mat-ripple [matRippleDisabled]="_disableScrollBefore || disableRipple"
|
||||
[class.mat-tab-header-pagination-disabled]="_disableScrollBefore"
|
||||
(click)="_handlePaginatorClick('before')"
|
||||
(mousedown)="_handlePaginatorPress('before')"
|
||||
(touchend)="_stopInterval()">
|
||||
<div class="mat-tab-header-pagination-chevron"></div>
|
||||
</div>
|
||||
|
||||
<div class="mat-tab-link-container" #tabListContainer (keydown)="_handleKeydown($event)">
|
||||
<div class="mat-tab-list" #tabList (cdkObserveContent)="_onContentChanges()">
|
||||
<div class="mat-tab-links">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<mat-ink-bar></mat-ink-bar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mat-tab-header-pagination mat-tab-header-pagination-after mat-elevation-z4"
|
||||
#nextPaginator
|
||||
aria-hidden="true"
|
||||
mat-ripple [matRippleDisabled]="_disableScrollAfter || disableRipple"
|
||||
[class.mat-tab-header-pagination-disabled]="_disableScrollAfter"
|
||||
(mousedown)="_handlePaginatorPress('after')"
|
||||
(click)="_handlePaginatorClick('after')"
|
||||
(touchend)="_stopInterval()">
|
||||
<div class="mat-tab-header-pagination-chevron"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
@import '../tabs-common';
|
||||
@import '../../core/style/variables';
|
||||
|
||||
// Wraps the bar containing the anchors
|
||||
.mat-tab-nav-bar {
|
||||
@include tab-header;
|
||||
}
|
||||
@include paginated-tab-header;
|
||||
|
||||
.mat-tab-links {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@include paginated-tab-header-item-wrapper;
|
||||
}
|
||||
|
||||
[mat-align-tabs='center'] & {
|
||||
justify-content: center;
|
||||
}
|
||||
.mat-ink-bar {
|
||||
@include ink-bar;
|
||||
}
|
||||
|
||||
[mat-align-tabs='end'] & {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.mat-tab-link-container {
|
||||
@include paginated-tab-header-container;
|
||||
}
|
||||
|
||||
// Wraps each link in the header
|
||||
@@ -42,14 +38,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media ($mat-xsmall) {
|
||||
.mat-tab-link {
|
||||
min-width: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
// Styling for the ink bar that displays near the activated anchor
|
||||
.mat-ink-bar {
|
||||
@include ink-bar;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('MatTabNavBar', () => {
|
||||
TabLinkWithNgIf,
|
||||
TabLinkWithTabIndexBinding,
|
||||
TabLinkWithNativeTabindexAttr,
|
||||
TabBarWithInactiveTabsOnInit,
|
||||
],
|
||||
providers: [
|
||||
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions},
|
||||
@@ -199,8 +200,18 @@ describe('MatTabNavBar', () => {
|
||||
|
||||
expect(inkBar.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should hide the ink bar if no tabs are active on init', fakeAsync(() => {
|
||||
const fixture = TestBed.createComponent(TabBarWithInactiveTabsOnInit);
|
||||
fixture.detectChanges();
|
||||
tick(20); // Angular turns rAF calls into 16.6ms timeouts in tests.
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.querySelector('.mat-ink-bar').style.visibility).toBe('hidden');
|
||||
}));
|
||||
|
||||
it('should clean up the ripple event handlers on destroy', () => {
|
||||
let fixture: ComponentFixture<TabLinkWithNgIf> = TestBed.createComponent(TabLinkWithNgIf);
|
||||
fixture.detectChanges();
|
||||
@@ -365,3 +376,15 @@ class TabLinkWithTabIndexBinding {
|
||||
`
|
||||
})
|
||||
class TabLinkWithNativeTabindexAttr {}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<nav mat-tab-nav-bar>
|
||||
<a mat-tab-link *ngFor="let tab of tabs" [active]="false">Tab link {{label}}</a>
|
||||
</nav>
|
||||
`
|
||||
})
|
||||
class TabBarWithInactiveTabsOnInit {
|
||||
tabs = [0, 1, 2];
|
||||
}
|
||||
|
||||
@@ -29,12 +29,10 @@ import {
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
CanColor, CanColorCtor,
|
||||
CanDisable, CanDisableCtor,
|
||||
CanDisableRipple, CanDisableRippleCtor,
|
||||
HasTabIndex, HasTabIndexCtor,
|
||||
MAT_RIPPLE_GLOBAL_OPTIONS,
|
||||
mixinColor,
|
||||
mixinDisabled,
|
||||
mixinDisableRipple,
|
||||
mixinTabIndex, RippleConfig,
|
||||
@@ -43,21 +41,13 @@ import {
|
||||
RippleTarget,
|
||||
ThemePalette,
|
||||
} from '@angular/material/core';
|
||||
import {merge, of as observableOf, Subject} from 'rxjs';
|
||||
import {takeUntil} from 'rxjs/operators';
|
||||
import {MatInkBar} from '../ink-bar';
|
||||
import {FocusMonitor} from '@angular/cdk/a11y';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
import {FocusMonitor, FocusableOption} from '@angular/cdk/a11y';
|
||||
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
|
||||
import {MatInkBar} from '../ink-bar';
|
||||
import {MatPaginatedTabHeader} from '../paginated-tab-header';
|
||||
|
||||
|
||||
// Boilerplate for applying mixins to MatTabNav.
|
||||
/** @docs-private */
|
||||
class MatTabNavBase {
|
||||
constructor(public _elementRef: ElementRef) {}
|
||||
}
|
||||
const _MatTabNavMixinBase: CanDisableRippleCtor & CanColorCtor & typeof MatTabNavBase =
|
||||
mixinDisableRipple(mixinColor(MatTabNavBase, 'primary'));
|
||||
|
||||
/**
|
||||
* Navigation component matching the styles of the tab group header.
|
||||
* Provides anchored navigation with animated ink bar.
|
||||
@@ -66,97 +56,99 @@ const _MatTabNavMixinBase: CanDisableRippleCtor & CanColorCtor & typeof MatTabNa
|
||||
moduleId: module.id,
|
||||
selector: '[mat-tab-nav-bar]',
|
||||
exportAs: 'matTabNavBar, matTabNav',
|
||||
inputs: ['color', 'disableRipple'],
|
||||
inputs: ['color'],
|
||||
templateUrl: 'tab-nav-bar.html',
|
||||
styleUrls: ['tab-nav-bar.css'],
|
||||
host: {'class': 'mat-tab-nav-bar'},
|
||||
host: {
|
||||
'class': 'mat-tab-nav-bar mat-tab-header',
|
||||
'[class.mat-tab-header-pagination-controls-enabled]': '_showPaginationControls',
|
||||
'[class.mat-tab-header-rtl]': "_getLayoutDirection() == 'rtl'",
|
||||
'[class.mat-primary]': 'color !== "warn" && color !== "accent"',
|
||||
'[class.mat-accent]': 'color === "accent"',
|
||||
'[class.mat-warn]': 'color === "warn"',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MatTabNav extends _MatTabNavMixinBase
|
||||
implements AfterContentChecked, AfterContentInit, CanColor, CanDisableRipple, OnDestroy {
|
||||
|
||||
/** Subject that emits when the component has been destroyed. */
|
||||
private readonly _onDestroy = new Subject<void>();
|
||||
|
||||
private _activeLinkChanged: boolean;
|
||||
private _activeLinkElement: ElementRef<HTMLElement> | null;
|
||||
|
||||
@ViewChild(MatInkBar, {static: true}) _inkBar: MatInkBar;
|
||||
export class MatTabNav extends MatPaginatedTabHeader implements AfterContentChecked,
|
||||
AfterContentInit, OnDestroy {
|
||||
|
||||
/** Query list of all tab links of the tab navigation. */
|
||||
@ContentChildren(forwardRef(() => MatTabLink), {descendants: true})
|
||||
_tabLinks: QueryList<MatTabLink>;
|
||||
@ContentChildren(forwardRef(() => MatTabLink), {descendants: true}) _items: QueryList<MatTabLink>;
|
||||
@ViewChild(MatInkBar, {static: true}) _inkBar: MatInkBar;
|
||||
@ViewChild('tabListContainer', {static: true}) _tabListContainer: ElementRef;
|
||||
@ViewChild('tabList', {static: true}) _tabList: ElementRef;
|
||||
@ViewChild('nextPaginator', {static: false}) _nextPaginator: ElementRef<HTMLElement>;
|
||||
@ViewChild('previousPaginator', {static: false}) _previousPaginator: ElementRef<HTMLElement>;
|
||||
|
||||
/** Background color of the tab nav. */
|
||||
@Input()
|
||||
get backgroundColor(): ThemePalette { return this._backgroundColor; }
|
||||
set backgroundColor(value: ThemePalette) {
|
||||
const nativeElement: HTMLElement = this._elementRef.nativeElement;
|
||||
|
||||
nativeElement.classList.remove(`mat-background-${this.backgroundColor}`);
|
||||
const classList = this._elementRef.nativeElement.classList;
|
||||
classList.remove(`mat-background-${this.backgroundColor}`);
|
||||
|
||||
if (value) {
|
||||
nativeElement.classList.add(`mat-background-${value}`);
|
||||
classList.add(`mat-background-${value}`);
|
||||
}
|
||||
|
||||
this._backgroundColor = value;
|
||||
}
|
||||
private _backgroundColor: ThemePalette;
|
||||
|
||||
/** Whether the ripple effect is disabled or not. */
|
||||
@Input()
|
||||
get disableRipple() { return this._disableRipple; }
|
||||
set disableRipple(value: any) { this._disableRipple = coerceBooleanProperty(value); }
|
||||
private _disableRipple: boolean = false;
|
||||
|
||||
/** Theme color of the nav bar. */
|
||||
@Input() color: ThemePalette = 'primary';
|
||||
|
||||
constructor(elementRef: ElementRef,
|
||||
@Optional() private _dir: Directionality,
|
||||
private _ngZone: NgZone,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _viewportRuler: ViewportRuler) {
|
||||
super(elementRef);
|
||||
@Optional() dir: Directionality,
|
||||
ngZone: NgZone,
|
||||
changeDetectorRef: ChangeDetectorRef,
|
||||
viewportRuler: ViewportRuler,
|
||||
/**
|
||||
* @deprecated @breaking-change 9.0.0 `platform` parameter to become required.
|
||||
*/
|
||||
@Optional() platform?: Platform,
|
||||
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
|
||||
super(elementRef, changeDetectorRef, viewportRuler, dir, ngZone, platform, animationMode);
|
||||
}
|
||||
|
||||
protected _itemSelected() {
|
||||
// noop
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.updateActiveLink();
|
||||
super.ngAfterContentInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the component that the active link has been changed.
|
||||
* @breaking-change 8.0.0 `element` parameter to be removed.
|
||||
*/
|
||||
updateActiveLink(element: ElementRef) {
|
||||
// Note: keeping the `element` for backwards-compat, but isn't being used for anything.
|
||||
// @breaking-change 8.0.0
|
||||
this._activeLinkChanged = !!element;
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._ngZone.runOutsideAngular(() => {
|
||||
const dirChange = this._dir ? this._dir.change : observableOf(null);
|
||||
|
||||
return merge(dirChange, this._viewportRuler.change(10))
|
||||
.pipe(takeUntil(this._onDestroy))
|
||||
.subscribe(() => this._alignInkBar());
|
||||
});
|
||||
}
|
||||
|
||||
/** Checks if the active link has been changed and, if so, will update the ink bar. */
|
||||
ngAfterContentChecked(): void {
|
||||
if (this._activeLinkChanged) {
|
||||
const activeTab = this._tabLinks.find(tab => tab.active);
|
||||
|
||||
this._activeLinkElement = activeTab ? activeTab._elementRef : null;
|
||||
this._alignInkBar();
|
||||
this._activeLinkChanged = false;
|
||||
updateActiveLink(_element?: ElementRef) {
|
||||
if (!this._items) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._onDestroy.next();
|
||||
this._onDestroy.complete();
|
||||
}
|
||||
const items = this._items.toArray();
|
||||
|
||||
/** Aligns the ink bar to the active link. */
|
||||
_alignInkBar(): void {
|
||||
if (this._activeLinkElement) {
|
||||
this._inkBar.show();
|
||||
this._inkBar.alignToElement(this._activeLinkElement.nativeElement);
|
||||
} else {
|
||||
this._inkBar.hide();
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].active) {
|
||||
this.selectedIndex = i;
|
||||
this._changeDetectorRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The ink bar should hide itself if no items are active.
|
||||
this.selectedIndex = -1;
|
||||
this._inkBar.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,14 +169,14 @@ const _MatTabLinkMixinBase:
|
||||
host: {
|
||||
'class': 'mat-tab-link',
|
||||
'[attr.aria-current]': 'active',
|
||||
'[attr.aria-disabled]': 'disabled.toString()',
|
||||
'[attr.aria-disabled]': 'disabled',
|
||||
'[attr.tabIndex]': 'tabIndex',
|
||||
'[class.mat-tab-disabled]': 'disabled',
|
||||
'[class.mat-tab-label-active]': 'active',
|
||||
}
|
||||
})
|
||||
export class MatTabLink extends _MatTabLinkMixinBase
|
||||
implements OnDestroy, CanDisable, CanDisableRipple, HasTabIndex, RippleTarget {
|
||||
export class MatTabLink extends _MatTabLinkMixinBase implements OnDestroy, CanDisable,
|
||||
CanDisableRipple, HasTabIndex, RippleTarget, FocusableOption {
|
||||
|
||||
/** Whether the tab link is active or not. */
|
||||
protected _isActive: boolean = false;
|
||||
@@ -198,7 +190,7 @@ export class MatTabLink extends _MatTabLinkMixinBase
|
||||
set active(value: boolean) {
|
||||
if (value !== this._isActive) {
|
||||
this._isActive = value;
|
||||
this._tabNavBar.updateActiveLink(this._elementRef);
|
||||
this._tabNavBar.updateActiveLink(this.elementRef);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,27 +212,31 @@ export class MatTabLink extends _MatTabLinkMixinBase
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _tabNavBar: MatTabNav, public _elementRef: ElementRef, ngZone: NgZone,
|
||||
private _tabNavBar: MatTabNav, public elementRef: ElementRef, ngZone: NgZone,
|
||||
platform: Platform,
|
||||
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalRippleOptions: RippleGlobalOptions|null,
|
||||
@Attribute('tabindex') tabIndex: string, private _focusMonitor: FocusMonitor,
|
||||
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
|
||||
super();
|
||||
|
||||
this._tabLinkRipple = new RippleRenderer(this, ngZone, _elementRef, platform);
|
||||
this._tabLinkRipple.setupTriggerEvents(_elementRef.nativeElement);
|
||||
this._tabLinkRipple = new RippleRenderer(this, ngZone, elementRef, platform);
|
||||
this._tabLinkRipple.setupTriggerEvents(elementRef.nativeElement);
|
||||
this.rippleConfig = globalRippleOptions || {};
|
||||
|
||||
this.tabIndex = parseInt(tabIndex) || 0;
|
||||
_focusMonitor.monitor(_elementRef);
|
||||
|
||||
if (animationMode === 'NoopAnimations') {
|
||||
this.rippleConfig.animation = {enterDuration: 0, exitDuration: 0};
|
||||
}
|
||||
|
||||
_focusMonitor.monitor(elementRef);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.elementRef.nativeElement.focus();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._tabLinkRipple._removeTriggerEvents();
|
||||
this._focusMonitor.stopMonitoring(this._elementRef);
|
||||
this._focusMonitor.stopMonitoring(this.elementRef);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-45
@@ -103,46 +103,16 @@ export declare class MatTabGroup extends _MatTabGroupMixinBase implements AfterC
|
||||
realignInkBar(): void;
|
||||
}
|
||||
|
||||
export declare class MatTabHeader extends _MatTabHeaderMixinBase implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy, CanDisableRipple {
|
||||
_animationMode?: string | undefined;
|
||||
_disableScrollAfter: boolean;
|
||||
_disableScrollBefore: boolean;
|
||||
export declare class MatTabHeader extends MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy {
|
||||
_inkBar: MatInkBar;
|
||||
_labelWrappers: QueryList<MatTabLabelWrapper>;
|
||||
_items: QueryList<MatTabLabelWrapper>;
|
||||
_nextPaginator: ElementRef<HTMLElement>;
|
||||
_previousPaginator: ElementRef<HTMLElement>;
|
||||
_showPaginationControls: boolean;
|
||||
_tabList: ElementRef;
|
||||
_tabListContainer: ElementRef;
|
||||
focusIndex: number;
|
||||
readonly indexFocused: EventEmitter<number>;
|
||||
scrollDistance: number;
|
||||
readonly selectFocusedIndex: EventEmitter<number>;
|
||||
selectedIndex: number;
|
||||
constructor(_elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, _viewportRuler: ViewportRuler, _dir: Directionality, _ngZone: NgZone, _platform: Platform, _animationMode?: string | undefined);
|
||||
_alignInkBarToSelectedTab(): void;
|
||||
_checkPaginationEnabled(): void;
|
||||
_checkScrollingControls(): void;
|
||||
_getLayoutDirection(): Direction;
|
||||
_getMaxScrollDistance(): number;
|
||||
_handleKeydown(event: KeyboardEvent): void;
|
||||
_handlePaginatorClick(direction: ScrollDirection): void;
|
||||
_handlePaginatorPress(direction: ScrollDirection): void;
|
||||
_isValidIndex(index: number): boolean;
|
||||
_onContentChanges(): void;
|
||||
_scrollHeader(direction: ScrollDirection): {
|
||||
maxScrollDistance: number;
|
||||
distance: number;
|
||||
};
|
||||
_scrollToLabel(labelIndex: number): void;
|
||||
_setTabFocus(tabIndex: number): void;
|
||||
_stopInterval(): void;
|
||||
_updateTabScrollPosition(): void;
|
||||
ngAfterContentChecked(): void;
|
||||
ngAfterContentInit(): void;
|
||||
ngAfterViewInit(): void;
|
||||
ngOnDestroy(): void;
|
||||
updatePagination(): void;
|
||||
disableRipple: any;
|
||||
constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, viewportRuler: ViewportRuler, dir: Directionality, ngZone: NgZone, platform: Platform, animationMode?: string);
|
||||
protected _itemSelected(event: KeyboardEvent): void;
|
||||
}
|
||||
|
||||
export declare type MatTabHeaderPosition = 'above' | 'below';
|
||||
@@ -158,27 +128,33 @@ export declare class MatTabLabelWrapper extends _MatTabLabelWrapperMixinBase imp
|
||||
getOffsetWidth(): number;
|
||||
}
|
||||
|
||||
export declare class MatTabLink extends _MatTabLinkMixinBase implements OnDestroy, CanDisable, CanDisableRipple, HasTabIndex, RippleTarget {
|
||||
_elementRef: ElementRef;
|
||||
export declare class MatTabLink extends _MatTabLinkMixinBase implements OnDestroy, CanDisable, CanDisableRipple, HasTabIndex, RippleTarget, FocusableOption {
|
||||
protected _isActive: boolean;
|
||||
protected _tabLinkRipple: RippleRenderer;
|
||||
active: boolean;
|
||||
elementRef: ElementRef;
|
||||
rippleConfig: RippleConfig & RippleGlobalOptions;
|
||||
readonly rippleDisabled: boolean;
|
||||
constructor(_tabNavBar: MatTabNav, _elementRef: ElementRef, ngZone: NgZone, platform: Platform, globalRippleOptions: RippleGlobalOptions | null, tabIndex: string, _focusMonitor: FocusMonitor, animationMode?: string);
|
||||
constructor(_tabNavBar: MatTabNav, elementRef: ElementRef, ngZone: NgZone, platform: Platform, globalRippleOptions: RippleGlobalOptions | null, tabIndex: string, _focusMonitor: FocusMonitor, animationMode?: string);
|
||||
focus(): void;
|
||||
ngOnDestroy(): void;
|
||||
}
|
||||
|
||||
export declare class MatTabNav extends _MatTabNavMixinBase implements AfterContentChecked, AfterContentInit, CanColor, CanDisableRipple, OnDestroy {
|
||||
export declare class MatTabNav extends MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit, OnDestroy {
|
||||
_inkBar: MatInkBar;
|
||||
_tabLinks: QueryList<MatTabLink>;
|
||||
_items: QueryList<MatTabLink>;
|
||||
_nextPaginator: ElementRef<HTMLElement>;
|
||||
_previousPaginator: ElementRef<HTMLElement>;
|
||||
_tabList: ElementRef;
|
||||
_tabListContainer: ElementRef;
|
||||
backgroundColor: ThemePalette;
|
||||
constructor(elementRef: ElementRef, _dir: Directionality, _ngZone: NgZone, _changeDetectorRef: ChangeDetectorRef, _viewportRuler: ViewportRuler);
|
||||
_alignInkBar(): void;
|
||||
ngAfterContentChecked(): void;
|
||||
color: ThemePalette;
|
||||
disableRipple: any;
|
||||
constructor(elementRef: ElementRef, dir: Directionality, ngZone: NgZone, changeDetectorRef: ChangeDetectorRef, viewportRuler: ViewportRuler,
|
||||
platform?: Platform, animationMode?: string);
|
||||
protected _itemSelected(): void;
|
||||
ngAfterContentInit(): void;
|
||||
ngOnDestroy(): void;
|
||||
updateActiveLink(element: ElementRef): void;
|
||||
updateActiveLink(_element?: ElementRef): void;
|
||||
}
|
||||
|
||||
export declare const matTabsAnimations: {
|
||||
|
||||
Reference in New Issue
Block a user