Merge "Fix and update toast colors"
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 a9b4ece..a0aa962 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
@@ -224,7 +224,7 @@
     hidden$="[[_computeIsColumnHidden('Repo', visibleChangeTableColumns)]]"
   >
     <a class="fullRepo" href$="[[_computeRepoUrl(change)]]">
-      <div class="content">[[_computeRepoDisplay(change)]]</div>
+      [[_computeRepoDisplay(change)]]
     </a>
     <a
       class="truncatedRepo"
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
index 96a19e0..aa76347 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
@@ -128,7 +128,7 @@
 
       // Open confirmation dialog and tap confirm button.
       await element.$.confirmDeleteOverlay.open();
-      MockInteractions.tap(element.$.confirmDeleteDialog.$.confirm);
+      MockInteractions.tap(element.$.confirmDeleteDialog.confirmButton);
       flush();
       assert.isTrue(deleteStub.calledWithExactly('-is:open'));
       assert.isTrue(element.$.confirmDeleteDialog.disabled);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
index 1256cc1..3df997f8 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
@@ -123,10 +123,11 @@
 
     test('cherry pick topic submit', async () => {
       element.branch = 'master';
+      await flush();
       const executeChangeActionStub = stubRestApi(
           'executeChangeAction').returns(Promise.resolve([]));
       MockInteractions.tap(element.shadowRoot.
-          querySelector('gr-dialog').$.confirm);
+          querySelector('gr-dialog').confirmButton);
       await flush();
       const args = executeChangeActionStub.args[0];
       assert.equal(args[0], 1);
@@ -137,26 +138,29 @@
       assert.isTrue(args[4].allow_empty);
     });
 
-    test('deselecting a change removes it from being cherry picked', () => {
-      const duplicateChangesStub = sinon.stub(element,
-          'containsDuplicateProject');
-      element.branch = 'master';
-      const executeChangeActionStub = stubRestApi(
-          'executeChangeAction').returns(Promise.resolve([]));
-      const checkboxes = element.shadowRoot.querySelectorAll(
-          'input[type="checkbox"]');
-      assert.equal(checkboxes.length, 2);
-      assert.isTrue(checkboxes[0].checked);
-      MockInteractions.tap(checkboxes[0]);
-      MockInteractions.tap(element.shadowRoot.
-          querySelector('gr-dialog').$.confirm);
-      flush();
-      assert.equal(executeChangeActionStub.callCount, 1);
-      assert.isTrue(duplicateChangesStub.called);
-    });
+    test('deselecting a change removes it from being cherry picked',
+        async () => {
+          const duplicateChangesStub = sinon.stub(element,
+              'containsDuplicateProject');
+          element.branch = 'master';
+          await flush();
+          const executeChangeActionStub = stubRestApi(
+              'executeChangeAction').returns(Promise.resolve([]));
+          const checkboxes = element.shadowRoot.querySelectorAll(
+              'input[type="checkbox"]');
+          assert.equal(checkboxes.length, 2);
+          assert.isTrue(checkboxes[0].checked);
+          MockInteractions.tap(checkboxes[0]);
+          MockInteractions.tap(element.shadowRoot.
+              querySelector('gr-dialog').confirmButton);
+          await flush();
+          assert.equal(executeChangeActionStub.callCount, 1);
+          assert.isTrue(duplicateChangesStub.called);
+        });
 
