diff --git a/css/xterm.css b/css/xterm.css index 85da42bc..6122fa40 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -151,6 +151,10 @@ color: transparent; } +.xterm .xterm-accessibility-buffer div { + white-space: pre-wrap; +} + .xterm .xterm-accessibility-buffer { position: absolute; left: 0; @@ -165,7 +169,7 @@ overflow-x: hidden; } -.xterm .xterm-accessibility-buffer:focus { +.xterm .xterm-accessibility-buffer:focus-within { opacity: 1; z-index: 20; } diff --git a/src/browser/AccessibleBuffer.ts b/src/browser/AccessibleBuffer.ts index 118fc0f0..b2d160d9 100644 --- a/src/browser/AccessibleBuffer.ts +++ b/src/browser/AccessibleBuffer.ts @@ -4,20 +4,19 @@ */ import * as Strings from 'browser/LocalizableStrings'; -import { ITerminal, IRenderDebouncer, ReadonlyColorSet } from 'browser/Types'; -import { isMac } from 'common/Platform'; -import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer'; +import { IBufferElementProvider, ITerminal, ReadonlyColorSet } from 'browser/Types'; 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 { IDisposable } from 'common/Types'; export class AccessibleBuffer extends Disposable { private _accessiblityBuffer: HTMLElement; private _isAccessibilityBufferActive: boolean = false; public get isAccessibilityBufferActive(): boolean { return this._isAccessibilityBufferActive; } - + private _provider: IBufferElementProvider | undefined; constructor( private readonly _terminal: ITerminal, @IOptionsService optionsService: IOptionsService, @@ -26,7 +25,7 @@ export class AccessibleBuffer extends Disposable { ) { super(); if (!this._terminal.element) { - throw new Error('Cannot enable accessibility before Terminal.open'); + throw new Error('Cannot enable accessibility buffer before Terminal.open'); } this._accessiblityBuffer = document.createElement('div'); @@ -39,10 +38,15 @@ export class AccessibleBuffer extends Disposable { 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(addDisposableDomListener(this._accessiblityBuffer, 'focus', () => this._refreshAccessibilityBuffer())); + this.register(addDisposableDomListener(this._accessiblityBuffer, 'focusout', (e) => { + if (!this._accessiblityBuffer.contains(e.element)) { + this._isAccessibilityBufferActive = false; + } + })); this._handleColorChange(themeService.colors); this.register(themeService.onChangeColors(e => this._handleColorChange(e))); @@ -51,19 +55,36 @@ export class AccessibleBuffer extends Disposable { this.register(toDisposable(() => this._accessiblityBuffer.remove())); } + public registerBufferElementProvider(bufferProvider: IBufferElementProvider): IDisposable { + if (this._provider) { + throw new Error('Buffer element provider already registered'); + } + this._provider = bufferProvider; + return { + dispose: () => { + this._provider = undefined; + } + }; + } + 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; + const bufferElements = this._provider?.provideBufferElements(); + if (!bufferElements) { + 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); + } else { + this._accessiblityBuffer.replaceChildren(bufferElements); + } } private _handleColorChange(colorSet: ReadonlyColorSet): void { diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 92f95547..fc4dce23 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange } from 'browser/Types'; +import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange, IBufferElementProvider } from 'browser/Types'; import { IRenderer } from 'browser/renderer/shared/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; @@ -193,6 +193,13 @@ export class Terminal extends CoreTerminal implements ITerminal { })); } + public registerBufferElementProvider(bufferProvider: IBufferElementProvider): IDisposable { + if (!this._accessibleBuffer) { + throw new Error ('Cannot register buffer element provider when terminal has not been opened yet'); + } + return this._accessibleBuffer.registerBufferElementProvider(bufferProvider); + } + /** * Handle color event from inputhandler for OSC 4|104 | 10|110 | 11|111 | 12|112. * An event from OSC 4|104 may contain multiple set or report requests, and multiple diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index a3464ffd..89494755 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -7,7 +7,7 @@ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; -import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet } from 'browser/Types'; +import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet, IBufferElementProvider } from 'browser/Types'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; @@ -143,6 +143,9 @@ export class MockTerminal implements ITerminal { public getBufferElements(startLine: number, endLine?: number | undefined): { bufferElements: HTMLElement[], cursorElement?: HTMLElement | undefined } { throw new Error('Method not implemented.'); } + public registerBufferElementProvider(bufferProvider: IBufferElementProvider): IDisposable { + throw new Error('Method not implemented.'); + } public bracketedPasteMode!: boolean; public renderer!: IRenderer; public linkifier2!: ILinkifier2; diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index f89b34de..43af106c 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -80,6 +80,7 @@ export interface IPublicTerminal extends IDisposable { refresh(start: number, end: number): void; clearTextureAtlas(): void; reset(): void; + registerBufferElementProvider(bufferProvider: IBufferElementProvider): IDisposable; } export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -220,3 +221,7 @@ export interface IRenderDebouncer extends IDisposable { export interface IRenderDebouncerWithCallback extends IRenderDebouncer { addRefreshCallback(callback: FrameRequestCallback): number; } + +export interface IBufferElementProvider { + provideBufferElements(): DocumentFragment; +} diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 7263d06d..43e92cf6 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, IMarker, IDisposable, ILocalizableStrings, ITerminalAddon, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, IModes, IDecorationOptions, IDecoration } from 'xterm'; +import { Terminal as ITerminalApi, IMarker, IDisposable, ILocalizableStrings, ITerminalAddon, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, IModes, IDecorationOptions, IDecoration, IBufferElementProvider } from 'xterm'; import { IBufferRange, ITerminal } from 'browser/Types'; import { Terminal as TerminalCore } from 'browser/Terminal'; import * as Strings from 'browser/LocalizableStrings'; @@ -236,6 +236,9 @@ export class Terminal implements ITerminalApi { public loadAddon(addon: ITerminalAddon): void { return this._addonManager.loadAddon(this, addon); } + public registerBufferElementProvider(bufferProvider: IBufferElementProvider): IDisposable { + return this._core.registerBufferElementProvider(bufferProvider); + } public static get strings(): ILocalizableStrings { return Strings; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 401be221..1b8e05d1 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1108,6 +1108,14 @@ declare module 'xterm' { * @param addon The addon to load. */ loadAddon(addon: ITerminalAddon): void; + + /* + * Registers a buffer element provider which will provide a custom element to + * use in the terminal's accessible buffer. Using this an xterm.js embedder + * has full control over how the buffer is represented in the DOM. + * @param bufferElementProvider The buffer element provider. + */ + registerBufferElementProvider(bufferProvider: IBufferElementProvider): IDisposable; } /** @@ -1360,6 +1368,13 @@ declare module 'xterm' { getNullCell(): IBufferCell; } + export interface IBufferElementProvider { + /** + * Provides a document fragment containing the buffer elements. + */ + provideBufferElements(): DocumentFragment; + } + /** * Represents the terminal's set of buffers. */