<commit_before>/**
 * Copyright (c) 2017 The xterm.js authors. All rights reserved.
 * @license MIT
 */

import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './utils/RenderDebouncer';
import { addDisposableListener } from './utils/Dom';
import { IDisposable } from 'xterm';

const MAX_ROWS_TO_READ = 20;
const ACTIVE_ITEM_ID_PREFIX = 'xterm-active-item-';

enum BoundaryPosition {
  Top,
  Bottom
}

export class AccessibilityManager implements IDisposable {
  private _accessibilityTreeRoot: HTMLElement;
  private _rowContainer: HTMLElement;
  private _rowElements: HTMLElement[] = [];
  private _liveRegion: HTMLElement;
  private _moreRowsElement: HTMLElement;
  private _liveRegionLineCount: number = 0;

  private _renderRowsDebouncer: RenderDebouncer;
  // private _navigationMode: NavigationMode;

  private _topBoundaryFocusListener: (e: FocusEvent) => void;
  private _bottomBoundaryFocusListener: (e: FocusEvent) => void;

  private _disposables: IDisposable[] = [];

  /**
   * This queue has a character pushed to it for keys that are pressed, if the
   * next character added to the terminal is equal to the key char then it is
   * not announced (added to live region) because it has already been announced
   * by the textarea event (which cannot be canceled). There are some race
   * condition cases if there is typing while data is streaming, but this covers
   * the main case of typing into the prompt and inputting the answer to a
   * question (Y/N, etc.).
   */
  private _charsToConsume: string[] = [];

