Skip to content

Commit af930dc

Browse files
authored
add comments accessible view (microsoft#209977)
1 parent 258537d commit af930dc

File tree

5 files changed

+176
-23
lines changed

5 files changed

+176
-23
lines changed

src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
1010
import { Registry } from 'vs/platform/registry/common/platform';
1111
import { IAccessibleViewService, AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
1212
import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution';
13-
import { HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions';
13+
import { CommentAccessibleViewContribution, HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions';
1414
import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus';
1515
import { EditorAccessibilityHelpContribution } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp';
1616
import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/saveAccessibilitySignal';
@@ -30,6 +30,7 @@ workbenchRegistry.registerWorkbenchContribution(UnfocusedViewDimmingContribution
3030

3131
workbenchRegistry.registerWorkbenchContribution(HoverAccessibleViewContribution, LifecyclePhase.Eventually);
3232
workbenchRegistry.registerWorkbenchContribution(NotificationAccessibleViewContribution, LifecyclePhase.Eventually);
33+
workbenchRegistry.registerWorkbenchContribution(CommentAccessibleViewContribution, LifecyclePhase.Eventually);
3334
workbenchRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually);
3435

3536
registerWorkbenchContribution2(AccessibilityStatus.ID, AccessibilityStatus, WorkbenchPhase.BlockRestore);

src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@ import { ThemeIcon } from 'vs/base/common/themables';
3030
import { Codicon } from 'vs/base/common/codicons';
3131
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
3232
import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys';
33-
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
33+
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
3434
import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService';
35+
import { COMMENTS_VIEW_ID, CommentsMenus } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer';
36+
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
37+
import { CommentsPanel, CONTEXT_KEY_HAS_COMMENTS } from 'vs/workbench/contrib/comments/browser/commentsView';
38+
import { IMenuService } from 'vs/platform/actions/common/actions';
39+
import { MarshalledId } from 'vs/base/common/marshallingIds';
3540
import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController';
3641

3742
export function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string {
@@ -224,6 +229,77 @@ export function alertFocusChange(index: number | undefined, length: number | und
224229
return;
225230
}
226231

232+
233+
export class CommentAccessibleViewContribution extends Disposable {
234+
static ID: 'commentAccessibleViewContribution';
235+
constructor() {
236+
super();
237+
this._register(AccessibleViewAction.addImplementation(90, 'comment', accessor => {
238+
const accessibleViewService = accessor.get(IAccessibleViewService);
239+
const contextKeyService = accessor.get(IContextKeyService);
240+
const viewsService = accessor.get(IViewsService);
241+
const menuService = accessor.get(IMenuService);
242+
const commentsView = viewsService.getActiveViewWithId<CommentsPanel>(COMMENTS_VIEW_ID);
243+
if (!commentsView) {
244+
return false;
245+
}
246+
const menus = this._register(new CommentsMenus(menuService));
247+
menus.setContextKeyService(contextKeyService);
248+
249+
function renderAccessibleView() {
250+
if (!commentsView) {
251+
return false;
252+
}
253+
254+
const commentNode = commentsView?.focusedCommentNode;
255+
const content = commentsView.focusedCommentInfo?.toString();
256+
if (!commentNode || !content) {
257+
return false;
258+
}
259+
const menuActions = [...menus.getResourceContextActions(commentNode)].filter(i => i.enabled);
260+
const actions = menuActions.map(action => {
261+
return {
262+
...action,
263+
run: () => {
264+
commentsView.focus();
265+
action.run({
266+
thread: commentNode.thread,
267+
$mid: MarshalledId.CommentThread,
268+
commentControlHandle: commentNode.controllerHandle,
269+
commentThreadHandle: commentNode.threadHandle,
270+
});
271+
}
272+
};
273+
});
274+
accessibleViewService.show({
275+
id: AccessibleViewProviderId.Notification,
276+
provideContent: () => {
277+
return content;
278+
},
279+
onClose(): void {
280+
commentsView.focus();
281+
},
282+
next(): void {
283+
commentsView.focus();
284+
commentsView.focusNextNode();
285+
renderAccessibleView();
286+
},
287+
previous(): void {
288+
commentsView.focus();
289+
commentsView.focusPreviousNode();
290+
renderAccessibleView();
291+
},
292+
verbositySettingKey: AccessibilityVerbositySettingId.Comments,
293+
options: { type: AccessibleViewType.View },
294+
actions
295+
});
296+
return true;
297+
}
298+
return renderAccessibleView();
299+
}, CONTEXT_KEY_HAS_COMMENTS));
300+
}
301+
}
302+
227303
export class InlineCompletionsAccessibleViewContribution extends Disposable {
228304
static ID: 'inlineCompletionsAccessibleViewContribution';
229305
private _options: IAccessibleViewOptions = { type: AccessibleViewType.View };

src/vs/workbench/contrib/comments/browser/comments.contribution.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
2525
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
2626
import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController';
2727
import { MarshalledCommentThreadInternal } from 'vs/workbench/common/comments';
28+
import { accessibleViewCurrentProviderId, accessibleViewIsShown, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
2829

2930
registerAction2(class Collapse extends ViewAction<CommentsPanel> {
3031
constructor() {
@@ -74,11 +75,15 @@ registerAction2(class Reply extends Action2 {
7475
id: 'comments.reply',
7576
title: nls.localize('reply', "Reply"),
7677
icon: Codicon.reply,
77-
menu: {
78+
precondition: ContextKeyExpr.equals('canReply', true),
79+
menu: [{
7880
id: MenuId.CommentsViewThreadActions,
79-
order: 100,
80-
when: ContextKeyExpr.equals('canReply', true)
81+
order: 100
8182
},
83+
{
84+
id: MenuId.AccessibleView,
85+
when: ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Comments)),
86+
}]
8287
});
8388
}
8489

src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export class ResourceWithCommentsRenderer implements IListRenderer<ITreeNode<Res
135135
}
136136
}
137137

138-
class CommentsMenus implements IDisposable {
138+
export class CommentsMenus implements IDisposable {
139139
private contextKeyService: IContextKeyService | undefined;
140140

141141
constructor(

src/vs/workbench/contrib/comments/browser/commentsView.ts

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import * as dom from 'vs/base/browser/dom';
99
import { basename } from 'vs/base/common/resources';
1010
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1111
import { IThemeService } from 'vs/platform/theme/common/themeService';
12-
import { CommentNode, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
13-
import { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
12+
import { CommentNode, ICommentThreadChangedEvent, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel';
13+
import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/browser/commentService';
1414
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
1515
import { ResourceLabels } from 'vs/workbench/browser/labels';
1616
import { CommentsList, COMMENTS_VIEW_TITLE, Filter } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer';
@@ -35,6 +35,8 @@ import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/comme
3535
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands';
3636
import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
3737
import { IHoverService } from 'vs/platform/hover/browser/hover';
38+
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
39+
import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions';
3840

3941
export const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey<boolean>('commentsView.hasComments', false);
4042
export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey<boolean>('commentsView.someCommentsExpanded', false);
@@ -68,6 +70,58 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
6870

6971
readonly onDidChangeVisibility = this.onDidChangeBodyVisibility;
7072

73+
get focusedCommentNode(): CommentNode | undefined {
74+
const focused = this.tree?.getFocus();
75+
if (focused?.length === 1 && focused[0] instanceof CommentNode) {
76+
return focused[0];
77+
}
78+
return undefined;
79+
}
80+
81+
get focusedCommentInfo(): string | undefined {
82+
const focused = this.focusedCommentNode;
83+
if (!focused) {
84+
return;
85+
}
86+
return this.getScreenReaderInfoForNode(focused, 'accessibleViewContent');
87+
}
88+
89+
focusNextNode(): void {
90+
if (!this.tree) {
91+
return;
92+
}
93+
const focused = this.tree.getFocus()?.[0];
94+
if (!focused) {
95+
return;
96+
}
97+
let next = this.tree.navigate(focused).next();
98+
while (next && !(next instanceof CommentNode)) {
99+
next = this.tree.navigate(next).next();
100+
}
101+
if (!next) {
102+
return;
103+
}
104+
this.tree.setFocus([next]);
105+
}
106+
107+
focusPreviousNode(): void {
108+
if (!this.tree) {
109+
return;
110+
}
111+
const focused = this.tree.getFocus()?.[0];
112+
if (!focused) {
113+
return;
114+
}
115+
let previous = this.tree.navigate(focused).previous();
116+
while (previous && !(previous instanceof CommentNode)) {
117+
previous = this.tree.navigate(previous).previous();
118+
}
119+
if (!previous) {
120+
return;
121+
}
122+
this.tree.setFocus([previous]);
123+
}
124+
71125
constructor(
72126
options: IViewPaneOptions,
73127
@IInstantiationService instantiationService: IInstantiationService,
@@ -263,46 +317,63 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
263317
this.messageBoxContainer.classList.toggle('hidden', this.commentService.commentsModel.hasCommentThreads());
264318
}
265319

266-
private getAriaForNode(element: CommentNode) {
320+
private getScreenReaderInfoForNode(element: CommentNode, type: 'ariaLabel' | 'accessibleViewContent') {
321+
let accessibleViewHint = '';
322+
if (type === 'ariaLabel' && this.configurationService.getValue(AccessibilityVerbositySettingId.Comments)) {
323+
const kbLabel = this.keybindingService.lookupKeybinding(AccessibleViewAction.id)?.getAriaLabel();
324+
accessibleViewHint = kbLabel ? nls.localize('acessibleViewHint', "Inspect this in the accessible view ({0}).\n", kbLabel) : nls.localize('acessibleViewHintNoKbOpen', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.\n");
325+
}
326+
const replies = this.getRepliesAsString(element, type) || '';
267327
if (element.range) {
268328
if (element.threadRelevance === CommentThreadApplicability.Outdated) {
269-
return nls.localize('resourceWithCommentLabelOutdated',
270-
"Outdated from ${0} at line {1} column {2} in {3}, source: {4}",
329+
return accessibleViewHint + nls.localize('resourceWithCommentLabelOutdated',
330+
"Outdated from ${0} at line {1} column {2} in {3}, comment: {4}",
271331
element.comment.userName,
272332
element.range.startLineNumber,
273333
element.range.startColumn,
274334
basename(element.resource),
275335
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value
276-
);
336+
) + replies;
277337
} else {
278-
return nls.localize('resourceWithCommentLabel',
279-
"${0} at line {1} column {2} in {3}, source: {4}",
338+
return accessibleViewHint + nls.localize('resourceWithCommentLabel',
339+
"${0} at line {1} column {2} in {3}, comment: {4}",
280340
element.comment.userName,
281341
element.range.startLineNumber,
282342
element.range.startColumn,
283343
basename(element.resource),
284-
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value
285-
);
344+
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value,
345+
) + replies;
286346
}
287347
} else {
288348
if (element.threadRelevance === CommentThreadApplicability.Outdated) {
289-
return nls.localize('resourceWithCommentLabelFileOutdated',
290-
"Outdated from {0} in {1}, source: {2}",
349+
return accessibleViewHint + nls.localize('resourceWithCommentLabelFileOutdated',
350+
"Outdated from {0} in {1}, comment: {2}",
291351
element.comment.userName,
292352
basename(element.resource),
293353
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value
294-
);
354+
) + replies;
295355
} else {
296-
return nls.localize('resourceWithCommentLabelFile',
297-
"{0} in {1}, source: {2}",
356+
return accessibleViewHint + nls.localize('resourceWithCommentLabelFile',
357+
"{0} in {1}, comment: {2}",
298358
element.comment.userName,
299359
basename(element.resource),
300360
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value
301-
);
361+
) + replies;
302362
}
303363
}
304364
}
305365

366+
private getRepliesAsString(node: CommentNode, type: 'ariaLabel' | 'accessibleViewContent'): string {
367+
if (!node.replies.length || type === 'ariaLabel') {
368+
return '';
369+
}
370+
return nls.localize('replies', " {0} replies:\n{1}", node.replies.length, node.replies.map(reply => nls.localize('resourceWithRepliesLabel',
371+
"${0} {1}",
372+
reply.comment.userName,
373+
(typeof reply.comment.body === 'string') ? reply.comment.body : reply.comment.body.value)
374+
).join('\n'));
375+
}
376+
306377
private createTree(): void {
307378
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
308379
this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer, {
@@ -323,7 +394,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
323394
return nls.localize('resourceWithCommentThreadsLabel', "Comments in {0}, full path {1}", basename(element.resource), element.resource.fsPath);
324395
}
325396
if (element instanceof CommentNode) {
326-
return this.getAriaForNode(element);
397+
return this.getScreenReaderInfoForNode(element, 'ariaLabel');
327398
}
328399
return '';
329400
},

0 commit comments

Comments
 (0)