Merge "Migrate gr-change-star to lit element"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index f481fd9..cd55e15 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -121,7 +121,7 @@
   </style>
   <td aria-hidden="true" class="cell leftPadding"></td>
   <td class="cell star" hidden$="[[!showStar]]" hidden="">
-    <gr-change-star change="{{change}}"></gr-change-star>
+    <gr-change-star change="[[change]]"></gr-change-star>
   </td>
   <td class="cell number" hidden$="[[!showNumber]]" hidden="">
     <a href$="[[changeURL]]">[[change._number]]</a>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 9c6a079..ca237d1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -43,6 +43,7 @@
 import '../gr-reply-dialog/gr-reply-dialog';
 import '../gr-thread-list/gr-thread-list';
 import '../../checks/gr-checks-tab';
+import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-change-view_html';
@@ -2549,7 +2550,7 @@
     this.$.replyOverlay.setFocusStops(dialog.getFocusStops());
   }
 
-  _handleToggleStar(e: CustomEvent<{change: ChangeInfo; starred: boolean}>) {
+  _handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
     if (e.detail.starred) {
       this.reporting.reportInteraction('change-starred-from-change-view');
       this.lastStarredTimestamp = Date.now();
@@ -2627,6 +2628,9 @@
 }
 
 declare global {
+  interface HTMLElementEventMap {
+    'toggle-star': CustomEvent<ChangeStarToggleStarDetail>;
+  }
   interface HTMLElementTagNameMap {
     'gr-change-view': GrChangeView;
   }
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index 155d817..9341b18 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -338,7 +338,7 @@
           </div>
           <gr-change-star
             id="changeStar"
-            change="{{_change}}"
+            change="[[_change]]"
             on-toggle-star="_handleToggleStar"
             hidden$="[[!_loggedIn]]"
           ></gr-change-star>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 591aa41..fd9ce1e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -106,6 +106,7 @@
 import {_testOnly_setState} from '../../../services/user/user-model';
 import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
+import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
 
 const pluginApi = _testOnly_initGerritPluginApi();
 const fixture = fixtureFromElement('gr-change-view');
@@ -2168,17 +2169,19 @@
     });
   });
 
