6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
8
9
- import { ElementRef , QueryList } from '@angular/core' ;
10
- import {
11
- MDCSlidingTabIndicatorFoundation ,
12
- MDCTabIndicatorAdapter ,
13
- MDCTabIndicatorFoundation ,
14
- } from '@material/tab-indicator' ;
9
+ import { BooleanInput , coerceBooleanProperty } from '@angular/cdk/coercion' ;
10
+ import { ElementRef , OnDestroy , OnInit , QueryList } from '@angular/core' ;
15
11
16
12
/**
17
13
* Item inside a tab header relative to which the ink bar can be aligned.
18
14
* @docs -private
19
15
*/
20
- export interface MatInkBarItem {
21
- _foundation : MatInkBarFoundation ;
16
+ export interface MatInkBarItem extends OnInit , OnDestroy {
22
17
elementRef : ElementRef < HTMLElement > ;
18
+ activateInkBar ( previousIndicatorClientRect ?: ClientRect ) : void ;
19
+ deactivateInkBar ( ) : void ;
20
+ fitInkBarToContent : boolean ;
23
21
}
24
22
23
+ /** Class that is applied when a tab indicator is active. */
24
+ const ACTIVE_CLASS = 'mdc-tab-indicator--active' ;
25
+
26
+ /** Class that is applied when the tab indicator should not transition. */
27
+ const NO_TRANSITION_CLASS = 'mdc-tab-indicator--no-transition' ;
28
+
25
29
/**
26
30
* Abstraction around the MDC tab indicator that acts as the tab header's ink bar.
27
31
* @docs -private
@@ -34,157 +38,144 @@ export class MatInkBar {
34
38
35
39
/** Hides the ink bar. */
36
40
hide ( ) {
37
- this . _items . forEach ( item => item . _foundation . deactivate ( ) ) ;
41
+ this . _items . forEach ( item => item . deactivateInkBar ( ) ) ;
38
42
}
39
43
40
44
/** Aligns the ink bar to a DOM node. */
41
45
alignToElement ( element : HTMLElement ) {
42
46
const correspondingItem = this . _items . find ( item => item . elementRef . nativeElement === element ) ;
43
47
const currentItem = this . _currentItem ;
44
48
45
- if ( currentItem ) {
46
- currentItem . _foundation . deactivate ( ) ;
47
- }
49
+ currentItem ?. deactivateInkBar ( ) ;
48
50
49
51
if ( correspondingItem ) {
50
- const clientRect = currentItem
51
- ? currentItem . _foundation . computeContentClientRect ( )
52
- : undefined ;
52
+ const clientRect = currentItem ?. elementRef . nativeElement . getBoundingClientRect ?.( ) ;
53
53
54
54
// The ink bar won't animate unless we give it the `ClientRect` of the previous item.
55
- correspondingItem . _foundation . activate ( clientRect ) ;
55
+ correspondingItem . activateInkBar ( clientRect ) ;
56
56
this . _currentItem = correspondingItem ;
57
57
}
58
58
}
59
59
}
60
60
61
61
/**
62
- * Implementation of MDC's sliding tab indicator (ink bar) foundation.
62
+ * Mixin that can be used to apply the `MatInkBarItem` behavior to a class.
63
+ * Base on MDC's `MDCSlidingTabIndicatorFoundation`:
64
+ * https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-tab-indicator/sliding-foundation.ts
63
65
* @docs -private
64
66
*/
65
- export class MatInkBarFoundation {
66
- private _destroyed : boolean ;
67
- private _foundation : MDCTabIndicatorFoundation ;
68
- private _inkBarElement : HTMLElement ;
69
- private _inkBarContentElement : HTMLElement ;
70
- private _fitToContent = false ;
71
- private _adapter : MDCTabIndicatorAdapter = {
72
- addClass : className => {
73
- if ( ! this . _destroyed ) {
74
- this . _hostElement . classList . add ( className ) ;
75
- }
76
- } ,
77
- removeClass : className => {
78
- if ( ! this . _destroyed ) {
79
- this . _hostElement . classList . remove ( className ) ;
80
- }
81
- } ,
82
- setContentStyleProperty : ( propName , value ) => {
83
- if ( ! this . _destroyed ) {
84
- this . _inkBarContentElement . style . setProperty ( propName , value ) ;
85
- }
86
- } ,
87
- computeContentClientRect : ( ) => {
88
- // `getBoundingClientRect` isn't available on the server.
89
- return this . _destroyed || ! this . _inkBarContentElement . getBoundingClientRect
90
- ? ( {
91
- width : 0 ,
92
- height : 0 ,
93
- top : 0 ,
94
- left : 0 ,
95
- right : 0 ,
96
- bottom : 0 ,
97
- x : 0 ,
98
- y : 0 ,
99
- } as ClientRect )
100
- : this . _inkBarContentElement . getBoundingClientRect ( ) ;
101
- } ,
102
- } ;
67
+ export function mixinInkBarItem <
68
+ T extends new ( ...args : any [ ] ) => { elementRef : ElementRef < HTMLElement > } ,
69
+ > ( base : T ) : T & ( new ( ...args : any [ ] ) => MatInkBarItem ) {
70
+ return class extends base {
71
+ constructor ( ...args : any [ ] ) {
72
+ super ( ...args ) ;
73
+ }
103
74
104
- constructor ( private _hostElement : HTMLElement , private _document : Document ) {
105
- this . _foundation = new MDCSlidingTabIndicatorFoundation ( this . _adapter ) ;
106
- }
75
+ private _inkBarElement : HTMLElement | null ;
76
+ private _inkBarContentElement : HTMLElement | null ;
77
+ private _fitToContent = false ;
107
78
108
- /** Aligns the ink bar to the current item. */
109
- activate ( clientRect ?: ClientRect ) {
110
- this . _foundation . activate ( clientRect ) ;
111
- }
79
+ /** Whether the ink bar should fit to the entire tab or just its content. */
80
+ get fitInkBarToContent ( ) : boolean {
81
+ return this . _fitToContent ;
82
+ }
83
+ set fitInkBarToContent ( v : BooleanInput ) {
84
+ const newValue = coerceBooleanProperty ( v ) ;
112
85
113
- /** Removes the ink bar from the current item. */
114
- deactivate ( ) {
115
- this . _foundation . deactivate ( ) ;
116
- }
86
+ if ( this . _fitToContent !== newValue ) {
87
+ this . _fitToContent = newValue ;
117
88
118
- /** Gets the ClientRect of the ink bar. */
119
- computeContentClientRect ( ) {
120
- return this . _foundation . computeContentClientRect ( ) ;
121
- }
89
+ if ( this . _inkBarElement ) {
90
+ this . _appendInkBarElement ( ) ;
91
+ }
92
+ }
93
+ }
122
94
123
- /** Initializes the foundation. */
124
- init ( ) {
125
- this . _createInkBarElement ( ) ;
126
- this . _foundation . init ( ) ;
127
- }
95
+ /** Aligns the ink bar to the current item. */
96
+ activateInkBar ( previousIndicatorClientRect ?: ClientRect ) {
97
+ const element = this . elementRef . nativeElement ;
98
+
99
+ // Early exit if no indicator is present to handle cases where an indicator
100
+ // may be activated without a prior indicator state
101
+ if (
102
+ ! previousIndicatorClientRect ||
103
+ ! element . getBoundingClientRect ||
104
+ ! this . _inkBarContentElement
105
+ ) {
106
+ element . classList . add ( ACTIVE_CLASS ) ;
107
+ return ;
108
+ }
128
109
129
- /** Destroys the foundation. */
130
- destroy ( ) {
131
- this . _inkBarElement . remove ( ) ;
132
- this . _hostElement = this . _inkBarElement = this . _inkBarContentElement = null ! ;
133
- this . _foundation . destroy ( ) ;
134
- this . _destroyed = true ;
135
- }
110
+ // This animation uses the FLIP approach. You can read more about it at the link below:
111
+ // https://aerotwist.com/blog/flip-your-animations/
112
+
113
+ // Calculate the dimensions based on the dimensions of the previous indicator
114
+ const currentClientRect = element . getBoundingClientRect ( ) ;
115
+ const widthDelta = previousIndicatorClientRect . width / currentClientRect . width ;
116
+ const xPosition = previousIndicatorClientRect . left - currentClientRect . left ;
117
+ element . classList . add ( NO_TRANSITION_CLASS ) ;
118
+ this . _inkBarContentElement . style . setProperty (
119
+ 'transform' ,
120
+ `translateX(${ xPosition } px) scaleX(${ widthDelta } )` ,
121
+ ) ;
122
+
123
+ // Force repaint before updating classes and transform to ensure the transform properly takes effect
124
+ element . getBoundingClientRect ( ) ;
125
+
126
+ element . classList . remove ( NO_TRANSITION_CLASS ) ;
127
+ element . classList . add ( ACTIVE_CLASS ) ;
128
+ this . _inkBarContentElement . style . setProperty ( 'transform' , '' ) ;
129
+ }
136
130
137
- /**
138
- * Sets whether the ink bar should be appended to the content, which will cause the ink bar
139
- * to match the width of the content rather than the tab host element.
140
- */
141
- setFitToContent ( fitToContent : boolean ) {
142
- if ( this . _fitToContent !== fitToContent ) {
143
- this . _fitToContent = fitToContent ;
144
- if ( this . _inkBarElement ) {
145
- this . _appendInkBarElement ( ) ;
146
- }
131
+ /** Removes the ink bar from the current item. */
132
+ deactivateInkBar ( ) {
133
+ this . elementRef . nativeElement . classList . remove ( ACTIVE_CLASS ) ;
147
134
}
148
- }
149
135
150
- /**
151
- * Gets whether the ink bar should be appended to the content, which will cause the ink bar
152
- * to match the width of the content rather than the tab host element.
153
- */
154
- getFitToContent ( ) : boolean {
155
- return this . _fitToContent ;
156
- }
136
+ /** Initializes the foundation. */
137
+ ngOnInit ( ) {
138
+ this . _createInkBarElement ( ) ;
139
+ }
157
140
158
- /** Creates and appends the ink bar element. */
159
- private _createInkBarElement ( ) {
160
- this . _inkBarElement = this . _document . createElement ( 'span' ) ;
161
- this . _inkBarContentElement = this . _document . createElement ( 'span' ) ;
141
+ /** Destroys the foundation. */
142
+ ngOnDestroy ( ) {
143
+ this . _inkBarElement ?. remove ( ) ;
144
+ this . _inkBarElement = this . _inkBarContentElement = null ! ;
145
+ }
162
146
163
- this . _inkBarElement . className = 'mdc-tab-indicator' ;
164
- this . _inkBarContentElement . className =
165
- 'mdc-tab-indicator__content' + ' mdc-tab-indicator__content--underline' ;
147
+ /** Creates and appends the ink bar element. */
148
+ private _createInkBarElement ( ) {
149
+ const documentNode = this . elementRef . nativeElement . ownerDocument || document ;
150
+ this . _inkBarElement = documentNode . createElement ( 'span' ) ;
151
+ this . _inkBarContentElement = documentNode . createElement ( 'span' ) ;
166
152
167
- this . _inkBarElement . appendChild ( this . _inkBarContentElement ) ;
168
- this . _appendInkBarElement ( ) ;
169
- }
153
+ this . _inkBarElement . className = 'mdc-tab-indicator' ;
154
+ this . _inkBarContentElement . className =
155
+ 'mdc-tab-indicator__content mdc-tab-indicator__content--underline' ;
170
156
171
- /**
172
- * Appends the ink bar to the tab host element or content, depending on whether
173
- * the ink bar should fit to content.
174
- */
175
- private _appendInkBarElement ( ) {
176
- if ( ! this . _inkBarElement && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) {
177
- throw Error ( 'Ink bar element has not been created and cannot be appended' ) ;
157
+ this . _inkBarElement . appendChild ( this . _inkBarContentElement ) ;
158
+ this . _appendInkBarElement ( ) ;
178
159
}
179
160
180
- const parentElement = this . _fitToContent
181
- ? this . _hostElement . querySelector ( '.mdc-tab__content' )
182
- : this . _hostElement ;
161
+ /**
162
+ * Appends the ink bar to the tab host element or content, depending on whether
163
+ * the ink bar should fit to content.
164
+ */
165
+ private _appendInkBarElement ( ) {
166
+ if ( ! this . _inkBarElement && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) {
167
+ throw Error ( 'Ink bar element has not been created and cannot be appended' ) ;
168
+ }
183
169
184
- if ( ! parentElement && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) {
185
- throw Error ( 'Missing element to host the ink bar' ) ;
186
- }
170
+ const parentElement = this . _fitToContent
171
+ ? this . elementRef . nativeElement . querySelector ( '.mdc-tab__content' )
172
+ : this . elementRef . nativeElement ;
187
173
188
- parentElement ! . appendChild ( this . _inkBarElement ) ;
189
- }
174
+ if ( ! parentElement && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) {
175
+ throw Error ( 'Missing element to host the ink bar' ) ;
176
+ }
177
+
178
+ parentElement ! . appendChild ( this . _inkBarElement ! ) ;
179
+ }
180
+ } ;
190
181
}
0 commit comments