@@ -353,6 +353,28 @@ export class TabBar<T> extends Widget {
353
353
}
354
354
}
355
355
356
+ /**
357
+ * Whether scrolling is enabled.
358
+ */
359
+ get scrollingEnabled ( ) : boolean {
360
+ return this . _scrollingEnabled ;
361
+ }
362
+
363
+ set scrollingEnabled ( value : boolean ) {
364
+ // Do nothing if the value does not change.
365
+ if ( this . _scrollingEnabled === value ) {
366
+ return ;
367
+ }
368
+
369
+ this . _scrollingEnabled = value ;
370
+ if ( value ) {
371
+ this . node . classList . add ( 'lm-mod-scrollable' ) ;
372
+ } else {
373
+ this . node . classList . add ( 'lm-mod-scrollable' ) ;
374
+ }
375
+ this . maybeSwitchScrollButtons ( ) ;
376
+ }
377
+
356
378
/**
357
379
* A read-only array of the titles in the tab bar.
358
380
*/
@@ -374,6 +396,20 @@ export class TabBar<T> extends Widget {
374
396
) [ 0 ] as HTMLUListElement ;
375
397
}
376
398
399
+ /**
400
+ * The tab bar content wrapper node.
401
+ *
402
+ * #### Notes
403
+ * This is the node which the content node and enables scrolling.
404
+ *
405
+ * Modifying this node directly can lead to undefined behavior.
406
+ */
407
+ get contentWrapperNode ( ) : HTMLUListElement {
408
+ return this . node . getElementsByClassName (
409
+ 'lm-TabBar-wrapper'
410
+ ) [ 0 ] as HTMLUListElement ;
411
+ }
412
+
377
413
/**
378
414
* The tab bar add button node.
379
415
*
@@ -388,6 +424,18 @@ export class TabBar<T> extends Widget {
388
424
) [ 0 ] as HTMLDivElement ;
389
425
}
390
426
427
+ get scrollBeforeButtonNode ( ) : HTMLDivElement {
428
+ return this . node . getElementsByClassName (
429
+ 'lm-TabBar-scrollBeforeButton'
430
+ ) [ 0 ] as HTMLDivElement ;
431
+ }
432
+
433
+ get scrollAfterButtonNode ( ) : HTMLDivElement {
434
+ return this . node . getElementsByClassName (
435
+ 'lm-TabBar-scrollAfterButton'
436
+ ) [ 0 ] as HTMLDivElement ;
437
+ }
438
+
391
439
/**
392
440
* Add a tab to the end of the tab bar.
393
441
*
@@ -618,6 +666,9 @@ export class TabBar<T> extends Widget {
618
666
event . preventDefault ( ) ;
619
667
event . stopPropagation ( ) ;
620
668
break ;
669
+ case 'scroll' :
670
+ this . _evtScroll ( event ) ;
671
+ break ;
621
672
}
622
673
}
623
674
@@ -628,6 +679,7 @@ export class TabBar<T> extends Widget {
628
679
this . node . addEventListener ( 'mousedown' , this ) ; // <DEPRECATED>
629
680
this . node . addEventListener ( 'pointerdown' , this ) ;
630
681
this . node . addEventListener ( 'dblclick' , this ) ;
682
+ this . contentNode . addEventListener ( 'scroll' , this ) ;
631
683
}
632
684
633
685
/**
@@ -637,6 +689,7 @@ export class TabBar<T> extends Widget {
637
689
this . node . removeEventListener ( 'mousedown' , this ) ; // <DEPRECATED>
638
690
this . node . removeEventListener ( 'pointerdown' , this ) ;
639
691
this . node . removeEventListener ( 'dblclick' , this ) ;
692
+ this . contentNode . removeEventListener ( 'scroll' , this ) ;
640
693
this . _releaseMouse ( ) ;
641
694
}
642
695
@@ -655,6 +708,80 @@ export class TabBar<T> extends Widget {
655
708
content [ i ] = renderer . renderTab ( { title, current, zIndex } ) ;
656
709
}
657
710
VirtualDOM . render ( content , this . contentNode ) ;
711
+ this . maybeSwitchScrollButtons ( ) ;
712
+ }
713
+
714
+ protected onResize ( msg : Widget . ResizeMessage ) : void {
715
+ super . onResize ( msg ) ;
716
+ this . maybeSwitchScrollButtons ( ) ;
717
+ }
718
+
719
+ protected maybeSwitchScrollButtons ( ) {
720
+ const scrollBefore = this . scrollBeforeButtonNode ;
721
+ const scrollAfter = this . scrollAfterButtonNode ;
722
+ const state = this . _scrollState ;
723
+
724
+ if ( this . scrollingEnabled && state . totalSize > state . displayedSize ) {
725
+ // show both buttons
726
+ scrollBefore . style . display = null ;
727
+ scrollAfter . style . display = null ;
728
+ } else {
729
+ // hide both buttons
730
+ scrollBefore . style . display = 'none' ;
731
+ scrollAfter . style . display = 'none' ;
732
+ }
733
+ this . updateScrollingHints ( state ) ;
734
+ }
735
+
736
+ /**
737
+ * Adjust data reflecting the ability to scroll in each direction.
738
+ */
739
+ protected updateScrollingHints ( scrollState : Private . IScrollState ) {
740
+ const wrapper = this . contentWrapperNode ;
741
+
742
+ if ( ! this . scrollingEnabled ) {
743
+ delete wrapper . dataset [ 'canScroll' ] ;
744
+ return ;
745
+ }
746
+
747
+ const canScrollBefore = scrollState . position != 0 ;
748
+ const canScrollAfter =
749
+ scrollState . position != scrollState . totalSize - scrollState . displayedSize ;
750
+
751
+ if ( canScrollBefore && canScrollAfter ) {
752
+ wrapper . dataset [ 'canScroll' ] = 'both' ;
753
+ } else if ( canScrollBefore ) {
754
+ wrapper . dataset [ 'canScroll' ] = 'before' ;
755
+ } else if ( canScrollAfter ) {
756
+ wrapper . dataset [ 'canScroll' ] = 'after' ;
757
+ } else {
758
+ delete wrapper . dataset [ 'canScroll' ] ;
759
+ }
760
+ }
761
+
762
+ private get _scrollState ( ) : Private . IScrollState {
763
+ const content = this . contentNode ;
764
+ const contentRect = content . getBoundingClientRect ( ) ;
765
+ const isHorizontal = this . orientation === 'horizontal' ;
766
+ const contentSize = isHorizontal ? contentRect . width : contentRect . height ;
767
+ const scrollTotal = isHorizontal
768
+ ? content . scrollWidth
769
+ : content . scrollHeight ;
770
+ const scroll = Math . round (
771
+ isHorizontal ? content . scrollLeft : content . scrollTop
772
+ ) ;
773
+ return {
774
+ displayedSize : Math . round ( contentSize ) ,
775
+ totalSize : scrollTotal ,
776
+ position : scroll
777
+ } ;
778
+ }
779
+
780
+ /**
781
+ * Handle the `'dblclick'` event for the tab bar.
782
+ */
783
+ private _evtScroll ( event : Event ) : void {
784
+ this . updateScrollingHints ( this . _scrollState ) ;
658
785
}
659
786
660
787
/**
@@ -734,6 +861,52 @@ export class TabBar<T> extends Widget {
734
861
}
735
862
}
736
863
864
+ protected beginScrolling ( direction : '-' | '+' ) {
865
+ const initialRate = 5 ;
866
+ const rateIncrease = 1 ;
867
+ const maxRate = 20 ;
868
+ const intervalHandle = setInterval ( ( ) => {
869
+ if ( ! this . _scrollData ) {
870
+ this . stopScrolling ( ) ;
871
+ return ;
872
+ }
873
+ const rate = this . _scrollData . rate ;
874
+ const direction = this . _scrollData . scrollDirection ;
875
+ const change = ( direction == '+' ? 1 : - 1 ) * rate ;
876
+ if ( this . orientation == 'horizontal' ) {
877
+ this . contentNode . scrollLeft += change ;
878
+ } else {
879
+ this . contentNode . scrollTop += change ;
880
+ }
881
+ this . _scrollData . rate = Math . min (
882
+ this . _scrollData . rate + rateIncrease ,
883
+ maxRate
884
+ ) ;
885
+ const state = this . _scrollState ;
886
+ if (
887
+ ( direction == '-' && state . position == 0 ) ||
888
+ ( direction == '+' &&
889
+ state . totalSize == state . position + state . displayedSize )
890
+ ) {
891
+ this . stopScrolling ( ) ;
892
+ }
893
+ } , 50 ) ;
894
+ this . _scrollData = {
895
+ timerHandle : intervalHandle ,
896
+ scrollDirection : direction ,
897
+ rate : initialRate
898
+ } ;
899
+ }
900
+
901
+ protected stopScrolling ( ) {
902
+ if ( this . _scrollData ) {
903
+ clearInterval ( this . _scrollData . timerHandle ) ;
904
+ }
905
+ this . _scrollData = null ;
906
+ const state = this . _scrollState ;
907
+ this . updateScrollingHints ( state ) ;
908
+ }
909
+
737
910
/**
738
911
* Handle the `'mousedown'` event for the tab bar.
739
912
*/
@@ -753,6 +926,19 @@ export class TabBar<T> extends Widget {
753
926
this . addButtonEnabled &&
754
927
this . addButtonNode . contains ( event . target as HTMLElement ) ;
755
928
929
+ let scrollBeforeButtonClicked =
930
+ this . scrollingEnabled &&
931
+ this . scrollBeforeButtonNode . contains ( event . target as HTMLElement ) ;
932
+
933
+ let scrollAfterButtonClicked =
934
+ this . scrollingEnabled &&
935
+ this . scrollAfterButtonNode . contains ( event . target as HTMLElement ) ;
936
+
937
+ const anyButtonClicked =
938
+ addButtonClicked || scrollAfterButtonClicked || scrollBeforeButtonClicked ;
939
+
940
+ const contentNode = this . contentNode ;
941
+
756
942
// Lookup the tab nodes.
757
943
let tabs = this . contentNode . children ;
758
944
@@ -761,8 +947,8 @@ export class TabBar<T> extends Widget {
761
947
return ElementExt . hitTest ( tab , event . clientX , event . clientY ) ;
762
948
} ) ;
763
949
764
- // Do nothing if the press is not on a tab or the add button .
765
- if ( index === - 1 && ! addButtonClicked ) {
950
+ // Do nothing if the press is not on a tab or any of the buttons .
951
+ if ( index === - 1 && ! anyButtonClicked ) {
766
952
return ;
767
953
}
768
954
@@ -776,6 +962,10 @@ export class TabBar<T> extends Widget {
776
962
index : index ,
777
963
pressX : event . clientX ,
778
964
pressY : event . clientY ,
965
+ initialScrollPosition :
966
+ this . orientation == 'horizontal'
967
+ ? contentNode . scrollLeft
968
+ : contentNode . scrollTop ,
779
969
tabPos : - 1 ,
780
970
tabSize : - 1 ,
781
971
tabPressPos : - 1 ,
@@ -796,6 +986,10 @@ export class TabBar<T> extends Widget {
796
986
if ( event . button === 1 || addButtonClicked ) {
797
987
return ;
798
988
}
989
+ if ( scrollBeforeButtonClicked || scrollAfterButtonClicked ) {
990
+ this . beginScrolling ( scrollBeforeButtonClicked ? '-' : '+' ) ;
991
+ return ;
992
+ }
799
993
800
994
// Do nothing else if the close icon is clicked.
801
995
let icon = tabs [ index ] . querySelector ( this . renderer . closeIconSelector ) ;
@@ -902,8 +1096,24 @@ export class TabBar<T> extends Widget {
902
1096
}
903
1097
}
904
1098
1099
+ let overBeforeScrollButton =
1100
+ this . scrollingEnabled &&
1101
+ this . scrollBeforeButtonNode . contains ( event . target as HTMLElement ) ;
1102
+
1103
+ let overAfterScrollButton =
1104
+ this . scrollingEnabled &&
1105
+ this . scrollAfterButtonNode . contains ( event . target as HTMLElement ) ;
1106
+
1107
+ if ( overBeforeScrollButton || overAfterScrollButton ) {
1108
+ // Start scrolling if the mouse is over scroll buttons
1109
+ this . beginScrolling ( overBeforeScrollButton ? '-' : '+' ) ;
1110
+ } else {
1111
+ // Stop scrolling if mouse is not over scroll buttons
1112
+ this . stopScrolling ( ) ;
1113
+ }
1114
+
905
1115
// Update the positions of the tabs.
906
- Private . layoutTabs ( tabs , data , event , this . _orientation ) ;
1116
+ Private . layoutTabs ( tabs , data , event , this . _orientation , this . _scrollState ) ;
907
1117
}
908
1118
909
1119
/**
@@ -938,6 +1148,10 @@ export class TabBar<T> extends Widget {
938
1148
// Clear the drag data.
939
1149
this . _dragData = null ;
940
1150
1151
+ if ( this . _scrollData ) {
1152
+ this . stopScrolling ( ) ;
1153
+ return ;
1154
+ }
941
1155
// Handle clicking the add button.
942
1156
let addButtonClicked =
943
1157
this . addButtonEnabled &&
@@ -989,7 +1203,7 @@ export class TabBar<T> extends Widget {
989
1203
}
990
1204
991
1205
// Position the tab at its final resting position.
992
- Private . finalizeTabPosition ( data , this . _orientation ) ;
1206
+ Private . finalizeTabPosition ( data , this . _orientation , this . _scrollState ) ;
993
1207
994
1208
// Remove the dragging class from the tab so it can be transitioned.
995
1209
data . tab . classList . remove ( 'lm-mod-dragging' ) ;
@@ -1241,7 +1455,9 @@ export class TabBar<T> extends Widget {
1241
1455
private _titlesEditable : boolean = false ;
1242
1456
private _previousTitle : Title < T > | null = null ;
1243
1457
private _dragData : Private . IDragData | null = null ;
1458
+ private _scrollData : Private . IScrollData | null = null ;
1244
1459
private _addButtonEnabled : boolean = false ;
1460
+ private _scrollingEnabled : boolean = false ;
1245
1461
private _tabMoved = new Signal < this, TabBar . ITabMovedArgs < T > > ( this ) ;
1246
1462
private _currentChanged = new Signal < this, TabBar . ICurrentChangedArgs < T > > (
1247
1463
this
@@ -1776,6 +1992,33 @@ namespace Private {
1776
1992
*/
1777
1993
export const DETACH_THRESHOLD = 20 ;
1778
1994
1995
+ /**
1996
+ * A struct which holds the scroll data for a tab bar.
1997
+ */
1998
+ export interface IScrollData {
1999
+ timerHandle : number ;
2000
+ scrollDirection : '+' | '-' ;
2001
+ rate : number ;
2002
+ }
2003
+
2004
+ /**
2005
+ * A struct which holds the scroll state for a tab bar.
2006
+ */
2007
+ export interface IScrollState {
2008
+ /**
2009
+ * The size of the container where scrolling occurs (the visible part of the total).
2010
+ */
2011
+ displayedSize : number ;
2012
+ /**
2013
+ * The total size of the content to be scrolled.
2014
+ */
2015
+ totalSize : number ;
2016
+ /**
2017
+ * The current position (offset) of the scroll state.
2018
+ */
2019
+ position : number ;
2020
+ }
2021
+
1779
2022
/**
1780
2023
* A struct which holds the drag data for a tab bar.
1781
2024
*/
@@ -1795,6 +2038,11 @@ namespace Private {
1795
2038
*/
1796
2039
pressX : number ;
1797
2040
2041
+ /**
2042
+ * The initial scroll position
2043
+ */
2044
+ initialScrollPosition : number ;
2045
+
1798
2046
/**
1799
2047
* The mouse press client Y position.
1800
2048
*/
@@ -1890,16 +2138,37 @@ namespace Private {
1890
2138
*/
1891
2139
export function createNode ( ) : HTMLDivElement {
1892
2140
let node = document . createElement ( 'div' ) ;
2141
+ let scrollBefore = document . createElement ( 'div' ) ;
2142
+ scrollBefore . className =
2143
+ 'lm-TabBar-button lm-TabBar-scrollButton lm-TabBar-scrollBeforeButton' ;
2144
+ scrollBefore . style . display = 'none' ;
2145
+ scrollBefore . setAttribute ( 'role' , 'button' ) ;
2146
+ scrollBefore . innerText = '<' ;
2147
+ let scrollAfter = document . createElement ( 'div' ) ;
2148
+ scrollAfter . className =
2149
+ 'lm-TabBar-button lm-TabBar-scrollButton lm-TabBar-scrollAfterButton' ;
2150
+ scrollAfter . style . display = 'none' ;
2151
+ scrollAfter . setAttribute ( 'role' , 'button' ) ;
2152
+ scrollAfter . innerText = '>' ;
2153
+ node . appendChild ( scrollBefore ) ;
2154
+
1893
2155
let content = document . createElement ( 'ul' ) ;
1894
2156
content . setAttribute ( 'role' , 'tablist' ) ;
1895
2157
content . className = 'lm-TabBar-content' ;
1896
2158
/* <DEPRECATED> */
1897
2159
content . classList . add ( 'p-TabBar-content' ) ;
1898
2160
/* </DEPRECATED> */
1899
- node . appendChild ( content ) ;
2161
+
2162
+ let wrapper = document . createElement ( 'div' ) ;
2163
+ wrapper . className = 'lm-TabBar-wrapper' ;
2164
+ wrapper . appendChild ( content ) ;
2165
+ node . appendChild ( wrapper ) ;
2166
+
2167
+ node . appendChild ( scrollAfter ) ;
1900
2168
1901
2169
let add = document . createElement ( 'div' ) ;
1902
- add . className = 'lm-TabBar-addButton lm-mod-hidden' ;
2170
+ add . setAttribute ( 'role' , 'button' ) ;
2171
+ add . className = 'lm-TabBar-button lm-TabBar-addButton lm-mod-hidden' ;
1903
2172
node . appendChild ( add ) ;
1904
2173
return node ;
1905
2174
}
@@ -1976,23 +2245,24 @@ namespace Private {
1976
2245
tabs : HTMLCollection ,
1977
2246
data : IDragData ,
1978
2247
event : MouseEvent ,
1979
- orientation : TabBar . Orientation
2248
+ orientation : TabBar . Orientation ,
2249
+ scrollState : IScrollState
1980
2250
) : void {
1981
2251
// Compute the orientation-sensitive values.
1982
2252
let pressPos : number ;
1983
2253
let localPos : number ;
1984
2254
let clientPos : number ;
1985
- let clientSize : number ;
2255
+ const clientSize = scrollState . totalSize ;
2256
+ const scrollShift = scrollState . position - data . initialScrollPosition ;
2257
+
1986
2258
if ( orientation === 'horizontal' ) {
1987
- pressPos = data . pressX ;
1988
- localPos = event . clientX - data . contentRect ! . left ;
2259
+ pressPos = data . pressX - scrollShift ;
2260
+ localPos = event . clientX - data . contentRect ! . left + scrollState . position ;
1989
2261
clientPos = event . clientX ;
1990
- clientSize = data . contentRect ! . width ;
1991
2262
} else {
1992
- pressPos = data . pressY ;
1993
- localPos = event . clientY - data . contentRect ! . top ;
2263
+ pressPos = data . pressY - scrollShift ;
2264
+ localPos = event . clientY - data . contentRect ! . top + scrollState . position ;
1994
2265
clientPos = event . clientY ;
1995
- clientSize = data . contentRect ! . height ;
1996
2266
}
1997
2267
1998
2268
// Compute the target data.
@@ -2034,15 +2304,10 @@ namespace Private {
2034
2304
*/
2035
2305
export function finalizeTabPosition (
2036
2306
data : IDragData ,
2037
- orientation : TabBar . Orientation
2307
+ orientation : TabBar . Orientation ,
2308
+ scrollState : IScrollState
2038
2309
) : void {
2039
- // Compute the orientation-sensitive client size.
2040
- let clientSize : number ;
2041
- if ( orientation === 'horizontal' ) {
2042
- clientSize = data . contentRect ! . width ;
2043
- } else {
2044
- clientSize = data . contentRect ! . height ;
2045
- }
2310
+ const clientSize = scrollState . totalSize ;
2046
2311
2047
2312
// Compute the ideal final tab position.
2048
2313
let ideal : number ;
0 commit comments