UX: Set focus when launching composer on iOS (#9443)

UX: Set focus when launching composer on iOS (#9443)

diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 9addddf..6a6cc8a 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -54,6 +54,7 @@
 //= require ./discourse/lib/autocomplete
 //= require ./discourse/lib/after-transition
 //= require ./discourse/lib/safari-hacks
+//= require ./discourse/lib/put-cursor-at-end
 //= require_tree ./discourse/adapters
 //= require ./discourse/models/post-action-type
 //= require ./discourse/models/post
diff --git a/app/assets/javascripts/discourse/components/composer-body.js b/app/assets/javascripts/discourse/components/composer-body.js
index e5a6e82..8379280 100644
--- a/app/assets/javascripts/discourse/components/composer-body.js
+++ b/app/assets/javascripts/discourse/components/composer-body.js
@@ -150,28 +150,27 @@ export default Component.extend(KeyEnterEscape, {
   },
 
   viewportResize() {
-    const composerVH = window.visualViewport.height * 0.01;
+    const composerVH = window.visualViewport.height * 0.01,
+      doc = document.documentElement;
 
-    document.documentElement.style.setProperty(
-      "--composer-vh",
-      `${composerVH}px`
-    );
+    doc.style.setProperty("--composer-vh", `${composerVH}px`);
 
     const viewportWindowDiff =
       window.innerHeight - window.visualViewport.height;
 
+    viewportWindowDiff
+      ? doc.classList.add("keyboard-visible")
+      : doc.classList.remove("keyboard-visible");
     // adds bottom padding when using a hardware keyboard and the accessory bar is visible
     // accessory bar height is 55px, using 75 allows a small buffer
-    if (viewportWindowDiff > 0 && viewportWindowDiff < 75) {
-      document.documentElement.style.setProperty(
+
+    if (viewportWindowDiff < 75) {
+      doc.style.setProperty(
         "--composer-ipad-padding",
         `${viewportWindowDiff}px`
       );
     } else {
-      document.documentElement.style.setProperty(
-        "--composer-ipad-padding",
-        "0px"
-      );
+      doc.style.setProperty("--composer-ipad-padding", "0px");
     }
   },
 
diff --git a/app/assets/javascripts/discourse/components/composer-editor.js b/app/assets/javascripts/discourse/components/composer-editor.js
index e6e000a..ad34e96 100644
--- a/app/assets/javascripts/discourse/components/composer-editor.js
+++ b/app/assets/javascripts/discourse/components/composer-editor.js
@@ -28,11 +28,10 @@ import {
   tinyAvatar,
   formatUsername,
   clipboardData,
-  safariHacksDisabled,
   caretPosition,
-  inCodeBlock,
-  putCursorAtEnd
+  inCodeBlock
 } from "discourse/lib/utilities";
+import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
 import {
   validateUploadedFiles,
   authorizesOneOrMoreImageExtensions,
@@ -210,10 +209,7 @@ export default Component.extend({
     }
 
     // Focus on the body unless we have a title
-    if (
-      !this.get("composer.canEditTitle") &&
-      (!this.capabilities.isIOS || safariHacksDisabled())
-    ) {
+    if (!this.get("composer.canEditTitle")) {
       putCursorAtEnd(this.element.querySelector(".d-editor-input"));
     }
 
diff --git a/app/assets/javascripts/discourse/components/composer-title.js b/app/assets/javascripts/discourse/components/composer-title.js
index e036bee..b4cb3ef 100644
--- a/app/assets/javascripts/discourse/components/composer-title.js
+++ b/app/assets/javascripts/discourse/components/composer-title.js
@@ -7,7 +7,7 @@ import { lookupCache } from "pretty-text/oneboxer-cache";
 import { ajax } from "discourse/lib/ajax";
 import ENV from "discourse-common/config/environment";
 import EmberObject from "@ember/object";
-import { putCursorAtEnd } from "discourse/lib/utilities";
+import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
 
 export default Component.extend({
   classNames: ["title-input"],
diff --git a/app/assets/javascripts/discourse/components/composer-user-selector.js b/app/assets/javascripts/discourse/components/composer-user-selector.js
index 3418697..efbc3aa 100644
--- a/app/assets/javascripts/discourse/components/composer-user-selector.js
+++ b/app/assets/javascripts/discourse/components/composer-user-selector.js
@@ -1,7 +1,7 @@
 import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import discourseComputed, { observes } from "discourse-common/utils/decorators";
-import { putCursorAtEnd } from "discourse/lib/utilities";
+import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
 
 export default Component.extend({
   showSelector: true,
diff --git a/app/assets/javascripts/discourse/controllers/composer.js b/app/assets/javascripts/discourse/controllers/composer.js
index 1706c82..b7855d5 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js
+++ b/app/assets/javascripts/discourse/controllers/composer.js
@@ -13,7 +13,7 @@ import discourseComputed, {
   on
 } from "discourse-common/utils/decorators";
 import { getOwner } from "discourse-common/lib/get-owner";
-import { escapeExpression, safariHacksDisabled } from "discourse/lib/utilities";
+import { escapeExpression } from "discourse/lib/utilities";
 import {
   authorizesOneOrMoreExtensions,
   uploadIcon
@@ -136,10 +136,6 @@ export default Controller.extend({
     "model.composeState"
   )
   focusTarget(replyingToTopic, creatingPM, usernames, composeState) {
-    if (this.capabilities.isIOS && !safariHacksDisabled()) {
-      return "none";
-    }
-
     // Focus on usernames if it's blank or if it's just you
     usernames = usernames || "";
     if (
diff --git a/app/assets/javascripts/discourse/lib/put-cursor-at-end.js b/app/assets/javascripts/discourse/lib/put-cursor-at-end.js
new file mode 100644
index 0000000..eec28b3
--- /dev/null
+++ b/app/assets/javascripts/discourse/lib/put-cursor-at-end.js
@@ -0,0 +1,16 @@
+import positioningWorkaround from "discourse/lib/safari-hacks";
+import { isAppleDevice } from "discourse/lib/utilities";
+
+export default function(element) {
+  if (isAppleDevice() && positioningWorkaround.touchstartEvent) {
+    positioningWorkaround.touchstartEvent(element);
+  } else {
+    element.focus();
+  }
+
+  const len = element.value.length;
+  element.setSelectionRange(len, len);
+
+  // Scroll to the bottom, in case we're in a tall textarea
+  element.scrollTop = 999999;
+}
diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js b/app/assets/javascripts/discourse/lib/safari-hacks.js
index 40d8ee4..d18d06f 100644
--- a/app/assets/javascripts/discourse/lib/safari-hacks.js
+++ b/app/assets/javascripts/discourse/lib/safari-hacks.js
@@ -84,6 +84,12 @@ function positioningWorkaround($fixedElement) {
     return;
   }
 
+  document.addEventListener("scroll", () => {
+    if (!caps.isIpadOS && workaroundActive) {
+      document.documentElement.scrollTop = 0;
+    }
+  });
+
   const fixedElement = $fixedElement[0];
   const oldHeight = fixedElement.style.height;
 
@@ -94,17 +100,17 @@ function positioningWorkaround($fixedElement) {
     if (workaroundActive) {
       $("body").removeClass("ios-safari-composer-hacks");
 
-      if (!iOSWithVisualViewport()) {
-        fixedElement.style.height = oldHeight;
-        later(() => $(fixedElement).removeClass("no-transition"), 500);
-      }
-
       $(window).scrollTop(originalScrollTop);
-
-      if (evt) {
+      if (evt && evt.target) {
         evt.target.removeEventListener("blur", blurred);
       }
+
       workaroundActive = false;
+
+      if (!iOSWithVisualViewport()) {
+        fixedElement.style.height = oldHeight;
+        later(() => $(fixedElement).removeClass("no-transition"), 500);
+      }
     }
   };
 
@@ -113,8 +119,9 @@ function positioningWorkaround($fixedElement) {
     // document.activeElement is also unreliable (iOS does not mark buttons as focused)
     // so instead, we store the last touched element and check against it
 
-    // cancel blur event if user is:
+    // cancel blur event when:
     // - switching to another iOS app

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

GitHub sha: 6fad0463

This commit appears in #9443 which was merged by pmusaraj.

I know this is not your code, but as your are now familiar with this, do you think we could use Ember.run.later here instead?

same question about using later

We could try, but I’m not sure how it would help. The second hack, especially, the one with a 30 millisecond delay, needs to be as quick as possible to make the flickering as minimal as possible, using Ember.run.later would run counter to that.

Any updates here? We should create a new issue if there’s still work to be done. Otherwise I’d like to close it out.

1 Like

Just had a very quick chat with Joffrey, I’m going to follow up on this very soon.

1 Like