Skip to content

Commit a3024a9

Browse files
authored
Merge pull request xtermjs#4879 from JasonXJ/a11y-char-width2
Align character position in a11y tree with the actual rendering
2 parents c73793f + 12e5841 commit a3024a9

File tree

2 files changed

+44
-10
lines changed

2 files changed

+44
-10
lines changed

css/xterm.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,16 @@
157157
}
158158

159159
.xterm .xterm-accessibility-tree {
160+
font-family: monospace;
160161
user-select: text;
161162
white-space: pre;
162163
}
163164

165+
.xterm .xterm-accessibility-tree > div {
166+
transform-origin: left;
167+
width: fit-content;
168+
}
169+
164170
.xterm .live-region {
165171
position: absolute;
166172
left: -9999px;

src/browser/AccessibilityManager.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,11 @@ export class AccessibilityManager extends Disposable {
5858
@IRenderService private readonly _renderService: IRenderService
5959
) {
6060
super();
61-
this._accessibilityContainer = this._coreBrowserService.mainDocument.createElement('div');
61+
const doc = this._coreBrowserService.mainDocument;
62+
this._accessibilityContainer = doc.createElement('div');
6263
this._accessibilityContainer.classList.add('xterm-accessibility');
6364

64-
this._rowContainer = this._coreBrowserService.mainDocument.createElement('div');
65+
this._rowContainer = doc.createElement('div');
6566
this._rowContainer.setAttribute('role', 'list');
6667
this._rowContainer.classList.add('xterm-accessibility-tree');
6768
this._rowElements = [];
@@ -75,10 +76,9 @@ export class AccessibilityManager extends Disposable {
7576
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
7677
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
7778

78-
this._refreshRowsDimensions();
7979
this._accessibilityContainer.appendChild(this._rowContainer);
8080

81-
this._liveRegion = this._coreBrowserService.mainDocument.createElement('div');
81+
this._liveRegion = doc.createElement('div');
8282
this._liveRegion.classList.add('live-region');
8383
this._liveRegion.setAttribute('aria-live', 'assertive');
8484
this._accessibilityContainer.appendChild(this._liveRegion);
@@ -93,12 +93,12 @@ export class AccessibilityManager extends Disposable {
9393
this._rowContainer.classList.add('debug');
9494

9595
// Use a `<div class="xterm">` container so that the css will still apply.
96-
this._debugRootContainer = document.createElement('div');
96+
this._debugRootContainer = doc.createElement('div');
9797
this._debugRootContainer.classList.add('xterm');
9898

99-
this._debugRootContainer.appendChild(document.createTextNode('------start a11y------'));
99+
this._debugRootContainer.appendChild(doc.createTextNode('------start a11y------'));
100100
this._debugRootContainer.appendChild(this._accessibilityContainer);
101-
this._debugRootContainer.appendChild(document.createTextNode('------end a11y------'));
101+
this._debugRootContainer.appendChild(doc.createTextNode('------end a11y------'));
102102

103103
this._terminal.element.insertAdjacentElement('afterend', this._debugRootContainer);
104104
} else {
@@ -115,9 +115,10 @@ export class AccessibilityManager extends Disposable {
115115
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
116116
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
117117
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
118-
this.register(addDisposableDomListener(document, 'selectionchange', () => this._handleSelectionChange()));
118+
this.register(addDisposableDomListener(doc, 'selectionchange', () => this._handleSelectionChange()));
119119
this.register(this._coreBrowserService.onDprChange(() => this._refreshRowsDimensions()));
120120

121+
this._refreshRowsDimensions();
121122
this._refreshRows();
122123
this.register(toDisposable(() => {
123124
if (DEBUG) {
@@ -192,6 +193,7 @@ export class AccessibilityManager extends Disposable {
192193
}
193194
element.setAttribute('aria-posinset', posInSet);
194195
element.setAttribute('aria-setsize', setSize);
196+
this._alignRowWidth(element);
195197
}
196198
}
197199
this._announceCharacters();
@@ -270,7 +272,7 @@ export class AccessibilityManager extends Disposable {
270272
return;
271273
}
272274

273-
const selection = document.getSelection();
275+
const selection = this._coreBrowserService.mainDocument.getSelection();
274276
if (!selection) {
275277
return;
276278
}
@@ -389,19 +391,45 @@ export class AccessibilityManager extends Disposable {
389391
this._refreshRowDimensions(element);
390392
return element;
391393
}
394+
392395
private _refreshRowsDimensions(): void {
393396
if (!this._renderService.dimensions.css.cell.height) {
394397
return;
395398
}
396-
this._accessibilityContainer.style.width = `${this._renderService.dimensions.css.canvas.width}px`;
399+
Object.assign(this._accessibilityContainer.style, {
400+
width: `${this._renderService.dimensions.css.canvas.width}px`,
401+
fontSize: `${this._terminal.options.fontSize}px`
402+
});
397403
if (this._rowElements.length !== this._terminal.rows) {
398404
this._handleResize(this._terminal.rows);
399405
}
400406
for (let i = 0; i < this._terminal.rows; i++) {
401407
this._refreshRowDimensions(this._rowElements[i]);
408+
this._alignRowWidth(this._rowElements[i]);
402409
}
403410
}
411+
404412
private _refreshRowDimensions(element: HTMLElement): void {
405413
element.style.height = `${this._renderService.dimensions.css.cell.height}px`;
406414
}
415+
416+
/**
417+
* Scale the width of a row so that each of the character is (mostly) aligned
418+
* with the actual rendering. This will allow the screen reader to draw
419+
* selection outline at the correct position.
420+
*
421+
* On top of using the "monospace" font and correct font size, the scaling
422+
* here is necessary to handle characters that are not covered by the font
423+
* (e.g. CJK).
424+
*/
425+
private _alignRowWidth(element: HTMLElement): void {
426+
element.style.transform = '';
427+
const width = element.getBoundingClientRect().width;
428+
const lastColumn = this._rowColumns.get(element)?.slice(-1)?.[0];
429+
if (!lastColumn) {
430+
return;
431+
}
432+
const targetWidth = lastColumn * this._renderService.dimensions.css.cell.width;
433+
element.style.transform = `scaleX(${targetWidth / width})`;
434+
}
407435
}

0 commit comments

Comments
 (0)