562 lines
20 KiB
TypeScript
562 lines
20 KiB
TypeScript
|
|
/**
|
||
|
|
* @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.dev/license
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {
|
||
|
|
AfterContentChecked,
|
||
|
|
AfterContentInit,
|
||
|
|
ChangeDetectionStrategy,
|
||
|
|
ChangeDetectorRef,
|
||
|
|
Component,
|
||
|
|
ContentChildren,
|
||
|
|
ElementRef,
|
||
|
|
EventEmitter,
|
||
|
|
Input,
|
||
|
|
OnDestroy,
|
||
|
|
Output,
|
||
|
|
QueryList,
|
||
|
|
ViewChild,
|
||
|
|
ViewEncapsulation,
|
||
|
|
booleanAttribute,
|
||
|
|
inject,
|
||
|
|
numberAttribute,
|
||
|
|
ANIMATION_MODULE_TYPE,
|
||
|
|
} from '@angular/core';
|
||
|
|
import {MAT_TAB_GROUP, MatTab} from './tab';
|
||
|
|
import {MatTabHeader} from './tab-header';
|
||
|
|
import {ThemePalette, MatRipple} from '@angular/material/core';
|
||
|
|
import {merge, Subscription} from 'rxjs';
|
||
|
|
import {MAT_TABS_CONFIG, MatTabsConfig} from './tab-config';
|
||
|
|
import {startWith} from 'rxjs/operators';
|
||
|
|
import {_IdGenerator, CdkMonitorFocus, FocusOrigin} from '@angular/cdk/a11y';
|
||
|
|
import {MatTabBody} from './tab-body';
|
||
|
|
import {CdkPortalOutlet} from '@angular/cdk/portal';
|
||
|
|
import {MatTabLabelWrapper} from './tab-label-wrapper';
|
||
|
|
import {Platform} from '@angular/cdk/platform';
|
||
|
|
|
||
|
|
/** @docs-private */
|
||
|
|
export interface MatTabGroupBaseHeader {
|
||
|
|
_alignInkBarToSelectedTab(): void;
|
||
|
|
updatePagination(): void;
|
||
|
|
focusIndex: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Possible positions for the tab header. */
|
||
|
|
export type MatTabHeaderPosition = 'above' | 'below';
|
||
|
|
|
||
|
|
/** Boolean constant that determines whether the tab group supports the `backgroundColor` input */
|
||
|
|
const ENABLE_BACKGROUND_INPUT = true;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Material design tab-group component. Supports basic tab pairs (label + content) and includes
|
||
|
|
* animated ink-bar, keyboard navigation, and screen reader.
|
||
|
|
* See: https://material.io/design/components/tabs.html
|
||
|
|
*/
|
||
|
|
@Component({
|
||
|
|
selector: 'mat-tab-group',
|
||
|
|
exportAs: 'matTabGroup',
|
||
|
|
templateUrl: 'tab-group.html',
|
||
|
|
styleUrl: 'tab-group.css',
|
||
|
|
encapsulation: ViewEncapsulation.None,
|
||
|
|
// tslint:disable-next-line:validate-decorators
|
||
|
|
changeDetection: ChangeDetectionStrategy.Default,
|
||
|
|
providers: [
|
||
|
|
{
|
||
|
|
provide: MAT_TAB_GROUP,
|
||
|
|
useExisting: MatTabGroup,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
host: {
|
||
|
|
'class': 'mat-mdc-tab-group',
|
||
|
|
'[class]': '"mat-" + (color || "primary")',
|
||
|
|
'[class.mat-mdc-tab-group-dynamic-height]': 'dynamicHeight',
|
||
|
|
'[class.mat-mdc-tab-group-inverted-header]': 'headerPosition === "below"',
|
||
|
|
'[class.mat-mdc-tab-group-stretch-tabs]': 'stretchTabs',
|
||
|
|
'[attr.mat-align-tabs]': 'alignTabs',
|
||
|
|
'[style.--mat-tab-animation-duration]': 'animationDuration',
|
||
|
|
},
|
||
|
|
imports: [
|
||
|
|
MatTabHeader,
|
||
|
|
MatTabLabelWrapper,
|
||
|
|
CdkMonitorFocus,
|
||
|
|
MatRipple,
|
||
|
|
CdkPortalOutlet,
|
||
|
|
MatTabBody,
|
||
|
|
],
|
||
|
|
})
|
||
|
|
export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDestroy {
|
||
|
|
readonly _elementRef = inject(ElementRef);
|
||
|
|
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||
|
|
_animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* All tabs inside the tab group. This includes tabs that belong to groups that are nested
|
||
|
|
* inside the current one. We filter out only the tabs that belong to this group in `_tabs`.
|
||
|
|
*/
|
||
|
|
@ContentChildren(MatTab, {descendants: true}) _allTabs: QueryList<MatTab>;
|
||
|
|
@ViewChild('tabBodyWrapper') _tabBodyWrapper: ElementRef;
|
||
|
|
@ViewChild('tabHeader') _tabHeader: MatTabHeader;
|
||
|
|
|
||
|
|
/** All of the tabs that belong to the group. */
|
||
|
|
_tabs: QueryList<MatTab> = new QueryList<MatTab>();
|
||
|
|
|
||
|
|
/** The tab index that should be selected after the content has been checked. */
|
||
|
|
private _indexToSelect: number | null = 0;
|
||
|
|
|
||
|
|
/** Index of the tab that was focused last. */
|
||
|
|
private _lastFocusedTabIndex: number | null = null;
|
||
|
|
|
||
|
|
/** Snapshot of the height of the tab body wrapper before another tab is activated. */
|
||
|
|
private _tabBodyWrapperHeight: number = 0;
|
||
|
|
|
||
|
|
/** Subscription to tabs being added/removed. */
|
||
|
|
private _tabsSubscription = Subscription.EMPTY;
|
||
|
|
|
||
|
|
/** Subscription to changes in the tab labels. */
|
||
|
|
private _tabLabelSubscription = Subscription.EMPTY;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Theme color of the tab group. This API is supported in M2 themes only, it
|
||
|
|
* has no effect in M3 themes.
|
||
|
|
*
|
||
|
|
* For information on applying color variants in M3, see
|
||
|
|
* https://material.angular.io/guide/theming#using-component-color-variants.
|
||
|
|
*/
|
||
|
|
@Input()
|
||
|
|
color: ThemePalette;
|
||
|
|
|
||
|
|
/** Whether the ink bar should fit its width to the size of the tab label content. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
get fitInkBarToContent(): boolean {
|
||
|
|
return this._fitInkBarToContent;
|
||
|
|
}
|
||
|
|
set fitInkBarToContent(value: boolean) {
|
||
|
|
this._fitInkBarToContent = value;
|
||
|
|
this._changeDetectorRef.markForCheck();
|
||
|
|
}
|
||
|
|
private _fitInkBarToContent = false;
|
||
|
|
|
||
|
|
/** Whether tabs should be stretched to fill the header. */
|
||
|
|
@Input({alias: 'mat-stretch-tabs', transform: booleanAttribute})
|
||
|
|
stretchTabs: boolean = true;
|
||
|
|
|
||
|
|
/** Alignment for tabs label. */
|
||
|
|
@Input({alias: 'mat-align-tabs'})
|
||
|
|
alignTabs: string | null = null;
|
||
|
|
|
||
|
|
/** Whether the tab group should grow to the size of the active tab. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
dynamicHeight: boolean = false;
|
||
|
|
|
||
|
|
/** The index of the active tab. */
|
||
|
|
@Input({transform: numberAttribute})
|
||
|
|
get selectedIndex(): number | null {
|
||
|
|
return this._selectedIndex;
|
||
|
|
}
|
||
|
|
set selectedIndex(value: number) {
|
||
|
|
this._indexToSelect = isNaN(value) ? null : value;
|
||
|
|
}
|
||
|
|
private _selectedIndex: number | null = null;
|
||
|
|
|
||
|
|
/** Position of the tab header. */
|
||
|
|
@Input() headerPosition: MatTabHeaderPosition = 'above';
|
||
|
|
|
||
|
|
/** Duration for the tab animation. Will be normalized to milliseconds if no units are set. */
|
||
|
|
@Input()
|
||
|
|
get animationDuration(): string {
|
||
|
|
return this._animationDuration;
|
||
|
|
}
|
||
|
|
set animationDuration(value: string | number) {
|
||
|
|
const stringValue = value + '';
|
||
|
|
this._animationDuration = /^\d+$/.test(stringValue) ? value + 'ms' : stringValue;
|
||
|
|
}
|
||
|
|
private _animationDuration: string;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* `tabindex` to be set on the inner element that wraps the tab content. Can be used for improved
|
||
|
|
* accessibility when the tab does not have focusable elements or if it has scrollable content.
|
||
|
|
* The `tabindex` will be removed automatically for inactive tabs.
|
||
|
|
* Read more at https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html
|
||
|
|
*/
|
||
|
|
@Input({transform: numberAttribute})
|
||
|
|
get contentTabIndex(): number | null {
|
||
|
|
return this._contentTabIndex;
|
||
|
|
}
|
||
|
|
|
||
|
|
set contentTabIndex(value: number) {
|
||
|
|
this._contentTabIndex = isNaN(value) ? null : value;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _contentTabIndex: number | null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Whether pagination should be disabled. This can be used to avoid unnecessary
|
||
|
|
* layout recalculations if it's known that pagination won't be required.
|
||
|
|
*/
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
disablePagination: boolean = false;
|
||
|
|
|
||
|
|
/** Whether ripples in the tab group are disabled. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
disableRipple: boolean = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* By default tabs remove their content from the DOM while it's off-screen.
|
||
|
|
* Setting this to `true` will keep it in the DOM which will prevent elements
|
||
|
|
* like iframes and videos from reloading next time it comes back into the view.
|
||
|
|
*/
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
preserveContent: boolean = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Theme color of the background of the tab group. This API is supported in M2 themes only, it
|
||
|
|
* has no effect in M3 themes.
|
||
|
|
*
|
||
|
|
* For information on applying color variants in M3, see
|
||
|
|
* https://material.angular.io/guide/theming#using-component-color-variants.
|
||
|
|
*
|
||
|
|
* @deprecated The background color should be customized through Sass theming APIs.
|
||
|
|
* @breaking-change 20.0.0 Remove this input
|
||
|
|
*/
|
||
|
|
@Input()
|
||
|
|
get backgroundColor(): ThemePalette {
|
||
|
|
return this._backgroundColor;
|
||
|
|
}
|
||
|
|
|
||
|
|
set backgroundColor(value: ThemePalette) {
|
||
|
|
if (!ENABLE_BACKGROUND_INPUT) {
|
||
|
|
throw new Error(`mat-tab-group background color must be set through the Sass theming API`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const classList: DOMTokenList = this._elementRef.nativeElement.classList;
|
||
|
|
|
||
|
|
classList.remove('mat-tabs-with-background', `mat-background-${this.backgroundColor}`);
|
||
|
|
|
||
|
|
if (value) {
|
||
|
|
classList.add('mat-tabs-with-background', `mat-background-${value}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._backgroundColor = value;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _backgroundColor: ThemePalette;
|
||
|
|
|
||
|
|
/** Aria label of the inner `tablist` of the group. */
|
||
|
|
@Input('aria-label') ariaLabel: string;
|
||
|
|
|
||
|
|
/** Sets the `aria-labelledby` of the inner `tablist` of the group. */
|
||
|
|
@Input('aria-labelledby') ariaLabelledby: string;
|
||
|
|
|
||
|
|
/** Output to enable support for two-way binding on `[(selectedIndex)]` */
|
||
|
|
@Output() readonly selectedIndexChange: EventEmitter<number> = new EventEmitter<number>();
|
||
|
|
|
||
|
|
/** Event emitted when focus has changed within a tab group. */
|
||
|
|
@Output() readonly focusChange: EventEmitter<MatTabChangeEvent> =
|
||
|
|
new EventEmitter<MatTabChangeEvent>();
|
||
|
|
|
||
|
|
/** Event emitted when the body animation has completed */
|
||
|
|
@Output() readonly animationDone: EventEmitter<void> = new EventEmitter<void>();
|
||
|
|
|
||
|
|
/** Event emitted when the tab selection has changed. */
|
||
|
|
@Output() readonly selectedTabChange: EventEmitter<MatTabChangeEvent> =
|
||
|
|
new EventEmitter<MatTabChangeEvent>(true);
|
||
|
|
|
||
|
|
private _groupId: string;
|
||
|
|
|
||
|
|
/** Whether the tab group is rendered on the server. */
|
||
|
|
protected _isServer: boolean = !inject(Platform).isBrowser;
|
||
|
|
|
||
|
|
constructor(...args: unknown[]);
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
const defaultConfig = inject<MatTabsConfig>(MAT_TABS_CONFIG, {optional: true});
|
||
|
|
|
||
|
|
this._groupId = inject(_IdGenerator).getId('mat-tab-group-');
|
||
|
|
this.animationDuration =
|
||
|
|
defaultConfig && defaultConfig.animationDuration ? defaultConfig.animationDuration : '500ms';
|
||
|
|
this.disablePagination =
|
||
|
|
defaultConfig && defaultConfig.disablePagination != null
|
||
|
|
? defaultConfig.disablePagination
|
||
|
|
: false;
|
||
|
|
this.dynamicHeight =
|
||
|
|
defaultConfig && defaultConfig.dynamicHeight != null ? defaultConfig.dynamicHeight : false;
|
||
|
|
if (defaultConfig?.contentTabIndex != null) {
|
||
|
|
this.contentTabIndex = defaultConfig.contentTabIndex;
|
||
|
|
}
|
||
|
|
this.preserveContent = !!defaultConfig?.preserveContent;
|
||
|
|
this.fitInkBarToContent =
|
||
|
|
defaultConfig && defaultConfig.fitInkBarToContent != null
|
||
|
|
? defaultConfig.fitInkBarToContent
|
||
|
|
: false;
|
||
|
|
this.stretchTabs =
|
||
|
|
defaultConfig && defaultConfig.stretchTabs != null ? defaultConfig.stretchTabs : true;
|
||
|
|
this.alignTabs =
|
||
|
|
defaultConfig && defaultConfig.alignTabs != null ? defaultConfig.alignTabs : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* After the content is checked, this component knows what tabs have been defined
|
||
|
|
* and what the selected index should be. This is where we can know exactly what position
|
||
|
|
* each tab should be in according to the new selected index, and additionally we know how
|
||
|
|
* a new selected tab should transition in (from the left or right).
|
||
|
|
*/
|
||
|
|
ngAfterContentChecked() {
|
||
|
|
// Don't clamp the `indexToSelect` immediately in the setter because it can happen that
|
||
|
|
// the amount of tabs changes before the actual change detection runs.
|
||
|
|
const indexToSelect = (this._indexToSelect = this._clampTabIndex(this._indexToSelect));
|
||
|
|
|
||
|
|
// If there is a change in selected index, emit a change event. Should not trigger if
|
||
|
|
// the selected index has not yet been initialized.
|
||
|
|
if (this._selectedIndex != indexToSelect) {
|
||
|
|
const isFirstRun = this._selectedIndex == null;
|
||
|
|
|
||
|
|
if (!isFirstRun) {
|
||
|
|
this.selectedTabChange.emit(this._createChangeEvent(indexToSelect));
|
||
|
|
// Preserve the height so page doesn't scroll up during tab change.
|
||
|
|
// Fixes https://stackblitz.com/edit/mat-tabs-scroll-page-top-on-tab-change
|
||
|
|
const wrapper = this._tabBodyWrapper.nativeElement;
|
||
|
|
wrapper.style.minHeight = wrapper.clientHeight + 'px';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Changing these values after change detection has run
|
||
|
|
// since the checked content may contain references to them.
|
||
|
|
Promise.resolve().then(() => {
|
||
|
|
this._tabs.forEach((tab, index) => (tab.isActive = index === indexToSelect));
|
||
|
|
|
||
|
|
if (!isFirstRun) {
|
||
|
|
this.selectedIndexChange.emit(indexToSelect);
|
||
|
|
// Clear the min-height, this was needed during tab change to avoid
|
||
|
|
// unnecessary scrolling.
|
||
|
|
this._tabBodyWrapper.nativeElement.style.minHeight = '';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Setup the position for each tab and optionally setup an origin on the next selected tab.
|
||
|
|
this._tabs.forEach((tab: MatTab, index: number) => {
|
||
|
|
tab.position = index - indexToSelect;
|
||
|
|
|
||
|
|
// If there is already a selected tab, then set up an origin for the next selected tab
|
||
|
|
// if it doesn't have one already.
|
||
|
|
if (this._selectedIndex != null && tab.position == 0 && !tab.origin) {
|
||
|
|
tab.origin = indexToSelect - this._selectedIndex;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (this._selectedIndex !== indexToSelect) {
|
||
|
|
this._selectedIndex = indexToSelect;
|
||
|
|
this._lastFocusedTabIndex = null;
|
||
|
|
this._changeDetectorRef.markForCheck();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ngAfterContentInit() {
|
||
|
|
this._subscribeToAllTabChanges();
|
||
|
|
this._subscribeToTabLabels();
|
||
|
|
|
||
|
|
// Subscribe to changes in the amount of tabs, in order to be
|
||
|
|
// able to re-render the content as new tabs are added or removed.
|
||
|
|
this._tabsSubscription = this._tabs.changes.subscribe(() => {
|
||
|
|
const indexToSelect = this._clampTabIndex(this._indexToSelect);
|
||
|
|
|
||
|
|
// Maintain the previously-selected tab if a new tab is added or removed and there is no
|
||
|
|
// explicit change that selects a different tab.
|
||
|
|
if (indexToSelect === this._selectedIndex) {
|
||
|
|
const tabs = this._tabs.toArray();
|
||
|
|
let selectedTab: MatTab | undefined;
|
||
|
|
|
||
|
|
for (let i = 0; i < tabs.length; i++) {
|
||
|
|
if (tabs[i].isActive) {
|
||
|
|
// Assign both to the `_indexToSelect` and `_selectedIndex` so we don't fire a changed
|
||
|
|
// event, otherwise the consumer may end up in an infinite loop in some edge cases like
|
||
|
|
// adding a tab within the `selectedIndexChange` event.
|
||
|
|
this._indexToSelect = this._selectedIndex = i;
|
||
|
|
this._lastFocusedTabIndex = null;
|
||
|
|
selectedTab = tabs[i];
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If we haven't found an active tab and a tab exists at the selected index, it means
|
||
|
|
// that the active tab was swapped out. Since this won't be picked up by the rendering
|
||
|
|
// loop in `ngAfterContentChecked`, we need to sync it up manually.
|
||
|
|
if (!selectedTab && tabs[indexToSelect]) {
|
||
|
|
Promise.resolve().then(() => {
|
||
|
|
tabs[indexToSelect].isActive = true;
|
||
|
|
this.selectedTabChange.emit(this._createChangeEvent(indexToSelect));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
this._changeDetectorRef.markForCheck();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Listens to changes in all of the tabs. */
|
||
|
|
private _subscribeToAllTabChanges() {
|
||
|
|
// Since we use a query with `descendants: true` to pick up the tabs, we may end up catching
|
||
|
|
// some that are inside of nested tab groups. We filter them out manually by checking that
|
||
|
|
// the closest group to the tab is the current one.
|
||
|
|
this._allTabs.changes.pipe(startWith(this._allTabs)).subscribe((tabs: QueryList<MatTab>) => {
|
||
|
|
this._tabs.reset(
|
||
|
|
tabs.filter(tab => {
|
||
|
|
return tab._closestTabGroup === this || !tab._closestTabGroup;
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
this._tabs.notifyOnChanges();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
ngOnDestroy() {
|
||
|
|
this._tabs.destroy();
|
||
|
|
this._tabsSubscription.unsubscribe();
|
||
|
|
this._tabLabelSubscription.unsubscribe();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Re-aligns the ink bar to the selected tab element. */
|
||
|
|
realignInkBar() {
|
||
|
|
if (this._tabHeader) {
|
||
|
|
this._tabHeader._alignInkBarToSelectedTab();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Recalculates the tab group's pagination dimensions.
|
||
|
|
*
|
||
|
|
* 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() {
|
||
|
|
if (this._tabHeader) {
|
||
|
|
this._tabHeader.updatePagination();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sets focus to a particular tab.
|
||
|
|
* @param index Index of the tab to be focused.
|
||
|
|
*/
|
||
|
|
focusTab(index: number) {
|
||
|
|
const header = this._tabHeader;
|
||
|
|
|
||
|
|
if (header) {
|
||
|
|
header.focusIndex = index;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_focusChanged(index: number) {
|
||
|
|
this._lastFocusedTabIndex = index;
|
||
|
|
this.focusChange.emit(this._createChangeEvent(index));
|
||
|
|
}
|
||
|
|
|
||
|
|
private _createChangeEvent(index: number): MatTabChangeEvent {
|
||
|
|
const event = new MatTabChangeEvent();
|
||
|
|
event.index = index;
|
||
|
|
if (this._tabs && this._tabs.length) {
|
||
|
|
event.tab = this._tabs.toArray()[index];
|
||
|
|
}
|
||
|
|
return event;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Subscribes to changes in the tab labels. This is needed, because the @Input for the label is
|
||
|
|
* on the MatTab component, whereas the data binding is inside the MatTabGroup. In order for the
|
||
|
|
* binding to be updated, we need to subscribe to changes in it and trigger change detection
|
||
|
|
* manually.
|
||
|
|
*/
|
||
|
|
private _subscribeToTabLabels() {
|
||
|
|
if (this._tabLabelSubscription) {
|
||
|
|
this._tabLabelSubscription.unsubscribe();
|
||
|
|
}
|
||
|
|
|
||
|
|
this._tabLabelSubscription = merge(...this._tabs.map(tab => tab._stateChanges)).subscribe(() =>
|
||
|
|
this._changeDetectorRef.markForCheck(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Clamps the given index to the bounds of 0 and the tabs length. */
|
||
|
|
private _clampTabIndex(index: number | null): number {
|
||
|
|
// Note the `|| 0`, which ensures that values like NaN can't get through
|
||
|
|
// and which would otherwise throw the component into an infinite loop
|
||
|
|
// (since Math.max(NaN, 0) === NaN).
|
||
|
|
return Math.min(this._tabs.length - 1, Math.max(index || 0, 0));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Returns a unique id for each tab label element */
|
||
|
|
_getTabLabelId(i: number): string {
|
||
|
|
return `${this._groupId}-label-${i}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Returns a unique id for each tab content element */
|
||
|
|
_getTabContentId(i: number): string {
|
||
|
|
return `${this._groupId}-content-${i}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sets the height of the body wrapper to the height of the activating tab if dynamic
|
||
|
|
* height property is true.
|
||
|
|
*/
|
||
|
|
_setTabBodyWrapperHeight(tabHeight: number): void {
|
||
|
|
if (!this.dynamicHeight || !this._tabBodyWrapperHeight) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const wrapper: HTMLElement = this._tabBodyWrapper.nativeElement;
|
||
|
|
|
||
|
|
wrapper.style.height = this._tabBodyWrapperHeight + 'px';
|
||
|
|
|
||
|
|
// This conditional forces the browser to paint the height so that
|
||
|
|
// the animation to the new height can have an origin.
|
||
|
|
if (this._tabBodyWrapper.nativeElement.offsetHeight) {
|
||
|
|
wrapper.style.height = tabHeight + 'px';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Removes the height of the tab body wrapper. */
|
||
|
|
_removeTabBodyWrapperHeight(): void {
|
||
|
|
const wrapper = this._tabBodyWrapper.nativeElement;
|
||
|
|
this._tabBodyWrapperHeight = wrapper.clientHeight;
|
||
|
|
wrapper.style.height = '';
|
||
|
|
this.animationDone.emit();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Handle click events, setting new selected index if appropriate. */
|
||
|
|
_handleClick(tab: MatTab, tabHeader: MatTabGroupBaseHeader, index: number) {
|
||
|
|
tabHeader.focusIndex = index;
|
||
|
|
|
||
|
|
if (!tab.disabled) {
|
||
|
|
this.selectedIndex = index;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Retrieves the tabindex for the tab. */
|
||
|
|
_getTabIndex(index: number): number {
|
||
|
|
const targetIndex = this._lastFocusedTabIndex ?? this.selectedIndex;
|
||
|
|
return index === targetIndex ? 0 : -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Callback for when the focused state of a tab has changed. */
|
||
|
|
_tabFocusChanged(focusOrigin: FocusOrigin, index: number) {
|
||
|
|
// Mouse/touch focus happens during the `mousedown`/`touchstart` phase which
|
||
|
|
// can cause the tab to be moved out from under the pointer, interrupting the
|
||
|
|
// click sequence (see #21898). We don't need to scroll the tab into view for
|
||
|
|
// such cases anyway, because it will be done when the tab becomes selected.
|
||
|
|
if (focusOrigin && focusOrigin !== 'mouse' && focusOrigin !== 'touch') {
|
||
|
|
this._tabHeader.focusIndex = index;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** A simple change event emitted on focus or selection changes. */
|
||
|
|
export class MatTabChangeEvent {
|
||
|
|
/** Index of the currently-selected tab. */
|
||
|
|
index: number;
|
||
|
|
/** Reference to the currently-selected tab. */
|
||
|
|
tab: MatTab;
|
||
|
|
}
|