content/static: unit directories tree layout

Updates unit directories section to nest subdirectories
in a two level tree structure.

For golang/go#43694

Change-Id: I374c042234aeb426b24ae44c861bfb61e6511db1
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/283715
Trust: Jamal Carvalho <jamal@golang.org>
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Alexander Rakoczy <alex@golang.org>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/content/static/css/unit_directories.css b/content/static/css/unit_directories.css
index 9ddfb96..88da146 100644
--- a/content/static/css/unit_directories.css
+++ b/content/static/css/unit_directories.css
@@ -19,22 +19,30 @@
   width: auto;
 }
 .UnitDirectories-table {
-  margin-top: 1.5rem;
+  border-collapse: collapse;
+  height: 0;
+  table-layout: auto;
   width: 100%;
 }
+.UnitDirectories-table--tree {
+  margin-top: -2rem;
+}
 .UnitDirectories-tableHeader {
   background-color: var(--gray-9);
 }
+.UnitDirectories-tableHeader--tree {
+  visibility: hidden;
+}
 .UnitDirectories td {
   border-bottom: 0.0625rem solid var(--gray-8);
   max-width: 32rem;
+  min-width: 12rem;
   padding: 0.5rem 1rem;
   word-break: break-word;
 }
 .UnitDirectories th {
-  text-align: left;
-  border-bottom: 0.0625rem solid var(--gray-8);
   padding: 0.5rem 1rem;
+  text-align: left;
 }
 .UnitDirectories-moduleTag {
   background-color: var(--blue);
@@ -43,3 +51,83 @@
   font-size: 0.74rem;
   padding: 0.2rem 0.4rem;
 }
+.UnitDirectories tr.hidden {
+  display: none;
+}
+.UnitDirectories tr[aria-controls] {
+  cursor: pointer;
+}
+.UnitDirectories tr[aria-controls]:hover {
+  background-color: var(--gray-10);
+}
+.UnitDirectories th.UnitDirectories-toggleHead {
+  font-size: 0;
+  max-width: 0.625rem;
+  padding: 0;
+  width: 0.625rem;
+}
+.UnitDirectories td.UnitDirectories-toggleCell,
+th.UnitDirectories-toggleCell {
+  background-color: var(--white);
+  border: var(--white);
+  max-width: 0.625rem;
+  padding: 0;
+  width: 0.625rem;
+}
+.UnitDirectories-toggleButton {
+  background-color: transparent;
+  border: none;
+  margin: 0 -0.5rem -1rem -1rem;
+  vertical-align: top;
+}
+.UnitDirectories-subSpacer {
+  border-right: 0.0625rem solid var(--gray-8);
+  display: inline;
+  margin-right: 0.875rem;
+  width: 0.0625rem;
+}
+.UnitDirectories-toggleButton[aria-expanded='true'] img {
+  transform: rotate(90deg);
+}
+.UnitDirectories-pathCell {
+  align-items: flex-start;
+  display: flex;
+  flex-direction: column;
+  line-height: 1.75rem;
+  word-break: break-all;
+}
+.UnitDirectories-subdirectory {
+  border-left: 0.0625rem solid var(--gray-8);
+  display: flex;
+  flex-direction: column;
+  padding: 0.5rem 1rem;
+}
+.UnitDirectories-mobileSynopsis {
+  display: none;
+  line-height: 1.25rem;
+  margin-top: 0.25rem;
+  word-break: keep-all;
+}
+@media only screen and (max-width: 52rem) {
+  .UnitDirectories-mobileSynopsis {
+    display: initial;
+  }
+  .UnitDirectories-table th.UnitDirectories-desktopSynopsis,
+  .UnitDirectories-table td.UnitDirectories-desktopSynopsis {
+    display: none;
+  }
+}
+.UnitDirectories-expandButton {
+  position: relative;
+}
+.UnitDirectories-expandButton button {
+  background-color: transparent;
+  border: none;
+  bottom: 1rem;
+  color: var(--turq-dark);
+  cursor: pointer;
+  font-size: 0.875rem;
+  position: absolute;
+  right: 0;
+  text-decoration: none;
+}
diff --git a/content/static/html/helpers/_unit_directories.tmpl b/content/static/html/helpers/_unit_directories.tmpl
index 336fa30..72274c4 100644
--- a/content/static/html/helpers/_unit_directories.tmpl
+++ b/content/static/html/helpers/_unit_directories.tmpl
@@ -9,6 +9,68 @@
     <h2 class="UnitDirectories-title">
       <img height="25px" width="20px" src="/static/img/pkg-icon-folder_20x16.svg" alt="">Directories
     </h2>
