Path: blob/main/src/vs/editor/browser/services/hoverService/hoverWidget.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './hover.css';6import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { Event, Emitter } from '../../../../base/common/event.js';8import * as dom from '../../../../base/browser/dom.js';9import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';10import { KeyCode } from '../../../../base/common/keyCodes.js';11import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';12import { EDITOR_FONT_DEFAULTS, IEditorOptions } from '../../../common/config/editorOptions.js';13import { HoverAction, HoverPosition, HoverWidget as BaseHoverWidget, getHoverAccessibleViewHint } from '../../../../base/browser/ui/hover/hoverWidget.js';14import { Widget } from '../../../../base/browser/ui/widget.js';15import { AnchorPosition } from '../../../../base/browser/ui/contextview/contextview.js';16import { IOpenerService } from '../../../../platform/opener/common/opener.js';17import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';18import { MarkdownRenderer, openLinkFromMarkdown } from '../../widget/markdownRenderer/browser/markdownRenderer.js';19import { isMarkdownString } from '../../../../base/common/htmlContent.js';20import { localize } from '../../../../nls.js';21import { isMacintosh } from '../../../../base/common/platform.js';22import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';23import { status } from '../../../../base/browser/ui/aria/aria.js';24import type { IHoverOptions, IHoverTarget, IHoverWidget } from '../../../../base/browser/ui/hover/hover.js';25import { TimeoutTimer } from '../../../../base/common/async.js';26import { isNumber } from '../../../../base/common/types.js';2728const $ = dom.$;29type TargetRect = {30left: number;31right: number;32top: number;33bottom: number;34width: number;35height: number;36center: { x: number; y: number };37};3839const enum Constants {40PointerSize = 3,41HoverBorderWidth = 2,42HoverWindowEdgeMargin = 2,43}4445export class HoverWidget extends Widget implements IHoverWidget {46private readonly _messageListeners = new DisposableStore();47private readonly _lockMouseTracker: CompositeMouseTracker;4849private readonly _hover: BaseHoverWidget;50private readonly _hoverPointer: HTMLElement | undefined;51private readonly _hoverContainer: HTMLElement;52private readonly _target: IHoverTarget;53private readonly _linkHandler: (url: string) => any;5455private _isDisposed: boolean = false;56private _hoverPosition: HoverPosition;57private _forcePosition: boolean = false;58private _x: number = 0;59private _y: number = 0;60private _isLocked: boolean = false;61private _enableFocusTraps: boolean = false;62private _addedFocusTrap: boolean = false;63private _maxHeightRatioRelativeToWindow: number = 0.5;6465private get _targetWindow(): Window {66return dom.getWindow(this._target.targetElements[0]);67}68private get _targetDocumentElement(): HTMLElement {69return dom.getWindow(this._target.targetElements[0]).document.documentElement;70}7172get isDisposed(): boolean { return this._isDisposed; }73get isMouseIn(): boolean { return this._lockMouseTracker.isMouseIn; }74get domNode(): HTMLElement { return this._hover.containerDomNode; }7576private readonly _onDispose = this._register(new Emitter<void>());77get onDispose(): Event<void> { return this._onDispose.event; }78private readonly _onRequestLayout = this._register(new Emitter<void>());79get onRequestLayout(): Event<void> { return this._onRequestLayout.event; }8081get anchor(): AnchorPosition { return this._hoverPosition === HoverPosition.BELOW ? AnchorPosition.BELOW : AnchorPosition.ABOVE; }82get x(): number { return this._x; }83get y(): number { return this._y; }8485/**86* Whether the hover is "locked" by holding the alt/option key. When locked, the hover will not87* hide and can be hovered regardless of whether the `hideOnHover` hover option is set.88*/89get isLocked(): boolean { return this._isLocked; }90set isLocked(value: boolean) {91if (this._isLocked === value) {92return;93}94this._isLocked = value;95this._hoverContainer.classList.toggle('locked', this._isLocked);96}9798constructor(99options: IHoverOptions,100@IKeybindingService private readonly _keybindingService: IKeybindingService,101@IConfigurationService private readonly _configurationService: IConfigurationService,102@IOpenerService private readonly _openerService: IOpenerService,103@IInstantiationService private readonly _instantiationService: IInstantiationService,104@IAccessibilityService private readonly _accessibilityService: IAccessibilityService105) {106super();107108this._linkHandler = options.linkHandler || (url => {109return openLinkFromMarkdown(this._openerService, url, isMarkdownString(options.content) ? options.content.isTrusted : undefined);110});111112this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target);113114this._hoverPointer = options.appearance?.showPointer ? $('div.workbench-hover-pointer') : undefined;115this._hover = this._register(new BaseHoverWidget(!options.appearance?.skipFadeInAnimation));116this._hover.containerDomNode.classList.add('workbench-hover');117if (options.appearance?.compact) {118this._hover.containerDomNode.classList.add('workbench-hover', 'compact');119}120if (options.additionalClasses) {121this._hover.containerDomNode.classList.add(...options.additionalClasses);122}123if (options.position?.forcePosition) {124this._forcePosition = true;125}126if (options.trapFocus) {127this._enableFocusTraps = true;128}129130const maxHeightRatio = options.appearance?.maxHeightRatio;131if (maxHeightRatio !== undefined && maxHeightRatio > 0 && maxHeightRatio <= 1) {132this._maxHeightRatioRelativeToWindow = maxHeightRatio;133}134135// Default to position above when the position is unspecified or a mouse event136this._hoverPosition = options.position?.hoverPosition === undefined137? HoverPosition.ABOVE138: isNumber(options.position.hoverPosition)139? options.position.hoverPosition140: HoverPosition.BELOW;141142// Don't allow mousedown out of the widget, otherwise preventDefault will call and text will143// not be selected.144this.onmousedown(this._hover.containerDomNode, e => e.stopPropagation());145146// Hide hover on escape147this.onkeydown(this._hover.containerDomNode, e => {148if (e.equals(KeyCode.Escape)) {149this.dispose();150}151});152153// Hide when the window loses focus154this._register(dom.addDisposableListener(this._targetWindow, 'blur', () => this.dispose()));155156const rowElement = $('div.hover-row.markdown-hover');157const contentsElement = $('div.hover-contents');158if (typeof options.content === 'string') {159contentsElement.textContent = options.content;160contentsElement.style.whiteSpace = 'pre-wrap';161162} else if (dom.isHTMLElement(options.content)) {163contentsElement.appendChild(options.content);164contentsElement.classList.add('html-hover-contents');165166} else {167const markdown = options.content;168const mdRenderer = this._instantiationService.createInstance(169MarkdownRenderer,170{ codeBlockFontFamily: this._configurationService.getValue<IEditorOptions>('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily }171);172173const { element, dispose } = mdRenderer.render(markdown, {174actionHandler: (content) => this._linkHandler(content),175asyncRenderCallback: () => {176contentsElement.classList.add('code-hover-contents');177this.layout();178// This changes the dimensions of the hover so trigger a layout179this._onRequestLayout.fire();180}181});182contentsElement.appendChild(element);183this._register(toDisposable(dispose));184}185rowElement.appendChild(contentsElement);186this._hover.contentsDomNode.appendChild(rowElement);187188if (options.actions && options.actions.length > 0) {189const statusBarElement = $('div.hover-row.status-bar');190const actionsElement = $('div.actions');191options.actions.forEach(action => {192const keybinding = this._keybindingService.lookupKeybinding(action.commandId);193const keybindingLabel = keybinding ? keybinding.getLabel() : null;194this._register(HoverAction.render(actionsElement, {195label: action.label,196commandId: action.commandId,197run: e => {198action.run(e);199this.dispose();200},201iconClass: action.iconClass202}, keybindingLabel));203});204statusBarElement.appendChild(actionsElement);205this._hover.containerDomNode.appendChild(statusBarElement);206}207208this._hoverContainer = $('div.workbench-hover-container');209if (this._hoverPointer) {210this._hoverContainer.appendChild(this._hoverPointer);211}212this._hoverContainer.appendChild(this._hover.containerDomNode);213214// Determine whether to hide on hover215let hideOnHover: boolean;216if (options.actions && options.actions.length > 0) {217// If there are actions, require hover so they can be accessed218hideOnHover = false;219} else {220if (options.persistence?.hideOnHover === undefined) {221// When unset, will default to true when it's a string or when it's markdown that222// appears to have a link using a naive check for '](' and '</a>'223hideOnHover = typeof options.content === 'string' ||224isMarkdownString(options.content) && !options.content.value.includes('](') && !options.content.value.includes('</a>');225} else {226// It's set explicitly227hideOnHover = options.persistence.hideOnHover;228}229}230231// Show the hover hint if needed232if (options.appearance?.showHoverHint) {233const statusBarElement = $('div.hover-row.status-bar');234const infoElement = $('div.info');235infoElement.textContent = localize('hoverhint', 'Hold {0} key to mouse over', isMacintosh ? 'Option' : 'Alt');236statusBarElement.appendChild(infoElement);237this._hover.containerDomNode.appendChild(statusBarElement);238}239240const mouseTrackerTargets = [...this._target.targetElements];241if (!hideOnHover) {242mouseTrackerTargets.push(this._hoverContainer);243}244const mouseTracker = this._register(new CompositeMouseTracker(mouseTrackerTargets));245this._register(mouseTracker.onMouseOut(() => {246if (!this._isLocked) {247this.dispose();248}249}));250251// Setup another mouse tracker when hideOnHover is set in order to track the hover as well252// when it is locked. This ensures the hover will hide on mouseout after alt has been253// released to unlock the element.254if (hideOnHover) {255const mouseTracker2Targets = [...this._target.targetElements, this._hoverContainer];256this._lockMouseTracker = this._register(new CompositeMouseTracker(mouseTracker2Targets));257this._register(this._lockMouseTracker.onMouseOut(() => {258if (!this._isLocked) {259this.dispose();260}261}));262} else {263this._lockMouseTracker = mouseTracker;264}265}266267private addFocusTrap() {268if (!this._enableFocusTraps || this._addedFocusTrap) {269return;270}271this._addedFocusTrap = true;272273// Add a hover tab loop if the hover has at least one element with a valid tabIndex274const firstContainerFocusElement = this._hover.containerDomNode;275const lastContainerFocusElement = this.findLastFocusableChild(this._hover.containerDomNode);276if (lastContainerFocusElement) {277const beforeContainerFocusElement = dom.prepend(this._hoverContainer, $('div'));278const afterContainerFocusElement = dom.append(this._hoverContainer, $('div'));279beforeContainerFocusElement.tabIndex = 0;280afterContainerFocusElement.tabIndex = 0;281this._register(dom.addDisposableListener(afterContainerFocusElement, 'focus', (e) => {282firstContainerFocusElement.focus();283e.preventDefault();284}));285this._register(dom.addDisposableListener(beforeContainerFocusElement, 'focus', (e) => {286lastContainerFocusElement.focus();287e.preventDefault();288}));289}290}291292private findLastFocusableChild(root: Node): HTMLElement | undefined {293if (root.hasChildNodes()) {294for (let i = 0; i < root.childNodes.length; i++) {295const node = root.childNodes.item(root.childNodes.length - i - 1);296if (node.nodeType === node.ELEMENT_NODE) {297const parsedNode = node as HTMLElement;298if (typeof parsedNode.tabIndex === 'number' && parsedNode.tabIndex >= 0) {299return parsedNode;300}301}302const recursivelyFoundElement = this.findLastFocusableChild(node);303if (recursivelyFoundElement) {304return recursivelyFoundElement;305}306}307}308return undefined;309}310311public render(container: HTMLElement): void {312container.appendChild(this._hoverContainer);313const hoverFocused = this._hoverContainer.contains(this._hoverContainer.ownerDocument.activeElement);314const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint(this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(), this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel());315if (accessibleViewHint) {316317status(accessibleViewHint);318}319this.layout();320this.addFocusTrap();321}322323public layout() {324this._hover.containerDomNode.classList.remove('right-aligned');325this._hover.contentsDomNode.style.maxHeight = '';326327const getZoomAccountedBoundingClientRect = (e: HTMLElement) => {328const zoom = dom.getDomNodeZoomLevel(e);329330const boundingRect = e.getBoundingClientRect();331return {332top: boundingRect.top * zoom,333bottom: boundingRect.bottom * zoom,334right: boundingRect.right * zoom,335left: boundingRect.left * zoom,336};337};338339const targetBounds = this._target.targetElements.map(e => getZoomAccountedBoundingClientRect(e));340const { top, right, bottom, left } = targetBounds[0];341const width = right - left;342const height = bottom - top;343344const targetRect: TargetRect = {345top, right, bottom, left, width, height,346center: {347x: left + (width / 2),348y: top + (height / 2)349}350};351352// These calls adjust the position depending on spacing.353this.adjustHorizontalHoverPosition(targetRect);354this.adjustVerticalHoverPosition(targetRect);355// This call limits the maximum height of the hover.356this.adjustHoverMaxHeight(targetRect);357358// Offset the hover position if there is a pointer so it aligns with the target element359this._hoverContainer.style.padding = '';360this._hoverContainer.style.margin = '';361if (this._hoverPointer) {362switch (this._hoverPosition) {363case HoverPosition.RIGHT:364targetRect.left += Constants.PointerSize;365targetRect.right += Constants.PointerSize;366this._hoverContainer.style.paddingLeft = `${Constants.PointerSize}px`;367this._hoverContainer.style.marginLeft = `${-Constants.PointerSize}px`;368break;369case HoverPosition.LEFT:370targetRect.left -= Constants.PointerSize;371targetRect.right -= Constants.PointerSize;372this._hoverContainer.style.paddingRight = `${Constants.PointerSize}px`;373this._hoverContainer.style.marginRight = `${-Constants.PointerSize}px`;374break;375case HoverPosition.BELOW:376targetRect.top += Constants.PointerSize;377targetRect.bottom += Constants.PointerSize;378this._hoverContainer.style.paddingTop = `${Constants.PointerSize}px`;379this._hoverContainer.style.marginTop = `${-Constants.PointerSize}px`;380break;381case HoverPosition.ABOVE:382targetRect.top -= Constants.PointerSize;383targetRect.bottom -= Constants.PointerSize;384this._hoverContainer.style.paddingBottom = `${Constants.PointerSize}px`;385this._hoverContainer.style.marginBottom = `${-Constants.PointerSize}px`;386break;387}388389targetRect.center.x = targetRect.left + (width / 2);390targetRect.center.y = targetRect.top + (height / 2);391}392393this.computeXCordinate(targetRect);394this.computeYCordinate(targetRect);395396if (this._hoverPointer) {397// reset398this._hoverPointer.classList.remove('top');399this._hoverPointer.classList.remove('left');400this._hoverPointer.classList.remove('right');401this._hoverPointer.classList.remove('bottom');402403this.setHoverPointerPosition(targetRect);404}405this._hover.onContentsChanged();406}407408private computeXCordinate(target: TargetRect): void {409const hoverWidth = this._hover.containerDomNode.clientWidth + Constants.HoverBorderWidth;410411if (this._target.x !== undefined) {412this._x = this._target.x;413}414415else if (this._hoverPosition === HoverPosition.RIGHT) {416this._x = target.right;417}418419else if (this._hoverPosition === HoverPosition.LEFT) {420this._x = target.left - hoverWidth;421}422423else {424if (this._hoverPointer) {425this._x = target.center.x - (this._hover.containerDomNode.clientWidth / 2);426} else {427this._x = target.left;428}429430// Hover is going beyond window towards right end431if (this._x + hoverWidth >= this._targetDocumentElement.clientWidth) {432this._hover.containerDomNode.classList.add('right-aligned');433this._x = Math.max(this._targetDocumentElement.clientWidth - hoverWidth - Constants.HoverWindowEdgeMargin, this._targetDocumentElement.clientLeft);434}435}436437// Hover is going beyond window towards left end438if (this._x < this._targetDocumentElement.clientLeft) {439this._x = target.left + Constants.HoverWindowEdgeMargin;440}441442}443444private computeYCordinate(target: TargetRect): void {445if (this._target.y !== undefined) {446this._y = this._target.y;447}448449else if (this._hoverPosition === HoverPosition.ABOVE) {450this._y = target.top;451}452453else if (this._hoverPosition === HoverPosition.BELOW) {454this._y = target.bottom - 2;455}456457else {458if (this._hoverPointer) {459this._y = target.center.y + (this._hover.containerDomNode.clientHeight / 2);460} else {461this._y = target.bottom;462}463}464465// Hover on bottom is going beyond window466if (this._y > this._targetWindow.innerHeight) {467this._y = target.bottom;468}469}470471private adjustHorizontalHoverPosition(target: TargetRect): void {472// Do not adjust horizontal hover position if x cordiante is provided473if (this._target.x !== undefined) {474return;475}476477const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0);478479// When force position is enabled, restrict max width480if (this._forcePosition) {481const padding = hoverPointerOffset + Constants.HoverBorderWidth;482if (this._hoverPosition === HoverPosition.RIGHT) {483this._hover.containerDomNode.style.maxWidth = `${this._targetDocumentElement.clientWidth - target.right - padding}px`;484} else if (this._hoverPosition === HoverPosition.LEFT) {485this._hover.containerDomNode.style.maxWidth = `${target.left - padding}px`;486}487return;488}489490// Position hover on right to target491if (this._hoverPosition === HoverPosition.RIGHT) {492const roomOnRight = this._targetDocumentElement.clientWidth - target.right;493// Hover on the right is going beyond window.494if (roomOnRight < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {495const roomOnLeft = target.left;496// There's enough room on the left, flip the hover position497if (roomOnLeft >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {498this._hoverPosition = HoverPosition.LEFT;499}500// Hover on the left would go beyond window too501else {502this._hoverPosition = HoverPosition.BELOW;503}504}505}506// Position hover on left to target507else if (this._hoverPosition === HoverPosition.LEFT) {508509const roomOnLeft = target.left;510// Hover on the left is going beyond window.511if (roomOnLeft < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {512const roomOnRight = this._targetDocumentElement.clientWidth - target.right;513// There's enough room on the right, flip the hover position514if (roomOnRight >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {515this._hoverPosition = HoverPosition.RIGHT;516}517// Hover on the right would go beyond window too518else {519this._hoverPosition = HoverPosition.BELOW;520}521}522// Hover on the left is going beyond window.523if (target.left - this._hover.containerDomNode.clientWidth - hoverPointerOffset <= this._targetDocumentElement.clientLeft) {524this._hoverPosition = HoverPosition.RIGHT;525}526}527}528529private adjustVerticalHoverPosition(target: TargetRect): void {530// Do not adjust vertical hover position if the y coordinate is provided531// or the position is forced532if (this._target.y !== undefined || this._forcePosition) {533return;534}535536const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0);537538// Position hover on top of the target539if (this._hoverPosition === HoverPosition.ABOVE) {540// Hover on top is going beyond window541if (target.top - this._hover.containerDomNode.clientHeight - hoverPointerOffset < 0) {542this._hoverPosition = HoverPosition.BELOW;543}544}545546// Position hover below the target547else if (this._hoverPosition === HoverPosition.BELOW) {548// Hover on bottom is going beyond window549if (target.bottom + this._hover.containerDomNode.clientHeight + hoverPointerOffset > this._targetWindow.innerHeight) {550this._hoverPosition = HoverPosition.ABOVE;551}552}553}554555private adjustHoverMaxHeight(target: TargetRect): void {556let maxHeight = this._targetWindow.innerHeight * this._maxHeightRatioRelativeToWindow;557558// When force position is enabled, restrict max height559if (this._forcePosition) {560const padding = (this._hoverPointer ? Constants.PointerSize : 0) + Constants.HoverBorderWidth;561if (this._hoverPosition === HoverPosition.ABOVE) {562maxHeight = Math.min(maxHeight, target.top - padding);563} else if (this._hoverPosition === HoverPosition.BELOW) {564maxHeight = Math.min(maxHeight, this._targetWindow.innerHeight - target.bottom - padding);565}566}567568this._hover.containerDomNode.style.maxHeight = `${maxHeight}px`;569if (this._hover.contentsDomNode.clientHeight < this._hover.contentsDomNode.scrollHeight) {570// Add padding for a vertical scrollbar571const extraRightPadding = `${this._hover.scrollbar.options.verticalScrollbarSize}px`;572if (this._hover.contentsDomNode.style.paddingRight !== extraRightPadding) {573this._hover.contentsDomNode.style.paddingRight = extraRightPadding;574}575}576}577578private setHoverPointerPosition(target: TargetRect): void {579if (!this._hoverPointer) {580return;581}582583switch (this._hoverPosition) {584case HoverPosition.LEFT:585case HoverPosition.RIGHT: {586this._hoverPointer.classList.add(this._hoverPosition === HoverPosition.LEFT ? 'right' : 'left');587const hoverHeight = this._hover.containerDomNode.clientHeight;588589// If hover is taller than target, then show the pointer at the center of target590if (hoverHeight > target.height) {591this._hoverPointer.style.top = `${target.center.y - (this._y - hoverHeight) - Constants.PointerSize}px`;592}593594// Otherwise show the pointer at the center of hover595else {596this._hoverPointer.style.top = `${Math.round((hoverHeight / 2)) - Constants.PointerSize}px`;597}598599break;600}601case HoverPosition.ABOVE:602case HoverPosition.BELOW: {603this._hoverPointer.classList.add(this._hoverPosition === HoverPosition.ABOVE ? 'bottom' : 'top');604const hoverWidth = this._hover.containerDomNode.clientWidth;605606// Position pointer at the center of the hover607let pointerLeftPosition = Math.round((hoverWidth / 2)) - Constants.PointerSize;608609// If pointer goes beyond target then position it at the center of the target610const pointerX = this._x + pointerLeftPosition;611if (pointerX < target.left || pointerX > target.right) {612pointerLeftPosition = target.center.x - this._x - Constants.PointerSize;613}614615this._hoverPointer.style.left = `${pointerLeftPosition}px`;616break;617}618}619}620621public focus() {622this._hover.containerDomNode.focus();623}624625public hide(): void {626this.dispose();627}628629public override dispose(): void {630if (!this._isDisposed) {631this._onDispose.fire();632this._target.dispose?.();633this._hoverContainer.remove();634this._messageListeners.dispose();635super.dispose();636}637this._isDisposed = true;638}639}640641class CompositeMouseTracker extends Widget {642private _isMouseIn: boolean = true;643private readonly _mouseTimer: MutableDisposable<TimeoutTimer> = this._register(new MutableDisposable());644645private readonly _onMouseOut = this._register(new Emitter<void>());646get onMouseOut(): Event<void> { return this._onMouseOut.event; }647648get isMouseIn(): boolean { return this._isMouseIn; }649650/**651* @param _elements The target elements to track mouse in/out events on.652* @param _eventDebounceDelay The delay in ms to debounce the event firing. This is used to653* allow a short period for the mouse to move into the hover or a nearby target element. For654* example hovering a scroll bar will not hide the hover immediately.655*/656constructor(657private _elements: HTMLElement[],658private _eventDebounceDelay: number = 200659) {660super();661662for (const element of this._elements) {663this.onmouseover(element, () => this._onTargetMouseOver());664this.onmouseleave(element, () => this._onTargetMouseLeave());665}666}667668private _onTargetMouseOver(): void {669this._isMouseIn = true;670this._mouseTimer.clear();671}672673private _onTargetMouseLeave(): void {674this._isMouseIn = false;675// Evaluate whether the mouse is still outside asynchronously such that other mouse targets676// have the opportunity to first their mouse in event.677this._mouseTimer.value = new TimeoutTimer(() => this._fireIfMouseOutside(), this._eventDebounceDelay);678}679680private _fireIfMouseOutside(): void {681if (!this._isMouseIn) {682this._onMouseOut.fire();683}684}685}686687class ElementHoverTarget implements IHoverTarget {688readonly targetElements: readonly HTMLElement[];689690constructor(691private _element: HTMLElement692) {693this.targetElements = [this._element];694}695696dispose(): void {697}698}699700701