Merge "Move a11y styles into its own file"
diff --git a/package.json b/package.json
index e1bbf7c..a47ba9f 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
     "eslint-plugin-html": "^6.1.2",
     "eslint-plugin-import": "^2.22.1",
     "eslint-plugin-jsdoc": "^32.3.0",
+    "eslint-plugin-lit": "^1.5.1",
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-prettier": "^3.4.0",
     "eslint-plugin-regex": "^1.8.0",
diff --git a/plugins/tsconfig-plugins-base.json b/plugins/tsconfig-plugins-base.json
index 97eae67..b7e9d52 100644
--- a/plugins/tsconfig-plugins-base.json
+++ b/plugins/tsconfig-plugins-base.json
@@ -20,6 +20,7 @@
     "noUnusedLocals": true, /* Report errors on unused locals. */
     "noUnusedParameters": true, /* Report errors on unused parameters. */
     "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+    "noImplicitOverride": true,
     "noFallthroughCasesInSwitch": true,/* Report errors for fallthrough cases in switch statement. */
 
     "skipLibCheck": true, /* Do not check node_modules */
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index 247227b..14f9e8c 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -412,11 +412,30 @@
         }],
       },
     },
+    {
+      files: ['*.ts'],
+      excludedFiles: '*_html.ts',
+      rules: {
+        'lit/attribute-value-entities': 'error',
+        'lit/binding-positions': 'error',
+        'lit/no-duplicate-template-bindings': 'error',
+        'lit/no-invalid-html': 'error',
+        'lit/no-legacy-template-syntax': 'error',
+        'lit/no-property-change-update': 'error',
+        'lit/no-invalid-escape-sequences': 'error',
+        'lit/no-legacy-imports': 'error',
+        'lit/no-private-properties': 'error',
+        'lit/no-useless-template-literals': 'error',
+        'lit/no-value-attribute': 'error',
+        'lit/prefer-static-styles': 'error',
+      },
+    },
   ],
   plugins: [
     'html',
     'jsdoc',
     'import',
+    'lit',
     'prettier',
     'regex',
   ],
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index cc62f05..00c8e65 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -106,7 +106,6 @@
     "elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.ts",
     "elements/admin/gr-repo/gr-repo_html.ts",
     "elements/admin/gr-rule-editor/gr-rule-editor_html.ts",
-    "elements/change-list/gr-change-list-item/gr-change-list-item_html.ts",
     "elements/change-list/gr-change-list-view/gr-change-list-view_html.ts",
     "elements/change-list/gr-change-list/gr-change-list_html.ts",
     "elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts",
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts
index 5a48399..04d3198 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts
@@ -45,7 +45,7 @@
   @property({type: String})
   itemTypeName?: string;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -57,7 +57,7 @@
     ];
   }
 
