Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
68 changes: 4 additions & 64 deletions src/browser/AccessibilityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,16 @@
*/

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

const MAX_ROWS_TO_READ = 20;

export class AccessibilityManager extends Disposable {
private _accessibilityContainer: HTMLElement;
private _accessiblityBuffer: HTMLElement;

private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;
Expand All @@ -41,9 +37,7 @@ export class AccessibilityManager extends Disposable {

constructor(
private readonly _terminal: ITerminal,
@IOptionsService optionsService: IOptionsService,
@IRenderService private readonly _renderService: IRenderService,
@IThemeService themeService: IThemeService
@IRenderService private readonly _renderService: IRenderService
) {
super();
this._accessibilityContainer = document.createElement('div');
Expand All @@ -60,23 +54,6 @@ export class AccessibilityManager extends Disposable {
}
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityContainer);

this._accessiblityBuffer = document.createElement('div');
this._accessiblityBuffer.setAttribute('role', 'document');
this._accessiblityBuffer.ariaRoleDescription = Strings.accessibilityBuffer;
this._accessiblityBuffer.tabIndex = 0;
this._accessibilityContainer.appendChild(this._accessiblityBuffer);
this._accessiblityBuffer.classList.add('xterm-accessibility-buffer');
this.register(addDisposableDomListener(this._accessiblityBuffer, 'keydown', (ev: KeyboardEvent) => {
if (ev.key === 'Tab') {
this._isAccessibilityBufferActive = false;
}}
));
this.register(addDisposableDomListener(this._accessiblityBuffer, 'focus',() => this._refreshAccessibilityBuffer()));
this.register(addDisposableDomListener(this._accessiblityBuffer, 'focusout',() => {
this._isAccessibilityBufferActive = false;
}));


this.register(this._liveRegionDebouncer);
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
this.register(this._terminal.onScroll(() => this._refreshRows()));
Expand All @@ -86,16 +63,7 @@ export class AccessibilityManager extends Disposable {
this.register(this._terminal.onA11yTab(spaceCount => this._handleTab(spaceCount)));
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));

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

this.register(toDisposable(() => {
this._accessiblityBuffer.remove();
this._accessibilityContainer.remove();
}));
this.register(toDisposable(() => this._accessibilityContainer.remove()));
}

private _handleTab(spaceCount: number): void {
Expand Down Expand Up @@ -167,32 +135,4 @@ export class AccessibilityManager extends Disposable {
this._liveRegion.textContent += this._charsToAnnounce;
this._charsToAnnounce = '';
}


private _refreshAccessibilityBuffer(): void {
if (!this._terminal.viewport) {
return;
}
this._isAccessibilityBufferActive = true;
const { bufferElements } = this._terminal.viewport.getBufferElements(0);
for (const element of bufferElements) {
if (element.textContent) {
element.textContent = element.textContent.replace(new RegExp(' ', 'g'), '\xA0');
}
}
this._accessiblityBuffer.replaceChildren(...bufferElements);
this._accessiblityBuffer.scrollTop = this._accessiblityBuffer.scrollHeight;
}

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`;
this._accessiblityBuffer.style.lineHeight = `${options.lineHeight * this._renderService.dimensions.css.cell.height}px`;
this._accessiblityBuffer.style.letterSpacing = `${options.letterSpacing}px`;
}
}
80 changes: 80 additions & 0 deletions src/browser/AccessibleBuffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/

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

export class AccessibleBuffer extends Disposable {
private _accessiblityBuffer: HTMLElement;
private _isAccessibilityBufferActive: boolean = false;
public get isAccessibilityBufferActive(): boolean { return this._isAccessibilityBufferActive; }

constructor(
private readonly _terminal: ITerminal,
@IOptionsService optionsService: IOptionsService,
@IRenderService private readonly _renderService: IRenderService,
@IThemeService themeService: IThemeService
) {
super();
if (!this._terminal.element) {
throw new Error('Cannot enable accessibility before Terminal.open');
}

this._accessiblityBuffer = document.createElement('div');
this._accessiblityBuffer.setAttribute('role', 'document');
this._accessiblityBuffer.ariaRoleDescription = Strings.accessibilityBuffer;
this._accessiblityBuffer.tabIndex = 0;
this._accessiblityBuffer.classList.add('xterm-accessibility-buffer');
this._terminal.element.insertAdjacentElement('afterbegin', this._accessiblityBuffer);

this.register(addDisposableDomListener(this._accessiblityBuffer, 'keydown', (ev: KeyboardEvent) => {
if (ev.key === 'Tab') {
this._isAccessibilityBufferActive = false;
}}
));
this.register(addDisposableDomListener(this._accessiblityBuffer, 'focus',() => this._refreshAccessibilityBuffer()));
this.register(addDisposableDomListener(this._accessiblityBuffer, 'focusout',() => this._isAccessibilityBufferActive = false));

this._handleColorChange(themeService.colors);
this.register(themeService.onChangeColors(e => this._handleColorChange(e)));
this._handleFontOptionChange(optionsService.options);
this.register(optionsService.onMultipleOptionChange(['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'], () => this._handleFontOptionChange(optionsService.options)));
this.register(toDisposable(() => this._accessiblityBuffer.remove()));
}

private _refreshAccessibilityBuffer(): void {
if (!this._terminal.viewport) {
return;
}
this._isAccessibilityBufferActive = true;
const { bufferElements } = this._terminal.viewport.getBufferElements(0);
for (const element of bufferElements) {
if (element.textContent) {
element.textContent = element.textContent.replace(new RegExp(' ', 'g'), '\xA0');
}
}
this._accessiblityBuffer.replaceChildren(...bufferElements);
this._accessiblityBuffer.scrollTop = this._accessiblityBuffer.scrollHeight;
}

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`;
this._accessiblityBuffer.style.lineHeight = `${options.lineHeight * (this._renderService.dimensions.css.cell.height)}px`;
this._accessiblityBuffer.style.letterSpacing = `${options.letterSpacing}px`;
}
}
4 changes: 4 additions & 0 deletions src/browser/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import * as Browser from 'common/Platform';
import { addDisposableDomListener } from 'browser/Lifecycle';
import * as Strings from 'browser/LocalizableStrings';
import { AccessibilityManager } from './AccessibilityManager';
import { AccessibleBuffer } from './AccessibleBuffer';
import { ITheme, IMarker, IDisposable, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm';
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types';
Expand Down Expand Up @@ -71,6 +72,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _viewportElement: HTMLElement | undefined;
private _helperContainer: HTMLElement | undefined;
private _compositionView: HTMLElement | undefined;
private _accessibleBuffer: AccessibleBuffer | undefined;

private _overviewRulerRenderer: OverviewRulerRenderer | undefined;

Expand Down Expand Up @@ -576,6 +578,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Listen for mouse events and translate
// them into terminal mouse protocols.
this.bindMouse();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to put this here so that dimensions from the render service wouldn't be 0. lmk if there's a better place

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right function, I'd put it just below the AccessibilityManagers init to be a little more organized. If you needed to move it here then something must not have been migrated to the new file correctly as it was working before when bundled into AccessibilityManager.

Unless that only worked when toggling screenReaderMode on after Terminal was opened? In that case there's probably some other event we're not listening to?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's where I put it originally and it didn't work, so seems like we're missing something

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it needs to be after this to work

    this._charSizeService.measure();

this._accessibleBuffer = this._instantiationService.createInstance(AccessibleBuffer, this);
}

private _createRenderer(): IRenderer {
Expand Down