From 26dc69446ef85a20a6db52004260029c3fb0accd Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Fri, 10 Nov 2023 21:21:50 +1100 Subject: [PATCH] Align character position in a11y tree with the actual rendering This helps screen readers to draw selection outline at the correct position. --- css/xterm.css | 6 ++++++ src/browser/AccessibilityManager.ts | 31 +++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/css/xterm.css b/css/xterm.css index e97b6439..51e9b39e 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -157,10 +157,16 @@ } .xterm .xterm-accessibility-tree { + font-family: monospace; user-select: text; white-space: pre; } +.xterm .xterm-accessibility-tree > div { + transform-origin: left; + width: fit-content; +} + .xterm .live-region { position: absolute; left: -9999px; diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index 4f550014..665968df 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -76,7 +76,6 @@ export class AccessibilityManager extends Disposable { this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener); this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); - this._refreshRowsDimensions(); this._accessibilityContainer.appendChild(this._rowContainer); this._liveRegion = doc.createElement('div'); @@ -119,6 +118,7 @@ export class AccessibilityManager extends Disposable { this.register(addDisposableDomListener(doc, 'selectionchange', () => this._handleSelectionChange())); this.register(this._coreBrowserService.onDprChange(() => this._refreshRowsDimensions())); + this._refreshRowsDimensions(); this._refreshRows(); this.register(toDisposable(() => { if (DEBUG) { @@ -193,6 +193,7 @@ export class AccessibilityManager extends Disposable { } element.setAttribute('aria-posinset', posInSet); element.setAttribute('aria-setsize', setSize); + this._alignRowWidth(element); } } this._announceCharacters(); @@ -390,19 +391,45 @@ export class AccessibilityManager extends Disposable { this._refreshRowDimensions(element); return element; } + private _refreshRowsDimensions(): void { if (!this._renderService.dimensions.css.cell.height) { return; } - this._accessibilityContainer.style.width = `${this._renderService.dimensions.css.canvas.width}px`; + Object.assign(this._accessibilityContainer.style, { + width: `${this._renderService.dimensions.css.canvas.width}px`, + fontSize: `${this._terminal.options.fontSize}px`, + }); if (this._rowElements.length !== this._terminal.rows) { this._handleResize(this._terminal.rows); } for (let i = 0; i < this._terminal.rows; i++) { this._refreshRowDimensions(this._rowElements[i]); + this._alignRowWidth(this._rowElements[i]); } } + private _refreshRowDimensions(element: HTMLElement): void { element.style.height = `${this._renderService.dimensions.css.cell.height}px`; } + + /** + * Scale the width of a row so that each of the character is (mostly) aligned + * with the actual rendering. This will allow the screen reader to draw + * selection outline at the correct position. + * + * On top of using the "monospace" font and correct font size, the scaling + * here is necessary to handle characters that are not covered by the font + * (e.g. CJK). + */ + private _alignRowWidth(element: HTMLElement): void { + element.style.transform = ''; + const width = element.getBoundingClientRect().width; + const lastColumn = this._rowColumns.get(element)?.slice(-1)?.[0]; + if (!lastColumn) { + return; + } + const targetWidth = lastColumn * this._renderService.dimensions.css.cell.width; + element.style.transform = `scaleX(${targetWidth / width})`; + } }