Merge "Merge branch 'stable-3.5'"
diff --git a/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java b/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
index 0e8e28e..74bccbd 100644
--- a/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
+++ b/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
@@ -37,6 +37,34 @@
    * @return WebLinkInfo that links to patch set in external service, null if there should be no
    *     link.
    */
+  @Deprecated
   WebLinkInfo getPatchSetWebLink(
       String projectName, String commit, String commitMessage, String branchName);
+
+  /**
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo} describing a link from a patch set to
+   * an external service.
+   *
+   * <p>In order for the web link to be visible {@link
+   * com.google.gerrit.extensions.common.WebLinkInfo#url} and {@link
+   * com.google.gerrit.extensions.common.WebLinkInfo#name} must be set.
+   *
+   * <p>
+   *
+   * @param projectName name of the project
+   * @param commit commit of the patch set
+   * @param commitMessage the commit message of the change
+   * @param branchName target branch of the change
+   * @param changeKey the changeID for this change
+   * @return WebLinkInfo that links to patch set in external service, null if there should be no
+   *     link.
+   */
+  default WebLinkInfo getPatchSetWebLink(
+      String projectName,
+      String commit,
+      String commitMessage,
+      String branchName,
+      String changeKey) {
+    return getPatchSetWebLink(projectName, commit, commitMessage, branchName);
+  }
 }
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index c92b194..58396f5 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -86,12 +86,19 @@
    * @param commit SHA1 of commit.
    * @param commitMessage the commit message of the commit.
    * @param branchName branch of the commit.
+   * @param changeKey change Identifier for this change
    */
   public ImmutableList<WebLinkInfo> getPatchSetLinks(
-      Project.NameKey project, String commit, String commitMessage, String branchName) {
+      Project.NameKey project,
+      String commit,
+      String commitMessage,
+      String branchName,
+      String changeKey) {
     return filterLinks(
         patchSetLinks,
-        webLink -> webLink.getPatchSetWebLink(project.get(), commit, commitMessage, branchName));
+        webLink ->
+            webLink.getPatchSetWebLink(
+                project.get(), commit, commitMessage, branchName, changeKey));
   }
 
   /**
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index 0321fcb..7d40f06 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -168,7 +168,8 @@
       RevCommit commit,
       boolean addLinks,
       boolean fillCommit,
-      String branchName)
+      String branchName,
+      String changeKey)
       throws IOException {
     CommitInfo info = new CommitInfo();
     if (fillCommit) {
@@ -182,7 +183,8 @@
 
     if (addLinks) {
       ImmutableList<WebLinkInfo> patchSetLinks =
-          webLinks.getPatchSetLinks(project, commit.name(), commit.getFullMessage(), branchName);
+          webLinks.getPatchSetLinks(
+              project, commit.name(), commit.getFullMessage(), branchName, changeKey);
       info.webLinks = patchSetLinks.isEmpty() ? null : patchSetLinks;
       ImmutableList<WebLinkInfo> resolveConflictsLinks =
           webLinks.getResolveConflictsLinks(
@@ -301,7 +303,9 @@
       rw.parseBody(commit);
       String branchName = cd.change().getDest().branch();
       if (setCommit) {
-        out.commit = getCommitInfo(project, rw, commit, has(WEB_LINKS), fillCommit, branchName);
+        out.commit =
+            getCommitInfo(
+                project, rw, commit, has(WEB_LINKS), fillCommit, branchName, c.getKey().get());
       }
       if (addFooters) {
         Ref ref = repo.exactRef(branchName);
diff --git a/java/com/google/gerrit/server/restapi/change/GetCommit.java b/java/com/google/gerrit/server/restapi/change/GetCommit.java
index d76ce04..5193501 100644
--- a/java/com/google/gerrit/server/restapi/change/GetCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/GetCommit.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static java.util.concurrent.TimeUnit.DAYS;
+
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -25,7 +27,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -64,10 +65,11 @@
                   commit,
                   addLinks,
                   /* fillCommit= */ true,
-                  rsrc.getChange().getDest().branch());
+                  rsrc.getChange().getDest().branch(),
+                  rsrc.getChange().getKey().get());
       Response<CommitInfo> r = Response.ok(info);
       if (rsrc.isCacheable()) {
-        r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+        r.caching(CacheControl.PRIVATE(7, DAYS));
       }
       return r;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/GetMergeList.java b/java/com/google/gerrit/server/restapi/change/GetMergeList.java
index f0639b5..551b50f 100644
--- a/java/com/google/gerrit/server/restapi/change/GetMergeList.java
+++ b/java/com/google/gerrit/server/restapi/change/GetMergeList.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static java.util.concurrent.TimeUnit.DAYS;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Project;
@@ -30,7 +32,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -77,7 +78,7 @@
         return createResponse(rsrc, ImmutableList.of());
       }
 
-      List<RevCommit> commits = MergeListBuilder.build(rw, commit, uninterestingParent);
+      ImmutableList<RevCommit> commits = MergeListBuilder.build(rw, commit, uninterestingParent);
       List<CommitInfo> result = new ArrayList<>(commits.size());
       RevisionJson changeJson = json.create(ImmutableSet.of());
       for (RevCommit c : commits) {
@@ -88,7 +89,8 @@
                 c,
                 addLinks,
                 /* fillCommit= */ true,
-                rsrc.getChange().getDest().branch()));
+                rsrc.getChange().getDest().branch(),
+                rsrc.getChange().getKey().get()));
       }
       return createResponse(rsrc, result);
     }
@@ -98,7 +100,7 @@
       RevisionResource rsrc, List<CommitInfo> result) {
     Response<List<CommitInfo>> r = Response.ok(result);
     if (rsrc.isCacheable()) {
-      r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+      r.caching(CacheControl.PRIVATE(7, DAYS));
     }
     return r;
   }
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 2dc401f..e8448b7 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
@@ -2555,8 +2555,9 @@
 
   _resetReplyOverlayFocusStops() {
     const dialog = query<GrReplyDialog>(this, '#replyDialog');
-    if (!dialog) return;
-    this.$.replyOverlay.setFocusStops(dialog.getFocusStops());
+    const focusStops = dialog?.getFocusStops();
+    if (!focusStops) return;
+    this.$.replyOverlay.setFocusStops(focusStops);
   }
 
   _handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
index d7b4ef3..2972d24 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
@@ -101,12 +101,12 @@
   test('submit blocked when invalid email is supplied to ccs', () => {
     const sendStub = sinon.stub(element, 'send').returns(Promise.resolve());
 
-    element.$.ccs.$.entry.setText('test');
+    element.$.ccs.entry!.setText('test');
     MockInteractions.tap(queryAndAssert(element, 'gr-button.send'));
     assert.isFalse(sendStub.called);
     flush();
 
-    element.$.ccs.$.entry.setText('test@test.test');
+    element.$.ccs.entry!.setText('test@test.test');
     MockInteractions.tap(queryAndAssert(element, 'gr-button.send'));
     assert.isTrue(sendStub.called);
   });
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index b91538b..c7b1982 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -116,6 +116,7 @@
 import {resolve, DIPolymerElement} from '../../../models/dependency';
 import {changeModelToken} from '../../../models/change/change-model';
 import {LabelNameToValuesMap} from '../../../api/rest-api';
+import {ValueChangedEvent} from '../../../types/events';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
@@ -477,6 +478,7 @@
 
   getFocusStops() {
     const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
+    if (!this.$.reviewers.focusStart) return undefined;
     return {
       start: this.$.reviewers.focusStart,
       end,
@@ -666,10 +668,10 @@
       setTimeout(() => textarea.getNativeTextarea().focus());
     } else if (section === FocusTarget.REVIEWERS) {
       const reviewerEntry = this.$.reviewers.focusStart;
-      setTimeout(() => reviewerEntry.focus());
+      setTimeout(() => reviewerEntry?.focus());
     } else if (section === FocusTarget.CCS) {
       const ccEntry = this.$.ccs.focusStart;
-      setTimeout(() => ccEntry.focus());
+      setTimeout(() => ccEntry?.focus());
     }
   }
 
@@ -1265,6 +1267,33 @@
       Object.keys(this.getLabelScores().getLabelValues(false)).length !== 0;
   }
 
+  // To decouple account-list and reply dialog
+  _getAccountListCopy(list: (AccountInfo | GroupInfo)[]) {
+    return list.slice();
+  }
+
+  _handleReviewersChanged(e: ValueChangedEvent<(AccountInfo | GroupInfo)[]>) {
+    this._reviewers = e.detail.value.slice();
+    this._reviewersMutated = true;
+  }
+
+  _handleCcsChanged(e: ValueChangedEvent<(AccountInfo | GroupInfo)[]>) {
+    this._ccs = e.detail.value.slice();
+    this._reviewersMutated = true;
+  }
+
+  _handleReviewersConfirmationChanged(
+    e: ValueChangedEvent<SuggestedReviewerGroupInfo | null>
+  ) {
+    this._reviewerPendingConfirmation = e.detail.value;
+  }
+
+  _handleCcsConfirmationChanged(
+    e: ValueChangedEvent<SuggestedReviewerGroupInfo | null>
+  ) {
+    this._ccPendingConfirmation = e.detail.value;
+  }
+
   _isState(knownLatestState?: LatestPatchState, value?: LatestPatchState) {
     return knownLatestState === value;
   }
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 c1d7cb1..c4b3578 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
@@ -266,11 +266,13 @@
           <div class="peopleListLabel">Reviewers</div>
           <gr-account-list
             id="reviewers"
-            accounts="{{_reviewers}}"
+            accounts="[[_getAccountListCopy(_reviewers)]]"
             on-account-added="accountAdded"
+            on-accounts-changed="_handleReviewersChanged"
             removable-values="[[change.removable_reviewers]]"
             filter="[[filterReviewerSuggestion]]"
-            pending-confirmation="{{_reviewerPendingConfirmation}}"
+            pending-confirmation="[[_reviewerPendingConfirmation]]"
+            on-pending-confirmation-changed="_handleReviewersConfirmationChanged"
             placeholder="Add reviewer..."
             on-account-text-changed="_handleAccountTextEntry"
             suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
@@ -284,10 +286,12 @@
         <div class="peopleListLabel">CC</div>
         <gr-account-list
           id="ccs"
-          accounts="{{_ccs}}"
+          accounts="[[_getAccountListCopy(_ccs)]]"
           on-account-added="accountAdded"
+          on-accounts-changed="_handleCcsChanged"
           filter="[[filterCCSuggestion]]"
-          pending-confirmation="{{_ccPendingConfirmation}}"
+          pending-confirmation="[[_ccPendingConfirmation]]"
+          pending-confirmation-changed="_handleCcsConfirmationChanged"
           allow-any-input=""
           placeholder="Add CC..."
           on-account-text-changed="_handleAccountTextEntry"
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 e324cf2d..883fbfa 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
@@ -1184,7 +1184,7 @@
     // We should be focused on account entry input.
     assert.isTrue(
       isFocusInsideElement(
-        queryAndAssert<GrAccountList>(element, '#reviewers').$.entry.$.input.$
+        queryAndAssert<GrAccountList>(element, '#reviewers').entry!.$.input.$
           .input
       )
     );
@@ -1245,13 +1245,13 @@
     if (cc) {
       assert.isTrue(
         isFocusInsideElement(
-          queryAndAssert<GrAccountList>(element, '#ccs').$.entry.$.input.$.input
+          queryAndAssert<GrAccountList>(element, '#ccs').entry!.$.input.$.input
         )
       );
     } else {
       assert.isTrue(
         isFocusInsideElement(
-          queryAndAssert<GrAccountList>(element, '#reviewers').$.entry.$.input.$
+          queryAndAssert<GrAccountList>(element, '#reviewers').entry!.$.input.$
             .input
         )
       );
@@ -1683,13 +1683,14 @@
     const cc1 = makeAccount();
     const cc2 = makeAccount();
     const cc3 = makeAccount();
-    element._reviewers = [reviewer1, reviewer2];
-    element._ccs = [cc1, cc2, cc3];
-
     element.change!.reviewers = {
       [ReviewerState.CC]: [],
       [ReviewerState.REVIEWER]: [{_account_id: 33 as AccountId}],
     };
+    await flush();
+
+    element._reviewers = [reviewer1, reviewer2];
+    element._ccs = [cc1, cc2, cc3];
 
     const mutations: ReviewerInput[] = [];
 
@@ -1697,6 +1698,8 @@
       mutations.push(...review.reviewers!);
     });
 
+    assert.isFalse(element._reviewersMutated);
+
     // Remove and add to other field.
     reviewers.dispatchEvent(
       new CustomEvent('remove', {
@@ -1705,7 +1708,10 @@
         bubbles: true,
       })
     );
