From 0900a621135265c95c0a499b5123e2cae848ae1f Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 18 Dec 2024 15:40:29 -0600 Subject: [PATCH] get terminal completions to work for screen reader users (#236516) fix #235022 --- .../suggest/browser/simpleSuggestWidget.ts | 72 ++++++++++++++++++- .../browser/simpleSuggestWidgetRenderer.ts | 2 +- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index 61fb7437cea..bc9c9ea0cba 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -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; + 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._onDidHide.event; private readonly _onDidShow = this._register(new Emitter()); readonly onDidShow: Event = this._onDidShow.event; + private readonly _onDidFocus = new PauseableEmitter(); + readonly onDidFocus: Event = this._onDidFocus.event; get list(): List { 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): 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) { diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts index 86e0f510b0f..83605e2156d 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts @@ -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 {