Introduce diff page sidebar for plugins

Demo: https://imgur.com/a/GiTHlVY
Dark: https://imgur.com/a/6GDffto

A plugin must register to 2 endpoints:

1. 'sidebarTrigger', typically a button or icon, placed in the headerbar

pluginApi.registerCustomComponent(
    'sidebarTrigger', 'my-sidebar-trigger');

The trigger component recives an onTrigger function to call with the
plugin name when triggered:

this.onTrigger(this.plugin.getPluginName());

2. 'sidebarContent', arbitrary content appearing in the sidebar

The trigger endpoint must be registered dynamically.

pluginApi.registerDynamicCustomComponent(
    'sidebarContent', 'my-sidebar-content');

Release-Notes: Add diff page sidebar for plugins
Change-Id: I3aba40fec65a2d7617034b5614ec52ab45cc812c
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 93cb124..1eef413 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -5,6 +5,8 @@
  */
 import '@polymer/iron-dropdown/iron-dropdown';
 import '@polymer/iron-input/iron-input';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../../../styles/gr-a11y-styles';
 import '../../../styles/shared-styles';
 import '../../shared/gr-button/gr-button';
@@ -139,6 +141,11 @@
   @query('#diffPreferencesDialog')
   diffPreferencesDialog?: GrDiffPreferencesDialog;
 
+  @query('.sidebarAnchor')
+  sidebarAnchor?: HTMLDivElement;
+
+  @state() private sidebarHeight = 0;
+
   // Private but used in tests.
   @state()
   get patchRange(): PatchRange | undefined {
@@ -182,6 +189,8 @@
 
   @state() path?: string;
 
+  @state() private shownSidebar?: string;
+
   /** Allows us to react when the user switches to the DIFF view. */
   // Private but used in tests.
   @state() isActiveChildView = false;
@@ -650,6 +659,31 @@
         :host(.hideComments) {
           --gr-comment-thread-display: none;
         }
+        .sidebarTriggerContainer {
+          display: inline-block;
+        }
+        .sidebarAnchor {
+          height: 0;
+          width: 0;
+          overflow: visible;
+        }
+        .sidebarContents {
+          background: var(--background-color-secondary);
+          width: max-content;
+          padding: var(--spacing-l);
+          border: var(--spacing-xs) solid var(--border-color);
+          border-left: 0;
+          overflow-y: auto;
+          animation: slide-in 50ms;
+        }
+        @keyframes slide-in {
+          0% {
+            transform: translateX(-100%);
+          }
+          100% {
+            transform: translateX(0);
+          }
+        }
       `,
     ];
   }
@@ -663,10 +697,14 @@
     // TODO(newdiff-cleanup): Remove once newdiff migration is completed.
     this.cursor = isNewDiff() ? new GrDiffCursorNew() : new GrDiffCursor();
     if (this.diffHost) this.reInitCursor();
+    window.addEventListener('scroll', this.updateSidebarHeight);
+    window.addEventListener('resize', this.updateSidebarHeight);
   }
 
   override disconnectedCallback() {
     this.cursor?.dispose();
+    window.removeEventListener('scroll', this.updateSidebarHeight);
+    window.removeEventListener('resize', this.updateSidebarHeight);
     super.disconnectedCallback();
   }
 
@@ -676,6 +714,13 @@
     this.cursor?.reInitCursor();
   }
 
+  private readonly updateSidebarHeight = () => {
+    if (this.sidebarAnchor) {
+      this.sidebarHeight =
+        window.innerHeight - this.sidebarAnchor.getBoundingClientRect().bottom;
+    }
+  };
+
   protected override updated(changedProperties: PropertyValues): void {
     super.updated(changedProperties);
     if (
@@ -720,6 +765,7 @@
         });
       }
     }
+    this.updateSidebarHeight();
   }
 
   override render() {
@@ -774,6 +820,7 @@
           >&gt;</a
         >
       </div>
+      ${this.renderSidebarContent()}
     </div>`;
   }
 
@@ -805,6 +852,7 @@
             @value-change=${this.handleFileChange}
           ></gr-dropdown-list>
         </div>
+        ${this.renderSidebarTriggers()}
       </div>
       <div class="navLinks desktop">
         <span class="fileNum ${ifDefined(fileNumClass)}">
@@ -843,6 +891,55 @@
       </div>`;
   }
 
+  private renderSidebarTriggers() {
+    return html`
+      <div class="sidebarTriggerContainer">
+        <gr-endpoint-decorator name="sidebarTrigger">
+          <gr-endpoint-param
+            name="onTrigger"
+            .value=${(pluginName: string) =>
+              (this.shownSidebar =
+                this.shownSidebar === pluginName ? undefined : pluginName)}
+          ></gr-endpoint-param>
+        </gr-endpoint-decorator>
+      </div>
+    `;
+  }
+
+  private renderSidebarContent() {
+    // Always renders the 0x0px .sidebarAnchor div for scroll measurements.
+    return html`
+      <div class="sidebarAnchor">
+        ${when(
+          this.shownSidebar !== undefined,
+          () => html`
+            <div
+              class="sidebarContents"
+              style=${`height: ${this.sidebarHeight}px`}
+            >
+              <gr-endpoint-decorator
+                name=${`sidebarContent-${this.shownSidebar}`}
+              >
+                <gr-endpoint-param
+                  name="change"
+                  .value=${this.change}
+                ></gr-endpoint-param>
+                <gr-endpoint-param
+                  name="path"
+                  .value=${this.path}
+                ></gr-endpoint-param>
+                <gr-endpoint-param
+                  name="patchNum"
+                  .value=${this.patchNum}
+                ></gr-endpoint-param>
+              </gr-endpoint-decorator>
+            </div>
+          `
+        )}
+      </div>
+    `;
+  }
+
   private renderPatchRangeLeft() {
     return html` <div class="patchRangeLeft">
       <gr-patch-range-select
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index c085953..8562741 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -227,6 +227,11 @@
                   <gr-dropdown-list id="dropdown" show-copy-for-trigger-text="">
                   </gr-dropdown-list>
                 </div>
+                <div class="sidebarTriggerContainer">
+                  <gr-endpoint-decorator name="sidebarTrigger">
+                    <gr-endpoint-param name="onTrigger"></gr-endpoint-param>
+                  </gr-endpoint-decorator>
+                </div>
               </div>
               <div class="desktop navLinks">
                 <span class="fileNum show">
@@ -348,6 +353,7 @@
                 >
               </a>
             </div>
+            <div class="sidebarAnchor"></div>
           </div>
           <h2 class="assistive-tech-only">Diff view</h2>
           <gr-diff-host id="diffHost"> </gr-diff-host>