diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index 9b790abf..47839a70 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -8,8 +8,13 @@ import { isMac } from './utils/Browser'; import { RenderDebouncer } from './utils/RenderDebouncer'; const MAX_ROWS_TO_READ = 20; +const ACTIVE_ITEM_ID_PREFIX = 'xterm-active-item-'; export class AccessibilityManager implements IDisposable { + private _activeItemId: string; + private _isNavigationModeActive: boolean = false; + private _navigationModeFocusedRow: number; + private _accessibilityTreeRoot: HTMLElement; private _rowContainer: HTMLElement; private _rowElements: HTMLElement[] = []; @@ -32,12 +37,13 @@ export class AccessibilityManager implements IDisposable { private _charsToConsume: string[] = []; constructor(private _terminal: ITerminal) { + this._activeItemId = ACTIVE_ITEM_ID_PREFIX + Math.floor((Math.random() * 100000)); this._accessibilityTreeRoot = document.createElement('div'); - this._accessibilityTreeRoot.classList.add('accessibility'); + this._accessibilityTreeRoot.classList.add('xterm-accessibility'); this._rowContainer = document.createElement('div'); - this._rowContainer.classList.add('accessibility-tree'); + this._rowContainer.classList.add('xterm-accessibility-tree'); for (let i = 0; i < this._terminal.rows; i++) { - this._rowElements[i] = document.createElement('div'); + this._rowElements[i] = this._createAccessibilityTreeNode(); this._rowContainer.appendChild(this._rowElements[i]); } this._refreshRowsDimensions(); @@ -73,6 +79,41 @@ export class AccessibilityManager implements IDisposable { // TODO: Dispose of this listener when disposed // TODO: Listen instead to when devicePixelRatio changed (depends on PR #1172) window.addEventListener('resize', () => this._refreshRowsDimensions()); + + this._rowContainer.addEventListener('keyup', e => { + switch (e.keyCode) { + case 27: // Escape + this.leaveNavigationMode(); + break; + // TODO: Jump up/down to next non-blank row + case 38: /*ArrowUp*/ + this._navigateToElement(this._navigationModeFocusedRow - 1); + break; + case 40: /*ArrowDown*/ + this._navigateToElement(this._navigationModeFocusedRow + 1); + break; + } + this._rowContainer.focus(); + console.log('keydown2', e); + e.preventDefault(); + e.stopPropagation(); + return true; + + // no handler + //return false; + }); + this._rowContainer.addEventListener('keydown', e => { + if (this._isNavigationModeActive) { + e.preventDefault(); + e.stopPropagation(); + return true; + } + return false; + }); + } + + public get isNavigationModeActive(): boolean { + return this._isNavigationModeActive; } private _addTerminalEventListener(type: string, listener: (...args: any[]) => any): void { @@ -99,7 +140,7 @@ export class AccessibilityManager implements IDisposable { private _onResize(cols: number, rows: number): void { // Grow rows as required for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) { - this._rowElements[i] = document.createElement('div'); + this._rowElements[i] = this._createAccessibilityTreeNode(); this._rowContainer.appendChild(this._rowElements[i]); } // Shrink rows as required @@ -110,6 +151,12 @@ export class AccessibilityManager implements IDisposable { this._refreshRowsDimensions(); } + private _createAccessibilityTreeNode(): HTMLElement { + const element = document.createElement('div'); + element.setAttribute('role', 'menuitem'); + return element; + } + private _onChar(char: string): void { if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) { // \n needs to be printed as a space, otherwise it will be collapsed to @@ -179,6 +226,7 @@ export class AccessibilityManager implements IDisposable { for (let i = start; i <= end; i++) { const lineData = buffer.translateBufferLineToString(buffer.ybase + i, true); this._rowElements[i].textContent = lineData; + this._rowElements[i].setAttribute('aria-label', lineData); } } @@ -189,4 +237,39 @@ export class AccessibilityManager implements IDisposable { this._rowElements[i].style.height = `${dimensions.actualCellHeight}px`; } } + + public enterNavigationMode(): void { + this._isNavigationModeActive = true; + this._clearLiveRegion(); + this._liveRegion.textContent += 'Entered line navigation mode'; + this._rowContainer.tabIndex = 0; + this._rowContainer.setAttribute('role', 'menu'); + this._rowContainer.setAttribute('aria-activedescendant', this._activeItemId); + this._rowContainer.focus(); + this._navigateToElement(this._terminal.buffer.y); + } + + public leaveNavigationMode(): void { + this._isNavigationModeActive = false; + this._liveRegion.textContent += 'Left line navigation mode'; + this._rowContainer.removeAttribute('tabindex'); + this._rowContainer.removeAttribute('aria-activedescendant'); + this._rowContainer.removeAttribute('role'); + const selected = document.querySelector('#' + this._activeItemId); + if (selected) { + selected.removeAttribute('id'); + } + this._terminal.textarea.focus(); + } + + private _navigateToElement(row: number): void { + // TODO: Store this state + const selected = document.querySelector('#' + this._activeItemId); + if (selected) { + selected.removeAttribute('id'); + } + this._navigationModeFocusedRow = row; + const selectedElement = this._rowElements[row]; + selectedElement.id = this._activeItemId; + } } diff --git a/src/Terminal.ts b/src/Terminal.ts index 9e338c62..5fbd2b97 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -454,6 +454,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Binds the desired focus behavior on a given terminal object. */ private _onTextAreaFocus(): void { + console.log('textarea.focus'); if (this.sendFocus) { this.send(C0.ESC + '[I'); } @@ -1374,6 +1375,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {KeyboardEvent} ev The keydown event to be handled. */ protected _keyDown(ev: KeyboardEvent): boolean { + console.log('a'); + if (this._accessibilityManager && this._accessibilityManager.isNavigationModeActive) { + console.log('b'); + return; + } + if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { return false; } diff --git a/src/xterm.css b/src/xterm.css index 68d0fd7d..583aa907 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -124,7 +124,7 @@ cursor: text; } -.xterm .accessibility { +.xterm .xterm-accessibility { position: absolute; left: 0; top: 0; @@ -133,10 +133,14 @@ z-index: 100; } -.xterm .accessibility-tree { +.xterm .xterm-accessibility-tree { color: transparent; } +.xterm .xterm-accessibility-tree:focus [id^="xterm-active-item-"] { + outline: 1px solid #F80; +} + .xterm .live-region { position: absolute; left: -9999px;