Skip to content

Commit fb62c1e

Browse files
authored
Fix accessibility issues with confirmation widget, add command to focus it directly if present, announce none if not present (#261874)
1 parent b3324f4 commit fb62c1e

File tree

4 files changed

+107
-4
lines changed

4 files changed

+107
-4
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { alert } from '../../../../../base/browser/ui/aria/aria.js';
7+
import { localize } from '../../../../../nls.js';
8+
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
9+
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
10+
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
11+
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
12+
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
13+
import { IChatWidgetService } from '../chat.js';
14+
import { ChatContextKeys } from '../../common/chatContextKeys.js';
15+
import { ChatAgentLocation } from '../../common/constants.js';
16+
import { isResponseVM } from '../../common/chatViewModel.js';
17+
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js';
18+
19+
export const ACTION_ID_FOCUS_CHAT_CONFIRMATION = 'workbench.action.chat.focusConfirmation';
20+
21+
class AnnounceChatConfirmationAction extends Action2 {
22+
constructor() {
23+
super({
24+
id: ACTION_ID_FOCUS_CHAT_CONFIRMATION,
25+
title: { value: localize('focusChatConfirmation', 'Focus Chat Confirmation'), original: 'Focus Chat Confirmation' },
26+
category: { value: localize('chat.category', 'Chat'), original: 'Chat' },
27+
f1: true,
28+
keybinding: {
29+
weight: KeybindingWeight.WorkbenchContrib,
30+
primary: KeyMod.Alt | KeyCode.KeyA,
31+
when: ContextKeyExpr.and(
32+
ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel),
33+
ChatContextKeys.inChatSession,
34+
CONTEXT_ACCESSIBILITY_MODE_ENABLED
35+
)
36+
},
37+
menu: [
38+
{
39+
id: MenuId.ChatConfirmationMenu,
40+
when: ChatContextKeys.inChatSession,
41+
group: '0_main'
42+
}
43+
]
44+
});
45+
}
46+
47+
async run(accessor: ServicesAccessor): Promise<void> {
48+
const chatWidgetService = accessor.get(IChatWidgetService);
49+
const lastFocusedWidget = chatWidgetService.lastFocusedWidget;
50+
51+
if (!lastFocusedWidget) {
52+
alert(localize('noChatSession', 'No active chat session found.'));
53+
return;
54+
}
55+
56+
const viewModel = lastFocusedWidget.viewModel;
57+
if (!viewModel) {
58+
alert(localize('chatNotReady', 'Chat interface not ready.'));
59+
return;
60+
}
61+
62+
// Check for active confirmations in the chat responses
63+
let firstConfirmationElement: HTMLElement | undefined;
64+
65+
const lastResponse = viewModel.getItems()[viewModel.getItems().length - 1];
66+
if (isResponseVM(lastResponse)) {
67+
const confirmationWidgets = lastFocusedWidget.domNode.querySelectorAll('.chat-confirmation-widget-container');
68+
if (confirmationWidgets.length > 0) {
69+
firstConfirmationElement = confirmationWidgets[0] as HTMLElement;
70+
}
71+
}
72+
73+
if (firstConfirmationElement) {
74+
firstConfirmationElement.focus();
75+
} else {
76+
alert(localize('noConfirmationRequired', 'No chat confirmation required'));
77+
}
78+
}
79+
}
80+
81+
export function registerChatAccessibilityActions(): void {
82+
registerAction2(AnnounceChatConfirmationAction);
83+
}

