UX: Horizontal scroll bar on top of user directory (when needed) (#13553)

UX: Horizontal scroll bar on top of user directory (when needed) (#13553)

diff --git a/app/assets/javascripts/discourse/app/components/directory-table.js b/app/assets/javascripts/discourse/app/components/directory-table.js
index f1b8193..f9c03ba 100644
--- a/app/assets/javascripts/discourse/app/components/directory-table.js
+++ b/app/assets/javascripts/discourse/app/components/directory-table.js
@@ -2,16 +2,103 @@ import Component from "@ember/component";
 import { action } from "@ember/object";
 
 export default Component.extend({
-  classNames: ["directory-table-container"],
+  lastScrollPosition: 0,
+  ticking: false,
+  _topHorizontalScrollBar: null,
+  _tableContainer: null,
+  _table: null,
+  _fakeScrollContent: null,
+
+  didInsertElement() {
+    this._super(...arguments);
+    this.setProperties({
+      _tableContainer: this.element.querySelector(".directory-table-container"),
+      _topHorizontalScrollBar: this.element.querySelector(
+        ".directory-table-top-scroll"
+      ),
+      _fakeScrollContent: this.element.querySelector(
+        ".directory-table-top-scroll-fake-content"
+      ),
+      _table: this.element.querySelector(".directory-table"),
+    });
+
+    this._tableContainer.addEventListener("scroll", this.onBottomScroll);
+    this._topHorizontalScrollBar.addEventListener("scroll", this.onTopScroll);
+
+    // Set active header might have already scrolled the _tableContainer.
+    // Call onHorizontalScroll manually to scroll the _topHorizontalScrollBar
+    this.onResize();
+    this.onHorizontalScroll(this._tableContainer, this._topHorizontalScrollBar);
+    window.addEventListener("resize", this.onResize);
+  },
+
+  @action
+  onResize() {
+    if (
+      this._tableContainer.getBoundingClientRect().bottom < window.innerHeight
+    ) {
+      // Bottom of the table is visible. Hide the scrollbar
+      this._fakeScrollContent.style.height = 0;
+    } else {
+      this._fakeScrollContent.style.width = `${this._table.offsetWidth}px`;
+      this._fakeScrollContent.style.height = "1px";
+    }
+  },
+
+  @action
+  onTopScroll() {
+    this.onHorizontalScroll(this._topHorizontalScrollBar, this._tableContainer);
+  },
+
+  @action
+  onBottomScroll() {
+    this.onHorizontalScroll(this._tableContainer, this._topHorizontalScrollBar);
+  },
+
+  @action
+  onHorizontalScroll(primary, replica) {
+    if (this.lastScrollPosition === primary.scrollLeft) {
+      return;
+    }
+
+    this.set("lastScrollPosition", primary.scrollLeft);
+
+    if (!this.ticking) {
+      window.requestAnimationFrame(() => {
+        replica.scrollLeft = this.lastScrollPosition;
+        this.set("ticking", false);
+      });
+
+      this.set("ticking", true);
+    }
+  },
+
+  willDestoryElement() {
+    this._tableContainer.removeEventListener("scroll", this.onBottomScroll);
+    this._topHorizontalScrollBar.removeEventListener(
+      "scroll",
+      this.onTopScroll
+    );
+    window.removeEventListener("resize", this.onResize);
+  },
 
   @action
   setActiveHeader(header) {
     // After render, scroll table left to ensure the order by column is visible
+    if (!this._tableContainer) {
+      this.set(
+        "_tableContainer",
+        document.querySelector(".directory-table-container")
+      );
+    }
     const scrollPixels =
-      header.offsetLeft + header.offsetWidth + 10 - this.element.offsetWidth;
+      header.offsetLeft +
+      header.offsetWidth +
+      10 -
+      this._tableContainer.offsetWidth;
 
     if (scrollPixels > 0) {
-      this.element.scrollLeft = scrollPixels;
+      this._tableContainer.scrollLeft = scrollPixels;
     }
   },
 });
diff --git a/app/assets/javascripts/discourse/app/templates/components/directory-table.hbs b/app/assets/javascripts/discourse/app/templates/components/directory-table.hbs
index a646d79..5864d03 100644
--- a/app/assets/javascripts/discourse/app/templates/components/directory-table.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/directory-table.hbs
@@ -1,25 +1,31 @@
-<table>
-  <thead>
-    {{table-header-toggle field="username" order=order asc=asc}}
-    {{#each columns as |column|}}
-      {{table-header-toggle
-        field=column.name
-        icon=column.icon
-        order=order
-        asc=asc
-        automatic=(directory-column-is-automatic column=column)
-        translated=column.user_field_id
-        onActiveRender=setActiveHeader
-      }}
-    {{/each}}
+<div class="directory-table-top-scroll">
+  <div class="directory-table-top-scroll-fake-content"></div>
+</div>
 
-    {{#if showTimeRead}}
-      <th>{{i18n "directory.time_read"}}</th>
-    {{/if}}
-  </thead>
-  <tbody>
-    {{#each items as |item|}}
-      {{directory-item item=item columns=columns showTimeRead=showTimeRead}}
-    {{/each}}
-  </tbody>
-</table>
+<div class="directory-table-container">
+  <table class="directory-table">
+    <thead>
+      {{table-header-toggle field="username" order=order asc=asc}}
+      {{#each columns as |column|}}
+        {{table-header-toggle
+          field=column.name
+          icon=column.icon
+          order=order
+          asc=asc
+          automatic=(directory-column-is-automatic column=column)
+          translated=column.user_field_id
+          onActiveRender=setActiveHeader
+        }}
+      {{/each}}
+
+      {{#if showTimeRead}}
+        <th>{{i18n "directory.time_read"}}</th>
+      {{/if}}
+    </thead>
+    <tbody>
+      {{#each items as |item|}}
+        {{directory-item item=item columns=columns showTimeRead=showTimeRead}}
+      {{/each}}
+    </tbody>
+  </table>
+</div>
diff --git a/app/assets/stylesheets/common/base/directory.scss b/app/assets/stylesheets/common/base/directory.scss
index 2a8e82b..ffbd1a5 100644
--- a/app/assets/stylesheets/common/base/directory.scss
+++ b/app/assets/stylesheets/common/base/directory.scss
@@ -6,6 +6,11 @@
     overflow-x: auto;
   }
 
+  .directory-table-top-scroll {
+    width: 100%;
+    overflow-x: auto;
+  }
+
   .open-edit-columns-btn {
     vertical-align: top;
     padding: 0.45em 0.8em;

GitHub sha: d03aee46427595ebff93827e388eb48e0fed1212

This commit appears in #13553 which was approved by eviltrout. It was merged by markvanlan.