Merge "Introduce keyboard shortcuts for toggling attention set status"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index a8d8769..b609643 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -600,8 +600,9 @@
 ----
 
 As a response, two link:#change-info[ChangeInfo] entities are returned
-that describe information added and removed from the `old` change state.
-Only fields that differ between the change's two states are returned.
+that describe information added and removed from the `old` change state, and
+the two link:#change-info[ChangeInfo] entities that generated the diff are
+returned. Only fields that differ between the change's two states are returned.
 
 .Response
 ----
@@ -625,9 +626,55 @@
       "topic": "new-topic"
     },
     "removed": {
-      "updated": "2013-02-20 12:05:34.111000000",
+      "updated": "2013-02-01 09:59:32.126000000",
       "topic": "old-topic"
-    }
+    },
+    "old_change_info": {
+      "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "project": "myProject",
+      "branch": "master",
+      "attention_set": [],
+      "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "subject": "Implementing Feature X",
+      "status": "NEW",
+      "topic": "old-topic",
+      "created": "2013-02-01 09:59:32.126000000",
+      "updated": "2013-02-01 09:59:32.126000000",
+      "mergeable": true,
+      "insertions": 34,
+      "deletions": 101,
+      "_number": 3965,
+      "owner": {
+        "name": "John Doe"
+      }
+    },
+    "new_change_info": {
+      "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "project": "myProject",
+      "branch": "master",
+      "attention_set": [
+        {
+          "account": {
+            "name": "John Doe"
+          },
+         "last_update": "2013-02-21 11:16:36.775000000",
+         "reason": "reviewer or cc replied"
+        }
+      ],
+      "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "subject": "Implementing Feature X",
+      "status": "NEW",
+      "topic": "new-topic",
+      "created": "2013-02-01 09:59:32.126000000",
+      "updated": "2013-02-21 11:16:36.775000000",
+      "mergeable": true,
+      "insertions": 34,
+      "deletions": 101,
+      "_number": 3965,
+      "owner": {
+        "name": "John Doe"
+      }
+    },
   }
 ----
 
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index ba5e323..ad112d3 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -63,9 +63,12 @@
    */
   public static ChangeInfoDifference getDifference(
       ChangeInfo oldChangeInfo, ChangeInfo newChangeInfo) {
-    return ChangeInfoDifference.create(
-        /* added= */ getAdded(oldChangeInfo, newChangeInfo),
-        /* removed= */ getAdded(newChangeInfo, oldChangeInfo));
+    return ChangeInfoDifference.builder()
+        .setOldChangeInfo(oldChangeInfo)
+        .setNewChangeInfo(newChangeInfo)
+        .setAdded(getAdded(oldChangeInfo, newChangeInfo))
+        .setRemoved(getAdded(newChangeInfo, oldChangeInfo))
+        .build();
   }
 
   @SuppressWarnings("unchecked") // reflection is used to construct instances of T
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java b/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java
index 269c673..997c3ee 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java
@@ -20,11 +20,29 @@
 @AutoValue
 public abstract class ChangeInfoDifference {
 
+  public abstract ChangeInfo oldChangeInfo();
+
+  public abstract ChangeInfo newChangeInfo();
+
   public abstract ChangeInfo added();
 
   public abstract ChangeInfo removed();
 
-  public static ChangeInfoDifference create(ChangeInfo added, ChangeInfo removed) {
-    return new AutoValue_ChangeInfoDifference(added, removed);
+  public static Builder builder() {
+    return new AutoValue_ChangeInfoDifference.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder setOldChangeInfo(ChangeInfo oldChangeInfo);
+
+    public abstract Builder setNewChangeInfo(ChangeInfo newChangeInfo);
+
+    public abstract Builder setAdded(ChangeInfo added);
+
+    public abstract Builder setRemoved(ChangeInfo removed);
+
+    public abstract ChangeInfoDifference build();
   }
 }
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index 4352fe8..024e35e 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -58,6 +58,17 @@
   }
 
   @Test
+  public void getDiff_returnsOldAndNewChangeInfos() {
+    ChangeInfo oldChangeInfo = createChangeInfoWithTopic("topic");
+    ChangeInfo newChangeInfo = createChangeInfoWithTopic(oldChangeInfo.topic);
+
+    ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+    assertThat(diff.oldChangeInfo()).isEqualTo(oldChangeInfo);
+    assertThat(diff.newChangeInfo()).isEqualTo(newChangeInfo);
+  }
+
+  @Test
   public void getDiff_givenUnchangedTopic_returnsNullTopics() {
     ChangeInfo oldChangeInfo = createChangeInfoWithTopic("topic");
     ChangeInfo newChangeInfo = createChangeInfoWithTopic(oldChangeInfo.topic);
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 1453fd0..ee579ff 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -53,30 +53,30 @@
 }
 
 /**
+ * Represents a "generic" text range in the code (e.g. text selection)
+ */
+interface TextRange {
+  /** first line of the range (1-based inclusive). */
+  start_line: number;
+  /** first column of the range (in the first line) (1-based inclusive). */
+  start_column: number;
+  /** last line of the range (1-based inclusive). */
+  end_line: number;
+  /** last column of the range (in the end line) (1-based inclusive). */
+  end_column: number;
+}
+
+/**
  * Represents a syntax block in a code (e.g. method, function, class, if-else).
  */
 export declare interface SyntaxBlock {
   /** Name of the block (e.g. name of the method/class)*/
   name: string;
-  /** Where does this block syntatically starts and ends (line number and column).*/
-  range: {
-    /** first line of the block (1-based inclusive). */
-    start_line: number;
-    /**
-     * column of the range start inside the first line (e.g. "{" character ending a function/method)
-     * (1-based inclusive).
-     */
-    start_column: number;
-    /**
-     * last line of the block (1-based inclusive).
-     */
-    end_line: number;
-    /**
-     * column of the block end inside the end line (e.g. "}" character ending a function/method)
-     * (1-based inclusive).
-     */
-    end_column: number;
-  };
+  /**
+   * Where does this block syntatically starts and ends (line number and
+   * column).
+   */
+  range: TextRange;
   /** Sub-blocks of the current syntax block (e.g. methods of a class) */
   children: SyntaxBlock[];
 }
@@ -210,15 +210,22 @@
 }
 
 /**
- * Listens to changes in token highlighting - when a new token starts or stopped being highlighted.
- * Examples:
- * - Token highlighted: ('myFunctionName', 12, [Element]).
- * - Token unhighlighted: (undefined, 0, undefined).
+ * Event details when a token is highlighted.
  */
-export type TokenHighlightedListener = (
-  newHighlight: string | undefined,
-  newLineNumber: number,
-  hoveredElement?: Element
+export declare interface TokenHighlightEventDetails {
+  token: string;
+  element: Element;
+  side: Side;
+  range: TextRange;
+}
+
+/**
+ * Listens to changes in token highlighting - when a new token starts or stopped
+ * being highlighted. undefined is sent if the event is about a clear in
+ * highlighting.
+ */
+export type TokenHighlightListener = (
+  tokenHighlightEvent?: TokenHighlightEventDetails
 ) => void;
 
 export declare interface ImageDiffPreferences {
diff --git a/polygerrit-ui/app/api/embed.ts b/polygerrit-ui/app/api/embed.ts
index fed724e..520aeec 100644
--- a/polygerrit-ui/app/api/embed.ts
+++ b/polygerrit-ui/app/api/embed.ts
@@ -24,7 +24,7 @@
   DiffLayer,
   GrAnnotation,
   GrDiffCursor,
-  TokenHighlightedListener,
+  TokenHighlightListener,
 } from './diff';
 
 declare global {
@@ -35,7 +35,7 @@
       TokenHighlightLayer: {
         new (
           container?: HTMLElement,
-          listener?: TokenHighlightedListener
+          listener?: TokenHighlightListener
         ): DiffLayer;
       };
     };
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
index 941a09a..6605350 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
@@ -17,7 +17,6 @@
 
 import '../../../styles/gr-table-styles';
 import '../../../styles/shared-styles';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../shared/gr-account-link/gr-account-link';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-group-audit-log_html';
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
index 40c2f30..828aa55 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
@@ -43,7 +43,7 @@
       <template is="dom-repeat" items="[[_auditLog]]">
         <tr class="table">
           <td class="date">
-            <gr-date-formatter has-tooltip="" date-str="[[item.date]]">
+            <gr-date-formatter withTooltip date-str="[[item.date]]">
             </gr-date-formatter>
           </td>
           <td class="type">[[itemType(item.type)]]</td>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
index d26d14c..b91b04b 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
@@ -140,7 +140,7 @@
     'https://test/site/group/url');
   });
 
