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

Make untitled editor hint accessible #190214

Merged
merged 1 commit into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const enum AccessibilityVerbositySettingId {
Notebook = 'accessibility.verbosity.notebook',
Editor = 'accessibility.verbosity.editor',
Hover = 'accessibility.verbosity.hover',
Notification = 'accessibility.verbosity.notification'
Notification = 'accessibility.verbosity.notification',
EditorUntitledHint = 'accessibility.verbosity.editor.untitledHint'
}

const baseProperty: object = {
Expand Down Expand Up @@ -70,6 +71,10 @@ const configuration: IConfigurationNode = {
description: localize('verbosity.notification', 'Provide information about how to open the notification in an accessible view.'),
...baseProperty
},
[AccessibilityVerbositySettingId.EditorUntitledHint]: {
description: localize('verbosity.editor.untitledhint', 'Provide information about relevant actions in an untitled text editor.'),
...baseProperty
},
[AccessibilitySettingId.UnfocusedViewOpacity]: {
description: localize('unfocusedViewOpacity', 'The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will dim inactive views to make the focused views more obvious.'),
type: 'number',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { Schemas } from 'vs/base/common/network';
import { Event } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions';
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
Expand All @@ -25,6 +26,10 @@ import { IInlineChatService, IInlineChatSessionProvider } from 'vs/workbench/con
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions';
import { IProductService } from 'vs/platform/product/common/productService';
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
import { OS } from 'vs/base/common/platform';
import { status } from 'vs/base/browser/ui/aria/aria';
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';

const $ = dom.$;

Expand Down Expand Up @@ -98,6 +103,8 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {

private domNode: HTMLElement | undefined;
private toDispose: DisposableStore;
private isVisible = false;
private ariaLabel: string = '';

constructor(
private readonly editor: ICodeEditor,
Expand All @@ -111,20 +118,28 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
) {
this.toDispose = new DisposableStore();
this.toDispose.add(this.inlineChatService.onDidChangeProviders(() => this.onDidChangeModelContent()));
this.toDispose.add(editor.onDidChangeModelContent(() => this.onDidChangeModelContent()));
this.toDispose.add(this.editor.onDidChangeModelContent(() => this.onDidChangeModelContent()));
this.toDispose.add(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
if (this.domNode && e.hasChanged(EditorOption.fontInfo)) {
this.editor.applyFontInfo(this.domNode);
}
}));
const onDidFocusEditorText = Event.debounce(this.editor.onDidFocusEditorText, () => undefined, 500);
this.toDispose.add(onDidFocusEditorText(() => {
if (this.editor.hasTextFocus() && this.isVisible && this.ariaLabel && this.configurationService.getValue(AccessibilityVerbositySettingId.EditorUntitledHint)) {
status(this.ariaLabel);
}
}));
this.onDidChangeModelContent();
}

private onDidChangeModelContent(): void {
if (this.editor.getValue() === '') {
this.editor.addContentWidget(this);
this.isVisible = true;
} else {
this.editor.removeContentWidget(this);
this.isVisible = false;
}
}

Expand All @@ -133,42 +148,79 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
}

private _getHintInlineChat(providers: IInlineChatSessionProvider[]) {
const providerName = providers.length === 1 ? providers[0].label : undefined;
const providerName = (providers.length === 1 ? providers[0].label : undefined) ?? this.productService.nameShort;

const hintMsg = localize({
key: 'inlineChatHint',
comment: [
'Preserve double-square brackets and their order',
]
}, '[[Ask {0} to do something]] or start typing to dismiss.', providerName ?? this.productService.nameShort);
const inlineChatId = 'inlineChat.start';
let ariaLabel = `Ask ${providerName} to do something or start typing to dismiss.`;

const handleClick = () => {
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: 'inlineChat.hintAction',
from: 'hint'
});
void this.commandService.executeCommand(inlineChatId, { from: 'hint' });
};

const hintHandler: IContentActionHandler = {
disposables: this.toDispose,
callback: (index, _event) => {
switch (index) {
case '0':
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: 'inlineChat.hintAction',
from: 'hint'
});
void this.commandService.executeCommand('inlineChat.start', { from: 'hint' });
handleClick();
break;
}
}
};

return { hintMsg, hintHandler, keybindingsLookup: ['inlineChat.start'] };
const hintElement = $('untitled-hint-text');
hintElement.style.display = 'block';

