|
6 | 6 | * found in the LICENSE file at https://angular.io/license
|
7 | 7 | */
|
8 | 8 |
|
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'; |
11 | 20 |
|
12 | 21 | /**
|
13 | 22 | * Item inside a tab header relative to which the ink bar can be aligned.
|
@@ -62,126 +71,113 @@ export class MatInkBar {
|
62 | 71 | }
|
63 | 72 | }
|
64 | 73 |
|
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; |
92 | 89 |
|
93 |
| - if (this._inkBarElement) { |
94 |
| - this._appendInkBarElement(); |
95 |
| - } |
| 90 | + if (this._inkBarElement) { |
| 91 | + this._appendInkBarElement(); |
96 | 92 | }
|
97 | 93 | }
|
| 94 | + } |
98 | 95 |
|
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 | + ) { |
131 | 107 | element.classList.add(ACTIVE_CLASS);
|
132 |
| - this._inkBarContentElement.style.setProperty('transform', ''); |
| 108 | + return; |
133 | 109 | }
|
134 | 110 |
|
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 | + } |
139 | 131 |
|
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 | + } |
144 | 136 |
|
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 | + } |
150 | 141 |
|
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 | + } |
156 | 147 |
|
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')); |
160 | 153 |
|
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'; |
164 | 157 |
|
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 | + } |
173 | 161 |
|
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 | + } |
177 | 170 |
|
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; |
181 | 174 |
|
182 |
| - parentElement!.appendChild(this._inkBarElement!); |
| 175 | + if (!parentElement && (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| 176 | + throw Error('Missing element to host the ink bar'); |
183 | 177 | }
|
184 |
| - }; |
| 178 | + |
| 179 | + parentElement!.appendChild(this._inkBarElement!); |
| 180 | + } |
185 | 181 | }
|
186 | 182 |
|
187 | 183 | /**
|
|
0 commit comments