+    <div class="UnitDirectories-expandButton js-expandAllDirectories">
+      <button>Expand all</button>
+    </div>
+    {{- if .Directories -}}
+      <table class="UnitDirectories-table UnitDirectories-table--tree js-expandableTable" data-test-id="UnitDirectories-table">
+        <tr class="UnitDirectories-tableHeader UnitDirectories-tableHeader--tree">
+          <th>Path</th>
+          <th class="UnitDirectories-desktopSynopsis">Synopsis</th>
+        </tr>
+        {{- range .Directories -}}
+          {{- $prefix := .Prefix -}}
+          <tr{{if .Subdirectories}} data-aria-controls="{{range .Subdirectories}}{{$prefix}}-{{.Suffix}} {{end}}"{{end}}>
+            <td data-id="{{$prefix}}" data-aria-owns="{{range .Subdirectories}}{{$prefix}}-{{.Suffix}} {{end}}">
+              <div class="UnitDirectories-pathCell">
+                <div>
+                  {{- if .Subdirectories -}}
+                    <button type="button" class="UnitDirectories-toggleButton"
+                        aria-expanded="false"
+                        aria-label="{{len .Subdirectories}} more from"
+                        data-aria-controls="{{range .Subdirectories}}{{$prefix}}-{{.Suffix}} {{end}}"
+                        data-aria-labelledby="{{$prefix}}-button {{$prefix}}"
+                        data-id="{{$prefix}}-button">
+                      <img alt="" src="/static/img/pkg-icon-arrowRight_24x24.svg" height="24" width="24">
+                    </button>
+                  {{- end -}}
+                  {{- if .Root -}}
+                    <a href="{{.Root.URL}}">{{.Root.Suffix}}</a>
+                    {{if .Root.IsModule}}<span class="UnitHeader-badge">Module</span>{{end}}
+                  </div>
+                  <div class="UnitDirectories-mobileSynopsis">{{.Root.Synopsis}}</div>
+                </div>
+              </td>
+              <td class="UnitDirectories-desktopSynopsis">{{.Root.Synopsis}}</td>
+            {{- else -}}
+                <span>{{.Prefix}}</span>
+              </td>
+              <td class="UnitDirectories-desktopSynopsis"></td>
+            {{- end -}}
+          </tr>
+          {{- range .Subdirectories -}}
+            <tr data-id="{{$prefix}}-{{.Suffix}}">
+              <td>
+                <div class="UnitDirectories-subdirectory">
+                  <a href="{{.URL}}">{{.Suffix}}</a>
+                  {{if .IsModule}}<span class="UnitHeader-badge">Module</span>{{end}}
+                  <div class="UnitDirectories-mobileSynopsis">{{.Synopsis}}</div>
+                </div>
+              </td>
+              <td class="UnitDirectories-desktopSynopsis">{{.Synopsis}}</td>
+            {{- end -}}
+          </tr>
+        {{- end -}}
+      </table>
+    {{- end -}}
+  </div>
+{{end}}
+
+{{define "legacy_unit_directories"}}
+  <div class="UnitDirectories js-unitDirectories" id="section-directories">
+    <h2 class="UnitDirectories-title">
+      <img height="25px" width="20px" src="/static/img/pkg-icon-folder_20x16.svg" alt="">Directories
+    </h2>
     {{if (or .Subdirectories .NestedModules) }}
       <table class="UnitDirectories-table" data-test-id="UnitDirectories-table">
         <tr class="UnitDirectories-tableHeader">