const keybindingHint = this.keybindingService.lookupKeybinding(inlineChatId);
const keybindingHintLabel = keybindingHint?.getLabel();

if (keybindingHint && keybindingHintLabel) {
const actionPart = localize('untitledText', 'Press {0} to ask {1} to do something. ', keybindingHintLabel, providerName);

const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => {
const hintPart = $('a', undefined, fragment);
hintPart.style.fontStyle = 'italic';
hintPart.style.cursor = 'pointer';
hintPart.onclick = handleClick;
return hintPart;
});

hintElement.appendChild(before);

const label = new KeybindingLabel(hintElement, OS);
label.set(keybindingHint);
label.element.style.width = 'min-content';
label.element.style.display = 'inline';
label.element.style.cursor = 'pointer';
label.element.onclick = handleClick;

hintElement.appendChild(after);

const typeToDismiss = localize('untitledText2', 'Start typing to dismiss.');
const textHint2 = $('span', undefined, typeToDismiss);
textHint2.style.fontStyle = 'italic';
hintElement.appendChild(textHint2);

ariaLabel = actionPart.concat(typeToDismiss);
} else {
const hintMsg = localize({
key: 'inlineChatHint',
comment: [
'Preserve double-square brackets and their order',
]
}, '[[Ask {0} to do something]] or start typing to dismiss.', providerName);
const rendered = renderFormattedText(hintMsg, { actionHandler: hintHandler });
hintElement.appendChild(rendered);
}

return { ariaLabel, hintHandler, hintElement };
}

private _getHintDefault() {
const hintMsg = localize({
key: 'message',
comment: [
'Preserve double-square brackets and their order',
'language refers to a programming language'
]
}, '[[Select a language]], or [[fill with template]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don\'t show]] this again.');

const hintHandler: IContentActionHandler = {
disposables: this.toDispose,
callback: (index, event) => {
Expand Down Expand Up @@ -234,37 +286,49 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
this.editor.focus();
};

return { hintMsg, hintHandler, keybindingsLookup: [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries'] };
const hintMsg = localize({
key: 'message',
comment: [
'Preserve double-square brackets and their order',
'language refers to a programming language'
]
}, '[[Select a language]], or [[fill with template]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don\'t show]] this again.');
const hintElement = renderFormattedText(hintMsg, {
actionHandler: hintHandler,
renderCodeSegments: false,
});
hintElement.style.fontStyle = 'italic';

// ugly way to associate keybindings...
const keybindingsLookup = [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries'];
const keybindingLabels = keybindingsLookup.map((id) => this.keybindingService.lookupKeybinding(id)?.getLabel() ?? id);
const ariaLabel = localize('defaultHintAriaLabel', 'Execute {0} to select a language, execute {1} to fill with template, or execute {2} to open a different editor and get started. Start typing to dismiss.', ...keybindingLabels);
for (const anchor of hintElement.querySelectorAll('a')) {
anchor.style.cursor = 'pointer';
const id = keybindingsLookup.shift();
const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel();
anchor.title = title ?? '';
}

return { hintElement, ariaLabel };
}

// Select a language to get started. Start typing to dismiss, or don't show this again.
getDomNode(): HTMLElement {
if (!this.domNode) {
this.domNode = $('.untitled-hint');
this.domNode.style.width = 'max-content';
this.domNode.style.paddingLeft = '4px';

const inlineChatProviders = [...this.inlineChatService.getAllProvider()];
const { hintMsg, hintHandler, keybindingsLookup } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders);
const hintElement = renderFormattedText(hintMsg, {
actionHandler: hintHandler,
renderCodeSegments: false,
});
const { hintElement, ariaLabel } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders);
this.domNode.append(hintElement);

// ugly way to associate keybindings...
for (const anchor of hintElement.querySelectorAll('a')) {
anchor.style.cursor = 'pointer';
const id = keybindingsLookup.shift();
const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel();
anchor.title = title ?? '';
}
this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.EditorUntitledHint));

this.toDispose.add(dom.addDisposableListener(this.domNode, 'click', () => {
this.editor.focus();
}));

this.domNode.style.fontStyle = 'italic';
this.domNode.style.paddingLeft = '4px';
this.editor.applyFontInfo(this.domNode);
}

Expand Down