From 0ca6b2fac94fe3b82857cca197c0c660f9b09ad3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartosz=20Dziewo=C5=84ski?= <matma.rex@gmail.com>
Date: Tue, 19 Mar 2019 00:04:40 +0100
Subject: [PATCH] mobile.editor.ve: Bring the toolbar back into view after it
 scrolls out

On iOS Safari, when the keyboard is open, the editor toolbar could
previously be scrolled out of view, due to how the keyboard affects
the viewport (or rather how it doesn't).

Detect when this happens and bring it back in, with a similar slide-in
animation as when the editor loads. Technical restrictions prevent us
from really keeping it in view at all times, and I think this is the
best we can do (and it looks almost intentional).

Bug: T218414
Change-Id: I5eed360d4644815bc9829fbc6b0ffd79b205d10b
---
 .../ve.init.mw.MobileFrontendArticleTarget.js | 70 ++++++++++++++++++-
 1 file changed, 69 insertions(+), 1 deletion(-)

diff --git a/src/mobile.editor.ve/ve.init.mw.MobileFrontendArticleTarget.js b/src/mobile.editor.ve/ve.init.mw.MobileFrontendArticleTarget.js
index a8b59c133..51ea3f235 100644
--- a/src/mobile.editor.ve/ve.init.mw.MobileFrontendArticleTarget.js
+++ b/src/mobile.editor.ve/ve.init.mw.MobileFrontendArticleTarget.js
@@ -82,7 +82,75 @@ MobileFrontendArticleTarget.prototype.isToolbarOverSurface = function () {
  * @instance
  */
 MobileFrontendArticleTarget.prototype.onContainerScroll = function () {
-	// MF provides the toolbar so there is no need to float the toolbar
+	var surfaceView, isActiveWithKeyboard, $header, pos, scrollPos, $overlaySurface, twiddleBy;
+	// Editor may not have loaded yet, in which case `this.surface` is undefined
+	surfaceView = this.surface && this.surface.getView();
+	isActiveWithKeyboard = surfaceView && surfaceView.isFocused() && !surfaceView.deactivated;
+
+	$header = this.overlay.$el.find( '.overlay-header-container' );
+	$overlaySurface = this.$overlaySurface;
+
+	// On iOS Safari, when the keyboard is open, the layout viewport reported by the browser is not
+	// updated to match the real viewport reduced by the keyboard (diagram: T218414#5027607). On all
+	// modern non-iOS browsers the layout viewport is updated to match real viewport.
+	//
+	// This allows the fixed toolbar to be scrolled out of view, ignoring `position: fixed` (because
+	// it refers to the layout viewport).
+	//
+	// When this happens, bring it back in by scrolling down a bit and back up until the top of the
+	// fake viewport is aligned with the top of the real viewport.
+
+	clearTimeout( this.onContainerScrollTimer );
+	if ( !isActiveWithKeyboard ) {
+		return;
+	}
+	// Wait until after the scroll, because 'scroll' events are not emitted for every frame the
+	// browser paints, so the toolbar would lag behind in a very uncouth manner. Additionally,
+	// getBoundingClientRect returns incorrect values during scrolling, so make sure to calculate
+	// it only after the scrolling ends (https://openradar.appspot.com/radar?id=6668472289329152).
+	this.onContainerScrollTimer = setTimeout( function () {
+		pos = $header[0].getBoundingClientRect().top;
+
+		// Check if toolbar is offscreen. In a better world, this would reject all negative values
+		// (pos >= 0), but getBoundingClientRect often returns funny small fractional values after
+		// this function has done its job (which triggers another 'scroll' event) and before the
+		// user scrolled again. If we allowed it to run, it would trigger a hilarious loop! Toolbar
+		// being 1px offscreen is not a big deal anyway.
+		if ( pos >= -1 ) {
+			return;
+		}
+
+		// We don't know how much we have to scroll because we don't know how large the real
+		// viewport is, but it no larger than the layout viewport.
+		twiddleBy = window.innerHeight;
+		scrollPos = document.body.scrollTop;
+
+		// Scroll down and translate the surface by the same amount, otherwise the content at new
+		// scroll position visibly flashes.
+		$overlaySurface.css( 'transform', 'translateY( ' + twiddleBy + 'px )' );
+		document.body.scrollTop += twiddleBy;
+
+		// (Note that the scrolling we just did will naturally trigger another 'scroll' event,
+		// and run this handler again after 250ms. This is okay.)
+
+		// Prepate to animate toolbar sliding into view
+		$header.removeClass( 'toolbar-shown toolbar-shown-done' );
+		$header.css( 'transform', 'translateY( ' + Math.max( -48, pos ) + 'px )' );
+
+		// The scroll back up must be after a delay, otherwise no scrolling happens and the
+		// viewports are not aligned. requestAnimationFrame() seems to minimize weird flashes
+		// of white (although they still happen and I have no explanation for them).
+		requestAnimationFrame( function () {
+			// Scroll back up
+			$overlaySurface.css( 'transform', '' );
+			document.body.scrollTop = scrollPos;
+			// Animate toolbar sliding into view
+			$header.addClass( 'toolbar-shown' ).css( 'transform', '' );
+			setTimeout( function () {
+				$header.addClass( 'toolbar-shown-done' );
+			}, 250 );
+		} );
+	}, 250 );
 };
 
 /**
-- 
2.17.1.windows.2

