Skip to content

Commit

Permalink
Adding accessibility help for verbose hover (#212783)
Browse files Browse the repository at this point in the history
* adding code in order to provide accessibility help for the hover

* adding the description of the possible commands that can be used

* reusing the method

* joining the content and changing the text in the help view

* polishing the code

* removing the question mark

* changing the import

* removing the setting ID from imports

* adding code in order to update when the hover updates

* adding methods to service

* adding code in order to dispose the accessible hover view

* fixing bug

* polishing the code

* checking that action not supported for the early return

* using disposable store instead

* using the appropriate string

* polishing the code

* using instead the type help and the resolved keybindings

* hiding also on the `onDidBlurEditorWidget` firing

* Revert "using instead the type help and the resolved keybindings"

This reverts commit 1f450dd.

* use hover accessible view, provide custom help

* Revert "Revert "using instead the type help and the resolved keybindings""

This reverts commit 12f0cf6.

* add HoverAccessibilityHelp, BaseHoverAccessibleViewProvider

* polishing the code

* polishing the code

* provide content at a specific index from the hover accessibility help provider

* introducing method _initializeOptions

* using readonly where possible

* using public everywhere

* using a getter for the actions

---------

Co-authored-by: meganrogge <megan.rogge@microsoft.com>
  • Loading branch information
aiday-mar and meganrogge authored May 28, 2024
1 parent 49eedd7 commit 7f55a08
Show file tree
Hide file tree
Showing 22 changed files with 394 additions and 92 deletions.
27 changes: 24 additions & 3 deletions src/vs/editor/contrib/hover/browser/contentHoverController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHover
import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHoverComputer';
import { ContentHoverVisibleData, HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes';
import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar';
import { Emitter } from 'vs/base/common/event';

export class ContentHoverController extends Disposable implements IHoverWidget {

Expand All @@ -35,6 +36,9 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
private readonly _markdownHoverParticipant: MarkdownHoverParticipant | undefined;
private readonly _hoverOperation: HoverOperation<IHoverPart>;

private readonly _onContentsChanged = this._register(new Emitter<void>());
public readonly onContentsChanged = this._onContentsChanged.event;

constructor(
private readonly _editor: ICodeEditor,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
Expand Down Expand Up @@ -214,7 +218,7 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
fragment,
statusBar,
setColorPicker: (widget) => colorPicker = widget,
onContentsChanged: () => this._widget.onContentsChanged(),
onContentsChanged: () => this._doOnContentsChanged(),
setMinimumDimensions: (dimensions: dom.Dimension) => this._widget.setMinimumDimensions(dimensions),
hide: () => this.hide()
};
Expand Down Expand Up @@ -261,6 +265,11 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
}
}

private _doOnContentsChanged(): void {
this._onContentsChanged.fire();
this._widget.onContentsChanged();
}

private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({
description: 'content-hover-highlight',
className: 'hoverHighlight'
Expand Down Expand Up @@ -351,8 +360,20 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null);
}

public async updateFocusedMarkdownHoverVerbosityLevel(action: HoverVerbosityAction): Promise<void> {
this._markdownHoverParticipant?.updateFocusedMarkdownHoverPartVerbosityLevel(action);
public async updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index?: number, focus?: boolean): Promise<void> {
this._markdownHoverParticipant?.updateMarkdownHoverVerbosityLevel(action, index, focus);
}

public focusedMarkdownHoverIndex(): number {
return this._markdownHoverParticipant?.focusedMarkdownHoverIndex() ?? -1;
}

public markdownHoverContentAtIndex(index: number): string {
return this._markdownHoverParticipant?.markdownHoverContentAtIndex(index) ?? '';
}

public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
return this._markdownHoverParticipant?.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action) ?? false;
}

