terminal: improve a11y for xterm.js

Bug: b/236205389
Change-Id: I2f66d75c870a7f49bc2be806f41657836de68f06
Reviewed-on: https://chromium-review.googlesource.com/c/apps/libapps/+/3947625
Reviewed-by: Joel Hockey <joelhockey@chromium.org>
Tested-by: kokoro <noreply+kokoro@google.com>
diff --git a/terminal/js/terminal_emulator.js b/terminal/js/terminal_emulator.js
index d25a2af..9d51c53 100644
--- a/terminal/js/terminal_emulator.js
+++ b/terminal/js/terminal_emulator.js
@@ -239,6 +239,68 @@
   }
 }
 
+const A11Y_BUTTON_STYLE = `
+position: fixed;
+z-index: 10;
+right: 16px;
+`;
+
+class A11yButtons {
+  /**
+   * @param {!Terminal} term
+   * @param {!Element} elem The container element for the terminal.
+   */
+  constructor(term, elem) {
+    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.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.resetPos_();
+    elem.prepend(this.pageUpButton_);
+    elem.append(this.pageDownButton_);
+
+    this.onSelectionChange_ = this.onSelectionChange_.bind(this);
+  }
+
+  /**
+   * @param {boolean} enabled
+   */
+  setEnabled(enabled) {
+    if (enabled) {
+      document.addEventListener('selectionchange', this.onSelectionChange_);
+    } else {
+      this.resetPos_();
+      document.removeEventListener('selectionchange', this.onSelectionChange_);
+    }
+  }
+
+  resetPos_() {
+    this.pageUpButton_.style.top = '-200px';
+    this.pageDownButton_.style.bottom = '-200px';
+  }
+
+  onSelectionChange_() {
+    this.resetPos_();
+
+    const selectedElement = document.getSelection().anchorNode.parentElement;
+    if (selectedElement === this.pageUpButton_) {
+      this.pageUpButton_.style.top = '16px';
+    } else if (selectedElement === this.pageDownButton_) {
+      this.pageDownButton_.style.bottom = '16px';
+    }
+  }
+}
+
 /**
  * A terminal class that 1) uses xterm.js and 2) behaves like a `hterm.Terminal`
  * so that it can be used in existing code.
@@ -323,7 +385,8 @@
 
     this.io = new XtermTerminalIO(this);
     this.notificationCenter_ = null;
-
+    this.htermA11yReader_ = null;
+    this.a11yButtons_ = null;
     this.copyNotice_ = null;
 
     this.term.options.linkHandler = new LinkHandler(this.term);
@@ -370,6 +433,13 @@
     this.xtermInternal_.cursorLeft(number ?? 1);
   }
 
+  /** @override */
+  setAccessibilityEnabled(enabled) {
+    this.a11yButtons_.setEnabled(enabled);
+    this.htermA11yReader_.setAccessibilityEnabled(enabled);
+    this.term.options.screenReaderMode = enabled;
+  }
+
   /**
    * Install stubs for stuff that we haven't implemented yet so that the code
    * still runs.
@@ -389,7 +459,6 @@
     this.keyboard.keyMap.keyDefs[78] = {};
 
     const methodNames = [
-        'setAccessibilityEnabled',
         'setBackgroundImage',
         'setCursorPosition',
         'setCursorVisible',
@@ -499,8 +568,9 @@
       }
       this.term.focus();
       (new ResizeObserver(() => this.scheduleFit_())).observe(elem);
-      // TODO: Make a11y work. Maybe we can just use hterm.AccessibilityReader.
-      this.notificationCenter_ = new hterm.NotificationCenter(document.body);
+      this.htermA11yReader_ = new hterm.AccessibilityReader(elem);
+      this.notificationCenter_ = new hterm.NotificationCenter(document.body,
+          this.htermA11yReader_);
 
       // Block right-click context menu from popping up.
       elem.addEventListener('contextmenu', (e) => e.preventDefault());
@@ -524,6 +594,8 @@
       });
 
       await this.scheduleFit_();
+      this.a11yButtons_ = new A11yButtons(this.term, elem);
+
       this.onTerminalReady();
     })();
   }
@@ -1063,6 +1135,16 @@
   }
 
   /** @override */
+  connectedCallback() {
+    super.connectedCallback();
+    if (!this.childNodes.length) {
+      // This is not visible since we use shadow dom. But this will allow the
+      // hterm.NotificationCenter to announce the the copy text.
+      this.append(hterm.messageManager.get('HTERM_NOTIFY_COPY'));
+    }
+  }
+
+  /** @override */
   render() {
     return html`
        ${ICON_COPY}