@angular/material
Version:
Angular Material
974 lines (966 loc) • 153 kB
JavaScript
import * as i1$2 from '@angular/common';
import { DOCUMENT, CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { forwardRef, Directive, Inject, EventEmitter, Optional, Output, Input, Component, ViewEncapsulation, ChangeDetectionStrategy, ViewChild, InjectionToken, TemplateRef, ContentChild, ContentChildren, QueryList, Attribute, NgModule } from '@angular/core';
import * as i5 from '@angular/material/core';
import { mixinDisabled, mixinColor, mixinDisableRipple, mixinTabIndex, MAT_RIPPLE_GLOBAL_OPTIONS, MatCommonModule, MatRippleModule } from '@angular/material/core';
import * as i2 from '@angular/cdk/portal';
import { CdkPortalOutlet, CdkPortal, TemplatePortal, PortalModule } from '@angular/cdk/portal';
import * as i5$1 from '@angular/cdk/observers';
import { ObserversModule } from '@angular/cdk/observers';
import * as i4 from '@angular/cdk/a11y';
import { FocusKeyManager, A11yModule } from '@angular/cdk/a11y';
import * as i1 from '@angular/cdk/bidi';
import { Subscription, Subject, fromEvent, of, merge, EMPTY, Observable, timer, BehaviorSubject } from 'rxjs';
import { startWith, distinctUntilChanged, takeUntil, take, switchMap, skip, filter } from 'rxjs/operators';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
import * as i1$1 from '@angular/cdk/scrolling';
import * as i3 from '@angular/cdk/platform';
import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations';
import { hasModifierKey, SPACE, ENTER } from '@angular/cdk/keycodes';
/**
* @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
*/
/**
* Animations used by the Material tabs.
* @docs-private
*/
const matTabsAnimations = {
/** Animation translates a tab along the X axis. */
translateTab: trigger('translateTab', [
// Transitions to `none` instead of 0, because some browsers might blur the content.
state('center, void, left-origin-center, right-origin-center', style({ transform: 'none' })),
// If the tab is either on the left or right, we additionally add a `min-height` of 1px
// in order to ensure that the element has a height before its state changes. This is
// necessary because Chrome does seem to skip the transition in RTL mode if the element does
// not have a static height and is not rendered. See related issue: #9465
state('left', style({
transform: 'translate3d(-100%, 0, 0)',
minHeight: '1px',
// Normally this is redundant since we detach the content from the DOM, but if the user
// opted into keeping the content in the DOM, we have to hide it so it isn't focusable.
visibility: 'hidden',
})),
state('right', style({
transform: 'translate3d(100%, 0, 0)',
minHeight: '1px',
visibility: 'hidden',
})),
transition('* => left, * => right, left => center, right => center', animate('{{animationDuration}} cubic-bezier(0.35, 0, 0.25, 1)')),
transition('void => left-origin-center', [
style({ transform: 'translate3d(-100%, 0, 0)', visibility: 'hidden' }),
animate('{{animationDuration}} cubic-bezier(0.35, 0, 0.25, 1)'),
]),
transition('void => right-origin-center', [
style({ transform: 'translate3d(100%, 0, 0)', visibility: 'hidden' }),
animate('{{animationDuration}} cubic-bezier(0.35, 0, 0.25, 1)'),
]),
]),
};
/**
* @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
*/
/**
* The portal host directive for the contents of the tab.
* @docs-private
*/
class MatTabBodyPortal extends CdkPortalOutlet {
constructor(componentFactoryResolver, viewContainerRef, _host, _document) {
super(componentFactoryResolver, viewContainerRef, _document);
this._host = _host;
/** Subscription to events for when the tab body begins centering. */
this._centeringSub = Subscription.EMPTY;
/** Subscription to events for when the tab body finishes leaving from center position. */
this._leavingSub = Subscription.EMPTY;
}
/** Set initial visibility or set up subscription for changing visibility. */
ngOnInit() {
super.ngOnInit();
this._centeringSub = this._host._beforeCentering
.pipe(startWith(this._host._isCenterPosition(this._host._position)))
.subscribe((isCentering) => {
if (isCentering && !this.hasAttached()) {
this.attach(this._host._content);
}
});
this._leavingSub = this._host._afterLeavingCenter.subscribe(() => {
if (!this._host.preserveContent) {
this.detach();
}
});
}
/** Clean up centering subscription. */
ngOnDestroy() {
super.ngOnDestroy();
this._centeringSub.unsubscribe();
this._leavingSub.unsubscribe();
}
}
MatTabBodyPortal.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabBodyPortal, deps: [{ token: i0.ComponentFactoryResolver }, { token: i0.ViewContainerRef }, { token: forwardRef(() => MatTabBody) }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Directive });
MatTabBodyPortal.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: MatTabBodyPortal, selector: "[matTabBodyHost]", usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabBodyPortal, decorators: [{
type: Directive,
args: [{
selector: '[matTabBodyHost]',
}]
}], ctorParameters: function () { return [{ type: i0.ComponentFactoryResolver }, { type: i0.ViewContainerRef }, { type: MatTabBody, decorators: [{
type: Inject,
args: [forwardRef(() => MatTabBody)]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }]; } });
/**
* Base class with all of the `MatTabBody` functionality.
* @docs-private
*/
class _MatTabBodyBase {
constructor(_elementRef, _dir, changeDetectorRef) {
this._elementRef = _elementRef;
this._dir = _dir;
/** Subscription to the directionality change observable. */
this._dirChangeSubscription = Subscription.EMPTY;
/** Emits when an animation on the tab is complete. */
this._translateTabComplete = new Subject();
/** Event emitted when the tab begins to animate towards the center as the active tab. */
this._onCentering = new EventEmitter();
/** Event emitted before the centering of the tab begins. */
this._beforeCentering = new EventEmitter();
/** Event emitted before the centering of the tab begins. */
this._afterLeavingCenter = new EventEmitter();
/** Event emitted when the tab completes its animation towards the center. */
this._onCentered = new EventEmitter(true);
// Note that the default value will always be overwritten by `MatTabBody`, but we need one
// anyway to prevent the animations module from throwing an error if the body is used on its own.
/** Duration for the tab's animation. */
this.animationDuration = '500ms';
/** Whether the tab's content should be kept in the DOM while it's off-screen. */
this.preserveContent = false;
if (_dir) {
this._dirChangeSubscription = _dir.change.subscribe((dir) => {
this._computePositionAnimationState(dir);
changeDetectorRef.markForCheck();
});
}
// Ensure that we get unique animation events, because the `.done` callback can get
// invoked twice in some browsers. See https://github.com/angular/angular/issues/24084.
this._translateTabComplete
.pipe(distinctUntilChanged((x, y) => {
return x.fromState === y.fromState && x.toState === y.toState;
}))
.subscribe(event => {
// If the transition to the center is complete, emit an event.
if (this._isCenterPosition(event.toState) && this._isCenterPosition(this._position)) {
this._onCentered.emit();
}
if (this._isCenterPosition(event.fromState) && !this._isCenterPosition(this._position)) {
this._afterLeavingCenter.emit();
}
});
}
/** The shifted index position of the tab body, where zero represents the active center tab. */
set position(position) {
this._positionIndex = position;
this._computePositionAnimationState();
}
/**
* After initialized, check if the content is centered and has an origin. If so, set the
* special position states that transition the tab from the left or right before centering.
*/
ngOnInit() {
if (this._position == 'center' && this.origin != null) {
this._position = this._computePositionFromOrigin(this.origin);
}
}
ngOnDestroy() {
this._dirChangeSubscription.unsubscribe();
this._translateTabComplete.complete();
}
_onTranslateTabStarted(event) {
const isCentering = this._isCenterPosition(event.toState);
this._beforeCentering.emit(isCentering);
if (isCentering) {
this._onCentering.emit(this._elementRef.nativeElement.clientHeight);
}
}
/** The text direction of the containing app. */
_getLayoutDirection() {
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
}
/** Whether the provided position state is considered center, regardless of origin. */
_isCenterPosition(position) {
return (position == 'center' || position == 'left-origin-center' || position == 'right-origin-center');
}
/** Computes the position state that will be used for the tab-body animation trigger. */
_computePositionAnimationState(dir = this._getLayoutDirection()) {
if (this._positionIndex < 0) {
this._position = dir == 'ltr' ? 'left' : 'right';
}
else if (this._positionIndex > 0) {
this._position = dir == 'ltr' ? 'right' : 'left';
}
else {
this._position = 'center';
}
}
/**
* Computes the position state based on the specified origin position. This is used if the
* tab is becoming visible immediately after creation.
*/
_computePositionFromOrigin(origin) {
const dir = this._getLayoutDirection();
if ((dir == 'ltr' && origin <= 0) || (dir == 'rtl' && origin > 0)) {
return 'left-origin-center';
}
return 'right-origin-center';
}
}
_MatTabBodyBase.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatTabBodyBase, deps: [{ token: i0.ElementRef }, { token: i1.Directionality, optional: true }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive });
_MatTabBodyBase.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: _MatTabBodyBase, inputs: { _content: ["content", "_content"], origin: "origin", animationDuration: "animationDuration", preserveContent: "preserveContent", position: "position" }, outputs: { _onCentering: "_onCentering", _beforeCentering: "_beforeCentering", _afterLeavingCenter: "_afterLeavingCenter", _onCentered: "_onCentered" }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatTabBodyBase, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1.Directionality, decorators: [{
type: Optional
}] }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { _onCentering: [{
type: Output
}], _beforeCentering: [{
type: Output
}], _afterLeavingCenter: [{
type: Output
}], _onCentered: [{
type: Output
}], _content: [{
type: Input,
args: ['content']
}], origin: [{
type: Input
}], animationDuration: [{
type: Input
}], preserveContent: [{
type: Input
}], position: [{
type: Input
}] } });
/**
* Wrapper for the contents of a tab.
* @docs-private
*/
class MatTabBody extends _MatTabBodyBase {
constructor(elementRef, dir, changeDetectorRef) {
super(elementRef, dir, changeDetectorRef);
}
}
MatTabBody.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabBody, deps: [{ token: i0.ElementRef }, { token: i1.Directionality, optional: true }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
MatTabBody.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: MatTabBody, selector: "mat-tab-body", host: { classAttribute: "mat-mdc-tab-body" }, viewQueries: [{ propertyName: "_portalHost", first: true, predicate: CdkPortalOutlet, descendants: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"mat-mdc-tab-body-content\" #content\n [@translateTab]=\"{\n value: _position,\n params: {animationDuration: animationDuration}\n }\"\n (@translateTab.start)=\"_onTranslateTabStarted($event)\"\n (@translateTab.done)=\"_translateTabComplete.next($event)\"\n cdkScrollable>\n <ng-template matTabBodyHost></ng-template>\n</div>\n", styles: [".mat-mdc-tab-body{top:0;left:0;right:0;bottom:0;position:absolute;display:block;overflow:hidden;outline:0;flex-basis:100%}.mat-mdc-tab-body.mat-mdc-tab-body-active{position:relative;overflow-x:hidden;overflow-y:auto;z-index:1;flex-grow:1}.mat-mdc-tab-group.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body.mat-mdc-tab-body-active{overflow-y:hidden}.mat-mdc-tab-body-content{height:100%;overflow:auto}.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body-content{overflow:hidden}.mat-mdc-tab-body-content[style*=\"visibility: hidden\"]{display:none}"], dependencies: [{ kind: "directive", type: MatTabBodyPortal, selector: "[matTabBodyHost]" }], animations: [matTabsAnimations.translateTab], changeDetection: i0.ChangeDetectionStrategy.Default, encapsulation: i0.ViewEncapsulation.None });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabBody, decorators: [{
type: Component,
args: [{ selector: 'mat-tab-body', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.Default, animations: [matTabsAnimations.translateTab], host: {
'class': 'mat-mdc-tab-body',
}, template: "<div class=\"mat-mdc-tab-body-content\" #content\n [@translateTab]=\"{\n value: _position,\n params: {animationDuration: animationDuration}\n }\"\n (@translateTab.start)=\"_onTranslateTabStarted($event)\"\n (@translateTab.done)=\"_translateTabComplete.next($event)\"\n cdkScrollable>\n <ng-template matTabBodyHost></ng-template>\n</div>\n", styles: [".mat-mdc-tab-body{top:0;left:0;right:0;bottom:0;position:absolute;display:block;overflow:hidden;outline:0;flex-basis:100%}.mat-mdc-tab-body.mat-mdc-tab-body-active{position:relative;overflow-x:hidden;overflow-y:auto;z-index:1;flex-grow:1}.mat-mdc-tab-group.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body.mat-mdc-tab-body-active{overflow-y:hidden}.mat-mdc-tab-body-content{height:100%;overflow:auto}.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body-content{overflow:hidden}.mat-mdc-tab-body-content[style*=\"visibility: hidden\"]{display:none}"] }]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1.Directionality, decorators: [{
type: Optional
}] }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { _portalHost: [{
type: ViewChild,
args: [CdkPortalOutlet]
}] } });
/**
* @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
*/
/**
* Injection token that can be used to reference instances of `MatTabContent`. It serves as
* alternative token to the actual `MatTabContent` class which could cause unnecessary
* retention of the class and its directive metadata.
*/
const MAT_TAB_CONTENT = new InjectionToken('MatTabContent');
/** Decorates the `ng-template` tags and reads out the template from it. */
class MatTabContent {
constructor(/** Content for the tab. */ template) {
this.template = template;
}
}
MatTabContent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabContent, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
MatTabContent.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: MatTabContent, selector: "[matTabContent]", providers: [{ provide: MAT_TAB_CONTENT, useExisting: MatTabContent }], ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabContent, decorators: [{
type: Directive,
args: [{
selector: '[matTabContent]',
providers: [{ provide: MAT_TAB_CONTENT, useExisting: MatTabContent }],
}]
}], ctorParameters: function () { return [{ type: i0.TemplateRef }]; } });
/**
* @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
*/
/**
* Injection token that can be used to reference instances of `MatTabLabel`. It serves as
* alternative token to the actual `MatTabLabel` class which could cause unnecessary
* retention of the class and its directive metadata.
*/
const MAT_TAB_LABEL = new InjectionToken('MatTabLabel');
/**
* Used to provide a tab label to a tab without causing a circular dependency.
* @docs-private
*/
const MAT_TAB = new InjectionToken('MAT_TAB');
/** Used to flag tab labels for use with the portal directive */
class MatTabLabel extends CdkPortal {
constructor(templateRef, viewContainerRef, _closestTab) {
super(templateRef, viewContainerRef);
this._closestTab = _closestTab;
}
}
MatTabLabel.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabLabel, deps: [{ token: i0.TemplateRef }, { token: i0.ViewContainerRef }, { token: MAT_TAB, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
MatTabLabel.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: MatTabLabel, selector: "[mat-tab-label], [matTabLabel]", providers: [{ provide: MAT_TAB_LABEL, useExisting: MatTabLabel }], usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabLabel, decorators: [{
type: Directive,
args: [{
selector: '[mat-tab-label], [matTabLabel]',
providers: [{ provide: MAT_TAB_LABEL, useExisting: MatTabLabel }],
}]
}], ctorParameters: function () { return [{ type: i0.TemplateRef }, { type: i0.ViewContainerRef }, { type: undefined, decorators: [{
type: Inject,
args: [MAT_TAB]
}, {
type: Optional
}] }]; } });
/**
* @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
*/
/** Class that is applied when a tab indicator is active. */
const ACTIVE_CLASS = 'mdc-tab-indicator--active';
/** Class that is applied when the tab indicator should not transition. */
const NO_TRANSITION_CLASS = 'mdc-tab-indicator--no-transition';
/**
* Abstraction around the MDC tab indicator that acts as the tab header's ink bar.
* @docs-private
*/
class MatInkBar {
constructor(_items) {
this._items = _items;
}
/** Hides the ink bar. */
hide() {
this._items.forEach(item => item.deactivateInkBar());
}
/** Aligns the ink bar to a DOM node. */
alignToElement(element) {
const correspondingItem = this._items.find(item => item.elementRef.nativeElement === element);
const currentItem = this._currentItem;
currentItem?.deactivateInkBar();
if (correspondingItem) {
const clientRect = currentItem?.elementRef.nativeElement.getBoundingClientRect?.();
// The ink bar won't animate unless we give it the `ClientRect` of the previous item.
correspondingItem.activateInkBar(clientRect);
this._currentItem = correspondingItem;
}
}
}
/**
* Mixin that can be used to apply the `MatInkBarItem` behavior to a class.
* Base on MDC's `MDCSlidingTabIndicatorFoundation`:
* https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-tab-indicator/sliding-foundation.ts
* @docs-private
*/
function mixinInkBarItem(base) {
return class extends base {
constructor(...args) {
super(...args);
this._fitToContent = false;
}
/** Whether the ink bar should fit to the entire tab or just its content. */
get fitInkBarToContent() {
return this._fitToContent;
}
set fitInkBarToContent(v) {
const newValue = coerceBooleanProperty(v);
if (this._fitToContent !== newValue) {
this._fitToContent = newValue;
if (this._inkBarElement) {
this._appendInkBarElement();
}
}
}
/** Aligns the ink bar to the current item. */
activateInkBar(previousIndicatorClientRect) {
const element = this.elementRef.nativeElement;
// Early exit if no indicator is present to handle cases where an indicator
// may be activated without a prior indicator state
if (!previousIndicatorClientRect ||
!element.getBoundingClientRect ||
!this._inkBarContentElement) {
element.classList.add(ACTIVE_CLASS);
return;
}
// This animation uses the FLIP approach. You can read more about it at the link below:
// https://aerotwist.com/blog/flip-your-animations/
// Calculate the dimensions based on the dimensions of the previous indicator
const currentClientRect = element.getBoundingClientRect();
const widthDelta = previousIndicatorClientRect.width / currentClientRect.width;
const xPosition = previousIndicatorClientRect.left - currentClientRect.left;
element.classList.add(NO_TRANSITION_CLASS);
this._inkBarContentElement.style.setProperty('transform', `translateX(${xPosition}px) scaleX(${widthDelta})`);
// Force repaint before updating classes and transform to ensure the transform properly takes effect
element.getBoundingClientRect();
element.classList.remove(NO_TRANSITION_CLASS);
element.classList.add(ACTIVE_CLASS);
this._inkBarContentElement.style.setProperty('transform', '');
}
/** Removes the ink bar from the current item. */
deactivateInkBar() {
this.elementRef.nativeElement.classList.remove(ACTIVE_CLASS);
}
/** Initializes the foundation. */
ngOnInit() {
this._createInkBarElement();
}
/** Destroys the foundation. */
ngOnDestroy() {
this._inkBarElement?.remove();
this._inkBarElement = this._inkBarContentElement = null;
}
/** Creates and appends the ink bar element. */
_createInkBarElement() {
const documentNode = this.elementRef.nativeElement.ownerDocument || document;
this._inkBarElement = documentNode.createElement('span');
this._inkBarContentElement = documentNode.createElement('span');
this._inkBarElement.className = 'mdc-tab-indicator';
this._inkBarContentElement.className =
'mdc-tab-indicator__content mdc-tab-indicator__content--underline';
this._inkBarElement.appendChild(this._inkBarContentElement);
this._appendInkBarElement();
}
/**
* Appends the ink bar to the tab host element or content, depending on whether
* the ink bar should fit to content.
*/
_appendInkBarElement() {
if (!this._inkBarElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('Ink bar element has not been created and cannot be appended');
}
const parentElement = this._fitToContent
? this.elementRef.nativeElement.querySelector('.mdc-tab__content')
: this.elementRef.nativeElement;
if (!parentElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('Missing element to host the ink bar');
}
parentElement.appendChild(this._inkBarElement);
}
};
}
/**
* The default positioner function for the MatInkBar.
* @docs-private
*/
function _MAT_INK_BAR_POSITIONER_FACTORY() {
const method = (element) => ({
left: element ? (element.offsetLeft || 0) + 'px' : '0',
width: element ? (element.offsetWidth || 0) + 'px' : '0',
});
return method;
}
/** Injection token for the MatInkBar's Positioner. */
const _MAT_INK_BAR_POSITIONER = new InjectionToken('MatInkBarPositioner', {
providedIn: 'root',
factory: _MAT_INK_BAR_POSITIONER_FACTORY,
});
/**
* @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
*/
// Boilerplate for applying mixins to MatTabLabelWrapper.
/** @docs-private */
const _MatTabLabelWrapperMixinBase = mixinDisabled(class {
});
/**
* Used in the `mat-tab-group` view to display tab labels.
* @docs-private
*/
class _MatTabLabelWrapperBase extends _MatTabLabelWrapperMixinBase {
constructor(elementRef) {
super();
this.elementRef = elementRef;
}
/** Sets focus on the wrapper element */
focus() {
this.elementRef.nativeElement.focus();
}
getOffsetLeft() {
return this.elementRef.nativeElement.offsetLeft;
}
getOffsetWidth() {
return this.elementRef.nativeElement.offsetWidth;
}
}
_MatTabLabelWrapperBase.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatTabLabelWrapperBase, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
_MatTabLabelWrapperBase.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: _MatTabLabelWrapperBase, usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatTabLabelWrapperBase, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }]; } });
const _MatTabLabelWrapperBaseWithInkBarItem = mixinInkBarItem(_MatTabLabelWrapperBase);
/**
* Used in the `mat-tab-group` view to display tab labels.
* @docs-private
*/
class MatTabLabelWrapper extends _MatTabLabelWrapperBaseWithInkBarItem {
}
MatTabLabelWrapper.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabLabelWrapper, deps: null, target: i0.ɵɵFactoryTarget.Directive });
MatTabLabelWrapper.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: MatTabLabelWrapper, selector: "[matTabLabelWrapper]", inputs: { disabled: "disabled", fitInkBarToContent: "fitInkBarToContent" }, host: { properties: { "class.mat-mdc-tab-disabled": "disabled", "attr.aria-disabled": "!!disabled" } }, usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTabLabelWrapper, decorators: [{
type: Directive,
args: [{
selector: '[matTabLabelWrapper]',
inputs: ['disabled', 'fitInkBarToContent'],
host: {
'[class.mat-mdc-tab-disabled]': 'disabled',
'[attr.aria-disabled]': '!!disabled',
},
}]
}] });
/**
* @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
*/
// Boilerplate for applying mixins to MatTab.
/** @docs-private */
const _MatTabMixinBase = mixinDisabled(class {
});
/**
* Used to provide a tab group to a tab without causing a circular dependency.
* @docs-private
*/
const MAT_TAB_GROUP = new InjectionToken('MAT_TAB_GROUP');
/** @docs-private */
class _MatTabBase extends _MatTabMixinBase {
constructor(_viewContainerRef, _closestTabGroup) {
super();
this._viewContainerRef = _viewContainerRef;
this._closestTabGroup = _closestTabGroup;
/** Plain text label for the tab, used when there is no template label. */
this.textLabel = '';
/** Portal that will be the hosted content of the tab */
this._contentPortal = null;
/** Emits whenever the internal state of the tab changes. */
this._stateChanges = new Subject();
/**
* The relatively indexed position where 0 represents the center, negative is left, and positive
* represents the right.
*/
this.position = null;
/**
* The initial relatively index origin of the tab if it was created and selected after there
* was already a selected tab. Provides context of what position the tab should originate from.
*/
this.origin = null;
/**
* Whether the tab is currently active.
*/
this.isActive = false;
}
/** @docs-private */
get content() {
return this._contentPortal;
}
ngOnChanges(changes) {
if (changes.hasOwnProperty('textLabel') || changes.hasOwnProperty('disabled')) {
this._stateChanges.next();
}
}
ngOnDestroy() {
this._stateChanges.complete();
}
ngOnInit() {
this._contentPortal = new TemplatePortal(this._explicitContent || this._implicitContent, this._viewContainerRef);
}
/**
* This has been extracted to a util because of TS 4 and VE.
* View Engine doesn't support property rename inheritance.
* TS 4.0 doesn't allow properties to override accessors or vice-versa.
* @docs-private
*/
_setTemplateLabelInput(value) {
// Only update the label if the query managed to find one. This works around an issue where a
// user may have manually set `templateLabel` during creation mode, which would then get
// clobbered by `undefined` when the query resolves. Also note that we check that the closest
// tab matches the current one so that we don't pick up labels from nested tabs.
if (value && value._closestTab === this) {
this._templateLabel = value;
}
}
}
_MatTabBase.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatTabBase, deps: [{ token: i0.ViewContainerRef }, { token: MAT_TAB_GROUP, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
_MatTabBase.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: _MatTabBase, inputs: { textLabel: ["label", "textLabel"], ariaLabel: ["aria-label", "ariaLabel"], ariaLabelledby: ["aria-labelledby", "ariaLabelledby"], labelClass: "labelClass", bodyClass: "bodyClass" }, viewQueries: [{ propertyName: "_implicitContent", first: true, predicate: TemplateRef, descendants: true, static: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatTabBase, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ViewContainerRef }, { type: undefined, decorators: [{
type: Inject,
args: [MAT_TAB_GROUP]
}, {
type: Optional
}] }]; }, propDecorators: { _implicitContent: [{
type: ViewChild,
args: [TemplateRef, { static: true }]
}], textLabel: [{
type: Input,
args: ['label']
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], ariaLabelledby: [{
type: Input,
args: ['aria-labelledby']
}], labelClass: [{
type: Input
}], bodyClass: [{
type: Input
}] } });
class MatTab extends _MatTabBase {
/** Content for the tab label given by `<ng-template mat-tab-label>`. */
get templateLabel() {
return this._templateLabel;
}
set templateLabel(value) {
this._setTemplateLabelInput(value);
}
}
MatTab.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTab, deps: null, target: i0.ɵɵFactoryTarget.Component });
MatTab.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: MatTab, selector: "mat-tab", inputs: { disabled: "disabled" }, providers: [{ provide: MAT_TAB, useExisting: MatTab }], queries: [{ propertyName: "_explicitContent", first: true, predicate: MatTabContent, descendants: true, read: TemplateRef, static: true }, { propertyName: "templateLabel", first: true, predicate: MatTabLabel, descendants: true }], exportAs: ["matTab"], usesInheritance: true, ngImport: i0, template: "<!-- Create a template for the content of the <mat-tab> so that we can grab a reference to this\n TemplateRef and use it in a Portal to render the tab content in the appropriate place in the\n tab-group. -->\n<ng-template><ng-content></ng-content></ng-template>\n", changeDetection: i0.ChangeDetectionStrategy.Default, encapsulation: i0.ViewEncapsulation.None });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatTab, decorators: [{
type: Component,
args: [{ selector: 'mat-tab', inputs: ['disabled'], changeDetection: ChangeDetectionStrategy.Default, encapsulation: ViewEncapsulation.None, exportAs: 'matTab', providers: [{ provide: MAT_TAB, useExisting: MatTab }], template: "<!-- Create a template for the content of the <mat-tab> so that we can grab a reference to this\n TemplateRef and use it in a Portal to render the tab content in the appropriate place in the\n tab-group. -->\n<ng-template><ng-content></ng-content></ng-template>\n" }]
}], propDecorators: { _explicitContent: [{
type: ContentChild,
args: [MatTabContent, { read: TemplateRef, static: true }]
}], templateLabel: [{
type: ContentChild,
args: [MatTabLabel]
}] } });
/**
* @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
*/
/** Config used to bind passive event listeners */
const passiveEventListenerOptions = normalizePassiveListenerOptions({
passive: true,
});
/**
* 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;
/**
* Base class for a tab header that supported pagination.
* @docs-private
*/
class MatPaginatedTabHeader {
constructor(_elementRef, _changeDetectorRef, _viewportRuler, _dir, _ngZone, _platform, _animationMode) {
this._elementRef = _elementRef;
this._changeDetectorRef = _changeDetectorRef;
this._viewportRuler = _viewportRuler;
this._dir = _dir;
this._ngZone = _ngZone;
this._platform = _platform;
this._animationMode = _animationMode;
/** The distance in pixels that the tab labels should be translated to the left. */
this._scrollDistance = 0;
/** Whether the header should scroll to the selected index after the view has been checked. */
this._selectedIndexChanged = false;
/** Emits when the component is destroyed. */
this._destroyed = new Subject();
/** Whether the controls for pagination should be displayed */
this._showPaginationControls = false;
/** Whether the tab list can be scrolled more towards the end of the tab label list. */
this._disableScrollAfter = true;
/** Whether the tab list can be scrolled more towards the beginning of the tab label list. */
this._disableScrollBefore = true;
/** Stream that will stop the automated scrolling. */
this._stopScrolling = new Subject();
this._disablePagination = false;
this._selectedIndex = 0;
/** Event emitted when the option is selected. */
this.selectFocusedIndex = new EventEmitter();
/** Event emitted when a label is focused. */
this.indexFocused = new EventEmitter();
// 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();
});
});
}
/**
* Whether pagination should be disabled. This can be used to avoid unnecessary
* layout recalculations if it's known that pagination won't be required.
*/
get disablePagination() {
return this._disablePagination;
}
set disablePagination(value) {
this._disablePagination = coerceBooleanProperty(value);
}
/** The index of the active tab. */
get selectedIndex() {
return this._selectedIndex;
}
set selectedIndex(value) {
value = coerceNumberProperty(value);
if (this._selectedIndex != value) {
this._selectedIndexChanged = true;
this._selectedIndex = value;
if (this._keyManager) {
this._keyManager.updateActiveItem(value);
}
}
}
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 : of('ltr');
const resize = this._viewportRuler.change(150);
const realign = () => {
this.updatePagination();
this._alignInkBarToSelectedTab();
};
this._keyManager = new FocusKeyManager(this._items)
.withHorizontalOrientation(this._getLayoutDirection())
.withHomeAndEnd()
.withWrap();
this._keyManager.updateActiveItem(this._selectedIndex);
// 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.
// Note that we use `onStable` instead of `requestAnimationFrame`, because the latter
// can hold up tests that are in a background tab.
this._ngZone.onStable.pipe(take(1)).subscribe(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, this._itemsResized())
.pipe(takeUntil(this._destroyed))
.subscribe(() => {
// We need to defer this to give the browser some time to recalculate
// the element dimensions. The call has to be wrapped in `NgZone.run`,
// because the viewport change handler runs outside of Angular.
this._ngZone.run(() => {
Promise.resolve().then(() => {
// Clamp the scroll distance, because it can change with the number of tabs.
this._scrollDistance = Math.max(0, Math.min(this._getMaxScrollDistance(), this._scrollDistance));
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.subscribe(newFocusIndex => {
this.indexFocused.emit(newFocusIndex);
this._setTabFocus(newFocusIndex);
});
}
/** Sends any changes that could affect the layout of the items. */
_itemsResized() {
if (typeof ResizeObserver !== 'function') {
return EMPTY;
}
return this._items.changes.pipe(startWith(this._items), switchMap((tabItems) => new Observable((observer) => this._ngZone.runOutsideAngular(() => {
const resizeObserver = new ResizeObserver(entries => observer.next(entries));
tabItems.forEach(item => resizeObserver.observe(item.elementRef.nativeElement));
return () => {
resizeObserver.disconnect();
};
}))),
// Skip the first emit since the resize observer emits when an item
// is observed for new items when the tab is already inserted
skip(1),
// Skip emissions where all the elements are invisible since we don't want
// the header to try and re-render with invalid measurements. See #25574.
filter(entries => entries.some(e => e.contentRect.width > 0 && e.contentRect.height > 0)));
}
ngAfterContentChecked() {
// 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._keyManager?.destroy();
this._destroyed.next();
this._destroyed.complete();
this._stopScrolling.complete();
}
/** Handles keyboard events on the header. */
_handleKeydown(event) {
// We don't handle any key bindings with a modifier key.
if (hasModifierKey(event)) {
return;
}
switch (event.keyCode) {
case ENTER:
case SPACE:
if (this.focusIndex !== this.selectedIndex) {
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() {
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) {
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) {
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) {
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() {
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() {
if (this.disablePagination) {
return;
}
const scrollDistance = this.scrollDistance;
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 Explor