diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index a85487020b0..7cff3e150c7 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -23,7 +23,7 @@ interface ITraitChangeEvent { class TraitRenderer implements IRenderer> { constructor( - private controller: Trait, + private controller: Trait, private renderer: IRenderer ) {} @@ -37,7 +37,8 @@ class TraitRenderer implements IRenderer> } renderElement(element: T, index: number, templateData: ITraitTemplateData): void { - DOM.toggleClass(templateData.container, this.controller.trait, this.controller.contains(index)); + this.controller.renderElement(element, index, templateData.container); + this.renderer.renderElement(element, index, templateData.data); } @@ -46,7 +47,7 @@ class TraitRenderer implements IRenderer> } } -class Trait implements IDisposable { +class Trait implements IDisposable { private indexes: number[]; @@ -74,8 +75,8 @@ class Trait implements IDisposable { this._onChange.fire({ indexes }); } - get trait(): string { - return this._trait; + renderElement(element: T, index: number, container:HTMLElement): void { + DOM.toggleClass(container, this._trait, this.contains(index)); } set(...indexes: number[]): number[] { @@ -93,7 +94,7 @@ class Trait implements IDisposable { return this.indexes.some(i => i === index); } - wrapRenderer(renderer: IRenderer): IRenderer> { + wrapRenderer(renderer: IRenderer): IRenderer> { return new TraitRenderer(this, renderer); } @@ -103,6 +104,22 @@ class Trait implements IDisposable { } } +class FocusTrait extends Trait { + + private _idPrefix:string; + + constructor(idPrefix:string) { + super('focused'); + this._idPrefix = idPrefix; + } + + renderElement(element: T, index: number, container:HTMLElement): void { + super.renderElement(element, index, container); + container.setAttribute('role', 'option'); + container.setAttribute('id', idForIndex(this._idPrefix, index)); + } +} + class Controller implements IDisposable { private toDispose: IDisposable[]; @@ -124,10 +141,17 @@ class Controller implements IDisposable { } } +function idForIndex(idPrefix:string, index:number): string { + return idPrefix + '_' + index; +} + export class List implements IDisposable { - private focus: Trait; - private selection: Trait; + private static LIST_INSTANCE_CNT = 0; + private _idPrefix:string; + + private focus: Trait; + private selection: Trait; private eventBufferer: EventBufferer; private view: ListView; private controller: Controller; @@ -145,7 +169,8 @@ export class List implements IDisposable { delegate: IDelegate, renderers: IRenderer[] ) { - this.focus = new Trait('focused'); + this._idPrefix = 'list_id_' + (++List.LIST_INSTANCE_CNT); + this.focus = new FocusTrait(this._idPrefix); this.selection = new Trait('selected'); this.eventBufferer = new EventBufferer(); @@ -156,9 +181,14 @@ export class List implements IDisposable { }); this.view = new ListView(container, delegate, renderers); + this.view.domNode.setAttribute('role', 'listbox'); this.controller = new Controller(this, this.view); } + idForIndex(index:number): string { + return idForIndex(this._idPrefix, index); + } + splice(start: number, deleteCount: number, ...elements: T[]): void { this.eventBufferer.bufferEvents(() => { this.focus.splice(start, deleteCount, elements.length); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 57536aca033..4e4bc2dd50b 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -86,6 +86,7 @@ export interface IView extends IDisposable { renderOnce(callback:() => any): any; render(now:boolean): void; + setAriaActiveDescendant(id:string): void; focus(): void; isFocused(): boolean; @@ -607,6 +608,8 @@ export interface ICodeEditor extends editorCommon.ICommonCodeEditor { * Set the model ranges that will be hidden in the view. */ setHiddenAreas(ranges:editorCommon.IRange[]): void; + + setAriaActiveDescendant(id:string): void; } /** diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index bc37d51cb34..2b9a18d00ab 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -171,6 +171,9 @@ export class View extends ViewEventHandler implements editorBrowser.IView, IDisp this.textArea.setAttribute('aria-label', this.context.configuration.editor.ariaLabel); this.textArea.setAttribute('role', 'textbox'); this.textArea.setAttribute('aria-multiline', 'true'); + this.textArea.setAttribute('aria-haspopup', 'false'); + this.textArea.setAttribute('aria-autocomplete', 'both'); + StyleMutator.setTop(this.textArea, 0); StyleMutator.setLeft(this.textArea, 0); // Give textarea same font size & line height as editor, for the IME case (when the textarea is visible) @@ -400,6 +403,20 @@ export class View extends ViewEventHandler implements editorBrowser.IView, IDisp }; } + public setAriaActiveDescendant(id:string): void { + if (id) { + this.textArea.setAttribute('role', 'combobox'); + if (this.textArea.getAttribute('aria-activedescendant') !== id) { + this.textArea.setAttribute('aria-haspopup', 'true'); + this.textArea.setAttribute('aria-activedescendant', id); + } + } else { + this.textArea.setAttribute('role', 'textbox'); + this.textArea.removeAttribute('aria-activedescendant'); + this.textArea.removeAttribute('aria-haspopup'); + } + } + // --- begin event handlers public onLayoutChanged(layoutInfo:editorCommon.IEditorLayoutInfo): boolean { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 127ea250804..d6149310dc3 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -386,6 +386,13 @@ export class CodeEditorWidget extends CommonCodeEditor implements editorBrowser. } } + public setAriaActiveDescendant(id:string): void { + if (!this.hasView) { + return; + } + this._view.setAriaActiveDescendant(id); + } + _attachModel(model:editorCommon.IModel): void { this._view = null; diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 422d5969b03..75705d76112 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -75,12 +75,11 @@ class Renderer implements IRenderer { const data = templateData; const suggestion = (element).suggestion; + data.root.setAttribute('aria-label', suggestion.label); if (suggestion.type && suggestion.type.charAt(0) === '#') { - data.root.setAttribute('aria-label', 'color'); data.icon.className = 'icon customcolor'; data.colorspan.style.backgroundColor = suggestion.type.substring(1); } else { - data.root.setAttribute('aria-label', suggestion.type); data.icon.className = 'icon ' + suggestion.type; data.colorspan.style.backgroundColor = ''; } @@ -375,10 +374,15 @@ export class SuggestWidget implements IContentWidget, IDisposable { } if (!e.elements.length) { + this.editor.setAriaActiveDescendant(null); return; } const item = e.elements[0]; + // TODO@Alex: the list is not done rendering... + setTimeout(() => { + this.editor.setAriaActiveDescendant(this.list.idForIndex(e.indexes[0])); + }, 100); if (item === this.focusedItem) { return;