Migrate gr-account-entry to lit

Release-Notes: skip
Change-Id: I97e3fd33acf0936134a7fa33b51c22226fd2ba6e
(cherry picked from commit 51f56467069d1501f41c9d59fda2e934674c308a)
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
index 2972d24..13cead6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
@@ -103,6 +103,7 @@
 
     element.$.ccs.entry!.setText('test');
     MockInteractions.tap(queryAndAssert(element, 'gr-button.send'));
+    assert.isFalse(element.$.ccs.submitEntryText());
     assert.isFalse(sendStub.called);
     flush();
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 883fbfa..5ea5cb4 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -69,6 +69,7 @@
 import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
 import {GrLabelScores} from '../gr-label-scores/gr-label-scores';
 import {GrThreadList} from '../gr-thread-list/gr-thread-list';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 
 const basicFixture = fixtureFromElement('gr-reply-dialog');
 
@@ -1182,10 +1183,10 @@
     );
 
     // We should be focused on account entry input.
+    const reviewersEntry = queryAndAssert<GrAccountList>(element, '#reviewers');
     assert.isTrue(
       isFocusInsideElement(
-        queryAndAssert<GrAccountList>(element, '#reviewers').entry!.$.input.$
-          .input
+        queryAndAssert<GrAutocomplete>(reviewersEntry.entry, '#input').$.input
       )
     );
 
@@ -1243,16 +1244,20 @@
 
     // We should be focused on account entry input.
     if (cc) {
+      const ccsEntry = queryAndAssert<GrAccountList>(element, '#ccs');
       assert.isTrue(
         isFocusInsideElement(
-          queryAndAssert<GrAccountList>(element, '#ccs').entry!.$.input.$.input
+          queryAndAssert<GrAutocomplete>(ccsEntry.entry, '#input').$.input
         )
       );
     } else {
+      const reviewersEntry = queryAndAssert<GrAccountList>(
+        element,
+        '#reviewers'
+      );
       assert.isTrue(
         isFocusInsideElement(
-          queryAndAssert<GrAccountList>(element, '#reviewers').entry!.$.input.$
-            .input
+          queryAndAssert<GrAutocomplete>(reviewersEntry.entry, '#input').$.input
         )
       );
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
index acb8348..71023a1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
@@ -14,30 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../styles/shared-styles';
+
 import '../gr-autocomplete/gr-autocomplete';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-account-entry_html';
-import {customElement, property} from '@polymer/decorators';
 import {
   AutocompleteQuery,
   GrAutocomplete,
 } from '../gr-autocomplete/gr-autocomplete';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, html, css} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
 
-export interface GrAccountEntry {
-  $: {
-    input: GrAutocomplete;
-  };
-}
 /**
  * gr-account-entry is an element for entering account
  * and/or group with autocomplete support.
  */
 @customElement('gr-account-entry')
