add first repo skill, comprehensive accessibility guidelines for VS Code features (#293231)

This commit is contained in:
Megan Rogge
2026-02-05 17:18:19 -06:00
committed by GitHub
parent 665de62fd7
commit ccef9ec342

292
.github/skills/accessibility.md vendored Normal file
View File

@@ -0,0 +1,292 @@
---
name: vscode-accessibility
description: Use when creating new UI or updating existing UI features. Accessibility guidelines for VS Code features — covers accessibility help dialogs, accessible views, verbosity settings, accessibility signals, ARIA alerts/status announcements, keyboard navigation, and ARIA labels/roles. Applies to both new interactive UI surfaces and updates to existing features.
---
When adding a **new interactive UI surface** to VS Code — a panel, view, widget, editor overlay, dialog, or any rich focusable component the user interacts with — you **must** provide three accessibility components (if they do not already exist for the feature):
1. **An Accessibility Help Dialog** — opened via the accessibility help keybinding when the feature has focus.
2. **An Accessible View** — a plain-text read-only editor that presents the feature's content to screen reader users (when the feature displays non-trivial visual content).
3. **An Accessibility Verbosity Setting** — a boolean setting that controls whether the "open accessibility help" hint is announced.
Examples of existing features that have all three: the **terminal**, **chat panel**, **notebook**, **diff editor**, **inline completions**, **comments**, **debug REPL**, **hover**, and **notifications**. Features with only a help dialog (no accessible view) include **find widgets**, **source control input**, **keybindings editor**, **problems panel**, and **walkthroughs**.
Sections 47 below (signals, ARIA announcements, keyboard navigation, ARIA labels) apply more broadly to **any UI change**, including modifications to existing features.
When **updating an existing feature** — for example, adding new commands, keyboard shortcuts, or interactive capabilities — you must also update the feature's existing accessibility help dialog (`provideContent()`) to document the new functionality. Screen reader users rely on the help dialog as the primary way to discover available actions.
---
## 1. Accessibility Help Dialog
An accessibility help dialog tells the user what the feature does, which keyboard shortcuts are available, and how to interact with it via a screen reader.
### Steps
1. **Create a class implementing `IAccessibleViewImplementation`** with `type = AccessibleViewType.Help`.
- Set a `priority` (higher = shown first when multiple providers match).
- Set `when` to a `ContextKeyExpression` that matches when the feature is focused.
- `getProvider(accessor)` returns an `AccessibleContentProvider`.
2. **Create a content-provider class** implementing `IAccessibleViewContentProvider`.
- `id` — add a new entry in the `AccessibleViewProviderId` enum in `src/vs/platform/accessibility/browser/accessibleView.ts`.
- `verbositySettingKey` — reference the new `AccessibilityVerbositySettingId` entry (see §3).
- `options``{ type: AccessibleViewType.Help }`.
- `provideContent()` — return localized, multi-line help text.
3. **Implement `onClose()`** to restore focus to whatever element was focused before the help dialog opened. This ensures keyboard users and screen reader users return to their previous context.
4. **Register** the implementation:
```ts
AccessibleViewRegistry.register(new MyFeatureAccessibilityHelp());
```
in the feature's `*.contribution.ts` file.
### Example skeleton
The simplest approach is to return an `AccessibleContentProvider` directly from `getProvider()`. This is the most common pattern in the codebase (used by chat, inline chat, quick chat, etc.):
```ts
import { AccessibleViewType, AccessibleContentProvider, AccessibleViewProviderId } from '…/accessibleView.js';
import { IAccessibleViewImplementation } from '…/accessibleViewRegistry.js';
import { AccessibilityVerbositySettingId } from '…/accessibilityConfiguration.js';
import { AccessibleViewType, AccessibleContentProvider, AccessibleViewProviderId, IAccessibleViewContentProvider, IAccessibleViewOptions } from '../../../../platform/accessibility/browser/accessibleView.js';
import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js';
import { AccessibilityVerbositySettingId } from '../../../../platform/accessibility/common/accessibilityConfiguration.js';
export class MyFeatureAccessibilityHelp implements IAccessibleViewImplementation {
readonly priority = 100;
readonly name = 'my-feature';
readonly type = AccessibleViewType.Help;
readonly when = MyFeatureContextKeys.isFocused;
getProvider(accessor: ServicesAccessor) {
const helpText = [
localize('myFeature.help.overview', "You are in My Feature. …"),
localize('myFeature.help.key1', "- {0}: Do something", '<keybinding:myFeature.doSomething>'),
].join('\n');
return new AccessibleContentProvider(
AccessibleViewProviderId.MyFeature,
{ type: AccessibleViewType.Help },
() => helpText,
() => { /* onClose — refocus whatever was focused before */ },
AccessibilityVerbositySettingId.MyFeature,
);
}
}
```
Alternatively, if the provider needs injected services or must track state (e.g., storing a reference to the previously focused element), create a custom class that extends `Disposable` and implements `IAccessibleViewContentProvider`, then instantiate it via `IInstantiationService` (see `CommentsAccessibilityHelpProvider` for an example):
```ts
class MyFeatureAccessibilityHelpProvider extends Disposable implements IAccessibleViewContentProvider {
readonly id = AccessibleViewProviderId.MyFeature;
readonly verbositySettingKey = AccessibilityVerbositySettingId.MyFeature;
readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help };
provideContent(): string { /* … */ }
onClose(): void { /* … */ }
}
// In getProvider():
getProvider(accessor: ServicesAccessor) {
return accessor.get(IInstantiationService).createInstance(MyFeatureAccessibilityHelpProvider);
}
```
---
## 2. Accessible View
An accessible view presents the feature's visual content as plain text in a read-only editor. It is required when the feature renders rich or visual content that a screen reader cannot directly read (for example: chat responses, hover tooltips, notifications, terminal output, inline completions).
If the feature is purely keyboard-driven with native text input/output (e.g., a simple input field), an accessible view is not needed — only an accessibility help dialog is required.
### Steps
1. **Create a class implementing `IAccessibleViewImplementation`** with `type = AccessibleViewType.View`.
2. **Create a content-provider** similar to the help dialog, but:
- `options` — `{ type: AccessibleViewType.View }`, optionally with a `language` for syntax highlighting.
- `provideContent()` — return the feature's current content as plain text.
- Optionally implement `provideNextContent()` / `providePreviousContent()` for item-by-item navigation.
- Implement `onClose()` to restore focus to whatever was focused before the accessible view was opened.
- Optionally provide `actions` for actions the user can take from the accessible view.
3. **Register** alongside the help dialog:
```ts
AccessibleViewRegistry.register(new MyFeatureAccessibleView());
```
### Example skeleton
```ts
export class MyFeatureAccessibleView implements IAccessibleViewImplementation {
readonly priority = 100;
readonly name = 'my-feature';
readonly type = AccessibleViewType.View;
readonly when = MyFeatureContextKeys.isFocused;
getProvider(accessor: ServicesAccessor) {
// Retrieve services, build content from the feature's current state
const content = getMyFeatureContent();
if (!content) {
return undefined;
}
return new AccessibleContentProvider(
AccessibleViewProviderId.MyFeature,
{ type: AccessibleViewType.View },
() => content,
() => { /* onClose — refocus whatever was focused before the accessible view opened */ },
AccessibilityVerbositySettingId.MyFeature,
);
}
}
```
---
## 3. Accessibility Verbosity Setting
A verbosity setting controls whether a hint such as "press Alt+F1 for accessibility help" is announced when the feature gains focus. Users who already know the shortcut can disable it.
### Steps
1. **Add an entry** to `AccessibilityVerbositySettingId` in
`src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts`:
```ts
export const enum AccessibilityVerbositySettingId {
// … existing entries …
MyFeature = 'accessibility.verbosity.myFeature'
}
```
2. **Register the configuration property** in the same file's `configuration.properties` object:
```ts
[AccessibilityVerbositySettingId.MyFeature]: {
description: localize('verbosity.myFeature.description',
'Provide information about how to access the My Feature accessibility help menu when My Feature is focused.'),
...baseVerbosityProperty
},
```
The `baseVerbosityProperty` gives it `type: 'boolean'`, `default: true`, and `tags: ['accessibility']`.
3. **Reference the setting key** in both the help-dialog provider (`verbositySettingKey`) and the accessible-view provider so the runtime can check whether to show the hint.
---
## 4. Accessibility Signals (Sounds & Announcements)
Accessibility signals provide audible and spoken feedback for events that happen visually. Use `IAccessibilitySignalService` to play signals when something important occurs (e.g., an error appears, a task completes, content changes).
### When to use
- **Use an existing signal** when the event already has one defined (see `AccessibilitySignal.*` static members — e.g., `AccessibilitySignal.error`, `AccessibilitySignal.terminalQuickFix`, `AccessibilitySignal.clear`).
- **If no existing signal fits**, reach out to @meganrogge to discuss adding a new one. Do not register new signals without coordinating first.
### How signals work
Each signal has two modalities controlled by user settings:
- **Sound** — a short audio cue, configurable to `auto` (on when screen reader attached), `on`, or `off`.
- **Announcement** — a spoken message via `aria-live`, configurable to `auto` or `off`.
### Usage
```ts
// Inject the service via constructor parameter
constructor(
@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService
) { }
// Play a signal
this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalQuickFix);
// Play with options
this._accessibilitySignalService.playSignal(AccessibilitySignal.error, { userGesture: true });
```
---
## 5. ARIA Alerts vs. Status Messages
Use the `alert()` and `status()` functions from `src/vs/base/browser/ui/aria/aria.ts` to announce dynamic changes to screen readers.
### `alert(msg)` — Assertive live region (`role="alert"`)
- **Use for**: Urgent, important information that the user must know immediately.
- **Examples**: Errors, warnings, critical state changes, results of a user-initiated action.
- **Behavior**: Interrupts the screen reader's current speech.
### `status(msg)` — Polite live region (`aria-live="polite"`)
- **Use for**: Non-urgent, informational updates that should be spoken when the screen reader is idle.
- **Examples**: Progress updates, search result counts, background state changes.
- **Behavior**: Queued and spoken after the screen reader finishes its current output.
### Guidelines
- **Prefer `status()` over `alert()`** unless the information is time-sensitive or the result of a direct user action. Overusing `alert()` creates a noisy, disruptive experience.
- **Keep messages concise.** Screen readers read the entire message; long messages delay the user.
- **Do not duplicate** — if an accessibility signal already announces the event, do not also call `alert()` / `status()` for the same information.
- **Localize** all messages with `nls.localize()`.
---
## 6. Keyboard Navigation
Every interactive UI element must be fully operable via the keyboard.
### Requirements
- **Tab order**: All interactive elements must be reachable via `Tab` / `Shift+Tab` in a logical order.
- **Arrow key navigation**: Lists, trees, grids, and toolbars must support arrow key navigation following WAI-ARIA patterns.
- **Focus visibility**: Focused elements must have a visible focus indicator (VS Code's theme system provides this via `focusBorder`).
- **No mouse-only interactions**: Every action reachable by click or hover must also be reachable via keyboard (context menus, buttons, toggles, etc.).
- **Escape to dismiss**: Overlays, dialogs, and popups must be dismissable with `Escape`, returning focus to the previous element.
- **Focus trapping**: Modal dialogs must trap focus within the dialog until dismissed.
---
## 7. ARIA Labels and Roles
All interactive UI elements must have appropriate ARIA attributes so screen readers can identify and describe them.
### Requirements
- **`aria-label`**: Every interactive element without visible text (icon buttons, icon-only actions, custom widgets) must have a descriptive `aria-label`. Labels should be localized.
- **`aria-labelledby`** / **`aria-describedby`**: Use these to associate elements with existing visible text rather than duplicating strings.
- **`role`**: Custom widgets that do not use native HTML elements must declare the correct ARIA role (e.g., `role="button"`, `role="tree"`, `role="tablist"`).
- **`aria-expanded`**, **`aria-selected`**, **`aria-checked`**: Toggle and selection states must be communicated via the appropriate ARIA state attributes.
- **`aria-hidden="true"`**: Decorative or redundant elements (icons next to text labels, decorative separators) must be hidden from the accessibility tree.
### Guidelines
- Avoid generic labels like "button" or "icon" — describe the action: "Close panel", "Toggle sidebar", "Run task".
- Test with a screen reader (VoiceOver on macOS, NVDA on Windows) to verify labels are spoken correctly in context.
- Lists and trees should use `aria-setsize` and `aria-posinset` when virtualized so screen readers report the correct count.
---
## Checklist for Every New Feature
- [ ] New `AccessibleViewProviderId` entry added in `accessibleView.ts`
- [ ] New `AccessibilityVerbositySettingId` entry added in `accessibilityConfiguration.ts`
- [ ] Verbosity setting registered in the configuration properties with `...baseVerbosityProperty`
- [ ] `IAccessibleViewImplementation` with `type = Help` created and registered
- [ ] Content provider references the correct `verbositySettingKey`
- [ ] Help text is fully localized using `nls.localize()`
- [ ] Keybindings in help text use `<keybinding:commandId>` syntax for dynamic resolution
- [ ] `when` context key is set so the dialog only appears when the feature is focused
- [ ] If the feature has rich/visual content: `IAccessibleViewImplementation` with `type = View` created and registered
- [ ] Registration calls in the feature's `*.contribution.ts` file
- [ ] Accessibility signal played for important events (use existing `AccessibilitySignal.*` or register a new one)
- [ ] `aria.alert()` or `aria.status()` used appropriately for dynamic changes (prefer `status()` unless urgent)
- [ ] All interactive elements reachable and operable via keyboard
- [ ] All interactive elements without visible text have a localized `aria-label`
- [ ] Custom widgets declare the correct ARIA `role` and state attributes
- [ ] Decorative elements are hidden with `aria-hidden="true"`
## Key Files
- `src/vs/platform/accessibility/browser/accessibleView.ts` — `AccessibleViewProviderId`, `AccessibleContentProvider`, `IAccessibleViewContentProvider`
- `src/vs/platform/accessibility/browser/accessibleViewRegistry.ts` — `AccessibleViewRegistry`, `IAccessibleViewImplementation`
- `src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts` — `AccessibilityVerbositySettingId`, verbosity setting registration
- `src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts` — `IAccessibilitySignalService`, `AccessibilitySignal`
- `src/vs/base/browser/ui/aria/aria.ts` — `alert()`, `status()` for ARIA live region announcements