From c3a5a8e0957f6c9c595b8e1609e397e2bac650ee Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Tue, 8 Oct 2019 11:16:41 -0400 Subject: [PATCH] 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 --- .../discourse/components/composer-body.js.es6 | 5 - .../discourse/lib/safari-hacks.js.es6 | 155 +++++++++--------- .../stylesheets/common/base/compose.scss | 16 ++ app/assets/stylesheets/desktop/compose.scss | 10 -- app/assets/stylesheets/mobile/compose.scss | 2 +- 5 files changed, 90 insertions(+), 98 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6 index 4a2fa237e32..301e22452ff 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 c0c76750cf1..27865f5c349 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 - - $("#main-outlet").hide(); - $("header").hide(); - - $(window).scrollTop(0); - - let i = 20; - let interval = setInterval(() => { - $(window).scrollTop(0); - if (i-- === 0) { - clearInterval(interval); + 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; + } } - }, 10); - fixedElement.style.top = "0px"; + if (fixedElement.style.top === "0px") { + if (this !== document.activeElement) { + evt.preventDefault(); - if (!iOSWithVisualViewport()) { - const height = calcHeight(); - fixedElement.style.height = height + "px"; - $(fixedElement).addClass("no-transition"); + // 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) + 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) { @@ -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; - } + attachTouchStart(fixedElement, lastTouched); - 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() { attachTouchStart(this, positioningHack); }); diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 2a4cab76329..ff516a3c3a3 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 b047169df79..7f4db285c2e 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -230,16 +230,6 @@ a.toggle-preview { 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 { overflow: hidden; diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index ba7199fdbc2..2f25202bbc6 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -24,7 +24,7 @@ } } - html.keyboard-visible &.open { + body.ios-safari-composer-hacks &.open { height: calc(var(--composer-vh, 1vh) * 100); .reply-area { padding-bottom: 0px;