UX: Use Visual Viewport API for iOS composer height

UX: Use Visual Viewport API for iOS composer height

This applies to iPhones running iOS 13+. Previous technique remains in place for iOS 12 and below.

Note that this does not apply to iPads on iOS 13 due to Apple no longer identifying iPads in the user agent string.

diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6
index 1d89f93..b1af547 100644
--- a/app/assets/javascripts/discourse/components/composer-body.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-body.js.es6
@@ -131,6 +131,25 @@ export default Ember.Component.extend(KeyEnterEscape, {
       $document.on(DRAG_EVENTS, throttledPerformDrag);
       $document.on(END_EVENTS, endDrag);
     });
+
+    if (window.visualViewport !== undefined) {
+      this.viewportResize();
+      window.visualViewport.addEventListener("resize", this.viewportResize);
+    }
+  },
+
+  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`
+    );
   },
 
   didInsertElement() {
@@ -155,6 +174,9 @@ export default Ember.Component.extend(KeyEnterEscape, {
   willDestroyElement() {
     this._super(...arguments);
     this.appEvents.off("composer:resize", this, this.resize);
+    if (window.visualViewport !== undefined) {
+      window.visualViewport.removeEventListener("resize", this.viewportResize);
+    }
   },
 
   click() {
diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
index 1c23463..3744c82 100644
--- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
+++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
@@ -1,6 +1,9 @@
 import debounce from "discourse/lib/debounce";
 import { isAppleDevice, safariHacksDisabled } from "discourse/lib/utilities";
 
+// TODO: remove calcHeight once iOS 13 adoption > 90%
+// In iOS 13 and up we use visualViewport API to calculate height
+
 // we can't tell what the actual visible window height is
 // because we cannot account for the height of the mobile keyboard
 // and any other mobile autocomplete UI that may appear
@@ -89,9 +92,14 @@ function positioningWorkaround($fixedElement) {
 
       fixedElement.style.position = "";
       fixedElement.style.top = "";
-      fixedElement.style.height = oldHeight;
 
-      Ember.run.later(() => $(fixedElement).removeClass("no-transition"), 500);
+      if (window.visualViewport === undefined) {
+        fixedElement.style.height = oldHeight;
+        Ember.run.later(
+          () => $(fixedElement).removeClass("no-transition"),
+          500
+        );
+      }
 
       $(window).scrollTop(originalScrollTop);
 
@@ -165,10 +173,11 @@ function positioningWorkaround($fixedElement) {
 
     fixedElement.style.top = "0px";
 
-    const height = calcHeight();
-    fixedElement.style.height = height + "px";
-
-    $(fixedElement).addClass("no-transition");
+    if (window.visualViewport === undefined) {
+      const height = calcHeight();
+      fixedElement.style.height = height + "px";
+      $(fixedElement).addClass("no-transition");
+    }
 
     evt.preventDefault();
     this.focus();
diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss
index f9da09f..4c8c05b 100644
--- a/app/assets/stylesheets/mobile/compose.scss
+++ b/app/assets/stylesheets/mobile/compose.scss
@@ -21,6 +21,14 @@
     height: 250px;
     &.edit-title {
       height: 100%;
+      height: calc(var(--composer-vh, 1vh) * 100);
+    }
+  }
+
+  html.keyboard-visible &.open {
+    height: calc(var(--composer-vh, 1vh) * 100);
+    .reply-area {
+      padding-bottom: 0px;
     }
   }

GitHub sha: 444d123f

1 Like

Any reason why not if (window.visualViewport) in this case?

Not really… it just seemed more explicit with !== undefined.

1 Like

It doesn’t really matter, really. It’s a few extra characters. I don’t feel strongly about it.

1 Like