terminal: xterm.js announces screen content on a11y buttons clicked

Bug: b/236205389
Change-Id: Ib1272982a72832ce9c039b197df2a72062257e3f
Reviewed-on: https://chromium-review.googlesource.com/c/apps/libapps/+/3974148
Tested-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Joel Hockey <joelhockey@chromium.org>
diff --git a/terminal/js/terminal_emulator.js b/terminal/js/terminal_emulator.js
index a7875f1..fbdce6c 100644
--- a/terminal/js/terminal_emulator.js
+++ b/terminal/js/terminal_emulator.js
@@ -247,25 +247,32 @@
 right: 16px;
 `;
 
-class A11yButtons {
+// TODO: we should subscribe to the xterm.js onscroll event, and
+// disable/enable the buttons accordingly. However, xterm.js does not seem to
+// emit the onscroll event when the viewport is scrolled by the mouse. See
+// https://github.com/xtermjs/xterm.js/issues/3864
+export class A11yButtons {
   /**
    * @param {!Terminal} term
    * @param {!Element} elem The container element for the terminal.
+   * @param {!hterm.AccessibilityReader} htermA11yReader
    */
-  constructor(term, elem) {
+  constructor(term, elem, htermA11yReader) {
+    this.term_ = term;
+    this.htermA11yReader_ = htermA11yReader;
     this.pageUpButton_ = document.createElement('button');
     this.pageUpButton_.style.cssText = A11Y_BUTTON_STYLE;
     this.pageUpButton_.textContent =
         hterm.messageManager.get('HTERM_BUTTON_PAGE_UP');
     this.pageUpButton_.addEventListener('click',
-        () => term.scrollPages(-1));
+        () => this.scrollPages_(-1));
 
     this.pageDownButton_ = document.createElement('button');
     this.pageDownButton_.style.cssText = A11Y_BUTTON_STYLE;
     this.pageDownButton_.textContent =
         hterm.messageManager.get('HTERM_BUTTON_PAGE_DOWN');
     this.pageDownButton_.addEventListener('click',
-        () => term.scrollPages(1));
+        () => this.scrollPages_(1));
 
     this.resetPos_();
     elem.prepend(this.pageUpButton_);
@@ -275,6 +282,41 @@
   }
 
   /**
+   * @param {number} amount
+   */
+  scrollPages_(amount) {
+    this.term_.scrollPages(amount);
+    this.announceScreenContent_();
+  }
+
+  announceScreenContent_() {
+    const activeBuffer = this.term_.buffer.active;
+
+    let percentScrolled = 100;
+    if (activeBuffer.baseY !== 0) {
+      percentScrolled = Math.round(
+          100 * activeBuffer.viewportY / activeBuffer.baseY);
+    }
+
+    let currentScreenContent = hterm.messageManager.get(
+        'HTERM_ANNOUNCE_CURRENT_SCREEN_HEADER',
+        [percentScrolled],
+        '$1% scrolled,');
+
+    currentScreenContent += '\n';
+
+    const rowEnd = Math.min(activeBuffer.viewportY + this.term_.rows,
+        activeBuffer.length);
+    for (let i = activeBuffer.viewportY; i < rowEnd; ++i) {
+      currentScreenContent +=
+          activeBuffer.getLine(i).translateToString(true) + '\n';
+    }
+    currentScreenContent = currentScreenContent.trim();
+
+    this.htermA11yReader_.assertiveAnnounce(currentScreenContent);
+  }
+
+  /**
    * @param {boolean} enabled
    */
   setEnabled(enabled) {
@@ -664,7 +706,8 @@
       });
 
       await this.scheduleFit_();
-      this.a11yButtons_ = new A11yButtons(this.term, elem);
+      this.a11yButtons_ = new A11yButtons(this.term, elem,
+          this.htermA11yReader_);
 
       this.onTerminalReady();
     })();