Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

accessible view adoption/ improvements #185614

Merged
merged 18 commits into from Jun 20, 2023
Merged
3 changes: 1 addition & 2 deletions src/vs/editor/common/standaloneStrings.ts
Expand Up @@ -7,7 +7,7 @@ import * as nls from 'vs/nls';

export namespace AccessibilityHelpNLS {
export const accessibilityHelpTitle = nls.localize('accessibilityHelpTitle', "Accessibility Help");
export const openingDocs = nls.localize("openingDocs", "Now opening the Editor Accessibility documentation page.");
export const openingDocs = nls.localize("openingDocs", "Now opening the Accessibility documentation page.");
export const readonlyDiffEditor = nls.localize("readonlyDiffEditor", "You are in a read-only pane of a diff editor.");
export const editableDiffEditor = nls.localize("editableDiffEditor", "You are in a pane of a diff editor.");
export const readonlyEditor = nls.localize("readonlyEditor", "You are in a read-only code editor");
Expand All @@ -20,7 +20,6 @@ export namespace AccessibilityHelpNLS {
export const tabFocusModeOnMsgNoKb = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding.");
export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior by pressing {0}.");
export const tabFocusModeOffMsgNoKb = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding.");
export const openDoc = nls.localize("openDoc", "Press H now to open a browser window with more information related to editor accessibility.");
export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help");
}

Expand Down
14 changes: 14 additions & 0 deletions src/vs/editor/contrib/hover/browser/contentHover.ts
Expand Up @@ -27,10 +27,24 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';

const $ = dom.$;

export interface IHoverElementInfo {
dimensions?: { width: number; height: number }; content?: string;
}

export class ContentHoverController extends Disposable {

private readonly _participants: IEditorHoverParticipant[];

private readonly _widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor));

getWidgetInfo(): IHoverElementInfo | undefined {
const node = this._widget.getDomNode();
if (!node.textContent) {
return undefined;
}
return { content: node.textContent, dimensions: { width: node.clientWidth, height: node.clientHeight } };
}

private readonly _computer: ContentHoverComputer;
private readonly _hoverOperation: HoverOperation<IHoverPart>;

Expand Down
5 changes: 4 additions & 1 deletion src/vs/editor/contrib/hover/browser/hover.ts
Expand Up @@ -15,7 +15,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition';
import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation';
import { ContentHoverWidget, ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHover';
import { ContentHoverWidget, ContentHoverController, IHoverElementInfo } from 'vs/editor/contrib/hover/browser/contentHover';
import { MarginHoverWidget } from 'vs/editor/contrib/hover/browser/marginHover';
import * as nls from 'vs/nls';
import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
Expand All @@ -40,6 +40,9 @@ export class ModesHoverController implements IEditorContribution {
private readonly _didChangeConfigurationHandler: IDisposable;

private _contentWidget: ContentHoverController | null;

getWidgetInfo(): IHoverElementInfo | undefined { return this._contentWidget?.getWidgetInfo(); }

private _glyphWidget: MarginHoverWidget | null;

private _isMouseDown: boolean;
Expand Down
Expand Up @@ -12,9 +12,9 @@ import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/b
import { localize } from 'vs/nls';
import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IKeybindingService, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { AccessibilityHelpAction, registerAccessibilityConfiguration } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution';
import { AccessibleViewService, IAccessibleContentProvider, IAccessibleViewOptions, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { AccessibilityHelpAction, AccessibilityViewAction, registerAccessibilityConfiguration } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution';
import { AccessibleViewService, AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import * as strings from 'vs/base/common/strings';
import * as platform from 'vs/base/common/platform';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
Expand All @@ -23,9 +23,8 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import { Registry } from 'vs/platform/registry/common/platform';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants';
import { KeyCode } from 'vs/base/common/keyCodes';
import { URI } from 'vs/base/common/uri';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ModesHoverController } from 'vs/editor/contrib/hover/browser/hover';
import { withNullAsUndefined } from 'vs/base/common/types';

registerAccessibilityConfiguration();
registerSingleton(IAccessibleViewService, AccessibleViewService, InstantiationType.Delayed);
Expand All @@ -35,25 +34,18 @@ class AccessibilityHelpProvider extends Disposable implements IAccessibleContent
this._editor.focus();
this.dispose();
}
options: IAccessibleViewOptions = { ariaLabel: localize('terminal-help-label', "terminal accessibility help") };
options: IAccessibleViewOptions = { type: AccessibleViewType.HelpMenu, ariaLabel: localize('terminal-help-label', "terminal accessibility help") };
id: string = 'editor';
onKeyDown(e: IKeyboardEvent): void {
if (e.keyCode === KeyCode.KeyH) {
alert(AccessibilityHelpNLS.openingDocs);

let url = (this._editor.getRawOptions() as any).accessibilityHelpUrl;
if (typeof url === 'undefined') {
url = 'https://go.microsoft.com/fwlink/?linkid=852450';
}
this._openerService.open(URI.parse(url));
}
}
constructor(
private readonly _editor: ICodeEditor,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IOpenerService private readonly _openerService: IOpenerService
@IKeybindingService private readonly _keybindingService: IKeybindingService
) {
super();
let url = (this._editor.getRawOptions() as any).accessibilityHelpUrl;
if (typeof url === 'undefined') {
url = 'https://go.microsoft.com/fwlink/?linkid=852450';
}
this.options.readMoreUrl = url;
}

private _descriptionForCommand(commandId: string, msg: string, noKbMsg: string): string {
Expand Down Expand Up @@ -105,7 +97,6 @@ class AccessibilityHelpProvider extends Disposable implements IAccessibleContent
} else {
content.push(this._descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOffMsg, AccessibilityHelpNLS.tabFocusModeOffMsgNoKb));
}
content.push(AccessibilityHelpNLS.openDoc);
return content.join('\n');
}
}
Expand All @@ -132,3 +123,45 @@ class EditorAccessibilityHelpContribution extends Disposable {

const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(EditorAccessibilityHelpContribution, LifecyclePhase.Eventually);



class HoverAccessibleViewContribution extends Disposable {
static ID: 'hoverAccessibleViewContribution';
constructor() {
super();
this._register(AccessibilityViewAction.addImplementation(90, 'hover', accessor => {
const accessibleViewService = accessor.get(IAccessibleViewService);
const codeEditorService = accessor.get(ICodeEditorService);
const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
if (!editor) {
return false;
}
const controller = ModesHoverController.get(editor);
const hoverInfo = withNullAsUndefined(controller?.getWidgetInfo());
if (!controller || !hoverInfo?.content) {
return false;
}
function provideContent(): string {
return hoverInfo?.content!;
}
const provider = accessibleViewService.registerProvider({
id: 'hover',
provideContent,
onClose() {
provider.dispose();
controller.focus();
},
options: {
ariaLabel: localize('hoverAccessibleView', "Hover Accessible View"), language: 'typescript', type: AccessibleViewType.View, dimensions: controller.getWidgetInfo()!.dimensions
}
});
accessibleViewService.show('hover');
return true;
}));
}
}

const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(HoverAccessibleViewContribution, LifecyclePhase.Eventually);

Expand Up @@ -13,7 +13,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis
export const enum AccessibilityVerbositySettingId {
Terminal = 'accessibility.verbosity.terminal',
DiffEditor = 'accessibility.verbosity.diffEditor',
Chat = 'accessibility.verbosity.chat',
Chat = 'accessibility.verbosity.panelChat',
InlineChat = 'accessibility.verbosity.inlineChat',
KeybindingsEditor = 'accessibility.verbosity.keybindingsEditor',
Notebook = 'accessibility.verbosity.notebook'
Expand Down Expand Up @@ -79,3 +79,17 @@ export const AccessibilityHelpAction = registerCommand(new MultiCommand({
}
}
}));