-  render() {
+  override render() {
     const item = this.item ?? 'UNKNOWN ITEM';
     const itemTypeName = this.itemTypeName ?? 'UNKNOWN ITEM TYPE';
     return html` <gr-dialog
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
index fc7ef4b..7800653 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
@@ -43,7 +43,7 @@
 
   private readonly restApiService = appContext.restApiService;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       tableStyles,
@@ -63,7 +63,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <table
       id="list"
       class="genericList ${this._computeLoadingClass(this._loading)}"
@@ -114,7 +114,7 @@
     </table>`;
   }
 
-  updated(changedProperties: PropertyValues) {
+  override updated(changedProperties: PropertyValues) {
     if (changedProperties.has('repo')) {
       this.repoChanged();
     }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index 02339cf..03a1ef1 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -180,7 +180,7 @@
         >
           <input
             is="iron-input"
-            value="${option.info.value}"
+            .value="${option.info.value}"
             @input=${this._handleStringChange}
             data-option-key="${option._key}"
             ?disabled=${!option.info.editable}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 313b91f..6770c00 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -354,10 +354,7 @@
     return this._computeAdditionalReviewers(change).length;
   }
 
-  _computeAdditionalReviewersTitle(
-    change: ChangeInfo | undefined,
-    config: ServerInfo
-  ) {
+  _computeAdditionalReviewersTitle(change?: ChangeInfo, config?: ServerInfo) {
     if (!change || !config) return '';
     return this._computeAdditionalReviewers(change)
       .map(user => getDisplayName(config, user, true))
@@ -393,8 +390,8 @@
   }
 
   _computeWaiting(
-    account?: AccountInfo,
-    change?: ChangeInfo
+    account?: AccountInfo | null,
+    change?: ChangeInfo | null
   ): Timestamp | undefined {
     if (!account?._account_id || !change?.attention_set) return undefined;
     return change?.attention_set[account._account_id]?.last_update;
@@ -417,6 +414,11 @@
     );
   }
 
+  _formatDate(date: Timestamp | undefined): string | undefined {
+    if (!date) return undefined;
+    return date.toString();
+  }
+
   _handleChangeClick() {
     // Don't prevent the default and neither stop bubbling. We just want to
     // report the click, but then let the browser handle the click on the link.
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index 8ee6a3f..4557aac 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -204,7 +204,7 @@
       </template>
       <template is="dom-if" if="[[_computeAdditionalReviewersCount(change)]]">
         <span title="[[_computeAdditionalReviewersTitle(change, config)]]">
-          +[[_computeAdditionalReviewersCount(change, config)]]
+          +[[_computeAdditionalReviewersCount(change)]]
         </span>
       </template>
     </div>
@@ -224,14 +224,14 @@
     hidden$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]"
   >
     <a class="fullRepo" href$="[[_computeRepoUrl(change)]]">
-      [[_computeRepoDisplay(change)]]
+      [[_computeRepoDisplay(change, false)]]
     </a>
     <a
       class="truncatedRepo"
       href$="[[_computeRepoUrl(change)]]"
-      title$="[[_computeRepoDisplay(change)]]"
+      title$="[[_computeRepoDisplay(change, false)]]"
     >
-      [[_computeRepoDisplay(change, 'true')]]
+      [[_computeRepoDisplay(change, true)]]
     </a>
   </td>
   <td
@@ -254,7 +254,7 @@
   >
     <gr-date-formatter
       has-tooltip=""
-      date-str="[[change.updated]]"
+      date-str="[[_formatDate(change.updated)]]"
     ></gr-date-formatter>
   </td>
   <td
@@ -263,7 +263,7 @@
   >
     <gr-date-formatter
       has-tooltip=""
-      date-str="[[change.submitted]]"
+      date-str="[[_formatDate(change.submitted)]]"
     ></gr-date-formatter>
   </td>
   <td
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
index bc21dc0..e87b4f9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -48,7 +48,7 @@
 
   private readonly restApiService = appContext.restApiService;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       dashboardHeaderStyles,
@@ -63,7 +63,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <gr-avatar
         .account="${this._accountDetails}"
         .imageSize=${100}
@@ -116,7 +116,7 @@
       </div>`;
   }
 
-  updated(changedProperties: PropertyValues) {
+  override updated(changedProperties: PropertyValues) {
     if (changedProperties.has('userId')) {
       this._accountChanged(this.userId);
     }
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
index 1052c91..4326939 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
@@ -43,7 +43,7 @@
   @property({type: Object})
   serverConfig: ServerInfo | undefined;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -55,7 +55,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <div class="container">
       <a
         target="_blank"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index c705319..3ab9c82 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -58,7 +58,7 @@
   @property({type: Boolean})
   _initialised = false;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       fontStyles,
@@ -154,7 +154,7 @@
     `;
   }
 
-  render() {
+  override render() {
     return html` <gr-dialog
       id="dialog"
       confirm-label="Continue"
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index b4e85a9..95e4301 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -209,7 +209,7 @@
     this.addEventListener('click', e => this._handleClick(e));
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     this.restApiService.getConfig().then(config => {
       this.config = config;
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 90f7215..68c7957 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -279,11 +279,10 @@
     ];
   }
 
-  override update(changedProperties: PropertyValues) {
+  override updated(changedProperties: PropertyValues) {
     if (changedProperties.has('result')) {
       this.isExpandable = !!this.result?.summary && !!this.result?.message;
     }
-    super.update(changedProperties);
   }
 
   override focus() {
@@ -978,6 +977,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const style = html`<style>
       .headerTopRow .right .goToLatest gr-button {
         --gr-button: {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
index 0dbe823a..c2066d7 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
@@ -76,7 +76,7 @@
     super.disconnectedCallback();
   }
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -92,9 +92,10 @@
     ];
   }
 
-  render() {
+  override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         gr-dropdown {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 7b12b1117..240bb22 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -204,7 +204,7 @@
     this.query = (input: string) => this._getSearchSuggestions(input);
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     this.restApiService.getConfig().then((serverConfig?: ServerInfo) => {
       const mergeability =
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 1c25755..f5072b9 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -150,7 +150,7 @@
     });
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     this.refitOverlay = () => {
       // re-center the dialog as content changed
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index ba1abd0..496d6bf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -579,6 +579,7 @@
 
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         paper-item {
@@ -648,7 +649,9 @@
   // We don't want property changes in updateSizes() to trigger infinite update
   // loops, so we perform this in update() instead of updated().
   override update(changedProperties: PropertyValues) {
+    // eslint-disable-next-line lit/no-property-change-update
     if (!this.baseUrl) this.baseSelected = false;
+    // eslint-disable-next-line lit/no-property-change-update
     if (!this.revisionUrl) this.baseSelected = true;
     this.updateSizes();
     super.update(changedProperties);
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts b/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
index bc3c054..3f2258d 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
@@ -45,6 +45,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         .row {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
index 1255256..db45c33 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
@@ -59,6 +59,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         gr-button,
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
index 988366c..7fee4a0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
@@ -37,7 +37,7 @@
     getPluginLoader().loadPlugins(pluginsPending);
   }
 
-  updated(changedProperties: PropertyValues<GrPluginHost>) {
+  override updated(changedProperties: PropertyValues<GrPluginHost>) {
     if (changedProperties.has('config') && this.config) {
       this._configChanged(this.config);
     }
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
index f913cf6..6580ad6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+/* eslint-disable lit/no-legacy-template-syntax,lit/prefer-static-styles */
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 import {customElement, property} from '@polymer/decorators';
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
index eca38d9..a972db3 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
@@ -41,7 +41,7 @@
     });
   }
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -71,7 +71,7 @@
     `;
   }
 
-  render() {
+  override render() {
     return html` <div class="gr-form-styles">
       <table id="agreements">
         <thead>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
index 6354c9a..c62cff4 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
@@ -75,7 +75,7 @@
     return Promise.all(promises);
   }
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       formStyles,
@@ -113,7 +113,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <div class="gr-form-styles">
         <div ?hidden=${this._passwordUrl}>
           <section>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index 66103fd..31c62b1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -128,6 +128,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         .container {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
index b302bee..fbf29c63 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
@@ -95,7 +95,7 @@
           ?hideStatus=${this.hideStatus}
           ?firstName=${this.firstName}
           .voteableText=${this.voteableText}
-          part="gr-account-link-text => gr-account-label-text"
+          exportparts="gr-account-label-text: gr-account-link-text"
         >
         </gr-account-label>
       </a>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
index 895c99b..ae3853e 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
@@ -30,7 +30,7 @@
 
 @customElement('gr-alert')
 export class GrAlert extends LitElement {
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -93,9 +93,10 @@
     >`;
   }
 
-  render() {
+  override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const style = html`<style>
       .action {
         --gr-button: {
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 2e95bd0..de2a017 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -89,6 +89,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         iron-icon {
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
index a3800a8..437e7e8 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
@@ -85,7 +85,7 @@
   }
 
   @property({type: String, notify: true})
-  dateStr: string | null = null;
+  dateStr: string | undefined = undefined;
 
   @property({type: Boolean})
   showDateAndTime = false;
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
index 0f94b11..3f759b5 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
@@ -36,7 +36,7 @@
   @property({type: Object})
   file?: NormalizedFileInfo;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -65,7 +65,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <span
       class="${this._computeStatusClass(this.file)}"
       tabindex="0"
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index a329736..b789bee 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -96,7 +96,7 @@
     this.reporting = appContext.reportingService;
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     this.restApiService.getConfig().then(config => {
       this._config = config;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
index 5bf5a8f..801b8bf 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
@@ -50,7 +50,7 @@
   @property({type: Number})
   limit?: number;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -84,9 +84,10 @@
     ];
   }
 
-  render() {
+  override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         gr-button::part(paper-button),
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
index b2477dd..423a1a8 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
@@ -53,7 +53,7 @@
     this.bodyScrollHandler = () => this._handleBodyScroll();
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     window.addEventListener('scroll', this.bodyScrollHandler);
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index 73ca99d..2fd3fb9 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -45,7 +45,7 @@
 
   private readonly flagsService = appContext.flagsService;
 
-  static get styles() {
+  static override get styles() {
     return [
       css`
         .vote-chip.max {
@@ -106,7 +106,7 @@
     ];
   }
 
-  render() {
+  override render() {
     if (!this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI))
       return;
     if (!this.vote?.value) return;
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index 9516dce..7b01226 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -25,6 +25,7 @@
     "noUnusedLocals": true, /* Report errors on unused locals. */
     "noUnusedParameters": true, /* Report errors on unused parameters. */
     "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+    "noImplicitOverride": true,
     "noFallthroughCasesInSwitch": true,/* Report errors for fallthrough cases in switch statement. */
 
     "skipLibCheck": true, /* Do not check node_modules */
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeader.soy b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
index 5dfe671..3d0edab 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeader.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
@@ -20,10 +20,10 @@
   {@param attentionSet: ?}
   {if $attentionSet}
     Attention is currently required from:{sp}
-    {for $attentionSetUser in $attentionSet}
+    {for $attentionSetUser, $index in $attentionSet}
       {$attentionSetUser}
       // add commas or dot.
-      {if isLast($attentionSetUser)}.
+      {if $index == length($attentionSet) - 1}.
       {else},{sp}
       {/if}
     {/for}
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
index 191737f..0d8da38 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
@@ -21,10 +21,10 @@
   {@param attentionSet: ?}
   {if $attentionSet}
     <p> Attention is currently required from:{sp}
-    {for $attentionSetUser in $attentionSet}
+    {for $attentionSetUser, $index in $attentionSet}
       {$attentionSetUser}
       //add commas or dot.
-      {if isLast($attentionSetUser)}.
+      {if $index == length($attentionSet) - 1}.
       {else},{sp}
       {/if}
     {/for} </p>
diff --git a/resources/com/google/gerrit/server/mail/Comment.soy b/resources/com/google/gerrit/server/mail/Comment.soy
index 4b923e6..4b66401 100644
--- a/resources/com/google/gerrit/server/mail/Comment.soy
+++ b/resources/com/google/gerrit/server/mail/Comment.soy
@@ -48,8 +48,8 @@
         {\n}
       {/if}
 
-      {for $line in $comment.lines}
-        {if isFirst($line)}
+      {for $line, $index in $comment.lines}
+        {if $index == 0}
           {if $comment.startLine != 0}
             {$comment.link}
           {/if}
diff --git a/resources/com/google/gerrit/server/mail/DeleteReviewer.soy b/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
index a3ed3ee..ae2a9c4 100644
--- a/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
@@ -26,8 +26,8 @@
   {@param email: ?}
   {@param fromName: ?}
   {$fromName} has removed{sp}
-  {for $reviewerName in $email.reviewerNames}
-    {if not isFirst($reviewerName)},{sp}{/if}
+  {for $reviewerName, $index in $email.reviewerNames}
+    {if $index > 0},{sp}{/if}
     {$reviewerName}
   {/for}{sp}
   from this change.{sp}
diff --git a/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy b/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
index 76a9199..fdcbbe7 100644
--- a/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
@@ -25,9 +25,9 @@
     {$fromName}{sp}
     <strong>
       removed{sp}
-      {for $reviewerName in $email.reviewerNames}
-        {if not isFirst($reviewerName)}
-          {if isLast($reviewerName)}{sp}and{else},{/if}{sp}
+      {for $reviewerName, $index in $email.reviewerNames}
+        {if $index > 0}
+          {if $index == (length($email.reviewerNames) - 1)}{sp}and{else},{/if}{sp}
         {/if}
         {$reviewerName}
       {/for}
diff --git a/resources/com/google/gerrit/server/mail/NewChange.soy b/resources/com/google/gerrit/server/mail/NewChange.soy
index aa2b946..c5f34b4 100644
--- a/resources/com/google/gerrit/server/mail/NewChange.soy
+++ b/resources/com/google/gerrit/server/mail/NewChange.soy
@@ -30,8 +30,8 @@
   {if $email.reviewerNames or $email.removedReviewerNames}
    {if $email.reviewerNames}
       Hello{sp}
-      {for $reviewerName in $email.reviewerNames}
-        {if not isFirst($reviewerName)},{sp}{/if}
+      {for $reviewerName, $index in $email.reviewerNames}
+        {if $index > 0},{sp}{/if}
         {$reviewerName}
       {/for},
 
@@ -43,8 +43,8 @@
     {/if}
     {if $email.removedReviewerNames}
       {$fromName} has removed{sp}
-      {for $reviewerName in $email.removedReviewerNames}
-        {if not isFirst($reviewerName)},{sp}{/if}
+      {for $reviewerName, $index in $email.removedReviewerNames}
+        {if $index > 0},{sp}{/if}
         {$reviewerName}
       {/for}{sp}
       from this change.{sp}
diff --git a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
index 272c3ef..008226f 100644
--- a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
+++ b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -29,9 +29,9 @@
     {if $email.reviewerNames or $email.removedReviewerNames}
       {if $email.reviewerNames}
         {$fromName} would like{sp}
-        {for $reviewerName in $email.reviewerNames}
-          {if not isFirst($reviewerName)}
-            {if isLast($reviewerName)}{sp}and{else},{/if}{sp}
+        {for $reviewerName, $index in $email.reviewerNames}
+          {if $index > 0}
+            {if $index == length($email.reviewerNames) - 1}{sp}and{else},{/if}{sp}
           {/if}
           {$reviewerName}
         {/for}{sp}
@@ -44,9 +44,9 @@
           {$fromName}{sp}
           <strong>
             removed{sp}
-            {for $reviewerName in $email.removedReviewerNames}
-              {if not isFirst($reviewerName)}
-                {if isLast($reviewerName)}{sp}and{else},{/if}{sp}
+            {for $reviewerName, $index in $email.removedReviewerNames}
+              {if $index > 0}
+                {if $index == length($email.removedReviewerNames) - 1}{sp}and{else},{/if}{sp}
               {/if}
               {$reviewerName}
             {/for}
diff --git a/yarn.lock b/yarn.lock
index be412a5..17c08c3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -954,6 +954,15 @@
     semver "^7.3.5"
     spdx-expression-parse "^3.0.1"
 
+eslint-plugin-lit@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-lit/-/eslint-plugin-lit-1.5.1.tgz#e5b86fee4aeb6023ad4bb90b3d9e462ca8eff755"
+  integrity sha512-pYB0QM11uyOk5L55QfGhBmWi8a56PkNsnx+zVpY4bxz9YVquEo4BeRnFmf9AwFyT89rhGud9QruFhM2xJ4piwg==
+  dependencies:
+    parse5 "^6.0.1"
+    parse5-htmlparser2-tree-adapter "^6.0.1"
+    requireindex "^1.2.0"
+
 eslint-plugin-node@^11.1.0:
   version "11.1.0"
   resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
@@ -2145,6 +2154,13 @@
     json-parse-even-better-errors "^2.3.0"
     lines-and-columns "^1.1.6"
 
+parse5-htmlparser2-tree-adapter@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
+  integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
+  dependencies:
+    parse5 "^6.0.1"
+
 parse5@^3.0.1:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
@@ -2152,6 +2168,11 @@
   dependencies:
     "@types/node" "*"
 
+parse5@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+
 path-exists@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@@ -2386,6 +2407,11 @@
   resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
   integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
 
+requireindex@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
+  integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"