Skip to content

Commit 2db0724

Browse files
committed
Improve scrolling: smoother, better draging, wheel support
- smoother: use `requestAnimationFrame` instead of `setInterval` - dragging: - detach by scrolling (requires scroll delta computation) - update drag when scrolling (by dispatching last event again to invoke handler as if mouse moved) - wheel: allow to scroll using mouse wheel
1 parent de12bf8 commit 2db0724

File tree

1 file changed

+117
-44
lines changed

1 file changed

+117
-44
lines changed

packages/widgets/src/tabbar.ts

Lines changed: 117 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -636,15 +636,19 @@ export class TabBar<T> extends Widget {
636636
handleEvent(event: Event): void {
637637
switch (event.type) {
638638
case 'pointerdown':
639-
this._evtPointerDown(event as PointerEvent);
639+
this._lastMouseEvent = event as MouseEvent;
640+
this._evtPointerDown(event as MouseEvent);
640641
break;
641642
case 'pointermove':
642-
this._evtPointerMove(event as PointerEvent);
643+
this._lastMouseEvent = event as MouseEvent;
644+
this._evtPointerMove(event as MouseEvent);
643645
break;
644646
case 'pointerup':
645-
this._evtPointerUp(event as PointerEvent);
647+
this._lastMouseEvent = event as MouseEvent;
648+
this._evtPointerUp(event as MouseEvent);
646649
break;
647650
case 'dblclick':
651+
this._lastMouseEvent = event as MouseEvent;
648652
this._evtDblClick(event as MouseEvent);
649653
break;
650654
case 'keydown':
@@ -657,6 +661,10 @@ export class TabBar<T> extends Widget {
657661
case 'scroll':
658662
this._evtScroll(event);
659663
break;
664+
case 'wheel':
665+
this._evtWheel(event as WheelEvent);
666+
event.preventDefault();
667+
break;
660668
}
661669
}
662670

@@ -667,6 +675,7 @@ export class TabBar<T> extends Widget {
667675
this.node.addEventListener('pointerdown', this);
668676
this.node.addEventListener('dblclick', this);
669677
this.contentNode.addEventListener('scroll', this);
678+
this.contentNode.addEventListener('wheel', this);
670679
}
671680

672681
/**
@@ -676,6 +685,7 @@ export class TabBar<T> extends Widget {
676685
this.node.removeEventListener('pointerdown', this);
677686
this.node.removeEventListener('dblclick', this);
678687
this.contentNode.removeEventListener('scroll', this);
688+
this.contentNode.removeEventListener('wheel', this);
679689
this._releaseMouse();
680690
}
681691

@@ -797,6 +807,10 @@ export class TabBar<T> extends Widget {
797807
this.updateScrollingHints(this._scrollState);
798808
}
799809

810+
private _evtWheel(event: WheelEvent): void {
811+
this.scrollBy(event.deltaY);
812+
}
813+
800814
/**
801815
* Handle the `'dblclick'` event for the tab bar.
802816
*/
@@ -874,25 +888,47 @@ export class TabBar<T> extends Widget {
874888
}
875889
}
876890

891+
protected scrollBy(change: number) {
892+
const orientation = this.orientation;
893+
const contentNode = this.contentNode;
894+
895+
if (orientation == 'horizontal') {
896+
contentNode.scrollLeft += change;
897+
} else {
898+
contentNode.scrollTop += change;
899+
}
900+
// Force-update drag state by dispatching last recorded mouse event.
901+
if (this._lastMouseEvent) {
902+
this._evtPointerMove(this._lastMouseEvent);
903+
}
904+
}
905+
877906
protected beginScrolling(direction: '-' | '+') {
878-
const initialRate = 5;
879-
const rateIncrease = 1;
880-
const maxRate = 20;
881-
const intervalHandle = setInterval(() => {
907+
// How many pixels should be scrolled per second initially?
908+
const initialRate = 150;
909+
// By how much should the scrolling rate increase per second?
910+
const rateIncrease = 80;
911+
// What should be the maximal scrolling speed (pixels/second?)
912+
const maxRate = 450;
913+
914+
let previousTime = performance.now();
915+
916+
const step = () => {
882917
if (!this._scrollData) {
883918
this.stopScrolling();
884919
return;
885920
}
921+
const stepTime = performance.now();
922+
const secondsChange = (stepTime - previousTime) / 1000;
923+
previousTime = stepTime;
886924
const rate = this._scrollData.rate;
887-
const direction = this._scrollData.scrollDirection;
888-
const change = (direction == '+' ? 1 : -1) * rate;
889-
if (this.orientation == 'horizontal') {
890-
this.contentNode.scrollLeft += change;
891-
} else {
892-
this.contentNode.scrollTop += change;
893-
}
925+
const direction = this._scrollData.direction;
926+
927+
const change = (direction == '+' ? 1 : -1) * rate * secondsChange;
928+
929+
this.scrollBy(change);
894930
this._scrollData.rate = Math.min(
895-
this._scrollData.rate + rateIncrease,
931+
this._scrollData.rate + rateIncrease * secondsChange,
896932
maxRate
897933
);
898934
const state = this._scrollState;
@@ -902,18 +938,26 @@ export class TabBar<T> extends Widget {
902938
state.totalSize == state.position + state.displayedSize)
903939
) {
904940
this.stopScrolling();
941+
return;
905942
}
906-
}, 50);
943+
window.requestAnimationFrame(step);
944+
};
945+
946+
const shouldRequest = !this._scrollData;
947+
907948
this._scrollData = {
908-
timerHandle: intervalHandle,
909-
scrollDirection: direction,
949+
direction: direction,
910950
rate: initialRate
911951
};
952+
953+
if (shouldRequest) {
954+
window.requestAnimationFrame(step);
955+
}
912956
}
913957