public getWidgetContent(): string | undefined {
Expand Down
262 changes: 234 additions & 28 deletions src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,252 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { localize } from 'vs/nls';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController';
import { AccessibleViewType, AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView';
import { AccessibleViewType, AccessibleViewProviderId, AdvancedContentProvider, IAccessibleViewContentProvider, IAccessibleViewOptions } from 'vs/platform/accessibility/browser/accessibleView';
import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IHoverService } from 'vs/platform/hover/browser/hover';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { HoverVerbosityAction } from 'vs/editor/common/languages';
import { DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID, DECREASE_HOVER_VERBOSITY_ACTION_ID, DECREASE_HOVER_VERBOSITY_ACTION_LABEL, INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_LABEL } from 'vs/editor/contrib/hover/browser/hoverActionIds';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { Action, IAction } from 'vs/base/common/actions';
import { ThemeIcon } from 'vs/base/common/themables';
import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';

namespace HoverAccessibilityHelpNLS {
export const intro = localize('intro', "The hover widget is focused. Press the Tab key to cycle through the hover parts.");
export const increaseVerbosity = localize('increaseVerbosity', "- The focused hover part verbosity level can be increased with the Increase Hover Verbosity command<keybinding:{0}>.", INCREASE_HOVER_VERBOSITY_ACTION_ID);
export const decreaseVerbosity = localize('decreaseVerbosity', "- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command<keybinding:{0}>.", DECREASE_HOVER_VERBOSITY_ACTION_ID);
export const hoverContent = localize('contentHover', "The last focused hover content is the following.");
}

export class HoverAccessibleView implements IAccessibleViewImplentation {
readonly type = AccessibleViewType.View;
readonly priority = 95;
readonly name = 'hover';
readonly when = EditorContextKeys.hoverFocused;
getProvider(accessor: ServicesAccessor) {

public readonly type = AccessibleViewType.View;
public readonly priority = 95;
public readonly name = 'hover';
public readonly when = EditorContextKeys.hoverFocused;

private _provider: HoverAccessibleViewProvider | undefined;

getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined {
const codeEditorService = accessor.get(ICodeEditorService);
const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
const editorHoverContent = editor ? HoverController.get(editor)?.getWidgetContent() ?? undefined : undefined;
if (!editor || !editorHoverContent) {
const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
if (!codeEditor) {
throw new Error('No active or focused code editor');
}
const hoverController = HoverController.get(codeEditor);
if (!hoverController) {
return;
}
return {
id: AccessibleViewProviderId.Hover,
verbositySettingKey: 'accessibility.verbosity.hover',
provideContent() { return editorHoverContent; },
onClose() {
HoverController.get(editor)?.focus();
},
options: {
language: editor?.getModel()?.getLanguageId() ?? 'typescript',
type: AccessibleViewType.View
}
};
this._provider = accessor.get(IInstantiationService).createInstance(HoverAccessibleViewProvider, codeEditor, hoverController);
return this._provider;
}

dispose(): void {
this._provider?.dispose();
}
}

export class HoverAccessibilityHelp implements IAccessibleViewImplentation {

public readonly priority = 100;
public readonly name = 'hover';
public readonly type = AccessibleViewType.Help;
public readonly when = EditorContextKeys.hoverVisible;

private _provider: HoverAccessibleViewProvider | undefined;

getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined {
const codeEditorService = accessor.get(ICodeEditorService);
const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
if (!codeEditor) {
throw new Error('No active or focused code editor');
}
const hoverController = HoverController.get(codeEditor);
if (!hoverController) {
return;
}
return accessor.get(IInstantiationService).createInstance(HoverAccessibilityHelpProvider, hoverController);
}

dispose(): void {
this._provider?.dispose();
}
}


abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAccessibleViewContentProvider {

abstract provideContent(): string;
abstract options: IAccessibleViewOptions;

public readonly id = AccessibleViewProviderId.Hover;
public readonly verbositySettingKey = 'accessibility.verbosity.hover';

private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());
public readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;

protected _markdownHoverFocusedIndex: number = -1;
private _onHoverContentsChanged: IDisposable | undefined;

constructor(
protected readonly _hoverController: HoverController,
) {
super();
}

public onOpen(): void {
if (!this._hoverController) {
return;
}
this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = true;
this._markdownHoverFocusedIndex = this._hoverController.focusedMarkdownHoverIndex();
this._onHoverContentsChanged = this._register(this._hoverController.onHoverContentsChanged(() => {
this._onDidChangeContent.fire();
}));
}

public onClose(): void {
if (!this._hoverController) {
return;
}
this._markdownHoverFocusedIndex = -1;
this._hoverController.focus();
this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = false;
this._onHoverContentsChanged?.dispose();
}
}


export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider {

public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help };

constructor(
hoverController: HoverController,
) {
super(hoverController);
}

provideContent(): string {
return this.provideContentAtIndex(this._markdownHoverFocusedIndex);
}

provideContentAtIndex(index: number): string {
const content: string[] = [];
content.push(HoverAccessibilityHelpNLS.intro);
content.push(...this._descriptionsOfVerbosityActionsForIndex(index));
content.push(...this._descriptionOfFocusedMarkdownHoverAtIndex(index));
return content.join('\n');
}

private _descriptionsOfVerbosityActionsForIndex(index: number): string[] {
const content: string[] = [];
const descriptionForIncreaseAction = this._descriptionOfVerbosityActionForIndex(HoverVerbosityAction.Increase, index);
if (descriptionForIncreaseAction !== undefined) {
content.push(descriptionForIncreaseAction);
}
const descriptionForDecreaseAction = this._descriptionOfVerbosityActionForIndex(HoverVerbosityAction.Decrease, index);
if (descriptionForDecreaseAction !== undefined) {
content.push(descriptionForDecreaseAction);
}
return content;
}

private _descriptionOfVerbosityActionForIndex(action: HoverVerbosityAction, index: number): string | undefined {
const isActionSupported = this._hoverController.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action);
if (!isActionSupported) {
return;
}
switch (action) {
case HoverVerbosityAction.Increase:
return HoverAccessibilityHelpNLS.increaseVerbosity;
case HoverVerbosityAction.Decrease:
return HoverAccessibilityHelpNLS.decreaseVerbosity;
}
}

protected _descriptionOfFocusedMarkdownHoverAtIndex(index: number): string[] {
const content: string[] = [];
const hoverContent = this._hoverController.markdownHoverContentAtIndex(index);
if (hoverContent) {
content.push('\n' + HoverAccessibilityHelpNLS.hoverContent);
content.push('\n' + hoverContent);
}
return content;
}
}