-export class GrAccountEntry extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
+export class GrAccountEntry extends LitElement {
+  @query('#input') private input?: GrAutocomplete;
 
   /**
    * Fired when an account is entered.
@@ -62,33 +56,70 @@
   @property({type: String})
   placeholder = '';
 
-  @property({type: Object, notify: true})
+  @property({type: Object})
   querySuggestions: AutocompleteQuery = () => Promise.resolve([]);
 
-  @property({type: String, observer: '_inputTextChanged'})
-  _inputText = '';
+  @state() private inputText = '';
+
+  static override get styles() {
+    return [
+      sharedStyles,
+      css`
+        gr-autocomplete {
+          display: inline-block;
+          flex: 1;
+          overflow: hidden;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html`
+      <gr-autocomplete
+        id="input"
+        .borderless=${this.borderless}
+        .placeholder=${this.placeholder}
+        .query=${this.querySuggestions}
+        .allowNonSuggestedValues=${this.allowAnyInput}
+        @commit=${this.handleInputCommit}
+        clear-on-commit=""
+        warn-uncommitted=""
+        .text=${this.inputText}
+        .verticalOffset=${24}
+        @text-changed=${this.handleTextChanged}
+      >
+      </gr-autocomplete>
+    `;
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('inputText')) {
+      this.inputTextChanged();
+    }
+  }
 
   get focusStart() {
-    return this.$.input.focusStart;
+    return this.input!.focusStart;
   }
 
   override focus() {
-    this.$.input.focus();
+    this.input!.focus();
   }
 
   clear() {
-    this.$.input.clear();
+    this.input!.clear();
   }
 
   setText(text: string) {
-    this.$.input.setText(text);
+    this.input!.setText(text);
   }
 
   getText() {
-    return this.$.input.text;
+    return this.input!.text;
   }
 
-  _handleInputCommit(e: CustomEvent) {
+  private handleInputCommit(e: CustomEvent) {
     this.dispatchEvent(
       new CustomEvent('add', {
         detail: {value: e.detail.value},
@@ -96,16 +127,20 @@
         bubbles: true,
       })
     );
-    this.$.input.focus();
+    this.input!.focus();
   }
 
-  _inputTextChanged(text: string) {
-    if (text.length && this.allowAnyInput) {
+  private inputTextChanged() {
+    if (this.inputText.length && this.allowAnyInput) {
       this.dispatchEvent(
         new CustomEvent('account-text-changed', {bubbles: true, composed: true})
       );
     }
   }
+
+  private handleTextChanged(e: BindValueChangeEvent) {
+    this.inputText = e.detail.value;
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts
deleted file mode 100644
index d84ef62..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts
+++ /dev/null
@@ -1,40 +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">
-    gr-autocomplete {
-      display: inline-block;
-      flex: 1;
-      overflow: hidden;
-    }
-  </style>
-  <gr-autocomplete
-    id="input"
-    borderless="[[borderless]]"
-    placeholder="[[placeholder]]"
-    query="[[querySuggestions]]"
-    allow-non-suggested-values="[[allowAnyInput]]"
-    on-commit="_handleInputCommit"
-    clear-on-commit=""
-    warn-uncommitted=""
-    text="{{_inputText}}"
-    vertical-offset="24"
-  >
-  </gr-autocomplete>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts
index 4bb2232..8b3452e 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts
@@ -18,48 +18,58 @@
 import '../../../test/common-test-setup-karma';
 import './gr-account-entry';
 import {GrAccountEntry} from './gr-account-entry';
-
-const basicFixture = fixtureFromElement('gr-account-entry');
+import {fixture, html} from '@open-wc/testing-helpers';
+import {queryAndAssert} from '../../../test/test-utils';
+import {GrAutocomplete} from '../gr-autocomplete/gr-autocomplete';
+import {PaperInputElementExt} from '../../../types/types';
 
 suite('gr-account-entry tests', () => {
   let element: GrAccountEntry;
 
-  setup(() => {
-    element = basicFixture.instantiate();
+  setup(async () => {
+    element = await fixture<GrAccountEntry>(html`
+      <gr-account-entry></gr-account-entry>
+    `);
   });
 
-  test('account-text-changed fired when input text changed and allowAnyInput', () => {
+  test('account-text-changed fired when input text changed and allowAnyInput', async () => {
     // Spy on query, as that is called when _updateSuggestions proceeds.
     const changeStub = sinon.stub();
     element.allowAnyInput = true;
     element.querySuggestions = () => Promise.resolve([]);
     element.addEventListener('account-text-changed', changeStub);
-    element.$.input.text = 'a';
+    queryAndAssert<GrAutocomplete>(element, '#input').text = 'a';
+    await element.updateComplete;
     assert.isTrue(changeStub.calledOnce);
-    element.$.input.text = 'ab';
+    queryAndAssert<GrAutocomplete>(element, '#input').text = 'ab';
+    await element.updateComplete;
     assert.isTrue(changeStub.calledTwice);
   });
 
-  test(
-    'account-text-changed not fired when input text changed without ' +
-      'allowAnyInput',
-    () => {
-      // Spy on query, as that is called when _updateSuggestions proceeds.
-      const changeStub = sinon.stub();
-      element.querySuggestions = () => Promise.resolve([]);
-      element.addEventListener('account-text-changed', changeStub);
-      element.$.input.text = 'a';
-      assert.isFalse(changeStub.called);
-    }
-  );
-
-  test('setText', () => {
+  test('account-text-changed not fired when input text changed without allowAnyInput', async () => {
     // Spy on query, as that is called when _updateSuggestions proceeds.
-    const suggestSpy = sinon.spy(element.$.input, 'query');
-    element.setText('test text');
-    flush();
+    const changeStub = sinon.stub();
+    element.querySuggestions = () => Promise.resolve([]);
+    element.addEventListener('account-text-changed', changeStub);
+    queryAndAssert<GrAutocomplete>(element, '#input').text = 'a';
+    await element.updateComplete;
+    assert.isFalse(changeStub.called);
+  });
 
-    assert.equal(element.$.input.$.input.value, 'test text');
+  test('setText', async () => {
+    // Spy on query, as that is called when _updateSuggestions proceeds.
+    const suggestSpy = sinon.spy(
+      queryAndAssert<GrAutocomplete>(element, '#input'),
+      'query'
+    );
+    element.setText('test text');
+    await element.updateComplete;
+
+    const input = queryAndAssert<GrAutocomplete>(element, '#input');
+    assert.equal(
+      queryAndAssert<PaperInputElementExt>(input, '#input').value,
+      'test text'
+    );
     assert.isFalse(suggestSpy.called);
   });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index cf24bcd..ddf177a 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -39,8 +39,10 @@
 import {
   AutocompleteQuery,
   AutocompleteSuggestion,
+  GrAutocomplete,
 } from '../gr-autocomplete/gr-autocomplete';
 import {ValueChangedEvent} from '../../../types/events';
+import {queryAndAssert} from '../../../utils/common-util';
 
 const VALID_EMAIL_ALERT = 'Please input a valid email.';
 
@@ -404,7 +406,8 @@
 
   private handleInputKeydown(e: KeyboardEvent) {
     const target = e.target as GrAccountEntry;
-    const input = this.getOwnNativeInput(target.$.input.$.input);
+    const entryInput = queryAndAssert<GrAutocomplete>(target, '#input');
+    const input = this.getOwnNativeInput(entryInput.$.input);
     if (
       input.selectionStart !== input.selectionEnd ||
       input.selectionStart !== 0
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index 7509023..5d7d312 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -30,9 +30,11 @@
   GroupName,
   Suggestion,
 } from '../../../types/common';
-import {queryAll} from '../../../test/test-utils';
+import {queryAll, queryAndAssert} from '../../../test/test-utils';
 import {ReviewerSuggestionsProvider} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {GrAutocomplete} from '../gr-autocomplete/gr-autocomplete';
+import {GrAccountEntry} from '../gr-account-entry/gr-account-entry';
 
 const basicFixture = fixtureFromElement('gr-account-list');
 
@@ -117,10 +119,14 @@
   test('account entry only appears when editable', async () => {
     element.readonly = false;
     await element.updateComplete;
-    assert.isFalse(element.entry!.hasAttribute('hidden'));
+    assert.isFalse(
+      queryAndAssert<GrAccountEntry>(element, '#entry').hasAttribute('hidden')
+    );
     element.readonly = true;
     await element.updateComplete;
-    assert.isTrue(element.entry!.hasAttribute('hidden'));
+    assert.isTrue(
+      queryAndAssert<GrAccountEntry>(element, '#entry').hasAttribute('hidden')
+    );
   });
 
   test('addition and removal of account/group chips', async () => {
@@ -295,13 +301,19 @@
     element.allowAnyInput = true;
     await element.updateComplete;
 
-    const getTextStub = sinon.stub(element.entry!, 'getText');
+    const getTextStub = sinon.stub(
+      queryAndAssert<GrAccountEntry>(element, '#entry'),
+      'getText'
+    );
     getTextStub.onFirstCall().returns('');
     getTextStub.onSecondCall().returns('test');
     getTextStub.onThirdCall().returns('test@test');
 
     // When entry is empty, return true.
-    const clearStub = sinon.stub(element.entry!, 'clear');
+    const clearStub = sinon.stub(
+      queryAndAssert<GrAccountEntry>(element, '#entry'),
+      'clear'
+    );
     assert.isTrue(element.submitEntryText());
     assert.isFalse(clearStub.called);
 
@@ -387,7 +399,9 @@
     const acct = makeAccount();
     handleAdd({account: acct, count: 1});
     await element.updateComplete;
-    assert.isTrue(element.entry!.hasAttribute('hidden'));
+    assert.isTrue(
+      queryAndAssert<GrAccountEntry>(element, '#entry').hasAttribute('hidden')
+    );
   });
 
   test('enter text calls suggestions provider', async () => {
@@ -410,8 +424,10 @@
       'makeSuggestionItem'
     );
 
-    const input = element.entry!.$.input;
-
+    const input = queryAndAssert<GrAutocomplete>(
+      queryAndAssert<GrAccountEntry>(element, '#entry'),
+      '#input'
+    );
     input.text = 'newTest';
     MockInteractions.focus(input.$.input);
     input.noDebounce = true;
@@ -446,7 +462,10 @@
 
   suite('keyboard interactions', () => {
     test('backspace at text input start removes last account', async () => {
-      const input = element.entry!.$.input;
+      const input = queryAndAssert<GrAutocomplete>(
+        queryAndAssert<GrAccountEntry>(element, '#entry'),
+        '#input'
+      );
       sinon.stub(input, '_updateSuggestions');
       sinon.stub(element, 'computeRemovable').returns(true);
       await await element.updateComplete;
@@ -472,7 +491,10 @@
     });
 
     test('arrow key navigation', async () => {
-      const input = element.entry!.$.input;
+      const input = queryAndAssert<GrAutocomplete>(
+        queryAndAssert<GrAccountEntry>(element, '#entry'),
+        '#input'
+      );
       input.text = '';
       element.accounts = [makeAccount(), makeAccount()];
       await element.updateComplete;