  constructor(private _terminal: ITerminal) {
    this._accessibilityTreeRoot = document.createElement('div');
    this._accessibilityTreeRoot.classList.add('xterm-accessibility');

    this._moreRowsElement = document.createElement('div');
    this._moreRowsElement.classList.add('xterm-message');
    this._moreRowsElement.style.clip = 'clip(0 0 0 0)';
    this._moreRowsElement.textContent = Strings.navigationModeMoreRows;
    this._accessibilityTreeRoot.appendChild(this._moreRowsElement);

    this._rowContainer = document.createElement('div');
    this._rowContainer.classList.add('xterm-accessibility-tree');
    for (let i = 0; i < this._terminal.rows; i++) {
      this._rowElements[i] = this._createAccessibilityTreeNode();
      this._rowContainer.appendChild(this._rowElements[i]);
    }

    this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Top);
    this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Bottom);
    this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
    this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);

    this._refreshRowsDimensions();
    this._accessibilityTreeRoot.appendChild(this._rowContainer);

    this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
    this._refreshRows();

    // this._navigationMode = new NavigationMode(this._terminal, this._rowContainer, this._rowElements, this);

    this._liveRegion = document.createElement('div');
    this._liveRegion.classList.add('live-region');
    this._liveRegion.setAttribute('aria-live', 'assertive');
    this._accessibilityTreeRoot.appendChild(this._liveRegion);

    this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);

    this._disposables.push(this._renderRowsDebouncer);
    // this._disposables.push(this._navigationMode);
    this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
    this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
    this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
    // Line feed is an issue as the prompt won't be read out after a command is run
    this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
    this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
    this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
    this._disposables.push(this._terminal.addDisposableListener('charsizechanged', () => this._refreshRowsDimensions()));
    this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
    this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
    // TODO: Maybe renderer should fire an event on terminal when the characters change and that
    //       should be listened to instead? That would mean that the order of events are always
    //       guarenteed
    this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
    // This shouldn't be needed on modern browsers but is present in case the
    // media query that drives the dprchange event isn't supported
    this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
  }

  public dispose(): void {
    this._terminal.element.removeChild(this._accessibilityTreeRoot);
    this._disposables.forEach(d => d.dispose());
    this._disposables = null;
    this._accessibilityTreeRoot = null;
    this._rowContainer = null;
    this._liveRegion = null;
    this._rowContainer = null;
    this._rowElements = null;
  }

  private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
    const boundaryElement = <HTMLElement>e.target;
    const beforeBoundaryElement = <HTMLElement>this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2];

    // Don't scroll if the buffer top has been reached
    const posInSet = this._rowElements[0].getAttribute('aria-posinset');
    if (posInSet === '1') {
      return;
    }
    console.log('posInSet', posInSet);

    // Don't scroll when the last focused item was not the second row (focus is going the other
    // direction)
    console.log('related', <HTMLElement>e.relatedTarget);
    if (<HTMLElement>e.relatedTarget !== beforeBoundaryElement) {
      console.log('cancel');
      return;
    }

    boundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
    let oldLastElement: HTMLElement;
    // TODO: oldLastElement.removeEventListener(...)

    if (position === BoundaryPosition.Top) {
      oldLastElement = this._rowElements.pop();
      this._rowElements.unshift(this._createAccessibilityTreeNode());
      this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
      this._rowContainer.insertAdjacentElement('afterbegin', this._rowElements[0]);
    } else {
      oldLastElement = this._rowElements.shift();
      this._rowElements.push(this._createAccessibilityTreeNode());
      this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._topBoundaryFocusListener);
      this._rowContainer.appendChild(this._rowElements[this._rowElements.length - 1]);
    }
    this._rowContainer.removeChild(oldLastElement);




    // TODO: Add bottom boundary listeners and remove in both cases




    // Scroll up
    this._terminal.scrollLines(position === BoundaryPosition.Top ? -1 : 1);

    // TODO: Only refresh single
    this._refreshRowsDimensions();

    // Focus the new active element
    // this._rowContainer.setAttribute('aria-activedescendant', this._activeItemId);
    // this._focusedElement = this._rowElements[1];
    // this._focusedElement.id = this._activeItemId;

    // Focus new boundary before element
    this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2].focus();

    // Prevent the standard behavior
    e.preventDefault();
    e.stopImmediatePropagation();
  }

  public get isNavigationModeActive(): boolean {
    // TODO: Remove this function
    return true;
    // return this._navigationMode.isActive;
  }

  public enterNavigationMode(): void {
    // this._navigationMode.enter();

    // this._isNavigationModeActive = true;
    this.announce('Entered line navigation mode');<commit_msg>Get navigation right/down working with nav mode<commit_after>/**
 * Copyright (c) 2017 The xterm.js authors. All rights reserved.
 * @license MIT
 */

import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './utils/RenderDebouncer';
import { addDisposableListener } from './utils/Dom';
import { IDisposable } from 'xterm';

const MAX_ROWS_TO_READ = 20;
const ACTIVE_ITEM_ID_PREFIX = 'xterm-active-item-';

enum BoundaryPosition {
  Top,
  Bottom
}

export class AccessibilityManager implements IDisposable {
  private _accessibilityTreeRoot: HTMLElement;
  private _rowContainer: HTMLElement;
  private _rowElements: HTMLElement[] = [];
  private _liveRegion: HTMLElement;
  private _moreRowsElement: HTMLElement;
  private _liveRegionLineCount: number = 0;

  private _renderRowsDebouncer: RenderDebouncer;
  // private _navigationMode: NavigationMode;

  private _topBoundaryFocusListener: (e: FocusEvent) => void;
  private _bottomBoundaryFocusListener: (e: FocusEvent) => void;

  private _disposables: IDisposable[] = [];

  /**
   * This queue has a character pushed to it for keys that are pressed, if the
   * next character added to the terminal is equal to the key char then it is
   * not announced (added to live region) because it has already been announced
   * by the textarea event (which cannot be canceled). There are some race
   * condition cases if there is typing while data is streaming, but this covers
   * the main case of typing into the prompt and inputting the answer to a
   * question (Y/N, etc.).
   */
  private _charsToConsume: string[] = [];

  constructor(private _terminal: ITerminal) {
    this._accessibilityTreeRoot = document.createElement('div');
    this._accessibilityTreeRoot.classList.add('xterm-accessibility');

    this._moreRowsElement = document.createElement('div');
    this._moreRowsElement.classList.add('xterm-message');
    this._moreRowsElement.style.clip = 'clip(0 0 0 0)';
    this._moreRowsElement.textContent = Strings.navigationModeMoreRows;
    this._accessibilityTreeRoot.appendChild(this._moreRowsElement);

    this._rowContainer = document.createElement('div');
    this._rowContainer.classList.add('xterm-accessibility-tree');
    for (let i = 0; i < this._terminal.rows; i++) {
      this._rowElements[i] = this._createAccessibilityTreeNode();
      this._rowContainer.appendChild(this._rowElements[i]);
    }

    this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Top);
    this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Bottom);
    this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
    this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);

    this._refreshRowsDimensions();
    this._accessibilityTreeRoot.appendChild(this._rowContainer);

    this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
    this._refreshRows();

    this._liveRegion = document.createElement('div');
    this._liveRegion.classList.add('live-region');
    this._liveRegion.setAttribute('aria-live', 'assertive');
    this._accessibilityTreeRoot.appendChild(this._liveRegion);

    this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);

    this._disposables.push(this._renderRowsDebouncer);
    this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
    this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
    this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
    // Line feed is an issue as the prompt won't be read out after a command is run
    this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
    this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
    this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
    this._disposables.push(this._terminal.addDisposableListener('charsizechanged', () => this._refreshRowsDimensions()));
    this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
    this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
    // TODO: Maybe renderer should fire an event on terminal when the characters change and that
    //       should be listened to instead? That would mean that the order of events are always
    //       guarenteed
    this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
    // This shouldn't be needed on modern browsers but is present in case the
    // media query that drives the dprchange event isn't supported
    this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
  }

  public dispose(): void {
    this._terminal.element.removeChild(this._accessibilityTreeRoot);
    this._disposables.forEach(d => d.dispose());
    this._disposables = null;
    this._accessibilityTreeRoot = null;
    this._rowContainer = null;
    this._liveRegion = null;
    this._rowContainer = null;
    this._rowElements = null;
  }

  private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
    const boundaryElement = <HTMLElement>e.target;
    const beforeBoundaryElement = <HTMLElement>this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2];

    // Don't scroll if the buffer top has reached the end in that direction
    const posInSet = boundaryElement.getAttribute('aria-posinset');
    const lastRowPos = position === BoundaryPosition.Top ? '1' : `${this._terminal.buffer.lines.length}`;
    if (posInSet === lastRowPos) {
      return;
    }

    // Don't scroll when the last focused item was not the second row (focus is going the other
    // direction)
    if (<HTMLElement>e.relatedTarget !== beforeBoundaryElement) {
      console.log('cancel');
      return;
    }

    // TODO: Refactor to reduce duplication, define top and bottom boundary elements
    let otherBoundaryElement: HTMLElement;
    if (position === BoundaryPosition.Top) {
      // Remove old other boundary element from array
      otherBoundaryElement = this._rowElements.pop();

      // Remove listeners from old boundary elements
      boundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
      otherBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);

      // Add new element to array/DOM
      this._rowElements.unshift(this._createAccessibilityTreeNode());
      this._rowContainer.insertAdjacentElement('afterbegin', this._rowElements[0]);

      // Add listeners to new boundary elements
      this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
      this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
    } else {
      // Remove old other boundary element from array
      otherBoundaryElement = this._rowElements.shift();

      // Remove listeners from old boundary elements
      otherBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
      boundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);

      // Add new element to array/DOM
      this._rowElements.push(this._createAccessibilityTreeNode());
      this._rowContainer.appendChild(this._rowElements[this._rowElements.length - 1]);

      // Add listeners to new boundary elements
      this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
      this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
    }
    this._rowContainer.removeChild(otherBoundaryElement);

    // Scroll up
    this._terminal.scrollLines(position === BoundaryPosition.Top ? -1 : 1);

    // TODO: Only refresh single
    this._refreshRowsDimensions();

    // Focus new boundary before element
    this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2].focus();

    // Prevent the standard behavior
    e.preventDefault();
    e.stopImmediatePropagation();
  }

  public get isNavigationModeActive(): boolean {
    // TODO: Remove this function
    return true;
    // return this._navigationMode.isActive;
  }

  public enterNavigationMode(): void {
    // this._navigationMode.enter();

    // this._isNavigationModeActive = true;
    this.announce('Entered line navigation mode');
{'fraction_non_alphanumeric': 0.114666356390715, 'fraction_numerical': 0.003606957938216301, 'mean_word_length': 4.376915858617454, 'pattern_counts': {'":': 0, '<': 12, '<?xml version=': 0, '>': 42, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1434297', 'n_tokens_mistral': 4884, 'n_tokens_neox': 4693, 'n_words': 1091}