Manually adapt UI when keyboard shows or hides on mobile Safari
Mobile Safari seems to be the only browser that does *not* resize the viewport when the keyboard shows and hides. Instead the window is moved to make room for the keyboard which moves content at the top off screen. This uses the VisualViewport API to manually resize the `SessionView` in response to keyboard display events. Additionally, if a DOM element exists that has the `bottom-aligned-scroll` CSS class, its scroll position is retained. Currently this only applies to the `Timeline`. Note that the VisualViewport API was only introduced with iOS 13. According to [statista.com], versions below 13 made up for 19% of all iOS users in summer 2020, with the share continuing to fall off. As a result, this seems like an acceptable workaround. Fixes: #181 [statista.com]: https://www.statista.com/statistics/565270/apple-devices-ios-version-share-worldwide/ Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
This commit is contained in:
parent
f4bb95f459
commit
14ed5fd1e8
5 changed files with 69 additions and 7 deletions
|
@ -351,7 +351,11 @@ async function buildCssLegacy(entryPath, urlMapper = null) {
|
||||||
const preCss = await fs.readFile(entryPath, "utf8");
|
const preCss = await fs.readFile(entryPath, "utf8");
|
||||||
const options = [
|
const options = [
|
||||||
postcssImport,
|
postcssImport,
|
||||||
cssvariables(),
|
cssvariables({
|
||||||
|
preserve: (declaration) => {
|
||||||
|
return declaration.value.indexOf("var(--ios-") == 0;
|
||||||
|
}
|
||||||
|
}),
|
||||||
autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}),
|
autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}),
|
||||||
flexbugsFixes()
|
flexbugsFixes()
|
||||||
];
|
];
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {WorkerPool} from "./dom/WorkerPool.js";
|
||||||
import {BlobHandle} from "./dom/BlobHandle.js";
|
import {BlobHandle} from "./dom/BlobHandle.js";
|
||||||
import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js";
|
import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js";
|
||||||
import {downloadInIframe} from "./dom/download.js";
|
import {downloadInIframe} from "./dom/download.js";
|
||||||
|
import {Disposables} from "../../utils/Disposables.js";
|
||||||
|
|
||||||
function addScript(src) {
|
function addScript(src) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
|
@ -83,6 +84,44 @@ async function loadOlmWorker(config) {
|
||||||
return olmWorker;
|
return olmWorker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// needed for mobile Safari which shifts the layout viewport up without resizing it
|
||||||
|
// when the keyboard shows (see https://bugs.webkit.org/show_bug.cgi?id=141832)
|
||||||
|
function adaptUIOnVisualViewportResize(container) {
|
||||||
|
if (!window.visualViewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handler = () => {
|
||||||
|
const sessionView = container.querySelector('.SessionView');
|
||||||
|
if (!sessionView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollable = container.querySelector('.bottom-aligned-scroll');
|
||||||
|
let scrollTopBefore, heightBefore, heightAfter;
|
||||||
|
|
||||||
|
if (scrollable) {
|
||||||
|
scrollTopBefore = scrollable.scrollTop;
|
||||||
|
heightBefore = scrollable.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ideally we'd use window.visualViewport.offsetTop but that seems to occasionally lag
|
||||||
|
// behind (last tested on iOS 14.4 simulator) so we have to compute the offset manually
|
||||||
|
const offsetTop = sessionView.offsetTop + sessionView.offsetHeight - window.visualViewport.height;
|
||||||
|
|
||||||
|
container.style.setProperty('--ios-viewport-height', window.visualViewport.height.toString() + 'px');
|
||||||
|
container.style.setProperty('--ios-viewport-top', offsetTop.toString() + 'px');
|
||||||
|
|
||||||
|
if (scrollable) {
|
||||||
|
heightAfter = scrollable.offsetHeight;
|
||||||
|
scrollable.scrollTop = scrollTopBefore + heightBefore - heightAfter;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.visualViewport.addEventListener('resize', handler);
|
||||||
|
return () => {
|
||||||
|
window.visualViewport.removeEventListener('resize', handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class Platform {
|
export class Platform {
|
||||||
constructor(container, config, cryptoExtras = null, options = null) {
|
constructor(container, config, cryptoExtras = null, options = null) {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
|
@ -115,6 +154,10 @@ export class Platform {
|
||||||
}
|
}
|
||||||
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
|
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
|
||||||
this.isIE11 = isIE11;
|
this.isIE11 = isIE11;
|
||||||
|
// From https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885
|
||||||
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) && !window.MSStream;
|
||||||
|
this.isIOS = isIOS;
|
||||||
|
this._disposables = new Disposables();
|
||||||
}
|
}
|
||||||
|
|
||||||
get updateService() {
|
get updateService() {
|
||||||
|
@ -135,6 +178,13 @@ export class Platform {
|
||||||
if (this.isIE11) {
|
if (this.isIE11) {
|
||||||
this._container.className += " legacy";
|
this._container.className += " legacy";
|
||||||
}
|
}
|
||||||
|
if (this.isIOS) {
|
||||||
|
this._container.className += " ios";
|
||||||
|
const disposable = adaptUIOnVisualViewportResize(this._container);
|
||||||
|
if (disposable) {
|
||||||
|
this._disposables.track(disposable);
|
||||||
|
}
|
||||||
|
}
|
||||||
window.__hydrogenViewModel = vm;
|
window.__hydrogenViewModel = vm;
|
||||||
const view = new RootView(vm);
|
const view = new RootView(vm);
|
||||||
this._container.appendChild(view.mount());
|
this._container.appendChild(view.mount());
|
||||||
|
@ -152,7 +202,7 @@ export class Platform {
|
||||||
if (navigator.msSaveBlob) {
|
if (navigator.msSaveBlob) {
|
||||||
navigator.msSaveBlob(blobHandle.nativeBlob, filename);
|
navigator.msSaveBlob(blobHandle.nativeBlob, filename);
|
||||||
} else {
|
} else {
|
||||||
downloadInIframe(this._container, this._config.downloadSandbox, blobHandle, filename);
|
downloadInIframe(this._container, this._config.downloadSandbox, blobHandle, filename, this.isIOS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,4 +251,8 @@ export class Platform {
|
||||||
get version() {
|
get version() {
|
||||||
return window.HYDROGEN_VERSION;
|
return window.HYDROGEN_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this._disposables.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// From https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885
|
export async function downloadInIframe(container, iframeSrc, blobHandle, filename, isIOS) {
|
||||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) && !window.MSStream;
|
|
||||||
|
|
||||||
export async function downloadInIframe(container, iframeSrc, blobHandle, filename) {
|
|
||||||
let iframe = container.querySelector("iframe.downloadSandbox");
|
let iframe = container.querySelector("iframe.downloadSandbox");
|
||||||
if (!iframe) {
|
if (!iframe) {
|
||||||
iframe = document.createElement("iframe");
|
iframe = document.createElement("iframe");
|
||||||
|
|
|
@ -54,6 +54,13 @@ main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* resize and reposition session view to account for mobile Safari which shifts
|
||||||
|
the layout viewport up without resizing it when the keyboard shows */
|
||||||
|
.hydrogen.ios .SessionView {
|
||||||
|
height: var(--ios-viewport-height, 100%);
|
||||||
|
top: var(--ios-viewport-top, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/* hide back button in middle section by default */
|
/* hide back button in middle section by default */
|
||||||
.middle .close-middle { display: none; }
|
.middle .close-middle { display: none; }
|
||||||
/* mobile layout */
|
/* mobile layout */
|
||||||
|
|
|
@ -40,7 +40,7 @@ function viewClassForEntry(entry) {
|
||||||
export class TimelineList extends ListView {
|
export class TimelineList extends ListView {
|
||||||
constructor(viewModel) {
|
constructor(viewModel) {
|
||||||
const options = {
|
const options = {
|
||||||
className: "Timeline",
|
className: "Timeline bottom-aligned-scroll",
|
||||||
list: viewModel.tiles,
|
list: viewModel.tiles,
|
||||||
}
|
}
|
||||||
super(options, entry => {
|
super(options, entry => {
|
||||||
|
|
Reference in a new issue