export const AccessibilityViewAction = registerCommand(new MultiCommand({
id: 'editor.action.accessibilityView',
precondition: undefined,
kbOpts: {
primary: KeyMod.Alt | KeyCode.F2,
weight: KeybindingWeight.WorkbenchContrib,
linux: {
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F1,
secondary: [KeyMod.Alt | KeyCode.F1]
}
}
}));
46 changes: 34 additions & 12 deletions src/vs/workbench/contrib/accessibility/browser/accessibleView.ts
Expand Up @@ -12,15 +12,21 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/model';
import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings';
import { LinkDetector } from 'vs/editor/contrib/links/browser/links';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextViewDelegate, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { IDisposable } from 'xterm';

const enum DIMENSION_DEFAULT {
WIDTH = .6,
HEIGHT = 200
}

export interface IAccessibleContentProvider {
id: string;
Expand All @@ -38,8 +44,17 @@ export interface IAccessibleViewService {
registerProvider(provider: IAccessibleContentProvider): IDisposable;
}

export const enum AccessibleViewType {
HelpMenu = 'helpMenu',
View = 'view'
}

export interface IAccessibleViewOptions {
ariaLabel: string;
readMoreUrl?: string;
language?: string;
type: AccessibleViewType;
dimensions?: { width: number; height: number };
}

class AccessibleView extends Disposable {
Expand All @@ -48,6 +63,7 @@ class AccessibleView extends Disposable {
private _editorContainer: HTMLElement;

constructor(
@IOpenerService private readonly _openerService: IOpenerService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IModelService private readonly _modelService: IModelService,
Expand Down Expand Up @@ -89,8 +105,11 @@ class AccessibleView extends Disposable {
}

private _render(provider: IAccessibleContentProvider, container: HTMLElement): IDisposable {

const fragment = localize('exit-tip', 'Exit this menu via the Escape key.\n') + provider.provideContent();
const settingKey = `accessibility.verbosity.${provider.id}`;
const value = this._configurationService.getValue(settingKey);
const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\nPress H now to open a browser window with more information related to accessibility.\n") : '';
const disableHelpHint = provider.options.type && value ? localize('disable-help-hint', '\nTo disable the `accessibility.verbosity` hint for this feature, press D now.\n') : '\n';
const fragment = provider.provideContent() + readMoreLink + disableHelpHint + localize('exit-tip', 'Exit this menu via the Escape key.');

this._getTextModel(URI.from({ path: `accessible-view-${provider.id}`, scheme: 'accessible-view', fragment })).then((model) => {
if (!model) {
Expand All @@ -101,32 +120,35 @@ class AccessibleView extends Disposable {
if (!domNode) {
return;
}
if (provider.options.language) {
model.setLanguage(provider.options.language);
}
container.appendChild(this._editorContainer);
this._layout();
this._register(this._editorWidget.onKeyUp((e) => {
if (e.keyCode === KeyCode.Escape) {
this._contextViewService.hideContextView();
} else if (e.keyCode === KeyCode.KeyD) {
this._configurationService.updateValue(settingKey, false);
} else if (e.keyCode === KeyCode.KeyH && provider.options.readMoreUrl) {
const url: string = provider.options.readMoreUrl!;
alert(AccessibilityHelpNLS.openingDocs);
this._openerService.open(URI.parse(url));
}
e.stopPropagation();
provider.onKeyDown?.(e);
}));
this._register(this._editorWidget.onDidBlurEditorText(() => this._contextViewService.hideContextView()));
this._register(this._editorWidget.onDidContentSizeChange(() => this._layout()));
this._register(this._editorWidget.onDidContentSizeChange(() => this._layout(provider)));
this._editorWidget.updateOptions({ ariaLabel: provider.options.ariaLabel });
this._editorWidget.focus();
});
return toDisposable(() => provider.onClose());
}

private _layout(): void {
private _layout(provider: IAccessibleContentProvider): void {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;

const width = windowWidth * .4;
const height = Math.min(.4 * windowHeight, this._editorWidget.getContentHeight());
this._editorWidget.layout({ width, height });
const top = Math.round((windowHeight - height) / 2);
this._editorContainer.style.top = `${top}px`;
const width = provider.options.dimensions?.width || windowWidth * DIMENSION_DEFAULT.WIDTH;
this._editorWidget.layout({ width, height: provider.options.dimensions?.height || this._editorWidget.getContentHeight() });
const left = Math.round((windowWidth - width) / 2);
this._editorContainer.style.left = `${left}px`;
}
Expand Down
Expand Up @@ -10,16 +10,16 @@ import { withNullAsUndefined } from 'vs/base/common/types';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';


const inputBox = localize('chat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.');

export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'chat' | 'inline'): string {
export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'panelChat' | 'inlineChat'): string {
const keybindingService = accessor.get(IKeybindingService);
const content = [];
if (type === 'chat') {
if (type === 'panelChat') {
content.push(localize('chat.overview', 'The chat view is comprised of an input box and a request/response list. The input box is used to make requests and the list is used to display responses.'));
content.push(inputBox);
content.push(localize('chat.announcement', 'Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.'));
Expand Down Expand Up @@ -50,10 +50,10 @@ function descriptionForCommand(commandId: string, msg: string, noKbMsg: string,
return format(noKbMsg, commandId);
}

export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor, type: 'chat' | 'inline'): Promise<void> {
export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor, type: 'panelChat' | 'inlineChat'): Promise<void> {
const widgetService = accessor.get(IChatWidgetService);
const accessibleViewService = accessor.get(IAccessibleViewService);
const inputEditor: ICodeEditor | undefined = type === 'chat' ? widgetService.lastFocusedWidget?.inputEditor : editor;
const inputEditor: ICodeEditor | undefined = type === 'panelChat' ? widgetService.lastFocusedWidget?.inputEditor : editor;
const editorUri = editor.getModel()?.uri;

if (!inputEditor || !editorUri) {
Expand All @@ -71,15 +71,15 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi
id: type,
provideContent: () => helpText,
onClose: () => {
if (type === 'chat' && cachedPosition) {
if (type === 'panelChat' && cachedPosition) {
inputEditor.setPosition(cachedPosition);
inputEditor.focus();
} else if (type === 'inline') {
} else if (type === 'inlineChat') {
InlineChatController.get(editor)?.focus();
}
provider.dispose();
},
options: { ariaLabel: type === 'chat' ? localize('chat-help-label', "Chat accessibility help") : localize('inline-chat-label', "Inline chat accessibility help") }
options: { type: AccessibleViewType.HelpMenu, ariaLabel: type === 'panelChat' ? localize('chat-help-label', "Chat accessibility help") : localize('inline-chat-label', "Inline chat accessibility help") }
});
accessibleViewService.show(type);
}
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/chat/browser/actions/chatActions.ts
Expand Up @@ -102,12 +102,12 @@ export function registerChatActions() {
static ID: 'chatAccessibilityHelpContribution';
constructor() {
super();
this._register(AccessibilityHelpAction.addImplementation(105, 'chat', async accessor => {
this._register(AccessibilityHelpAction.addImplementation(105, 'panelChat', async accessor => {
const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();
if (!codeEditor) {
return;
}
runAccessibilityHelpAction(accessor, codeEditor, 'chat');
runAccessibilityHelpAction(accessor, codeEditor, 'panelChat');
}, CONTEXT_IN_CHAT_INPUT));
}
}
Expand Down