Bug 1606183 - Pin-to-bottom in Network panel using scroll anchoring. r=Honza
authorHarald Kirschner <hkirschner@mozilla.com>
Sun, 05 Jan 2020 04:59:29 +0000
changeset 508848 85fb2858c3512a632ada63d2c791b809af0ef041
parent 508847 9d401beea71d16526843d414e0f17415ce606a8c
child 508849 6e59e8b14b97a6d2d448e10edd5e4d96b005a9a0
push id36983
push userrmaries@mozilla.com
push dateMon, 06 Jan 2020 09:24:27 +0000
treeherdermozilla-central@bc5880b621d5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1606183
milestone73.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1606183 - Pin-to-bottom in Network panel using scroll anchoring. r=Honza Differential Revision: https://phabricator.services.mozilla.com/D58336
devtools/client/netmonitor/src/assets/styles/RequestList.css
devtools/client/netmonitor/src/components/request-list/RequestListContent.js
--- a/devtools/client/netmonitor/src/assets/styles/RequestList.css
+++ b/devtools/client/netmonitor/src/assets/styles/RequestList.css
@@ -51,16 +51,27 @@
 }
 
 .requests-list-scroll {
   overflow-x: hidden;
   overflow-y: auto;
   width: 100% !important;
 }
 
+.requests-list-scroll table {
+  /* Disable overflow-anchor for only child in the scrollable element */
+  overflow-anchor: none;
+}
+
+.requests-list-anchor {
+  overflow-anchor: auto;
+  opacity: 0;
+  height: 1px;
+}
+
 .requests-list-table,
 .ws-frames-list-table {
   /* Reset default browser style of <table> */
   border-spacing: 0;
   width: 100%;
   /* The layout must be fixed for resizing of columns to work.
   The layout is based on the first row.
   Set the width of those cells, and the rest of the table follows. */
@@ -109,20 +120,17 @@
 .requests-list-headers .requests-list-column:first-child .requests-list-header-button,
 .ws-frames-list-headers .ws-frames-list-column:first-child .ws-frames-list-header-button {
   border-width: 0;
 }
 
 .requests-list-header-button,
 .ws-frames-list-header-button {
   background-color: transparent;
-  border-image: linear-gradient(transparent 15%,
-                                var(--theme-splitter-color) 15%,
-                                var(--theme-splitter-color) 85%,
-                                transparent 85%) 1 1;
+  border-image: linear-gradient( transparent 15%, var(--theme-splitter-color) 15%, var(--theme-splitter-color) 85%, transparent 85%) 1 1;
   border-width: 0;
   border-inline-start-width: 1px;
   width: 100%;
   min-height: var(--theme-toolbar-height);
   text-align: start;
   color: inherit;
   padding: 1px 4px;
 }
--- a/devtools/client/netmonitor/src/components/request-list/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/request-list/RequestListContent.js
@@ -53,18 +53,16 @@ const RequestListItem = createFactory(
 const RequestListContextMenu = require("devtools/client/netmonitor/src/widgets/RequestListContextMenu");
 
 const { div } = dom;
 
 // Tooltip show / hide delay in ms
 const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
 // Tooltip image maximum dimension in px
 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
-// Gecko's scrollTop is int32_t, so the maximum value is 2^31 - 1 = 2147483647
-const MAX_SCROLL_HEIGHT = 2147483647;
 
 const LEFT_MOUSE_BUTTON = 0;
 const RIGHT_MOUSE_BUTTON = 2;
 
 /**
  * Renders the actual contents of the request list.
  */
 class RequestListContent extends Component {
@@ -97,26 +95,25 @@ class RequestListContent extends Compone
       selectRequest: PropTypes.func.isRequired,
       selectedRequest: PropTypes.object,
       requestFilterTypes: PropTypes.object.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
-    this.isScrolledToBottom = this.isScrolledToBottom.bind(this);
     this.onHover = this.onHover.bind(this);
     this.onScroll = this.onScroll.bind(this);
     this.onResize = this.onResize.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
     this.openRequestInTab = this.openRequestInTab.bind(this);
     this.onDoubleClick = this.onDoubleClick.bind(this);
     this.onContextMenu = this.onContextMenu.bind(this);
-    this.onFocusedNodeChange = this.onFocusedNodeChange.bind(this);
     this.onMouseDown = this.onMouseDown.bind(this);
+    this.hasOverflow = false;
   }
 
   componentWillMount() {
     this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
     window.addEventListener("resize", this.onResize);
   }
 
   componentDidMount() {
@@ -125,27 +122,21 @@ class RequestListContent extends Compone
       toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
       interactive: true,
     });
     // Install event handler to hide the tooltip on scroll
     this.refs.scrollEl.addEventListener("scroll", this.onScroll, true);
     this.onResize();
   }
 