-  test('_handleToggleStar called when star is tapped', () => {
+  test('_handleToggleStar called when star is tapped', async () => {
     element._change = {
       ...createChangeViewChange(),
       owner: {_account_id: 1 as AccountId},
       starred: false,
     };
     element._loggedIn = true;
-    const stub = sinon.stub(element, '_handleToggleStar');
-    flush();
+    await flush();
 
-    tap(element.$.changeStar.shadowRoot!.querySelector('button')!);
+    const stub = sinon.stub(element, '_handleToggleStar');
+
+    const changeStar = queryAndAssert<GrChangeStar>(element, '#changeStar');
+    tap(queryAndAssert<HTMLButtonElement>(changeStar, 'button')!);
     assert.isTrue(stub.called);
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
index a23621e..c6fd01c 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
@@ -15,10 +15,6 @@
  * limitations under the License.
  */
 import '../gr-icons/gr-icons';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-star_html';
-import {customElement, property} from '@polymer/decorators';
 import {ChangeInfo} from '../../../types/common';
 import {fireAlert} from '../../../utils/event-util';
 import {
@@ -26,6 +22,9 @@
   ShortcutSection,
 } from '../../../services/shortcuts/shortcuts-config';
 import {appContext} from '../../../services/app-context';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -39,44 +38,78 @@
 }
 
 @customElement('gr-change-star')
-export class GrChangeStar extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrChangeStar extends LitElement {
   /**
    * Fired when star state is toggled.
    *
    * @event toggle-star
    */
 
-  @property({type: Object, notify: true})
+  @property({type: Object})
   change?: ChangeInfo;
 
   private readonly shortcuts = appContext.shortcutsService;
 
-  _computeStarClass(starred?: boolean) {
-    return starred ? 'active' : '';
+  static override get styles() {
+    return [
+      sharedStyles,
+      css`
+        button {
+          background-color: transparent;
+          cursor: pointer;
+        }
+        iron-icon.active {
+          fill: var(--link-color);
+        }
+        iron-icon {
+          vertical-align: top;
+          --iron-icon-height: var(
+            --gr-change-star-size,
+            var(--line-height-normal, 20px)
+          );
+          --iron-icon-width: var(
+            --gr-change-star-size,
+            var(--line-height-normal, 20px)
+          );
+        }
+        :host([hidden]) {
+          visibility: hidden;
+          display: block !important;
+        }
+      `,
+    ];
   }
 
-  _computeStarIcon(starred?: boolean) {
-    // Hollow star is used to indicate inactive state.
-    return `gr-icons:star${starred ? '' : '-border'}`;
-  }
-
-  _computeAriaLabel(starred?: boolean) {
-    return starred ? 'Unstar this change' : 'Star this change';
+  override render() {
+    return html`
+      <button
+        role="checkbox"
+        title=${this.shortcuts.createTitle(
+          Shortcut.TOGGLE_CHANGE_STAR,
+          ShortcutSection.ACTIONS
+        )}
+        aria-label=${this.change?.starred
+          ? 'Unstar this change'
+          : 'Star this change'}
+        @click=${this.toggleStar}
+      >
+        <iron-icon
+          class=${this.change?.starred ? 'active' : ''}
+          .icon=${`gr-icons:star${this.change?.starred ? '' : '-border'}`}
+        ></iron-icon>
+      </button>
+    `;
   }
 
   toggleStar() {
     // Note: change should always be defined when use gr-change-star
     // but since we don't have a good way to enforce usage to always
     // set the change, we still check it here.
-    if (!this.change) {
-      return;
-    }
+    if (!this.change) return;
+
     const newVal = !this.change.starred;
-    this.set('change.starred', newVal);
+    this.change.starred = newVal;
+    this.requestUpdate('change');
     const detail: ChangeStarToggleStarDetail = {
       change: this.change,
       starred: newVal,
@@ -90,8 +123,4 @@
       })
     );
   }
-
-  createTitle(shortcutName: Shortcut, section: ShortcutSection) {
-    return this.shortcuts.createTitle(shortcutName, section);
-  }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
deleted file mode 100644
index d404795..0000000
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
+++ /dev/null
@@ -1,56 +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">
-    button {
-      background-color: transparent;
-      cursor: pointer;
-    }
-    iron-icon.active {
-      fill: var(--link-color);
-    }
-    iron-icon {
-      vertical-align: top;
-      --iron-icon-height: var(
-        --gr-change-star-size,
-        var(--line-height-normal, 20px)
-      );
-      --iron-icon-width: var(
-        --gr-change-star-size,
-        var(--line-height-normal, 20px)
-      );
-    }
-    :host([hidden]) {
-      visibility: hidden;
-      display: block !important;
-    }
-  </style>
-  <button
-    role="checkbox"
-    title="[[createTitle(Shortcut.TOGGLE_CHANGE_STAR,
-      ShortcutSection.ACTIONS)]]"
-    aria-label="[[_computeAriaLabel(change.starred)]]"
-    on-click="toggleStar"
-  >
-    <iron-icon
-      class$="[[_computeStarClass(change.starred)]]"
-      icon$="[[_computeStarIcon(change.starred)]]"
-    ></iron-icon>
-  </button>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
index 8f411ae..2c5d7a2 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
@@ -27,45 +27,47 @@
 suite('gr-change-star tests', () => {
   let element: GrChangeStar;
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
     element.change = {
       ...createChange(),
       starred: true,
     };
+    await element.updateComplete;
   });
 
   test('star visibility states', async () => {
-    element.set('change.starred', true);
-    await flush();
+    element.change!.starred = true;
+    await element.updateComplete;
     let icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
     assert.isTrue(icon.classList.contains('active'));
     assert.equal(icon.icon, 'gr-icons:star');
 
-    element.set('change.starred', false);
-    await flush();
+    element.change!.starred = false;
+    element.requestUpdate('change');
+    await element.updateComplete;
     icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
     assert.isFalse(icon.classList.contains('active'));
     assert.equal(icon.icon, 'gr-icons:star-border');
   });
 
   test('starring', async () => {
-    element.set('change.starred', false);
-    await flush();
+    element.change!.starred = false;
+    await element.updateComplete;
     assert.equal(element.change!.starred, false);
 
-    MockInteractions.tap(queryAndAssert(element, 'button'));
-    await flush();
+    MockInteractions.tap(queryAndAssert<HTMLButtonElement>(element, 'button'));
+    await element.updateComplete;
     assert.equal(element.change!.starred, true);
   });
 
   test('unstarring', async () => {
-    element.set('change.starred', true);
-    await flush();
+    element.change!.starred = true;
+    await element.updateComplete;
     assert.equal(element.change!.starred, true);
 
-    MockInteractions.tap(queryAndAssert(element, 'button'));
-    await flush();
+    MockInteractions.tap(queryAndAssert<HTMLButtonElement>(element, 'button'));
+    await element.updateComplete;
     assert.equal(element.change!.starred, false);
   });
 });