UX: Set focus when launching composer on iOS (#9443)
This commit is contained in:
parent
5a60a4233e
commit
6fad04635b
|
@ -54,6 +54,7 @@
|
||||||
//= require ./discourse/lib/autocomplete
|
//= require ./discourse/lib/autocomplete
|
||||||
//= require ./discourse/lib/after-transition
|
//= require ./discourse/lib/after-transition
|
||||||
//= require ./discourse/lib/safari-hacks
|
//= require ./discourse/lib/safari-hacks
|
||||||
|
//= require ./discourse/lib/put-cursor-at-end
|
||||||
//= require_tree ./discourse/adapters
|
//= require_tree ./discourse/adapters
|
||||||
//= require ./discourse/models/post-action-type
|
//= require ./discourse/models/post-action-type
|
||||||
//= require ./discourse/models/post
|
//= require ./discourse/models/post
|
||||||
|
|
|
@ -150,28 +150,27 @@ export default Component.extend(KeyEnterEscape, {
|
||||||
},
|
},
|
||||||
|
|
||||||
viewportResize() {
|
viewportResize() {
|
||||||
const composerVH = window.visualViewport.height * 0.01;
|
const composerVH = window.visualViewport.height * 0.01,
|
||||||
|
doc = document.documentElement;
|
||||||
|
|
||||||
document.documentElement.style.setProperty(
|
doc.style.setProperty("--composer-vh", `${composerVH}px`);
|
||||||
"--composer-vh",
|
|
||||||
`${composerVH}px`
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewportWindowDiff =
|
const viewportWindowDiff =
|
||||||
window.innerHeight - window.visualViewport.height;
|
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
|
// 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
|
// 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",
|
"--composer-ipad-padding",
|
||||||
`${viewportWindowDiff}px`
|
`${viewportWindowDiff}px`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.style.setProperty(
|
doc.style.setProperty("--composer-ipad-padding", "0px");
|
||||||
"--composer-ipad-padding",
|
|
||||||
"0px"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -28,11 +28,10 @@ import {
|
||||||
tinyAvatar,
|
tinyAvatar,
|
||||||
formatUsername,
|
formatUsername,
|
||||||
clipboardData,
|
clipboardData,
|
||||||
safariHacksDisabled,
|
|
||||||
caretPosition,
|
caretPosition,
|
||||||
inCodeBlock,
|
inCodeBlock
|
||||||
putCursorAtEnd
|
|
||||||
} from "discourse/lib/utilities";
|
} from "discourse/lib/utilities";
|
||||||
|
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
||||||
import {
|
import {
|
||||||
validateUploadedFiles,
|
validateUploadedFiles,
|
||||||
authorizesOneOrMoreImageExtensions,
|
authorizesOneOrMoreImageExtensions,
|
||||||
|
@ -210,10 +209,7 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus on the body unless we have a title
|
// Focus on the body unless we have a title
|
||||||
if (
|
if (!this.get("composer.canEditTitle")) {
|
||||||
!this.get("composer.canEditTitle") &&
|
|
||||||
(!this.capabilities.isIOS || safariHacksDisabled())
|
|
||||||
) {
|
|
||||||
putCursorAtEnd(this.element.querySelector(".d-editor-input"));
|
putCursorAtEnd(this.element.querySelector(".d-editor-input"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { lookupCache } from "pretty-text/oneboxer-cache";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import ENV from "discourse-common/config/environment";
|
import ENV from "discourse-common/config/environment";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { putCursorAtEnd } from "discourse/lib/utilities";
|
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
classNames: ["title-input"],
|
classNames: ["title-input"],
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
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({
|
export default Component.extend({
|
||||||
showSelector: true,
|
showSelector: true,
|
||||||
|
|
|
@ -13,7 +13,7 @@ import discourseComputed, {
|
||||||
on
|
on
|
||||||
} from "discourse-common/utils/decorators";
|
} from "discourse-common/utils/decorators";
|
||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
import { escapeExpression, safariHacksDisabled } from "discourse/lib/utilities";
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
import {
|
import {
|
||||||
authorizesOneOrMoreExtensions,
|
authorizesOneOrMoreExtensions,
|
||||||
uploadIcon
|
uploadIcon
|
||||||
|
@ -136,10 +136,6 @@ export default Controller.extend({
|
||||||
"model.composeState"
|
"model.composeState"
|
||||||
)
|
)
|
||||||
focusTarget(replyingToTopic, creatingPM, usernames, 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
|
// Focus on usernames if it's blank or if it's just you
|
||||||
usernames = usernames || "";
|
usernames = usernames || "";
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -84,6 +84,12 @@ function positioningWorkaround($fixedElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener("scroll", () => {
|
||||||
|
if (!caps.isIpadOS && workaroundActive) {
|
||||||
|
document.documentElement.scrollTop = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const fixedElement = $fixedElement[0];
|
const fixedElement = $fixedElement[0];
|
||||||
const oldHeight = fixedElement.style.height;
|
const oldHeight = fixedElement.style.height;
|
||||||
|
|
||||||
|
@ -94,17 +100,17 @@ function positioningWorkaround($fixedElement) {
|
||||||
if (workaroundActive) {
|
if (workaroundActive) {
|
||||||
$("body").removeClass("ios-safari-composer-hacks");
|
$("body").removeClass("ios-safari-composer-hacks");
|
||||||
|
|
||||||
|
$(window).scrollTop(originalScrollTop);
|
||||||
|
if (evt && evt.target) {
|
||||||
|
evt.target.removeEventListener("blur", blurred);
|
||||||
|
}
|
||||||
|
|
||||||
|
workaroundActive = false;
|
||||||
|
|
||||||
if (!iOSWithVisualViewport()) {
|
if (!iOSWithVisualViewport()) {
|
||||||
fixedElement.style.height = oldHeight;
|
fixedElement.style.height = oldHeight;
|
||||||
later(() => $(fixedElement).removeClass("no-transition"), 500);
|
later(() => $(fixedElement).removeClass("no-transition"), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(window).scrollTop(originalScrollTop);
|
|
||||||
|
|
||||||
if (evt) {
|
|
||||||
evt.target.removeEventListener("blur", blurred);
|
|
||||||
}
|
|
||||||
workaroundActive = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -113,8 +119,9 @@ function positioningWorkaround($fixedElement) {
|
||||||
// document.activeElement is also unreliable (iOS does not mark buttons as focused)
|
// document.activeElement is also unreliable (iOS does not mark buttons as focused)
|
||||||
// so instead, we store the last touched element and check against it
|
// 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
|
// - switching to another iOS app
|
||||||
|
// - displaying title field
|
||||||
// - invoking a select-kit dropdown
|
// - invoking a select-kit dropdown
|
||||||
// - invoking mentions
|
// - invoking mentions
|
||||||
// - invoking emoji dropdown via : and hitting return
|
// - invoking emoji dropdown via : and hitting return
|
||||||
|
@ -123,6 +130,7 @@ function positioningWorkaround($fixedElement) {
|
||||||
if (
|
if (
|
||||||
lastTouchedElement &&
|
lastTouchedElement &&
|
||||||
(document.visibilityState === "hidden" ||
|
(document.visibilityState === "hidden" ||
|
||||||
|
$fixedElement.hasClass("edit-title") ||
|
||||||
$(lastTouchedElement).hasClass("select-kit-header") ||
|
$(lastTouchedElement).hasClass("select-kit-header") ||
|
||||||
$(lastTouchedElement).closest(".autocomplete").length ||
|
$(lastTouchedElement).closest(".autocomplete").length ||
|
||||||
(lastTouchedElement.nodeName.toLowerCase() === "textarea" &&
|
(lastTouchedElement.nodeName.toLowerCase() === "textarea" &&
|
||||||
|
@ -140,6 +148,12 @@ function positioningWorkaround($fixedElement) {
|
||||||
var blurred = discourseDebounce(blurredNow, INPUT_DELAY);
|
var blurred = discourseDebounce(blurredNow, INPUT_DELAY);
|
||||||
|
|
||||||
var positioningHack = function(evt) {
|
var positioningHack = function(evt) {
|
||||||
|
let _this = this;
|
||||||
|
|
||||||
|
if (evt === undefined) {
|
||||||
|
evt = new CustomEvent("no-op");
|
||||||
|
}
|
||||||
|
|
||||||
// we need this, otherwise changing focus means we never clear
|
// we need this, otherwise changing focus means we never clear
|
||||||
this.addEventListener("blur", blurred);
|
this.addEventListener("blur", blurred);
|
||||||
|
|
||||||
|
@ -150,12 +164,22 @@ function positioningWorkaround($fixedElement) {
|
||||||
.find(".select-kit > button.is-focused")
|
.find(".select-kit > button.is-focused")
|
||||||
.removeClass("is-focused");
|
.removeClass("is-focused");
|
||||||
|
|
||||||
if ($(window).scrollTop() > 0) {
|
|
||||||
originalScrollTop = $(window).scrollTop();
|
originalScrollTop = $(window).scrollTop();
|
||||||
|
|
||||||
|
const elementRect = _this.getBoundingClientRect();
|
||||||
|
if (elementRect.top > 100) {
|
||||||
|
// this tricks iOS safari into assuming input/textarea 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(-400px)";
|
||||||
|
setTimeout(function() {
|
||||||
|
_this.style.transform = "none";
|
||||||
|
}, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let delay = caps.isIpadOS ? 350 : 150;
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
if (iOSWithVisualViewport()) {
|
if (caps.isIpadOS && iOSWithVisualViewport()) {
|
||||||
// disable hacks when using a hardware keyboard
|
// disable hacks when using a hardware keyboard
|
||||||
// by default, a hardware keyboard will show the keyboard accessory bar
|
// by default, a hardware keyboard will show the keyboard accessory bar
|
||||||
// whose height is currently 55px (using 75 for a bit of a buffer)
|
// whose height is currently 55px (using 75 for a bit of a buffer)
|
||||||
|
@ -165,38 +189,14 @@ function positioningWorkaround($fixedElement) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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";
|
|
||||||
}, 30);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't trigger keyboard on disabled element (happens when a category is required)
|
// don't trigger keyboard on disabled element (happens when a category is required)
|
||||||
if (this.disabled) {
|
if (_this.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$("body").addClass("ios-safari-composer-hacks");
|
$("body").addClass("ios-safari-composer-hacks");
|
||||||
$(window).scrollTop(0);
|
$(window).scrollTop(0);
|
||||||
|
|
||||||
let i = 20;
|
|
||||||
let interval = setInterval(() => {
|
|
||||||
$(window).scrollTop(0);
|
|
||||||
if (i-- === 0) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
if (!iOSWithVisualViewport()) {
|
if (!iOSWithVisualViewport()) {
|
||||||
const height = calcHeight();
|
const height = calcHeight();
|
||||||
fixedElement.style.height = height + "px";
|
fixedElement.style.height = height + "px";
|
||||||
|
@ -204,9 +204,9 @@ function positioningWorkaround($fixedElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.focus();
|
_this.focus();
|
||||||
workaroundActive = true;
|
workaroundActive = true;
|
||||||
}, 350);
|
}, delay);
|
||||||
};
|
};
|
||||||
|
|
||||||
var lastTouched = function(evt) {
|
var lastTouched = function(evt) {
|
||||||
|
@ -230,6 +230,11 @@ function positioningWorkaround($fixedElement) {
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
positioningWorkaround.touchstartEvent = function(element) {
|
||||||
|
var triggerHack = positioningHack.bind(element);
|
||||||
|
triggerHack();
|
||||||
|
};
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
|
|
|
@ -444,11 +444,5 @@ export function inCodeBlock(text, pos) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function putCursorAtEnd(element) {
|
|
||||||
element.focus();
|
|
||||||
const len = element.value.length;
|
|
||||||
element.setSelectionRange(len, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This prevents a mini racer crash
|
// This prevents a mini racer crash
|
||||||
export default {};
|
export default {};
|
||||||
|
|
|
@ -26,9 +26,10 @@
|
||||||
|
|
||||||
body.ios-safari-composer-hacks &.open {
|
body.ios-safari-composer-hacks &.open {
|
||||||
height: calc(var(--composer-vh, 1vh) * 100);
|
height: calc(var(--composer-vh, 1vh) * 100);
|
||||||
.reply-area {
|
|
||||||
padding-bottom: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyboard-visible body.ios-safari-composer-hacks &.open .reply-area {
|
||||||
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-to {
|
.reply-to {
|
||||||
|
|
Loading…
Reference in New Issue