-    test('deselecting all change shows error message', () => {
+    test('deselecting all change shows error message', async () => {
       element.branch = 'master';
+      await flush();
       const executeChangeActionStub = stubRestApi(
           'executeChangeAction').returns(Promise.resolve([]));
       const checkboxes = element.shadowRoot.querySelectorAll(
@@ -165,8 +169,8 @@
       MockInteractions.tap(checkboxes[0]);
       MockInteractions.tap(checkboxes[1]);
       MockInteractions.tap(element.shadowRoot.
-          querySelector('gr-dialog').$.confirm);
-      flush();
+          querySelector('gr-dialog').confirmButton);
+      await flush();
       assert.equal(executeChangeActionStub.callCount, 0);
       assert.equal(element.shadowRoot.querySelector('.error-message').innerText
           , 'No change selected');
@@ -180,8 +184,8 @@
     });
 
     test('submit button is blocked while cherry picks is running', async () => {
-      const confirmButton = element.shadowRoot.querySelector('gr-dialog').$
-          .confirm;
+      const confirmButton = element.shadowRoot.querySelector('gr-dialog')
+          .confirmButton;
       assert.isTrue(confirmButton.hasAttribute('disabled'));
       element.branch = 'b';
       await flush();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
index d115899..dec65e2 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
@@ -58,6 +58,7 @@
     gr-vote-chip {
       --gr-vote-chip-width: 14px;
       --gr-vote-chip-height: 14px;
+      margin-right: var(--spacing-s);
     }
   </style>
   <div class="container">
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 19bc388..21e8093 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -21,6 +21,7 @@
 import {
   AccountInfo,
   isDetailedLabelInfo,
+  LabelInfo,
   LabelNameToInfoMap,
   SubmitRequirementResultInfo,
   SubmitRequirementStatus,
@@ -28,6 +29,7 @@
 import {unique} from '../../../utils/common-util';
 import {
   extractAssociatedLabels,
+  getAllUniqueApprovals,
   hasVotes,
   iconForStatus,
 } from '../../../utils/label-util';
@@ -92,9 +94,19 @@
           visibility: visible;
         }
         .requirements,
-        section.votes {
+        section.trigger-votes {
           margin-left: var(--spacing-l);
         }
+        .trigger-votes {
+          padding-top: var(--spacing-s);
+          display: flex;
+          flex-wrap: wrap;
+          gap: var(--spacing-s);
+          /* Setting max-width as defined in Submit Requirements design,
+           *  to wrap overflowed items to next row.
+           */
+          max-width: 390px;
+        }
         gr-limited-text.name {
           font-weight: var(--font-weight-bold);
         }
@@ -115,6 +127,9 @@
           color: var(--error-foreground);
           vertical-align: top;
         }
+        gr-vote-chip {
+          margin-right: var(--spacing-s);
+        }
       `,
     ];
   }
@@ -211,12 +226,7 @@
   renderLabelVote(label: string, labels: LabelNameToInfoMap) {
     const labelInfo = labels[label];
     if (!isDetailedLabelInfo(labelInfo)) return;
-    const uniqueApprovals = (labelInfo.all ?? [])
-      .filter(
-        (approvalInfo, index, array) =>
-          index === array.findIndex(other => other.value === approvalInfo.value)
-      )
-      .sort((a, b) => -(a.value ?? 0) + (b.value ?? 0));
+    const uniqueApprovals = getAllUniqueApprovals(labelInfo);
     return uniqueApprovals.map(
       approvalInfo =>
         html`<gr-vote-chip
@@ -262,16 +272,13 @@
       .filter(label => hasVotes(labels[label]));
     if (!triggerVotes.length) return;
     return html`<h3 class="metadata-title heading-3">Trigger Votes</h3>
-      <section class="votes">
+      <section class="trigger-votes">
         ${triggerVotes.map(
-          label => html`${label}:
-            <gr-label-info
-              .change="${this.change}"
-              .account="${this.account}"
-              .mutable="${this.mutable ?? false}"
-              label="${label}"
+          label =>
+            html`<gr-trigger-vote
+              .label="${label}"
               .labelInfo="${labels[label]}"
-            ></gr-label-info>`
+            ></gr-trigger-vote>`
         )}
       </section>`;
   }
@@ -320,8 +327,61 @@
   }
 }
 
+@customElement('gr-trigger-vote')
+export class GrTriggerVote extends LitElement {
+  @property()
+  label?: string;
+
+  @property({type: Object})
+  labelInfo?: LabelInfo;
+
+  static override get styles() {
+    return css`
+      :host {
+        display: block;
+      }
+      .container {
+        box-sizing: border-box;
+        border: 1px solid var(--border-color);
+        border-radius: calc(var(--border-radius) + 2px);
+        background-color: var(--background-color-primary);
+        display: flex;
+        padding: 0;
+        padding-left: var(--spacing-s);
+        padding-right: var(--spacing-xxs);
+        align-items: center;
+      }
+      .label {
+        padding-right: var(--spacing-s);
+        font-weight: var(--font-weight-bold);
+      }
+      gr-vote-chip {
+        --gr-vote-chip-width: 14px;
+        --gr-vote-chip-height: 14px;
+        margin-right: 0px;
+      }
+    `;
+  }
+
+  override render() {
+    const uniqueApprovals = getAllUniqueApprovals(this.labelInfo);
+    return html`
+      <div class="container">
+        <span class="label">${this.label}</span>
+        ${uniqueApprovals.map(
+          approvalInfo => html`<gr-vote-chip
+            .vote="${approvalInfo}"
+            .label="${this.labelInfo}"
+          ></gr-vote-chip>`
+        )}
+      </div>
+    `;
+  }
+}
+
 declare global {
   interface HTMLElementTagNameMap {
     'gr-submit-requirements': GrSubmitRequirements;
+    'gr-trigger-vote': GrTriggerVote;
   }
 }
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index e1d8932..a643c18 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -469,6 +469,11 @@
         .testing:hover * {
           visibility: visible;
         }
+        .zero {
+          padding: var(--spacing-m) 0;
+          color: var(--primary-text-color);
+          margin-top: var(--spacing-m);
+        }
         .login,
         .error {
           padding: var(--spacing-m);
@@ -528,7 +533,7 @@
         <div class="flex-space"></div>
         ${this.renderTitleButtons()} ${this.renderCollapseButton()}
       </h2>
-      ${this.renderErrors()} ${this.renderSignIn()}
+      ${this.renderErrors()} ${this.renderSignIn()} ${this.renderZeroState()}
       <input
         id="filterInput"
         type="text"
@@ -542,6 +547,11 @@
     `;
   }
 
+  private renderZeroState() {
+    if (this.runs.length > 0) return;
+    return html`<div class="zero">No Check Run to show</div>`;
+  }
+
   private renderErrors() {
     return Object.entries(this.errorMessages).map(
       ([plugin, message]) =>
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
index 2dec8d2..c51988e 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
@@ -26,15 +26,16 @@
 suite('gr-error-dialog tests', () => {
   let element: GrErrorDialog;
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await flush();
   });
 
   test('dismiss tap fires event', async () => {
     const dismissCalled = mockPromise();
     element.addEventListener('dismiss', () => dismissCalled.resolve());
     MockInteractions.tap(
-      (queryAndAssert(element, '#dialog') as GrDialog).$.confirm
+      (queryAndAssert(element, '#dialog') as GrDialog).confirmButton!
     );
     await dismissCalled;
   });
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
index 2e1fc21..0ba68e2 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
@@ -76,33 +76,33 @@
       assert.isTrue(element._isValidPath('test.js'));
     });
 
-    test('open', () => {
+    test('open', async () => {
       assert.isFalse(hideDialogStub.called);
       MockInteractions.tap(queryAndAssert(element, '#open'));
       element.patchNum = 1 as PatchSetNum;
-      return showDialogSpy.lastCall.returnValue.then(() => {
-        assert.isTrue(hideDialogStub.called);
-        assert.isTrue(element.$.openDialog.disabled);
-        assert.isFalse(queryStub.called);
-        // Setup _focused manually - in headless mode Chrome sometimes don't
-        // setup focus. flush and/or flushAsynchronousOperations don't help
-        openAutoComplete._focused = true;
-        openAutoComplete.noDebounce = true;
-        openAutoComplete.text = 'src/test.cpp';
-        assert.isTrue(queryStub.called);
-        assert.isFalse(element.$.openDialog.disabled);
-        MockInteractions.tap(
-          queryAndAssert(element.$.openDialog, 'gr-button[primary]')
-        );
-        assert.isTrue(editDiffStub.called);
-        assert.isTrue(navStub.called);
-        assert.deepEqual(editDiffStub.lastCall.args, [
-          element.change,
-          'src/test.cpp',
-          element.patchNum,
-        ]);
-        assert.isTrue(closeDialogSpy.called);
-      });
+      await showDialogSpy.lastCall.returnValue;
+      assert.isTrue(hideDialogStub.called);
+      assert.isTrue(element.$.openDialog.disabled);
+      assert.isFalse(queryStub.called);
+      // Setup _focused manually - in headless mode Chrome sometimes don't
+      // setup focus. flush and/or flushAsynchronousOperations don't help
+      openAutoComplete._focused = true;
+      openAutoComplete.noDebounce = true;
+      openAutoComplete.text = 'src/test.cpp';
+      await flush();
+      assert.isTrue(queryStub.called);
+      assert.isFalse(element.$.openDialog.disabled);
+      MockInteractions.tap(
+        queryAndAssert(element.$.openDialog, 'gr-button[primary]')
+      );
+      assert.isTrue(editDiffStub.called);
+      assert.isTrue(navStub.called);
+      assert.deepEqual(editDiffStub.lastCall.args, [
+        element.change,
+        'src/test.cpp',
+        element.patchNum,
+      ]);
+      assert.isTrue(closeDialogSpy.called);
     });
 
     test('cancel', () => {
@@ -133,59 +133,56 @@
         element.$.deleteDialog!.querySelector('gr-autocomplete')!;
     });
 
-    test('delete', () => {
+    test('delete', async () => {
       deleteStub.returns(Promise.resolve({ok: true}));
       MockInteractions.tap(queryAndAssert(element, '#delete'));
-      return showDialogSpy.lastCall.returnValue.then(() => {
-        assert.isTrue(element.$.deleteDialog.disabled);
-        assert.isFalse(queryStub.called);
-        // Setup _focused manually - in headless mode Chrome sometimes don't
-        // setup focus. flush and/or flushAsynchronousOperations don't help
-        deleteAutocomplete._focused = true;
-        deleteAutocomplete.noDebounce = true;
-        deleteAutocomplete.text = 'src/test.cpp';
-        assert.isTrue(queryStub.called);
-        assert.isFalse(element.$.deleteDialog.disabled);
-        MockInteractions.tap(
-          queryAndAssert(element.$.deleteDialog, 'gr-button[primary]')
-        );
-        flush();
+      await showDialogSpy.lastCall.returnValue;
+      assert.isTrue(element.$.deleteDialog.disabled);
+      assert.isFalse(queryStub.called);
+      // Setup _focused manually - in headless mode Chrome sometimes don't
+      // setup focus. flush and/or flushAsynchronousOperations don't help
+      deleteAutocomplete._focused = true;
+      deleteAutocomplete.noDebounce = true;
+      deleteAutocomplete.text = 'src/test.cpp';
+      await flush();
+      assert.isTrue(queryStub.called);
+      assert.isFalse(element.$.deleteDialog.disabled);
+      MockInteractions.tap(
+        queryAndAssert(element.$.deleteDialog, 'gr-button[primary]')
+      );
+      await flush();
 
-        assert.isTrue(deleteStub.called);
-
-        return deleteStub.lastCall.returnValue.then(() => {
-          assert.equal(element._path, '');
-          assert.isTrue(navStub.called);
-          assert.isTrue(closeDialogSpy.called);
-        });
-      });
+      assert.isTrue(deleteStub.called);
+      await deleteStub.lastCall.returnValue;
+      assert.equal(element._path, '');
+      assert.isTrue(navStub.called);
+      assert.isTrue(closeDialogSpy.called);
     });
 
-    test('delete fails', () => {
+    test('delete fails', async () => {
       deleteStub.returns(Promise.resolve({ok: false}));
       MockInteractions.tap(queryAndAssert(element, '#delete'));
-      return showDialogSpy.lastCall.returnValue.then(() => {
-        assert.isTrue(element.$.deleteDialog.disabled);
-        assert.isFalse(queryStub.called);
-        // Setup _focused manually - in headless mode Chrome sometimes don't
-        // setup focus. flush and/or flushAsynchronousOperations don't help
-        deleteAutocomplete._focused = true;
-        deleteAutocomplete.noDebounce = true;
-        deleteAutocomplete.text = 'src/test.cpp';
-        assert.isTrue(queryStub.called);
-        assert.isFalse(element.$.deleteDialog.disabled);
-        MockInteractions.tap(
-          queryAndAssert(element.$.deleteDialog, 'gr-button[primary]')
-        );
-        flush();
+      await showDialogSpy.lastCall.returnValue;
+      assert.isTrue(element.$.deleteDialog.disabled);
+      assert.isFalse(queryStub.called);
+      // Setup _focused manually - in headless mode Chrome sometimes don't
+      // setup focus. flush and/or flushAsynchronousOperations don't help
+      deleteAutocomplete._focused = true;
+      deleteAutocomplete.noDebounce = true;
+      deleteAutocomplete.text = 'src/test.cpp';
+      await flush();
+      assert.isTrue(queryStub.called);
+      assert.isFalse(element.$.deleteDialog.disabled);
+      MockInteractions.tap(
+        queryAndAssert(element.$.deleteDialog, 'gr-button[primary]')
+      );
+      await flush();
 
-        assert.isTrue(deleteStub.called);
+      assert.isTrue(deleteStub.called);
 
-        return deleteStub.lastCall.returnValue.then(() => {
-          assert.isFalse(navStub.called);
-          assert.isFalse(closeDialogSpy.called);
-        });
-      });
+      await deleteStub.lastCall.returnValue;
+      assert.isFalse(navStub.called);
+      assert.isFalse(closeDialogSpy.called);
     });
 
     test('cancel', () => {
@@ -217,67 +214,66 @@
         element.$.renameDialog!.querySelector('gr-autocomplete')!;
     });
 
-    test('rename', () => {
+    test('rename', async () => {
       renameStub.returns(Promise.resolve({ok: true}));
       MockInteractions.tap(queryAndAssert(element, '#rename'));
-      return showDialogSpy.lastCall.returnValue.then(() => {
-        assert.isTrue(element.$.renameDialog.disabled);
-        assert.isFalse(queryStub.called);
-        // Setup _focused manually - in headless mode Chrome sometimes don't
-        // setup focus. flush and/or flushAsynchronousOperations don't help
-        renameAutocomplete._focused = true;
-        renameAutocomplete.noDebounce = true;
-        renameAutocomplete.text = 'src/test.cpp';
-        assert.isTrue(queryStub.called);
-        assert.isTrue(element.$.renameDialog.disabled);
+      await showDialogSpy.lastCall.returnValue;
+      assert.isTrue(element.$.renameDialog.disabled);
+      assert.isFalse(queryStub.called);
+      // Setup _focused manually - in headless mode Chrome sometimes don't
+      // setup focus. flush and/or flushAsynchronousOperations don't help
+      renameAutocomplete._focused = true;
+      renameAutocomplete.noDebounce = true;
+      renameAutocomplete.text = 'src/test.cpp';
+      await flush();
+      assert.isTrue(queryStub.called);
+      assert.isTrue(element.$.renameDialog.disabled);
 
-        element.$.newPathIronInput.bindValue = 'src/test.newPath';
+      element.$.newPathIronInput.bindValue = 'src/test.newPath';
+      await flush();
 
-        assert.isFalse(element.$.renameDialog.disabled);
-        MockInteractions.tap(
-          queryAndAssert(element.$.renameDialog, 'gr-button[primary]')
-        );
-        flush();
+      assert.isFalse(element.$.renameDialog.disabled);
+      MockInteractions.tap(
+        queryAndAssert(element.$.renameDialog, 'gr-button[primary]')
+      );
+      await flush();
+      assert.isTrue(renameStub.called);
 
-        assert.isTrue(renameStub.called);
-
-        return renameStub.lastCall.returnValue.then(() => {
-          assert.equal(element._path, '');
-          assert.isTrue(navStub.called);
-          assert.isTrue(closeDialogSpy.called);
-        });
-      });
+      await renameStub.lastCall.returnValue;
+      assert.equal(element._path, '');
+      assert.isTrue(navStub.called);
+      assert.isTrue(closeDialogSpy.called);
     });
 
-    test('rename fails', () => {
+    test('rename fails', async () => {
       renameStub.returns(Promise.resolve({ok: false}));
       MockInteractions.tap(queryAndAssert(element, '#rename'));
-      return showDialogSpy.lastCall.returnValue.then(() => {
-        assert.isTrue(element.$.renameDialog.disabled);
-        assert.isFalse(queryStub.called);
-        // Setup _focused manually - in headless mode Chrome sometimes don't
-        // setup focus. flush and/or flushAsynchronousOperations don't help
-        renameAutocomplete._focused = true;
-        renameAutocomplete.noDebounce = true;
-        renameAutocomplete.text = 'src/test.cpp';
-        assert.isTrue(queryStub.called);
-        assert.isTrue(element.$.renameDialog.disabled);
+      await showDialogSpy.lastCall.returnValue;
+      assert.isTrue(element.$.renameDialog.disabled);
+      assert.isFalse(queryStub.called);
+      // Setup _focused manually - in headless mode Chrome sometimes don't
+      // setup focus. flush and/or flushAsynchronousOperations don't help
+      renameAutocomplete._focused = true;
+      renameAutocomplete.noDebounce = true;
+      renameAutocomplete.text = 'src/test.cpp';
+      await flush();
+      assert.isTrue(queryStub.called);
+      assert.isTrue(element.$.renameDialog.disabled);
 
-        element.$.newPathIronInput.bindValue = 'src/test.newPath';
+      element.$.newPathIronInput.bindValue = 'src/test.newPath';
+      await flush();
 
-        assert.isFalse(element.$.renameDialog.disabled);
-        MockInteractions.tap(
-          queryAndAssert(element.$.renameDialog, 'gr-button[primary]')
-        );
-        flush();
+      assert.isFalse(element.$.renameDialog.disabled);
+      MockInteractions.tap(
+        queryAndAssert(element.$.renameDialog, 'gr-button[primary]')
+      );
+      await flush();
 
-        assert.isTrue(renameStub.called);
+      assert.isTrue(renameStub.called);
 
-        return renameStub.lastCall.returnValue.then(() => {
-          assert.isFalse(navStub.called);
-          assert.isFalse(closeDialogSpy.called);
-        });
-      });
+      await renameStub.lastCall.returnValue;
+      assert.isFalse(navStub.called);
+      assert.isFalse(closeDialogSpy.called);
     });
 
     test('cancel', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
index 74e34a0..97ee39e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
@@ -15,12 +15,11 @@
  * limitations under the License.
  */
 import '../gr-button/gr-button';
-import '../../../styles/gr-font-styles';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-dialog_html';
-import {customElement, property, observe} from '@polymer/decorators';
+import {customElement, property, query} from 'lit/decorators';
 import {GrButton} from '../gr-button/gr-button';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {fontStyles} from '../../../styles/gr-font-styles';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -28,18 +27,8 @@
   }
 }
 
-export interface GrDialog {
-  $: {
-    confirm: GrButton;
-  };
-}
-
 @customElement('gr-dialog')
-export class GrDialog extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrDialog extends LitElement {
   /**
    * Fired when the confirm button is pressed.
    *
@@ -52,33 +41,132 @@
    * @event cancel
    */
 
-  @property({type: String})
+  @query('#confirm')
+  confirmButton?: GrButton;
+
+  @property({type: String, attribute: 'confirm-label'})
   confirmLabel = 'Confirm';
 
   // Supplying an empty cancel label will hide the button completely.
-  @property({type: String})
+  @property({type: String, attribute: 'cancel-label'})
   cancelLabel = 'Cancel';
 
   @property({type: Boolean})
   disabled = false;
 
-  @property({type: Boolean})
+  @property({type: Boolean, attribute: 'confirm-on-enter'})
   confirmOnEnter = false;
 
-  @property({type: String})
+  @property({type: String, attribute: 'confirm-tooltip'})
   confirmTooltip?: string;
 
-  override ready() {
-    super.ready();
-    this._ensureAttribute('role', 'dialog');
+  override firstUpdated(changedProperties: PropertyValues) {
+    super.firstUpdated(changedProperties);
+    if (!this.getAttribute('role')) this.setAttribute('role', 'dialog');
   }
 
-  @observe('confirmTooltip')
-  _handleConfirmTooltipUpdate(confirmTooltip?: string) {
-    if (confirmTooltip) {
-      this.$.confirm.setAttribute('has-tooltip', 'true');
+  static override get styles() {
+    return [
+      sharedStyles,
+      fontStyles,
+      css`
+        :host {
+          color: var(--primary-text-color);
+          display: block;
+          max-height: 90vh;
+          overflow: auto;
+        }
+        .container {
+          display: flex;
+          flex-direction: column;
+          max-height: 90vh;
+          padding: var(--spacing-xl);
+        }
+        header {
+          flex-shrink: 0;
+          padding-bottom: var(--spacing-xl);
+        }
+        main {
+          display: flex;
+          flex-shrink: 1;
+          width: 100%;
+          flex: 1;
+          /* IMPORTANT: required for firefox */
+          min-height: 0px;
+        }
+        main .overflow-container {
+          flex: 1;
+          overflow: auto;
+        }
+        footer {
+          display: flex;
+          flex-shrink: 0;
+          justify-content: flex-end;
+          padding-top: var(--spacing-xl);
+        }
+        gr-button {
+          margin-left: var(--spacing-l);
+        }
+        .hidden {
+          display: none;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    // Note that we are using (e: Event) => this._handleKeyDown because the
+    // tests mock out _handleKeydown so the lookup needs to be dynamic, not
+    // bound statically here.
+    return html`
+      <div
+        class="container"
+        @keydown=${(e: KeyboardEvent) => this._handleKeydown(e)}
+      >
+        <header class="heading-3"><slot name="header"></slot></header>
+        <main>
+          <div class="overflow-container">
+            <slot name="main"></slot>
+          </div>
+        </main>
+        <footer>
+          <slot name="footer"></slot>
+          <gr-button
+            id="cancel"
+            class="${this.cancelLabel.length ? '' : 'hidden'}"
+            link
+            @click=${(e: Event) => this.handleCancelTap(e)}
+          >
+            ${this.cancelLabel}
+          </gr-button>
+          <gr-button
+            id="confirm"
+            link
+            primary
+            @click=${(e: Event) => this._handleConfirm(e)}
+            ?disabled=${this.disabled}
+            title=${this.confirmTooltip ?? ''}
+          >
+            ${this.confirmLabel}
+          </gr-button>
+        </footer>
+      </div>
+    `;
+  }
+
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('confirmTooltip')) {
+      this.updateTooltip();
+    }
+  }
+
+  private updateTooltip() {
+    const confirmButton = this.confirmButton;
+    if (!confirmButton) return;
+    if (this.confirmTooltip) {
+      confirmButton.setAttribute('has-tooltip', 'true');
     } else {
-      this.$.confirm.removeAttribute('has-tooltip');
+      confirmButton.removeAttribute('has-tooltip');
     }
   }
 