diff --git a/content/static/html/pages/unit_details.tmpl b/content/static/html/pages/unit_details.tmpl
index 6dd91d0..805bb51 100644
--- a/content/static/html/pages/unit_details.tmpl
+++ b/content/static/html/pages/unit_details.tmpl
@@ -36,8 +36,14 @@
       {{if .Details.SourceFiles}}
         {{block "unit_files" .Details}}{{end}}
       {{end}}
-      {{if (or .Details.Subdirectories .Details.NestedModules)}}
-        {{block "unit_directories" .Details}}{{end}}
+      {{if (.Experiments.IsActive "directory-tree")}}
+        {{if .Details.Directories}}
+          {{block "unit_directories" .Details}}{{end}}
+        {{end}}
+      {{else}}
+        {{if (or .Details.Subdirectories .Details.NestedModules)}}
+          {{block "legacy_unit_directories" .Details}}{{end}}
+        {{end}}
       {{end}}
     </div>
     <div class="UnitDetails-meta" role="complementary" aria-label="links">
diff --git a/content/static/img/pkg-icon-arrowRight_24x24.svg b/content/static/img/pkg-icon-arrowRight_24x24.svg
new file mode 100644
index 0000000..5938446
--- /dev/null
+++ b/content/static/img/pkg-icon-arrowRight_24x24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="black" width="18px" height="18px"><path d="M10 17l5-5-5-5v10z"/><path d="M0 24V0h24v24H0z" fill="none"/></svg>
\ No newline at end of file
diff --git a/content/static/js/table.js b/content/static/js/table.js
new file mode 100644
index 0000000..60f4f13
--- /dev/null
+++ b/content/static/js/table.js
@@ -0,0 +1,68 @@
+/*!
+ * @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 ExpandableRowsTableController {
+    constructor(table, expandAll) {
+        this.table = table;
+        this.expandAll = expandAll;
+        this.toggles = table.querySelectorAll('[data-aria-controls]');
+        this.setAttributes();
+        this.attachEventListeners();
+        this.updateVisibleItems();
+    }
+    setAttributes() {
+        for (const a of ['data-aria-controls', 'data-aria-labelledby', 'data-id']) {
+            this.table.querySelectorAll(`[${a}]`).forEach(t => {
+                t.setAttribute(a.replace('data-', ''), t.getAttribute(a) ?? '');
+                t.removeAttribute(a);
+            });
+        }
+    }
+    attachEventListeners() {
+        this.toggles.forEach(t => {
+            t.addEventListener('click', e => {
+                this.handleToggleClick(e);
+                this.updateVisibleItems();
+            });
+        });
+        this.expandAll?.addEventListener('click', () => {
+            this.handleExpandAllClick();
+            this.updateVisibleItems();
+        });
+    }
+    handleToggleClick(e) {
+        let target = e.currentTarget;
+        if (!target?.hasAttribute('aria-expanded')) {
+            target = this.table.querySelector(`button[aria-controls="${target?.getAttribute('aria-controls')}"]`);
+        }
+        const isExpanded = target?.getAttribute('aria-expanded') === 'true';
+        target?.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
+        e.stopPropagation();
+    }
+    handleExpandAllClick() {
+        this.table
+            .querySelectorAll('[aria-expanded=false]')
+            .forEach(t => t.setAttribute('aria-expanded', 'true'));
+    }
+    updateVisibleItems() {
+        this.toggles.forEach(t => {
+            const isExpanded = t?.getAttribute('aria-expanded') === 'true';
+            const rowIds = t?.getAttribute('aria-controls')?.trimEnd().split(' ');
+            rowIds?.forEach(id => {
+                const target = document.getElementById(`${id}`);
+                if (isExpanded) {
+                    target?.classList.add('visible');
+                    target?.classList.remove('hidden');
+                }
+                else {
+                    target?.classList.add('hidden');
+                    target?.classList.remove('visible');
+                }
+            });
+        });
+    }
+}
+//# sourceMappingURL=table.js.map
\ No newline at end of file
diff --git a/content/static/js/table.js.map b/content/static/js/table.js.map
new file mode 100644
index 0000000..1bfd1cc
--- /dev/null
+++ b/content/static/js/table.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"table.js","sourceRoot":"","sources":["table.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/content/static/js/table.test.ts b/content/static/js/table.test.ts
new file mode 100644
index 0000000..78b715b
--- /dev/null
+++ b/content/static/js/table.test.ts
@@ -0,0 +1,90 @@
+/*!
+ * @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.
+ */
+
+import { ExpandableRowsTableController } from './table';
+
+describe('ExpandableRowsTableController', () => {
+  let table: HTMLTableElement;
+  let toggle: HTMLButtonElement;
+
+  beforeEach(() => {
+    document.body.innerHTML = `
+      <table class="js-table">
+        <tbody>
+          <tr>
+            <th>Toggle</th>
+            <th>Foo</th>
+            <th>Bar</th>
+          </tr>
+          <tr>
+            <td></td>
+            <td data-id="label-id-1">Hello World</td>
+            <td>Simple row with no toggle or hidden elements</td>
+          </tr>
+          <tr data-aria-controls="hidden-row-id-1 hidden-row-id-2">
+            <td>
+              <button
+                type="button"
+                aria-expanded="false"
+                aria-label="2 more from"
+                data-aria-controls="hidden-row-id-1 hidden-row-id-2"
+                data-aria-labelledby="toggle-id label-id-2"
+                data-id="toggle-id"
+              >
+                +
+              </button>
+            </td>
+            <td data-id="label-id-2">
+              <span>Baz</span>
+            </td>
+            <td></td>
+          </tr>
+          <tr data-id="hidden-row-id-1">
+            <td></td>
+            <td>First hidden row</td>
+            <td></td>
+          </tr>
+          <tr data-id="hidden-row-id-2">
+            <td></td>
+            <td>Second hidden row</td>
+            <td></td>
+          </tr>
+        </tbody>
+      </table>
+    `;
+    table = document.querySelector<HTMLTableElement>('.js-table');
+    new ExpandableRowsTableController(table);
+    toggle = document.querySelector<HTMLButtonElement>('#toggle-id');
+  });
+
+  afterEach(() => {
+    document.body.innerHTML = '';
+  });
+
+  it('sets data-aria-* and data-id attributes to regular html attributes', () => {
+    expect(document.querySelector('#label-id-1')).toBeTruthy();
+    expect(
+      document.querySelector('[aria-controls="hidden-row-id-1 hidden-row-id-2"]')
+    ).toBeTruthy();
+    expect(document.querySelector('[aria-labelledby="toggle-id label-id-2"]')).toBeTruthy();
+    expect(document.querySelector('#toggle-id')).toBeTruthy();
+    expect(document.querySelector('#label-id-2')).toBeTruthy();
+    expect(document.querySelector('#hidden-row-id-1')).toBeTruthy();
+    expect(document.querySelector('#hidden-row-id-2')).toBeTruthy();
+  });
+
+  it('hides rows with unexpanded toggles', () => {
+    expect(document.querySelector('#hidden-row-id-1').classList).toContain('hidden');
+    expect(document.querySelector('#hidden-row-id-2').classList).toContain('hidden');
+  });
+
+  it('shows rows with expanded toggles', () => {
+    toggle.click();
+    expect(document.querySelector('#hidden-row-id-1').classList).toContain('visible');
+    expect(document.querySelector('#hidden-row-id-2').classList).toContain('visible');
+  });
+});
diff --git a/content/static/js/table.ts b/content/static/js/table.ts
new file mode 100644
index 0000000..3845a9d
--- /dev/null
+++ b/content/static/js/table.ts
@@ -0,0 +1,99 @@
+/*!
+ * @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.
+ */
+
+/**
+ * Controller for a table element with expandable rows. Adds event listeners to
+ * a toggle within a table row that controls visiblity of additional related
+ * rows in the table.
+ *
+ * @example
+ * ```typescript
+ * import {ExpandableRowsTableController} from '/static/js/table';
+ *
+ * const el = document .querySelector<HTMLTableElement>('.js-myTableElement')
+ * new ExpandableRowsTableController(el));
+ * ```
+ */
+export class ExpandableRowsTableController {
+  private table: HTMLTableElement;
+  private expandAll?: HTMLButtonElement;
+  private toggles: NodeListOf<HTMLTableRowElement>;
+
+  /**
+   * Create a table controller.
+   * @param table - The table element to which the controller binds.
+   */
+  constructor(table: HTMLTableElement, expandAll?: HTMLButtonElement) {
+    this.table = table;
+    this.expandAll = expandAll;
+    this.toggles = table.querySelectorAll<HTMLTableRowElement>('[data-aria-controls]');
+    this.setAttributes();
+    this.attachEventListeners();
+    this.updateVisibleItems();
+  }
+
+  /**
+   * setAttributes sets data-aria-* and data-id attributes to regular
+   * html attributes as a workaround for limitations from safehtml.
+   */
+  private setAttributes() {
+    for (const a of ['data-aria-controls', 'data-aria-labelledby', 'data-id']) {
+      this.table.querySelectorAll(`[${a}]`).forEach(t => {
+        t.setAttribute(a.replace('data-', ''), t.getAttribute(a) ?? '');
+        t.removeAttribute(a);
+      });
+    }
+  }
+
+  private attachEventListeners() {
+    this.toggles.forEach(t => {
+      t.addEventListener('click', e => {
+        this.handleToggleClick(e);
+        this.updateVisibleItems();
+      });
+    });
+    this.expandAll?.addEventListener('click', () => {
+      this.handleExpandAllClick();
+      this.updateVisibleItems();
+    });
+  }
+
+  private handleToggleClick(e: MouseEvent) {
+    let target = e.currentTarget as HTMLTableRowElement | null;
+    if (!target?.hasAttribute('aria-expanded')) {
+      target = this.table.querySelector(
+        `button[aria-controls="${target?.getAttribute('aria-controls')}"]`
+      );
+    }
+    const isExpanded = target?.getAttribute('aria-expanded') === 'true';
+    target?.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
+    e.stopPropagation();
+  }
+
+  private handleExpandAllClick() {
+    this.table
+      .querySelectorAll('[aria-expanded=false]')
+      .forEach(t => t.setAttribute('aria-expanded', 'true'));
+  }
+
+  private updateVisibleItems() {
+    this.toggles.forEach(t => {
+      const isExpanded = t?.getAttribute('aria-expanded') === 'true';
+      const rowIds = t?.getAttribute('aria-controls')?.trimEnd().split(' ');
+      rowIds?.forEach(id => {
+        const target = document.getElementById(`${id}`);
+        if (isExpanded) {
+          target?.classList.add('visible');
+          target?.classList.remove('hidden');
+        } else {
+          target?.classList.add('hidden');
+          target?.classList.remove('visible');
+        }
+      });
+    });
+  }
+}
diff --git a/content/static/js/unit.js b/content/static/js/unit.js
index d9c5981..b606ab1 100644
--- a/content/static/js/unit.js
+++ b/content/static/js/unit.js
@@ -7,6 +7,13 @@
 
 import { CopyToClipboardController } from './clipboard.js';
 import './toggle-tip.js';
+import { ExpandableRowsTableController } from '/static/js/table.js';
+
+document
+  .querySelectorAll('.js-expandableTable')
+  .forEach(
+    el => new ExpandableRowsTableController(el, document.querySelector('.js-expandAllDirectories'))
+  );
 
 /**
  * Instantiates CopyToClipboardController controller copy buttons