-    ccs.$.entry.dispatchEvent(
+
+    await flush();
+    assert.isTrue(element._reviewersMutated);
+    ccs.entry!.dispatchEvent(
       new CustomEvent('add', {
         detail: {value: {account: reviewer1}},
         composed: true,
@@ -1726,7 +1732,7 @@
         bubbles: true,
       })
     );
-    reviewers.$.entry.dispatchEvent(
+    reviewers.entry!.dispatchEvent(
       new CustomEvent('add', {
         detail: {value: {account: cc1}},
         composed: true,
@@ -1736,14 +1742,14 @@
 
     // Add to other field without removing from former field.
     // (Currently not possible in UI, but this is a good consistency check).
-    reviewers.$.entry.dispatchEvent(
+    reviewers.entry!.dispatchEvent(
       new CustomEvent('add', {
         detail: {value: {account: cc2}},
         composed: true,
         bubbles: true,
       })
     );
-    ccs.$.entry.dispatchEvent(
+    ccs.entry!.dispatchEvent(
       new CustomEvent('add', {
         detail: {value: {account: reviewer2}},
         composed: true,
@@ -1766,6 +1772,7 @@
 
     // Send and purge and verify moves, delete cc3.
     await element.send(false, false);
+    await flush();
     expect(mutations).to.have.lengthOf(5);
     expect(mutations[0]).to.deep.equal(
       mapReviewer(cc1, ReviewerState.REVIEWER)
@@ -1814,7 +1821,7 @@
         bubbles: true,
       })
     );
-    ccs.$.entry.dispatchEvent(
+    ccs.entry!.dispatchEvent(
       new CustomEvent('add', {
         detail: {value: {account: reviewer1}},
         composed: true,
@@ -2216,7 +2223,7 @@
     await flush();
 
     assert.equal(
-      element.getFocusStops().end,
+      element.getFocusStops()!.end,
       queryAndAssert(element, '#cancelButton')
     );
     element.draftCommentThreads = [
@@ -2234,7 +2241,7 @@
     await flush();
 
     assert.equal(
-      element.getFocusStops().end,
+      element.getFocusStops()!.end,
       queryAndAssert(element, '#sendButton')
     );
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index 1553eef..cf24bcd 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -16,11 +16,7 @@
  */
 import '../gr-account-chip/gr-account-chip';
 import '../gr-account-entry/gr-account-entry';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-account-list_html';
 import {getAppContext} from '../../../services/app-context';
-import {customElement, property} from '@polymer/decorators';
 import {
   ChangeInfo,
   Suggestion,
@@ -30,20 +26,29 @@
   SuggestedReviewerGroupInfo,
   SuggestedReviewerAccountInfo,
 } from '../../../types/common';
-import {
-  ReviewerSuggestionsProvider,
-  SuggestionItem,
-} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {ReviewerSuggestionsProvider} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
 import {GrAccountEntry} from '../gr-account-entry/gr-account-entry';
 import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
 import {PaperInputElementExt} from '../../../types/types';
-import {fireAlert, fire} from '../../../utils/event-util';
+import {fire, fireAlert} from '../../../utils/event-util';
 import {accountOrGroupKey} from '../../../utils/account-util';
+import {LitElement, css, html, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {classMap} from 'lit/directives/class-map';
+import {
+  AutocompleteQuery,
+  AutocompleteSuggestion,
+} from '../gr-autocomplete/gr-autocomplete';
+import {ValueChangedEvent} from '../../../types/events';
 
 const VALID_EMAIL_ALERT = 'Please input a valid email.';
 
 declare global {
+  interface HTMLElementEventMap {
+    'accounts-changed': ValueChangedEvent<(AccountInfo | GroupInfo)[]>;
+    'pending-confirmation-changed': ValueChangedEvent<SuggestedReviewerGroupInfo | null>;
+  }
   interface HTMLElementTagNameMap {
     'gr-account-list': GrAccountList;
   }
@@ -51,13 +56,6 @@
     'account-added': CustomEvent<AccountInputDetail>;
   }
 }
-
-export interface GrAccountList {
-  $: {
-    entry: GrAccountEntry;
-  };
-}
-
 export interface AccountInputDetail {
   account: AccountInput;
 }
@@ -115,18 +113,15 @@
 }
 
 @customElement('gr-account-list')
-export class GrAccountList extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrAccountList extends LitElement {
   /**
    * Fired when user inputs an invalid email address.
    *
    * @event show-alert
    */
+  @query('#entry') entry?: GrAccountEntry;
 
-  @property({type: Array, notify: true})
+  @property({type: Array})
   accounts: AccountInput[] = [];
 
   @property({type: Object})
@@ -135,7 +130,7 @@
   @property({type: Object})
   filter?: (input: Suggestion) => boolean;
 
-  @property({type: String})
+  @property()
   placeholder = '';
 
   @property({type: Boolean})
@@ -150,7 +145,7 @@
   /**
    * Needed for template checking since value is initially set to null.
    */
-  @property({type: Object, notify: true})
+  @property({type: Object})
   pendingConfirmation: SuggestedReviewerGroupInfo | null = null;
 
   @property({type: Boolean})
@@ -159,7 +154,7 @@
   /**
    * When true, allows for non-suggested inputs to be added.
    */
-  @property({type: Boolean})
+  @property({type: Boolean, attribute: 'allow-any-input'})
   allowAnyInput = false;
 
   /**
@@ -175,8 +170,7 @@
   /**
    * Returns suggestion items
    */
-  @property({type: Object})
-  _querySuggestions: (input: string) => Promise<SuggestionItem[]>;
+  @state() private querySuggestions: AutocompleteQuery;
 
   private readonly reporting = getAppContext().reportingService;
 
@@ -184,21 +178,92 @@
 
   constructor() {
     super();
-    this._querySuggestions = input => this._getSuggestions(input);
+    this.querySuggestions = input => this.getSuggestions(input);
     this.addEventListener('remove', e =>
-      this._handleRemove(e as CustomEvent<{account: AccountInput}>)
+      this.handleRemove(e as CustomEvent<{account: AccountInput}>)
     );
   }
 
-  get accountChips() {
-    return Array.from(this.root?.querySelectorAll('gr-account-chip') || []);
+  static override styles = [
+    sharedStyles,
+    css`
+      gr-account-chip {
+        display: inline-block;
+        margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
+      }
+      gr-account-entry {
+        display: flex;
+        flex: 1;
+        min-width: 10em;
+        margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
+      }
+      .group {
+        --account-label-suffix: ' (group)';
+      }
+      .pending-add {
+        font-style: italic;
+      }
+      .list {
+        align-items: center;
+        display: flex;
+        flex-wrap: wrap;
+      }
+    `,
+  ];
+
+  override render() {
+    return html`<div class="list">
+        ${this.accounts.map(
+          account => html`
+            <gr-account-chip
+              .account=${account}
+              class=${classMap({
+                group: !!account._group,
+                pendingAdd: !!account._pendingAdd,
+              })}
+              ?removable=${this.computeRemovable(account)}
+              @keydown=${this.handleChipKeydown}
+              tabindex="-1"
+            >
+            </gr-account-chip>
+          `
+        )}
+      </div>
+      <gr-account-entry
+        borderless=""
+        ?hidden=${(this.maxCount && this.maxCount <= this.accounts.length) ||
+        this.readonly}
+        id="entry"
+        .placeholder=${this.placeholder}
+        @add=${this.handleAdd}
+        @keydown=${this.handleInputKeydown}
+        .allowAnyInput=${this.allowAnyInput}
+        .querySuggestions=${this.querySuggestions}
+      >
+      </gr-account-entry>
+      <slot></slot>`;
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('pendingConfirmation')) {
+      fire(this, 'pending-confirmation-changed', {
+        value: this.pendingConfirmation,
+      });
+    }
+  }
+
+  get accountChips(): GrAccountChip[] {
+    return Array.from(
+      this.shadowRoot?.querySelectorAll('gr-account-chip') || []
+    );
   }
 
   get focusStart() {
-    return this.$.entry.focusStart;
+    // Entry is always defined and we cannot return undefined.
+    return this.entry?.focusStart;
   }
 
-  _getSuggestions(input: string) {
+  getSuggestions(input: string): Promise<AutocompleteSuggestion[]> {
     const provider = this.suggestionsProvider;
     if (!provider) return Promise.resolve([]);
     return provider.getSuggestions(input).then(suggestions => {
@@ -212,8 +277,11 @@
     });
   }
 
-  _handleAdd(e: CustomEvent<{value: RawAccountInput}>) {
-    this.addAccountItem(e.detail.value);
+  // private but used in test
+  handleAdd(e: ValueChangedEvent<string>) {
+    // TODO(TS) this is temporary hack to avoid cascade of ts issues
+    const item = e.detail.value as RawAccountInput;
+    this.addAccountItem(item);
   }
 
   addAccountItem(item: RawAccountInput) {
@@ -226,7 +294,7 @@
     if (isAccountObject(item)) {
       account = {...item.account, _pendingAdd: true};
       this.removeFromPendingRemoval(account);
-      this.push('accounts', account);
+      this.accounts.push(account);
       itemTypeAdded = 'account';
     } else if (isSuggestedReviewerGroupInfo(item)) {
       if (item.confirm) {
@@ -234,41 +302,45 @@
         return;
       }
       group = {...item.group, _pendingAdd: true, _group: true};
-      this.push('accounts', group);
+      this.accounts.push(group);
       this.removeFromPendingRemoval(group);
       itemTypeAdded = 'group';
     } else if (this.allowAnyInput) {
       if (!item.includes('@')) {
         // Repopulate the input with what the user tried to enter and have
         // a toast tell them why they can't enter it.
-        this.$.entry.setText(item);
+        this.entry?.setText(item);
         fireAlert(this, VALID_EMAIL_ALERT);
         return false;
       } else {
         account = {email: item as EmailAddress, _pendingAdd: true};
-        this.push('accounts', account);
+        this.accounts.push(account);
         this.removeFromPendingRemoval(account);
         itemTypeAdded = 'email';
       }
     }
-
+    fire(this, 'accounts-changed', {value: this.accounts.slice()});
     fire(this, 'account-added', {account: (account ?? group)! as AccountInput});
     this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
     this.pendingConfirmation = null;
+    this.requestUpdate();
     return true;
   }
 
   confirmGroup(group: GroupInfo) {
-    this.push('accounts', {
+    this.accounts.push({
       ...group,
       confirmed: true,
       _pendingAdd: true,
       _group: true,
     });
     this.pendingConfirmation = null;
+    fire(this, 'accounts-changed', {value: this.accounts});
+    this.requestUpdate();
   }
 
-  _computeChipClass(account: AccountInput) {
+  // private but used in test
+  computeChipClass(account: AccountInput) {
     const classes = [];
     if (account._group) {
       classes.push('group');
@@ -279,8 +351,9 @@
     return classes.join(' ');
   }
 
-  _computeRemovable(account: AccountInput, readonly: boolean) {
-    if (readonly) {
+  // private but used in test
+  computeRemovable(account: AccountInput) {
+    if (this.readonly) {
       return false;
     }
     if (this.removableValues) {
@@ -297,21 +370,23 @@
     return true;
   }
 
-  _handleRemove(e: CustomEvent<{account: AccountInput}>) {
+  private handleRemove(e: CustomEvent<{account: AccountInput}>) {
     const toRemove = e.detail.account;
     this.removeAccount(toRemove);
-    this.$.entry.focus();
+    this.entry?.focus();
   }
 
   removeAccount(toRemove?: AccountInput) {
-    if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+    if (!toRemove || !this.computeRemovable(toRemove)) {
       return;
     }
     for (let i = 0; i < this.accounts.length; i++) {
       if (accountOrGroupKey(toRemove) === accountOrGroupKey(this.accounts[i])) {
-        this.splice('accounts', i, 1);
+        this.accounts.splice(i, 1);
         this.pendingRemoval.add(toRemove);
         this.reporting.reportInteraction(`Remove from ${this.id}`);
+        this.requestUpdate();
+        fire(this, 'accounts-changed', {value: this.accounts.slice()});
         return;
       }
     }
@@ -320,23 +395,23 @@
     );
   }
 
-  _getNativeInput(paperInput: PaperInputElementExt) {
+  // private but used in test
+  getOwnNativeInput(paperInput: PaperInputElementExt) {
     // In Polymer 2 inputElement isn't nativeInput anymore
     return (paperInput.$.nativeInput ||
       paperInput.inputElement) as HTMLTextAreaElement;
   }
 
-  _handleInputKeydown(
-    e: CustomEvent<{input: PaperInputElementExt; keyCode: number}>
-  ) {
-    const input = this._getNativeInput(e.detail.input);
+  private handleInputKeydown(e: KeyboardEvent) {
+    const target = e.target as GrAccountEntry;
+    const input = this.getOwnNativeInput(target.$.input.$.input);
     if (
       input.selectionStart !== input.selectionEnd ||
       input.selectionStart !== 0
     ) {
       return;
     }
-    switch (e.detail.keyCode) {
+    switch (e.keyCode) {
       case 8: // Backspace
         this.removeAccount(this.accounts[this.accounts.length - 1]);
         break;
@@ -348,7 +423,7 @@
     }
   }
 
-  _handleChipKeydown(e: KeyboardEvent) {
+  private handleChipKeydown(e: KeyboardEvent) {
     const chip = e.target as GrAccountChip;
     const chips = this.accountChips;
     const index = chips.indexOf(chip);
@@ -366,7 +441,7 @@
         } else if (index > 0) {
           chips[index - 1].focus();
         } else {
-          this.$.entry.focus();
+          this.entry?.focus();
         }
         break;
       case 37: // Left arrow
@@ -380,7 +455,7 @@
         if (index < chips.length - 1) {
           chips[index + 1].focus();
         } else {
-          this.$.entry.focus();
+          this.entry?.focus();
         }
         break;
     }
@@ -395,13 +470,13 @@
    * return true.
    */
   submitEntryText() {
-    const text = this.$.entry.getText();
-    if (!text.length) {
+    const text = this.entry?.getText();
+    if (!text?.length) {
       return true;
     }
     const wasSubmitted = this.addAccountItem(text);
     if (wasSubmitted) {
-      this.$.entry.clear();
+      this.entry?.clear();
     }
     return wasSubmitted;
   }
@@ -432,19 +507,11 @@
     });
   }
 
-  removeFromPendingRemoval(account: AccountInput) {
+  private removeFromPendingRemoval(account: AccountInput) {
     this.pendingRemoval.delete(account);
   }
 
   clearPendingRemovals() {
     this.pendingRemoval.clear();
   }
-
-  _computeEntryHidden(
-    maxCount: number,
-    accountsRecord: PolymerDeepPropertyChange<AccountInput[], AccountInput[]>,
-    readonly: boolean
-  ) {
-    return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
-  }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.ts
deleted file mode 100644
index 7a47e29..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    gr-account-chip {
-      display: inline-block;
-      margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
-    }
-    gr-account-entry {
-      display: flex;
-      flex: 1;
-      min-width: 10em;
-      margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
-    }
-    .group {
-      --account-label-suffix: ' (group)';
-    }
-    .pending-add {
-      font-style: italic;
-    }
-    .list {
-      align-items: center;
-      display: flex;
-      flex-wrap: wrap;
-    }
-  </style>
-  <!--
-      NOTE(Issue 6419): Nest the inner dom-repeat template in a div rather than
-      as a direct child of the dom-module's template.
-    -->
-  <div class="list">
-    <template id="chips" is="dom-repeat" items="[[accounts]]" as="account">
-      <gr-account-chip
-        account="[[account]]"
-        class$="[[_computeChipClass(account)]]"
-        data-account-id$="[[account._account_id]]"
-        removable="[[_computeRemovable(account, readonly)]]"
-        on-keydown="_handleChipKeydown"
-        tabindex="-1"
-      >
-      </gr-account-chip>
-    </template>
-  </div>
-  <gr-account-entry
-    borderless=""
-    hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]"
-    id="entry"
-    placeholder="[[placeholder]]"
-    on-add="_handleAdd"
-    on-input-keydown="_handleInputKeydown"
-    allow-any-input="[[allowAnyInput]]"
-    query-suggestions="[[_querySuggestions]]"
-  >
-  </gr-account-entry>
-  <slot></slot>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index d77dec3..7509023 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -28,7 +28,6 @@
   GroupBaseInfo,
   GroupId,
   GroupName,
-  SuggestedReviewerAccountInfo,
   Suggestion,
 } from '../../../types/common';
 import {queryAll} from '../../../test/test-utils';
@@ -52,7 +51,7 @@
           _account_id: 1 as AccountId,
         } as AccountInfo,
         count: 1,
-      } as SuggestedReviewerAccountInfo,
+      } as unknown as string,
     };
   }
 }
@@ -85,12 +84,14 @@
   }
 
   function handleAdd(value: RawAccountInput) {
-    element._handleAdd(
-      new CustomEvent<{value: RawAccountInput}>('add', {detail: {value}})
+    element.handleAdd(
+      new CustomEvent<{value: string}>('add', {
+        detail: {value: value as unknown as string},
+      })
     );
   }
 
-  setup(() => {
+  setup(async () => {
     existingAccount1 = makeAccount();
     existingAccount2 = makeAccount();
 
@@ -98,18 +99,33 @@
     element.accounts = [existingAccount1, existingAccount2];
     suggestionsProvider = new MockSuggestionsProvider();
     element.suggestionsProvider = suggestionsProvider;
+    await element.updateComplete;
   });
 
-  test('account entry only appears when editable', () => {
+  test('renders', () => {
+    expect(element).shadowDom.to.equal(
+      /* HTML */
+      `<div class="list">
+          <gr-account-chip removable="" tabindex="-1"> </gr-account-chip>
+          <gr-account-chip removable="" tabindex="-1"> </gr-account-chip>
+        </div>
+        <gr-account-entry borderless="" id="entry"></gr-account-entry>
+        <slot></slot>`
+    );
+  });
+
+  test('account entry only appears when editable', async () => {
     element.readonly = false;
-    assert.isFalse(element.$.entry.hasAttribute('hidden'));
+    await element.updateComplete;
+    assert.isFalse(element.entry!.hasAttribute('hidden'));
     element.readonly = true;
-    assert.isTrue(element.$.entry.hasAttribute('hidden'));
+    await element.updateComplete;
+    assert.isTrue(element.entry!.hasAttribute('hidden'));
   });
 
-  test('addition and removal of account/group chips', () => {
-    flush();
-    sinon.stub(element, '_computeRemovable').returns(true);
+  test('addition and removal of account/group chips', async () => {
+    await element.updateComplete;
+    sinon.stub(element, 'computeRemovable').returns(true);
     // Existing accounts are listed.
     let chips = getChips();
     assert.equal(chips.length, 2);
@@ -119,7 +135,7 @@
     // New accounts are added to end with pendingAdd class.
     const newAccount = makeAccount();
     handleAdd({account: newAccount, count: 1});
-    flush();
+    await element.updateComplete;
     chips = getChips();
     assert.equal(chips.length, 3);
     assert.isFalse(chips[0].classList.contains('pendingAdd'));
@@ -134,7 +150,7 @@
         bubbles: true,
       })
     );
-    flush();
+    await element.updateComplete;
     chips = getChips();
     assert.equal(chips.length, 2);
     assert.isFalse(chips[0].classList.contains('pendingAdd'));
@@ -155,7 +171,7 @@
         bubbles: true,
       })
     );
-    flush();
+    await element.updateComplete;
     chips = getChips();
     assert.equal(chips.length, 1);
     assert.isFalse(chips[0].classList.contains('pendingAdd'));
@@ -163,7 +179,7 @@
     // New groups are added to end with pendingAdd and group classes.
     const newGroup = makeGroup();
     handleAdd({group: newGroup, confirm: false, count: 1});
-    flush();
+    await element.updateComplete;
     chips = getChips();
     assert.equal(chips.length, 2);
     assert.isTrue(chips[1].classList.contains('group'));
@@ -177,13 +193,13 @@
         bubbles: true,
       })
     );
-    flush();
+    await element.updateComplete;
     chips = getChips();
     assert.equal(chips.length, 1);
     assert.isFalse(chips[0].classList.contains('pendingAdd'));
   });
 
-  test('_getSuggestions uses filter correctly', () => {
+  test('getSuggestions uses filter correctly', () => {
     const originalSuggestions: Suggestion[] = [
       {
         email: 'abc@example.com' as EmailAddress,
@@ -212,12 +228,12 @@
           value: {
             account: suggestion as AccountInfo,
             count: 1,
-          },
+          } as unknown as string,
         };
       });
 
     return element
-      ._getSuggestions('')
+      .getSuggestions('')
       .then(suggestions => {
         // Default is no filtering.
         assert.equal(suggestions.length, 3);
@@ -228,7 +244,7 @@
           return (suggestion as AccountInfo)._account_id === accountId;
         };
 
-        return element._getSuggestions('');
+        return element.getSuggestions('');
       })
       .then(suggestions => {
         assert.deepEqual(suggestions, [
@@ -237,52 +253,55 @@
             value: {
               account: originalSuggestions[0] as AccountInfo,
               count: 1,
-            },
+            } as unknown as string,
           },
         ]);
       });
   });
 
-  test('_computeChipClass', () => {
+  test('computeChipClass', () => {
     const account = makeAccount() as AccountInfoInput;
-    assert.equal(element._computeChipClass(account), '');
+    assert.equal(element.computeChipClass(account), '');
     account._pendingAdd = true;
-    assert.equal(element._computeChipClass(account), 'pendingAdd');
+    assert.equal(element.computeChipClass(account), 'pendingAdd');
     account._group = true;
-    assert.equal(element._computeChipClass(account), 'group pendingAdd');
+    assert.equal(element.computeChipClass(account), 'group pendingAdd');
     account._pendingAdd = false;
-    assert.equal(element._computeChipClass(account), 'group');
+    assert.equal(element.computeChipClass(account), 'group');
   });
 
-  test('_computeRemovable', () => {
+  test('computeRemovable', async () => {
     const newAccount = makeAccount() as AccountInfoInput;
     newAccount._pendingAdd = true;
     element.readonly = false;
     element.removableValues = [];
-    assert.isFalse(element._computeRemovable(existingAccount1, false));
-    assert.isTrue(element._computeRemovable(newAccount, false));
+    element.updateComplete;
+    assert.isFalse(element.computeRemovable(existingAccount1));
+    assert.isTrue(element.computeRemovable(newAccount));
 
     element.removableValues = [existingAccount1];
-    assert.isTrue(element._computeRemovable(existingAccount1, false));
-    assert.isTrue(element._computeRemovable(newAccount, false));
-    assert.isFalse(element._computeRemovable(existingAccount2, false));
+    element.updateComplete;
+    assert.isTrue(element.computeRemovable(existingAccount1));
+    assert.isTrue(element.computeRemovable(newAccount));
+    assert.isFalse(element.computeRemovable(existingAccount2));
 
     element.readonly = true;
-    assert.isFalse(element._computeRemovable(existingAccount1, true));
-    assert.isFalse(element._computeRemovable(newAccount, true));
+    element.updateComplete;
+    assert.isFalse(element.computeRemovable(existingAccount1));
+    assert.isFalse(element.computeRemovable(newAccount));
   });
 
-  test('submitEntryText', () => {
+  test('submitEntryText', async () => {
     element.allowAnyInput = true;
-    flush();
+    await element.updateComplete;
 
-    const getTextStub = sinon.stub(element.$.entry, 'getText');
+    const getTextStub = sinon.stub(element.entry!, 'getText');
     getTextStub.onFirstCall().returns('');
     getTextStub.onSecondCall().returns('test');
     getTextStub.onThirdCall().returns('test@test');
 
     // When entry is empty, return true.
-    const clearStub = sinon.stub(element.$.entry, 'clear');
+    const clearStub = sinon.stub(element.entry!, 'clear');
     assert.isTrue(element.submitEntryText());
     assert.isFalse(clearStub.called);
 
@@ -363,12 +382,12 @@
     assert.equal(element.accounts.length, 1);
   });
 
-  test('max-count', () => {
+  test('max-count', async () => {
     element.maxCount = 1;
     const acct = makeAccount();
     handleAdd({account: acct, count: 1});
-    flush();
-    assert.isTrue(element.$.entry.hasAttribute('hidden'));
+    await element.updateComplete;
+    assert.isTrue(element.entry!.hasAttribute('hidden'));
   });
 
   test('enter text calls suggestions provider', async () => {
@@ -391,12 +410,12 @@
       'makeSuggestionItem'
     );
 
-    const input = element.$.entry.$.input;
+    const input = element.entry!.$.input;
 
     input.text = 'newTest';
     MockInteractions.focus(input.$.input);
     input.noDebounce = true;
-    await flush();
+    await element.updateComplete;
     assert.isTrue(getSuggestionsStub.calledOnce);
     assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
     assert.equal(makeSuggestionItemSpy.getCalls().length, 2);
@@ -427,38 +446,38 @@
 
   suite('keyboard interactions', () => {
     test('backspace at text input start removes last account', async () => {
-      const input = element.$.entry.$.input;
+      const input = element.entry!.$.input;
       sinon.stub(input, '_updateSuggestions');
-      sinon.stub(element, '_computeRemovable').returns(true);
-      await flush();
+      sinon.stub(element, 'computeRemovable').returns(true);
+      await await element.updateComplete;
       // Next line is a workaround for Firefox not moving cursor
       // on input field update
-      assert.equal(element._getNativeInput(input.$.input).selectionStart, 0);
+      assert.equal(element.getOwnNativeInput(input.$.input).selectionStart, 0);
       input.text = 'test';
       MockInteractions.focus(input.$.input);
-      flush();
+      await element.updateComplete;
       assert.equal(element.accounts.length, 2);
       MockInteractions.pressAndReleaseKeyOn(
-        element._getNativeInput(input.$.input),
+        element.getOwnNativeInput(input.$.input),
         8
       ); // Backspace
       assert.equal(element.accounts.length, 2);
       input.text = '';
       MockInteractions.pressAndReleaseKeyOn(
-        element._getNativeInput(input.$.input),
+        element.getOwnNativeInput(input.$.input),
         8
       ); // Backspace
-      flush();
+      await element.updateComplete;
       assert.equal(element.accounts.length, 1);
     });
 
     test('arrow key navigation', async () => {
-      const input = element.$.entry.$.input;
+      const input = element.entry!.$.input;
       input.text = '';
       element.accounts = [makeAccount(), makeAccount()];
-      flush();
+      await element.updateComplete;
       MockInteractions.focus(input.$.input);
-      await flush();
+      await await element.updateComplete;
       const chips = element.accountChips;
       const chipsOneSpy = sinon.spy(chips[1], 'focus');
       MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
@@ -472,9 +491,9 @@
       assert.isTrue(chipsOneSpy.calledTwice);
     });
 
-    test('delete', () => {
+    test('delete', async () => {
       element.accounts = [makeAccount(), makeAccount()];
-      flush();
+      await element.updateComplete;
       const focusSpy = sinon.spy(element.accountChips[1], 'focus');
       const removeSpy = sinon.spy(element, 'removeAccount');
       MockInteractions.pressAndReleaseKeyOn(element.accountChips[0], 8); // Backspace
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index a685f32..36baf87 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -54,7 +54,7 @@
   name?: string;
   label?: string;
   value?: T;
-  text?: T;
+  text?: string;
 }
 
 export interface AutocompleteCommitEventDetail {
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts
deleted file mode 100644
index 77ba8cd..0000000
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../elements/shared/gr-button/gr-button';
-import {html, LitElement} from 'lit';
-import {customElement, property, state} from 'lit/decorators';
-import {DiffInfo, DiffViewMode, RenderPreferences} from '../../../api/diff';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {diffClasses} from '../gr-diff/gr-diff-utils';
-import {getShowConfig} from './gr-context-controls';
-import {ifDefined} from 'lit/directives/if-defined';
-
-@customElement('gr-context-controls-section')
-export class GrContextControlsSection extends LitElement {
-  /** Should context controls be rendered for expanding above the section? */
-  @property({type: Boolean}) showAbove = false;
-
-  /** Should context controls be rendered for expanding below the section? */
-  @property({type: Boolean}) showBelow = false;
-
-  @property({type: Object}) viewMode = DiffViewMode.SIDE_BY_SIDE;
-
-  /** Must be of type GrDiffGroupType.CONTEXT_CONTROL. */
-  @property({type: Object})
-  group?: GrDiffGroup;
-
-  @property({type: Object})
-  diff?: DiffInfo;
-
-  @property({type: Object})
-  renderPrefs?: RenderPreferences;
-
-  /**
-   * Semantic DOM diff testing does not work with just table fragments, so when
-   * running such tests the render() method has to wrap the DOM in a proper
-   * <table> element.
-   */
-  @state()
-  addTableWrapperForTesting = false;
-
-  /**
-   * The browser API for handling selection does not (yet) work for selection
-   * across multiple shadow DOM elements. So we are rendering gr-diff components
-   * into the light DOM instead of the shadow DOM by overriding this method,
-   * which was the recommended workaround by the lit team.
-   * See also https://github.com/WICG/webcomponents/issues/79.
-   */
-  override createRenderRoot() {
-    return this;
-  }
-
-  private renderPaddingRow(whereClass: 'above' | 'below') {
-    if (!this.showAbove && whereClass === 'above') return;
-    if (!this.showBelow && whereClass === 'below') return;
-    const sideBySide = this.viewMode === DiffViewMode.SIDE_BY_SIDE;
-    const modeClass = sideBySide ? 'side-by-side' : 'unified';
-    const type = sideBySide ? GrDiffGroupType.CONTEXT_CONTROL : undefined;
-    return html`
-      <tr
-        class=${diffClasses('contextBackground', modeClass, whereClass)}
-        left-type=${ifDefined(type)}
-        right-type=${ifDefined(type)}
-      >
-        <td class=${diffClasses('blame')} data-line-number="0"></td>
-        <td class=${diffClasses('contextLineNum')}></td>
-        ${sideBySide ? html`<td class=${diffClasses()}></td>` : ''}
-        <td class=${diffClasses('contextLineNum')}></td>
-        <td class=${diffClasses()}></td>
-      </tr>
-    `;
-  }
-
-  private createContextControlRow() {
-    const sideBySide = this.viewMode === DiffViewMode.SIDE_BY_SIDE;
-    const showConfig = getShowConfig(this.showAbove, this.showBelow);
-    return html`
-      <tr class=${diffClasses('dividerRow', `show-${showConfig}`)}>
-        <td class=${diffClasses('blame')} data-line-number="0"></td>
-        ${sideBySide ? html`<td class=${diffClasses()}></td>` : ''}
-        <td class=${diffClasses('dividerCell')} colspan="3">
-          <gr-context-controls
-            .diff=${this.diff}
-            .renderPreferences=${this.renderPrefs}
-            .group=${this.group}
-            .showConfig=${showConfig}
-          >
-          </gr-context-controls>
-        </td>
-      </tr>
-    `;
-  }
-
-  override render() {
-    const rows = html`
-      ${this.renderPaddingRow('above')} ${this.createContextControlRow()}
-      ${this.renderPaddingRow('below')}
-    `;
-    if (this.addTableWrapperForTesting) {
-      return html`<table>
-        ${rows}
-      </table>`;
-    }
-    return rows;
-  }
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-context-controls-section': GrContextControlsSection;
-  }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
deleted file mode 100644
index 2c1043d..0000000
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup-karma';
-import './gr-context-controls-section';
-import {GrContextControlsSection} from './gr-context-controls-section';
-import {html} from 'lit';
-import {fixture} from '@open-wc/testing-helpers';
-
-suite('gr-context-controls-section test', () => {
-  let element: GrContextControlsSection;
-
-  setup(async () => {
-    element = await fixture<GrContextControlsSection>(
-      html`<gr-context-controls-section></gr-context-controls-section>`
-    );
-    element.addTableWrapperForTesting = true;
-    await element.updateComplete;
-  });
-
-  test('render: normal with showAbove and showBelow', async () => {
-    element.showAbove = true;
-    element.showBelow = true;
-    await element.updateComplete;
-    expect(element).lightDom.to.equal(/* HTML */ `
-      <table>
-        <tbody>
-          <tr
-            class="above contextBackground gr-diff side-by-side style-scope"
-            left-type="contextControl"
-            right-type="contextControl"
-          >
-            <td class="blame gr-diff style-scope" data-line-number="0"></td>
-            <td class="contextLineNum gr-diff style-scope"></td>
-            <td class="gr-diff style-scope"></td>
-            <td class="contextLineNum gr-diff style-scope"></td>
-            <td class="gr-diff style-scope"></td>
-          </tr>
-          <tr class="dividerRow gr-diff show-both style-scope">
-            <td class="blame gr-diff style-scope" data-line-number="0"></td>
-            <td class="gr-diff style-scope"></td>
-            <td class="dividerCell gr-diff style-scope" colspan="3">
-              <gr-context-controls showconfig="both"> </gr-context-controls>
-            </td>
-          </tr>
-          <tr
-            class="below contextBackground gr-diff side-by-side style-scope"
-            left-type="contextControl"
-            right-type="contextControl"
-          >
-            <td class="blame gr-diff style-scope" data-line-number="0"></td>
-            <td class="contextLineNum gr-diff style-scope"></td>
-            <td class="gr-diff style-scope"></td>
-            <td class="contextLineNum gr-diff style-scope"></td>
-            <td class="gr-diff style-scope"></td>
-          </tr>
-        </tbody>
-      </table>
-    `);
-  });
-});
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index a451700..77a5dfb 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -80,19 +80,6 @@
 
 export type GrContextControlsShowConfig = 'above' | 'below' | 'both';
 
-export function getShowConfig(
-  showAbove: boolean,
-  showBelow: boolean
-): GrContextControlsShowConfig {
-  if (showAbove && !showBelow) return 'above';
-  if (!showAbove && showBelow) return 'below';
-
-  // Note that !showAbove && !showBelow also intentionally returns 'both'.
-  // This means the file is completely collapsed, which is unusual, but at least
-  // happens in one test.
-  return 'both';
-}
-
 @customElement('gr-context-controls')
 export class GrContextControls extends LitElement {
   @property({type: Object}) renderPreferences?: RenderPreferences;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index 27ebe4e..aabdf57 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -25,7 +25,6 @@
 import {GrDiffBuilderImage} from './gr-diff-builder-image';
 import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
 import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
-import {GrDiffBuilderLit} from './gr-diff-builder-lit';
 import {CancelablePromise, util} from '../../../scripts/util';
 import {customElement, property, observe} from '@polymer/decorators';
 import {BlameInfo, ImageInfo} from '../../../types/common';
@@ -464,24 +463,13 @@
       // If the diff is binary, but not an image.
       return new GrDiffBuilderBinary(this.diff, localPrefs, this.diffElement);
     } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
-      const useLit = this.renderPrefs?.use_lit_components;
-      if (useLit) {
-        builder = new GrDiffBuilderLit(
-          this.diff,
-          localPrefs,
-          this.diffElement,
-          this._layers,
-          this.renderPrefs
-        );
-      } else {
-        builder = new GrDiffBuilderSideBySide(
-          this.diff,
-          localPrefs,
-          this.diffElement,
-          this._layers,
-          this.renderPrefs
-        );
-      }
+      builder = new GrDiffBuilderSideBySide(
+        this.diff,
+        localPrefs,
+        this.diffElement,
+        this._layers,
+        this.renderPrefs
+      );
     } else if (this.viewMode === DiffViewMode.UNIFIED) {
       builder = new GrDiffBuilderUnified(
         this.diff,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts
deleted file mode 100644
index 3687747..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts
+++ /dev/null
@@ -1,185 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {RenderPreferences} from '../../../api/diff';
-import {LineNumber} from '../gr-diff/gr-diff-line';
-import {GrDiffGroup} from '../gr-diff/gr-diff-group';
-import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {Side} from '../../../constants/constants';
-import {DiffLayer, notUndefined} from '../../../types/types';
-import {diffClasses} from '../gr-diff/gr-diff-utils';
-import {GrDiffBuilder} from './gr-diff-builder';
-import {BlameInfo} from '../../../types/common';
-import {html, render} from 'lit';
-import {GrDiffSection} from './gr-diff-section';
-import '../gr-context-controls/gr-context-controls';
-import './gr-diff-section';
-import {GrDiffRow} from './gr-diff-row';
-
-/**
- * Base class for builders that are creating the diff using Lit elements.
- */
-export class GrDiffBuilderLit extends GrDiffBuilder {
-  constructor(
-    diff: DiffInfo,
-    prefs: DiffPreferencesInfo,
-    outputEl: HTMLElement,
-    layers: DiffLayer[] = [],
-    renderPrefs?: RenderPreferences
-  ) {
-    super(diff, prefs, outputEl, layers, renderPrefs);
-  }
-
-  override getContentTdByLine(
-    lineNumber: LineNumber,
-    side?: Side,
-    _root: Element = this.outputEl
-  ): HTMLTableCellElement | null {
-    if (!side) return null;
-    const row = this.findRow(lineNumber, side);
-    return row?.getContentCell(side) ?? null;
-  }
-
-  override getLineElByNumber(lineNumber: LineNumber, side: Side) {
-    const row = this.findRow(lineNumber, side);
-    return row?.getLineNumberCell(side) ?? null;
-  }
-
-  private findRow(lineNumber?: LineNumber, side?: Side): GrDiffRow | undefined {
-    if (!side || !lineNumber) return undefined;
-    const group = this.findGroup(side, lineNumber);
-    if (!group) return undefined;
-    const section = this.findSection(group);
-    if (!section) return undefined;
-    return section.findRow(side, lineNumber);
-  }
-
-  private getDiffRows() {
-    const sections = [
-      ...this.outputEl.querySelectorAll<GrDiffSection>('gr-diff-section'),
-    ];
-    return sections.map(s => s.getDiffRows()).flat();
-  }
-
-  override getLineNumberRows(): HTMLTableRowElement[] {
-    const rows = this.getDiffRows();
-    return rows.map(r => r.getTableRow()).filter(notUndefined);
-  }
-
-  override getLineNumEls(side: Side): HTMLTableCellElement[] {
-    const rows = this.getDiffRows();
-    return rows.map(r => r.getLineNumberCell(side)).filter(notUndefined);
-  }
-
-  override getBlameTdByLine(lineNumber: number): Element | undefined {
-    return this.findRow(lineNumber, Side.LEFT)?.getBlameCell();
-  }
-
-  override getContentByLine(
-    lineNumber: LineNumber,
-    side?: Side,
-    _root?: HTMLElement
-  ): HTMLElement | null {
-    const cell = this.getContentTdByLine(lineNumber, side);
-    return (cell?.firstChild ?? null) as HTMLElement | null;
-  }
-
-  override renderContentByRange(
-    start: LineNumber,
-    end: LineNumber,
-    side: Side
-  ) {
-    // TODO: Revisit whether there is maybe a more efficient and reliable
-    // approach. renderContentByRange() is only used when layers announce
-    // updates. We have to look deeper into the design of layers anyway. So
-    // let's defer optimizing this code until a refactor of layers in general.
-    const groups = this.getGroupsByLineRange(start, end, side);
-    for (const group of groups) {
-      const section = this.findSection(group);
-      for (const row of section?.getDiffRows() ?? []) {
-        row.requestUpdate();
-      }
-    }
-  }
-
-  private findSection(group?: GrDiffGroup): GrDiffSection | undefined {
-    if (!group) return undefined;
-    const leftClass = `left-${group.lineRange.left.start_line}`;
-    const rightClass = `right-${group.lineRange.right.start_line}`;
-    return (
-      this.outputEl.querySelector<GrDiffSection>(
-        `gr-diff-section.${leftClass}.${rightClass}`
-      ) ?? undefined
-    );
-  }
-
-  override renderBlameByRange(
-    blameInfo: BlameInfo,
-    start: number,
-    end: number
-  ) {
-    for (let lineNumber = start; lineNumber <= end; lineNumber++) {
-      const row = this.findRow(lineNumber, Side.LEFT);
-      if (!row) continue;
-      row.blameInfo = blameInfo;
-    }
-  }
-
-  // TODO: Refactor this such that adding the move controls becomes part of the
-  // lit element.
-  protected override getMoveControlsConfig() {
-    return {
-      numberOfCells: 4, // How many cells does the diff table have?
-      movedOutIndex: 1, // Index of left content column in diff table.
-      movedInIndex: 3, // Index of right content column in diff table.
-      lineNumberCols: [0, 2], // Indices of line number columns in diff table.
-    };
-  }
-
-  protected override buildSectionElement(group: GrDiffGroup) {
-    const leftCl = `left-${group.lineRange.left.start_line}`;
-    const rightCl = `right-${group.lineRange.right.start_line}`;
-    const section = html`
-      <gr-diff-section
-        class="${leftCl} ${rightCl}"
-        .group=${group}
-        .diff=${this._diff}
-        .layers=${this.layers}
-        .diffPrefs=${this._prefs}
-        .renderPrefs=${this.renderPrefs}
-      ></gr-diff-section>
-    `;
-    // TODO: Refactor GrDiffBuilder.emitGroup() and buildSectionElement()
-    // such that we can render directly into the correct container.
-    const tempContainer = document.createElement('div');
-    render(section, tempContainer);
-    return tempContainer.firstElementChild as GrDiffSection;
-  }
-
-  override addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
-    render(
-      html`
-        <colgroup>
-         <col class=${diffClasses('blame')}></col>
-         <col class=${diffClasses(Side.LEFT)} width=${lineNumberWidth}></col>
-         <col class=${diffClasses(Side.LEFT)}></col>
-         <col class=${diffClasses(Side.RIGHT)} width=${lineNumberWidth}></col>
-         <col class=${diffClasses(Side.RIGHT)}></col>
-        </colgroup>
-      `,
-      outputEl
-    );
-  }
-
-  protected override getNextContentOnSide(
-    _content: HTMLElement,
-    _side: Side
-  ): HTMLElement | null {
-    // TODO: getNextContentOnSide() is not required by lit based rendering.
-    // So let's refactor it to be moved into gr-diff-builder-legacy.
-    console.warn('unimplemented method getNextContentOnSide() called');
-    return null;
-  }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
index 4efa238..add7ffa 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
@@ -194,7 +194,7 @@
     group.element = element;
   }
 
-  protected getGroupsByLineRange(
+  private getGroupsByLineRange(
     startLine: LineNumber,
     endLine: LineNumber,
     side: Side
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
deleted file mode 100644
index ae18e59..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {html, LitElement, TemplateResult} from 'lit';
-import {customElement, property, state} from 'lit/decorators';
-import {ifDefined} from 'lit/directives/if-defined';
-import {createRef, Ref, ref} from 'lit/directives/ref';
-import {
-  DiffResponsiveMode,
-  Side,
-  LineNumber,
-  DiffLayer,
-} from '../../../api/diff';
-import {BlameInfo} from '../../../types/common';
-import {assertIsDefined} from '../../../utils/common-util';
-import {fire} from '../../../utils/event-util';
-import {getBaseUrl} from '../../../utils/url-util';
-import './gr-diff-text';
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
-import {diffClasses, isResponsive} from '../gr-diff/gr-diff-utils';
-
-@customElement('gr-diff-row')
-export class GrDiffRow extends LitElement {
-  contentLeftRef: Ref<HTMLDivElement> = createRef();
-
-  contentRightRef: Ref<HTMLDivElement> = createRef();
-
-  lineNumberLeftRef: Ref<HTMLTableCellElement> = createRef();
-
-  lineNumberRightRef: Ref<HTMLTableCellElement> = createRef();
-
-  blameCellRef: Ref<HTMLTableCellElement> = createRef();
-
-  tableRowRef: Ref<HTMLTableRowElement> = createRef();
-
-  @property({type: Object})
-  left?: GrDiffLine;
-
-  @property({type: Object})
-  right?: GrDiffLine;
-
-  @property({type: Object})
-  blameInfo?: BlameInfo;
-
-  @property({type: Object})
-  responsiveMode?: DiffResponsiveMode;
-
-  @property({type: Number})
-  tabSize = 2;
-
-  @property({type: Number})
-  lineLength = 80;
-
-  @property({type: Boolean})
-  hideFileCommentButton = false;
-
-  @property({type: Object})
-  layers: DiffLayer[] = [];
-
-  /**
-   * While not visible we are trying to optimize rendering performance by
-   * rendering a simpler version of the diff. Once this has become true it
-   * cannot be set back to false.
-   */
-  @state()
-  isVisible = false;
-
-  /**
-   * Semantic DOM diff testing does not work with just table fragments, so when
-   * running such tests the render() method has to wrap the DOM in a proper
-   * <table> element.
-   */
-  @state()
-  addTableWrapperForTesting = false;
-
-  /**
-   * The browser API for handling selection does not (yet) work for selection
-   * across multiple shadow DOM elements. So we are rendering gr-diff components
-   * into the light DOM instead of the shadow DOM by overriding this method,
-   * which was the recommended workaround by the lit team.
-   * See also https://github.com/WICG/webcomponents/issues/79.
-   */
-  override createRenderRoot() {
-    return this;
-  }
-
-  override updated() {
-    this.updateLayers(Side.LEFT);
-    this.updateLayers(Side.RIGHT);
-  }
-
-  /**
-   * TODO: This needs some refinement, because layers do not detect whether they
-   * have already applied their information, so at the moment all layers would
-   * constantly re-apply their information to the diff in each lit rendering
-   * pass.
-   */
-  private updateLayers(side: Side) {
-    if (!this.isVisible) return;
-    const line = this.line(side);
-    const contentEl = this.contentRef(side).value;
-    const lineNumberEl = this.lineNumberRef(side).value;
-    if (!line || !contentEl || !lineNumberEl) return;
-    for (const layer of this.layers) {
-      if (typeof layer.annotate === 'function') {
-        layer.annotate(contentEl, lineNumberEl, line, side);
-      }
-    }
-  }
-
-  private renderInvisible() {
-    return html`
-      <tr>
-        <td class="style-scope gr-diff blame"></td>
-        <td class="style-scope gr-diff left"></td>
-        <td class="style-scope gr-diff left content">
-          <div>${this.left?.text ?? ''}</div>
-        </td>
-        <td class="style-scope gr-diff right"></td>
-        <td class="style-scope gr-diff right content">
-          <div>${this.right?.text ?? ''}</div>
-        </td>
-      </tr>
-    `;
-  }
-
-  override render() {
-    if (!this.left || !this.right) return;
-    if (!this.isVisible) return this.renderInvisible();
-    const row = html`
-      <tr
-        ${ref(this.tableRowRef)}
-        class=${diffClasses('diff-row', 'side-by-side')}
-        left-type=${this.left.type}
-        right-type=${this.right.type}
-        tabindex="-1"
-      >
-        ${this.renderBlameCell()} ${this.renderLineNumberCell(Side.LEFT)}
-        ${this.renderContentCell(Side.LEFT)}
-        ${this.renderLineNumberCell(Side.RIGHT)}
-        ${this.renderContentCell(Side.RIGHT)}
-      </tr>
-    `;
-    if (this.addTableWrapperForTesting) {
-      return html`<table>
-        ${row}
-      </table>`;
-    }
-    return row;
-  }
-
-  getTableRow(): HTMLTableRowElement | undefined {
-    return this.tableRowRef.value;
-  }
-
-  getLineNumberCell(side: Side): HTMLTableCellElement | undefined {
-    return this.lineNumberRef(side).value;
-  }
-
-  getContentCell(side: Side) {
-    const div = this.contentRef(side)?.value;
-    if (!div) return undefined;
-    return div.parentElement as HTMLTableCellElement;
-  }
-
-  getBlameCell() {
-    return this.blameCellRef.value;
-  }
-
-  private renderBlameCell() {
-    // td.blame has `white-space: pre`, so prettier must not add spaces.
-    // prettier-ignore
-    return html`
-      <td
-        ${ref(this.blameCellRef)}
-        class=${diffClasses('blame')}
-        data-line-number=${this.left?.beforeNumber ?? 0}
-      >${this.renderBlameElement()}</td>
-    `;
-  }
-
-  private renderBlameElement() {
-    const lineNum = this.left?.beforeNumber;
-    const commit = this.blameInfo;
-    if (!lineNum || !commit) return;
-
-    const isStartOfRange = commit.ranges.some(r => r.start === lineNum);
-    const extras: string[] = [];
-    if (isStartOfRange) extras.push('startOfRange');
-    const date = new Date(commit.time * 1000).toLocaleDateString();
-    const shortName = commit.author.split(' ')[0];
-    const url = `${getBaseUrl()}/q/${commit.id}`;
-
-    // td.blame has `white-space: pre`, so prettier must not add spaces.
-    // prettier-ignore
-    return html`<span class=${diffClasses(...extras)}
-        ><a href=${url} class=${diffClasses('blameDate')}>${date}</a
-        ><span class=${diffClasses('blameAuthor')}> ${shortName}</span
-        ><gr-hovercard class=${diffClasses()}>
-          <span class=${diffClasses('blameHoverCard')}>
-            Commit ${commit.id}<br />
-            Author: ${commit.author}<br />
-            Date: ${date}<br />
-            <br />
-            ${commit.commit_msg}
-          </span>
-        </gr-hovercard
-      ></span>`;
-  }
-
-  private renderLineNumberCell(side: Side): TemplateResult {
-    const line = this.line(side);
-    const lineNumber = this.lineNumber(side);
-    if (!line || !lineNumber || line.type === GrDiffLineType.BLANK) {
-      return html`<td
-        ${ref(this.lineNumberRef(side))}
-        class=${diffClasses(side)}
-      ></td>`;
-    }
-
-    return html`<td
-      ${ref(this.lineNumberRef(side))}
-      class=${diffClasses(side, 'lineNum')}
-      data-value=${lineNumber}
-    >
-      ${this.renderLineNumberButton(line, lineNumber, side)}
-    </td>`;
-  }
-
-  private renderLineNumberButton(
-    line: GrDiffLine,
-    lineNumber: LineNumber,
-    side: Side
-  ) {
-    if (this.hideFileCommentButton && lineNumber === 'FILE') return;
-    if (lineNumber === 'LOST') return;
-    // .lineNumButton has `white-space: pre`, so prettier must not add spaces.
-    // prettier-ignore
-    return html`
-      <button
-        class=${diffClasses('lineNumButton', side)}
-        tabindex="-1"
-        data-value=${lineNumber}
-        aria-label=${ifDefined(
-          this.computeLineNumberAriaLabel(line, lineNumber)
-        )}
-        @mouseenter=${() =>
-          fire(this, 'line-mouse-enter', {lineNum: lineNumber, side})}
-        @mouseleave=${() =>
-          fire(this, 'line-mouse-leave', {lineNum: lineNumber, side})}
-      >${lineNumber === 'FILE' ? 'File' : lineNumber.toString()}</button>
-    `;
-  }
-
-  private computeLineNumberAriaLabel(line: GrDiffLine, lineNumber: LineNumber) {
-    if (lineNumber === 'FILE') return 'Add file comment';
-
-    // Add aria-labels for valid line numbers.
-    // For unified diff, this method will be called with number set to 0 for
-    // the empty line number column for added/removed lines. This should not
-    // be announced to the screenreader.
-    if (lineNumber <= 0) return undefined;
-
-    switch (line.type) {
-      case GrDiffLineType.REMOVE:
-        return `${lineNumber} removed`;
-      case GrDiffLineType.ADD:
-        return `${lineNumber} added`;
-      case GrDiffLineType.BOTH:
-      case GrDiffLineType.BLANK:
-        return undefined;
-    }
-  }
-
-  private renderContentCell(side: Side): TemplateResult {
-    const line = this.line(side);
-    const lineNumber = this.lineNumber(side);
-    assertIsDefined(line, 'line');
-    const extras: string[] = [line.type, side];
-    if (line.type !== GrDiffLineType.BLANK) extras.push('content');
-    if (!line.hasIntralineInfo) extras.push('no-intraline-info');
-    if (line.beforeNumber === 'FILE') extras.push('file');
-    if (line.beforeNumber === 'LOST') extras.push('lost');
-
-    // .content has `white-space: pre`, so prettier must not add spaces.
-    // prettier-ignore
-    return html`
-      <td
-        class=${diffClasses(...extras)}
-        @mouseenter=${() => {
-          if (lineNumber)
-            fire(this, 'line-mouse-enter', {lineNum: lineNumber, side});
-        }}
-        @mouseleave=${() => {
-          if (lineNumber)
-            fire(this, 'line-mouse-leave', {lineNum: lineNumber, side});
-        }}
-      >${this.renderText(side)}${this.renderThreadGroup(side, lineNumber)}</td>
-    `;
-  }
-
-  private renderThreadGroup(side: Side, lineNumber?: LineNumber) {
-    if (!lineNumber) return;
-    // TODO: For the LOST line number the convention is that a <tr> will always
-    // be rendered, but it will not be visible, because of all cells being
-    // empty. For this to work with lit-based rendering we may only render a
-    // thread-group and a <slot> when there is a thread using that slot. The
-    // cleanest solution for that is probably introducing a gr-diff-model, where
-    // each diff row can look up or observe comment threads.
-    // .content has `white-space: pre`, so prettier must not add spaces.
-    // prettier-ignore
-    return html`<div class="thread-group" data-side=${side}><slot name="${side}-${lineNumber}"></slot></div>`;
-  }
-
-  private contentRef(side: Side) {
-    return side === Side.LEFT ? this.contentLeftRef : this.contentRightRef;
-  }
-
-  private lineNumberRef(side: Side) {
-    return side === Side.LEFT
-      ? this.lineNumberLeftRef
-      : this.lineNumberRightRef;
-  }
-
-  private lineNumber(side: Side) {
-    return this.line(side)?.lineNumber(side);
-  }
-
-  private line(side: Side) {
-    return side === Side.LEFT ? this.left : this.right;
-  }
-
-  /**
-   * Returns a 'div' element containing the supplied |text| as its innerText,
-   * with '\t' characters expanded to a width determined by |tabSize|, and the
-   * text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
-   * desired.
-   */
-  private renderText(side: Side) {
-    const line = this.line(side);
-    const lineNumber = this.lineNumber(side);
-    if (lineNumber === 'FILE' || lineNumber === 'LOST') return;
-    // prettier-ignore
-    const textElement = line?.text
-      ? html`<gr-diff-text
-          ${ref(this.contentRef(side))}
-          .text=${line?.text}
-          .tabSize=${this.tabSize}
-          .lineLimit=${this.lineLength}
-          .isResponsive=${isResponsive(this.responsiveMode)}
-        ></gr-diff-text>` : '';
-    // .content has `white-space: pre`, so prettier must not add spaces.
-    // prettier-ignore
-    return html`<div
-        class=${diffClasses('contentText', side)}
-        .ariaLabel=${line?.text ?? ''}
-        data-side=${ifDefined(side)}
-      >${textElement}</div>`;
-  }
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-diff-row': GrDiffRow;
-  }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
deleted file mode 100644
index 757d906..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup-karma';
-import './gr-diff-row';
-import {GrDiffRow} from './gr-diff-row';
-import {html} from 'lit';
-import {fixture} from '@open-wc/testing-helpers';
-import {GrDiffLine} from '../gr-diff/gr-diff-line';
-import {GrDiffLineType} from '../../../api/diff';
-
-suite('gr-diff-row test', () => {
-  let element: GrDiffRow;
-
-  setup(async () => {
-    element = await fixture<GrDiffRow>(html`<gr-diff-row></gr-diff-row>`);
-    element.isVisible = true;
-    element.addTableWrapperForTesting = true;
-    await element.updateComplete;
-  });
-
-  test('both', async () => {
-    const line = new GrDiffLine(GrDiffLineType.BOTH, 1, 1);
-    line.text = 'lorem ipsum';
-    element.left = line;
-    element.right = line;
-    await element.updateComplete;
-    expect(element).lightDom.to.equal(/* HTML */ `
-      <table>
-        <tbody>
-          <tr
-            class="diff-row gr-diff side-by-side style-scope"
-            left-type="both"
-            right-type="both"
-            tabindex="-1"
-          >
-            <td class="blame gr-diff style-scope" data-line-number="1"></td>
-            <td class="gr-diff left lineNum style-scope" data-value="1">
-              <button
-                class="gr-diff left lineNumButton style-scope"
-                data-value="1"
-                tabindex="-1"
-              >
-                1
-              </button>
-            </td>
-            <td class="both content gr-diff left no-intraline-info style-scope">
-              <div
-                aria-label="lorem ipsum"
-                class="contentText gr-diff left style-scope"
-                data-side="left"
-              >
-                <gr-diff-text>lorem ipsum</gr-diff-text>
-              </div>
-              <div class="thread-group" data-side="left">
-                <slot name="left-1"> </slot>
-              </div>
-            </td>
-            <td class="gr-diff lineNum right style-scope" data-value="1">
-              <button
-                class="gr-diff lineNumButton right style-scope"
-                data-value="1"
-                tabindex="-1"
-              >
-                1
-              </button>
-            </td>
-            <td
-              class="both content gr-diff no-intraline-info right style-scope"
-            >
-              <div
-                aria-label="lorem ipsum"
-                class="contentText gr-diff right style-scope"
-                data-side="right"
-              >
-                <gr-diff-text>lorem ipsum</gr-diff-text>
-              </div>
-              <div class="thread-group" data-side="right">
-                <slot name="right-1"> </slot>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    `);
-  });
-
-  test('add', async () => {
-    const line = new GrDiffLine(GrDiffLineType.ADD, 0, 1);
-    line.text = 'lorem ipsum';
-    element.left = new GrDiffLine(GrDiffLineType.BLANK);
-    element.right = line;
-    await element.updateComplete;
-    expect(element).lightDom.to.equal(/* HTML */ `
-      <table>
-        <tbody>
-          <tr
-            class="diff-row gr-diff side-by-side style-scope"
-            left-type="blank"
-            right-type="add"
-            tabindex="-1"
-          >
-            <td class="blame gr-diff style-scope" data-line-number="0"></td>
-            <td class="gr-diff left style-scope"></td>
-            <td class="blank gr-diff left no-intraline-info style-scope">
-              <div
-                aria-label=""
-                class="contentText gr-diff left style-scope"
-                data-side="left"
-              ></div>
-            </td>
-            <td class="gr-diff lineNum right style-scope" data-value="1">
-              <button
-                aria-label="1 added"
-                class="gr-diff lineNumButton right style-scope"
-                data-value="1"
-                tabindex="-1"
-              >
-                1
-              </button>
-            </td>
-            <td class="add content gr-diff no-intraline-info right style-scope">
-              <div
-                aria-label="lorem ipsum"
-                class="contentText gr-diff right style-scope"
-                data-side="right"
-              >
-                <gr-diff-text>lorem ipsum</gr-diff-text>
-              </div>
-              <div class="thread-group" data-side="right">
-                <slot name="right-1"> </slot>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    `);
-  });
-
-  test('remove', async () => {
-    const line = new GrDiffLine(GrDiffLineType.REMOVE, 1, 0);
-    line.text = 'lorem ipsum';
-    element.left = line;
-    element.right = new GrDiffLine(GrDiffLineType.BLANK);
-    await element.updateComplete;
-    expect(element).lightDom.to.equal(/* HTML */ `
-      <table>
-        <tbody>
-          <tr
-            class="diff-row gr-diff side-by-side style-scope"
-            left-type="remove"
-            right-type="blank"
-            tabindex="-1"
-          >
-            <td class="blame gr-diff style-scope" data-line-number="1"></td>
-            <td class="gr-diff left lineNum style-scope" data-value="1">
-              <button
-                aria-label="1 removed"
-                class="gr-diff left lineNumButton style-scope"
-                data-value="1"
-                tabindex="-1"
-              >
-                1
-              </button>
-            </td>
-            <td
-              class="content gr-diff left no-intraline-info remove style-scope"
-            >
-              <div
-                aria-label="lorem ipsum"
-                class="contentText gr-diff left style-scope"
-                data-side="left"
-              >
-                <gr-diff-text>lorem ipsum</gr-diff-text>
-              </div>
-              <div class="thread-group" data-side="left">
-                <slot name="left-1"> </slot>
-              </div>
-            </td>
-            <td class="gr-diff right style-scope"></td>
-            <td class="blank gr-diff no-intraline-info right style-scope">
-              <div
-                aria-label=""
-                class="contentText gr-diff right style-scope"
-                data-side="right"
-              ></div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    `);
-  });
-});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
deleted file mode 100644
index b11d767..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
+++ /dev/null
@@ -1,240 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {html, LitElement} from 'lit';
-import {customElement, property, state} from 'lit/decorators';
-import {
-  DiffInfo,
-  DiffLayer,
-  DiffViewMode,
-  MovedLinkClickedEventDetail,
-  RenderPreferences,
-  Side,
-  LineNumber,
-  DiffPreferencesInfo,
-} from '../../../api/diff';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {countLines, diffClasses} from '../gr-diff/gr-diff-utils';
-import {GrDiffRow} from './gr-diff-row';
-import '../gr-context-controls/gr-context-controls-section';
-import '../gr-context-controls/gr-context-controls';
-import '../gr-range-header/gr-range-header';
-import './gr-diff-row';
-import {whenVisible} from '../../../utils/dom-util';
-
-@customElement('gr-diff-section')
-export class GrDiffSection extends LitElement {
-  @property({type: Object})
-  group?: GrDiffGroup;
-
-  @property({type: Object})
-  diff?: DiffInfo;
-
-  @property({type: Object})
-  renderPrefs?: RenderPreferences;
-
-  @property({type: Object})
-  diffPrefs?: DiffPreferencesInfo;
-
-  @property({type: Object})
-  layers: DiffLayer[] = [];
-
-  /**
-   * While not visible we are trying to optimize rendering performance by
-   * rendering a simpler version of the diff.
-   */
-  @state()
-  isVisible = false;
-
-  /**
-   * Semantic DOM diff testing does not work with just table fragments, so when
-   * running such tests the render() method has to wrap the DOM in a proper
-   * <table> element.
-   */
-  @state()
-  addTableWrapperForTesting = false;
-
-  /**
-   * The browser API for handling selection does not (yet) work for selection
-   * across multiple shadow DOM elements. So we are rendering gr-diff components
-   * into the light DOM instead of the shadow DOM by overriding this method,
-   * which was the recommended workaround by the lit team.
-   * See also https://github.com/WICG/webcomponents/issues/79.
-   */
-  override createRenderRoot() {
-    return this;
-  }
-
-  override connectedCallback() {
-    super.connectedCallback();
-    // TODO: Refine this obviously simplistic approach to optimized rendering.
-    whenVisible(this.parentElement!, () => (this.isVisible = true), 1000);
-  }
-
-  override render() {
-    if (!this.group) return;
-    const extras: string[] = [];
-    extras.push('section');
-    extras.push(this.group.type);
-    if (this.group.isTotal()) extras.push('total');
-    if (this.group.dueToRebase) extras.push('dueToRebase');
-    if (this.group.moveDetails) extras.push('dueToMove');
-    if (this.group.ignoredWhitespaceOnly) extras.push('ignoredWhitespaceOnly');
-
-    const isControl = this.group.type === GrDiffGroupType.CONTEXT_CONTROL;
-    const pairs = isControl ? [] : this.group.getSideBySidePairs();
-    const body = html`
-      <tbody class=${diffClasses(...extras)}>
-        ${this.renderContextControls()} ${this.renderMoveControls()}
-        ${pairs.map(pair => {
-          const leftCl = `left-${pair.left.lineNumber(Side.LEFT)}`;
-          const rightCl = `right-${pair.right.lineNumber(Side.RIGHT)}`;
-          return html`
-            <gr-diff-row
-              class="${leftCl} ${rightCl}"
-              .left=${pair.left}
-              .right=${pair.right}
-              .layers=${this.layers}
-              .lineLength=${this.diffPrefs?.line_length ?? 80}
-              .tabSize=${this.diffPrefs?.tab_size ?? 2}
-              .isVisible=${this.isVisible}
-            >
-            </gr-diff-row>
-          `;
-        })}
-      </tbody>
-    `;
-    if (this.addTableWrapperForTesting) {
-      return html`<table>
-        ${body}
-      </table>`;
-    }
-    return body;
-  }
-
-  getDiffRows(): GrDiffRow[] {
-    return [...this.querySelectorAll<GrDiffRow>('gr-diff-row')];
-  }
-
-  private renderContextControls() {
-    if (this.group?.type !== GrDiffGroupType.CONTEXT_CONTROL) return;
-
-    const leftStart = this.group.lineRange.left.start_line;
-    const leftEnd = this.group.lineRange.left.end_line;
-    const firstGroupIsSkipped = !!this.group.contextGroups[0].skip;
-    const lastGroupIsSkipped =
-      !!this.group.contextGroups[this.group.contextGroups.length - 1].skip;
-    const lineCountLeft = countLines(this.diff, Side.LEFT);
-    const containsWholeFile = lineCountLeft === leftEnd - leftStart + 1;
-    const showAbove =
-      (leftStart > 1 && !firstGroupIsSkipped) || containsWholeFile;
-    const showBelow = leftEnd < lineCountLeft && !lastGroupIsSkipped;
-
-    return html`
-      <gr-context-controls-section
-        .showAbove=${showAbove}
-        .showBelow=${showBelow}
-        .group=${this.group}
-        .diff=${this.diff}
-        .renderPrefs=${this.renderPrefs}
-        .viewMode=${DiffViewMode.SIDE_BY_SIDE}
-      >
-      </gr-context-controls-section>
-    `;
-  }
-
-  findRow(side: Side, lineNumber: LineNumber): GrDiffRow | undefined {
-    return (
-      this.querySelector<GrDiffRow>(`gr-diff-row.${side}-${lineNumber}`) ??
-      undefined
-    );
-  }
-
-  private renderMoveControls() {
-    if (!this.group?.moveDetails) return;
-    const movedIn = this.group.adds.length > 0;
-    const plainCell = html`<td class=${diffClasses()}></td>`;
-    const lineNumberCell = html`
-      <td class=${diffClasses('moveControlsLineNumCol')}></td>
-    `;
-    const moveCell = html`
-      <td class=${diffClasses('moveHeader')}>
-        <gr-range-header class=${diffClasses()} icon="gr-icons:move-item">
-          ${this.renderMoveDescription(movedIn)}
-        </gr-range-header>
-      </td>
-    `;
-    return html`
-      <tr
-        class=${diffClasses('moveControls', movedIn ? 'movedIn' : 'movedOut')}
-      >
-        ${lineNumberCell} ${movedIn ? plainCell : moveCell} ${lineNumberCell}
-        ${movedIn ? moveCell : plainCell}
-      </tr>
-    `;
-  }
-
-  private renderMoveDescription(movedIn: boolean) {
-    if (this.group?.moveDetails?.range) {
-      const {changed, range} = this.group.moveDetails;
-      const otherSide = movedIn ? Side.LEFT : Side.RIGHT;
-      const andChangedLabel = changed ? 'and changed ' : '';
-      const direction = movedIn ? 'from' : 'to';
-      const textLabel = `Moved ${andChangedLabel}${direction} lines `;
-      return html`
-        <div class=${diffClasses()}>
-          <span class=${diffClasses()}>${textLabel}</span>
-          ${this.renderMovedLineAnchor(range.start, otherSide)}
-          <span class=${diffClasses()}> - </span>
-          ${this.renderMovedLineAnchor(range.end, otherSide)}
-        </div>
-      `;
-    }
-
-    return html`
-      <div class=${diffClasses()}>
-        <span class=${diffClasses()}
-          >${movedIn ? 'Moved in' : 'Moved out'}</span
-        >
-      </div>
-    `;
-  }
-
-  private renderMovedLineAnchor(line: number, side: Side) {
-    const listener = (e: MouseEvent) => {
-      e.preventDefault();
-      this.handleMovedLineAnchorClick(e.target, side, line);
-    };
-    // `href` is not actually used but important for Screen Readers
-    return html`
-      <a class=${diffClasses()} href=${`#${line}`} @click=${listener}
-        >${line}</a
-      >
-    `;
-  }
-
-  private handleMovedLineAnchorClick(
-    anchor: EventTarget | null,
-    side: Side,
-    line: number
-  ) {
-    anchor?.dispatchEvent(
-      new CustomEvent<MovedLinkClickedEventDetail>('moved-link-clicked', {
-        detail: {
-          lineNum: line,
-          side,
-        },
-        composed: true,
-        bubbles: true,
-      })
-    );
-  }
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-diff-section': GrDiffSection;
-  }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
deleted file mode 100644
index 88c0e83..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup-karma';
-import './gr-diff-section';
-import {GrDiffSection} from './gr-diff-section';
-import {html} from 'lit';
-import {fixture} from '@open-wc/testing-helpers';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {GrDiffLine} from '../gr-diff/gr-diff-line';
-import {GrDiffLineType} from '../../../api/diff';
-
-suite('gr-diff-section test', () => {
-  let element: GrDiffSection;
-
-  setup(async () => {
-    element = await fixture<GrDiffSection>(
-      html`<gr-diff-section></gr-diff-section>`
-    );
-    element.addTableWrapperForTesting = true;
-    element.isVisible = true;
-    await element.updateComplete;
-  });
-
-  test('3 normal unchanged rows', async () => {
-    const lines = [
-      new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
-      new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
-      new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
-    ];
-    lines[0].text = 'asdf';
-    lines[1].text = 'qwer';
-    lines[2].text = 'zxcv';
-    const group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
-    element.group = group;
-    await element.updateComplete;
-    expect(element).dom.to.equal(/* HTML */ `
-      <gr-diff-section>
-        <gr-diff-row class="left-1 right-1"> </gr-diff-row>
-        <gr-diff-row class="left-1 right-1"> </gr-diff-row>
-        <gr-diff-row class="left-1 right-1"> </gr-diff-row>
-        <table>
-          <tbody class="both gr-diff section style-scope">
-            <tr
-              class="diff-row gr-diff side-by-side style-scope"
-              left-type="both"
-              right-type="both"
-              tabindex="-1"
-            >
-              <td class="blame gr-diff style-scope" data-line-number="1"></td>
-              <td class="gr-diff left lineNum style-scope" data-value="1">
-                <button
-                  class="gr-diff left lineNumButton style-scope"
-                  data-value="1"
-                  tabindex="-1"
-                >
-                  1
-                </button>
-              </td>
-              <td
-                class="both content gr-diff left no-intraline-info style-scope"
-              >
-                <div
-                  aria-label="asdf"
-                  class="contentText gr-diff left style-scope"
-                  data-side="left"
-                >
-                  <gr-diff-text></gr-diff-text>
-                </div>
-                <div class="thread-group" data-side="left">
-                  <slot name="left-1"> </slot>
-                </div>
-              </td>
-              <td class="gr-diff lineNum right style-scope" data-value="1">
-                <button
-                  class="gr-diff lineNumButton right style-scope"
-                  data-value="1"
-                  tabindex="-1"
-                >
-                  1
-                </button>
-              </td>
-              <td
-                class="both content gr-diff no-intraline-info right style-scope"
-              >
-                <div
-                  aria-label="asdf"
-                  class="contentText gr-diff right style-scope"
-                  data-side="right"
-                >
-                  <gr-diff-text></gr-diff-text>
-                </div>
-                <div class="thread-group" data-side="right">
-                  <slot name="right-1"> </slot>
-                </div>
-              </td>
-            </tr>
-            <tr
-              class="diff-row gr-diff side-by-side style-scope"
-              left-type="both"
-              right-type="both"
-              tabindex="-1"
-            >
-              <td class="blame gr-diff style-scope" data-line-number="1"></td>
-              <td class="gr-diff left lineNum style-scope" data-value="1">
-                <button
-                  class="gr-diff left lineNumButton style-scope"
-                  data-value="1"
-                  tabindex="-1"
-                >
-                  1
-                </button>
-              </td>
-              <td
-                class="both content gr-diff left no-intraline-info style-scope"
-              >
-                <div
-                  aria-label="qwer"
-                  class="contentText gr-diff left style-scope"
-                  data-side="left"
-                >
-                  <gr-diff-text></gr-diff-text>
-                </div>
-                <div class="thread-group" data-side="left">
-                  <slot name="left-1"> </slot>
-                </div>
-              </td>
-              <td class="gr-diff lineNum right style-scope" data-value="1">
-                <button
-                  class="gr-diff lineNumButton right style-scope"
-                  data-value="1"
-                  tabindex="-1"
-                >
-                  1
-                </button>
-              </td>
-              <td
-                class="both content gr-diff no-intraline-info right style-scope"
-              >
-                <div
-                  aria-label="qwer"
-                  class="contentText gr-diff right style-scope"
-                  data-side="right"
-                >
-                  <gr-diff-text></gr-diff-text>
-                </div>
-                <div class="thread-group" data-side="right">
-                  <slot name="right-1"> </slot>
-                </div>
-              </td>
-            </tr>
-            <tr
-              class="diff-row gr-diff side-by-side style-scope"
-              left-type="both"
-              right-type="both"
-              tabindex="-1"
-            >
-              <td class="blame gr-diff style-scope" data-line-number="1"></td>
-              <td class="gr-diff left lineNum style-scope" data-value="1">
-                <button
-                  class="gr-diff left lineNumButton style-scope"
-                  data-value="1"
-                  tabindex="-1"
-                >
-                  1
-                </button>
-              </td>
-              <td
-                class="both content gr-diff left no-intraline-info style-scope"
-              >
-                <div
-                  aria-label="zxcv"
-                  class="contentText gr-diff left style-scope"
-                  data-side="left"
-                >
-                  <gr-diff-text></gr-diff-text>
-                </div>
-                <div class="thread-group" data-side="left">
-                  <slot name="left-1"> </slot>
-                </div>
-              </td>
-              <td class="gr-diff lineNum right style-scope" data-value="1">
-                <button
-                  class="gr-diff lineNumButton right style-scope"
-                  data-value="1"
-                  tabindex="-1"
-                >
-                  1
-                </button>
-              </td>
-              <td
-                class="both content gr-diff no-intraline-info right style-scope"
-              >
-                <div
-                  aria-label="zxcv"
-                  class="contentText gr-diff right style-scope"
-                  data-side="right"
-                >
-                  <gr-diff-text></gr-diff-text>
-                </div>
-                <div class="thread-group" data-side="right">
-                  <slot name="right-1"> </slot>
-                </div>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </gr-diff-section>
-    `);
-  });
-});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
deleted file mode 100644
index bb37c43..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {LitElement, html, TemplateResult} from 'lit';
-import {customElement, property} from 'lit/decorators';
-import {diffClasses} from '../gr-diff/gr-diff-utils';
-
-const SURROGATE_PAIR = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
-
-const TAB = '\t';
-
-@customElement('gr-diff-text')
-export class GrDiffText extends LitElement {
-  /**
-   * The browser API for handling selection does not (yet) work for selection
-   * across multiple shadow DOM elements. So we are rendering gr-diff components
-   * into the light DOM instead of the shadow DOM by overriding this method,
-   * which was the recommended workaround by the lit team.
-   * See also https://github.com/WICG/webcomponents/issues/79.
-   */
-  override createRenderRoot() {
-    return this;
-  }
-
-  @property({type: String})
-  text = '';
-
-  @property({type: Boolean})
-  isResponsive = false;
-
-  @property({type: Number})
-  tabSize = 2;
-
-  @property({type: Number})
-  lineLimit = 80;
-
-  /** Temporary state while rendering. */
-  private textOffset = 0;
-
-  /** Temporary state while rendering. */
-  private columnPos = 0;
-
-  /** Temporary state while rendering. */
-  private pieces: (string | TemplateResult)[] = [];
-
-  /** Split up the string into tabs, surrogate pairs and regular segments. */
-  override render() {
-    this.textOffset = 0;
-    this.columnPos = 0;
-    this.pieces = [];
-    const splitByTab = this.text.split('\t');
-    for (let i = 0; i < splitByTab.length; i++) {
-      const splitBySurrogate = splitByTab[i].split(SURROGATE_PAIR);
-      for (let j = 0; j < splitBySurrogate.length; j++) {
-        this.renderSegment(splitBySurrogate[j]);
-        if (j < splitBySurrogate.length - 1) {
-          this.renderSurrogatePair();
-        }
-      }
-      if (i < splitByTab.length - 1) {
-        this.renderTab();
-      }
-    }
-    if (this.textOffset !== this.text.length) throw new Error('unfinished');
-    return this.pieces;
-  }
-
-  /** Render regular characters, but insert line breaks appropriately. */
-  private renderSegment(segment: string) {
-    let segmentOffset = 0;
-    while (segmentOffset < segment.length) {
-      const newOffset = Math.min(
-        segment.length,
-        segmentOffset + this.lineLimit - this.columnPos
-      );
-      this.renderString(segment.substring(segmentOffset, newOffset));
-      segmentOffset = newOffset;
-      if (segmentOffset < segment.length && this.columnPos === this.lineLimit) {
-        this.renderLineBreak();
-      }
-    }
-  }
-
-  /** Render regular characters. */
-  private renderString(s: string) {
-    if (s.length === 0) return;
-    this.pieces.push(s);
-    this.textOffset += s.length;
-    this.columnPos += s.length;
-    if (this.columnPos > this.lineLimit) throw new Error('over line limit');
-  }
-
-  /** Render a tab character. */
-  private renderTab() {
-    let tabSize = this.tabSize - (this.columnPos % this.tabSize);
-    if (this.columnPos + tabSize > this.lineLimit) {
-      this.renderLineBreak();
-      tabSize = this.tabSize;
-    }
-    const piece = html`<span
-      class=${diffClasses('tab')}
-      style="tab-size: ${tabSize}; -moz-tab-size: ${tabSize};"
-      >${TAB}</span
-    >`;
-    this.pieces.push(piece);
-    this.textOffset += 1;
-    this.columnPos += tabSize;
-  }
-
-  /** Render a surrogate pair: string length is 2, but is just 1 char. */
-  private renderSurrogatePair() {
-    if (this.columnPos === this.lineLimit) {
-      this.renderLineBreak();
-    }
-    this.pieces.push(this.text.substring(this.textOffset, this.textOffset + 2));
-    this.textOffset += 2;
-    this.columnPos += 1;
-  }
-
-  /** Render a line break, don't advance text offset, reset col position. */
-  private renderLineBreak() {
-    if (this.isResponsive) {
-      this.pieces.push(html`<wbr class=${diffClasses()}></wbr>`);
-    } else {
-      this.pieces.push(html`<span class=${diffClasses('br')}></span>`);
-    }
-    // this.textOffset += 0;
-    this.columnPos = 0;
-  }
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-diff-text': GrDiffText;
-  }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
deleted file mode 100644
index 21c0936..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup-karma';
-import './gr-diff-text';
-import {GrDiffText} from './gr-diff-text';
-import {html} from 'lit';
-import {fixture} from '@open-wc/testing-helpers';
-
-const LINE_BREAK = '<span class="style-scope gr-diff br"></span>';
-
-const TAB = '<span class="" style=""></span>';
-
-const TAB_IGNORE = ['class', 'style'];
-
-suite('gr-diff-text test', () => {
-  let element: GrDiffText;
-
-  setup(async () => {
-    element = await fixture<GrDiffText>(html`<gr-diff-text></gr-diff-text>`);
-    await element.updateComplete;
-  });
-
-  const check = async (
-    text: string,
-    html: string,
-    ignoreAttributes: string[] = []
-  ) => {
-    element.text = text;
-    element.tabSize = 4;
-    element.lineLimit = 10;
-    await element.updateComplete;
-    expect(element).lightDom.to.equal(html, {ignoreAttributes});
-  };
-
-  suite('lit rendering', () => {
-    test('renderText newlines 1', () => {
-      check('abcdef', 'abcdef');
-      check('a'.repeat(20), `aaaaaaaaaa${LINE_BREAK}aaaaaaaaaa`);
-    });
-
-    test('renderText newlines 2', () => {
-      check(
-        '<span class="thumbsup">👍</span>',
-        '&lt;span clas' +
-          LINE_BREAK +
-          's="thumbsu' +
-          LINE_BREAK +
-          'p"&gt;👍&lt;/span' +
-          LINE_BREAK +
-          '&gt;'
-      );
-    });
-
-    test('renderText newlines 3', () => {
-      check(
-        '01234\t56789',
-        '01234' + TAB + '56' + LINE_BREAK + '789',
-        TAB_IGNORE
-      );
-    });
-
-    test('renderText newlines 4', async () => {
-      element.lineLimit = 20;
-      await element.updateComplete;
-      check(
-        '👍'.repeat(58),
-        '👍'.repeat(20) +
-          LINE_BREAK +
-          '👍'.repeat(20) +
-          LINE_BREAK +
-          '👍'.repeat(18)
-      );
-    });
-
-    test('tab wrapper style', async () => {
-      for (const size of [1, 3, 8, 55]) {
-        element.tabSize = size;
-        await element.updateComplete;
-        check(
-          '\t',
-          /* HTML */ `
-            <span
-              class="style-scope gr-diff tab"
-              style="tab-size: ${size}; -moz-tab-size: ${size};"
-            >
-            </span>
-          `
-        );
-      }
-    });
-
-    test('tab wrapper insertion', () => {
-      check('abc\tdef', 'abc' + TAB + 'def', TAB_IGNORE);
-    });
-
-    test('escaping HTML', async () => {
-      element.lineLimit = 100;
-      await element.updateComplete;
-      check(
-        '<script>alert("XSS");<' + '/script>',
-        '&lt;script&gt;alert("XSS");&lt;/script&gt;'
-      );
-      check('& < > " \' / `', '&amp; &lt; &gt; " \' / `');
-    });
-
-    test('text length with tabs and unicode', async () => {
-      async function expectTextLength(
-        text: string,
-        tabSize: number,
-        expected: number
-      ) {
-        element.text = text;
-        element.tabSize = tabSize;
-        element.lineLimit = expected;
-        await element.updateComplete;
-        const result = element.innerHTML;
-
-        // Must not contain a line break.
-        assert.isNotOk(element.querySelector('span.br'));
-
-        // Increasing the line limit by 1 should not change anything.
-        element.lineLimit = expected + 1;
-        await element.updateComplete;
-        const resultPlusOne = element.innerHTML;
-        assert.equal(resultPlusOne, result);
-
-        // Increasing the line limit to infinity should not change anything.
-        element.lineLimit = Infinity;
-        await element.updateComplete;
-        const resultInf = element.innerHTML;
-        assert.equal(resultInf, result);
-
-        // Decreasing the line limit by 1 should introduce a line break.
-        element.lineLimit = expected + 1;
-        await element.updateComplete;
-        assert.isNotOk(element.querySelector('span.br'));
-      }
-      expectTextLength('12345', 4, 5);
-      expectTextLength('\t\t12', 4, 10);
-      expectTextLength('abc💢123', 4, 7);
-      expectTextLength('abc\t', 8, 8);
-      expectTextLength('abc\t\t', 10, 20);
-      expectTextLength('', 10, 0);
-      // 17 Thai combining chars.
-      expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
-      expectTextLength('abc\tde', 10, 12);
-      expectTextLength('abc\tde\t', 10, 20);
-      expectTextLength('\t\t\t\t\t', 20, 100);
-    });
-  });
-});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
index 4ecdcb0..dfe8a15 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -228,7 +228,6 @@
     path?: string,
     intentionalMove?: boolean
   ) {
-    this._updateStops();
     const row = this._findRowByNumberAndFile(number, side, path);
     if (row) {
       this.side = side;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
index 22af7e3..2665ef0 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
@@ -23,11 +23,7 @@
   normalize,
   NormalizedRange,
 } from '../gr-diff-highlight/gr-range-normalizer';
-import {
-  descendedFromClass,
-  isElementTarget,
-  querySelectorAll,
-} from '../../../utils/dom-util';
+import {descendedFromClass, querySelectorAll} from '../../../utils/dom-util';
 import {customElement, property, observe} from '@polymer/decorators';
 import {DiffInfo} from '../../../types/diff';
 import {Side} from '../../../constants/constants';
@@ -113,9 +109,8 @@
   }
 
   _handleDown(e: Event) {
-    const target = e.composedPath()[0];
-    if (!isElementTarget(target)) return;
-
+    const target = e.target;
+    if (!(target instanceof Element)) return;
     // Handle the down event on comment thread in Polymer 2
     const handled = this._handleDownOnRangeComment(target);
     if (handled) return;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts
index 8593e1b..2927101 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts
@@ -19,7 +19,6 @@
   GrDiffLine as GrDiffLineApi,
   GrDiffLineType,
   LineNumber,
-  Side,
 } from '../../../api/diff';
 
 export {GrDiffLineType, LineNumber};
@@ -39,10 +38,6 @@
 
   text = '';
 
-  lineNumber(side: Side) {
-    return side === Side.LEFT ? this.beforeNumber : this.afterNumber;
-  }
-
   // TODO(TS): remove this properties
   static readonly Type = GrDiffLineType;
 
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
index a12867f..182d48e 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
@@ -45,20 +45,12 @@
  *   Graphemes: http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
  *   A proposed JS API: https://github.com/tc39/proposal-intl-segmenter
  */
-export const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
 
 // If any line of the diff is more than the character limit, then disable
 // syntax highlighting for the entire file.
 export const SYNTAX_MAX_LINE_LENGTH = 500;
 
-export function countLines(diff?: DiffInfo, side?: Side) {
-  if (!diff?.content || !side) return 0;
-  return diff.content.reduce((sum, chunk) => {
-    const sideChunk = side === Side.LEFT ? chunk.a : chunk.b;
-    return sum + (sideChunk?.length ?? chunk.ab?.length ?? chunk.skip ?? 0);
-  }, 0);
-}
-
 export function getResponsiveMode(
   prefs: DiffPreferencesInfo,
   renderPrefs?: RenderPreferences
@@ -73,7 +65,7 @@
   return 'NONE';
 }
 
-export function isResponsive(responsiveMode?: DiffResponsiveMode) {
+export function isResponsive(responsiveMode: DiffResponsiveMode) {
   return (
     responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY'
   );
@@ -124,12 +116,7 @@
         return null;
       }
     }
-    node =
-      (node as Element).assignedSlot ??
-      (node as ShadowRoot).host ??
-      node.previousSibling ??
-      node.parentNode ??
-      undefined;
+    node = node.previousSibling ?? node.parentElement ?? undefined;
   }
   return null;
 }
@@ -208,19 +195,6 @@
 }
 
 /**
- * Simple helper method for creating element classes in the context of
- * gr-diff.
- *
- * We are adding 'style-scope', 'gr-diff' classes for compatibility with
- * Shady DOM. TODO: Is that still required??
- *
- * Otherwise this is just a super simple convenience function.
- */
-export function diffClasses(...additionalClasses: string[]) {
-  return ['style-scope', 'gr-diff', ...additionalClasses].join(' ');
-}
-
-/**
  * Simple helper method for creating elements in the context of gr-diff.
  *
  * We are adding 'style-scope', 'gr-diff' classes for compatibility with
@@ -281,8 +255,6 @@
 }
 
 /**
- * Deprecated: Lit based rendering uses the textToPieces() function above.
- *
  * Returns a 'div' element containing the supplied |text| as its innerText,
  * with '\t' characters expanded to a width determined by |tabSize|, and the
  * text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
index 793b0ef..600913e 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
@@ -4,171 +4,150 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup-karma';
-import {
-  createElementDiff,
-  formatText,
-  createTabWrapper,
-  diffClasses,
-} from './gr-diff-utils';
+import {createElementDiff, formatText, createTabWrapper} from './gr-diff-utils';
 
 const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
 
 suite('gr-diff-utils tests', () => {
-  suite('legacy rendering', () => {
-    test('createElementDiff classStr applies all classes', () => {
-      const node = createElementDiff('div', 'test classes');
-      assert.isTrue(node.classList.contains('gr-diff'));
-      assert.isTrue(node.classList.contains('test'));
-      assert.isTrue(node.classList.contains('classes'));
-    });
-
-    test('formatText newlines 1', () => {
-      let text = 'abcdef';
-
-      assert.equal(formatText(text, 'NONE', 4, 10).innerHTML, text);
-      text = 'a'.repeat(20);
-      assert.equal(
-        formatText(text, 'NONE', 4, 10).innerHTML,
-        'a'.repeat(10) + LINE_BREAK_HTML + 'a'.repeat(10)
-      );
-    });
-
-    test('formatText newlines 2', () => {
-      const text = '<span class="thumbsup">👍</span>';
-      assert.equal(
-        formatText(text, 'NONE', 4, 10).innerHTML,
-        '&lt;span clas' +
-          LINE_BREAK_HTML +
-          's="thumbsu' +
-          LINE_BREAK_HTML +
-          'p"&gt;👍&lt;/span' +
-          LINE_BREAK_HTML +
-          '&gt;'
-      );
-    });
-
-    test('formatText newlines 3', () => {
-      const text = '01234\t56789';
-      assert.equal(
-        formatText(text, 'NONE', 4, 10).innerHTML,
-        '01234' + createTabWrapper(3).outerHTML + '56' + LINE_BREAK_HTML + '789'
-      );
-    });
-
-    test('formatText newlines 4', () => {
-      const text = '👍'.repeat(58);
-      assert.equal(
-        formatText(text, 'NONE', 4, 20).innerHTML,
-        '👍'.repeat(20) +
-          LINE_BREAK_HTML +
-          '👍'.repeat(20) +
-          LINE_BREAK_HTML +
-          '👍'.repeat(18)
-      );
-    });
-
-    test('tab wrapper style', () => {
-      const pattern = new RegExp(
-        '^<span class="style-scope gr-diff tab" ' +
-          'style="((?:-moz-)?tab-size: (\\d+);.?)+">\\t<\\/span>$'
-      );
-
-      for (const size of [1, 3, 8, 55]) {
-        const html = createTabWrapper(size).outerHTML;
-        expect(html).to.match(pattern);
-        assert.equal(html.match(pattern)?.[2], size.toString());
-      }
-    });
-
-    test('tab wrapper insertion', () => {
-      const html = 'abc\tdef';
-      const tabSize = 8;
-      const wrapper = createTabWrapper(tabSize - 3);
-      assert.ok(wrapper);
-      assert.equal(wrapper.innerText, '\t');
-      assert.equal(
-        formatText(html, 'NONE', tabSize, Infinity).innerHTML,
-        'abc' + wrapper.outerHTML + 'def'
-      );
-    });
-
-    test('escaping HTML', () => {
-      let input = '<script>alert("XSS");<' + '/script>';
-      let expected = '&lt;script&gt;alert("XSS");&lt;/script&gt;';
-
-      let result = formatText(
-        input,
-        'NONE',
-        1,
-        Number.POSITIVE_INFINITY
-      ).innerHTML;
-      assert.equal(result, expected);
-
-      input = '& < > " \' / `';
-      expected = '&amp; &lt; &gt; " \' / `';
-      result = formatText(input, 'NONE', 1, Number.POSITIVE_INFINITY).innerHTML;
-      assert.equal(result, expected);
-    });
-
-    test('text length with tabs and unicode', () => {
-      function expectTextLength(
-        text: string,
-        tabSize: number,
-        expected: number
-      ) {
-        // Formatting to |expected| columns should not introduce line breaks.
-        const result = formatText(text, 'NONE', tabSize, expected);
-        assert.isNotOk(
-          result.querySelector('.contentText > .br'),
-          '  Expected the result of: \n' +
-            `      _formatText(${text}', 'NONE',  ${tabSize}, ${expected})\n` +
-            '  to not contain a br. But the actual result HTML was:\n' +
-            `      '${result.innerHTML}'\nwhereupon`
-        );
-
-        // Increasing the line limit should produce the same markup.
-        assert.equal(
-          formatText(text, 'NONE', tabSize, Infinity).innerHTML,
-          result.innerHTML
-        );
-        assert.equal(
-          formatText(text, 'NONE', tabSize, expected + 1).innerHTML,
-          result.innerHTML
-        );
-
-        // Decreasing the line limit should introduce line breaks.
-        if (expected > 0) {
-          const tooSmall = formatText(text, 'NONE', tabSize, expected - 1);
-          assert.isOk(
-            tooSmall.querySelector('.contentText > .br'),
-            '  Expected the result of: \n' +
-              `      _formatText(${text}', ${tabSize}, ${expected - 1})\n` +
-              '  to contain a br. But the actual result HTML was:\n' +
-              `      '${tooSmall.innerHTML}'\nwhereupon`
-          );
-        }
-      }
-      expectTextLength('12345', 4, 5);
-      expectTextLength('\t\t12', 4, 10);
-      expectTextLength('abc💢123', 4, 7);
-      expectTextLength('abc\t', 8, 8);
-      expectTextLength('abc\t\t', 10, 20);
-      expectTextLength('', 10, 0);
-      // 17 Thai combining chars.
-      expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
-      expectTextLength('abc\tde', 10, 12);
-      expectTextLength('abc\tde\t', 10, 20);
-      expectTextLength('\t\t\t\t\t', 20, 100);
-    });
+  test('createElementDiff classStr applies all classes', () => {
+    const node = createElementDiff('div', 'test classes');
+    assert.isTrue(node.classList.contains('gr-diff'));
+    assert.isTrue(node.classList.contains('test'));
+    assert.isTrue(node.classList.contains('classes'));
   });
 
-  suite('lit rendering', () => {
-    test('diffClasses', () => {
-      const c = diffClasses('div', 'test classes').split(' ');
-      assert.include(c, 'gr-diff');
-      assert.include(c, 'style-scope');
-      assert.include(c, 'test');
-      assert.include(c, 'classes');
-    });
+  test('formatText newlines 1', () => {
+    let text = 'abcdef';
+
+    assert.equal(formatText(text, 'NONE', 4, 10).innerHTML, text);
+    text = 'a'.repeat(20);
+    assert.equal(
+      formatText(text, 'NONE', 4, 10).innerHTML,
+      'a'.repeat(10) + LINE_BREAK_HTML + 'a'.repeat(10)
+    );
+  });
+
+  test('formatText newlines 2', () => {
+    const text = '<span class="thumbsup">👍</span>';
+    assert.equal(
+      formatText(text, 'NONE', 4, 10).innerHTML,
+      '&lt;span clas' +
+        LINE_BREAK_HTML +
+        's="thumbsu' +
+        LINE_BREAK_HTML +
+        'p"&gt;👍&lt;/span' +
+        LINE_BREAK_HTML +
+        '&gt;'
+    );
+  });
+
+  test('formatText newlines 3', () => {
+    const text = '01234\t56789';
+    assert.equal(
+      formatText(text, 'NONE', 4, 10).innerHTML,
+      '01234' + createTabWrapper(3).outerHTML + '56' + LINE_BREAK_HTML + '789'
+    );
+  });
+
+  test('formatText newlines 4', () => {
+    const text = '👍'.repeat(58);
+    assert.equal(
+      formatText(text, 'NONE', 4, 20).innerHTML,
+      '👍'.repeat(20) +
+        LINE_BREAK_HTML +
+        '👍'.repeat(20) +
+        LINE_BREAK_HTML +
+        '👍'.repeat(18)
+    );
+  });
+
+  test('tab wrapper style', () => {
+    const pattern = new RegExp(
+      '^<span class="style-scope gr-diff tab" ' +
+        'style="((?:-moz-)?tab-size: (\\d+);.?)+">\\t<\\/span>$'
+    );
+
+    for (const size of [1, 3, 8, 55]) {
+      const html = createTabWrapper(size).outerHTML;
+      expect(html).to.match(pattern);
+      assert.equal(html.match(pattern)?.[2], size.toString());
+    }
+  });
+
+  test('tab wrapper insertion', () => {
+    const html = 'abc\tdef';
+    const tabSize = 8;
+    const wrapper = createTabWrapper(tabSize - 3);
+    assert.ok(wrapper);
+    assert.equal(wrapper.innerText, '\t');
+    assert.equal(
+      formatText(html, 'NONE', tabSize, Infinity).innerHTML,
+      'abc' + wrapper.outerHTML + 'def'
+    );
+  });
+
+  test('escaping HTML', () => {
+    let input = '<script>alert("XSS");<' + '/script>';
+    let expected = '&lt;script&gt;alert("XSS");&lt;/script&gt;';
+
+    let result = formatText(
+      input,
+      'NONE',
+      1,
+      Number.POSITIVE_INFINITY
+    ).innerHTML;
+    assert.equal(result, expected);
+
+    input = '& < > " \' / `';
+    expected = '&amp; &lt; &gt; " \' / `';
+    result = formatText(input, 'NONE', 1, Number.POSITIVE_INFINITY).innerHTML;
+    assert.equal(result, expected);
+  });
+
+  test('text length with tabs and unicode', () => {
+    function expectTextLength(text: string, tabSize: number, expected: number) {
+      // Formatting to |expected| columns should not introduce line breaks.
+      const result = formatText(text, 'NONE', tabSize, expected);
+      assert.isNotOk(
+        result.querySelector('.contentText > .br'),
+        '  Expected the result of: \n' +
+          `      _formatText(${text}', 'NONE',  ${tabSize}, ${expected})\n` +
+          '  to not contain a br. But the actual result HTML was:\n' +
+          `      '${result.innerHTML}'\nwhereupon`
+      );
+
+      // Increasing the line limit should produce the same markup.
+      assert.equal(
+        formatText(text, 'NONE', tabSize, Infinity).innerHTML,
+        result.innerHTML
+      );
+      assert.equal(
+        formatText(text, 'NONE', tabSize, expected + 1).innerHTML,
+        result.innerHTML
+      );
+
+      // Decreasing the line limit should introduce line breaks.
+      if (expected > 0) {
+        const tooSmall = formatText(text, 'NONE', tabSize, expected - 1);
+        assert.isOk(
+          tooSmall.querySelector('.contentText > .br'),
+          '  Expected the result of: \n' +
+            `      _formatText(${text}', ${tabSize}, ${expected - 1})\n` +
+            '  to contain a br. But the actual result HTML was:\n' +
+            `      '${tooSmall.innerHTML}'\nwhereupon`
+        );
+      }
+    }
+    expectTextLength('12345', 4, 5);
+    expectTextLength('\t\t12', 4, 10);
+    expectTextLength('abc💢123', 4, 7);
+    expectTextLength('abc\t', 8, 8);
+    expectTextLength('abc\t\t', 10, 20);
+    expectTextLength('', 10, 0);
+    // 17 Thai combining chars.
+    expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
+    expectTextLength('abc\tde', 10, 12);
+    expectTextLength('abc\tde\t', 10, 20);
+    expectTextLength('\t\t\t\t\t', 20, 100);
   });
 });
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index f7e40f9..a38ec91 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -24,7 +24,7 @@
 import '../gr-ranged-comment-themes/gr-ranged-comment-theme';
 import '../gr-ranged-comment-hint/gr-ranged-comment-hint';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {htmlTemplate} from './gr-diff_html';
 import {LineNumber} from './gr-diff-line';
 import {
@@ -77,7 +77,7 @@
   GrDiff as GrDiffApi,
   DisplayLine,
 } from '../../../api/diff';
-import {isElementTarget, isSafari, toggleClass} from '../../../utils/dom-util';
+import {isSafari, toggleClass} from '../../../utils/dom-util';
 import {assertIsDefined} from '../../../utils/common-util';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 
@@ -530,8 +530,7 @@
   }
 
   _handleTap(e: CustomEvent) {
-    const el = e.composedPath()[0];
-    if (!isElementTarget(el)) return;
+    const el = (dom(e) as EventApi).localTarget as Element;
 
     if (
       el.getAttribute('data-value') !== 'LOST' &&
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
index c2e5550..e05e85a 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
@@ -670,12 +670,6 @@
     .token-highlight {
       background-color: var(--token-highlighting-color, #fffd54);
     }
-
-    gr-diff-section,
-    gr-context-controls-section,
-    gr-diff-row {
-      display: contents;
-    }
   </style>
   <style include="gr-syntax-theme">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
index a74adf6..78cff25 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
@@ -25,10 +25,10 @@
   isReviewerGroupSuggestion,
   NumericChangeId,
   ServerInfo,
-  SuggestedReviewerInfo,
   Suggestion,
 } from '../../types/common';
 import {assertNever} from '../../utils/common-util';
+import {AutocompleteSuggestion} from '../../elements/shared/gr-autocomplete/gr-autocomplete';
 
 // TODO(TS): enum name doesn't follow typescript style guid rules
 // Rename it
@@ -44,15 +44,10 @@
 
 type ApiCallCallback = (input: string) => Promise<Suggestion[] | void>;
 
-export interface SuggestionItem {
-  name: string;
-  value: SuggestedReviewerInfo;
-}
-
 export interface ReviewerSuggestionsProvider {
   init(): void;
   getSuggestions(input: string): Promise<Suggestion[]>;
-  makeSuggestionItem(suggestion: Suggestion): SuggestionItem;
+  makeSuggestionItem(suggestion: Suggestion): AutocompleteSuggestion;
 }
 
 export class GrReviewerSuggestionsProvider
@@ -120,12 +115,15 @@
     return this._apiCall(input).then(reviewers => reviewers || []);
   }
 
-  makeSuggestionItem(suggestion: Suggestion): SuggestionItem {
+  // this can be retyped to AutocompleteSuggestion<SuggestedReviewerInfo> but
+  // this would need to change generics of gr-autocomplete.
+  makeSuggestionItem(suggestion: Suggestion): AutocompleteSuggestion {
     if (isReviewerAccountSuggestion(suggestion)) {
       // Reviewer is an account suggestion from getChangeSuggestedReviewers.
       return {
         name: getAccountDisplayName(this.config, suggestion.account),
-        value: suggestion,
+        // TODO(TS) this is temporary hack to avoid cascade of ts issues
+        value: suggestion as unknown as string,
       };
     }
 
@@ -133,7 +131,8 @@
       // Reviewer is a group suggestion from getChangeSuggestedReviewers.
       return {
         name: getGroupDisplayName(suggestion.group),
-        value: suggestion,
+        // TODO(TS) this is temporary hack to avoid cascade of ts issues
+        value: suggestion as unknown as string,
       };
     }
 
@@ -141,7 +140,8 @@
       // Reviewer is an account suggestion from getSuggestedAccounts.
       return {
         name: getAccountDisplayName(this.config, suggestion),
-        value: {account: suggestion, count: 1},
+        // TODO(TS) this is temporary hack to avoid cascade of ts issues
+        value: {account: suggestion, count: 1} as unknown as string,
       };
     }
     assertNever(suggestion, 'Received an incorrect suggestion');