Reland "[logging] Migrate LogHeader component to lit"

This is a reland of commit 02444cf705ae0783b322cd92ca673815509910ad

Original change's description:
> [logging] Migrate LogHeader component to lit
>
> Change-Id: I3f115afcf2a0954848d646608ae88967b86f6927
> Reviewed-on: https://fuchsia-review.googlesource.com/c/vscode-plugins/+/756083
> Reviewed-by: Miguel Flores <miguelfrde@google.com>
> Kokoro: Kokoro <noreply+kokoro@google.com>

Change-Id: Idc0243ec47c97ac991fabc351f15ca77826c8629
Reviewed-on: https://fuchsia-review.googlesource.com/c/vscode-plugins/+/772342
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Miguel Flores <miguelfrde@google.com>
diff --git a/media/webview-logging.css b/media/webview-logging.css
index 6dd643f..d32bca2 100644
--- a/media/webview-logging.css
+++ b/media/webview-logging.css
@@ -12,7 +12,7 @@
   flex-direction: column;
 }
 
-#logs-table-header>div {
+log-header>div {
   background-color: var(--vscode-editor-background);
   border-bottom: 2px solid var(--vscode-panel-border);
   padding-bottom: 5px;
@@ -20,7 +20,7 @@
   top: 0;
 }
 
-#logs-table-header,
+log-header,
 log-view-row {
   display: table-row-group;
 }
@@ -34,7 +34,7 @@
 }
 
 .log-list-cell,
