get terminal completions to work for screen reader users (#236516)

fix #235022
pull/236531/head
Megan Rogge 2024-12-18 15:40:29 -06:00 committed by GitHub
parent 37c543ba48
commit 0900a62113
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 70 additions and 4 deletions

View File

@ -10,9 +10,9 @@ import { List } from '../../../../base/browser/ui/list/listWidget.js';
import { ResizableHTMLElement } from '../../../../base/browser/ui/resizable/resizable.js';
import { SimpleCompletionItem } from './simpleCompletionItem.js';
import { LineContext, SimpleCompletionModel } from './simpleCompletionModel.js';
import { SimpleSuggestWidgetItemRenderer, type ISimpleSuggestWidgetFontInfo } from './simpleSuggestWidgetRenderer.js';
import { getAriaId, SimpleSuggestWidgetItemRenderer, type ISimpleSuggestWidgetFontInfo } from './simpleSuggestWidgetRenderer.js';
import { TimeoutTimer } from '../../../../base/common/async.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Emitter, Event, PauseableEmitter } from '../../../../base/common/event.js';
import { MutableDisposable, Disposable } from '../../../../base/common/lifecycle.js';
import { clamp } from '../../../../base/common/numbers.js';
import { localize } from '../../../../nls.js';
@ -68,7 +68,9 @@ export class SimpleSuggestWidget extends Disposable {
private _forceRenderingAbove: boolean = false;
private _preference?: WidgetPositionPreference;
private readonly _pendingLayout = this._register(new MutableDisposable());
// private _currentSuggestionDetails?: CancelablePromise<void>;
private _focusedItem?: SimpleCompletionItem;
private _ignoreFocusEvents: boolean = false;
readonly element: ResizableHTMLElement;
private readonly _messageElement: HTMLElement;
private readonly _listElement: HTMLElement;
@ -83,6 +85,8 @@ export class SimpleSuggestWidget extends Disposable {
readonly onDidHide: Event<this> = this._onDidHide.event;
private readonly _onDidShow = this._register(new Emitter<this>());
readonly onDidShow: Event<this> = this._onDidShow.event;
private readonly _onDidFocus = new PauseableEmitter<ISimpleSelectedSuggestion>();
readonly onDidFocus: Event<ISimpleSelectedSuggestion> = this._onDidFocus.event;
get list(): List<SimpleCompletionItem> { return this._list; }
@ -206,6 +210,7 @@ export class SimpleSuggestWidget extends Disposable {
this._register(this._list.onMouseDown(e => this._onListMouseDownOrTap(e)));
this._register(this._list.onTap(e => this._onListMouseDownOrTap(e)));
this._register(this._list.onDidChangeFocus(e => this._onListFocus(e)));
this._register(this._list.onDidChangeSelection(e => this._onListSelection(e)));
this._register(configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('editor.suggest.showIcons')) {
@ -214,6 +219,67 @@ export class SimpleSuggestWidget extends Disposable {
}));
}
private _onListFocus(e: IListEvent<SimpleCompletionItem>): void {
if (this._ignoreFocusEvents) {
return;
}
if (this._state === State.Details) {
// This can happen when focus is in the details-panel and when
// arrow keys are pressed to select next/prev items
this._setState(State.Open);
}
if (!e.elements.length) {
// if (this._currentSuggestionDetails) {
// this._currentSuggestionDetails.cancel();
// this._currentSuggestionDetails = undefined;
// this._focusedItem = undefined;
// }
this._clearAriaActiveDescendant();
return;
}
if (!this._completionModel) {
return;
}
// this._ctxSuggestWidgetHasFocusedSuggestion.set(true);
const item = e.elements[0];
const index = e.indexes[0];
if (item !== this._focusedItem) {
// this._currentSuggestionDetails?.cancel();
// this._currentSuggestionDetails = undefined;
this._focusedItem = item;
this._list.reveal(index);
const id = getAriaId(index);
const node = dom.getActiveWindow().document.activeElement;
if (node && id) {
node.setAttribute('aria-haspopup', 'true');
node.setAttribute('aria-autocomplete', 'list');
node.setAttribute('aria-activedescendant', id);
} else {
this._clearAriaActiveDescendant();
}
}
// emit an event
this._onDidFocus.fire({ item, index, model: this._completionModel });
}
private _clearAriaActiveDescendant(): void {
const node = dom.getActiveWindow().document.activeElement;
if (!node) {
return;
}
node.setAttribute('aria-haspopup', 'false');
node.setAttribute('aria-autocomplete', 'both');
node.removeAttribute('aria-activedescendant');
}
private _cursorPosition?: { top: number; left: number; height: number };
setCompletionModel(completionModel: SimpleCompletionModel) {

View File

@ -14,7 +14,7 @@ import { DisposableStore } from '../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
export function getAriaId(index: number): string {
return `simple-suggest-aria-id:${index}`;
return `simple-suggest-aria-id-${index}`;
}
export interface ISimpleSuggestionTemplateData {