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 68c65ebd..a4a58187 100644
--- a/src/browser/AccessibilityManager.ts
+++ b/src/browser/AccessibilityManager.ts
@@ -58,10 +58,11 @@ export class AccessibilityManager extends Disposable {
@IRenderService private readonly _renderService: IRenderService
) {
super();
- this._accessibilityContainer = this._coreBrowserService.mainDocument.createElement('div');
+ const doc = this._coreBrowserService.mainDocument;
+ this._accessibilityContainer = doc.createElement('div');
this._accessibilityContainer.classList.add('xterm-accessibility');
- this._rowContainer = this._coreBrowserService.mainDocument.createElement('div');
+ this._rowContainer = doc.createElement('div');
this._rowContainer.setAttribute('role', 'list');
this._rowContainer.classList.add('xterm-accessibility-tree');
this._rowElements = [];
@@ -75,10 +76,9 @@ 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 = this._coreBrowserService.mainDocument.createElement('div');
+ this._liveRegion = doc.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
this._accessibilityContainer.appendChild(this._liveRegion);
@@ -93,12 +93,12 @@ export class AccessibilityManager extends Disposable {
this._rowContainer.classList.add('debug');
// Use a `
` container so that the css will still apply.
- this._debugRootContainer = document.createElement('div');
+ this._debugRootContainer = doc.createElement('div');
this._debugRootContainer.classList.add('xterm');
- this._debugRootContainer.appendChild(document.createTextNode('------start a11y------'));
+ this._debugRootContainer.appendChild(doc.createTextNode('------start a11y------'));
this._debugRootContainer.appendChild(this._accessibilityContainer);
- this._debugRootContainer.appendChild(document.createTextNode('------end a11y------'));
+ this._debugRootContainer.appendChild(doc.createTextNode('------end a11y------'));
this._terminal.element.insertAdjacentElement('afterend', this._debugRootContainer);
} else {
@@ -115,9 +115,10 @@ export class AccessibilityManager extends Disposable {
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
- this.register(addDisposableDomListener(document, 'selectionchange', () => this._handleSelectionChange()));
+ this.register(addDisposableDomListener(doc, 'selectionchange', () => this._handleSelectionChange()));
this.register(this._coreBrowserService.onDprChange(() => this._refreshRowsDimensions()));
+ this._refreshRowsDimensions();
this._refreshRows();
this.register(toDisposable(() => {
if (DEBUG) {
@@ -192,6 +193,7 @@ export class AccessibilityManager extends Disposable {
}
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
+ this._alignRowWidth(element);
}
}
this._announceCharacters();
@@ -270,7 +272,7 @@ export class AccessibilityManager extends Disposable {
return;
}
- const selection = document.getSelection();
+ const selection = this._coreBrowserService.mainDocument.getSelection();
if (!selection) {
return;
}
@@ -389,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})`;
+ }
}