Bug 1637652 - switch to a sticky-position and deal with the content moving while dropdowns are up, r=eeejay
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 22 May 2020 21:33:38 +0000
changeset 531737 8f9d7916be0157d299f1b88a4a09ae8fbff334b3
parent 531736 3017e4a904482f1f33b08d268441b42590db4ef5
child 531738 56423aee609ec894e9d7e1077d5cdb3751891376
push id37442
push userncsoregi@mozilla.com
push dateSat, 23 May 2020 09:21:24 +0000
treeherdermozilla-central@bbcc193fe0f0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseeejay
bugs1637652
milestone78.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 1637652 - switch to a sticky-position and deal with the content moving while dropdowns are up, r=eeejay Differential Revision: https://phabricator.services.mozilla.com/D76492
toolkit/components/reader/AboutReader.jsm
toolkit/components/reader/content/aboutReader.html
toolkit/themes/shared/aboutReader.css
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -376,16 +376,17 @@ AboutReader.prototype = {
         // height / font-size changes that caused a page height change.
         if (lastHeight == this._lastHeight) {
           this._closeDropdowns(true);
         }
 
         break;
       case "resize":
         this._updateImageMargins();
+        this._scheduleToolbarOverlapHandler();
         break;
 
       case "wheel":
         let doZoom =
           (aEvent.ctrlKey && zoomOnCtrl) || (aEvent.metaKey && zoomOnMeta);
         if (!doZoom) {
           return;
         }
@@ -511,23 +512,25 @@ AboutReader.prototype = {
     }
   },
 
   _changeFontSize(changeAmount) {
     let currentSize =
       Services.prefs.getIntPref("reader.font_size") + changeAmount;
     this._setFontSize(currentSize);
     this._updateFontSizeButtonControls();
+    this._scheduleToolbarOverlapHandler();
   },
 
   _setContentWidth(newContentWidth) {
     this._contentWidth = newContentWidth;
     this._displayContentWidth(newContentWidth);
     let width = 20 + 5 * (this._contentWidth - 1) + "em";
     this._doc.body.style.setProperty("--content-width", width);
+    this._scheduleToolbarOverlapHandler();
     return AsyncPrefs.set("reader.content_width", this._contentWidth);
   },
 
   _displayContentWidth(currentContentWidth) {
     let contentWidthValue = this._doc.querySelector(".content-width-value");
     contentWidthValue.textContent = currentContentWidth;
   },
 
@@ -1110,40 +1113,104 @@ AboutReader.prototype = {
     this._lastHeight = windowUtils.getBoundsWithoutFlushing(
       this._doc.body
     ).height;
     this._doc.addEventListener("scroll", this);
 
     dropdown.classList.add("open");
 
     this._toolbarContainerElement.classList.add("dropdown-open");
+    this._toggleToolbarFixedPosition(true);
   },
 
   /*
    * If the ReaderView has open dropdowns, close them. If we are closing the
    * dropdowns because the page is scrolling, allow popups to stay open with
    * the keep-open class.
    */
   _closeDropdowns(scrolling) {
     let selector = ".dropdown.open";
     if (scrolling) {
       selector += ":not(.keep-open)";
     }
 
     let openDropdowns = this._doc.querySelectorAll(selector);
+    let haveOpenDropdowns = openDropdowns.length;
     for (let dropdown of openDropdowns) {
       dropdown.classList.remove("open");
     }
 
     this._toolbarContainerElement.classList.remove("dropdown-open");
+    if (haveOpenDropdowns) {
+      this._toggleToolbarFixedPosition(false);
+    }
 
     // Stop handling scrolling:
     this._doc.removeEventListener("scroll", this);
   },
 
