@@ -27,16 +27,21 @@ import {
27
27
QueryList ,
28
28
ViewChild ,
29
29
ViewEncapsulation ,
30
+ AfterViewInit ,
30
31
} from '@angular/core' ;
31
32
import { CanDisableRipple , CanDisableRippleCtor , mixinDisableRipple } from '@angular/material/core' ;
32
- import { merge , of as observableOf , Subject } from 'rxjs' ;
33
+ import { merge , of as observableOf , Subject , timer , fromEvent } from 'rxjs' ;
33
34
import { takeUntil } from 'rxjs/operators' ;
34
35
import { MatInkBar } from './ink-bar' ;
35
36
import { MatTabLabelWrapper } from './tab-label-wrapper' ;
36
37
import { FocusKeyManager } from '@angular/cdk/a11y' ;
37
- import { Platform } from '@angular/cdk/platform' ;
38
+ import { Platform , normalizePassiveListenerOptions } from '@angular/cdk/platform' ;
38
39
39
40
41
+ /** Config used to bind passive event listeners */
42
+ const passiveEventListenerOptions =
43
+ normalizePassiveListenerOptions ( { passive : true } ) as EventListenerOptions ;
44
+
40
45
/**
41
46
* The directions that scrolling can go in when the header's tabs exceed the header width. 'After'
42
47
* will scroll the header towards the end of the tabs list and 'before' will scroll towards the
@@ -50,6 +55,18 @@ export type ScrollDirection = 'after' | 'before';
50
55
*/
51
56
const EXAGGERATED_OVERSCROLL = 60 ;
52
57
58
+ /**
59
+ * Amount of milliseconds to wait before starting to scroll the header automatically.
60
+ * Set a little conservatively in order to handle fake events dispatched on touch devices.
61
+ */
62
+ const HEADER_SCROLL_DELAY = 650 ;
63
+
64
+ /**
65
+ * Interval in milliseconds at which to scroll the header
66
+ * while the user is holding their pointer.
67
+ */
68
+ const HEADER_SCROLL_INTERVAL = 100 ;
69
+
53
70
// Boilerplate for applying mixins to MatTabHeader.
54
71
/** @docs -private */
55
72
export class MatTabHeaderBase { }
@@ -78,12 +95,14 @@ export const _MatTabHeaderMixinBase: CanDisableRippleCtor & typeof MatTabHeaderB
78
95
} ,
79
96
} )
80
97
export class MatTabHeader extends _MatTabHeaderMixinBase
81
- implements AfterContentChecked , AfterContentInit , OnDestroy , CanDisableRipple {
98
+ implements AfterContentChecked , AfterContentInit , AfterViewInit , OnDestroy , CanDisableRipple {
82
99
83
100
@ContentChildren ( MatTabLabelWrapper ) _labelWrappers : QueryList < MatTabLabelWrapper > ;
84
101
@ViewChild ( MatInkBar ) _inkBar : MatInkBar ;
85
102
@ViewChild ( 'tabListContainer' ) _tabListContainer : ElementRef ;
86
103
@ViewChild ( 'tabList' ) _tabList : ElementRef ;
104
+ @ViewChild ( 'nextPaginator' ) _nextPaginator : ElementRef < HTMLElement > ;
105
+ @ViewChild ( 'previousPaginator' ) _previousPaginator : ElementRef < HTMLElement > ;
87
106
88
107
/** The distance in pixels that the tab labels should be translated to the left. */
89
108
private _scrollDistance = 0 ;
@@ -118,6 +137,9 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
118
137
/** Cached text content of the header. */
119
138
private _currentTextContent : string ;
120
139
140
+ /** Stream that will stop the automated scrolling. */
141
+ private _stopScrolling = new Subject < void > ( ) ;
142
+
121
143
/** The index of the active tab. */
122
144
@Input ( )
123
145
get selectedIndex ( ) : number { return this . _selectedIndex ; }
@@ -146,6 +168,23 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
146
168
private _ngZone ?: NgZone ,
147
169
private _platform ?: Platform ) {
148
170
super ( ) ;
171
+
172
+ const element = _elementRef . nativeElement ;
173
+ const bindEvent = ( ) => {
174
+ fromEvent ( element , 'mouseleave' )
175
+ . pipe ( takeUntil ( this . _destroyed ) )
176
+ . subscribe ( ( ) => {
177
+ this . _stopInterval ( ) ;
178
+ } ) ;
179
+ } ;
180
+
181
+ // @breaking -change 8.0.0 remove null check once _ngZone is made into a required parameter.
182
+ if ( _ngZone ) {
183
+ // Bind the `mouseleave` event on the outside since it doesn't change anything in the view.
184
+ _ngZone . runOutsideAngular ( bindEvent ) ;
185
+ } else {
186
+ bindEvent ( ) ;
187
+ }
149
188
}
150
189
151
190
ngAfterContentChecked ( ) : void {
@@ -175,6 +214,7 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
175
214
}
176
215
}
177
216
217
+ /** Handles keyboard events on the header. */
178
218
_handleKeydown ( event : KeyboardEvent ) {
179
219
// We don't handle any key bindings with a modifier key.
180
220
if ( hasModifierKey ( event ) ) {
@@ -237,9 +277,25 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
237
277
} ) ;
238
278
}
239
279
280
+ ngAfterViewInit ( ) {
281
+ // We need to handle these events manually, because we want to bind passive event listeners.
282
+ fromEvent ( this . _previousPaginator . nativeElement , 'touchstart' , passiveEventListenerOptions )
283
+ . pipe ( takeUntil ( this . _destroyed ) )
284
+ . subscribe ( ( ) => {
285
+ this . _handlePaginatorPress ( 'before' ) ;
286
+ } ) ;
287
+
288
+ fromEvent ( this . _nextPaginator . nativeElement , 'touchstart' , passiveEventListenerOptions )
289
+ . pipe ( takeUntil ( this . _destroyed ) )
290
+ . subscribe ( ( ) => {
291
+ this . _handlePaginatorPress ( 'after' ) ;
292
+ } ) ;
293
+ }
294
+
240
295
ngOnDestroy ( ) {
241
296
this . _destroyed . next ( ) ;
242
297
this . _destroyed . complete ( ) ;
298
+ this . _stopScrolling . complete ( ) ;
243
299
}
244
300
245
301
/**
@@ -362,13 +418,8 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
362
418
363
419
/** Sets the distance in pixels that the tab header should be transformed in the X-axis. */
364
420
get scrollDistance ( ) : number { return this . _scrollDistance ; }
365
- set scrollDistance ( v : number ) {
366
- this . _scrollDistance = Math . max ( 0 , Math . min ( this . _getMaxScrollDistance ( ) , v ) ) ;
367
-
368
- // Mark that the scroll distance has changed so that after the view is checked, the CSS
369
- // transformation can move the header.
370
- this . _scrollDistanceChanged = true ;
371
- this . _checkScrollingControls ( ) ;
421
+ set scrollDistance ( value : number ) {
422
+ this . _scrollTo ( value ) ;
372
423
}
373
424
374
425
/**
@@ -379,11 +430,19 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
379
430
* This is an expensive call that forces a layout reflow to compute box and scroll metrics and
380
431
* should be called sparingly.
381
432
*/
382
- _scrollHeader ( scrollDir : ScrollDirection ) {
433
+ _scrollHeader ( direction : ScrollDirection ) {
383
434
const viewLength = this . _tabListContainer . nativeElement . offsetWidth ;
384
435
385
436
// Move the scroll distance one-third the length of the tab list's viewport.
386
- this . scrollDistance += ( scrollDir == 'before' ? - 1 : 1 ) * viewLength / 3 ;
437
+ const scrollAmount = ( direction == 'before' ? - 1 : 1 ) * viewLength / 3 ;
438
+
439
+ return this . _scrollTo ( this . _scrollDistance + scrollAmount ) ;
440
+ }
441
+
442
+ /** Handles click events on the pagination arrows. */
443
+ _handlePaginatorClick ( direction : ScrollDirection ) {
444
+ this . _stopInterval ( ) ;
445
+ this . _scrollHeader ( direction ) ;
387
446
}
388
447
389
448
/**
@@ -481,4 +540,49 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
481
540
482
541
this . _inkBar . alignToElement ( selectedLabelWrapper ! ) ;
483
542
}
543
+
544
+ /** Stops the currently-running paginator interval. */
545
+ _stopInterval ( ) {
546
+ this . _stopScrolling . next ( ) ;
547
+ }
548
+
549
+ /**
550
+ * Handles the user pressing down on one of the paginators.
551
+ * Starts scrolling the header after a certain amount of time.
552
+ * @param direction In which direction the paginator should be scrolled.
553
+ */
554
+ _handlePaginatorPress ( direction : ScrollDirection ) {
555
+ // Avoid overlapping timers.
556
+ this . _stopInterval ( ) ;
557
+
558
+ // Start a timer after the delay and keep firing based on the interval.
559
+ timer ( HEADER_SCROLL_DELAY , HEADER_SCROLL_INTERVAL )
560
+ // Keep the timer going until something tells it to stop or the component is destroyed.
561
+ . pipe ( takeUntil ( merge ( this . _stopScrolling , this . _destroyed ) ) )
562
+ . subscribe ( ( ) => {
563
+ const { maxScrollDistance, distance} = this . _scrollHeader ( direction ) ;
564
+
565
+ // Stop the timer if we've reached the start or the end.
566
+ if ( distance === 0 || distance >= maxScrollDistance ) {
567
+ this . _stopInterval ( ) ;
568
+ }
569
+ } ) ;
570
+ }
571
+
572
+ /**
573
+ * Scrolls the header to a given position.
574
+ * @param position Position to which to scroll.
575
+ * @returns Information on the current scroll distance and the maximum.
576
+ */
577
+ private _scrollTo ( position : number ) {
578
+ const maxScrollDistance = this . _getMaxScrollDistance ( ) ;
579
+ this . _scrollDistance = Math . max ( 0 , Math . min ( maxScrollDistance , position ) ) ;
580
+
581
+ // Mark that the scroll distance has changed so that after the view is checked, the CSS
582
+ // transformation can move the header.
583
+ this . _scrollDistanceChanged = true ;
584
+ this . _checkScrollingControls ( ) ;
585
+
586
+ return { maxScrollDistance, distance : this . _scrollDistance } ;
587
+ }
484
588
}
0 commit comments