Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions css/xterm.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,25 @@
color: transparent;
}

.xterm .xterm-accessibility-buffer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
padding: .5em;
background: #000;
color: #fff;
opacity: 0;
overflow: scroll;
overflow-x: hidden;
}

.xterm .xterm-accessibility-buffer:focus {
opacity: 1;
z-index: 20;
}

.xterm .live-region {
position: absolute;
left: -9999px;
Expand Down
54 changes: 50 additions & 4 deletions src/browser/AccessibilityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
*/

import * as Strings from 'browser/LocalizableStrings';
import { ITerminal, IRenderDebouncer } from 'browser/Types';
import { ITerminal, IRenderDebouncer, ReadonlyColorSet } from 'browser/Types';
import { IBuffer } from 'common/buffer/Types';
import { isMac } from 'common/Platform';
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { IRenderService } from 'browser/services/Services';
import { IRenderService, IThemeService } from 'browser/services/Services';
import { IOptionsService } from 'common/services/Services';
import { ITerminalOptions } from 'xterm';

const MAX_ROWS_TO_READ = 20;

Expand All @@ -26,6 +28,7 @@ export class AccessibilityManager extends Disposable {
private _rowElements: HTMLElement[];
private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;
private _accessibleBuffer: HTMLElement;

private _renderRowsDebouncer: IRenderDebouncer;
private _screenDprMonitor: ScreenDprMonitor;
Expand All @@ -48,7 +51,9 @@ export class AccessibilityManager extends Disposable {

constructor(
private readonly _terminal: ITerminal,
private readonly _renderService: IRenderService
@IOptionsService optionsService: IOptionsService,
@IRenderService private readonly _renderService: IRenderService,
@IThemeService themeService: IThemeService
) {
super();
this._accessibilityTreeRoot = document.createElement('div');
Expand Down Expand Up @@ -83,7 +88,14 @@ export class AccessibilityManager extends Disposable {
if (!this._terminal.element) {
throw new Error('Cannot enable accessibility before Terminal.open');
}
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);

this._accessibleBuffer = document.createElement('div');
this._accessibleBuffer.ariaLabel = Strings.accessibleBuffer;
this._accessibleBuffer.classList.add('xterm-accessibility-buffer');
this._refreshAccessibilityBuffer();
Comment thread
meganrogge marked this conversation as resolved.
this._accessibleBuffer.addEventListener('focus', () => this._refreshAccessibilityBuffer());
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibleBuffer);


this.register(this._renderRowsDebouncer);
this.register(this._terminal.onResize(e => this._handleResize(e.rows)));
Expand All @@ -97,6 +109,11 @@ export class AccessibilityManager extends Disposable {
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));

this._handleColorChange(themeService.colors);
this.register(themeService.onChangeColors(e => this._handleColorChange(e)));
this._handleFontOptionChange(optionsService.options);
this.register(optionsService.onMultipleOptionChange(['fontSize', 'fontFamily'], () => this._handleFontOptionChange(optionsService.options)));

this._screenDprMonitor = new ScreenDprMonitor(window);
this.register(this._screenDprMonitor);
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
Expand Down Expand Up @@ -299,4 +316,33 @@ export class AccessibilityManager extends Disposable {
this._liveRegion.textContent += this._charsToAnnounce;
this._charsToAnnounce = '';
}


private _refreshAccessibilityBuffer(): void {
if (!this._terminal.viewport) {
return;
}

const { bufferElements, cursorElement } = this._terminal.viewport.getBufferElements(0);
for (const element of bufferElements) {
if (element.textContent) {
element.textContent = element.textContent.replace(new RegExp(' ', 'g'), '\xA0');
}
}
this._accessibleBuffer.tabIndex = 0;
Comment thread
meganrogge marked this conversation as resolved.
Outdated
this._accessibleBuffer.ariaRoleDescription = "document";
this._accessibleBuffer.replaceChildren(...bufferElements);
this._accessibleBuffer.scrollTop = this._accessibleBuffer.scrollHeight;
this._accessibleBuffer.focus();
}

private _handleColorChange(colorSet: ReadonlyColorSet): void {
this._accessibleBuffer.style.backgroundColor = colorSet.background.css;
this._accessibleBuffer.style.color = colorSet.foreground.css;
}

private _handleFontOptionChange(options: Required<ITerminalOptions>): void {
this._accessibleBuffer.style.fontFamily = options.fontFamily;
this._accessibleBuffer.style.fontSize = `${options.fontSize}px`;
Comment thread
meganrogge marked this conversation as resolved.
Outdated
}
}
2 changes: 2 additions & 0 deletions src/browser/LocalizableStrings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export let promptLabel = 'Terminal input';

// eslint-disable-next-line prefer-const
export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read';

export const accessibleBuffer = 'Accessible terminal buffer';
Comment thread
meganrogge marked this conversation as resolved.
Outdated
5 changes: 2 additions & 3 deletions src/browser/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _handleScreenReaderModeOptionChange(value: boolean): void {
if (value) {
if (!this._accessibilityManager && this._renderService) {
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
this._accessibilityManager = this._instantiationService.createInstance(AccessibilityManager, this);
}
} else {
this._accessibilityManager?.dispose();
Expand Down Expand Up @@ -419,7 +419,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.element.dir = 'ltr'; // xterm.css assumes LTR
this.element.classList.add('terminal');
this.element.classList.add('xterm');
this.element.setAttribute('tabindex', '0');
parent.appendChild(this.element);

// Performance: Use a document fragment to build the terminal
Expand Down Expand Up @@ -553,7 +552,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
if (this.options.screenReaderMode) {
// Note that this must be done *after* the renderer is created in order to
// ensure the correct order of the dprchange event
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
this._accessibilityManager = this._instantiationService.createInstance(AccessibilityManager, this);
}
this.register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));

Expand Down
6 changes: 6 additions & 0 deletions src/browser/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ export class MockTerminal implements ITerminal {
public write(data: string): void {
throw new Error('Method not implemented.');
}
public getBufferElements(startLine: number, endLine?: number | undefined): { bufferElements: HTMLElement[]; cursorElement?: HTMLElement | undefined; } {
throw new Error('Method not implemented.');
}
public bracketedPasteMode!: boolean;
public renderer!: IRenderer;
public linkifier2!: ILinkifier2;
Expand Down Expand Up @@ -310,6 +313,9 @@ export class MockViewport implements IViewport {
public getLinesScrolled(ev: WheelEvent): number {
throw new Error('Method not implemented.');
}
public getBufferElements(startLine: number, endLine?: number | undefined): { bufferElements: HTMLElement[]; cursorElement?: HTMLElement | undefined; } {
throw new Error('Method not implemented.');
}
}

export class MockCompositionHelper implements ICompositionHelper {
Expand Down
1 change: 1 addition & 0 deletions src/browser/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(immediate?: boolean): void;
getLinesScrolled(ev: WheelEvent): number;
getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[]; cursorElement?: HTMLElement };
handleWheel(ev: WheelEvent): boolean;
handleTouchStart(ev: TouchEvent): void;
handleTouchMove(ev: TouchEvent): boolean;
Expand Down
27 changes: 27 additions & 0 deletions src/browser/Viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,33 @@ export class Viewport extends Disposable implements IViewport {
return amount;
}


public getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[]; cursorElement?: HTMLElement } {
let currentLine: string = '';
let cursorElement: HTMLElement | undefined;
const bufferElements: HTMLElement[] = [];
const end = endLine ?? this._bufferService.buffer.lines.length;
const lines = this._bufferService.buffer.lines;
for (let i = startLine; i < end; i++) {
const line = lines.get(i);
if (!line) {
continue;
}
const isWrapped = lines.get(i + 1)?.isWrapped;
currentLine += line.translateToString(!isWrapped);
if (!isWrapped || i === lines.length - 1) {
const div = document.createElement('div');
div.textContent = currentLine;
bufferElements.push(div);
if (currentLine.length > 0) {
cursorElement = div;
}
currentLine = '';
}
}
return { bufferElements, cursorElement };
}

/**
* Gets the number of pixels scrolled by the mouse event taking into account what type of delta
* is being used.
Expand Down