Skip to content

Commit d7c062a

Browse files
committed
refactor(material/tabs): remove mixin function (#28606)
Replaces a usage of a mixin function in the tabs with a base class. (cherry picked from commit f44c5aa)
1 parent 0c5781b commit d7c062a

File tree

4 files changed

+109
-128
lines changed

4 files changed

+109
-128
lines changed

src/material/tabs/ink-bar.ts

Lines changed: 101 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,17 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
10-
import {ElementRef, InjectionToken, OnDestroy, OnInit, QueryList} from '@angular/core';
9+
import {
10+
Directive,
11+
ElementRef,
12+
InjectionToken,
13+
Input,
14+
OnDestroy,
15+
OnInit,
16+
QueryList,
17+
inject,
18+
numberAttribute,
19+
} from '@angular/core';
1120

1221
/**
1322
* Item inside a tab header relative to which the ink bar can be aligned.
@@ -62,126 +71,113 @@ export class MatInkBar {
6271
}
6372
}
6473

65-
/**
66-
* Mixin that can be used to apply the `MatInkBarItem` behavior to a class.
67-
* Base on MDC's `MDCSlidingTabIndicatorFoundation`:
68-
* https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-tab-indicator/sliding-foundation.ts
69-
* @docs-private
70-
*/
71-
export function mixinInkBarItem<
72-
T extends new (...args: any[]) => {elementRef: ElementRef<HTMLElement>},
73-
>(base: T): T & (new (...args: any[]) => MatInkBarItem) {
74-
return class extends base {
75-
constructor(...args: any[]) {
76-
super(...args);
77-
}
78-
79-
private _inkBarElement: HTMLElement | null;
80-
private _inkBarContentElement: HTMLElement | null;
81-
private _fitToContent = false;
82-
83-
/** Whether the ink bar should fit to the entire tab or just its content. */
84-
get fitInkBarToContent(): boolean {
85-
return this._fitToContent;
86-
}
87-
set fitInkBarToContent(v: BooleanInput) {
88-
const newValue = coerceBooleanProperty(v);
89-
90-
if (this._fitToContent !== newValue) {
91-
this._fitToContent = newValue;
74+
@Directive()
75+
export abstract class InkBarItem implements OnInit, OnDestroy {
76+
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
77+
private _inkBarElement: HTMLElement | null;
78+
private _inkBarContentElement: HTMLElement | null;
79+
private _fitToContent = false;
80+
81+
/** Whether the ink bar should fit to the entire tab or just its content. */
82+
@Input({transform: numberAttribute})
83+
get fitInkBarToContent(): boolean {
84+
return this._fitToContent;
85+
}
86+
set fitInkBarToContent(newValue: boolean) {
87+
if (this._fitToContent !== newValue) {
88+
this._fitToContent = newValue;
9289

93-
if (this._inkBarElement) {
94-
this._appendInkBarElement();
95-
}
90+
if (this._inkBarElement) {
91+
this._appendInkBarElement();
9692
}
9793
}
94+
}
9895

99-
/** Aligns the ink bar to the current item. */
100-
activateInkBar(previousIndicatorClientRect?: DOMRect) {
101-
const element = this.elementRef.nativeElement;
102-
103-
// Early exit if no indicator is present to handle cases where an indicator
104-
// may be activated without a prior indicator state
105-
if (
106-
!previousIndicatorClientRect ||
107-
!element.getBoundingClientRect ||
108-
!this._inkBarContentElement
109-
) {
110-
element.classList.add(ACTIVE_CLASS);
111-
return;
112-
}
113-
114-
// This animation uses the FLIP approach. You can read more about it at the link below:
115-
// https://aerotwist.com/blog/flip-your-animations/
116-
117-
// Calculate the dimensions based on the dimensions of the previous indicator
118-
const currentClientRect = element.getBoundingClientRect();
119-
const widthDelta = previousIndicatorClientRect.width / currentClientRect.width;
120-
const xPosition = previousIndicatorClientRect.left - currentClientRect.left;
121-
element.classList.add(NO_TRANSITION_CLASS);
122-
this._inkBarContentElement.style.setProperty(
123-
'transform',
124-
`translateX(${xPosition}px) scaleX(${widthDelta})`,
125-
);
126-
127-
// Force repaint before updating classes and transform to ensure the transform properly takes effect
128-
element.getBoundingClientRect();
129-
130-
element.classList.remove(NO_TRANSITION_CLASS);
96+
/** Aligns the ink bar to the current item. */
97+
activateInkBar(previousIndicatorClientRect?: DOMRect) {
98+
const element = this._elementRef.nativeElement;
99+
100+
// Early exit if no indicator is present to handle cases where an indicator
101+
// may be activated without a prior indicator state
102+
if (
103+
!previousIndicatorClientRect ||
104+
!element.getBoundingClientRect ||
105+
!this._inkBarContentElement
106+
) {
131107
element.classList.add(ACTIVE_CLASS);
132-
this._inkBarContentElement.style.setProperty('transform', '');
108+
return;
133109
}
134110

135-
/** Removes the ink bar from the current item. */
136-
deactivateInkBar() {
137-
this.elementRef.nativeElement.classList.remove(ACTIVE_CLASS);
138-
}
111+
// This animation uses the FLIP approach. You can read more about it at the link below:
112+
// https://aerotwist.com/blog/flip-your-animations/
113+
114+
// Calculate the dimensions based on the dimensions of the previous indicator
115+
const currentClientRect = element.getBoundingClientRect();
116+
const widthDelta = previousIndicatorClientRect.width / currentClientRect.width;
117+
const xPosition = previousIndicatorClientRect.left - currentClientRect.left;
118+
element.classList.add(NO_TRANSITION_CLASS);
119+
this._inkBarContentElement.style.setProperty(
120+
'transform',
121+
`translateX(${xPosition}px) scaleX(${widthDelta})`,
122+
);
123+
124+
// Force repaint before updating classes and transform to ensure the transform properly takes effect
125+
element.getBoundingClientRect();
126+
127+
element.classList.remove(NO_TRANSITION_CLASS);
128+
element.classList.add(ACTIVE_CLASS);
129+
this._inkBarContentElement.style.setProperty('transform', '');
130+
}
139131

140-
/** Initializes the foundation. */
141-
ngOnInit() {
142-
this._createInkBarElement();
143-
}
132+
/** Removes the ink bar from the current item. */
133+
deactivateInkBar() {
134+
this._elementRef.nativeElement.classList.remove(ACTIVE_CLASS);
135+
}
144136

145-
/** Destroys the foundation. */
146-
ngOnDestroy() {
147-
this._inkBarElement?.remove();
148-
this._inkBarElement = this._inkBarContentElement = null!;
149-
}
137+
/** Initializes the foundation. */
138+
ngOnInit() {
139+
this._createInkBarElement();
140+
}
150141

151-
/** Creates and appends the ink bar element. */
152-
private _createInkBarElement() {
153-
const documentNode = this.elementRef.nativeElement.ownerDocument || document;
154-
this._inkBarElement = documentNode.createElement('span');
155-
this._inkBarContentElement = documentNode.createElement('span');
142+
/** Destroys the foundation. */
143+
ngOnDestroy() {
144+
this._inkBarElement?.remove();
145+
this._inkBarElement = this._inkBarContentElement = null!;
146+
}
156147

157-
this._inkBarElement.className = 'mdc-tab-indicator';
158-
this._inkBarContentElement.className =
159-
'mdc-tab-indicator__content mdc-tab-indicator__content--underline';
148+
/** Creates and appends the ink bar element. */
149+
private _createInkBarElement() {
150+
const documentNode = this._elementRef.nativeElement.ownerDocument || document;
151+
const inkBarElement = (this._inkBarElement = documentNode.createElement('span'));
152+
const inkBarContentElement = (this._inkBarContentElement = documentNode.createElement('span'));
160153

161-
this._inkBarElement.appendChild(this._inkBarContentElement);
162-
this._appendInkBarElement();
163-
}
154+
inkBarElement.className = 'mdc-tab-indicator';
155+
inkBarContentElement.className =
156+
'mdc-tab-indicator__content mdc-tab-indicator__content--underline';
164157

165-
/**
166-
* Appends the ink bar to the tab host element or content, depending on whether
167-
* the ink bar should fit to content.
168-
*/
169-
private _appendInkBarElement() {
170-
if (!this._inkBarElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
171-
throw Error('Ink bar element has not been created and cannot be appended');
172-
}
158+
inkBarElement.appendChild(this._inkBarContentElement);
159+
this._appendInkBarElement();
160+
}
173161

174-
const parentElement = this._fitToContent
175-
? this.elementRef.nativeElement.querySelector('.mdc-tab__content')
176-
: this.elementRef.nativeElement;
162+
/**
163+
* Appends the ink bar to the tab host element or content, depending on whether
164+
* the ink bar should fit to content.
165+
*/
166+
private _appendInkBarElement() {
167+
if (!this._inkBarElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
168+
throw Error('Ink bar element has not been created and cannot be appended');
169+
}
177170

178-
if (!parentElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
179-
throw Error('Missing element to host the ink bar');
180-
}
171+
const parentElement = this._fitToContent
172+
? this._elementRef.nativeElement.querySelector('.mdc-tab__content')
173+
: this._elementRef.nativeElement;
181174

182-
parentElement!.appendChild(this._inkBarElement!);
175+
if (!parentElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
176+
throw Error('Missing element to host the ink bar');
183177
}
184-
};
178+
179+
parentElement!.appendChild(this._inkBarElement!);
180+
}
185181
}
186182

187183
/**

src/material/tabs/tab-label-wrapper.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,7 @@
77
*/
88

99
import {Directive, ElementRef, Input, booleanAttribute} from '@angular/core';
10-
import {mixinInkBarItem} from './ink-bar';
11-
12-
// Boilerplate for applying mixins to MatTabLabelWrapper.
13-
/** @docs-private */
14-
const _MatTabLabelWrapperMixinBase = mixinInkBarItem(
15-
class {
16-
elementRef: ElementRef;
17-
},
18-
);
10+
import {InkBarItem} from './ink-bar';
1911

2012
/**
2113
* Used in the `mat-tab-group` view to display tab labels.
@@ -30,12 +22,12 @@ const _MatTabLabelWrapperMixinBase = mixinInkBarItem(
3022
},
3123
standalone: true,
3224
})
33-
export class MatTabLabelWrapper extends _MatTabLabelWrapperMixinBase {
25+
export class MatTabLabelWrapper extends InkBarItem {
3426
/** Whether the tab is disabled. */
3527
@Input({transform: booleanAttribute})
3628
disabled: boolean = false;
3729

38-
constructor(override elementRef: ElementRef) {
30+
constructor(public elementRef: ElementRef) {
3931
super();
4032
}
4133

src/material/tabs/tab-nav-bar/tab-nav-bar.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {FocusableOption, FocusMonitor} from '@angular/cdk/a11y';
4040
import {Directionality} from '@angular/cdk/bidi';
4141
import {ViewportRuler} from '@angular/cdk/scrolling';
4242
import {Platform} from '@angular/cdk/platform';
43-
import {MatInkBar, mixinInkBarItem} from '../ink-bar';
43+
import {MatInkBar, InkBarItem} from '../ink-bar';
4444
import {BehaviorSubject, Subject} from 'rxjs';
4545
import {startWith, takeUntil} from 'rxjs/operators';
4646
import {ENTER, SPACE} from '@angular/cdk/keycodes';
@@ -228,13 +228,6 @@ export class MatTabNav
228228
}
229229
}
230230

231-
// Boilerplate for applying mixins to MatTabLink.
232-
const _MatTabLinkMixinBase = mixinInkBarItem(
233-
class {
234-
elementRef: ElementRef;
235-
},
236-
);
237-
238231
/**
239232
* Link inside a `mat-tab-nav-bar`.
240233
*/
@@ -263,7 +256,7 @@ const _MatTabLinkMixinBase = mixinInkBarItem(
263256
imports: [MatRipple],
264257
})
265258
export class MatTabLink
266-
extends _MatTabLinkMixinBase
259+
extends InkBarItem
267260
implements AfterViewInit, OnDestroy, RippleTarget, FocusableOption
268261
{
269262
private readonly _destroyed = new Subject<void>();
@@ -323,7 +316,7 @@ export class MatTabLink
323316

324317
constructor(
325318
private _tabNavBar: MatTabNav,
326-
/** @docs-private */ override elementRef: ElementRef,
319+
/** @docs-private */ public elementRef: ElementRef,
327320
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalRippleOptions: RippleGlobalOptions | null,
328321
@Attribute('tabindex') tabIndex: string,
329322
private _focusMonitor: FocusMonitor,

tools/public_api_guard/material/tabs.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ export class MatTabLabel extends CdkPortal {
380380
}
381381

382382
// @public
383-
export class MatTabLabelWrapper extends _MatTabLabelWrapperMixinBase {
383+
export class MatTabLabelWrapper extends InkBarItem {
384384
constructor(elementRef: ElementRef);
385385
disabled: boolean;
386386
// (undocumented)
@@ -399,7 +399,7 @@ export class MatTabLabelWrapper extends _MatTabLabelWrapperMixinBase {
399399
}
400400

401401
// @public
402-
export class MatTabLink extends _MatTabLinkMixinBase implements AfterViewInit, OnDestroy, RippleTarget, FocusableOption {
402+
export class MatTabLink extends InkBarItem implements AfterViewInit, OnDestroy, RippleTarget, FocusableOption {
403403
constructor(_tabNavBar: MatTabNav,
404404
elementRef: ElementRef, globalRippleOptions: RippleGlobalOptions | null, tabIndex: string, _focusMonitor: FocusMonitor, animationMode?: string);
405405
get active(): boolean;

0 commit comments

Comments
 (0)