-  componentWillUpdate(nextProps) {
-    // Check if the list is scrolled to bottom before the UI update.
-    this.shouldScrollBottom = this.isScrolledToBottom();
-  }
-
   componentDidUpdate(prevProps) {
-    const node = this.refs.scrollEl;
-    // Keep the list scrolled to bottom if a new row was added
-    if (this.shouldScrollBottom && node.scrollTop !== MAX_SCROLL_HEIGHT) {
-      // Using maximum scroll height rather than node.scrollHeight to avoid sync reflow.
-      node.scrollTop = MAX_SCROLL_HEIGHT;
+    const output = this.refs.scrollEl;
+    if (!this.hasOverflow && output.scrollHeight > output.clientHeight) {
+      output.scrollTop = output.scrollHeight;
+      this.hasOverflow = true;
     }
     if (
       prevProps.networkDetailsOpen !== this.props.networkDetailsOpen ||
       prevProps.networkDetailsWidth !== this.props.networkDetailsWidth ||
       prevProps.networkDetailsHeight !== this.props.networkDetailsHeight
     ) {
       this.onResize();
     }
@@ -164,31 +155,16 @@ class RequestListContent extends Compone
    * So it is needed in ComponentDidMount and ComponentDidUpdate. See Bug 1532914.
    */
   onResize() {
     const parent = this.refs.scrollEl.parentNode;
     this.refs.scrollEl.style.width = parent.offsetWidth + "px";
     this.refs.scrollEl.style.height = parent.offsetHeight + "px";
   }
 
-  isScrolledToBottom() {
-    const { scrollEl, rowGroupEl } = this.refs;
-    const lastChildEl = rowGroupEl.lastElementChild;
-
-    if (!lastChildEl) {
-      return false;
-    }
-
-    const lastNodeHeight = lastChildEl.clientHeight;
-    return (
-      scrollEl.scrollTop + scrollEl.clientHeight >=
-      scrollEl.scrollHeight - lastNodeHeight / 2
-    );
-  }
-
   /**
    * The predicate used when deciding whether a popup should be shown
    * over a request item or not.
    *
    * @param Node target
    *        The element node currently being hovered.
    * @param object tooltip
    *        The current tooltip instance.
@@ -330,24 +306,16 @@ class RequestListContent extends Compone
         removeBlockedUrl,
         openRequestInTab: this.openRequestInTab,
       });
     }
 
     this.contextMenu.open(evt, clickedRequest, displayedRequests, blockedUrls);
   }
 
-  /**
-   * If selection has just changed (by keyboard navigation), don't keep the list
-   * scrolled to bottom, but allow scrolling up with the selection.
-   */
-  onFocusedNodeChange() {
-    this.shouldScrollBottom = false;
-  }
-
   render() {
     const {
       connector,
       columns,
       displayedRequests,
       firstRequestStartedMs,
       onCauseBadgeMouseDown,
       onSecurityIconMouseDown,
@@ -359,62 +327,68 @@ class RequestListContent extends Compone
       openRequestBlockingAndDisableUrls,
     } = this.props;
 
     return div(
       {
         ref: "scrollEl",
         className: "requests-list-scroll",
       },
-      dom.table(
-        {
-          className: "requests-list-table",
-        },
-        RequestListHeader(),
-        dom.tbody(
+      [
+        dom.table(
           {
-            ref: "rowGroupEl",
-            className: "requests-list-row-group",
-            tabIndex: 0,
-            onKeyDown: this.onKeyDown,
-            style: {
-              "--timings-scale": scale,
-              "--timings-rev-scale": 1 / scale,
-            },
+            className: "requests-list-table",
+            key: "table",
           },
-          displayedRequests.map((item, index) =>
-            RequestListItem({
-              blocked: !!item.blockedReason,
-              firstRequestStartedMs,
-              fromCache: item.status === "304" || item.fromCache,
-              networkDetailsOpen: this.props.networkDetailsOpen,
-              connector,
-              columns,
-              item,
-              index,
-              isSelected: item.id === (selectedRequest && selectedRequest.id),
-              key: item.id,
-              onContextMenu: this.onContextMenu,
-              onFocusedNodeChange: this.onFocusedNodeChange,
-              onDoubleClick: () => this.onDoubleClick(item),
-              onMouseDown: evt =>
-                this.onMouseDown(evt, item.id, item.channelId),
-              onCauseBadgeMouseDown: () => onCauseBadgeMouseDown(item.cause),
-              onSecurityIconMouseDown: () =>
-                onSecurityIconMouseDown(item.securityState),
-              onWaterfallMouseDown: () => onWaterfallMouseDown(),
-              requestFilterTypes,
-              openRequestBlockingAndAddUrl: url =>
-                openRequestBlockingAndAddUrl(url),
-              openRequestBlockingAndDisableUrls: url =>
-                openRequestBlockingAndDisableUrls(url),
-            })
-          )
-        ) // end of requests-list-row-group">
-      )
+          RequestListHeader(),
+          dom.tbody(
+            {
+              ref: "rowGroupEl",
+              className: "requests-list-row-group",
+              tabIndex: 0,
+              onKeyDown: this.onKeyDown,
+              style: {
+                "--timings-scale": scale,
+                "--timings-rev-scale": 1 / scale,
+              },
+            },
+            displayedRequests.map((item, index) =>
+              RequestListItem({
+                blocked: !!item.blockedReason,
+                firstRequestStartedMs,
+                fromCache: item.status === "304" || item.fromCache,
+                networkDetailsOpen: this.props.networkDetailsOpen,
+                connector,
+                columns,
+                item,
+                index,
+                isSelected: item.id === (selectedRequest && selectedRequest.id),
+                key: item.id,
+                onContextMenu: this.onContextMenu,
+                onDoubleClick: () => this.onDoubleClick(item),
+                onMouseDown: evt =>
+                  this.onMouseDown(evt, item.id, item.channelId),
+                onCauseBadgeMouseDown: () => onCauseBadgeMouseDown(item.cause),
+                onSecurityIconMouseDown: () =>
+                  onSecurityIconMouseDown(item.securityState),
+                onWaterfallMouseDown: () => onWaterfallMouseDown(),
+                requestFilterTypes,
+                openRequestBlockingAndAddUrl: url =>
+                  openRequestBlockingAndAddUrl(url),
+                openRequestBlockingAndDisableUrls: url =>
+                  openRequestBlockingAndDisableUrls(url),
+              })
+            )
+          ) // end of requests-list-row-group">
+        ),
+        dom.div({
+          className: "requests-list-anchor",
+          key: "anchor",
+        }),
+      ]
     );
   }
 }
 
 module.exports = connect(
   state => ({
     blockedUrls: state.requestBlocking.blockedUrls
       .map(({ enabled, url }) => (enabled ? url : null))