Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype improved a11y buffer view #4340

Merged
merged 22 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
56 changes: 52 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 _accessiblityBuffer: 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,16 @@ 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._accessiblityBuffer = document.createElement('div');
this._accessiblityBuffer.ariaLabel = Strings.accessibilityBuffer;
this._accessiblityBuffer.classList.add('xterm-accessibility-buffer');

// TODO: this is needed when content editable is false
this._refreshAccessibilityBuffer();
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
this._accessiblityBuffer.addEventListener('focus', () => this._refreshAccessibilityBuffer());
this._terminal.element.insertAdjacentElement('afterbegin', this._accessiblityBuffer);


this.register(this._renderRowsDebouncer);
this.register(this._terminal.onResize(e => this._handleResize(e.rows)));
Expand All @@ -97,6 +111,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 +318,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._accessiblityBuffer.tabIndex = 0;
this._accessiblityBuffer.ariaRoleDescription = 'document';
this._accessiblityBuffer.replaceChildren(...bufferElements);
this._accessiblityBuffer.scrollTop = this._accessiblityBuffer.scrollHeight;
this._accessiblityBuffer.focus();
}

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

private _handleFontOptionChange(options: Required<ITerminalOptions>): void {
this._accessiblityBuffer.style.fontFamily = options.fontFamily;
this._accessiblityBuffer.style.fontSize = `${options.fontSize}px`;
}
}
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 accessibilityBuffer = 'Accessibility buffer';
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
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
5 changes: 5 additions & 0 deletions typings/xterm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,11 @@ declare module 'xterm' {
* being printed to the terminal when `screenReaderMode` is enabled.
*/
tooMuchOutput: string;

/**
* The aria label for the accessibility buffer
*/
accessibilityBuffer: string;
}

/**
Expand Down