Migrate gr-plugin-config-array-editor to lit

Release-Notes: skip
Change-Id: I8767940bcbd6e2b404329ae5cbeee6e99ad513f0
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
index 55f7567..2033180 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
@@ -14,19 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import '@polymer/iron-input/iron-input';
 import '@polymer/paper-toggle-button/paper-toggle-button';
-import '../../../styles/gr-form-styles';
-import '../../../styles/shared-styles';
 import '../../shared/gr-button/gr-button';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-plugin-config-array-editor_html';
-import {property, customElement} from '@polymer/decorators';
 import {
   PluginConfigOptionsChangedEventDetail,
   ArrayPluginOption,
 } from '../gr-repo-plugin-config/gr-repo-plugin-config-types';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, html, css} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -35,61 +35,153 @@
 }
 
 @customElement('gr-plugin-config-array-editor')
-export class GrPluginConfigArrayEditor extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrPluginConfigArrayEditor extends LitElement {
   /**
    * Fired when the plugin config option changes.
    *
    * @event plugin-config-option-changed
    */
 
-  @property({type: String})
-  _newValue = '';
+  // private but used in test
+  @state() newValue = '';
 
   // This property is never null, since this component in only about operations
   // on pluginOption.
   @property({type: Object})
   pluginOption!: ArrayPluginOption;
 
-  @property({type: Boolean, reflectToAttribute: true})
+  @property({type: Boolean, reflect: true})
   disabled = false;
 
-  _handleAddTap(e: MouseEvent) {
-    e.preventDefault();
-    this._handleAdd();
+  static override get styles() {
+    return [
+      sharedStyles,
+      formStyles,
+      css`
+        .wrapper {
+          width: 30em;
+        }
+        .existingItems {
+          background: var(--table-header-background-color);
+          border: 1px solid var(--border-color);
+          border-radius: var(--border-radius);
+        }
+        gr-button {
+          float: right;
+          margin-left: var(--spacing-m);
+          width: 4.5em;
+        }
+        .row {
+          align-items: center;
+          display: flex;
+          justify-content: space-between;
+          padding: var(--spacing-m) 0;
+          width: 100%;
+        }
+        .existingItems .row {
+          padding: var(--spacing-m);
+        }
+        .existingItems .row:not(:first-of-type) {
+          border-top: 1px solid var(--border-color);
+        }
+        input {
+          flex-grow: 1;
+        }
+        .hide {
+          display: none;
+        }
+        .placeholder {
+          color: var(--deemphasized-text-color);
+          padding-top: var(--spacing-m);
+        }
+      `,
+    ];
   }
 
-  _handleInputKeydown(e: KeyboardEvent) {
+  override render() {
+    return html`
+      <div class="wrapper gr-form-styles">
+        ${this.renderPluginOptions()}
+        <div class="row ${this.disabled ? 'hide' : ''}">
+          <iron-input
+            .bindValue=${this.newValue}
+            @bind-value-changed=${this.handleBindValueChangedNewValue}
+          >
+            <input
+              id="input"
+              @keydown=${this.handleInputKeydown}
+              ?disabled=${this.disabled}
+            />
+          </iron-input>
+          <gr-button
+            id="addButton"
+            ?disabled=${!this.newValue.length}
+            link
+            @click=${this.handleAddTap}
+            >Add</gr-button
+          >
+        </div>
+      </div>
+    `;
+  }
+
+  private renderPluginOptions() {
+    if (!this.pluginOption?.info?.values?.length) {
+      return html`<div class="row placeholder">None configured.</div>`;
+    }
+
+    return html`
+      <div class="existingItems">
+        ${this.pluginOption.info.values.map(item =>
+          this.renderPluginOptionValue(item)
+        )}
+      </div>
+    `;
+  }
+
+  private renderPluginOptionValue(item: string) {
+    return html`
+      <div class="row">
+        <span>${item}</span>
+        <gr-button
+          link
+          ?disabled=${this.disabled}
+          @click=${() => this.handleDelete(item)}
+          >Delete</gr-button
+        >
+      </div>
+    `;
+  }
+
+  private handleAddTap(e: MouseEvent) {
+    e.preventDefault();
+    this.handleAdd();
+  }
+
+  private handleInputKeydown(e: KeyboardEvent) {
     // Enter.
     if (e.keyCode === 13) {
       e.preventDefault();
-      this._handleAdd();
+      this.handleAdd();
     }
   }
 
-  _handleAdd() {
-    if (!this._newValue.length) {
+  private handleAdd() {
+    if (!this.newValue.length) {
       return;
     }
-    this._dispatchChanged(
-      this.pluginOption.info.values.concat([this._newValue])
-    );
-    this._newValue = '';
+    this.dispatchChanged(this.pluginOption.info.values.concat([this.newValue]));
+    this.newValue = '';
   }
 
-  _handleDelete(e: MouseEvent) {
-    const value = ((dom(e) as EventApi).localTarget as HTMLElement).dataset[
-      'item'
-    ];
-    this._dispatchChanged(
+  private handleDelete(value: string) {
+    this.dispatchChanged(
       this.pluginOption.info.values.filter(str => str !== value)
     );
   }
 
-  _dispatchChanged(values: string[]) {
+  // private but used in test
+  dispatchChanged(values: string[]) {
     const {_key, info} = this.pluginOption;
     const detail: PluginConfigOptionsChangedEventDetail = {
       _key,
@@ -101,7 +193,7 @@
     );
   }
 
-  _computeShowInputRow(disabled: boolean) {
-    return disabled ? 'hide' : '';
+  private handleBindValueChangedNewValue(e: BindValueChangeEvent) {
+    this.newValue = e.detail.value;
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.ts b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.ts
deleted file mode 100644
index 7709198..0000000
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="gr-form-styles">
-    .wrapper {
-      width: 30em;
-    }
-    .existingItems {
-      background: var(--table-header-background-color);
-      border: 1px solid var(--border-color);
-      border-radius: var(--border-radius);
-    }
-    gr-button {
-      float: right;
-      margin-left: var(--spacing-m);
-      width: 4.5em;
-    }
-    .row {
-      align-items: center;
-      display: flex;
-      justify-content: space-between;
-      padding: var(--spacing-m) 0;
-      width: 100%;
-    }
-    .existingItems .row {
-      padding: var(--spacing-m);
-    }
-    .existingItems .row:not(:first-of-type) {
-      border-top: 1px solid var(--border-color);
-    }
-    input {
-      flex-grow: 1;
-    }
-    .hide {
-      display: none;
-    }
-    .placeholder {
-      color: var(--deemphasized-text-color);
-      padding-top: var(--spacing-m);
-    }
-  </style>
-  <div class="wrapper gr-form-styles">
-    <template is="dom-if" if="[[pluginOption.info.values.length]]">
-      <div class="existingItems">
-        <template is="dom-repeat" items="[[pluginOption.info.values]]">
-          <div class="row">
-            <span>[[item]]</span>
-            <gr-button
-              link=""
-              disabled$="[[disabled]]"
-              data-item$="[[item]]"
-              on-click="_handleDelete"
-              >Delete</gr-button
-            >
-          </div>
-        </template>
-      </div>
-    </template>
-    <template is="dom-if" if="[[!pluginOption.info.values.length]]">
-      <div class="row placeholder">None configured.</div>
-    </template>
-    <div class$="row [[_computeShowInputRow(disabled)]]">
-      <iron-input on-keydown="_handleInputKeydown" bind-value="{{_newValue}}">
-        <input
-          is="iron-input"
-          id="input"
-          on-keydown="_handleInputKeydown"
-          bind-value="{{_newValue}}"
-          disabled$="[[disabled]]"
-        />
-      </iron-input>
-      <gr-button
-        id="addButton"
-        disabled$="[[!_newValue.length]]"
-        link=""
-        on-click="_handleAddTap"
-        >Add</gr-button
-      >
-    </div>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.ts b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.ts
index 655eb6b..5de1f1e 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.ts
@@ -42,80 +42,96 @@
     };
   });
 
-  test('_computeShowInputRow', () => {
-    assert.equal(element._computeShowInputRow(true), 'hide');
-    assert.equal(element._computeShowInputRow(false), '');
-  });
-
   suite('adding', () => {
     setup(() => {
-      dispatchStub = sinon.stub(element, '_dispatchChanged');
+      dispatchStub = sinon.stub(element, 'dispatchChanged');
     });
 
-    test('with enter', () => {
-      element._newValue = '';
-      MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
-      assert.isFalse(element.$.input.hasAttribute('disabled'));
-      flush();
+    test('with enter', async () => {
+      element.newValue = '';
+      await element.updateComplete;
+      MockInteractions.pressAndReleaseKeyOn(
+        queryAndAssert<HTMLInputElement>(element, '#input'),
+        13
+      ); // Enter
+      await element.updateComplete;
+      assert.isFalse(
+        queryAndAssert<HTMLInputElement>(element, '#input').hasAttribute(
+          'disabled'
+        )
+      );
+      await element.updateComplete;
 
       assert.isFalse(dispatchStub.called);
-      element._newValue = 'test';
-      MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
-      assert.isFalse(element.$.input.hasAttribute('disabled'));
-      flush();
+      element.newValue = 'test';
+      await element.updateComplete;
+
+      MockInteractions.pressAndReleaseKeyOn(
+        queryAndAssert<HTMLInputElement>(element, '#input'),
+        13
+      ); // Enter
+      await element.updateComplete;
+      assert.isFalse(
+        queryAndAssert<HTMLInputElement>(element, '#input').hasAttribute(
+          'disabled'
+        )
+      );
+      await element.updateComplete;
 
       assert.isTrue(dispatchStub.called);
       assert.equal(dispatchStub.lastCall.args[0], 'test');
-      assert.equal(element._newValue, '');
+      assert.equal(element.newValue, '');
     });
 
-    test('with add btn', () => {
-      element._newValue = '';
-      MockInteractions.tap(element.$.addButton);
-      flush();
+    test('with add btn', async () => {
+      element.newValue = '';
+      queryAndAssert<GrButton>(element, '#addButton').click();
+      await element.updateComplete;
 
       assert.isFalse(dispatchStub.called);
-      element._newValue = 'test';
-      MockInteractions.tap(element.$.addButton);
-      flush();
+
+      element.newValue = 'test';
+      await element.updateComplete;
+
+      queryAndAssert<GrButton>(element, '#addButton').click();
+      await element.updateComplete;
 
       assert.isTrue(dispatchStub.called);
       assert.equal(dispatchStub.lastCall.args[0], 'test');
-      assert.equal(element._newValue, '');
+      assert.equal(element.newValue, '');
     });
   });
 
   test('deleting', async () => {
-    dispatchStub = sinon.stub(element, '_dispatchChanged');
+    dispatchStub = sinon.stub(element, 'dispatchChanged');
     element.pluginOption = {
       _key: '',
       info: {type: ConfigParameterInfoType.ARRAY, values: ['test', 'test2']},
     };
     element.disabled = true;
-    await flush();
+    await element.updateComplete;
 
     const rows = queryAll(element, '.existingItems .row');
     assert.equal(rows.length, 2);
     const button = queryAndAssert<GrButton>(rows[0], 'gr-button');
 
     MockInteractions.tap(button);
-    await flush();
+    await element.updateComplete;
 
     assert.isFalse(dispatchStub.called);
     element.disabled = false;
-    element.notifyPath('pluginOption.info.editable');
-    await flush();
+    await element.updateComplete;
 
-    MockInteractions.tap(button);
-    await flush();
+    button.click();
+    await element.updateComplete;
 
     assert.isTrue(dispatchStub.called);
     assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']);
   });
 
-  test('_dispatchChanged', () => {
+  test('dispatchChanged', () => {
     const eventStub = sinon.stub(element, 'dispatchEvent');
-    element._dispatchChanged(['new-test-value']);
+    element.dispatchChanged(['new-test-value']);
 
     assert.isTrue(eventStub.called);
     const {detail} = eventStub.lastCall.args[0] as CustomEvent;