914958
protected stopScrolling() {
915-
if (this._scrollData) {
916-
clearInterval(this._scrollData.timerHandle);
959+
if (!this._scrollData) {
960+
return;
917961
}
918962
this._scrollData = null;
919963
const state = this._scrollState;
@@ -969,6 +1013,18 @@ export class TabBar<T> extends Widget {
9691013
event.preventDefault();
9701014
event.stopPropagation();
9711015

1016+
// Add the document mouse up listener.
1017+
this.document.addEventListener('pointerup', this, true);
1018+
1019+
// Do nothing else if the middle button or add button is clicked.
1020+
if (event.button === 1 || addButtonClicked) {
1021+
return;
1022+
}
1023+
if (scrollBeforeButtonClicked || scrollAfterButtonClicked) {
1024+
this.beginScrolling(scrollBeforeButtonClicked ? '-' : '+');
1025+
return;
1026+
}
1027+
9721028
// Initialize the non-measured parts of the drag data.
9731029
this._dragData = {
9741030
tab: tabs[index] as HTMLElement,
@@ -1039,6 +1095,21 @@ export class TabBar<T> extends Widget {
10391095
* Handle the `'pointermove'` event for the tab bar.
10401096
*/
10411097
private _evtPointerMove(event: PointerEvent | MouseEvent): void {
1098+
let overBeforeScrollButton =
1099+
this.scrollingEnabled &&
1100+
this.scrollBeforeButtonNode.contains(event.target as HTMLElement);
1101+
1102+
let overAfterScrollButton =
1103+
this.scrollingEnabled &&
1104+
this.scrollAfterButtonNode.contains(event.target as HTMLElement);
1105+
1106+
const isOverScrollButton = overBeforeScrollButton || overAfterScrollButton;
1107+
1108+
if (!isOverScrollButton) {
1109+
// Stop scrolling if mouse is not over scroll buttons
1110+
this.stopScrolling();
1111+
}
1112+
10421113
// Do nothing if no drag is in progress.
10431114
let data = this._dragData;
10441115
if (!data) {
@@ -1049,14 +1120,22 @@ export class TabBar<T> extends Widget {
10491120
event.preventDefault();
10501121
event.stopPropagation();
10511122

1052-
// Lookup the tab nodes.
1053-
let tabs = this.contentNode.children;
1123+
if (isOverScrollButton) {
1124+
// Start scrolling if the mouse is over scroll buttons
1125+
this.beginScrolling(overBeforeScrollButton ? '-' : '+');
1126+
}
10541127

10551128
// Bail early if the drag threshold has not been met.
1056-
if (!data.dragActive && !Private.dragExceeded(data, event)) {
1129+
if (
1130+
!data.dragActive &&
1131+
!Private.dragExceeded(data, event, this._scrollState)
1132+
) {
10571133
return;
10581134
}
10591135

1136+
// Lookup the tab nodes.
1137+
let tabs = this.contentNode.children;
1138+
10601139
// Activate the drag if necessary.
10611140
if (!data.dragActive) {
10621141
// Fill in the rest of the drag data measurements.
@@ -1103,22 +1182,6 @@ export class TabBar<T> extends Widget {
11031182
}
11041183
}
11051184

1106-
let overBeforeScrollButton =
1107-
this.scrollingEnabled &&
1108-
this.scrollBeforeButtonNode.contains(event.target as HTMLElement);
1109-
1110-
let overAfterScrollButton =
1111-
this.scrollingEnabled &&
1112-
this.scrollAfterButtonNode.contains(event.target as HTMLElement);
1113-
1114-
if (overBeforeScrollButton || overAfterScrollButton) {
1115-
// Start scrolling if the mouse is over scroll buttons
1116-
this.beginScrolling(overBeforeScrollButton ? '-' : '+');
1117-
} else {
1118-
// Stop scrolling if mouse is not over scroll buttons
1119-
this.stopScrolling();
1120-
}
1121-
11221185
// Update the positions of the tabs.
11231186
Private.layoutTabs(tabs, data, event, this._orientation, this._scrollState);
11241187
}
@@ -1135,6 +1198,9 @@ export class TabBar<T> extends Widget {
11351198
// Do nothing if no drag is in progress.
11361199
const data = this._dragData;
11371200
if (!data) {
1201+
if (this._scrollData) {
1202+
this.stopScrolling();
1203+
}
11381204
return;
11391205
}
11401206

@@ -1450,6 +1516,7 @@ export class TabBar<T> extends Widget {
14501516
private _titles: Title<T>[] = [];
14511517
private _orientation: TabBar.Orientation;
14521518
private _document: Document | ShadowRoot;
1519+
private _lastMouseEvent: MouseEvent | null = null;
14531520
private _titlesEditable: boolean = false;
14541521
private _previousTitle: Title<T> | null = null;
14551522
private _dragData: Private.IDragData | null = null;
@@ -1961,8 +2028,7 @@ namespace Private {
19612028
* A struct which holds the scroll data for a tab bar.
19622029
*/
19632030
export interface IScrollData {
1964-
timerHandle: number;
1965-
scrollDirection: '+' | '-';
2031+
direction: '+' | '-';
19662032
rate: number;
19672033
}
19682034

@@ -2177,12 +2243,19 @@ namespace Private {
21772243
}
21782244

21792245
/**
2180-
* Test if the event exceeds the drag threshold.
2246+
* Test if the event or scroll state exceeds the drag threshold.
21812247
*/
2182-
export function dragExceeded(data: IDragData, event: MouseEvent): boolean {
2248+
export function dragExceeded(
2249+
data: IDragData,
2250+
event: MouseEvent,
2251+
scrollState: IScrollState | null
2252+
): boolean {
21832253
let dx = Math.abs(event.clientX - data.pressX);
21842254
let dy = Math.abs(event.clientY - data.pressY);
2185-
return dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD;
2255+
let ds = scrollState
2256+
? Math.abs(data.initialScrollPosition - scrollState.position)
2257+
: 0;
2258+
return dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD || ds >= DRAG_THRESHOLD;
21862259
}
21872260

21882261
/**

0 commit comments

Comments
 (0)