Skip to content

Commit ecf3c2d

Browse files
committed
Address review comments
Implement a brief hysteresis also on the dismissal of the hover pop-up on mouse movement out of the target element so that (a) the user has an opportunity to mouse over the pop-up itself to interact with it, clicking links, hitting buttons, copying text (b) not dismiss the pop-up until the mouse pointer stops moving (helps accessibility for those with pointer accuracy challenges) The previous commit also had a misunderstanding of the the disposable returned by the DisposableCollection::push() method, that it would also dispose the original disposable, but it doesn't. Reworking disposable management fixed other issues in which transitioning to another hover target sometimes would not pop up. Signed-off-by: Christian W. Damus <[email protected]>
1 parent aaad3aa commit ecf3c2d

File tree

1 file changed

+31
-17
lines changed

1 file changed

+31
-17
lines changed

packages/core/src/browser/hover-service.ts

+31-17
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import '../../src/browser/style/hover-service.css';
2525

2626
export type HoverPosition = 'left' | 'right' | 'top' | 'bottom';
2727

28+
// Threshold, in milliseconds, over which a mouse movement is not considered
29+
// quick enough as to be ignored
30+
const quickMouseThresholdMillis = 200;
31+
2832
export namespace HoverPosition {
2933
export function invertIfNecessary(position: HoverPosition, target: DOMRect, host: DOMRect, totalWidth: number, totalHeight: number): HoverPosition {
3034
if (position === 'left') {
@@ -91,31 +95,36 @@ export class HoverService {
9195
}
9296
return this._hoverHost;
9397
}
98+
99+
// Pending presentation of the hover pop-up
94100
protected pendingTimeout: Disposable | undefined;
95-
protected pendingHoverCancellation: Disposable | undefined;
101+
// Pending cancellation of presentation of the hover pop-up or
102+
// dismissal of the hover pop-up currently presented
103+
protected pendingHoverCancel: Disposable | undefined;
96104
protected hoverTarget: HTMLElement | undefined;
97105
protected lastHidHover = Date.now();
98106
protected readonly disposeOnHide = new DisposableCollection();
99107

100108
requestHover(request: HoverRequest): void {
101109
if (request.target !== this.hoverTarget) {
110+
this.pendingHoverCancel?.dispose();
102111
this.cancelHover();
103112
const hoverDelay = this.getHoverDelay();
104113
this.pendingTimeout = disposableTimeout(() => this.renderHover(request), hoverDelay);
105114
if (hoverDelay > 0) {
106-
this.cancelHoverOnMouseOut(request);
115+
this.cancelPendingHoverOnMouseOut(request);
107116
}
108117
}
109118
}
110119

111120
protected getHoverDelay(): number {
112-
return Date.now() - this.lastHidHover < 200
121+
return Date.now() - this.lastHidHover < quickMouseThresholdMillis
113122
? 0
114123
: this.preferences.get('workbench.hover.delay', isOSX ? 1500 : 500);
115124
}
116125

117126
protected async renderHover(request: HoverRequest): Promise<void> {
118-
this.cancelPendingHoverCancellation();
127+
this.pendingHoverCancel?.dispose();
119128

120129
const host = this.hoverHost;
121130
let firstChild: HTMLElement | undefined;
@@ -201,36 +210,41 @@ export class HoverService {
201210
return position;
202211
}
203212

204-
/** Cancel the pending cancellation of hover. */
205-
protected cancelPendingHoverCancellation(): void {
206-
this.pendingHoverCancellation?.dispose();
207-
this.pendingHoverCancellation = undefined;
208-
}
209-
210213
protected listenForMouseOut(): void {
211214
const handleMouseMove = (e: MouseEvent) => {
212215
if (e.target instanceof Node && !this.hoverHost.contains(e.target) && !this.hoverTarget?.contains(e.target)) {
213-
this.cancelHover();
216+
this.pendingHoverCancel?.dispose();
217+
this.pendingHoverCancel = disposableTimeout(() => {
218+
if (!this.hoverHost.matches(':hover')) {
219+
this.cancelHover();
220+
}
221+
}, quickMouseThresholdMillis);
222+
this.disposeOnHide.push(this.pendingHoverCancel);
214223
}
215224
};
216225
document.addEventListener('mousemove', handleMouseMove);
217226
this.disposeOnHide.push({ dispose: () => document.removeEventListener('mousemove', handleMouseMove) });
218227
}
219228

220-
protected cancelHoverOnMouseOut(request: HoverRequest): void {
221-
this.cancelPendingHoverCancellation();
229+
protected cancelPendingHoverOnMouseOut(request: HoverRequest): void {
230+
this.pendingHoverCancel?.dispose();
222231
const target = request.target;
223232
const handleMouseLeave = (): void => {
224-
this.cancelHover();
225-
this.cancelPendingHoverCancellation();
233+
// Cancel the pending hover if it hasn't yet been presented
234+
if (!this.hoverHost.isConnected) {
235+
this.cancelHover();
236+
}
237+
this.pendingHoverCancel?.dispose();
226238
};
227239
target.addEventListener('mouseleave', handleMouseLeave);
228-
this.pendingHoverCancellation = this.disposeOnHide.push(
229-
Disposable.create(() => target.removeEventListener('mouseleave', handleMouseLeave)));
240+
this.pendingHoverCancel = Disposable.create(
241+
() => target.removeEventListener('mouseleave', handleMouseLeave));
242+
this.disposeOnHide.push(this.pendingHoverCancel);
230243
}
231244

232245
cancelHover(): void {
233246
this.pendingTimeout?.dispose();
247+
this.pendingHoverCancel?.dispose();
234248
this.unRenderHover();
235249
this.disposeOnHide.dispose();
236250
this.hoverTarget = undefined;

0 commit comments

Comments
 (0)