src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui
7979
content.push(localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command{0}.', getChatFocusKeybindingLabel(keybindingService, type, false)));
8080
content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command{0}.', getChatFocusKeybindingLabel(keybindingService, type, true)));
8181
content.push(localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command{0}.', '<keybinding:workbench.action.chat.nextCodeBlock>'));
82+
content.push(localize('workbench.action.chat.announceConfirmation', 'To focus pending chat confirmation dialogs, invoke the Focus Chat Confirmation Status command{0}.', '<keybinding:workbench.action.chat.focusConfirmation>'));
8283
if (type === 'panelChat') {
8384
content.push(localize('workbench.action.chat.newChat', 'To create a new chat session, invoke the New Chat command{0}.', '<keybinding:workbench.action.chat.new>'));
8485
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { LanguageModelToolsExtensionPointHandler } from '../common/tools/languag
5959
import { BuiltinToolsContribution } from '../common/tools/tools.js';
6060
import { IVoiceChatService, VoiceChatService } from '../common/voiceChatService.js';
6161
import { AgentChatAccessibilityHelp, EditsChatAccessibilityHelp, PanelChatAccessibilityHelp, QuickChatAccessibilityHelp } from './actions/chatAccessibilityHelp.js';
62+
import { registerChatAccessibilityActions } from './actions/chatAccessibilityActions.js';
6263
import { ACTION_ID_NEW_CHAT, CopilotTitleBarMenuRendering, registerChatActions } from './actions/chatActions.js';
6364
import { registerNewChatActions } from './actions/chatClearActions.js';
6465
import { CodeBlockActionRendering, registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js';
@@ -802,6 +803,7 @@ registerWorkbenchContribution2(PromptUrlHandler.ID, PromptUrlHandler, WorkbenchP
802803
registerWorkbenchContribution2(ChatSessionsView.ID, ChatSessionsView, WorkbenchPhase.AfterRestored);
803804

804805
registerChatActions();
806+
registerChatAccessibilityActions();
805807
registerChatCopyActions();
806808
registerChatCodeBlockActions();
807809
registerChatCodeCompareBlockActions();

src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface IChatConfirmationButton {
4040

4141
export interface IChatConfirmationWidgetOptions {
4242
title: string | IMarkdownString;
43+
message: string | IMarkdownString;
4344
subtitle?: string | IMarkdownString;
4445
buttons: IChatConfirmationButton[];
4546
toolbarData?: { arg: any; partType: string; partSource?: string };
@@ -144,9 +145,10 @@ abstract class BaseSimpleChatConfirmationWidget extends Disposable {
144145
) {
145146
super();
146147

147-
const { title, subtitle, buttons } = options;
148+
const { title, subtitle, message, buttons } = options;
148149
this.title = title;
149150

151+
150152
const elements = dom.h('.chat-confirmation-widget@root', [
151153
dom.h('.chat-confirmation-widget-title@title'),
152154
dom.h('.chat-confirmation-widget-message@message'),
@@ -155,7 +157,9 @@ abstract class BaseSimpleChatConfirmationWidget extends Disposable {
155157
dom.h('.chat-toolbar@toolbar'),
156158
]),
157159
]);
158-
this._domNode = elements.root;
160+
const container = createAccessibilityContainer(title, message);
161+
container.appendChild(elements.root);
162+
this._domNode = container;
159163
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});
160164

161165
const titlePart = this._register(instantiationService.createInstance(
@@ -302,6 +306,7 @@ export class SimpleChatConfirmationWidget extends BaseSimpleChatConfirmationWidg
302306

303307
export interface IChatConfirmationWidget2Options {
304308
title: string | IMarkdownString;
309+
message: string | IMarkdownString | HTMLElement;
305310
icon?: ThemeIcon;
306311
subtitle?: string | IMarkdownString;
307312
buttons: IChatConfirmationButton[];
@@ -345,7 +350,7 @@ abstract class BaseChatConfirmationWidget extends Disposable {
345350
) {
346351
super();
347352

348-
const { title, subtitle, buttons, icon } = options;
353+
const { title, subtitle, message, buttons, icon } = options;
349354
this.title = title;
350355

351356
const elements = dom.h('.chat-confirmation-widget2@root', [
@@ -360,7 +365,9 @@ abstract class BaseChatConfirmationWidget extends Disposable {
360365
dom.h('.chat-buttons@buttons'),
361366
]),
362367
]);
363-
this._domNode = elements.root;
368+
const container = createAccessibilityContainer(title, message);
369+
container.appendChild(elements.root);
370+
this._domNode = container;
364371
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});
365372

366373
const titlePart = this._register(instantiationService.createInstance(
@@ -538,3 +545,13 @@ export class ChatCustomConfirmationWidget extends BaseChatConfirmationWidget {
538545
this.renderMessage(options.message, container);
539546
}
540547
}
548+
549+
function createAccessibilityContainer(title: string | IMarkdownString, message?: string | IMarkdownString | HTMLElement): HTMLElement {
550+
const container = document.createElement('div');
551+
container.tabIndex = 0;
552+
const titleAsString = typeof title === 'string' ? title : title.value;
553+
const messageAsString = typeof message === 'string' ? message : message && 'value' in message ? message.value : message && 'textContent' in message ? message.textContent : '';
554+
container.setAttribute('aria-label', localize('chat.confirmationWidget.ariaLabel', "Chat Confirmation Dialog {0} {1}", titleAsString, messageAsString));
555+
container.classList.add('chat-confirmation-widget-container');
556+
return container;
557+
}

0 commit comments

Comments
 (0)