From 5dcb935e3208a96abe5b5ab6a8417d14eb4605fc Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Fri, 6 Feb 2026 08:05:47 -0800 Subject: [PATCH] Add accessibility instructions for AI coding agents (#293290) --- .../accessibility.instructions.md | 300 ++++-------------- 1 file changed, 53 insertions(+), 247 deletions(-) diff --git a/.github/instructions/accessibility.instructions.md b/.github/instructions/accessibility.instructions.md index 7777574315a..d6e800992fa 100644 --- a/.github/instructions/accessibility.instructions.md +++ b/.github/instructions/accessibility.instructions.md @@ -1,273 +1,79 @@ --- -description: 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. +description: 'Use when implementing accessibility features, ARIA labels, screen reader support, accessible help dialogs, or keybinding scoping for accessibility. Covers AccessibleContentProvider, CONTEXT_ACCESSIBILITY_MODE_ENABLED, verbosity settings, and announcement patterns.' --- -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): +# Accessibility Guidelines -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. +Accessibility is a high-priority area in VS Code. Follow these patterns to ensure features work correctly with screen readers and assistive technologies. -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**. +## Keybinding Scoping -Sections 4–7 below (signals, ARIA announcements, keyboard navigation, ARIA labels) apply more broadly to **any UI change**, including modifications to existing features. +Accessibility-specific keybindings MUST be scoped to prevent conflicts with standard shortcuts: -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. +```typescript +keybinding: { + primary: KeyCode.F7, + when: ContextKeyExpr.and( + EditorContextKeys.focus, + CONTEXT_ACCESSIBILITY_MODE_ENABLED + ), + weight: KeybindingWeight.WorkbenchContrib, +}, +``` ---- +**Why**: Without scoping, accessibility shortcuts steal commonly used keybindings (e.g., F7 for "Go to Next Symbol Highlight"). PR #293163 fixed this exact conflict. -## 1. Accessibility Help Dialog +## Accessible Help Dialogs (Alt+F1) -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. +Use `AccessibleContentProvider` directly — do not create custom subclasses: -### 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 - -```ts -import { AccessibleViewType, AccessibleContentProvider, AccessibleViewProviderId, IAccessibleViewContentProvider, IAccessibleViewOptions } from '…/accessibleView.js'; -import { IAccessibleViewImplementation } from '…/accessibleViewRegistry.js'; -import { AccessibilityVerbositySettingId } from '…/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) { - return new MyFeatureAccessibilityHelpProvider(); - } -} - -class MyFeatureAccessibilityHelpProvider extends Disposable implements IAccessibleViewContentProvider { - readonly id = AccessibleViewProviderId.MyFeature; - readonly verbositySettingKey = AccessibilityVerbositySettingId.MyFeature; - readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; - - provideContent(): string { - return [ - localize('myFeature.help.header', "Accessibility Help: My Feature"), - localize('myFeature.help.overview', "You are in My Feature. …"), - '', - localize('myFeature.help.keys', "Keyboard shortcuts:"), - localize('myFeature.help.key1', "- {0}: Do something", ''), - ].join('\n'); - } +```typescript +getProvider(): AccessibleContentProvider { + return new AccessibleContentProvider( + AccessibleViewProviderId.MyFeature, + { type: AccessibleViewType.Help }, + () => this.getHelpContent(), + () => this.onClose(), + 'accessibility.verbosity.myFeature' + ); } ``` ---- +## Announcement Patterns -## 2. Accessible View +When announcing state changes to screen readers: -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). +1. **Only announce when conditions are met**: Check that a search string is present, widget is visible, and screen reader is active +2. **Prevent double-speak**: Track announcement state with a flag and use a timeout (~1 second) +3. **Use verbosity settings**: Check `accessibility.verbosity.[feature]` before verbose announcements -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, - ); - } +```typescript +if (this.accessibilityService.isScreenReaderOptimized() && + this.configService.getValue('accessibility.verbosity.hover') && + !this._accessibilityHelpHintAnnounced) { + this._accessibilityHelpHintAnnounced = true; + // announce hint } ``` ---- +## Multi-Modal Notifications -## 3. Accessibility Verbosity Setting +For important state changes, provide all three: +1. **Audio signal**: `this.accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired)` +2. **ARIA alert**: `status(message)` for screen readers +3. **OS notification**: Respect `chat.notifyWindowOnConfirmation` setting. OS notifications are **conditional on window focus** — only fire when `targetWindow.document.hasFocus()` is `false` (see `ChatWindowNotifier`) -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. +## Configuration -### Steps +When adding a new accessible feature, register a verbosity setting: -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.errorAtPosition`, `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 }); +```typescript +// In accessibility configuration +'accessibility.verbosity.myFeature': { + type: 'boolean', + default: true, + description: localize('accessibility.verbosity.myFeature', "...") +} ``` ---- - -## 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 `` 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 +And extend `AccessibleViewProviderId` and `AccessibilityVerbositySettingId` enums for new help providers.