@@ -10,6 +10,7 @@ import { Disposable, toDisposable } from 'common/Lifecycle';
10
10
import { ICoreBrowserService , IRenderService } from 'browser/services/Services' ;
11
11
import { IBuffer } from 'common/buffer/Types' ;
12
12
import { IInstantiationService } from 'common/services/Services' ;
13
+ import { addDisposableDomListener } from 'browser/Lifecycle' ;
13
14
14
15
const MAX_ROWS_TO_READ = 20 ;
15
16
@@ -18,11 +19,17 @@ const enum BoundaryPosition {
18
19
BOTTOM
19
20
}
20
21
22
+ // Turn this on to unhide the accessibility tree and display it under
23
+ // (instead of overlapping with) the terminal.
24
+ const DEBUG = false ;
25
+
21
26
export class AccessibilityManager extends Disposable {
27
+ private _debugRootContainer : HTMLElement | undefined ;
22
28
private _accessibilityContainer : HTMLElement ;
23
29
24
30
private _rowContainer : HTMLElement ;
25
31
private _rowElements : HTMLElement [ ] ;
32
+ private _rowColumns : WeakMap < HTMLElement , number [ ] > = new WeakMap ( ) ;
26
33
27
34
private _liveRegion : HTMLElement ;
28
35
private _liveRegionLineCount : number = 0 ;
@@ -80,7 +87,23 @@ export class AccessibilityManager extends Disposable {
80
87
if ( ! this . _terminal . element ) {
81
88
throw new Error ( 'Cannot enable accessibility before Terminal.open' ) ;
82
89
}
83
- this . _terminal . element . insertAdjacentElement ( 'afterbegin' , this . _accessibilityContainer ) ;
90
+
91
+ if ( DEBUG ) {
92
+ this . _accessibilityContainer . classList . add ( 'debug' ) ;
93
+ this . _rowContainer . classList . add ( 'debug' ) ;
94
+
95
+ // Use a `<div class="xterm">` container so that the css will still apply.
96
+ this . _debugRootContainer = document . createElement ( 'div' ) ;
97
+ this . _debugRootContainer . classList . add ( 'xterm' ) ;
98
+
99
+ this . _debugRootContainer . appendChild ( document . createTextNode ( '------start a11y------' ) ) ;
100
+ this . _debugRootContainer . appendChild ( this . _accessibilityContainer ) ;
101
+ this . _debugRootContainer . appendChild ( document . createTextNode ( '------end a11y------' ) ) ;
102
+
103
+ this . _terminal . element . insertAdjacentElement ( 'afterend' , this . _debugRootContainer ) ;
104
+ } else {
105
+ this . _terminal . element . insertAdjacentElement ( 'afterbegin' , this . _accessibilityContainer ) ;
106
+ }
84
107
85
108
this . register ( this . _terminal . onResize ( e => this . _handleResize ( e . rows ) ) ) ;
86
109
this . register ( this . _terminal . onRender ( e => this . _refreshRows ( e . start , e . end ) ) ) ;
@@ -92,11 +115,16 @@ export class AccessibilityManager extends Disposable {
92
115
this . register ( this . _terminal . onKey ( e => this . _handleKey ( e . key ) ) ) ;
93
116
this . register ( this . _terminal . onBlur ( ( ) => this . _clearLiveRegion ( ) ) ) ;
94
117
this . register ( this . _renderService . onDimensionsChange ( ( ) => this . _refreshRowsDimensions ( ) ) ) ;
118
+ this . register ( addDisposableDomListener ( document , 'selectionchange' , ( ) => this . _handleSelectionChange ( ) ) ) ;
95
119
this . register ( this . _coreBrowserService . onDprChange ( ( ) => this . _refreshRowsDimensions ( ) ) ) ;
96
120
97
121
this . _refreshRows ( ) ;
98
122
this . register ( toDisposable ( ( ) => {
99
- this . _accessibilityContainer . remove ( ) ;
123
+ if ( DEBUG ) {
124
+ this . _debugRootContainer ! . remove ( ) ;
125
+ } else {
126
+ this . _accessibilityContainer . remove ( ) ;
127
+ }
100
128
this . _rowElements . length = 0 ;
101
129
} ) ) ;
102
130
}
@@ -149,14 +177,18 @@ export class AccessibilityManager extends Disposable {
149
177
const buffer : IBuffer = this . _terminal . buffer ;
150
178
const setSize = buffer . lines . length . toString ( ) ;
151
179
for ( let i = start ; i <= end ; i ++ ) {
152
- const lineData = buffer . translateBufferLineToString ( buffer . ydisp + i , true ) ;
180
+ const line = buffer . lines . get ( buffer . ydisp + i ) ;
181
+ const columns : number [ ] = [ ] ;
182
+ const lineData = line ?. translateToString ( true , undefined , undefined , columns ) || '' ;
153
183
const posInSet = ( buffer . ydisp + i + 1 ) . toString ( ) ;
154
184
const element = this . _rowElements [ i ] ;
155
185
if ( element ) {
156
186
if ( lineData . length === 0 ) {
157
187
element . innerText = '\u00a0' ;
188
+ this . _rowColumns . set ( element , [ 0 , 1 ] ) ;
158
189
} else {
159
190
element . textContent = lineData ;
191
+ this . _rowColumns . set ( element , columns ) ;
160
192
}
161
193
element . setAttribute ( 'aria-posinset' , posInSet ) ;
162
194
element . setAttribute ( 'aria-setsize' , setSize ) ;
@@ -233,6 +265,103 @@ export class AccessibilityManager extends Disposable {
233
265
e . stopImmediatePropagation ( ) ;
234
266
}
235
267
268
+ private _handleSelectionChange ( ) : void {
269
+ if ( this . _rowElements . length === 0 ) {
270
+ return ;
271
+ }
272
+
273
+ const selection = document . getSelection ( ) ;
274
+ if ( ! selection ) {
275
+ return ;
276
+ }
277
+
278
+ if ( selection . isCollapsed ) {
279
+ // Only do something when the anchorNode is inside the row container. This
280
+ // behavior mirrors what we do with mouse --- if the mouse clicks
281
+ // somewhere outside of the terminal, we don't clear the selection.
282
+ if ( this . _rowContainer . contains ( selection . anchorNode ) ) {
283
+ this . _terminal . clearSelection ( ) ;
284
+ }
285
+ return ;
286
+ }
287
+
288
+ if ( ! selection . anchorNode || ! selection . focusNode ) {
289
+ console . error ( 'anchorNode and/or focusNode are null' ) ;
290
+ return ;
291
+ }
292
+
293
+ // Sort the two selection points in document order.
294
+ let begin = { node : selection . anchorNode , offset : selection . anchorOffset } ;
295
+ let end = { node : selection . focusNode , offset : selection . focusOffset } ;
296
+ if ( ( begin . node . compareDocumentPosition ( end . node ) & Node . DOCUMENT_POSITION_PRECEDING ) || ( begin . node === end . node && begin . offset > end . offset ) ) {
297
+ [ begin , end ] = [ end , begin ] ;
298
+ }
299
+
300
+ // Clamp begin/end to the inside of the row container.
301
+ if ( begin . node . compareDocumentPosition ( this . _rowElements [ 0 ] ) & ( Node . DOCUMENT_POSITION_CONTAINED_BY | Node . DOCUMENT_POSITION_FOLLOWING ) ) {
302
+ begin = { node : this . _rowElements [ 0 ] . childNodes [ 0 ] , offset : 0 } ;
303
+ }
304
+ if ( ! this . _rowContainer . contains ( begin . node ) ) {
305
+ // This happens when `begin` is below the last row.
306
+ return ;
307
+ }
308
+ const lastRowElement = this . _rowElements . slice ( - 1 ) [ 0 ] ;
309
+ if ( end . node . compareDocumentPosition ( lastRowElement ) & ( Node . DOCUMENT_POSITION_CONTAINED_BY | Node . DOCUMENT_POSITION_PRECEDING ) ) {
310
+ end = {
311
+ node : lastRowElement ,
312
+ offset : lastRowElement . textContent ?. length ?? 0
313
+ } ;
314
+ }
315
+ if ( ! this . _rowContainer . contains ( end . node ) ) {
316
+ // This happens when `end` is above the first row.
317
+ return ;
318
+ }
319
+
320
+ const toRowColumn = ( { node, offset } : typeof begin ) : { row : number , column : number } | null => {
321
+ // `node` is either the row element or the Text node inside it.
322
+ const rowElement : any = node instanceof Text ? node . parentNode : node ;
323
+ let row = parseInt ( rowElement ?. getAttribute ( 'aria-posinset' ) , 10 ) - 1 ;
324
+ if ( isNaN ( row ) ) {
325
+ console . warn ( 'row is invalid. Race condition?' ) ;
326
+ return null ;
327
+ }
328
+
329
+ const columns = this . _rowColumns . get ( rowElement ) ;
330
+ if ( ! columns ) {
331
+ console . warn ( 'columns is null. Race condition?' ) ;
332
+ return null ;
333
+ }
334
+
335
+ let column = offset < columns . length ? columns [ offset ] : columns . slice ( - 1 ) [ 0 ] + 1 ;
336
+ if ( column >= this . _terminal . cols ) {
337
+ ++ row ;
338
+ column = 0 ;
339
+ }
340
+ return {
341
+ row,
342
+ column
343
+ } ;
344
+ } ;
345
+
346
+ const beginRowColumn = toRowColumn ( begin ) ;
347
+ const endRowColumn = toRowColumn ( end ) ;
348
+
349
+ if ( ! beginRowColumn || ! endRowColumn ) {
350
+ return ;
351
+ }
352
+
353
+ if ( beginRowColumn . row > endRowColumn . row || ( beginRowColumn . row === endRowColumn . row && beginRowColumn . column >= endRowColumn . column ) ) {
354
+ // This should not happen unless we have some bugs.
355
+ throw new Error ( 'invalid range' ) ;
356
+ }
357
+
358
+ this . _terminal . select (
359
+ beginRowColumn . column ,
360
+ beginRowColumn . row ,
361
+ ( endRowColumn . row - beginRowColumn . row ) * this . _terminal . cols - beginRowColumn . column + endRowColumn . column
362
+ ) ;
363
+ }
364
+
236
365
private _handleResize ( rows : number ) : void {
237
366
// Remove bottom boundary listener
238
367
this . _rowElements [ this . _rowElements . length - 1 ] . removeEventListener ( 'focus' , this . _bottomBoundaryFocusListener ) ;
0 commit comments