UX: Refactor iOS composer layout

UX: Refactor iOS composer layout

This addresses the following issues:

  • on iPad, with keyboard attached, the composer is no longer forced to full screen
  • on iPad, with keyboard attached, the topic no longer scrolls when starting a reply and then cancelling it
  • switching between inputs and buttons (formatting, emojis, categories/tags, etc.) no longer causes layout to bounce around
diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6
index 4a2fa23..301e224 100644
--- a/app/assets/javascripts/discourse/components/composer-body.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-body.js.es6
@@ -142,11 +142,6 @@ export default Ember.Component.extend(KeyEnterEscape, {
   viewportResize() {
     const composerVH = window.visualViewport.height * 0.01;
 
-    if (window.visualViewport.height !== window.innerHeight) {
-      document.documentElement.classList.add("keyboard-visible");
-    } else {
-      document.documentElement.classList.remove("keyboard-visible");
-    }
     document.documentElement.style.setProperty(
       "--composer-vh",
       `${composerVH}px`
diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
index c0c7675..27865f5 100644
--- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
+++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
@@ -85,18 +85,12 @@ function positioningWorkaround($fixedElement) {
   const fixedElement = $fixedElement[0];
   const oldHeight = fixedElement.style.height;
 
-  var done = false;
   var originalScrollTop = 0;
+  let lastTouchedElement = null;
 
   positioningWorkaround.blur = function(evt) {
     if (workaroundActive) {
-      done = true;
-
-      $("#main-outlet").show();
-      $("header").show();
-
-      fixedElement.style.position = "";
-      fixedElement.style.top = "";
+      $("body").removeClass("ios-safari-composer-hacks");
 
       if (!iOSWithVisualViewport()) {
         fixedElement.style.height = oldHeight;
@@ -116,15 +110,17 @@ function positioningWorkaround($fixedElement) {
   };
 
   var blurredNow = function(evt) {
+    // we cannot use evt.relatedTarget to get the last focused element in safari iOS
+    // document.activeElement is also unreliable (iOS does not mark buttons as focused)
+    // so instead, we store the last touched element and check against it
+
     if (
-      !done &&
-      evt.srcElement !== document.activeElement &&
-      $(document.activeElement)
-        .parents()
-        .toArray()
-        .indexOf(fixedElement) > -1
+      lastTouchedElement &&
+      ($(lastTouchedElement).hasClass("select-kit-header") ||
+        ["span", "svg", "button"].includes(
+          lastTouchedElement.nodeName.toLowerCase()
+        ))
     ) {
-      // something in focus so skip
       return;
     }
 
@@ -134,60 +130,77 @@ function positioningWorkaround($fixedElement) {
   var blurred = debounce(blurredNow, 250);
 
   var positioningHack = function(evt) {
-    done = false;
-
     // we need this, otherwise changing focus means we never clear
     this.addEventListener("blur", blurred);
 
-    if (fixedElement.style.top === "0px") {
-      if (this !== document.activeElement) {
-        evt.preventDefault();
-
-        // this tricks safari into assuming current input is at top of the viewport
-        // via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input
-        this.style.transform = "translateY(-200px)";
-        this.focus();
-        let _this = this;
-        setTimeout(function() {
-          _this.style.transform = "none";
-        }, 50);
-      }
-      return;
-    }
-
-    // don't trigger keyboard on disabled element (happens when a category is required)
-    if (this.disabled) {
-      return;
-    }
+    // resets focus out of select-kit elements
+    // might become redundant after select-kit refactoring
+    $fixedElement.find(".select-kit.is-expanded > button").trigger("click");
+    $fixedElement
+      .find(".select-kit > button.is-focused")
+      .removeClass("is-focused");
 
     originalScrollTop = $(window).scrollTop();
 
-    // take care of body
+    setTimeout(function() {
+      if (iOSWithVisualViewport()) {
+        // disable hacks when using a hardware keyboard
+        // by default, a hardware keyboard will show the keyboard accessory bar
+        // whose height is currently 55px (using 75 for a bit of a buffer)
+        let heightDiff = window.innerHeight - window.visualViewport.height;
+        if (heightDiff < 75) {
+          return;
+        }
+      }
 
-    $("#main-outlet").hide();
-    $("header").hide();
+      if (fixedElement.style.top === "0px") {
+        if (this !== document.activeElement) {
+          evt.preventDefault();
 
-    $(window).scrollTop(0);
+          // this tricks safari into assuming current input is at top of the viewport
+          // via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input
+          this.style.transform = "translateY(-200px)";
+          this.focus();
+          let _this = this;
+          setTimeout(function() {
+            _this.style.transform = "none";
+          }, 30);
+        }
+        return;
+      }
 
-    let i = 20;
-    let interval = setInterval(() => {
+      // don't trigger keyboard on disabled element (happens when a category is required)
+      if (this.disabled) {
+        return;
+      }
+
+      $("body").addClass("ios-safari-composer-hacks");
       $(window).scrollTop(0);
-      if (i-- === 0) {
-        clearInterval(interval);
+
+      let i = 20;
+      let interval = setInterval(() => {
+        $(window).scrollTop(0);
+        if (i-- === 0) {
+          clearInterval(interval);
+        }
+      }, 10);
+
+      if (!iOSWithVisualViewport()) {
+        const height = calcHeight();
+        fixedElement.style.height = height + "px";
+        $(fixedElement).addClass("no-transition");
       }
-    }, 10);
 
-    fixedElement.style.top = "0px";
+      evt.preventDefault();
+      this.focus();
+      workaroundActive = true;
+    }, 350);
+  };
 
-    if (!iOSWithVisualViewport()) {
-      const height = calcHeight();
-      fixedElement.style.height = height + "px";
-      $(fixedElement).addClass("no-transition");
+  var lastTouched = function(evt) {
+    if (evt && evt.target) {
+      lastTouchedElement = evt.target;
     }
-
-    evt.preventDefault();
-    this.focus();
-    workaroundActive = true;
   };
 
   function attachTouchStart(elem, fn) {
@@ -198,30 +211,8 @@ function positioningWorkaround($fixedElement) {
   }
 
   const checkForInputs = debounce(function() {
-    $fixedElement
-      .find(
-        "button:not(.hide-preview),a:not(.mobile-file-upload):not(.toggle-toolbar)"
-      )
-      .each(function(idx, elem) {
-        if ($(elem).parents(".emoji-picker").length > 0) {
-          return;
-        }
-
-        if ($(elem).parents(".autocomplete").length > 0) {
-          return;
-        }
+    attachTouchStart(fixedElement, lastTouched);
 
-        if ($(elem).parents(".d-editor-button-bar").length > 0) {
-          return;
-        }
-
-        attachTouchStart(this, function(evt) {
-          done = true;
-          $(document.activeElement).blur();
-          evt.preventDefault();
-          $(this).click();
-        });
-      });
     $fixedElement.find("input[type=text],textarea").each(function() {
       attachTouchStart(this, positioningHack);
     });
diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss
index 2a4cab7..ff516a3 100644
--- a/app/assets/stylesheets/common/base/compose.scss
+++ b/app/assets/stylesheets/common/base/compose.scss
@@ -483,3 +483,19 @@ div.ac-wrap {
     opacity: 0;
   }
 }
+
+body.ios-safari-composer-hacks {
+  #main-outlet,
+  header,
+  .grippie,
+  html:not(.fullscreen-composer) & .toggle-fullscreen {
+    display: none;
+  }
+
+  #reply-control {
+    top: 0px;
+    &.open {
+      height: calc(var(--composer-vh, 1vh) * 100);
+    }
+  }
+}
diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss
index b047169..7f4db28 100644
--- a/app/assets/stylesheets/desktop/compose.scss

[... diff too long, it was truncated ...]

GitHub sha: c3a5a8e0

1 Like

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there: