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 options = [
|
||||
postcssImport,
|
||||
cssvariables(),
|
||||
cssvariables({
|
||||
preserve: (declaration) => {
|
||||
return declaration.value.indexOf("var(--ios-") == 0;
|
||||
}
|
||||
}),
|
||||
autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}),
|
||||
flexbugsFixes()
|
||||
];
|
||||
|
|
|
@ -35,6 +35,7 @@ import {WorkerPool} from "./dom/WorkerPool.js";
|
|||
import {BlobHandle} from "./dom/BlobHandle.js";
|
||||
import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js";
|
||||
import {downloadInIframe} from "./dom/download.js";
|
||||
import {Disposables} from "../../utils/Disposables.js";
|
||||
|
||||
function addScript(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
|
@ -83,6 +84,44 @@ async function loadOlmWorker(config) {
|
|||
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 {
|
||||
constructor(container, config, cryptoExtras = null, options = null) {
|
||||
this._config = config;
|
||||
|
@ -115,6 +154,10 @@ export class Platform {
|
|||
}
|
||||
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
|
||||
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() {
|
||||
|
@ -135,6 +178,13 @@ export class Platform {
|
|||
if (this.isIE11) {
|
||||
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;
|
||||
const view = new RootView(vm);
|
||||
this._container.appendChild(view.mount());
|
||||
|
@ -152,7 +202,7 @@ export class Platform {
|
|||
if (navigator.msSaveBlob) {
|
||||
navigator.msSaveBlob(blobHandle.nativeBlob, filename);
|
||||
} 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() {
|
||||
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.
|
||||
*/
|
||||
|
||||
// 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;
|
||||
|
||||
export async function downloadInIframe(container, iframeSrc, blobHandle, filename) {
|
||||
export async function downloadInIframe(container, iframeSrc, blobHandle, filename, isIOS) {
|
||||
let iframe = container.querySelector("iframe.downloadSandbox");
|
||||
if (!iframe) {
|
||||
iframe = document.createElement("iframe");
|
||||
|
|
|
@ -54,6 +54,13 @@ main {
|
|||
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 */
|
||||
.middle .close-middle { display: none; }
|
||||
/* mobile layout */
|
||||
|
|
|
@ -40,7 +40,7 @@ function viewClassForEntry(entry) {
|
|||
export class TimelineList extends ListView {
|
||||
constructor(viewModel) {
|
||||
const options = {
|
||||
className: "Timeline",
|
||||
className: "Timeline bottom-aligned-scroll",
|
||||
list: viewModel.tiles,
|
||||
}
|
||||
super(options, entry => {
|
||||
|
|
Reference in a new issue