+  _toggleToolbarFixedPosition(shouldBeFixed) {
+    let el = this._toolbarContainerElement;
+    let fontSize = this._doc.body.style.getPropertyValue("--font-size");
+    let contentWidth = this._doc.body.style.getPropertyValue("--content-width");
+    if (shouldBeFixed) {
+      el.style.setProperty("--font-size", fontSize);
+      el.style.setProperty("--content-width", contentWidth);
+      el.classList.add("transition-location");
+    } else {
+      let expectTransition =
+        el.style.getPropertyValue("--font-size") != fontSize ||
+        el.style.getPropertyValue("--content-width") != contentWidth;
+      if (expectTransition) {
+        el.addEventListener(
+          "transitionend",
+          () => el.classList.remove("transition-location"),
+          { once: true }
+        );
+      } else {
+        el.classList.remove("transition-location");
+      }
+      el.style.removeProperty("--font-size");
+      el.style.removeProperty("--content-width");
+      el.classList.remove("overlaps");
+    }
+  },
+
+  _scheduleToolbarOverlapHandler() {
+    if (this._enqueuedToolbarOverlapHandler) {
+      return;
+    }
+    this._enqueuedToolbarOverlapHandler = this._win.requestAnimationFrame(
+      () => {
+        this._win.setTimeout(() => this._toolbarOverlapHandler(), 0);
+      }
+    );
+  },
+
+  _toolbarOverlapHandler() {
+    delete this._enqueuedToolbarOverlapHandler;
+    // Ensure the dropdown is still open to avoid racing with that changing.
+    if (this._toolbarContainerElement.classList.contains("dropdown-open")) {
+      let { windowUtils } = this._win;
+      let toolbarBounds = windowUtils.getBoundsWithoutFlushing(
+        this._toolbarElement.parentNode
+      );
+      let textBounds = windowUtils.getBoundsWithoutFlushing(
+        this._containerElement
+      );
+      let overlaps = false;
+      if (Services.locale.isAppLocaleRTL) {
+        overlaps = textBounds.right > toolbarBounds.left;
+      } else {
+        overlaps = textBounds.left < toolbarBounds.right;
+      }
+      this._toolbarContainerElement.classList.toggle("overlaps", overlaps);
+    }
+  },
+
   _topScrollChange(entries) {
     if (!entries.length) {
       return;
     }
     // If we don't intersect the item at the top of the document, we're
     // scrolled down:
     let scrolled = !entries[entries.length - 1].isIntersecting;
     let tbc = this._toolbarContainerElement;
--- a/toolkit/components/reader/content/aboutReader.html
+++ b/toolkit/components/reader/content/aboutReader.html
@@ -9,37 +9,16 @@
   <meta http-equiv="Content-Security-Policy" content="default-src chrome:; img-src data: *; media-src *; object-src 'none'" />
   <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
   <meta name="viewport" content="width=device-width; user-scalable=0" />
   <link rel="stylesheet" href="chrome://global/skin/aboutReader.css" type="text/css"/>
 </head>
 
 <body>
   <div class="top-anchor"></div>
-  <div class="container">
-    <div class="header reader-header">
-      <a class="domain reader-domain"></a>
-      <div class="domain-border"></div>
-      <h1 class="reader-title"></h1>
-      <div class="credits reader-credits"></div>
-      <div class="meta-data">
-        <div class="reader-estimated-time"></div>
-      </div>
-    </div>
-
-    <hr>
-
-    <div class="content">
-      <div class="moz-reader-content"></div>
-    </div>
-
-    <div>
-      <div class="reader-message"></div>
-    </div>
-  </div>
 
   <div class="toolbar-container">
     <div class="toolbar reader-toolbar">
       <div class="reader-controls">
         <button class="close-button button "></button>
         <ul class="dropdown style-dropdown">
           <li>
             <button class="dropdown-toggle button style-button"></button>
@@ -67,11 +46,33 @@
             <hr>
             <div class="color-scheme-buttons"></div>
             <div class="dropdown-arrow"/>
           </li>
         </ul>
       </div>
     </div>
   </div>
+
+  <div class="container">
+    <div class="header reader-header">
+      <a class="domain reader-domain"></a>
+      <div class="domain-border"></div>
+      <h1 class="reader-title"></h1>
+      <div class="credits reader-credits"></div>
+      <div class="meta-data">
+        <div class="reader-estimated-time"></div>
+      </div>
+    </div>
+
+    <hr>
+
+    <div class="content">
+      <div class="moz-reader-content"></div>
+    </div>
+
+    <div>
+      <div class="reader-message"></div>
+    </div>
+  </div>
 </body>
 
 </html>
--- a/toolkit/themes/shared/aboutReader.css
+++ b/toolkit/themes/shared/aboutReader.css
@@ -221,47 +221,61 @@ body.dark blockquote {
 .header > .meta-data {
   font-size: 0.65em;
   margin: 0 0 15px 0;
 }
 
 /*======= Controls toolbar =======*/
 
 .toolbar-container {
-  position: fixed;
+  position: sticky;
   z-index: 1;
-  top: var(--body-padding);
-  bottom: 0;
-  inset-inline-start: 0;
-  /* Ensure we have the same width as the body padding: */
-  font-size: var(--font-size);
-  width: max(var(--body-padding), calc((100% - var(--content-width)) / 2));
+  top: 32px;
+  height: 0; /* take up no space, so body is at the top. */
+
+  /* As a stick container, we're positioned relative to the body. Move us to
+   * the edge of the viewport using margins, and take the width of
+   * the body padding into account for calculating our width.
+   */
+  margin-left: calc(-1 * var(--body-padding));
+  width: max(var(--body-padding), calc((100% - var(--content-width)) / 2 + var(--body-padding)));
+  font-size: var(--font-size); /* Needed to ensure `em` units match, is reset for .reader-controls */
+}
+
+/* Have to do this manually (instead of using margin-inline-start),
+ * because dir=rtl is not set on its parent. */
+.toolbar-container[dir=rtl] {
+  margin-left: auto;
+  margin-right: calc(-1 * var(--body-padding));
 }
 
 .toolbar {
   padding-block: 16px;
   border: 1px solid var(--toolbar-border);
   border-radius: 6px;
   box-shadow: 0 2px 8px var(--toolbar-box-shadow);
 
   width: 32px; /* basic width, without padding/border */
-  float: inline-end; /* float this towards the body */
 
   /* padding should be 16px, except if there's not enough space for that, in
    * which case use half the available space for padding (=25% on each side).
    * The 34px here is the width + borders. We use a variable because we need
    * to know this size for the margin calculation.
    */
   --inline-padding: min(16px, calc(25% - 0.25 * 34px));
   padding-inline: var(--inline-padding);
 
   /* Keep a maximum of 96px distance to the body, but center once the margin
-   * gets too small. To center, use 50% of the container, subtract half our
-   * own width (16px) and half the border (1px) and the inline padding. */
-  margin-inline-end: min(calc(50% - 17px - var(--inline-padding)), 96px);
+   * gets too small. We need to set the start margin, however...
+   * To center, we'd want 50% of the container, but we'd subtract half our
+   * own width (16px) and half the border (1px) and the inline padding.
+   * When the other margin would be 96px, we want 100% - 96px - the complete
+   * width of the actual toolbar (34px + 2 * padding)
+   */
+  margin-inline-start: max(calc(50% - 17px - var(--inline-padding)), calc(100% - 96px - 34px - 2 * var(--inline-padding)));
 
   font-family: Helvetica, Arial, sans-serif;
   list-style: none;
   -moz-user-select: none;
 }
 
 @media (prefers-reduced-motion: no-preference) {
   .toolbar {
@@ -277,16 +291,25 @@ body.dark blockquote {
   .toolbar-container.scrolled .toolbar:not(:hover) {
     border-color: transparent;
     box-shadow: 0 2px 8px transparent;
   }
 
   .toolbar-container.scrolled .toolbar:not(:hover) .button {
     opacity: 0.6;
   }
+
+  .toolbar-container.transition-location {
+    transition-duration: 250ms;
+    transition-property: width;
+  }
+}
+
+.toolbar-container.overlaps .button {
+  opacity: 0.1;
 }
 
 .dropdown-open .toolbar {
   border-color: transparent;
   box-shadow: 0 2px 8px transparent;
 }
 
 .reader-controls {