-#logs-table-header>div {
+log-header>div {
   overflow: clip;
   text-align: left;
   text-overflow: ellipsis;
diff --git a/webviews/logging/components/log_header.ts b/webviews/logging/components/log_header.ts
index df7e9ff..9e66fc3 100644
--- a/webviews/logging/components/log_header.ts
+++ b/webviews/logging/components/log_header.ts
@@ -4,57 +4,69 @@
 
 import * as constant from '../src/constants';
 import { State } from '../src/state';
+import {html, LitElement} from 'lit';
+import {customElement} from 'lit/decorators.js';
 import { LogField } from '../src/fields';
 
-export class LogHeader {
-  private tableHeader: Element;
+@customElement('log-header')
+export class LogHeader extends LitElement {
   private maxWidths = {} as Record<string, number>;
+  private headerKeys: LogField[];
 
   constructor(private state: State) {
+    super();
     const fields = this.state.currentFields;
-    const keys = Object.keys(fields);
-    this.tableHeader = this.initTableHeader();
+    const keys = Object.keys(fields).map(field => field as LogField);
+    this.headerKeys = keys;
     for (let i = 0; i < keys.length; i++) {
       const header = keys[i] as LogField;
-      const headerCell = document.createElement('div');
       this.maxWidths[header] = 0;
-      headerCell.id = header;
-      headerCell.style.width = fields[header].width;
-      headerCell.innerText = fields[header].displayName;
-      headerCell.ariaLabel = fields[header].displayName;
-      headerCell.ariaColIndex = `${i + 1}`;
-      this.tableHeader.appendChild(headerCell);
-      if (i === keys.length - 1) {
-        break;
-      }
-      const headerDiv = document.createElement('div');
-      headerDiv.classList.add('column-resize');
-      headerCell.appendChild(headerDiv);
-      createResizableColumn(this.state, this.maxWidths, headerCell, headerDiv);
     }
   }
 
-  /**
-   * Returns the core element.
-   */
-  get element() {
-    return this.tableHeader;
+  render() {
+    const fields = this.state.currentFields;
+    return html`
+
+    ${this.headerKeys.map((field, i) => {
+    return html`
+      <div
+        id="${field}"
+        aria-label="${fields[field].displayName}"
+        aria-colindex="${i + 1}"
+        style="width: ${fields[field].width}">
+        ${fields[field].displayName}
+        ${(i !== this.headerKeys.length - 1) ? (html`
+          <div
+            class="column-resize"
+          ></div>
+        `) : ''}
+      </div>
+      `;
+  })}
+    `;
   }
 
-  /**
-   * Initializes the header element.
-   *
-   * @return the header element.
-   */
-  private initTableHeader(): HTMLDivElement {
-    const tableHeader = document.createElement('div');
-    tableHeader.id = 'logs-table-header';
-    return tableHeader;
+  protected createRenderRoot() {
+    return this;
   }
 
   public setMaxCellWidth(id: LogField, maxWidth: number): void {
     this.maxWidths[id] = maxWidth;
   };
+
+  firstUpdated(changedProperties: any) {
+    super.firstUpdated(changedProperties);
+    const resizers = this.querySelectorAll('.column-resize');
+    for (const index in Array.from(resizers)) {
+      const curResizer = resizers[index];
+      createResizableColumn(
+        this.state,
+        this.maxWidths,
+        curResizer.parentElement!,
+        curResizer as HTMLElement);
+    }
+  }
 }
 
 /**
diff --git a/webviews/logging/components/log_list.ts b/webviews/logging/components/log_list.ts
index d1712ed..cd2cb89 100644
--- a/webviews/logging/components/log_list.ts
+++ b/webviews/logging/components/log_list.ts
@@ -74,7 +74,7 @@
    */
   public reset() {
     this.logList.innerHTML = '';
-    this.logList.appendChild(this.logHeader.element);
+    this.logList.appendChild(this.logHeader);
     this.hasPreviousLog = false;
     this.filterResultsCount = 0;
   }
@@ -99,7 +99,7 @@
    */
   private initLogList(): HTMLDivElement {
     let logList = document.createElement('div');
-    logList.appendChild(this.logHeader.element);
+    logList.appendChild(this.logHeader);
     logList.id = 'log-list';
     return logList;
   }
@@ -121,7 +121,11 @@
       element.setAttribute('hidden', '');
     }
     this.logList.appendChild(element);
-    this.setMaxCellWidth(element);
+    element.updateComplete
+      .then(() => {
+        this.setMaxCellWidth(element.children[0]);
+      })
+      .catch(e => { });
   }
 
   private setMaxCellWidth(row: Element) {
diff --git a/webviews/logging/test/log_header.test.ts b/webviews/logging/test/log_header.test.ts
index 80cfe6b..aaf390d 100644
--- a/webviews/logging/test/log_header.test.ts
+++ b/webviews/logging/test/log_header.test.ts
@@ -6,23 +6,27 @@
 import * as constant from '../src/constants';
 import { State } from '../src/state';
 import { LogHeader } from '../components/log_header';
-import { FakeWebviewAPi } from './util';
+import {FakeWebviewAPi} from './util';
+import {LogList} from '../components/log_list';
 
 before(() => {
   chai.should();
 });
 
-// TODO(fxbug.dev/110279): re-enable
-describe.skip('LogHeader', () => {
+describe('LogHeader', () => {
   let state: State;
   let logHeader: LogHeader;
+  let logList: LogList;
   let root: HTMLElement;
 
-  beforeEach(() => {
+  beforeEach(async () => {
     root = document.createElement('div');
+    document.body.appendChild(root);
     state = new State(new FakeWebviewAPi());
-    logHeader = new LogHeader(state);
-    document.body.appendChild(logHeader.element);
+    logList = new LogList(state);
+    root.appendChild(logList.element);
+    logHeader = logList.element.children[0] as LogHeader;
+    await logHeader.updateComplete;
   });
 
   afterEach(() => {
@@ -32,56 +36,54 @@
   describe('#constructor', () => {
     it('creates header cells for table with resize div', () => {
       const headers = Object.keys(constant.LOGS_HEADERS);
-      const el = logHeader.element as HTMLElement;
-      el.children.length.should.equal(7);
-      for (let i = 0; i < el.children.length; i++) {
-        el.children[i].id.should.equal(headers[i]);
-        if (i === el.children.length - 1) {
-          el.children[i].innerHTML.should.equal('');
+      logHeader.children.length.should.equal(7);
+      for (let i = 0; i < logHeader.children.length; i++) {
+        logHeader.children[i].id.should.equal(headers[i]);
+        if (i === logHeader.children.length - 1) {
+          logHeader.children[i].children.length.should.equal(0);
         } else {
-          el.children[i].innerHTML.should.equal('<div class="column-resize"></div>');
+          logHeader.children[i].children[0].outerHTML.should.equal('<div class="column-resize"></div>');
         }
         // Check aria label and index.
-        el.children[i].should.have.attr('ariaLabel');
-        el.children[i].should.have.attr('ariaColIndex');
-        el.children[i].ariaColIndex?.should.equal(i + 1);
+        logHeader.children[i].should.have.attr('aria-label');
+        logHeader.children[i].should.have.attr('aria-colindex');
+        logHeader.children[i].ariaColIndex?.should.equal(`${i + 1}`);
       }
     });
   });
 
   describe('on mouse drag', () => {
     it('has the resizing class', () => {
-      const monikerEl = logHeader.element.children[3] as HTMLElement;
+      const monikerEl = logHeader.children[3] as HTMLElement;
       const monikerWidth = state.currentFields['moniker'].width;
-
-      window.getComputedStyle(monikerEl).width.should.equal(monikerWidth);
+      monikerEl.style.width.should.equal(monikerWidth);
       monikerEl.children[0].classList.contains('resizing').should.equal(false);
       monikerEl.children[0].dispatchEvent(new MouseEvent('mousedown'));
       monikerEl.children[0].classList.contains('resizing').should.equal(true);
     });
 
     it('resizes based on mouse move', () => {
-      const monikerEl = logHeader.element.children[3] as HTMLElement;
+      const monikerEl = logHeader.children[3] as HTMLElement;
       const monikerWidth = state.currentFields['moniker'].width;
+      const monikerWidthOnPage = monikerEl.clientWidth;
 
-      window.getComputedStyle(monikerEl).width.should.equal(monikerWidth);
+      monikerEl.style.width.should.equal(monikerWidth);
       monikerEl.children[0].dispatchEvent(new MouseEvent('mousedown', { clientX: 0 }));
       document.dispatchEvent(new MouseEvent('mousemove', { clientX: 20 }));
-      window.getComputedStyle(monikerEl).width.should.equal('20px');
+      window.getComputedStyle(monikerEl).width.should.equal(`${monikerWidthOnPage + 20}px`);
     });
   });
 
   describe('on mouse double click', () => {
     it('resizes column to stored size', () => {
-      const monikerEl = logHeader.element.children[3] as HTMLElement;
+      const monikerEl = logHeader.children[3] as HTMLElement;
       const monikerWidth = state.currentFields['moniker'].width;
       const testWidth = 200;
 
-      window.getComputedStyle(monikerEl).width.should.equal(monikerWidth);
+      monikerEl.style.width.should.equal(monikerWidth);
       logHeader.setMaxCellWidth('moniker', testWidth);
       monikerEl.children[0].dispatchEvent(new MouseEvent('dblclick'));
       window.getComputedStyle(monikerEl).width.should.equal(`${testWidth}px`);
     });
   });
 });
-
diff --git a/webviews/logging/test/log_list.test.ts b/webviews/logging/test/log_list.test.ts
index 33f135c..8f439bf 100644
--- a/webviews/logging/test/log_list.test.ts
+++ b/webviews/logging/test/log_list.test.ts
@@ -32,10 +32,12 @@
   });
 
   describe('#constructor', () => {
-    it('crates a new empty view', () => {
+    it('crates a new empty view', async () => {
       const logsList = new LogList(state);
+      root.appendChild(logsList.element);
       logsList.element.children.length.should.equal(1);
-      logsList.element.children[0].id.should.equal('logs-table-header');
+      await (logsList.element.children[0] as LitElement).updateComplete;
+      logsList.element.children[0].nodeName.should.equal('LOG-HEADER');
       logsList.element.should.have.attr('aria-colcount');
       const expectedColumns = Object.keys(state.currentFields).length;
       logsList.element.getAttribute('aria-colcount')?.should.equal(`${expectedColumns}`);
@@ -59,7 +61,8 @@
       document.body.appendChild(logsList);
 
       logsList.children.length.should.equal(1);
-      logsList.children[0].id.should.equal('logs-table-header');
+      await (logsList.children[0] as LitElement).updateComplete;
+      logsList.children[0].nodeName.should.equal('LOG-HEADER');
       view.addLog(logDataForTest('core/foo'));
       logsList.children.length.should.equal(2);
 
@@ -108,7 +111,8 @@
       const logsList = view.element;
       document.body.appendChild(logsList);
       logsList.children.length.should.equal(2);
-      logsList.children[0].id.should.equal('logs-table-header');
+      await (logsList.children[0] as LitElement).updateComplete;
+      logsList.children[0].nodeName.should.equal('LOG-HEADER');
 
       const logLine = logsList.children[1] as LitElement;
       await logLine.updateComplete;
@@ -133,7 +137,8 @@
       const logsList = view.element;
       document.body.appendChild(logsList);
       logsList.children.length.should.equal(2);
-      logsList.children[0].id.should.equal('logs-table-header');
+      await (logsList.children[0] as LitElement).updateComplete;
+      logsList.children[0].nodeName.should.equal('LOG-HEADER');
 
       const logLine = logsList.children[1] as LitElement;
       await logLine.updateComplete;