-  test('save members correctly', () => {
+  test('save members correctly', async () => {
     element._groupOwner = true;
 
     const memberName = 'test-admin';
@@ -155,6 +155,7 @@
     element.$.groupMemberSearchInput.text = memberName;
     element.$.groupMemberSearchInput.value = 1234;
 
+    await flush();
     assert.isFalse(button.hasAttribute('disabled'));
 
     return element._handleSavingGroupMember().then(() => {
@@ -165,7 +166,7 @@
     });
   });
 
-  test('save included groups correctly', () => {
+  test('save included groups correctly', async () => {
     element._groupOwner = true;
 
     const includedGroupName = 'testName';
@@ -179,7 +180,7 @@
 
     element.$.includedGroupSearchInput.text = includedGroupName;
     element.$.includedGroupSearchInput.value = 'testId';
-
+    await flush();
     assert.isFalse(button.hasAttribute('disabled'));
 
     return element._handleSavingIncludedGroups().then(() => {
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
index e492a15..e390ac5 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
@@ -94,6 +94,7 @@
 
     element.$.groupNameInput.text = groupName2;
 
+    await flush();
     assert.isFalse(button.hasAttribute('disabled'));
     assert.isTrue(element.$.groupName.classList.contains('edited'));
 
@@ -122,6 +123,7 @@
 
     element.$.groupOwnerInput.text = 'testId2';
 
+    await flush();
     assert.isFalse(button.hasAttribute('disabled'));
     assert.isTrue(element.$.groupOwner.classList.contains('edited'));
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
index 4f66f0d..429a6d6 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
@@ -145,10 +145,7 @@
             <td class$="tagger [[_hideIfBranch(detailType)]]">
               <div class$="tagger [[_computeHideTagger(item.tagger)]]">
                 <gr-account-link account="[[item.tagger]]"> </gr-account-link>
-                (<gr-date-formatter
-                  has-tooltip=""
-                  date-str="[[item.tagger.date]]"
-                >
+                (<gr-date-formatter withTooltip date-str="[[item.tagger.date]]">
                 </gr-date-formatter
                 >)
               </div>
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 dad451b..f0d9268 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
@@ -253,7 +253,7 @@
     hidden$="[[_computeIsColumnHidden('Updated', visibleChangeTableColumns)]]"
   >
     <gr-date-formatter
-      has-tooltip=""
+      withTooltip
       date-str="[[_formatDate(change.updated)]]"
     ></gr-date-formatter>
   </td>
@@ -262,7 +262,7 @@
     hidden$="[[_computeIsColumnHidden('Submitted', visibleChangeTableColumns)]]"
   >
     <gr-date-formatter
-      has-tooltip=""
+      withTooltip
       date-str="[[_formatDate(change.submitted)]]"
     ></gr-date-formatter>
   </td>
@@ -271,9 +271,9 @@
     hidden$="[[_computeIsColumnHidden('Waiting', visibleChangeTableColumns)]]"
   >
     <gr-date-formatter
-      has-tooltip=""
-      force-relative=""
-      relative-option-no-ago=""
+      withTooltip
+      forceRelative
+      relativeOptionNoAge
       date-str="[[_computeWaiting(account, change)]]"
     ></gr-date-formatter>
   </td>
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
index beadea3..9242a58 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {RepoName} from '../../../types/common';
 import {WebLinkInfo} from '../../../types/diff';
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
index 614339a..b5f55ca 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
@@ -84,24 +84,27 @@
       hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]"
     >
       <template is="dom-repeat" items="[[_topLevelPrimaryActions]]" as="action">
-        <gr-button
-          link=""
+        <gr-tooltip-content
           title$="[[action.title]]"
           has-tooltip="[[_computeHasTooltip(action.title)]]"
           position-below="true"
-          data-action-key$="[[action.__key]]"
-          class$="[[action.__key]]"
-          data-action-type$="[[action.__type]]"
-          data-label$="[[action.label]]"
-          disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
-          on-click="_handleActionTap"
         >
-          <iron-icon
-            class$="[[_computeHasIcon(action)]]"
-            icon$="gr-icons:[[action.icon]]"
-          ></iron-icon>
-          [[action.label]]
-        </gr-button>
+          <gr-button
+            link=""
+            data-action-key$="[[action.__key]]"
+            class$="[[action.__key]]"
+            data-action-type$="[[action.__type]]"
+            data-label$="[[action.label]]"
+            disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
+            on-click="_handleActionTap"
+          >
+            <iron-icon
+              class$="[[_computeHasIcon(action)]]"
+              icon$="gr-icons:[[action.icon]]"
+            ></iron-icon>
+            [[action.label]]
+          </gr-button>
+        </gr-tooltip-content>
       </template>
     </section>
     <section
@@ -113,24 +116,27 @@
         items="[[_topLevelSecondaryActions]]"
         as="action"
       >
-        <gr-button
-          link=""
+        <gr-tooltip-content
           title$="[[action.title]]"
           has-tooltip="[[_computeHasTooltip(action.title)]]"
           position-below="true"
-          data-action-key$="[[action.__key]]"
-          class$="[[action.__key]]"
-          data-action-type$="[[action.__type]]"
-          data-label$="[[action.label]]"
-          disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
-          on-click="_handleActionTap"
         >
-          <iron-icon
-            class$="[[_computeHasIcon(action)]]"
-            icon$="gr-icons:[[action.icon]]"
-          ></iron-icon>
-          [[action.label]]
-        </gr-button>
+          <gr-button
+            link=""
+            data-action-key$="[[action.__key]]"
+            class$="[[action.__key]]"
+            data-action-type$="[[action.__type]]"
+            data-label$="[[action.label]]"
+            disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
+            on-click="_handleActionTap"
+          >
+            <iron-icon
+              class$="[[_computeHasIcon(action)]]"
+              icon$="gr-icons:[[action.icon]]"
+            ></iron-icon>
+            [[action.label]]
+          </gr-button>
+        </gr-tooltip-content>
       </template>
     </section>
     <gr-button hidden$="[[!_loading]]" disabled=""
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index c080345..c105aaa 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -134,9 +134,9 @@
         <span class="title">Submitted</span>
         <span class="value">
           <gr-date-formatter
-            has-tooltip=""
+            withTooltip
             date-str="[[change.submitted]]"
-            show-yesterday=""
+            showYesterday=""
           ></gr-date-formatter>
         </span>
       </section>
@@ -154,9 +154,9 @@
       </span>
       <span class="value">
         <gr-date-formatter
-          has-tooltip=""
+          withTooltip
           date-str="[[change.updated]]"
-          show-yesterday=""
+          showYesterday
         ></gr-date-formatter>
       </span>
     </section>
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 7bf92b2..3396e13 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
@@ -24,7 +24,6 @@
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-change-star/gr-change-star';
 import '../../shared/gr-change-status/gr-change-status';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../shared/gr-editable-content/gr-editable-content';
 import '../../shared/gr-linked-text/gr-linked-text';
 import '../../shared/gr-overlay/gr-overlay';
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 ff078f0..1256cc1 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
@@ -115,10 +115,10 @@
         current_revision: 'a',
       },
     ];
-    setup(() => {
+    setup(async () => {
       element.updateChanges(changes);
       element._cherryPickType = CHERRY_PICK_TYPES.TOPIC;
-      flush();
+      await flush();
     });
 
     test('cherry pick topic submit', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index bceee27..91f7e46 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -180,50 +180,61 @@
           hidden$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]"
           hidden=""
         >
-          <gr-button
-            link=""
+          <gr-tooltip-content
             has-tooltip=""
             title="Diff preferences"
-            class="prefsButton desktop"
-            on-click="_handlePrefsTap"
-            ><iron-icon icon="gr-icons:settings"></iron-icon
-          ></gr-button>
+          >
+            <gr-button
+              link=""
+
+              class="prefsButton desktop"
+              on-click="_handlePrefsTap"
+              ><iron-icon icon="gr-icons:settings"></iron-icon
+            ></gr-button>
+          </gr-tooltip-content>
         </span>
         <span class="separator"></span>
       </div>
       <span class="downloadContainer desktop">
-        <gr-button
-          link=""
-          class="download"
-          title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
-                ShortcutSection.ACTIONS)]]"
+        <gr-tooltip-content
           has-tooltip=""
-          on-click="_handleDownloadTap"
-          >Download</gr-button
+          title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
+                   ShortcutSection.ACTIONS)]]"
         >
+          <gr-button
+            link=""
+            class="download"
+            on-click="_handleDownloadTap"
+            >Download</gr-button
+          >
+        </gr-tooltip-content>
       </span>
       <template
         is="dom-if"
         if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]"
       >
-        <gr-button
-          id="expandBtn"
-          link=""
-          title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
-                ShortcutSection.FILE_LIST)]]"
-          has-tooltip=""
-          on-click="_expandAllDiffs"
-          >Expand All</gr-button
-        >
-        <gr-button
-          id="collapseBtn"
-          link=""
-          on-click="_collapseAllDiffs"
-          title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
-          ShortcutSection.FILE_LIST)]]"
-          has-tooltip=""
-          >Collapse All</gr-button
-        >
+        <gr-tooltip-content
+            has-tooltip=""
+            title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
+                  ShortcutSection.FILE_LIST)]]">
+          <gr-button
+            id="expandBtn"
+            link=""
+
+            on-click="_expandAllDiffs"
+            >Expand All</gr-button
+          >
+        <gr-tooltip-content
+            has-tooltip=""
+            title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
+                  ShortcutSection.FILE_LIST)]]">
+          <gr-button
+            id="collapseBtn"
+            link=""
+            on-click="_collapseAllDiffs"
+            >Collapse All</gr-button
+          >
+        </gr-tooltip-content>
       </template>
       <template
         is="dom-if"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index 5def25a..4acf245 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
 import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-file-list.js';
 import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api.js';
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
index f9d21d9..618403c 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
@@ -53,25 +53,27 @@
       );
       padding: 0 var(--spacing-m);
     }
