content/static: create accordion controller for sidebar

Creates accordion controller for use in the left sidebar.
Collapses and expands top level sidenav sections.

Change-Id: I11122b96a10241d55e125515b5b8ccb41f27c461
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/259629
Trust: Jamal Carvalho <jamal@golang.org>
Run-TryBot: Jamal Carvalho <jamal@golang.org>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/content/static/css/unit_outline.css b/content/static/css/unit_outline.css
index 6c10f7d..8708d23 100644
--- a/content/static/css/unit_outline.css
+++ b/content/static/css/unit_outline.css
@@ -32,7 +32,7 @@
   position: sticky;
   top: 4.5rem;
 }
-a.UnitOutline-accordian {
+a.UnitOutline-accordion {
   align-items: center;
   color: var(--gray-2);
   display: flex;
@@ -41,7 +41,7 @@
   height: 2.5rem;
   padding: 1rem;
 }
-a.UnitOutline-accordian--active {
+a.UnitOutline-accordion[aria-expanded='true'] {
   background-color: var(--gray-9);
 }
 .UnitOutline-panel {
@@ -50,6 +50,9 @@
   display: block;
   overflow-y: auto;
 }
+.UnitOutline-panel[aria-hidden='true'] {
+  display: none;
+}
 .UnitOutline-jumpTo {
   display: flex;
   margin-bottom: 0.5625rem;
diff --git a/content/static/html/helpers/_unit_outline.tmpl b/content/static/html/helpers/_unit_outline.tmpl
index 1079981..dfb2116 100644
--- a/content/static/html/helpers/_unit_outline.tmpl
+++ b/content/static/html/helpers/_unit_outline.tmpl
@@ -5,28 +5,47 @@
 -->
 
 {{define "unit_outline"}}
-  <div class="UnitOutline">
+  <div class="UnitOutline js-accordion">
     <div class="UnitOutline-jumpTo">
       <button class="UnitOutline-jumpToInput js-jumpToInput"{{if (not .DocOutline.String)}} disabled{{end}}>
         Jump to
       </button>
     </div>
     {{if .Readme.String}}
-      <a class="UnitOutline-accordian js-accordian js-readmeExpand" href="?readme=expanded#readme-top">README</a>
+      <a href="?readme=expanded#readme-top" class="UnitOutline-accordion js-accordionTrigger js-readmeExpand"
+          aria-expanded="true" aria-controls="readme-panel" id="readme-accordion">
+        README
+      </a>
+      <div class="UnitOutline-panel js-accordionPanel"
+          id="readme-panel" role="region" aria-labelledby="readme-accordion" aria-hidden="false"></div>
     {{end}}
     {{if (or .DocOutline.String .Unit.IsPackage)}}
-      <a class="UnitOutline-accordian js-accordian" href="#doc-top">Documentation</a>
-      <div class="UnitOutline-panel">
+      <a class="UnitOutline-accordion  js-accordionTrigger" href="#doc-top"
+          aria-expanded="false" aria-controls="outline-panel" id="outline-accordion">
+        Documentation
+      </a>
+      <div class="UnitOutline-panel js-accordionPanel"
+          id="outline-panel" role="region" aria-labelledby="ouline-accordion" aria-hidden="true">
         <div class="Documentation">
           {{.DocOutline}}
         </div>
       </div>
     {{end}}
     {{if .SourceFiles}}
-      <a class="UnitOutline-accordian js-accordian" href="#files-top">Source Files</a>
+      <a class="UnitOutline-accordion js-accordionTrigger" href="#files-top"
+          aria-expanded="false" aria-controls="files-panel" id="files-accordion">
+        Source Files
+      </a>
+      <div class="UnitOutline-panel js-accordionPanel"
+          id="files-panel" role="region" aria-labelledby="files-accordion" aria-hidden="true"></div>
     {{end}}
     {{if (or .Packages .NestedModules)}}
-      <a class="UnitOutline-accordian js-accordian" href="#directories-top">Directories</a>
+      <a class="UnitOutline-accordion js-accordionTrigger" href="#directories-top"
+          aria-expanded="false" aria-controls="directories-panel" id="directories-accordion">
+        Directories
+      </a>
+      <div class="UnitOutline-panel js-accordionPanel"
+          id="directories-panel" role="region" aria-labelledby="directories-accordion" aria-hidden="true"></div>
     {{end}}
   </div>
 {{end}}
diff --git a/content/static/js/accordion.js b/content/static/js/accordion.js
new file mode 100644
index 0000000..62b640c
--- /dev/null
+++ b/content/static/js/accordion.js
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright 2020 The Go Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file.
+ */
+
+export class AccordionController {
+  constructor(accordion) {
+    this.accordion = accordion;
+    this.triggers = [...this.accordion.querySelectorAll('.js-accordionTrigger')];
+    this.activeTrigger = this.triggers[0];
+    this.init();
+  }
+
+  init() {
+    this.accordion.addEventListener('click', e => {
+      if (e.target.classList.contains('js-accordionTrigger')) {
+        this.select(e.target);
+      }
+    });
+
+    this.accordion.addEventListener('keydown', e => {
+      if (e.target.classList.contains('js-accordionTrigger')) {
+        this.handleKeyPress(e);
+      }
+    });
+  }
+
+  select(target) {
+    const isExpanded = target.getAttribute('aria-expanded') === 'true';
+    if (!isExpanded) {
+      target.setAttribute('aria-expanded', 'true');
+      document
+        .getElementById(target.getAttribute('aria-controls'))
+        .setAttribute('aria-hidden', 'false');
+    }
+    if (this.activeTrigger !== target) {
+      this.activeTrigger.setAttribute('aria-expanded', 'false');
+      document
+        .getElementById(this.activeTrigger.getAttribute('aria-controls'))
+        .setAttribute('aria-hidden', 'true');
+    }
+    this.activeTrigger = target;
+  }
+
+  handleKeyPress(e) {
+    const target = e.target;
+    const key = e.which;
+    const PAGE_UP = 33;
+    const PAGE_DOWN = 34;
+    const END = 35;
+    const HOME = 36;
+    const ARROW_UP = 38;
+    const ARROW_DOWN = 40;
+
+    switch (key) {
+      case PAGE_UP:
+      case PAGE_DOWN:
+      case ARROW_UP:
+      case ARROW_DOWN:
+        const index = this.triggers.indexOf(target);
+        const direction = [PAGE_UP, ARROW_UP].includes(key) ? -1 : 1;
+        const newIndex = (index + this.triggers.length + direction) % this.triggers.length;
+        this.triggers[newIndex].focus();
+        e.preventDefault();
+        break;
+      case END:
+        this.triggers[this.triggers.length - 1].focus();
+        e.preventDefault();
+        break;
+      case HOME:
+        this.triggers[0].focus();
+        e.preventDefault();
+        break;
+
+      default:
+        break;
+    }
+  }
+}
diff --git a/content/static/js/unit.js b/content/static/js/unit.js
index c34d8ae..9ef592a 100644
--- a/content/static/js/unit.js
+++ b/content/static/js/unit.js
@@ -5,20 +5,22 @@
  * license that can be found in the LICENSE file.
  */
 
-// Highlights the active accordian on page load and adds event
-// listeners to update on interaction from user.
-document.querySelectorAll('a.js-accordian').forEach((anchor, index) => {
-  const activeClass = 'UnitOutline-accordian--active';
-  if (index === 0) {
-    anchor.classList.add(activeClass);
+import { AccordionController } from './accordion.js';
+
+/**
+ * Instantiates accordian controller for the left sidebar and sets
+ * the panel for the current location hash as active.
+ */
+const accordion = document.querySelector('.js-accordion');
+if (accordion) {
+  const accordionCtlr = new AccordionController(accordion);
+  const activePanel =
+    window.location.hash &&
+    document.querySelector(`a[href=${JSON.stringify(window.location.hash)}]`);
+  if (activePanel) {
+    accordionCtlr.select(activePanel);
   }
-  anchor.addEventListener('click', () => {
-    document.querySelectorAll('a.js-accordian').forEach(el => {
-      el.classList.remove(activeClass);
-    });
-    anchor.classList.add(activeClass);
-  });
-});
+}
 
 /**
  * Event handlers for expanding and collapsing the readme section.