@@ -97,7 +185,7 @@
     );
   }
 
-  _handleCancelTap(e: Event) {
+  private handleCancelTap(e: Event) {
     e.preventDefault();
     e.stopPropagation();
     this.dispatchEvent(
@@ -115,10 +203,6 @@
   }
 
   resetFocus() {
-    this.$.confirm.focus();
-  }
-
-  _computeCancelClass(cancelLabel: string) {
-    return cancelLabel.length ? '' : 'hidden';
+    this.confirmButton!.focus();
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts
deleted file mode 100644
index a5cf8f1..0000000
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts
+++ /dev/null
@@ -1,94 +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="gr-font-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="shared-styles">
-    :host {
-      color: var(--primary-text-color);
-      display: block;
-      max-height: 90vh;
-      overflow: auto;
-    }
-    .container {
-      display: flex;
-      flex-direction: column;
-      max-height: 90vh;
-      padding: var(--spacing-xl);
-    }
-    header {
-      flex-shrink: 0;
-      padding-bottom: var(--spacing-xl);
-    }
-    main {
-      display: flex;
-      flex-shrink: 1;
-      width: 100%;
-      flex: 1;
-      /* IMPORTANT: required for firefox */
-      min-height: 0px;
-    }
-    main .overflow-container {
-      flex: 1;
-      overflow: auto;
-    }
-    footer {
-      display: flex;
-      flex-shrink: 0;
-      justify-content: flex-end;
-      padding-top: var(--spacing-xl);
-    }
-    gr-button {
-      margin-left: var(--spacing-l);
-    }
-    .hidden {
-      display: none;
-    }
-  </style>
-  <div class="container" on-keydown="_handleKeydown">
-    <header class="heading-3"><slot name="header"></slot></header>
-    <main>
-      <div class="overflow-container">
-        <slot name="main"></slot>
-      </div>
-    </main>
-    <footer>
-      <slot name="footer"></slot>
-      <gr-button
-        id="cancel"
-        class$="[[_computeCancelClass(cancelLabel)]]"
-        link=""
-        on-click="_handleCancelTap"
-      >
-        [[cancelLabel]]
-      </gr-button>
-      <gr-button
-        id="confirm"
-        link=""
-        primary=""
-        on-click="_handleConfirm"
-        disabled="[[disabled]]"
-        title$="[[confirmTooltip]]"
-      >
-        [[confirmLabel]]
-      </gr-button>
-    </footer>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts
index e7b7130..171fc6c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts
@@ -17,6 +17,7 @@
 
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
 import '../../../test/common-test-setup-karma';
+import './gr-dialog';
 import {GrDialog} from './gr-dialog';
 import {isHidden, queryAndAssert} from '../../../test/test-utils';
 
@@ -25,8 +26,9 @@
 suite('gr-dialog tests', () => {
   let element: GrDialog;
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await element.updateComplete;
   });
 
   test('events', () => {
@@ -42,56 +44,59 @@
     assert.equal(cancel.callCount, 1);
   });
 
-  test('confirmOnEnter', () => {
+  test('confirmOnEnter', async () => {
     element.confirmOnEnter = false;
+    await element.updateComplete;
     const handleConfirmStub = sinon.stub(element, '_handleConfirm');
     const handleKeydownSpy = sinon.spy(element, '_handleKeydown');
-    MockInteractions.pressAndReleaseKeyOn(
+    MockInteractions.keyDownOn(
       queryAndAssert(element, 'main'),
       13,
       null,
       'enter'
     );
-    flush();
+    await flush();
 
     assert.isTrue(handleKeydownSpy.called);
     assert.isFalse(handleConfirmStub.called);
 
     element.confirmOnEnter = true;
-    MockInteractions.pressAndReleaseKeyOn(
+    await element.updateComplete;
+
+    MockInteractions.keyDownOn(
       queryAndAssert(element, 'main'),
       13,
       null,
       'enter'
     );
-    flush();
+    await flush();
 
     assert.isTrue(handleConfirmStub.called);
   });
 
   test('resetFocus', () => {
-    const focusStub = sinon.stub(element.$.confirm, 'focus');
+    const focusStub = sinon.stub(element.confirmButton!, 'focus');
     element.resetFocus();
     assert.isTrue(focusStub.calledOnce);
   });
 
   suite('tooltip', () => {
     test('tooltip not added by default', () => {
-      assert.isNull(element.$.confirm.getAttribute('has-tooltip'));
+      assert.isNull(element.confirmButton!.getAttribute('has-tooltip'));
     });
 
-    test('tooltip added if confirm tooltip is passed', () => {
+    test('tooltip added if confirm tooltip is passed', async () => {
       element.confirmTooltip = 'confirm tooltip';
-      flush();
-      assert(element.$.confirm.getAttribute('has-tooltip'));
+      await element.updateComplete;
+      assert(element.confirmButton!.getAttribute('has-tooltip'));
     });
   });
 
-  test('empty cancel label hides cancel btn', () => {
+  test('empty cancel label hides cancel btn', async () => {
     const cancelButton = queryAndAssert(element, '#cancel');
     assert.isFalse(isHidden(cancelButton));
     element.cancelLabel = '';
-    flush();
+    await element.updateComplete;
 
     assert.isTrue(isHidden(cancelButton));
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index 2fd3fb9..3762d8d 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -83,8 +83,8 @@
           display: flex;
           width: var(--gr-vote-chip-width, 16px);
           height: var(--gr-vote-chip-height, 16px);
+          font-size: var(--font-size-small);
           justify-content: center;
-          margin-right: var(--spacing-s);
           padding: 1px;
           border-radius: var(--border-radius);
           line-height: var(--gr-vote-chip-width, 16px);
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index cde9a45..884afd7 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -122,6 +122,17 @@
   return label.all?.filter(x => x._account_id === account._account_id)[0];
 }
 
+export function getAllUniqueApprovals(labelInfo?: LabelInfo) {
+  if (!labelInfo || !isDetailedLabelInfo(labelInfo)) return [];
+  const uniqueApprovals = (labelInfo.all ?? [])
+    .filter(
+      (approvalInfo, index, array) =>
+        index === array.findIndex(other => other.value === approvalInfo.value)
+    )
+    .sort((a, b) => -(a.value ?? 0) + (b.value ?? 0));
+  return uniqueApprovals;
+}
+
 export function hasVotes(labelInfo: LabelInfo): boolean {
   if (isDetailedLabelInfo(labelInfo)) {
     return (labelInfo.all ?? []).some(