diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index b76361c2695c7b81c0860b04d2fc1f8970f57522..b7c858d1ed209154a89444de2d107c7a398353fc 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -44,7 +44,7 @@ export interface IListViewDragAndDrop extends IListDragAndDrop { export interface IListViewAccessibilityProvider { getSetSize?(element: T, index: number, listLength: number): number; getPosInSet?(element: T, index: number): number; - getRole?(element: T): string; + getRole?(element: T): string | undefined; isChecked?(element: T): boolean | undefined; } @@ -158,7 +158,7 @@ class ListViewAccessibilityProvider implements Required number; readonly getPosInSet: (element: any, index: number) => number; - readonly getRole: (element: T) => string; + readonly getRole: (element: T) => string | undefined; readonly isChecked: (element: T) => boolean | undefined; constructor(accessibilityProvider?: IListViewAccessibilityProvider) { @@ -652,7 +652,7 @@ export class ListView implements ISpliceable, IDisposable { if (!item.row) { item.row = this.cache.alloc(item.templateId); - const role = this.accessibilityProvider.getRole(item.element); + const role = this.accessibilityProvider.getRole(item.element) || 'listitem'; item.row!.domNode!.setAttribute('role', role); const checked = this.accessibilityProvider.isChecked(item.element); if (typeof checked !== 'undefined') { diff --git a/src/vs/platform/accessibility/common/accessibility.ts b/src/vs/platform/accessibility/common/accessibility.ts index 3a025be247a59d618033b999cd52b71b0fb01381..65fcf4abaf6995821b18dba9d3d15987146a0c07 100644 --- a/src/vs/platform/accessibility/common/accessibility.ts +++ b/src/vs/platform/accessibility/common/accessibility.ts @@ -32,3 +32,8 @@ export const enum AccessibilitySupport { } export const CONTEXT_ACCESSIBILITY_MODE_ENABLED = new RawContextKey('accessibilityModeEnabled', false); + +export interface IAccessibilityInformation { + label: string; + role?: string; +} diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 9f0c6ee57230de0530ac674e7bcaae50ec0e456e..53e50a94eba841799e8aab49cca29d1ac604580a 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1039,6 +1039,11 @@ declare module 'vscode' { */ label?: string | TreeItemLabel | /* for compilation */ any; + /** + * Accessibility information used when screen reader interacts with this tree item. + */ + accessibilityInformation?: AccessibilityInformation; + /** * @param label Label describing this item * @param collapsibleState [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. Default is [TreeItemCollapsibleState.None](#TreeItemCollapsibleState.None) @@ -1100,6 +1105,11 @@ declare module 'vscode' { */ name: string; + /** + * Accessibility information used when screen reader interacts with this status bar item. + */ + accessibilityInformation?: AccessibilityInformation; + /** * The alignment of the status bar item. */ @@ -1986,6 +1996,11 @@ declare module 'vscode' { */ contextValue?: string; + /** + * Accessibility information used when screen reader interacts with this timeline item. + */ + accessibilityInformation?: AccessibilityInformation; + /** * @param label A human-readable string describing the timeline item * @param timestamp A timestamp (in milliseconds since 1 January 1970 00:00:00) for when the timeline item occurred @@ -2133,4 +2148,24 @@ declare module 'vscode' { } //#endregion + + //#region Accessibility information: https://github.com/microsoft/vscode/issues/95360 + + /** + * Accessibility information which controls screen reader behavior. + */ + export interface AccessibilityInformation { + label: string; + role?: string; + } + + export interface StatusBarItem { + /** + * Accessibility information used when screen reader interacts with this StatusBar item + */ + accessibilityInformation?: AccessibilityInformation; + } + + //#endregion + } diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index d2846ff106e9cdfa6f01971d21d19513b14534ee..33e5b948c9f386269aa144aca3eb3ff1e918a00d 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -9,6 +9,7 @@ import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { dispose } from 'vs/base/common/lifecycle'; import { Command } from 'vs/editor/common/modes'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { @@ -25,9 +26,14 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this.entries.clear(); } - $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined): void { + $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation): void { // if there are icons in the text use the tooltip for the aria label - const ariaLabel = text.indexOf('$(') === -1 ? text : tooltip || text; + let ariaLabel: string; + if (accessibilityInformation) { + ariaLabel = accessibilityInformation.label; + } else { + ariaLabel = text.indexOf('$(') === -1 ? text : tooltip || text; + } const entry: IStatusbarEntry = { text, tooltip, command, color, ariaLabel }; if (typeof priority === 'undefined') { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 82c79c2b59cca102c8fb19dc2f986d2008878a55..448f568c1951aacbd347c5fb58bc02c35a306409 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -531,12 +531,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I let id: string; let name: string; let alignment: number | undefined; + let accessibilityInformation: vscode.AccessibilityInformation | undefined = undefined; if (alignmentOrOptions && typeof alignmentOrOptions !== 'number') { id = alignmentOrOptions.id; name = alignmentOrOptions.name; alignment = alignmentOrOptions.alignment; priority = alignmentOrOptions.priority; + accessibilityInformation = alignmentOrOptions.accessibilityInformation; } else { id = extension.identifier.value; name = nls.localize('extensionLabel', "{0} (Extension)", extension.displayName || extension.name); @@ -544,7 +546,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I priority = priority; } - return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority); + return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 739d5d69e3d339ab6ebae852c4a2c2ecd66ca272..6b6c96648f03c42f62c52fa437da215b5979a83e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -56,6 +56,7 @@ import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/cal import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -545,7 +546,7 @@ export interface MainThreadQuickOpenShape extends IDisposable { } export interface MainThreadStatusBarShape extends IDisposable { - $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: ICommandDto | undefined, color: string | ThemeColor | undefined, alignment: statusbar.StatusbarAlignment, priority: number | undefined): void; + $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: ICommandDto | undefined, color: string | ThemeColor | undefined, alignment: statusbar.StatusbarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void; $dispose(id: number): void; } diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index d26ee3d7c8602103ab833ce04489c01e0c11bfdc..bc2c6fa2d0b1ade3d1cb2a63d67a86d71f741913 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -35,8 +35,9 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _timeoutHandle: any; private _proxy: MainThreadStatusBarShape; private _commands: CommandsConverter; + private _accessibilityInformation?: vscode.AccessibilityInformation; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation) { this._id = ExtHostStatusBarEntry.ID_GEN++; this._proxy = proxy; this._commands = commands; @@ -44,6 +45,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this._statusName = name; this._alignment = alignment; this._priority = priority; + this._accessibilityInformation = accessibilityInformation; } public get id(): number { @@ -74,6 +76,10 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { return this._command?.fromApi; } + public get accessibilityInformation(): vscode.AccessibilityInformation | undefined { + return this._accessibilityInformation; + } + public set text(text: string) { this._text = text; this.update(); @@ -136,7 +142,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { // Set to status bar this._proxy.$setEntry(this.id, this._statusId, this._statusName, this.text, this.tooltip, this._command?.internal, this.color, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, - this._priority); + this._priority, this._accessibilityInformation); }, 0); } @@ -196,8 +202,8 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority); + createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation): vscode.StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index c15a9e736173c08992bd33f78d1efea0a875f9c1..24a5092542e6b5c0b412282f815367034d62fe08 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -152,7 +152,8 @@ export class ExtHostTimeline implements IExtHostTimeline { command: item.command ? commandConverter.toInternal(item.command, disposables) : undefined, icon: icon, iconDark: iconDark, - themeIcon: themeIcon + themeIcon: themeIcon, + accessibilityInformation: item.accessibilityInformation }; }; }; @@ -188,4 +189,3 @@ export class ExtHostTimeline implements IExtHostTimeline { function getUriKey(uri: URI | undefined): string | undefined { return uri?.toString(); } - diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index a2e117ad7e515b70603a4415e6062ea04ae244ba..67a97da2d527a3dd59d68cbfc12ac5c52f232b57 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -486,7 +486,7 @@ class ExtHostTreeView extends Disposable { return node; } - private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem, parent: TreeNode | Root): TreeNode { + private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem2, parent: TreeNode | Root): TreeNode { const disposable = new DisposableStore(); const handle = this.createHandle(element, extensionTreeItem, parent); const icon = this.getLightIconPath(extensionTreeItem); @@ -502,7 +502,8 @@ class ExtHostTreeView extends Disposable { icon, iconDark: this.getDarkIconPath(extensionTreeItem) || icon, themeIcon: extensionTreeItem.iconPath instanceof ThemeIcon ? { id: extensionTreeItem.iconPath.id } : undefined, - collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState + collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState, + accessibilityInformation: extensionTreeItem.accessibilityInformation }; return { diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 37752ef5b3d1e5454b7d93d5cf764f23036f9eb9..f1875064562ee39ed59efa63f401184ffa303466 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -425,8 +425,15 @@ export class TreeView extends Disposable implements ITreeView { identityProvider: new TreeViewIdentityProvider(), accessibilityProvider: { getAriaLabel(element: ITreeItem): string { + if (element.accessibilityInformation) { + return element.accessibilityInformation.label; + } + return element.tooltip ? element.tooltip : element.label ? element.label.label : ''; }, + getRole(element: ITreeItem): string | undefined { + return element.accessibilityInformation?.role; + }, getWidgetAriaLabel(): string { return widgetAriaLabel; } diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 6b7a1506c99b6ba51d01a7f8ada7de69d57eb4df..7ed78a972f589486fd858d0fa7f29e17c39130f0 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -22,6 +22,7 @@ import { SetMap } from 'vs/base/common/collections'; import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import Severity from 'vs/base/common/severity'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; @@ -634,6 +635,8 @@ export interface ITreeItem { command?: Command; children?: ITreeItem[]; + + accessibilityInformation?: IAccessibilityInformation; } export interface ITreeViewDataProvider { diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 6ddc7254512007697e5167180e7605dc2c46c80d..bd7cfc2b4c81fefb64f74f7349b8333eecee2a1e 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -883,7 +883,7 @@ export class TimelinePane extends ViewPane { if (isLoadMoreCommand(element)) { return element.ariaLabel; } - return element.ariaLabel ?? localize('timeline.aria.item', "{0}: {1}", element.relativeTime ?? '', element.label); + return element.accessibilityInformation ? element.accessibilityInformation.label : localize('timeline.aria.item', "{0}: {1}", element.relativeTime ?? '', element.label); }, getWidgetAriaLabel(): string { return localize('timeline', "Timeline"); diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index 8f2823c6b7bf97142dbf360b2cd3ee950b13d9a5..c63e6ad52a108390aaa8b923fe72e43dd0f48bbb 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { Command } from 'vs/editor/common/modes'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; export function toKey(extension: ExtensionIdentifier | string, source: string) { return `${typeof extension === 'string' ? extension : ExtensionIdentifier.toKey(extension)}|${source}`; @@ -24,7 +25,7 @@ export interface TimelineItem { id?: string; timestamp: number; label: string; - ariaLabel?: string; + accessibilityInformation?: IAccessibilityInformation; icon?: URI, iconDark?: URI, themeIcon?: { id: string },