@@ -8,16 +8,32 @@ import { ITerminal, IRenderDebouncer } from 'browser/Types';
88import { isMac } from 'common/Platform' ;
99import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer' ;
1010import { Disposable , toDisposable } from 'common/Lifecycle' ;
11+ import { ScreenDprMonitor } from 'browser/ScreenDprMonitor' ;
12+ import { IRenderService } from 'browser/services/Services' ;
13+ import { addDisposableDomListener } from 'browser/Lifecycle' ;
1114
1215const MAX_ROWS_TO_READ = 20 ;
1316
17+ const enum BoundaryPosition {
18+ TOP ,
19+ BOTTOM
20+ }
21+
1422export class AccessibilityManager extends Disposable {
1523 private _accessibilityContainer : HTMLElement ;
1624
25+ private _rowContainer : HTMLElement ;
26+ private _rowElements : HTMLElement [ ] ;
27+
1728 private _liveRegion : HTMLElement ;
1829 private _liveRegionLineCount : number = 0 ;
1930 private _liveRegionDebouncer : IRenderDebouncer ;
2031
32+ private _screenDprMonitor : ScreenDprMonitor ;
33+
34+ private _topBoundaryFocusListener : ( e : FocusEvent ) => void ;
35+ private _bottomBoundaryFocusListener : ( e : FocusEvent ) => void ;
36+
2137 /**
2238 * This queue has a character pushed to it for keys that are pressed, if the
2339 * next character added to the terminal is equal to the key char then it is
@@ -32,12 +48,30 @@ export class AccessibilityManager extends Disposable {
3248 private _charsToAnnounce : string = '' ;
3349
3450 constructor (
35- private readonly _terminal : ITerminal
51+ private readonly _terminal : ITerminal ,
52+ @IRenderService private readonly _renderService : IRenderService
3653 ) {
3754 super ( ) ;
3855 this . _accessibilityContainer = document . createElement ( 'div' ) ;
3956 this . _accessibilityContainer . classList . add ( 'xterm-accessibility' ) ;
4057
58+ this . _rowContainer = document . createElement ( 'div' ) ;
59+ this . _rowContainer . setAttribute ( 'role' , 'list' ) ;
60+ this . _rowContainer . classList . add ( 'xterm-accessibility-tree' ) ;
61+ this . _rowElements = [ ] ;
62+ for ( let i = 0 ; i < this . _terminal . rows ; i ++ ) {
63+ this . _rowElements [ i ] = this . _createAccessibilityTreeNode ( ) ;
64+ this . _rowContainer . appendChild ( this . _rowElements [ i ] ) ;
65+ }
66+
67+ this . _topBoundaryFocusListener = e => this . _handleBoundaryFocus ( e , BoundaryPosition . TOP ) ;
68+ this . _bottomBoundaryFocusListener = e => this . _handleBoundaryFocus ( e , BoundaryPosition . BOTTOM ) ;
69+ this . _rowElements [ 0 ] . addEventListener ( 'focus' , this . _topBoundaryFocusListener ) ;
70+ this . _rowElements [ this . _rowElements . length - 1 ] . addEventListener ( 'focus' , this . _bottomBoundaryFocusListener ) ;
71+
72+ this . _refreshRowsDimensions ( ) ;
73+ this . _accessibilityContainer . appendChild ( this . _rowContainer ) ;
74+
4175 this . _liveRegion = document . createElement ( 'div' ) ;
4276 this . _liveRegion . classList . add ( 'live-region' ) ;
4377 this . _liveRegion . setAttribute ( 'aria-live' , 'assertive' ) ;
@@ -50,6 +84,7 @@ export class AccessibilityManager extends Disposable {
5084 this . _terminal . element . insertAdjacentElement ( 'afterbegin' , this . _accessibilityContainer ) ;
5185
5286 this . register ( this . _liveRegionDebouncer ) ;
87+ this . register ( this . _terminal . onResize ( e => this . _handleResize ( e . rows ) ) ) ;
5388 this . register ( this . _terminal . onRender ( e => this . _refreshRows ( e . start , e . end ) ) ) ;
5489 this . register ( this . _terminal . onScroll ( ( ) => this . _refreshRows ( ) ) ) ;
5590 // Line feed is an issue as the prompt won't be read out after a command is run
@@ -58,7 +93,20 @@ export class AccessibilityManager extends Disposable {
5893 this . register ( this . _terminal . onA11yTab ( spaceCount => this . _handleTab ( spaceCount ) ) ) ;
5994 this . register ( this . _terminal . onKey ( e => this . _handleKey ( e . key ) ) ) ;
6095 this . register ( this . _terminal . onBlur ( ( ) => this . _clearLiveRegion ( ) ) ) ;
61- this . register ( toDisposable ( ( ) => this . _accessibilityContainer . remove ( ) ) ) ;
96+ this . register ( this . _renderService . onDimensionsChange ( ( ) => this . _refreshRowsDimensions ( ) ) ) ;
97+
98+ this . _screenDprMonitor = new ScreenDprMonitor ( window ) ;
99+ this . register ( this . _screenDprMonitor ) ;
100+ this . _screenDprMonitor . setListener ( ( ) => this . _refreshRowsDimensions ( ) ) ;
101+ // This shouldn't be needed on modern browsers but is present in case the
102+ // media query that drives the ScreenDprMonitor isn't supported
103+ this . register ( addDisposableDomListener ( window , 'resize' , ( ) => this . _refreshRowsDimensions ( ) ) ) ;
104+
105+ this . _refreshRows ( ) ;
106+ this . register ( toDisposable ( ( ) => {
107+ this . _accessibilityContainer . remove ( ) ;
108+ this . _rowElements . length = 0 ;
109+ } ) ) ;
62110 }
63111
64112 private _handleTab ( spaceCount : number ) : void {
@@ -126,4 +174,107 @@ export class AccessibilityManager extends Disposable {
126174 this . _liveRegion . textContent += this . _charsToAnnounce ;
127175 this . _charsToAnnounce = '' ;
128176 }
177+
178+ private _handleBoundaryFocus ( e : FocusEvent , position : BoundaryPosition ) : void {
179+ const boundaryElement = e . target as HTMLElement ;
180+ const beforeBoundaryElement = this . _rowElements [ position === BoundaryPosition . TOP ? 1 : this . _rowElements . length - 2 ] ;
181+
182+ // Don't scroll if the buffer top has reached the end in that direction
183+ const posInSet = boundaryElement . getAttribute ( 'aria-posinset' ) ;
184+ const lastRowPos = position === BoundaryPosition . TOP ? '1' : `${ this . _terminal . buffer . lines . length } ` ;
185+ if ( posInSet === lastRowPos ) {
186+ return ;
187+ }
188+
189+ // Don't scroll when the last focused item was not the second row (focus is going the other
190+ // direction)
191+ if ( e . relatedTarget !== beforeBoundaryElement ) {
192+ return ;
193+ }
194+
195+ // Remove old boundary element from array
196+ let topBoundaryElement : HTMLElement ;
197+ let bottomBoundaryElement : HTMLElement ;
198+ if ( position === BoundaryPosition . TOP ) {
199+ topBoundaryElement = boundaryElement ;
200+ bottomBoundaryElement = this . _rowElements . pop ( ) ! ;
201+ this . _rowContainer . removeChild ( bottomBoundaryElement ) ;
202+ } else {
203+ topBoundaryElement = this . _rowElements . shift ( ) ! ;
204+ bottomBoundaryElement = boundaryElement ;
205+ this . _rowContainer . removeChild ( topBoundaryElement ) ;
206+ }
207+
208+ // Remove listeners from old boundary elements
209+ topBoundaryElement . removeEventListener ( 'focus' , this . _topBoundaryFocusListener ) ;
210+ bottomBoundaryElement . removeEventListener ( 'focus' , this . _bottomBoundaryFocusListener ) ;
211+
212+ // Add new element to array/DOM
213+ if ( position === BoundaryPosition . TOP ) {
214+ const newElement = this . _createAccessibilityTreeNode ( ) ;
215+ this . _rowElements . unshift ( newElement ) ;
216+ this . _rowContainer . insertAdjacentElement ( 'afterbegin' , newElement ) ;
217+ } else {
218+ const newElement = this . _createAccessibilityTreeNode ( ) ;
219+ this . _rowElements . push ( newElement ) ;
220+ this . _rowContainer . appendChild ( newElement ) ;
221+ }
222+
223+ // Add listeners to new boundary elements
224+ this . _rowElements [ 0 ] . addEventListener ( 'focus' , this . _topBoundaryFocusListener ) ;
225+ this . _rowElements [ this . _rowElements . length - 1 ] . addEventListener ( 'focus' , this . _bottomBoundaryFocusListener ) ;
226+
227+ // Scroll up
228+ this . _terminal . scrollLines ( position === BoundaryPosition . TOP ? - 1 : 1 ) ;
229+
230+ // Focus new boundary before element
231+ this . _rowElements [ position === BoundaryPosition . TOP ? 1 : this . _rowElements . length - 2 ] . focus ( ) ;
232+
233+ // Prevent the standard behavior
234+ e . preventDefault ( ) ;
235+ e . stopImmediatePropagation ( ) ;
236+ }
237+
238+ private _handleResize ( rows : number ) : void {
239+ // Remove bottom boundary listener
240+ this . _rowElements [ this . _rowElements . length - 1 ] . removeEventListener ( 'focus' , this . _bottomBoundaryFocusListener ) ;
241+
242+ // Grow rows as required
243+ for ( let i = this . _rowContainer . children . length ; i < this . _terminal . rows ; i ++ ) {
244+ this . _rowElements [ i ] = this . _createAccessibilityTreeNode ( ) ;
245+ this . _rowContainer . appendChild ( this . _rowElements [ i ] ) ;
246+ }
247+ // Shrink rows as required
248+ while ( this . _rowElements . length > rows ) {
249+ this . _rowContainer . removeChild ( this . _rowElements . pop ( ) ! ) ;
250+ }
251+
252+ // Add bottom boundary listener
253+ this . _rowElements [ this . _rowElements . length - 1 ] . addEventListener ( 'focus' , this . _bottomBoundaryFocusListener ) ;
254+
255+ this . _refreshRowsDimensions ( ) ;
256+ }
257+
258+ private _createAccessibilityTreeNode ( ) : HTMLElement {
259+ const element = document . createElement ( 'div' ) ;
260+ element . setAttribute ( 'role' , 'listitem' ) ;
261+ element . tabIndex = - 1 ;
262+ this . _refreshRowDimensions ( element ) ;
263+ return element ;
264+ }
265+ private _refreshRowsDimensions ( ) : void {
266+ if ( ! this . _renderService . dimensions . css . cell . height ) {
267+ return ;
268+ }
269+ this . _accessibilityContainer . style . width = `${ this . _renderService . dimensions . css . canvas . width } px` ;
270+ if ( this . _rowElements . length !== this . _terminal . rows ) {
271+ this . _handleResize ( this . _terminal . rows ) ;
272+ }
273+ for ( let i = 0 ; i < this . _terminal . rows ; i ++ ) {
274+ this . _refreshRowDimensions ( this . _rowElements [ i ] ) ;
275+ }
276+ }
277+ private _refreshRowDimensions ( element : HTMLElement ) : void {
278+ element . style . height = `${ this . _renderService . dimensions . css . cell . height } px` ;
279+ }
129280}
0 commit comments