From 618dbf478546497fd04a20ef8a3c57d534276a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 4 Jan 2017 16:12:52 +0100 Subject: [PATCH] improve list accessibility fixes #17113 --- src/vs/base/browser/ui/list/listPaging.ts | 7 ++++--- src/vs/base/browser/ui/list/listWidget.ts | 20 ++++++++++++++++--- .../extensions/browser/extensionsList.ts | 5 ++++- .../electron-browser/extensionsViewlet.ts | 6 ++++-- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index c5ceceb4cf42e..2c36a2f801264 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -7,7 +7,7 @@ import 'vs/css!./list'; import { IDisposable } from 'vs/base/common/lifecycle'; import { range } from 'vs/base/common/arrays'; import { IDelegate, IRenderer, IFocusChangeEvent, ISelectionChangeEvent } from './list'; -import { List } from './listWidget'; +import { List, IListOptions } from './listWidget'; import { IPagedModel } from 'vs/base/common/paging'; import Event, { mapEvent } from 'vs/base/common/event'; @@ -67,10 +67,11 @@ export class PagedList { constructor( container: HTMLElement, delegate: IDelegate, - renderers: IPagedRenderer[] + renderers: IPagedRenderer[], + options: IListOptions = {} ) { const pagedRenderers = renderers.map(r => new PagedRenderer>(r, () => this.model)); - this.list = new List(container, delegate, pagedRenderers); + this.list = new List(container, delegate, pagedRenderers, options); } get onFocusChange(): Event> { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index f05b962354a05..64ab0b7ed485b 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -116,7 +116,7 @@ class FocusTrait extends Trait { renderElement(element: T, index: number, container: HTMLElement): void { super.renderElement(element, index, container); - container.setAttribute('role', 'option'); + container.setAttribute('role', 'treeitem'); container.setAttribute('id', this.getElementId(index)); } } @@ -201,6 +201,7 @@ class Controller implements IDisposable { } export interface IListOptions extends IListViewOptions { + ariaLabel?: string; } const DefaultOptions: IListOptions = {}; @@ -245,13 +246,17 @@ export class List implements IDisposable { }); this.view = new ListView(container, delegate, renderers, options); - this.view.domNode.setAttribute('role', 'listbox'); + this.view.domNode.setAttribute('role', 'tree'); this.view.domNode.tabIndex = 0; this.controller = new Controller(this, this.view); this.disposables = [this.focus, this.selection, this.view, this.controller]; this._onDOMFocus = domEvent(this.view.domNode, 'focus'); this.onFocusChange(this._onFocusChange, this, this.disposables); + + if (options.ariaLabel) { + this.view.domNode.setAttribute('aria-label', options.ariaLabel); + } } splice(start: number, deleteCount: number, ...elements: T[]): void { @@ -418,7 +423,16 @@ export class List implements IDisposable { } private _onFocusChange(): void { - DOM.toggleClass(this.view.domNode, 'element-focused', this.focus.get().length > 0); + const focus = this.focus.get(); + + if (focus.length > 0) { + this.view.domNode.setAttribute('aria-activedescendant', this.getElementId(focus[0])); + } else { + this.view.domNode.removeAttribute('aria-activedescendant'); + } + + this.view.domNode.setAttribute('role', 'tree'); + DOM.toggleClass(this.view.domNode, 'element-focused', focus.length > 0); } dispose(): void { diff --git a/src/vs/workbench/parts/extensions/browser/extensionsList.ts b/src/vs/workbench/parts/extensions/browser/extensionsList.ts index c5dd01e143ad0..88bb1de5ce3da 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsList.ts @@ -23,6 +23,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IExtensionService } from 'vs/platform/extensions/common/extensions'; export interface ITemplateData { + root: HTMLElement; element: HTMLElement; icon: HTMLImageElement; name: HTMLElement; @@ -92,7 +93,7 @@ export class Renderer implements IPagedRenderer { const disposables = [versionWidget, installCountWidget, ratingsWidget, builtinStatusAction, updateAction, reloadAction, manageAction, actionbar]; return { - element, icon, name, installCount, ratings, author, description, disposables, + root, element, icon, name, installCount, ratings, author, description, disposables, extensionDisposables: [], set extension(extension: IExtension) { versionWidget.extension = extension; @@ -110,6 +111,7 @@ export class Renderer implements IPagedRenderer { renderPlaceholder(index: number, data: ITemplateData): void { addClass(data.element, 'loading'); + data.root.removeAttribute('aria-label'); data.extensionDisposables = dispose(data.extensionDisposables); data.icon.src = ''; data.name.textContent = ''; @@ -142,6 +144,7 @@ export class Renderer implements IPagedRenderer { data.icon.style.visibility = 'inherit'; } + data.root.setAttribute('aria-label', extension.displayName); data.name.textContent = extension.displayName; data.author.textContent = extension.publisherDisplayName; data.description.textContent = extension.description; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 08263b5fe2381..6ee74bb6e7ab6 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -102,7 +102,9 @@ export class ExtensionsViewlet extends Viewlet implements IExtensionsViewlet { const delegate = new Delegate(); const renderer = this.instantiationService.createInstance(Renderer); - this.list = new PagedList(this.extensionsBox, delegate, [renderer]); + this.list = new PagedList(this.extensionsBox, delegate, [renderer], { + ariaLabel: localize('extensions', "Extensions") + }); const onKeyDown = chain(domEvent(this.searchBox, 'keydown')) .map(e => new StandardKeyboardEvent(e)); @@ -434,7 +436,7 @@ export class StatusUpdater implements IWorkbenchContribution { private onServiceChange(): void { if (this.extensionsWorkbenchService.local.some(e => e.state === ExtensionState.Installing)) { - this.activityBarService.showActivity(VIEWLET_ID, new ProgressBadge(() => localize('extensions', 'Extensions')), 'extensions-badge progress-badge'); + this.activityBarService.showActivity(VIEWLET_ID, new ProgressBadge(() => localize('extensions', "Extensions")), 'extensions-badge progress-badge'); return; }