export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider {

public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.View };

constructor(
private readonly _editor: ICodeEditor,
hoverController: HoverController,
) {
super(hoverController);
this._initializeOptions(this._editor, hoverController);
}

public provideContent(): string {
const hoverContent = this._hoverController.markdownHoverContentAtIndex(this._markdownHoverFocusedIndex);
return hoverContent.length > 0 ? hoverContent : HoverAccessibilityHelpNLS.intro;
}

public get actions(): IAction[] {
const actions: IAction[] = [];
actions.push(this._getActionFor(this._editor, HoverVerbosityAction.Increase));
actions.push(this._getActionFor(this._editor, HoverVerbosityAction.Decrease));
return actions;
}

private _getActionFor(editor: ICodeEditor, action: HoverVerbosityAction): IAction {
let actionId: string;
let accessibleActionId: string;
let actionLabel: string;
let actionCodicon: ThemeIcon;
switch (action) {
case HoverVerbosityAction.Increase:
actionId = INCREASE_HOVER_VERBOSITY_ACTION_ID;
accessibleActionId = INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID;
actionLabel = INCREASE_HOVER_VERBOSITY_ACTION_LABEL;
actionCodicon = Codicon.add;
break;
case HoverVerbosityAction.Decrease:
actionId = DECREASE_HOVER_VERBOSITY_ACTION_ID;
accessibleActionId = DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID;
actionLabel = DECREASE_HOVER_VERBOSITY_ACTION_LABEL;
actionCodicon = Codicon.remove;
break;
}
const actionEnabled = this._hoverController.doesMarkdownHoverAtIndexSupportVerbosityAction(this._markdownHoverFocusedIndex, action);
return new Action(accessibleActionId, actionLabel, ThemeIcon.asClassName(actionCodicon), actionEnabled, () => {
editor.getAction(actionId)?.run({ index: this._markdownHoverFocusedIndex, focus: false });
});
}

private _initializeOptions(editor: ICodeEditor, hoverController: HoverController): void {
const helpProvider = this._register(new HoverAccessibilityHelpProvider(hoverController));
this.options.language = editor.getModel()?.getLanguageId();
this.options.customHelp = () => { return helpProvider.provideContentAtIndex(this._markdownHoverFocusedIndex); };
}
}

export class ExtHoverAccessibleView implements IAccessibleViewImplentation {
readonly type = AccessibleViewType.View;
readonly priority = 90;
readonly name = 'extension-hover';
getProvider(accessor: ServicesAccessor) {

public readonly type = AccessibleViewType.View;
public readonly priority = 90;
public readonly name = 'extension-hover';

getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined {
const contextViewService = accessor.get(IContextViewService);
const contextViewElement = contextViewService.getContextViewElement();
const extensionHoverContent = contextViewElement?.textContent ?? undefined;
Expand All @@ -63,4 +267,6 @@ export class ExtHoverAccessibleView implements IAccessibleViewImplentation {
options: { language: 'typescript', type: AccessibleViewType.View }
};
}

dispose() { }
}
5 changes: 5 additions & 0 deletions src/vs/editor/contrib/hover/browser/hoverActionIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';

export const SHOW_OR_FOCUS_HOVER_ACTION_ID = 'editor.action.showHover';
export const SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID = 'editor.action.showDefinitionPreviewHover';
Expand All @@ -14,4 +15,8 @@ export const PAGE_DOWN_HOVER_ACTION_ID = 'editor.action.pageDownHover';
export const GO_TO_TOP_HOVER_ACTION_ID = 'editor.action.goToTopHover';
export const GO_TO_BOTTOM_HOVER_ACTION_ID = 'editor.action.goToBottomHover';
export const INCREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.increaseHoverVerbosityLevel';
export const INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID = 'editor.action.increaseHoverVerbosityLevelFromAccessibleView';
export const INCREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'increaseHoverVerbosityLevel', comment: ['Label for action that will increase the hover verbosity level.'] }, "Increase Hover Verbosity Level");
export const DECREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevel';
export const DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevelFromAccessibleView';
export const DECREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'decreaseHoverVerbosityLevel', comment: ['Label for action that will decrease the hover verbosity level.'] }, "Decrease Hover Verbosity Level");
Loading

0 comments on commit 7f55a08

Please sign in to comment.