-    gr-button.iron-selected[vote='max'] {
+    gr-tooltip-content.iron-selected > gr-button[vote='max'] {
       --button-background-color: var(--vote-color-approved);
     }
-    gr-button.iron-selected[vote='positive'] {
+    gr-tooltip-content.iron-selected > gr-buttonvote='positive'] {
       --button-background-color: var(--vote-color-recommended);
     }
-    gr-button.iron-selected[vote='min'] {
+    gr-tooltip-content.iron-selected > gr-button[vote='min'] {
       --button-background-color: var(--vote-color-rejected);
     }
-    gr-button.iron-selected[vote='negative'] {
+    gr-tooltip-content.iron-selected > gr-button[vote='negative'] {
       --button-background-color: var(--vote-color-disliked);
     }
-    gr-button.iron-selected[vote='neutral'] {
+    gr-tooltip-content.iron-selected > gr-button[vote='neutral'] {
       --button-background-color: var(--vote-color-neutral);
     }
-    gr-button.iron-selected[vote='positive']::part(paper-button) {
+    gr-tooltip-content.iron-selected
+      > gr-button[vote='positive']::part(paper-button) {
       border-color: var(--vote-outline-recommended);
     }
-    gr-button.iron-selected[vote='negative']::part(paper-button) {
+    gr-tooltip-content.iron-selected
+      > gr-button[vote='negative']::part(paper-button) {
       border-color: var(--vote-outline-disliked);
     }
     .placeholder {
@@ -116,17 +118,20 @@
       aria-labelledby="labelName"
     >
       <template is="dom-repeat" items="[[_items]]" as="value">
-        <gr-button
-          role="radio"
-          vote$="[[_computeVoteAttribute(value, index, _items.length)]]"
-          vote-chip
+        <gr-tooltip-content
           has-tooltip=""
+          title$="[[_computeLabelValueTitle(labels, label.name, value)]]"
           data-name$="[[label.name]]"
           data-value$="[[value]]"
-          title$="[[_computeLabelValueTitle(labels, label.name, value)]]"
         >
-          [[value]]</gr-button
-        >
+          <gr-button
+            role="radio"
+            vote="[[_computeVoteAttribute(value, index, _items.length)]]"
+            voteChip
+          >
+            [[value]]
+          </gr-button>
+        </gr-tooltip-content>
       </template>
     </iron-selector>
     <template
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
index e7ff236..4e66b4e 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
@@ -97,14 +97,14 @@
     }
   }
 
-  test('label picker', () => {
+  test('label picker', async () => {
     const labelsChangedHandler = sinon.stub();
     element.addEventListener('labels-changed', labelsChangedHandler);
     assert.ok(element.$.labelSelector);
     MockInteractions.tap(element.shadowRoot
         .querySelector(
-            'gr-button[data-value="-1"]'));
-    flush();
+            'gr-tooltip-content[data-value="-1"] > gr-button'));
+    await flush();
     assert.strictEqual(element.selectedValue, '-1');
     assert.strictEqual(element.selectedItem
         .textContent.trim(), '-1');
@@ -160,24 +160,28 @@
     checkAriaCheckedValid();
   });
 
-  test('do not display tooltips on touch devices', () => {
-    const verifiedBtn = element.shadowRoot
-        .querySelector(
-            'iron-selector > gr-button[data-value="-1"]');
+  test('do not display tooltips on touch devices', async () => {
+    const verifiedTooltip = element.shadowRoot
+        .querySelector('iron-selector > gr-tooltip-content');
 
     // On touch devices, tooltips should not be shown.
-    verifiedBtn._isTouchDevice = true;
-    verifiedBtn._handleShowTooltip();
-    assert.isNotOk(verifiedBtn._tooltip);
-    verifiedBtn._handleHideTooltip();
-    assert.isNotOk(verifiedBtn._tooltip);
+    verifiedTooltip._isTouchDevice = true;
+    await flush();
+    verifiedTooltip._handleShowTooltip();
+    await flush();
+    assert.isNotOk(verifiedTooltip._tooltip);
+    verifiedTooltip._handleHideTooltip();
+    await flush();
+    assert.isNotOk(verifiedTooltip._tooltip);
 
     // On other devices, tooltips should be shown.
-    verifiedBtn._isTouchDevice = false;
-    verifiedBtn._handleShowTooltip();
-    assert.isOk(verifiedBtn._tooltip);
-    verifiedBtn._handleHideTooltip();
-    assert.isNotOk(verifiedBtn._tooltip);
+    verifiedTooltip._isTouchDevice = false;
+    verifiedTooltip._handleShowTooltip();
+    await flush();
+    assert.isOk(verifiedTooltip._tooltip);
+    verifiedTooltip._handleHideTooltip();
+    await flush();
+    assert.isNotOk(verifiedTooltip._tooltip);
   });
 
   test('_computeLabelValue', () => {
@@ -209,7 +213,7 @@
         'Code-Review'), []);
   });
 
-  test('changes in label score are reflected in the DOM', () => {
+  test('changes in label score are reflected in the DOM', async () => {
     element.labels = {
       'Code-Review': {
         values: {
@@ -232,9 +236,10 @@
         default_value: 0,
       },
     };
+    await flush();
     const selector = element.$.labelSelector;
     element.set('label', {name: 'Verified', value: ' 0'});
-    flush();
+    await flush();
     assert.strictEqual(selector.selected, ' 0');
     assert.strictEqual(
         element.$.selectedValueLabel.textContent.trim(), 'No score');
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
index cfecf91..f529464 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
@@ -85,7 +85,7 @@
     await flush();
   });
 
-  test('get and set label scores', () => {
+  test('get and set label scores', async () => {
     for (const label of Object.keys(element.permittedLabels!)) {
       const row = queryAndAssert<GrLabelScoreRow>(
         element,
@@ -93,6 +93,7 @@
       );
       row.setSelectedValue('-1');
     }
+    await flush();
     assert.deepEqual(element.getLabelValues(), {
       'Code-Review': -1,
       Verified: -1,
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index ddbd22e..c9680ef 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -325,8 +325,8 @@
         <template is="dom-if" if="[[!message.id]]">
           <span class="date">
             <gr-date-formatter
-              has-tooltip=""
-              show-date-and-time=""
+              withTooltip
+              showDateAndTime
               date-str="[[message.date]]"
             ></gr-date-formatter>
           </span>
@@ -334,8 +334,8 @@
         <template is="dom-if" if="[[message.id]]">
           <span class="date" on-click="_handleAnchorClick">
             <gr-date-formatter
-              has-tooltip=""
-              show-date-and-time=""
+              withTooltip
+              showDateAndTime
               date-str="[[message.date]]"
             ></gr-date-formatter>
           </span>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
index 933cb821..b8c9319 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
@@ -127,7 +127,7 @@
     const labelScoreRows = element.getLabelScores().shadowRoot
         .querySelector('gr-label-score-row[name="Code-Review"]');
     const selectedBtn = labelScoreRows.shadowRoot
-        .querySelector('gr-button[data-value="+1"].iron-selected');
+        .querySelector('gr-tooltip-content[data-value="+1"] > gr-button');
     assert.isOk(selectedBtn);
   });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index b571985..3fedcd9 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -441,23 +441,26 @@
                 ></gr-account-label>
               </template>
             </template>
-            <gr-button
-              class="edit-attention-button"
-              on-click="_handleAttentionModify"
-              disabled="[[_sendDisabled]]"
-              link=""
-              position-below=""
-              data-label="Edit"
-              data-action-type="change"
-              data-action-key="edit"
+            <gr-tooltip-content
               has-tooltip=""
               title="[[_computeAttentionButtonTitle(_sendDisabled)]]"
-              role="button"
-              tabindex="0"
             >
-              <iron-icon icon="gr-icons:edit"></iron-icon>
-              Modify
-            </gr-button>
+              <gr-button
+                class="edit-attention-button"
+                on-click="_handleAttentionModify"
+                disabled="[[_sendDisabled]]"
+                link=""
+                position-below=""
+                data-label="Edit"
+                data-action-type="change"
+                data-action-key="edit"
+                role="button"
+                tabindex="0"
+              >
+                <iron-icon icon="gr-icons:edit"></iron-icon>
+                Modify
+              </gr-button>
+            </gr-tooltip-content>
           </div>
           <div>
             <a
@@ -612,26 +615,32 @@
             <!-- Use 'Send' here as the change may only about reviewers / ccs
                 and when this button is visible, the next button will always
                 be 'Start review' -->
-            <gr-button
-              link=""
-              disabled="[[_isState(knownLatestState, 'not-latest')]]"
-              class="action save"
+            <gr-tooltip-content
               has-tooltip=""
-              title="[[_saveTooltip]]"
-              on-click="_saveClickHandler"
-              >Send As WIP</gr-button
+              title$="[[_saveTooltip]]"
             >
+              <gr-button
+                link=""
+                disabled="[[_isState(knownLatestState, 'not-latest')]]"
+                class="action save"
+                on-click="_saveClickHandler"
+                >Send As WIP</gr-button
+              >
+            </gr-tooltip-content>
           </template>
-          <gr-button
-            id="sendButton"
-            primary=""
-            disabled="[[_sendDisabled]]"
-            class="action send"
+          <gr-tooltip-content
             has-tooltip=""
             title$="[[_computeSendButtonTooltip(canBeStarted, _commentEditing)]]"
-            on-click="_sendTapHandler"
-            >[[_sendButtonLabel]]</gr-button
           >
+            <gr-button
+              id="sendButton"
+              primary=""
+              disabled="[[_sendDisabled]]"
+              class="action send"
+              on-click="_sendTapHandler"
+              >[[_sendButtonLabel]]
+            </gr-button>
+          </gr-tooltip-content>
         </div>
       </section>
     </div>
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 e3bbd9e..e57ffc7 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
@@ -116,7 +116,7 @@
     return {id: `${lastId++}` as GroupId};
   };
 
-  setup(() => {
+  setup(async () => {
     changeNum = 42 as NumericChangeId;
     patchNum = 1 as PatchSetNum;
 
@@ -168,7 +168,7 @@
     //     .returns(Promise.resolve({isLatest: true}));
 
     // Allow the elements created by dom-repeat to be stamped.
-    flush();
+    await flush();
   });
 
   function stubSaveReview(
@@ -216,6 +216,7 @@
     // which the dom-repeat elements are stamped.
     await flush();
     tap(queryAndAssert(element, '.send'));
+    await flush();
 
     const review = await saveReviewPromise;
     assert.deepEqual(review, {
@@ -1063,6 +1064,7 @@
     const label = 'Verified';
     const value = '+1';
     element.setLabelValue(label, value);
+    await flush();
 
     const labels = (
       queryAndAssert(element, '#labelScores') as GrLabelScores
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 9e79035..c197599 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -601,42 +601,48 @@
         @click="${() => fireRunSelectionReset(this)}"
         >Unselect All</gr-button
       >
-      <gr-button
-        class="font-normal"
-        link
+      <gr-tooltip-content
         title="${runButtonDisabled
           ? 'Disabled. Unselect checks without a "Run" action to enable the button.'
           : ''}"
         has-tooltip="${runButtonDisabled}"
-        ?disabled="${runButtonDisabled}"
-        @click="${() => {
-          actions.forEach(action => this.checksService.triggerAction(action));
-        }}"
-        >Run Selected</gr-button
       >
+        <gr-button
+          class="font-normal"
+          link
+          ?disabled="${runButtonDisabled}"
+          @click="${() => {
+            actions.forEach(action => this.checksService.triggerAction(action));
+          }}"
+          >Run Selected</gr-button
+        >
+      </gr-tooltip-content>
     `;
   }
 
   private renderCollapseButton() {
     return html`
-      <gr-button
-        link
-        class="expandButton"
-        role="switch"
-        ?aria-checked="${this.collapsed}"
-        aria-label="${this.collapsed
-          ? 'Expand runs panel'
-          : 'Collapse runs panel'}"
+      <gr-tooltip-content
         has-tooltip="true"
         title="${this.collapsed ? 'Expand runs panel' : 'Collapse runs panel'}"
-        @click="${() => (this.collapsed = !this.collapsed)}"
-        ><iron-icon
-          class="expandIcon"
-          icon="${this.collapsed
-            ? 'gr-icons:chevron-right'
-            : 'gr-icons:chevron-left'}"
-        ></iron-icon>
-      </gr-button>
+      >
+        <gr-button
+          link
+          class="expandButton"
+          role="switch"
+          ?aria-checked="${this.collapsed}"
+          aria-label="${this.collapsed
+            ? 'Expand runs panel'
+            : 'Collapse runs panel'}"
+          @click="${() => (this.collapsed = !this.collapsed)}"
+          ><iron-icon
+            class="expandIcon"
+            icon="${this.collapsed
+              ? 'gr-icons:chevron-right'
+              : 'gr-icons:chevron-left'}"
+          ></iron-icon>
+        </gr-button>
+      </gr-tooltip-content>
     `;
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index d3d7615..94d37f5 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -186,6 +186,7 @@
         })
       );
       element._isApplyFixLoading = true;
+      await flush();
       const button = getConfirmButton();
       assert.isTrue(button.hasAttribute('disabled'));
       assert.equal(button.getAttribute('title'), '');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index 480e26c..de7d007 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -15,9 +15,17 @@
  * limitations under the License.
  */
 import {DiffLayer, DiffLayerListener} from '../../../types/types';
-import {GrDiffLine, Side, TokenHighlightedListener} from '../../../api/diff';
+import {GrDiffLine, Side, TokenHighlightListener} from '../../../api/diff';
+import {assertIsDefined} from '../../../utils/common-util';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
 import {debounce, DelayedTask} from '../../../utils/async-util';
+
+import {
+  getLineElByChild,
+  getSideByLineEl,
+  getPreviousContentNodes,
+} from '../gr-diff/gr-diff-utils';
+
 import {
   getLineNumberByChild,
   lineNumberToNumber,
@@ -66,7 +74,7 @@
   private currentHighlight?: string;
 
   /** Trigger when a new token starts or stoped being highlighted.*/
-  private readonly tokenHighlightedListener?: TokenHighlightedListener;
+  private readonly tokenHighlightListener?: TokenHighlightListener;
 
   /**
    * The line of the currently highlighted token. We store this in order to
@@ -100,9 +108,9 @@
 
   constructor(
     container: HTMLElement = document.documentElement,
-    tokenHighlightedListener?: TokenHighlightedListener
+    tokenHighlightListener?: TokenHighlightListener
   ) {
-    this.tokenHighlightedListener = tokenHighlightedListener;
+    this.tokenHighlightListener = tokenHighlightListener;
     container.addEventListener('click', e => {
       this.handleContainerClick(e);
     });
@@ -260,18 +268,42 @@
     const oldLineNumber = this.currentHighlightLineNumber;
     this.currentHighlight = newHighlight;
     this.currentHighlightLineNumber = newLineNumber;
-
-    if (this.tokenHighlightedListener) {
-      this.tokenHighlightedListener(
-        newHighlight,
-        newLineNumber,
-        newHoveredElement
-      );
-    }
+    this.triggerTokenHighlightEvent(
+      newHighlight,
+      newLineNumber,
+      newHoveredElement
+    );
     this.notifyForToken(oldHighlight, oldLineNumber);
     this.notifyForToken(newHighlight, newLineNumber);
   }
 
+  triggerTokenHighlightEvent(
+    token: string | undefined,
+    line: number,
+    element: Element | undefined
+  ) {
+    if (!this.tokenHighlightListener) {
+      return;
+    }
+    if (!token || !element) {
+      this.tokenHighlightListener(undefined);
+      return;
+    }
+    const previousTextLength = getPreviousContentNodes(element)
+      .map(sib => sib.textContent!.length)
+      .reduce((partial_sum, a) => partial_sum + a, 0);
+    const lineEl = getLineElByChild(element);
+    assertIsDefined(lineEl, 'Line element should be found!');
+    const side = getSideByLineEl(lineEl);
+    const range = {
+      start_line: line,
+      start_column: previousTextLength + 1, // 1-based inclusive
+      end_line: line,
+      end_column: previousTextLength + token.length, // 1-based inclusive
+    };
+    this.tokenHighlightListener({token, element, side, range});
+  }
+
   getSortedLinesForSide(
     lineMapping: Map<string, Set<number>>,
     token: string | undefined,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
index 4f44665..2993d35 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -17,7 +17,7 @@
 
 import '../../../test/common-test-setup-karma';
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {Side} from '../../../api/diff';
+import {Side, TokenHighlightEventDetails} from '../../../api/diff';
 import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
 import {HOVER_DELAY_MS, TokenHighlightLayer} from './token-highlight-layer';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
@@ -66,14 +66,12 @@
   let container: HTMLElement;
   let listener: MockListener;
   let highlighter: TokenHighlightLayer;
-  let tokenHighlightingCalls: any[] = [];
+  let tokenHighlightingCalls: {details?: TokenHighlightEventDetails}[] = [];
 
-  function tokenHighlightedListener(
-    newHighlight: string | undefined,
-    newLineNumber: number,
-    hoveredElement?: Element
+  function tokenHighlightListener(
+    highlightDetails?: TokenHighlightEventDetails
   ) {
-    tokenHighlightingCalls.push({newHighlight, newLineNumber, hoveredElement});
+    tokenHighlightingCalls.push({details: highlightDetails});
   }
 
   setup(async () => {
@@ -81,7 +79,7 @@
     tokenHighlightingCalls = [];
     container = document.createElement('div');
     document.body.appendChild(container);
-    highlighter = new TokenHighlightLayer(container, tokenHighlightedListener);
+    highlighter = new TokenHighlightLayer(container, tokenHighlightListener);
     highlighter.addListener((...args) => listener.notify(...args));
   });
 
@@ -107,10 +105,13 @@
     const lineId = createLineId();
     const template = html`
       <div class="line">
-        <div data-value=${line} class="lineNum"></div>
-        <div id=${lineId} class="line-content">${text}</div>
+        <div data-value=${line} class="lineNum right"></div>
+        <div class="content">
+          <div id=${lineId} class="contentText">${text}</div>
+        </div>
       </div>
     `;
+
     const div = document.createElement('div');
     render(template, div);
     container.appendChild(div);
@@ -277,19 +278,16 @@
       assert.equal(tokenHighlightingCalls.length, 0);
       clock.tick(HOVER_DELAY_MS);
       assert.equal(tokenHighlightingCalls.length, 1);
-      assert.deepEqual(tokenHighlightingCalls[0], {
-        newHighlight: 'words',
-        newLineNumber: 1,
-        hoveredElement: words1,
+      assert.deepEqual(tokenHighlightingCalls[0].details, {
+        token: 'words',
+        side: Side.RIGHT,
+        element: words1,
+        range: {start_line: 1, start_column: 5, end_line: 1, end_column: 9},
       });
 
       MockInteractions.click(container);
       assert.equal(tokenHighlightingCalls.length, 2);
-      assert.deepEqual(tokenHighlightingCalls[1], {
-        newHighlight: undefined,
-        newLineNumber: 0,
-        hoveredElement: undefined,
-      });
+      assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
     });
 
     test('clicking clears highlight', async () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
index 4e2b6a1..8a6d95d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
@@ -30,28 +30,34 @@
       width: 1.3rem;
     }
   </style>
-  <gr-button
-    id="sideBySideBtn"
-    link=""
+  <gr-tooltip-content
     has-tooltip=""
-    position-below="[[showTooltipBelow]]"
-    class$="[[_computeSideBySideSelected(mode)]]"
     title="Side-by-side diff"
-    aria-pressed$="[[isSideBySideSelected(mode)]]"
-    on-click="_handleSideBySideTap"
+    position-below="[[showTooltipBelow]]"
   >
-    <iron-icon icon="gr-icons:side-by-side"></iron-icon>
-  </gr-button>
-  <gr-button
-    id="unifiedBtn"
-    link=""
+    <gr-button
+      id="sideBySideBtn"
+      link=""
+      class$="[[_computeSideBySideSelected(mode)]]"
+      aria-pressed$="[[isSideBySideSelected(mode)]]"
+      on-click="_handleSideBySideTap"
+    >
+      <iron-icon icon="gr-icons:side-by-side"></iron-icon>
+    </gr-button>
+  </gr-tooltip-content>
+  <gr-tooltip-content
     has-tooltip=""
     position-below="[[showTooltipBelow]]"
     title="Unified diff"
-    class$="[[_computeUnifiedSelected(mode)]]"
-    aria-pressed$="[[isUnifiedSelected(mode)]]"
-    on-click="_handleUnifiedTap"
   >
-    <iron-icon icon="gr-icons:unified"></iron-icon>
-  </gr-button>
+    <gr-button
+      id="unifiedBtn"
+      link=""
+      class$="[[_computeUnifiedSelected(mode)]]"
+      aria-pressed$="[[isUnifiedSelected(mode)]]"
+      on-click="_handleUnifiedTap"
+    >
+      <iron-icon icon="gr-icons:unified"></iron-icon>
+    </gr-button>
+  </gr-tooltip-content>
 `;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index 3f99790..4f1047f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
@@ -354,15 +354,15 @@
           hidden=""
         >
           <span class="preferences desktop">
-            <gr-button
-              link=""
-              class="prefsButton"
+            <gr-tooltip-content
               has-tooltip=""
               position-below=""
               title="Diff preferences"
-              on-click="_handlePrefsTap"
-              ><iron-icon icon="gr-icons:settings"></iron-icon
-            ></gr-button>
+            >
+              <gr-button link="" class="prefsButton" on-click="_handlePrefsTap"
+                ><iron-icon icon="gr-icons:settings"></iron-icon
+              ></gr-button>
+            </gr-tooltip-content>
           </span>
         </span>
         <gr-endpoint-decorator name="annotation-toggler">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
index fada9cb..7393606 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
@@ -129,6 +129,29 @@
   rootId: string;
 }
 
+const VISIBLE_TEXT_NODE_TYPES = [Node.TEXT_NODE, Node.ELEMENT_NODE];
+
+export function getPreviousContentNodes(node?: Node | null) {
+  const sibs = [];
+  while (node) {
+    const {parentNode, previousSibling} = node;
+    const topContentLevel =
+      parentNode &&
+      (parentNode as HTMLElement).classList.contains('contentText');
+    let previousEl: Node | undefined | null;
+    if (previousSibling) {
+      previousEl = previousSibling;
+    } else if (!topContentLevel) {
+      previousEl = parentNode?.previousSibling;
+    }
+    if (previousEl && VISIBLE_TEXT_NODE_TYPES.includes(previousEl.nodeType)) {
+      sibs.push(previousEl);
+    }
+    node = previousEl;
+  }
+  return sibs;
+}
+
 export function isThreadEl(node: Node): node is GrDiffThreadElement {
   return (
     node.nodeType === Node.ELEMENT_NODE &&
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
index 514f00e..51259c8 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
@@ -56,7 +56,7 @@
       <span class="title">Registered</span>
       <span class="value">
         <gr-date-formatter
-          has-tooltip=""
+          withTooltip
           date-str="[[_account.registered_on]]"
         ></gr-date-formatter>
       </span>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index 2abedf1..96b1ded 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 import '../../shared/gr-button/gr-button';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../../styles/shared-styles';
 import '../../../styles/gr-form-styles';
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
index ace1e1a..4096b02 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
@@ -16,7 +16,6 @@
  */
 import '@polymer/iron-input/iron-input';
 import '../../shared/gr-button/gr-button';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../../styles/shared-styles';
 import '../../../styles/gr-form-styles';
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 61498f6..94333c7 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -29,7 +29,6 @@
 import '../gr-change-table-editor/gr-change-table-editor';
 import '../../shared/gr-button/gr-button';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../shared/gr-diff-preferences/gr-diff-preferences';
 import '../../shared/gr-page-nav/gr-page-nav';
 import '../../shared/gr-select/gr-select';
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index f746c29..9897a9f 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -206,18 +206,7 @@
             ></gr-hovercard-account>`
           : ''}
         ${hasAttention
-          ? html`<gr-button
-              id="attentionButton"
-              link=""
-              aria-label="Remove user from attention set"
-              @click=${this._handleRemoveAttentionClick}
-              ?disabled=${!this._computeAttentionButtonEnabled(
-                highlightAttention,
-                account,
-                change,
-                this.selected,
-                this._selfAccount
-              )}
+          ? html` <gr-tooltip-content
               ?has-tooltip=${this._computeAttentionButtonEnabled(
                 highlightAttention,
                 account,
@@ -233,11 +222,25 @@
                 this.selected,
                 this._selfAccount
               )}"
-              ><iron-icon
-                class="attention"
-                icon="gr-icons:attention"
-              ></iron-icon>
-            </gr-button>`
+            >
+              <gr-button
+                id="attentionButton"
+                link=""
+                aria-label="Remove user from attention set"
+                @click=${this._handleRemoveAttentionClick}
+                ?disabled=${!this._computeAttentionButtonEnabled(
+                  highlightAttention,
+                  account,
+                  change,
+                  this.selected,
+                  this._selfAccount
+                )}
+                ><iron-icon
+                  class="attention"
+                  icon="gr-icons:attention"
+                ></iron-icon>
+              </gr-button>
+            </gr-tooltip-content>`
           : ''}
       </span>
       <span
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index 1ece10a..f7ebd66 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -15,14 +15,11 @@
  * limitations under the License.
  */
 import '@polymer/paper-button/paper-button';
-import '../../../styles/shared-styles';
-import '../../../styles/gr-voting-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement, property, computed, observe} from '@polymer/decorators';
-import {htmlTemplate} from './gr-button_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
+import {votingStyles} from '../../../styles/gr-voting-styles';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {
-  PolymerEvent,
   getEventPath,
   getKeyboardEvent,
   isModifierPressed,
@@ -37,87 +34,243 @@
   }
 }
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
 @customElement('gr-button')
-export class GrButton extends base {
-  static get template() {
-    return htmlTemplate;
-  }
+export class GrButton extends LitElement {
+  private readonly reporting: ReportingService = appContext.reportingService;
 
   /**
    * Should this button be rendered as a vote chip? Then we are applying
    * the .voteChip class (see gr-voting-styles) to the paper-button.
    */
-  @property({type: Boolean, reflectToAttribute: true})
+  @property({type: Boolean, reflect: true})
   voteChip = false;
 
-  @property({type: Boolean, reflectToAttribute: true})
-  downArrow = false;
-
-  @property({type: Boolean, reflectToAttribute: true})
-  link = false;
-
-  @property({type: Boolean})
-  noUppercase = false;
-
-  @property({type: Boolean, reflectToAttribute: true})
-  loading = false;
-
-  @property({type: Boolean, reflectToAttribute: true})
-  disabled: boolean | null = null;
-
-  @property({type: String})
-  tooltip = '';
-
   // Note: don't assign a value to this, since constructor is called
   // after created, the initial value maybe overridden by this
-  @property({type: String})
-  _initialTabindex?: string;
+  private initialTabindex?: string;
 
-  @computed('disabled', 'loading')
-  get _disabled() {
-    return this.disabled || this.loading;
+  @property({type: Boolean, reflect: true})
+  downArrow = false;
+
+  @property({type: Boolean, reflect: true})
+  link = false;
+
+  @property({type: Boolean, reflect: true})
+  loading = false;
+
+  @property({type: Boolean, reflect: true})
+  disabled: boolean | null = null;
+
+  static override get styles() {
+    return [
+      votingStyles,
+      spinnerStyles,
+      css`
+        /* general styles for all buttons */
+        :host {
+          --background-color: var(
+            --button-background-color,
+            var(--default-button-background-color)
+          );
+          --text-color: var(--default-button-text-color);
+          display: inline-block;
+          position: relative;
+        }
+        :host([hidden]) {
+          display: none;
+        }
+        :host([no-uppercase]) paper-button {
+          text-transform: none;
+        }
+        paper-button {
+          /* The next lines contains a copy of paper-button style.
+            Without a copy, the @apply works incorrectly with Polymer 2.
+            @apply is deprecated and is not recommended to use. It is expected
+            that @apply will be replaced with the ::part CSS pseudo-element.
+            After replacement copied lines can be removed.
+          */
+          @apply --layout-inline;
+          @apply --layout-center-center;
+          position: relative;
+          box-sizing: border-box;
+          min-width: 5.14em;
+          margin: 0 0.29em;
+          background: transparent;
+          -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+          -webkit-tap-highlight-color: transparent;
+          font: inherit;
+          text-transform: uppercase;
+          outline-width: 0;
+          border-top-left-radius: var(--border-radius);
+          border-top-right-radius: var(--border-radius);
+          border-bottom-right-radius: var(--border-radius);
+          border-bottom-left-radius: var(--border-radius);
+          -moz-user-select: none;
+          -ms-user-select: none;
+          -webkit-user-select: none;
+          user-select: none;
+          cursor: pointer;
+          z-index: 0;
+          padding: var(--spacing-m);
+
+          @apply --paper-font-common-base;
+          @apply --paper-button;
+          /* End of copy*/
+
+          /* paper-button sets this to anti-aliased, which appears different than
+            bold font elsewhere on macOS. */
+          -webkit-font-smoothing: initial;
+          align-items: center;
+          background-color: var(--background-color);
+          color: var(--text-color);
+          display: flex;
+          font-family: inherit;
+          justify-content: center;
+          margin: var(--margin, 0);
+          min-width: var(--border, 0);
+          padding: var(--padding, 4px 8px);
+          @apply --gr-button;
+        }
+        /* https://github.com/PolymerElements/paper-button/blob/2.x/paper-button.html */
+        /* BEGIN: Copy from paper-button */
+        paper-button[elevation='1'] {
+          @apply --paper-material-elevation-1;
+        }
+        paper-button[elevation='2'] {
+          @apply --paper-material-elevation-2;
+        }
+        paper-button[elevation='3'] {
+          @apply --paper-material-elevation-3;
+        }
+        paper-button[elevation='4'] {
+          @apply --paper-material-elevation-4;
+        }
+        paper-button[elevation='5'] {
+          @apply --paper-material-elevation-5;
+        }
+        /* END: Copy from paper-button */
+        paper-button:hover {
+          background: linear-gradient(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.12)),
+            var(--background-color);
+        }
+
+        /* Some mobile browsers treat focused element as hovered element.
+        As a result, element remains hovered after click (has grey background in default theme).
+        Use @media (hover:none) to remove background if
+        user's primary input mechanism can't hover over elements.
+        See: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover
+
+        Note 1: not all browsers support this media query
+        (see https://caniuse.com/#feat=css-media-interaction).
+        If browser doesn't support it, then the whole content of @media .. is ignored.
+        This is why the default behavior is placed outside of @media.
+        */
+        @media (hover: none) {
+          paper-button:hover {
+            background: transparent;
+          }
+        }
+
+        :host([primary]) {
+          --background-color: var(--primary-button-background-color);
+          --text-color: var(--primary-button-text-color);
+        }
+        :host([link][primary]) {
+          --text-color: var(--primary-button-background-color);
+        }
+
+        /* Keep below color definition for primary so that this takes precedence
+          when disabled. */
+        :host([disabled]),
+        :host([loading]) {
+          --background-color: var(--disabled-button-background-color);
+          --text-color: var(--deemphasized-text-color);
+          cursor: default;
+        }
+
+        /* Styles for link buttons specifically */
+        :host([link]) {
+          --background-color: transparent;
+          --margin: 0;
+          --padding: var(--spacing-s);
+        }
+        :host([disabled][link]),
+        :host([loading][link]) {
+          --background-color: transparent;
+          --text-color: var(--deemphasized-text-color);
+          cursor: default;
+        }
+
+        /* Styles for the optional down arrow */
+        :host(:not([down-arrow])) .downArrow {
+          display: none;
+        }
+        :host([down-arrow]) .downArrow {
+          border-top: 0.36em solid #ccc;
+          border-left: 0.36em solid transparent;
+          border-right: 0.36em solid transparent;
+          margin-bottom: var(--spacing-xxs);
+          margin-left: var(--spacing-m);
+          transition: border-top-color 200ms;
+        }
+        :host([down-arrow]) paper-button:hover .downArrow {
+          border-top-color: var(--deemphasized-text-color);
+        }
+      `,
+    ];
   }
 
-  @property({
-    computed: 'computeAriaDisabled(disabled, loading)',
-    reflectToAttribute: true,
-    type: String,
-  })
-  ariaDisabled!: string;
-
-  computeAriaDisabled() {
-    return this._disabled ? 'true' : 'false';
+  override render() {
+    return html`<paper-button
+      ?raised="${!this.link}"
+      ?disabled="${this.disabled || this.loading}"
+      role="button"
+      tabindex="-1"
+      part="paper-button"
+      class="${this.voteChip ? 'voteChip' : ''}"
+    >
+      ${this.loading ? html`<span class="loadingSpin"></span>` : ''}
+      <slot></slot>
+      <i class="downArrow"></i>
+    </paper-button>`;
   }
 
-  computePaperButtonClass(voteChip?: boolean) {
-    return voteChip ? 'voteChip' : '';
-  }
-
-  private readonly reporting: ReportingService = appContext.reportingService;
-
   constructor() {
     super();
-    this._initialTabindex = this.getAttribute('tabindex') || '0';
-    // TODO(TS): try avoid using unknown
-    this.addEventListener('click', e =>
-      this._handleAction(e as unknown as PolymerEvent)
-    );
+    this.initialTabindex = this.getAttribute('tabindex') || '0';
+    this.addEventListener('click', e => this._handleAction(e));
     this.addEventListener('keydown', e =>
       this._handleKeydown(e as unknown as CustomKeyboardEvent)
     );
   }
 
-  override ready() {
-    super.ready();
-    this._ensureAttribute('role', 'button');
-    this._ensureAttribute('tabindex', '0');
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('disabled')) {
+      this.setAttribute(
+        'tabindex',
+        this.disabled ? '-1' : this.initialTabindex || '0'
+      );
+    }
+    if (changedProperties.has('loading') || changedProperties.has('disabled')) {
+      this.setAttribute(
+        'aria-disabled',
+        this.disabled || this.loading ? 'true' : 'false'
+      );
+    }
   }
 
-  _handleAction(e: PolymerEvent) {
-    if (this._disabled) {
+  override connectedCallback() {
+    super.connectedCallback();
+    if (!this.getAttribute('role')) {
+      this.setAttribute('role', 'button');
+    }
+    if (!this.getAttribute('tabindex')) {
+      this.setAttribute('tabindex', '0');
+    }
+  }
+
+  _handleAction(e: MouseEvent) {
+    if (this.disabled || this.loading) {
       e.preventDefault();
       e.stopPropagation();
       e.stopImmediatePropagation();
@@ -127,15 +280,6 @@
     this.reporting.reportInteraction('button-click', {path: getEventPath(e)});
   }
 
-  @observe('disabled')
-  _disabledChanged(disabled: boolean) {
-    this.setAttribute(
-      'tabindex',
-      disabled ? '-1' : this._initialTabindex || '0'
-    );
-    this.updateStyles();
-  }
-
   _handleKeydown(e: CustomKeyboardEvent) {
     if (isModifierPressed(e)) {
       return;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts
deleted file mode 100644
index 22ec2f4..0000000
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts
+++ /dev/null
@@ -1,188 +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-voting-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="gr-spinner-styles">
-    /* general styles for all buttons */
-    :host {
-      --background-color: var(
-        --button-background-color,
-        var(--default-button-background-color)
-      );
-      --text-color: var(--default-button-text-color);
-      display: inline-block;
-      position: relative;
-    }
-    :host([hidden]) {
-      display: none;
-    }
-    :host([no-uppercase]) paper-button {
-      text-transform: none;
-    }
-    paper-button {
-      /* The next lines contains a copy of paper-button style.
-          Without a copy, the @apply works incorrectly with Polymer 2.
-          @apply is deprecated and is not recommended to use. It is expected
-          that @apply will be replaced with the ::part CSS pseudo-element.
-          After replacement copied lines can be removed.
-        */
-      @apply --layout-inline;
-      @apply --layout-center-center;
-      position: relative;
-      box-sizing: border-box;
-      min-width: 5.14em;
-      margin: 0 0.29em;
-      background: transparent;
-      -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-      -webkit-tap-highlight-color: transparent;
-      font: inherit;
-      text-transform: uppercase;
-      outline-width: 0;
-      border-top-left-radius: var(--border-radius);
-      border-top-right-radius: var(--border-radius);
-      border-bottom-right-radius: var(--border-radius);
-      border-bottom-left-radius: var(--border-radius);
-      -moz-user-select: none;
-      -ms-user-select: none;
-      -webkit-user-select: none;
-      user-select: none;
-      cursor: pointer;
-      z-index: 0;
-      padding: var(--spacing-m);
-
-      @apply --paper-font-common-base;
-      @apply --paper-button;
-      /* End of copy*/
-
-      /* paper-button sets this to anti-aliased, which appears different than
-          bold font elsewhere on macOS. */
-      -webkit-font-smoothing: initial;
-      align-items: center;
-      background-color: var(--background-color);
-      color: var(--text-color);
-      display: flex;
-      font-family: inherit;
-      justify-content: center;
-      margin: var(--margin, 0);
-      min-width: var(--border, 0);
-      padding: var(--padding, 4px 8px);
-      @apply --gr-button;
-    }
-    /* https://github.com/PolymerElements/paper-button/blob/2.x/paper-button.html */
-    /* BEGIN: Copy from paper-button */
-    paper-button[elevation='1'] {
-      @apply --paper-material-elevation-1;
-    }
-    paper-button[elevation='2'] {
-      @apply --paper-material-elevation-2;
-    }
-    paper-button[elevation='3'] {
-      @apply --paper-material-elevation-3;
-    }
-    paper-button[elevation='4'] {
-      @apply --paper-material-elevation-4;
-    }
-    paper-button[elevation='5'] {
-      @apply --paper-material-elevation-5;
-    }
-    /* END: Copy from paper-button */
-    paper-button:hover {
-      background: linear-gradient(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.12)),
-        var(--background-color);
-    }
-
-    /* Some mobile browsers treat focused element as hovered element.
-      As a result, element remains hovered after click (has grey background in default theme).
-      Use @media (hover:none) to remove background if
-      user's primary input mechanism can't hover over elements.
-      See: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover
-
-      Note 1: not all browsers support this media query
-      (see https://caniuse.com/#feat=css-media-interaction).
-      If browser doesn't support it, then the whole content of @media .. is ignored.
-      This is why the default behavior is placed outside of @media.
-      */
-    @media (hover: none) {
-      paper-button:hover {
-        background: transparent;
-      }
-    }
-
-    :host([primary]) {
-      --background-color: var(--primary-button-background-color);
-      --text-color: var(--primary-button-text-color);
-    }
-    :host([link][primary]) {
-      --text-color: var(--primary-button-background-color);
-    }
-
-    /* Keep below color definition for primary so that this takes precedence
-        when disabled. */
-    :host([disabled]),
-    :host([loading]) {
-      --background-color: var(--disabled-button-background-color);
-      --text-color: var(--deemphasized-text-color);
-      cursor: default;
-    }
-
-    /* Styles for link buttons specifically */
-    :host([link]) {
-      --background-color: transparent;
-      --margin: 0;
-      --padding: var(--spacing-s);
-    }
-    :host([disabled][link]),
-    :host([loading][link]) {
-      --background-color: transparent;
-      --text-color: var(--deemphasized-text-color);
-      cursor: default;
-    }
-
-    /* Styles for the optional down arrow */
-    :host(:not([down-arrow])) .downArrow {
-      display: none;
-    }
-    :host([down-arrow]) .downArrow {
-      border-top: 0.36em solid #ccc;
-      border-left: 0.36em solid transparent;
-      border-right: 0.36em solid transparent;
-      margin-bottom: var(--spacing-xxs);
-      margin-left: var(--spacing-m);
-      transition: border-top-color 200ms;
-    }
-    :host([down-arrow]) paper-button:hover .downArrow {
-      border-top-color: var(--deemphasized-text-color);
-    }
-  </style>
-  <paper-button
-    raised="[[!link]]"
-    disabled="[[_disabled]]"
-    tabindex="-1"
-    part="paper-button"
-    class$="[[computePaperButtonClass(voteChip)]]"
-  >
-    <template is="dom-if" if="[[loading]]">
-      <span class="loadingSpin"></span>
-    </template>
-    <slot></slot>
-    <i class="downArrow"></i>
-  </paper-button>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
index f0f122a..0149bd5 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
@@ -17,6 +17,7 @@
 
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
 import '../../../test/common-test-setup-karma';
+import './gr-button';
 import {addListener} from '@polymer/polymer/lib/utils/gestures';
 import {appContext} from '../../../services/app-context';
 import {html} from '@polymer/polymer/lib/utils/html-tag';
@@ -49,23 +50,26 @@
     return spy;
   };
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await element.updateComplete;
   });
 
-  test('disabled is set by disabled', () => {
+  test('disabled is set by disabled', async () => {
     const paperBtn = queryAndAssert<PaperButtonElement>(
       element,
       'paper-button'
     );
     assert.isFalse(paperBtn.disabled);
     element.disabled = true;
+    await element.updateComplete;
     assert.isTrue(paperBtn.disabled);
     element.disabled = false;
+    await element.updateComplete;
     assert.isFalse(paperBtn.disabled);
   });
 
-  test('loading set from listener', () => {
+  test('loading set from listener', async () => {
     let resolve: Function;
     element.addEventListener('click', e => {
       const target = e.target as HTMLElement;
@@ -78,36 +82,44 @@
     );
     assert.isFalse(paperBtn.disabled);
     MockInteractions.tap(element);
+    await element.updateComplete;
     assert.isTrue(paperBtn.disabled);
     assert.isTrue(element.hasAttribute('loading'));
     resolve!();
-    flush();
+    await element.updateComplete;
     assert.isFalse(paperBtn.disabled);
     assert.isFalse(element.hasAttribute('loading'));
   });
 
-  test('tabindex should be -1 if disabled', () => {
+  test('tabindex should be -1 if disabled', async () => {
     element.disabled = true;
-    assert.isTrue(element.getAttribute('tabindex') === '-1');
+    await element.updateComplete;
+    assert.equal(element.getAttribute('tabindex'), '-1');
   });
 
   // Regression tests for Issue: 11969
-  test('tabindex should be reset to 0 if enabled', () => {
+  test('tabindex should be reset to 0 if enabled', async () => {
     element.disabled = false;
+    await element.updateComplete;
     assert.equal(element.getAttribute('tabindex'), '0');
     element.disabled = true;
+    await element.updateComplete;
     assert.equal(element.getAttribute('tabindex'), '-1');
     element.disabled = false;
+    await element.updateComplete;
     assert.equal(element.getAttribute('tabindex'), '0');
   });
 
-  test('tabindex should be preserved', () => {
+  test('tabindex should be preserved', async () => {
     const tabIndexElement = tabindexFixture.instantiate() as GrButton;
     tabIndexElement.disabled = false;
+    await element.updateComplete;
     assert.equal(tabIndexElement.getAttribute('tabindex'), '3');
     tabIndexElement.disabled = true;
+    await element.updateComplete;
     assert.equal(tabIndexElement.getAttribute('tabindex'), '-1');
     tabIndexElement.disabled = false;
+    await element.updateComplete;
     assert.equal(tabIndexElement.getAttribute('tabindex'), '3');
   });
 
@@ -152,8 +164,9 @@
   }
 
   suite('disabled', () => {
-    setup(() => {
+    setup(async () => {
       element.disabled = true;
+      await element.updateComplete;
     });
 
     for (const eventName of ['tap', 'click']) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 1f2983a..fa04860 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -20,7 +20,6 @@
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../gr-button/gr-button';
 import '../gr-dialog/gr-dialog';
-import '../gr-date-formatter/gr-date-formatter';
 import '../gr-formatted-text/gr-formatted-text';
 import '../gr-icons/gr-icons';
 import '../gr-overlay/gr-overlay';
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index b00bf8b..4cb7738 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -313,7 +313,7 @@
       <template is="dom-if" if="[[comment.updated]]">
         <span class="date" tabindex="0" on-click="_handleAnchorClick">
           <gr-date-formatter
-            has-tooltip=""
+            withTooltip
             date-str="[[comment.updated]]"
           ></gr-date-formatter>
         </span>
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index de2a017..99be86b 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -123,17 +123,20 @@
             part="text-container-style"
           />
         </iron-input>
-        <gr-button
-          id="copy-clipboard-button"
-          link=""
+        <gr-tooltip-content
           ?has-tooltip=${this.hasTooltip}
-          class="copyToClipboard"
           title="${ifDefined(this.buttonTitle)}"
-          @click="${this._copyToClipboard}"
-          aria-label="Click to copy to clipboard"
         >
-          <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
-        </gr-button>
+          <gr-button
+            id="copy-clipboard-button"
+            link=""
+            class="copyToClipboard"
+            @click="${this._copyToClipboard}"
+            aria-label="Click to copy to clipboard"
+          >
+            <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
+          </gr-button>
+        </gr-tooltip-content>
       </div> `;
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
index 437e7e8..99f9265 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
@@ -14,11 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-date-formatter_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
-import {property, customElement} from '@polymer/decorators';
+import '../gr-tooltip-content/gr-tooltip-content';
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {
   parseDate,
   fromNow,
@@ -75,16 +73,9 @@
   }
 }
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
 @customElement('gr-date-formatter')
-export class GrDateFormatter extends base {
-  static get template() {
-    return htmlTemplate;
-  }
-
-  @property({type: String, notify: true})
+export class GrDateFormatter extends LitElement {
+  @property({type: String})
   dateStr: string | undefined = undefined;
 
   @property({type: Boolean})
@@ -95,30 +86,20 @@
    * native browser tooltip.
    */
   @property({type: Boolean})
-  override hasTooltip = false;
+  withTooltip = false;
 
   @property({type: Boolean})
   showYesterday = false;
 
-  /**
-   * The title to be used as the native tooltip or by the tooltip behavior.
-   */
-  @property({
-    type: String,
-    reflectToAttribute: true,
-    computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)',
-  })
-  override title = '';
-
   /** @type {?{short: string, full: string}} */
   @property({type: Object})
-  _dateFormat?: DateFormatPair;
+  private dateFormat?: DateFormatPair;
 
   @property({type: String})
-  _timeFormat?: string;
+  private timeFormat?: string;
 
   @property({type: Boolean})
-  _relative = false;
+  private relative = false;
 
   @property({type: Boolean})
   forceRelative = false;
@@ -132,76 +113,110 @@
     super();
   }
 
+  static override get styles() {
+    return [
+      css`
+        host {
+          color: inherit;
+          display: inline;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    if (!this.withTooltip) {
+      return this.renderDateString();
+    }
+
+    const fullDateStr = this.computeFullDateStr();
+    if (!fullDateStr) {
+      return this.renderDateString();
+    }
+    return html`
+      <gr-tooltip-content has-tooltip title=${fullDateStr}>
+        ${this.renderDateString()}
+      </gr-tooltip-content>
+    `;
+  }
+
+  private renderDateString() {
+    return html` <span>${this._computeDateStr()}</span>`;
+  }
+
   override connectedCallback() {
     super.connectedCallback();
     this._loadPreferences();
   }
 
+  // private but used by tests
   _getUtcOffsetString() {
     return utcOffsetString();
   }
 
+  // private but used by tests
   _loadPreferences() {
     return this._getLoggedIn().then(loggedIn => {
       if (!loggedIn) {
-        this._timeFormat = TimeFormats.TIME_24;
-        this._dateFormat = DateFormats.STD;
-        this._relative = this.forceRelative;
+        this.timeFormat = TimeFormats.TIME_24;
+        this.dateFormat = DateFormats.STD;
+        this.relative = this.forceRelative;
         return;
       }
-      return Promise.all([this._loadTimeFormat(), this._loadRelative()]);
+      return Promise.all([this._loadTimeFormat(), this.loadRelative()]);
     });
   }
 
+  // private but used in gr/file-list_test.js
   _loadTimeFormat() {
-    return this._getPreferences().then(preferences => {
+    return this.getPreferences().then(preferences => {
       if (!preferences) {
         throw Error('Preferences is not set');
       }
-      this._decideTimeFormat(preferences.time_format);
-      this._decideDateFormat(preferences.date_format);
+      this.decideTimeFormat(preferences.time_format);
+      this.decideDateFormat(preferences.date_format);
     });
   }
 
-  _decideTimeFormat(timeFormat: TimeFormat) {
+  private decideTimeFormat(timeFormat: TimeFormat) {
     switch (timeFormat) {
       case TimeFormat.HHMM_12:
-        this._timeFormat = TimeFormats.TIME_12;
+        this.timeFormat = TimeFormats.TIME_12;
         break;
       case TimeFormat.HHMM_24:
-        this._timeFormat = TimeFormats.TIME_24;
+        this.timeFormat = TimeFormats.TIME_24;
         break;
       default:
         assertNever(timeFormat, `Invalid time format: ${timeFormat}`);
     }
   }
 
-  _decideDateFormat(dateFormat: DateFormat) {
+  private decideDateFormat(dateFormat: DateFormat) {
     switch (dateFormat) {
       case DateFormat.STD:
-        this._dateFormat = DateFormats.STD;
+        this.dateFormat = DateFormats.STD;
         break;
       case DateFormat.US:
-        this._dateFormat = DateFormats.US;
+        this.dateFormat = DateFormats.US;
         break;
       case DateFormat.ISO:
-        this._dateFormat = DateFormats.ISO;
+        this.dateFormat = DateFormats.ISO;
         break;
       case DateFormat.EURO:
-        this._dateFormat = DateFormats.EURO;
+        this.dateFormat = DateFormats.EURO;
         break;
       case DateFormat.UK:
-        this._dateFormat = DateFormats.UK;
+        this.dateFormat = DateFormats.UK;
         break;
       default:
         assertNever(dateFormat, `Invalid date format: ${dateFormat}`);
     }
   }
 
-  _loadRelative() {
-    return this._getPreferences().then(prefs => {
+  private loadRelative() {
+    return this.getPreferences().then(prefs => {
       // prefs.relative_date_in_change_table is not set when false.
-      this._relative =
+      this.relative =
         this.forceRelative || !!(prefs && prefs.relative_date_in_change_table);
     });
   }
@@ -210,70 +225,60 @@
     return this.restApiService.getLoggedIn();
   }
 
-  _getPreferences() {
+  private getPreferences() {
     return this.restApiService.getPreferences();
   }
 
-  _computeDateStr(
-    dateStr?: Timestamp,
-    timeFormat?: string,
-    dateFormat?: DateFormatPair,
-    relative?: boolean,
-    showDateAndTime?: boolean,
-    showYesterday?: boolean
-  ) {
-    if (!dateStr || !timeFormat || !dateFormat) {
+  // private but used by tests
+  _computeDateStr() {
+    if (!this.dateStr || !this.timeFormat || !this.dateFormat) {
       return '';
     }
-    const date = parseDate(dateStr);
+    const date = parseDate(this.dateStr as Timestamp);
     if (!isValidDate(date)) {
       return '';
     }
-    if (relative) {
+    if (this.relative) {
       return fromNow(date, this.relativeOptionNoAgo);
     }
     const now = new Date();
-    let format = dateFormat.full;
+    let format = this.dateFormat.full;
     if (isWithinDay(now, date)) {
-      format = timeFormat;
-    } else if (showYesterday && wasYesterday(now, date)) {
-      return `Yesterday at ${formatDate(date, timeFormat)}`;
+      format = this.timeFormat;
+    } else if (this.showYesterday && wasYesterday(now, date)) {
+      return `Yesterday at ${formatDate(date, this.timeFormat)}`;
     } else {
       if (isWithinHalfYear(now, date)) {
-        format = dateFormat.short;
+        format = this.dateFormat.short;
       }
-      if (this.showDateAndTime || showDateAndTime) {
-        format = `${format} ${timeFormat}`;
+      if (this.showDateAndTime || this.showDateAndTime) {
+        format = `${format} ${this.timeFormat}`;
       }
     }
     return formatDate(date, format);
   }
 
-  _timeToSecondsFormat(timeFormat: string | undefined) {
-    return timeFormat === TimeFormats.TIME_12
-      ? TimeFormats.TIME_12_WITH_SEC
-      : TimeFormats.TIME_24_WITH_SEC;
-  }
-
-  _computeFullDateStr(
-    dateStr?: Timestamp,
-    timeFormat?: string,
-    dateFormat?: DateFormatPair
-  ) {
+  private computeFullDateStr() {
     // Polymer 2: check for undefined
-    if ([dateStr, timeFormat].includes(undefined) || !dateFormat) {
+    if (
+      [this.dateStr, this.timeFormat].includes(undefined) ||
+      !this.dateFormat
+    ) {
       return undefined;
     }
 
-    if (!dateStr) {
+    if (!this.dateStr) {
       return '';
     }
-    const date = parseDate(dateStr);
+    const date = parseDate(this.dateStr as Timestamp);
     if (!isValidDate(date)) {
       return '';
     }
-    let format = dateFormat.full + ', ';
-    format += this._timeToSecondsFormat(timeFormat);
+    let format = this.dateFormat.full + ', ';
+    format +=
+      this.timeFormat === TimeFormats.TIME_12
+        ? TimeFormats.TIME_12_WITH_SEC
+        : TimeFormats.TIME_24_WITH_SEC;
     return formatDate(date, format) + this._getUtcOffsetString();
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts
deleted file mode 100644
index 4808832..0000000
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts
+++ /dev/null
@@ -1,30 +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>
-    :host {
-      color: inherit;
-      display: inline;
-    }
-  </style>
-  <span>
-    [[_computeDateStr(dateStr, _timeFormat, _dateFormat, _relative,
-    showDateAndTime, showYesterday)]]
-  </span>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
index 9a96c2d..860a7e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
@@ -22,14 +22,18 @@
 import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromTemplate(html`
-<gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
+<gr-date-formatter withTooltip dateStr="2015-09-24 23:30:17.033000000">
+</gr-date-formatter>
+`);
+
+const lightFixture = fixtureFromTemplate(html`
+<gr-date-formatter dateStr="2015-09-24 23:30:17.033000000"></gr-date-formatter>
 `);
 
 suite('gr-date-formatter tests', () => {
   let element;
 
   setup(() => {
-
   });
 
   /**
@@ -41,7 +45,7 @@
     return d;
   }
 
-  function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
+  async function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
       expectedTooltip) {
     // Normalize and convert the date to mimic server response.
     dateStr = normalizedDate(dateStr)
@@ -50,13 +54,13 @@
         .slice(0, -1);
     sinon.useFakeTimers(normalizedDate(nowStr).getTime());
     element.dateStr = dateStr;
-    flush();
-    const span = element.shadowRoot
-        .querySelector('span');
+    await element.updateComplete;
+    const span = element.shadowRoot.querySelector('span');
+    const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
     assert.equal(span.textContent.trim(), expected);
-    assert.equal(element.title, expectedTooltip);
+    assert.equal(tooltip.title, expectedTooltip);
     element.showDateAndTime = true;
-    flush();
+    await element.updateComplete;
     assert.equal(span.textContent.trim(), expectedWithDateAndTime);
   }
 
@@ -81,35 +85,37 @@
 
     test('invalid dates are quietly rejected', () => {
       assert.notOk((new Date('foo')).valueOf());
-      assert.equal(element._computeDateStr('foo', 'h:mm A'), '');
+      element.dateStr = 'foo';
+      element.timeFormat = 'h:mm A';
+      assert.equal(element._computeDateStr(), '');
     });
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           'Jul 29, 2015, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           'Jul 28',
           'Jul 28 20:25',
           'Jul 28, 2015, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           'Jun 15',
           'Jun 15 03:25',
           'Jun 15, 2015, 03:25:14');
     });
 
-    test('More than six months', () => {
-      testDates('2015-09-15 20:34:00.000000000',
+    test('More than six months', async () => {
+      await testDates('2015-09-15 20:34:00.000000000',
           '2015-01-15 03:25:00.000000000',
           'Jan 15, 2015',
           'Jan 15, 2015 03:25',
@@ -128,24 +134,24 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           '07/29/15, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           '07/28',
           '07/28 20:25',
           '07/28/15, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           '06/15',
           '06/15 03:25',
@@ -164,24 +170,24 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           '2015-07-29, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           '07-28',
           '07-28 20:25',
           '2015-07-28, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           '06-15',
           '06-15 03:25',
@@ -200,24 +206,24 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           '29.07.2015, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           '28. Jul',
           '28. Jul 20:25',
           '28.07.2015, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           '15. Jun',
           '15. Jun 03:25',
@@ -236,24 +242,24 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           '29/07/2015, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           '28/07',
           '28/07 20:25',
           '28/07/2015, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           '15/06',
           '15/06 03:25',
@@ -273,8 +279,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -294,8 +300,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -315,8 +321,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -336,8 +342,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -357,8 +363,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -377,16 +383,16 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '5 hours ago',
           '5 hours ago',
           'Jul 29, 2015, 3:34:14 PM');
     });
 
-    test('More than six months', () => {
-      testDates('2015-09-15 20:34:00.000000000',
+    test('More than six months', async () => {
+      await testDates('2015-09-15 20:34:00.000000000',
           '2015-01-15 03:25:00.000000000',
           '8 months ago',
           '8 months ago',
@@ -405,10 +411,10 @@
     }));
 
     test('Preferences are respected', () => {
-      assert.equal(element._timeFormat, 'h:mm A');
-      assert.equal(element._dateFormat.short, 'MM/DD');
-      assert.equal(element._dateFormat.full, 'MM/DD/YY');
-      assert.isTrue(element._relative);
+      assert.equal(element.timeFormat, 'h:mm A');
+      assert.equal(element.dateFormat.short, 'MM/DD');
+      assert.equal(element.dateFormat.full, 'MM/DD/YY');
+      assert.isTrue(element.relative);
     });
   });
 
@@ -419,10 +425,38 @@
     }));
 
     test('Default preferences are respected', () => {
-      assert.equal(element._timeFormat, 'HH:mm');
-      assert.equal(element._dateFormat.short, 'MMM DD');
-      assert.equal(element._dateFormat.full, 'MMM DD, YYYY');
-      assert.isFalse(element._relative);
+      assert.equal(element.timeFormat, 'HH:mm');
+      assert.equal(element.dateFormat.short, 'MMM DD');
+      assert.equal(element.dateFormat.full, 'MMM DD, YYYY');
+      assert.isFalse(element.relative);
+    });
+  });
+
+  suite('with tooltip', () => {
+    setup(async () => {
+      await stubRestAPI(null);
+      element = basicFixture.instantiate();
+      await element._loadPreferences();
+      await element.updateComplete;
+    });
+
+    test('Tooltip is present', () => {
+      const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
+      assert.isOk(tooltip);
+    });
+  });
+
+  suite('without tooltip', () => {
+    setup(async () => {
+      await stubRestAPI(null);
+      element = lightFixture.instantiate();
+      await element._loadPreferences();
+      await element.updateComplete;
+    });
+
+    test('Tooltip is absent', () => {
+      const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
+      assert.isNotOk(tooltip);
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
index 18a46a0..9ec1d39 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
@@ -123,6 +123,7 @@
     class="dropdown-trigger"
     on-click="_showDropdownTapHandler"
     slot="dropdown-trigger"
+    no-uppercase
   >
     <span id="triggerText">[[text]]</span>
     <gr-copy-clipboard
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
index f1f6bf8..076553b 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
@@ -128,7 +128,7 @@
             <span class="value">[[_computeReason(change)]]</span>
             <template is="dom-if" if="[[_computeLastUpdate(change)]]">
               (<gr-date-formatter
-                has-tooltip
+                withTooltip
                 date-str="[[_computeLastUpdate(change)]]"
               ></gr-date-formatter
               >)
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
index e9a224c..82f64d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
@@ -109,7 +109,7 @@
     stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
-    flush();
+    await flush();
     const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
     assert.isOk(button);
     assert.equal(button.innerText, 'Remove Reviewer');
@@ -132,7 +132,7 @@
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
 
-    flush();
+    await flush();
     const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
 
     assert.isOk(button);
@@ -156,7 +156,7 @@
     stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
-    flush();
+    await flush();
 
     const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
     assert.isOk(button);
@@ -180,7 +180,7 @@
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
 
-    flush();
+    await flush();
     const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
 
     assert.equal(button.innerText, 'Remove CC');
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
index 203784d..87f6052 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
@@ -125,7 +125,7 @@
           .querySelector('[data-action-key="' + key + '"]'));
     });
 
-    test('action button properties', () => {
+    test('action button properties', async () => {
       const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
       flush();
       const button = element.shadowRoot
@@ -137,17 +137,17 @@
       changeActions.setTitle(key, 'Yo hint');
       changeActions.setEnabled(key, false);
       changeActions.setIcon(key, 'pupper');
-      flush();
+      await flush();
       assert.equal(button.getAttribute('data-label'), 'Yo');
-      assert.equal(button.getAttribute('title'), 'Yo hint');
+      assert.equal(button.parentElement.getAttribute('title'), 'Yo hint');
       assert.isTrue(button.disabled);
       assert.equal(button.querySelector('iron-icon').icon,
           'gr-icons:pupper');
     });
 
-    test('hide action buttons', () => {
+    test('hide action buttons', async () => {
       const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
-      flush();
+      await flush();
       let button = element.shadowRoot
           .querySelector('[data-action-key="' + key + '"]');
       assert.isOk(button);
@@ -168,7 +168,7 @@
           .querySelector('[data-action-key="' + key + '"]'));
       changeActions.setActionOverflow(
           changeActions.ActionType.REVISION, key, true);
-      flush();
+      await flush();
       assert.isNotOk(element.shadowRoot
           .querySelector('[data-action-key="' + key + '"]'));
       assert.isFalse(element.$.moreActions.hidden);
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
index f31b57f..723f8c1 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
@@ -116,16 +116,17 @@
           ></gr-account-link>
         </td>
         <td>
-          <gr-button
-            link=""
-            aria-label="Remove vote"
-            on-click="_onDeleteVote"
-            tooltip="Remove vote"
-            data-account-id$="[[mappedLabel.account._account_id]]"
-            class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]"
-          >
-            <iron-icon icon="gr-icons:delete"></iron-icon>
-          </gr-button>
+          <gr-tooltip-content has-tooltip="" title="Remove vote">
+            <gr-button
+              link=""
+              aria-label="Remove vote"
+              on-click="_onDeleteVote"
+              data-account-id$="[[mappedLabel.account._account_id]]"
+              class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]"
+            >
+              <iron-icon icon="gr-icons:delete"></iron-icon>
+            </gr-button>
+          </gr-tooltip-content>
         </td>
       </tr>
     </template>
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.ts b/polygerrit-ui/app/styles/gr-voting-styles.ts
index 12d0784..a623d99 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.ts
+++ b/polygerrit-ui/app/styles/gr-voting-styles.ts
@@ -18,24 +18,26 @@
 // Mark the file as a module. Otherwise typescript assumes this is a script
 // and $_documentContainer is a global variable.
 // See: https://www.typescriptlang.org/docs/handbook/modules.html
-export {};
+import {css} from 'lit';
+
+export const votingStyles = css`
+  .voteChip {
+    border: 1px solid var(--border-color);
+    /* max rounded */
+    border-radius: 1em;
+    box-shadow: none;
+    box-sizing: border-box;
+    min-width: 3em;
+    color: var(--vote-text-color);
+  }
+`;
 
 const $_documentContainer = document.createElement('template');
-
 $_documentContainer.innerHTML = `<dom-module id="gr-voting-styles">
   <template>
     <style>
-      .voteChip {
-        border: 1px solid var(--border-color);
-        /* max rounded */
-        border-radius: 1em;
-        box-shadow: none;
-        box-sizing: border-box;
-        min-width: 3em;
-        color: var(--vote-text-color);
-      }
+    ${votingStyles.cssText}
     </style>
   </template>
 </dom-module>`;
-
 document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 5370cf9..0002254 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -100,7 +100,7 @@
 }
 
 export function query<E extends Element = Element>(
-  el: Element | undefined,
+  el: Element | null | undefined,
   selector: string
 ): E | undefined {
   if (!el) return undefined;
@@ -109,7 +109,7 @@
 }
 
 export function queryAndAssert<E extends Element = Element>(
-  el: Element | undefined,
+  el: Element | null | undefined,
   selector: string
 ): E {
   const found = query<E>(el, selector);
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 16129af..7b1f3e3 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -171,7 +171,7 @@
  *  getEventPath(e); // eg: div.class1>p#pid.class2
  * }
  */
-export function getEventPath<T extends PolymerEvent>(e?: T) {
+export function getEventPath<T extends MouseEvent>(e?: T) {
   if (!e) return '';
 
   let path = e.composedPath();