diff --git a/src/material/tabs/ink-bar.ts b/src/material/tabs/ink-bar.ts index f41102c734b2..26367ce03721 100644 --- a/src/material/tabs/ink-bar.ts +++ b/src/material/tabs/ink-bar.ts @@ -6,8 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {ElementRef, InjectionToken, OnDestroy, OnInit, QueryList} from '@angular/core'; +import { + Directive, + ElementRef, + InjectionToken, + Input, + OnDestroy, + OnInit, + QueryList, + inject, + numberAttribute, +} from '@angular/core'; /** * Item inside a tab header relative to which the ink bar can be aligned. @@ -62,126 +71,113 @@ export class MatInkBar { } } -/** - * 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 - */ -export function mixinInkBarItem< - T extends new (...args: any[]) => {elementRef: ElementRef}, ->(base: T): T & (new (...args: any[]) => MatInkBarItem) { - return class extends base { - constructor(...args: any[]) { - super(...args); - } - - private _inkBarElement: HTMLElement | null; - private _inkBarContentElement: HTMLElement | null; - private _fitToContent = false; - - /** Whether the ink bar should fit to the entire tab or just its content. */ - get fitInkBarToContent(): boolean { - return this._fitToContent; - } - set fitInkBarToContent(v: BooleanInput) { - const newValue = coerceBooleanProperty(v); - - if (this._fitToContent !== newValue) { - this._fitToContent = newValue; +@Directive() +export abstract class InkBarItem implements OnInit, OnDestroy { + private _elementRef = inject>(ElementRef); + private _inkBarElement: HTMLElement | null; + private _inkBarContentElement: HTMLElement | null; + private _fitToContent = false; + + /** Whether the ink bar should fit to the entire tab or just its content. */ + @Input({transform: numberAttribute}) + get fitInkBarToContent(): boolean { + return this._fitToContent; + } + set fitInkBarToContent(newValue: boolean) { + if (this._fitToContent !== newValue) { + this._fitToContent = newValue; - if (this._inkBarElement) { - this._appendInkBarElement(); - } + if (this._inkBarElement) { + this._appendInkBarElement(); } } + } - /** Aligns the ink bar to the current item. */ - activateInkBar(previousIndicatorClientRect?: DOMRect) { - 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); + /** Aligns the ink bar to the current item. */ + activateInkBar(previousIndicatorClientRect?: DOMRect) { + 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); - this._inkBarContentElement.style.setProperty('transform', ''); + return; } - /** Removes the ink bar from the current item. */ - deactivateInkBar() { - this.elementRef.nativeElement.classList.remove(ACTIVE_CLASS); - } + // 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', ''); + } - /** Initializes the foundation. */ - ngOnInit() { - this._createInkBarElement(); - } + /** Removes the ink bar from the current item. */ + deactivateInkBar() { + this._elementRef.nativeElement.classList.remove(ACTIVE_CLASS); + } - /** Destroys the foundation. */ - ngOnDestroy() { - this._inkBarElement?.remove(); - this._inkBarElement = this._inkBarContentElement = null!; - } + /** Initializes the foundation. */ + ngOnInit() { + this._createInkBarElement(); + } - /** Creates and appends the ink bar element. */ - private _createInkBarElement() { - const documentNode = this.elementRef.nativeElement.ownerDocument || document; - this._inkBarElement = documentNode.createElement('span'); - this._inkBarContentElement = documentNode.createElement('span'); + /** Destroys the foundation. */ + ngOnDestroy() { + this._inkBarElement?.remove(); + this._inkBarElement = this._inkBarContentElement = null!; + } - this._inkBarElement.className = 'mdc-tab-indicator'; - this._inkBarContentElement.className = - 'mdc-tab-indicator__content mdc-tab-indicator__content--underline'; + /** Creates and appends the ink bar element. */ + private _createInkBarElement() { + const documentNode = this._elementRef.nativeElement.ownerDocument || document; + const inkBarElement = (this._inkBarElement = documentNode.createElement('span')); + const inkBarContentElement = (this._inkBarContentElement = documentNode.createElement('span')); - this._inkBarElement.appendChild(this._inkBarContentElement); - this._appendInkBarElement(); - } + inkBarElement.className = 'mdc-tab-indicator'; + inkBarContentElement.className = + 'mdc-tab-indicator__content mdc-tab-indicator__content--underline'; - /** - * Appends the ink bar to the tab host element or content, depending on whether - * the ink bar should fit to content. - */ - private _appendInkBarElement() { - if (!this._inkBarElement && (typeof ngDevMode === 'undefined' || ngDevMode)) { - throw Error('Ink bar element has not been created and cannot be appended'); - } + inkBarElement.appendChild(this._inkBarContentElement); + this._appendInkBarElement(); + } - const parentElement = this._fitToContent - ? this.elementRef.nativeElement.querySelector('.mdc-tab__content') - : this.elementRef.nativeElement; + /** + * Appends the ink bar to the tab host element or content, depending on whether + * the ink bar should fit to content. + */ + private _appendInkBarElement() { + if (!this._inkBarElement && (typeof ngDevMode === 'undefined' || ngDevMode)) { + throw Error('Ink bar element has not been created and cannot be appended'); + } - if (!parentElement && (typeof ngDevMode === 'undefined' || ngDevMode)) { - throw Error('Missing element to host the ink bar'); - } + const parentElement = this._fitToContent + ? this._elementRef.nativeElement.querySelector('.mdc-tab__content') + : this._elementRef.nativeElement; - parentElement!.appendChild(this._inkBarElement!); + if (!parentElement && (typeof ngDevMode === 'undefined' || ngDevMode)) { + throw Error('Missing element to host the ink bar'); } - }; + + parentElement!.appendChild(this._inkBarElement!); + } } /** diff --git a/src/material/tabs/tab-label-wrapper.ts b/src/material/tabs/tab-label-wrapper.ts index f7157cbaafa3..634e09d7340c 100644 --- a/src/material/tabs/tab-label-wrapper.ts +++ b/src/material/tabs/tab-label-wrapper.ts @@ -7,15 +7,7 @@ */ import {Directive, ElementRef, Input, booleanAttribute} from '@angular/core'; -import {mixinInkBarItem} from './ink-bar'; - -// Boilerplate for applying mixins to MatTabLabelWrapper. -/** @docs-private */ -const _MatTabLabelWrapperMixinBase = mixinInkBarItem( - class { - elementRef: ElementRef; - }, -); +import {InkBarItem} from './ink-bar'; /** * Used in the `mat-tab-group` view to display tab labels. @@ -30,12 +22,12 @@ const _MatTabLabelWrapperMixinBase = mixinInkBarItem( }, standalone: true, }) -export class MatTabLabelWrapper extends _MatTabLabelWrapperMixinBase { +export class MatTabLabelWrapper extends InkBarItem { /** Whether the tab is disabled. */ @Input({transform: booleanAttribute}) disabled: boolean = false; - constructor(override elementRef: ElementRef) { + constructor(public elementRef: ElementRef) { super(); } diff --git a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts index 2bf7c694a261..3c0e6f23c5d9 100644 --- a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts @@ -40,7 +40,7 @@ import {FocusableOption, FocusMonitor} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {ViewportRuler} from '@angular/cdk/scrolling'; import {Platform} from '@angular/cdk/platform'; -import {MatInkBar, mixinInkBarItem} from '../ink-bar'; +import {MatInkBar, InkBarItem} from '../ink-bar'; import {BehaviorSubject, Subject} from 'rxjs'; import {startWith, takeUntil} from 'rxjs/operators'; import {ENTER, SPACE} from '@angular/cdk/keycodes'; @@ -228,13 +228,6 @@ export class MatTabNav } } -// Boilerplate for applying mixins to MatTabLink. -const _MatTabLinkMixinBase = mixinInkBarItem( - class { - elementRef: ElementRef; - }, -); - /** * Link inside a `mat-tab-nav-bar`. */ @@ -263,7 +256,7 @@ const _MatTabLinkMixinBase = mixinInkBarItem( imports: [MatRipple], }) export class MatTabLink - extends _MatTabLinkMixinBase + extends InkBarItem implements AfterViewInit, OnDestroy, RippleTarget, FocusableOption { private readonly _destroyed = new Subject(); @@ -323,7 +316,7 @@ export class MatTabLink constructor( private _tabNavBar: MatTabNav, - /** @docs-private */ override elementRef: ElementRef, + /** @docs-private */ public elementRef: ElementRef, @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalRippleOptions: RippleGlobalOptions | null, @Attribute('tabindex') tabIndex: string, private _focusMonitor: FocusMonitor, diff --git a/tools/public_api_guard/material/tabs.md b/tools/public_api_guard/material/tabs.md index 11fa027ac4e2..3aac6557bace 100644 --- a/tools/public_api_guard/material/tabs.md +++ b/tools/public_api_guard/material/tabs.md @@ -380,7 +380,7 @@ export class MatTabLabel extends CdkPortal { } // @public -export class MatTabLabelWrapper extends _MatTabLabelWrapperMixinBase { +export class MatTabLabelWrapper extends InkBarItem { constructor(elementRef: ElementRef); disabled: boolean; // (undocumented) @@ -399,7 +399,7 @@ export class MatTabLabelWrapper extends _MatTabLabelWrapperMixinBase { } // @public -export class MatTabLink extends _MatTabLinkMixinBase implements AfterViewInit, OnDestroy, RippleTarget, FocusableOption { +export class MatTabLink extends InkBarItem implements AfterViewInit, OnDestroy, RippleTarget, FocusableOption { constructor(_tabNavBar: MatTabNav, elementRef: ElementRef, globalRippleOptions: RippleGlobalOptions | null, tabIndex: string, _focusMonitor: FocusMonitor, animationMode?: string); get active(): boolean;