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
This commit is contained in:
Penar Musaraj 2019-10-08 11:16:41 -04:00
parent d527f3a723
commit c3a5a8e095
5 changed files with 90 additions and 98 deletions

View File

@ -142,11 +142,6 @@ export default Ember.Component.extend(KeyEnterEscape, {
viewportResize() { viewportResize() {
const composerVH = window.visualViewport.height * 0.01; 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( document.documentElement.style.setProperty(
"--composer-vh", "--composer-vh",
`${composerVH}px` `${composerVH}px`

View File

@ -85,18 +85,12 @@ function positioningWorkaround($fixedElement) {
const fixedElement = $fixedElement[0]; const fixedElement = $fixedElement[0];
const oldHeight = fixedElement.style.height; const oldHeight = fixedElement.style.height;
var done = false;
var originalScrollTop = 0; var originalScrollTop = 0;
let lastTouchedElement = null;
positioningWorkaround.blur = function(evt) { positioningWorkaround.blur = function(evt) {
if (workaroundActive) { if (workaroundActive) {
done = true; $("body").removeClass("ios-safari-composer-hacks");
$("#main-outlet").show();
$("header").show();
fixedElement.style.position = "";
fixedElement.style.top = "";
if (!iOSWithVisualViewport()) { if (!iOSWithVisualViewport()) {
fixedElement.style.height = oldHeight; fixedElement.style.height = oldHeight;
@ -116,15 +110,17 @@ function positioningWorkaround($fixedElement) {
}; };
var blurredNow = function(evt) { 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 ( if (
!done && lastTouchedElement &&
evt.srcElement !== document.activeElement && ($(lastTouchedElement).hasClass("select-kit-header") ||
$(document.activeElement) ["span", "svg", "button"].includes(
.parents() lastTouchedElement.nodeName.toLowerCase()
.toArray() ))
.indexOf(fixedElement) > -1
) { ) {
// something in focus so skip
return; return;
} }
@ -134,60 +130,77 @@ function positioningWorkaround($fixedElement) {
var blurred = debounce(blurredNow, 250); var blurred = debounce(blurredNow, 250);
var positioningHack = function(evt) { var positioningHack = function(evt) {
done = false;
// 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);
if (fixedElement.style.top === "0px") { // resets focus out of select-kit elements
if (this !== document.activeElement) { // might become redundant after select-kit refactoring
evt.preventDefault(); $fixedElement.find(".select-kit.is-expanded > button").trigger("click");
$fixedElement
// this tricks safari into assuming current input is at top of the viewport .find(".select-kit > button.is-focused")
// via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input .removeClass("is-focused");
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;
}
originalScrollTop = $(window).scrollTop(); originalScrollTop = $(window).scrollTop();
// take care of body setTimeout(function() {
if (iOSWithVisualViewport()) {
$("#main-outlet").hide(); // disable hacks when using a hardware keyboard
$("header").hide(); // by default, a hardware keyboard will show the keyboard accessory bar
// whose height is currently 55px (using 75 for a bit of a buffer)
$(window).scrollTop(0); let heightDiff = window.innerHeight - window.visualViewport.height;
if (heightDiff < 75) {
let i = 20; return;
let interval = setInterval(() => { }
$(window).scrollTop(0);
if (i-- === 0) {
clearInterval(interval);
} }
}, 10);
fixedElement.style.top = "0px"; if (fixedElement.style.top === "0px") {
if (this !== document.activeElement) {
evt.preventDefault();
if (!iOSWithVisualViewport()) { // this tricks safari into assuming current input is at top of the viewport
const height = calcHeight(); // via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input
fixedElement.style.height = height + "px"; this.style.transform = "translateY(-200px)";
$(fixedElement).addClass("no-transition"); 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)
if (this.disabled) {
return;
}
$("body").addClass("ios-safari-composer-hacks");
$(window).scrollTop(0);
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");
}
evt.preventDefault();
this.focus();
workaroundActive = true;
}, 350);
};
var lastTouched = function(evt) {
if (evt && evt.target) {
lastTouchedElement = evt.target;
} }
evt.preventDefault();
this.focus();
workaroundActive = true;
}; };
function attachTouchStart(elem, fn) { function attachTouchStart(elem, fn) {
@ -198,30 +211,8 @@ function positioningWorkaround($fixedElement) {
} }
const checkForInputs = debounce(function() { const checkForInputs = debounce(function() {
$fixedElement attachTouchStart(fixedElement, lastTouched);
.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;
}
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() { $fixedElement.find("input[type=text],textarea").each(function() {
attachTouchStart(this, positioningHack); attachTouchStart(this, positioningHack);
}); });

View File

@ -483,3 +483,19 @@ div.ac-wrap {
opacity: 0; 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);
}
}
}

View File

@ -230,16 +230,6 @@ a.toggle-preview {
text-align: right; text-align: right;
} }
html.keyboard-visible {
.grippie,
&:not(.fullscreen-composer) .toggle-fullscreen {
display: none;
}
#reply-control.open {
height: calc(var(--composer-vh, 1vh) * 100);
}
}
// fullscreen composer styles // fullscreen composer styles
.fullscreen-composer { .fullscreen-composer {
overflow: hidden; overflow: hidden;

View File

@ -24,7 +24,7 @@
} }
} }
html.keyboard-visible &.open { body.ios-safari-composer-hacks &.open {
height: calc(var(--composer-vh, 1vh) * 100); height: calc(var(--composer-vh, 1vh) * 100);
.reply-area { .reply-area {
padding-bottom: 0px; padding-bottom: 0px;