[gold] Add fork of infra-sk/sort-sk called sort-toggle-sk

sort-sk was not working for 2 reasons:
1) it was slow because it was always moving the nodes, which
lit-html can do faster when it keeps the nodes in place and just
changes their content (which it does when you do map).
2) It didn't work right when the data array changed (see
screencast in attached bug).

This behaves a little different in that you pass sort-toggle-sk
the array directly (instead of it sniffing it from the DOM)
and then it sorts that array (which the client uses to
render the nodes). Given the client control on whether to use
map or repeat() allows for the performance optimal solution
to be chosen (which speeds up sorting on the followup CL
by a factor of 2).

Change-Id: Ib04b2cacc22f538725c39650a67854a26c2293ea
Bug: skia:10504
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/303355
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/modules/sort-toggle-sk/index.ts b/golden/modules/sort-toggle-sk/index.ts
new file mode 100644
index 0000000..c9a682e
--- /dev/null
+++ b/golden/modules/sort-toggle-sk/index.ts
@@ -0,0 +1,2 @@
+import './sort-toggle-sk';
+import './sort-toggle-sk.scss';
diff --git a/golden/modules/sort-toggle-sk/sort-toggle-sk-demo.html b/golden/modules/sort-toggle-sk/sort-toggle-sk-demo.html
new file mode 100644
index 0000000..91994fc
--- /dev/null
+++ b/golden/modules/sort-toggle-sk/sort-toggle-sk-demo.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>sort-toggle-sk</title>
+  <meta charset="utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <style>
+    sort-toggle-sk {
+      display: flex; /* Puppeteer's screenshots complain that node has 0 width w/o this */
+    }
+  </style>
+</head>
+<body>
+  <h1>sort-toggle-sk</h1>
+  <div id=container></div>
+</body>
+</html>
diff --git a/golden/modules/sort-toggle-sk/sort-toggle-sk-demo.ts b/golden/modules/sort-toggle-sk/sort-toggle-sk-demo.ts
new file mode 100644
index 0000000..551b0a9
--- /dev/null
+++ b/golden/modules/sort-toggle-sk/sort-toggle-sk-demo.ts
@@ -0,0 +1,73 @@
+import './index';
+import { html, render } from 'lit-html';
+import { $$ } from 'common-sk/modules/dom';
+import { repeat } from 'lit-html/directives/repeat';
+import { SortToggleSk } from './sort-toggle-sk';
+
+interface DemoSortable {
+  name: string;
+  cost: number;
+  weight: number;
+}
+
+const data: DemoSortable[] = [
+  {
+    name: 'bravo',
+    cost: 10,
+    weight: 36,
+  },
+  {
+    name: 'alfa',
+    cost: 8,
+    weight: 13,
+  },
+  {
+    name: 'charlie',
+    cost: 4,
+    weight: 200,
+  },
+  {
+    name: 'delta',
+    cost: 2,
+    weight: 4,
+  }
+];
+
+
+const rowTemplate = (row: DemoSortable) => html`
+<tr>
+  <td>${row.name}</td>
+  <td>${row.cost}</td>
+  <td>${row.weight}</td>
+</tr>
+`;
+
+// lit-html (or maybe html in general) doesn't like sort-toggle-sk to go inside the table.
+const usingMap = html`
+<sort-toggle-sk .data=${data} @sort-changed=${renderTemplates}>
+  <table>
+     <thead>
+         <tr>
+          <th data-key=name data-sort-toggle-sk=up>Item</th>
+          <th data-key=cost>Cost</th>
+          <th data-key=weight>Weight</th>
+        </tr>
+    </thead>
+    <tbody>
+      <!-- map is generally faster than repeat when the rowTemplate is small, but
+           for this demo, map wasn't working quite right with data being a global.-->
+      ${repeat(data, (row) => row.name, rowTemplate)}
+    </tbody>
+  </table>
+</sort-toggle-sk>`;
+
+
+function renderTemplates() {
+  render(usingMap, $$('#container')!);
+}
+
+renderTemplates();
+// Clients should call sort using the appropriate key and direction after the data is loaded.
+const sortToggleSK = $$('sort-toggle-sk')! as SortToggleSk<DemoSortable>;
+sortToggleSK.sort('name', 'up');
+
diff --git a/golden/modules/sort-toggle-sk/sort-toggle-sk.scss b/golden/modules/sort-toggle-sk/sort-toggle-sk.scss
new file mode 100644
index 0000000..e9b5bfb
--- /dev/null
+++ b/golden/modules/sort-toggle-sk/sort-toggle-sk.scss
@@ -0,0 +1,31 @@
+sort-toggle-sk {
+  [data-sort-toggle-sk='up'] {
+    arrow-drop-up-icon-sk {
+      visibility: visible;
+      display: inline-block;
+    }
+    arrow-drop-down-icon-sk {
+      display: none;
+    }
+  }
+
+  [data-sort-toggle-sk='down'] {
+    arrow-drop-up-icon-sk {
+      visibility: hidden;
+      display: none;
+    }
+    arrow-drop-down-icon-sk {
+      display: inline-block;
+    }
+  }
+
+  arrow-drop-down-icon-sk {
+    display: none;
+  }
+  arrow-drop-up-icon-sk {
+    /* This pre-allocates the space for the arrows to go so the header doesn't have to re-layout
+       (i.e. shift around) when things are toggled. */
+    visibility: hidden;
+    display: inline-block;
+  }
+}
diff --git a/golden/modules/sort-toggle-sk/sort-toggle-sk.ts b/golden/modules/sort-toggle-sk/sort-toggle-sk.ts
new file mode 100644
index 0000000..d8955b2
--- /dev/null
+++ b/golden/modules/sort-toggle-sk/sort-toggle-sk.ts
@@ -0,0 +1,130 @@
+/**
+ * @module module/sort-toggle-sk
+ * @description <h2><code>sort-toggle-sk</code></h2>
+ *
+ * "forked" from sort-sk in infra-sk for performance and correctness reasons when the
+ * data being sorted changes.
+ *
+ * sort-toggle-sk renders a sort arrow on the elements marked with data-key and listens to
+ * clicks on those elements to change an underlying array. It triggers an event which the client
+ * should use to render the many templates, using map or render; whichever is more performant.
+ *
+ * The keys on data-key will be the fields used to sort the array of objects by.
+ *
+ * Clients should set data-sort-toggle-sk to be "up" or "down" on the data-key that the data will
+ * start off sorted in. After the data is loaded, clients are expected to call sort on this element
+ * to make sure the data becomes sorted.
+ *
+ * @evt sort-changed: The user has changed how to sort the data. The arr passed in via property
+ *   is now sorted to match that intent.
+ */
+import { define } from 'elements-sk/define';
+import { $, $$ } from 'common-sk/modules/dom';
+import { ElementSk } from '../../../infra-sk/modules/ElementSk';
+
+import 'elements-sk/icon/arrow-drop-down-icon-sk';
+import 'elements-sk/icon/arrow-drop-up-icon-sk';
+
+export type SortDirection = 'down' | 'up';
+
+// The states to move each button through on a click.
+const toggle = (value: string): SortDirection => {
+  return value === 'down' ? 'up' : 'down';
+};
+
+export class SortToggleSk<T extends Object> extends ElementSk {
+
+  private _data: Array<T> = [];
+
+  constructor() {
+    super(null); // There is no template to use for rendering.
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    // Attach the icons, but only once.
+    $('[data-key]', this).forEach((ele) => {
+      // Only attach the icons once.
+      if ($$('arrow-drop-down-icon-sk', ele)) {
+        return;
+      }
+      ele.appendChild(document.createElement('arrow-drop-down-icon-sk'));
+      ele.appendChild(document.createElement('arrow-drop-up-icon-sk'));
+      ele.addEventListener('click', (e) => this._clickHandler(e));
+    });
+  }
+
+  get data() {
+    return this._data;
+  }
+
+  set data(d: Array<T>) {
+    this._data = d;
+  }
+
+  private _setSortAttribute(ele: Element, value: SortDirection) {
+    ele.setAttribute('data-sort-toggle-sk', value);
+  }
+
+  private _clearSortAttribute(ele: Element) {
+    ele.removeAttribute('data-sort-toggle-sk');
+  }
+
+  private _getSortAttribute(ele: Element) {
+    return ele.getAttribute('data-sort-toggle-sk') || '';
+  }
+
+  private _clickHandler(e: Event) {
+    let ele = e.target! as HTMLElement;
+    // The click might have been on something inside the button (e.g. on the arrow-drop-up-icon-sk),
+    // so we want to bubble up to where the key is and set the class that displays the appropriate
+    // arrow.
+    while (!ele.hasAttribute('data-key') && ele.parentElement !== this) {
+      if (ele.parentElement === null) {
+        break;
+      }
+      ele = ele.parentElement;
+    }
+
+    if (!ele.dataset.key) {
+      throw new DOMException('Inconsistent state: data-key must be non-empty');
+    }
+
+    const dir = toggle(this._getSortAttribute(ele));
+
+    $('[data-key]', this).forEach((e) => {
+      this._clearSortAttribute(e);
+    });
+    this._setSortAttribute(ele, dir);
+
+    // Sort the children of the element at #target.
+    const sortBy = ele.dataset.key! as keyof T;
+    this.sort(sortBy, dir);
+  }
+
+  /**
+   * Re-sort the data by the given key in the given direction. If alpha is true, it will
+   * sort the data as if it were a string (using localeCompare).
+   */
+  sort(key: keyof T, dir: SortDirection) {
+    this._data.sort((a, b) => {
+      let left = a[key] as unknown;
+      let right = b[key] as unknown;
+      if (dir === 'down') {
+        [right, left] = [left, right];
+      }
+      if (typeof left === 'number' && typeof right === 'number') {
+        return left - right;
+      }
+      if (typeof left === 'string' && typeof right === 'string') {
+        return left.localeCompare(right);
+      }
+      throw new Error(
+        `Trying to sort by key "${key}", which is neither a number nor a string. ${left}, ${right}`
+      );
+    });
+    this.dispatchEvent(new CustomEvent('sort-changed', {bubbles: true}));
+  }
+}
+
+define('sort-toggle-sk', SortToggleSk);
diff --git a/golden/modules/sort-toggle-sk/sort-toggle-sk_puppeteer_test.ts b/golden/modules/sort-toggle-sk/sort-toggle-sk_puppeteer_test.ts
new file mode 100644
index 0000000..3f230e2
--- /dev/null
+++ b/golden/modules/sort-toggle-sk/sort-toggle-sk_puppeteer_test.ts
@@ -0,0 +1,69 @@
+import { expect } from 'chai';
+import { addEventListenersToPuppeteerPage, EventName,
+    takeScreenshot, TestBed } from '../../../puppeteer-tests/util';
+import { loadGoldWebpack } from '../common_puppeteer_test/common_puppeteer_test';
+import { ElementHandle } from 'puppeteer';
+
+describe('sort-toggle-sk', () => {
+    let testBed: TestBed;
+    before(async () => {
+        testBed = await loadGoldWebpack();
+    });
+
+    let promiseFactory: <T>(eventName: EventName) => Promise<T>;
+    let sortToggleSk: ElementHandle;
+
+    beforeEach(async () => {
+        promiseFactory = await addEventListenersToPuppeteerPage(testBed.page,
+            ['sort-changed']);
+        const loaded = promiseFactory('sort-changed'); // Emitted when sorted.
+        await testBed.page.goto(`${testBed.baseUrl}/dist/sort-toggle-sk.html`);
+        await loaded;
+        sortToggleSk = (await testBed.page.$('#container sort-toggle-sk'))!;
+    });
+
+    it('should render the demo page', async () => {
+        // Smoke test.
+        expect(await testBed.page.$$('sort-toggle-sk')).to.have.length(1);
+    });
+
+    it('should respect the default sort order', async () => {
+        await expectSortOrderToMatch(['alfa', 'bravo', 'charlie', 'delta']);
+        await takeScreenshot(sortToggleSk, 'gold', 'sort-toggle-sk_sort-alpha-ascending');
+    });
+
+    it('can sort alphabetically in descending order', async () => {
+        await clickSortHeader('name');
+
+        await expectSortOrderToMatch(['delta', 'charlie', 'bravo', 'alfa']);
+        await takeScreenshot(sortToggleSk, 'gold', 'sort-toggle-sk_sort-alpha-descending');
+    });
+
+    it('it can sort by numeric values in descending order', async () => {
+        await clickSortHeader('weight');
+
+        await expectSortOrderToMatch(['charlie', 'bravo', 'alfa', 'delta']);
+        await takeScreenshot(sortToggleSk, 'gold', 'sort-toggle-sk_sort-numeric-descending');
+    });
+
+    it('it can sort by numeric values in ascending order', async () => {
+        await clickSortHeader('weight'); // first in descending order
+        await clickSortHeader('weight'); // then should toggle to be in ascending order
+
+        await expectSortOrderToMatch(['delta', 'alfa', 'bravo', 'charlie']);
+        await takeScreenshot(sortToggleSk, 'gold', 'sort-toggle-sk_sort-numeric-ascending');
+    });
+
+    async function expectSortOrderToMatch(names: string[]) {
+        const nameOrder = await sortToggleSk.$$eval('tbody tr td:first-child',
+            (tds: Element[]) => tds.map(td => td.textContent));
+        expect(names).to.deep.equal(nameOrder);
+    }
+
+    async function clickSortHeader(key: string) {
+        const sortEvent = promiseFactory('sort-changed');
+        const header = await sortToggleSk.$(`th[data-key="${key}"]`);
+        await header!.click();
+        await sortEvent;
+    }
+});
diff --git a/infra-sk/modules/sort-sk/sort-sk.scss b/infra-sk/modules/sort-sk/sort-sk.scss
index 38afe6d..c2a543f 100644
--- a/infra-sk/modules/sort-sk/sort-sk.scss
+++ b/infra-sk/modules/sort-sk/sort-sk.scss
@@ -29,6 +29,8 @@
     display: none;
   }
   arrow-drop-up-icon-sk {
+    /* This pre-allocates the space for the arrows to go so the header doesn't have to re-layout
+   (i.e. shift around) when things are toggled. */
     visibility: hidden;
     display: inline-block;
   }