Make List / Tree Hovers Focusable (#212818)

* fixes #211951

* fix test

* Support updatable hover
This commit is contained in:
Benjamin Christopher Simmonds 2024-05-15 18:39:03 +02:00 committed by GitHub
parent b8881656c6
commit c924205321
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 39 additions and 39 deletions

View file

@ -43,6 +43,11 @@ export interface IHoverDelegate2 {
// TODO: Change hoverDelegate arg to exclude the actual delegate and instead use the new options
setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions): IUpdatableHover;
/**
* Shows the hover for the given element if one has been setup.
*/
triggerUpdatableHover(htmlElement: HTMLElement): void;
}
export interface IHoverWidget extends IDisposable {
@ -246,6 +251,7 @@ export type IUpdatableHoverContentOrFactory = IUpdatableHoverContent | (() => IU
export interface IUpdatableHoverOptions {
actions?: IHoverAction[];
linkHandler?(url: string): void;
trapFocus?: boolean;
}
export interface IUpdatableHover extends IDisposable {

View file

@ -10,6 +10,7 @@ let baseHoverDelegate: IHoverDelegate2 = {
hideHover: () => undefined,
showAndFocusLastHover: () => undefined,
setupUpdatableHover: () => null!,
triggerUpdatableHover: () => undefined
};
/**

View file

@ -62,7 +62,9 @@ export class HoverService extends Disposable implements IHoverService {
// HACK, remove this check when #189076 is fixed
if (!skipLastFocusedUpdate) {
if (trapFocus && activeElement) {
this._lastFocusedElementBeforeOpen = activeElement as HTMLElement;
if (!activeElement.classList.contains('monaco-hover')) {
this._lastFocusedElementBeforeOpen = activeElement as HTMLElement;
}
} else {
this._lastFocusedElementBeforeOpen = undefined;
}
@ -187,6 +189,8 @@ export class HoverService extends Disposable implements IHoverService {
}
}
private readonly _existingHovers = new Map<HTMLElement, IUpdatableHover>();
// TODO: Investigate performance of this function. There seems to be a lot of content created
// and thrown away on start up
setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions | undefined): IUpdatableHover {
@ -216,15 +220,13 @@ export class HoverService extends Disposable implements IHoverService {
hoverDelegate.onDidHideHover?.();
hoverWidget = undefined;
}
htmlElement.removeAttribute('custom-hover-active');
};
const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget) => {
const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget, trapFocus?: boolean) => {
return new TimeoutTimer(async () => {
if (!hoverWidget || hoverWidget.isDisposed) {
hoverWidget = new UpdatableHoverWidget(hoverDelegate, target || htmlElement, delay > 0);
await hoverWidget.update(typeof content === 'function' ? content() : content, focus, options);
htmlElement.setAttribute('custom-hover-active', 'true');
await hoverWidget.update(typeof content === 'function' ? content() : content, focus, { ...options, trapFocus });
}
}, delay);
};
@ -299,7 +301,7 @@ export class HoverService extends Disposable implements IHoverService {
const hover: IUpdatableHover = {
show: focus => {
hideHover(false, true); // terminate a ongoing mouse over preparation
triggerShowHover(0, focus); // show hover immediately
triggerShowHover(0, focus, undefined, focus); // show hover immediately
},
hide: () => {
hideHover(true, true);
@ -309,6 +311,7 @@ export class HoverService extends Disposable implements IHoverService {
await hoverWidget?.update(content, undefined, hoverOptions);
},
dispose: () => {
this._existingHovers.delete(htmlElement);
mouseOverDomEmitter.dispose();
mouseLeaveEmitter.dispose();
mouseDownEmitter.dispose();
@ -317,8 +320,21 @@ export class HoverService extends Disposable implements IHoverService {
hideHover(true, true);
}
};
this._existingHovers.set(htmlElement, hover);
return hover;
}
triggerUpdatableHover(target: HTMLElement): void {
const hover = this._existingHovers.get(target);
if (hover) {
hover.show(true);
}
}
public override dispose(): void {
this._existingHovers.forEach(hover => hover.dispose());
super.dispose();
}
}
function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOptions | number | string | undefined {

View file

@ -42,7 +42,7 @@ export class UpdatableHoverWidget implements IDisposable {
// show 'Loading' if no hover is up yet
if (!this._hoverWidget) {
this.show(localize('iconLabel.loading', "Loading..."), focus);
this.show(localize('iconLabel.loading', "Loading..."), focus, options);
}
// compute the content

View file

@ -12,4 +12,5 @@ export const NullHoverService: IHoverService = {
showHover: () => undefined,
setupUpdatableHover: () => Disposable.None as any,
showAndFocusLastHover: () => undefined,
triggerUpdatableHover: () => undefined
};

View file

@ -18,11 +18,11 @@ import { ITreeNode } from 'vs/base/browser/ui/tree/tree';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { Table } from 'vs/base/browser/ui/table/tableWidget';
import { AbstractTree, TreeFindMatchType, TreeFindMode } from 'vs/base/browser/ui/tree/abstractTree';
import { EventType, getActiveWindow, isActiveElement } from 'vs/base/browser/dom';
import { isActiveElement } from 'vs/base/browser/dom';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { localize, localize2 } from 'vs/nls';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IHoverService } from 'vs/platform/hover/browser/hover';
function ensureDOMFocus(widget: ListWidget | undefined): void {
// it can happen that one of the commands is executed while
@ -60,10 +60,6 @@ async function navigate(widget: WorkbenchListWidget | undefined, updateFocusFn:
return;
}
if (activeHover) {
toggleCustomHover(activeHover, widget);
}
await updateFocus(widget, updateFocusFn);
const listFocus = widget.getFocus();
@ -733,33 +729,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
return;
}
toggleCustomHover(elementWithHover as HTMLElement, lastFocusedList);
accessor.get(IHoverService).triggerUpdatableHover(elementWithHover as HTMLElement);
},
});
let activeHover: undefined | HTMLElement;
let disposable: IDisposable | undefined;
function toggleCustomHover(element: HTMLElement, list: WorkbenchListWidget) {
const show = !element.getAttribute('custom-hover-active');
const mouseEvent = new MouseEvent(show ? EventType.MOUSE_OVER : EventType.MOUSE_LEAVE, {
view: getActiveWindow(),
bubbles: true,
cancelable: true,
});
element.dispatchEvent(mouseEvent);
if (activeHover === element && !show) {
activeHover = undefined;
disposable?.dispose();
disposable = undefined;
} else {
activeHover = element;
disposable = list.onDidBlur(() => {
toggleCustomHover(element, list);
});
}
}
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'list.toggleExpand',
weight: KeybindingWeight.WorkbenchContrib,

View file

@ -553,7 +553,7 @@ export class ExtensionHoverWidget extends ExtensionWidget {
if (this.extension) {
this.hover.value = this.hoverService.setupUpdatableHover({
delay: this.configurationService.getValue<number>('workbench.hover.delay'),
showHover: (options) => {
showHover: (options, focus) => {
return this.hoverService.showHover({
...options,
additionalClasses: ['extension-hover'],
@ -561,7 +561,10 @@ export class ExtensionHoverWidget extends ExtensionWidget {
hoverPosition: this.options.position(),
forcePosition: true,
},
});
persistence: {
hideOnKeyDown: true,
}
}, focus);
},
placement: 'element'
}, this.options.target, { markdown: () => Promise.resolve(this.getHoverMarkdown()), markdownNotSupportedFallback: undefined });