Use `resized-content` in meta tag to standardize behaviour

Previously we were we were setting `overlaysContent = true` for Chrome
only, which made it very hard for the same CSS to work well across all
three major browsers. With `interactive-widget=resizes-content`, there
sems to be better consistency, we can use bottom fixed-positioning in
Firefox and Chrome for Android and a CSS workaround for Safari.

See also https://developer.chrome.com/blog/viewport-resize-behavior
This commit is contained in:
Penar Musaraj 2024-12-27 13:10:28 -05:00
parent f2b4baff36
commit 5c702c8429
4 changed files with 43 additions and 94 deletions

View File

@ -8,19 +8,14 @@ export default class DComposerPosition extends Component {
constructor() {
super(...arguments);
if (!window.visualViewport) {
return;
}
const html = document.documentElement;
if (
html.classList.contains("ios-device") ||
html.classList.contains("mobile-device") ||
html.classList.contains("ipados-device")
) {
window.addEventListener("scroll", this._correctScrollPosition);
this._correctScrollPosition();
const editor = document.querySelector(".d-editor-input");
editor?.addEventListener("touchmove", this._textareaTouchMove);
}
@ -29,10 +24,6 @@ export default class DComposerPosition extends Component {
willDestroy() {
super.willDestroy(...arguments);
if (!window.visualViewport) {
return;
}
const html = document.documentElement;
if (
@ -69,7 +60,7 @@ export default class DComposerPosition extends Component {
_textareaTouchMove(event) {
// This is an alternative to locking up the body
// It stops scrolls from bubbling up to the body
// It stops scrolling in the given element from bubbling up to the body
// when the textarea does not have any content to scroll
if (event.target) {
const notScrollable =

View File

@ -5,13 +5,13 @@ import isZoomed from "discourse/lib/zoom-check";
import discourseDebounce from "discourse-common/lib/debounce";
import { bind } from "discourse-common/utils/decorators";
const FF_KEYBOARD_DETECT_THRESHOLD = 150;
export default class DVirtualHeight extends Component {
@service site;
@service capabilities;
@service appEvents;
MIN_THRESHOLD = 120;
constructor() {
super(...arguments);
@ -23,7 +23,6 @@ export default class DVirtualHeight extends Component {
return;
}
// TODO: Handle device rotation
this.windowInnerHeight = window.innerHeight;
scheduleOnce("afterRender", this, this.debouncedOnViewportResize);
@ -32,13 +31,6 @@ export default class DVirtualHeight extends Component {
"resize",
this.debouncedOnViewportResize
);
if ("virtualKeyboard" in navigator) {
navigator.virtualKeyboard.overlaysContent = true;
navigator.virtualKeyboard.addEventListener(
"geometrychange",
this.debouncedOnViewportResize
);
}
}
willDestroy() {
@ -50,13 +42,6 @@ export default class DVirtualHeight extends Component {
"resize",
this.debouncedOnViewportResize
);
if ("virtualKeyboard" in navigator) {
navigator.virtualKeyboard.overlaysContent = false;
navigator.virtualKeyboard.removeEventListener(
"geometrychange",
this.debouncedOnViewportResize
);
}
}
setVH() {
@ -64,15 +49,7 @@ export default class DVirtualHeight extends Component {
return;
}
let height;
if ("virtualKeyboard" in navigator) {
height =
window.visualViewport.height -
navigator.virtualKeyboard.boundingRect.height;
} else {
const activeWindow = window.visualViewport || window;
height = activeWindow?.height || window.innerHeight;
}
const height = Math.round(window.visualViewport.height);
if (this.previousHeight && Math.abs(this.previousHeight - height) <= 1) {
return false;
@ -84,8 +61,6 @@ export default class DVirtualHeight extends Component {
"--composer-vh",
`${height / 100}px`
);
document.documentElement.style.setProperty("--vvh", `${height}px`);
}
@bind
@ -104,26 +79,12 @@ export default class DVirtualHeight extends Component {
const docEl = document.documentElement;
let keyboardVisible = false;
if ("virtualKeyboard" in navigator) {
if (navigator.virtualKeyboard.boundingRect.height > 0) {
keyboardVisible = true;
}
} else if (this.capabilities.isFirefox && this.capabilities.isAndroid) {
if (
Math.abs(
this.windowInnerHeight -
Math.min(window.innerHeight, window.visualViewport.height)
) > FF_KEYBOARD_DETECT_THRESHOLD
) {
keyboardVisible = true;
}
} else {
let viewportWindowDiff =
this.windowInnerHeight - window.visualViewport.height;
const MIN_THRESHOLD = 20;
if (viewportWindowDiff > MIN_THRESHOLD) {
keyboardVisible = true;
}
let viewportWindowDiff =
this.windowInnerHeight - window.visualViewport.height;
if (viewportWindowDiff > this.MIN_THRESHOLD) {
keyboardVisible = true;
}
this.appEvents.trigger("keyboard-visibility-change", keyboardVisible);

View File

@ -1,14 +1,10 @@
html.composer-open.not-mobile-device {
html.composer-open {
#main-outlet {
padding-bottom: var(--composer-height);
transition: padding-bottom 250ms ease;
}
}
html.composer-open {
--d-min-composer-reply-height: 35vh;
}
#reply-control {
position: fixed;
display: flex;
@ -44,7 +40,7 @@ html.composer-open {
}
z-index: z("composer", "content");
transition: height 0.2s, max-width 0.2s, padding-bottom 0.2s, top 0.2s,
min-height 0.2s;
transform 0.2s, min-height 0.2s;
background-color: var(--secondary);
box-shadow: var(--shadow-composer);
@ -71,7 +67,6 @@ html.composer-open {
&.open {
box-sizing: border-box;
height: var(--composer-height);
min-height: var(--d-min-composer-reply-height);
max-height: calc(100vh - var(--header-offset, 4em));
padding-bottom: var(--composer-ipad-padding);
}
@ -632,28 +627,17 @@ div.ac-wrap {
}
}
// The composer on mobile is fixed-positioned, same as on desktop because
// we are using interactive-widget=resizes-content in the viewport meta tag
.ipados-device,
.mobile-device {
// Let's have the composer be top-anchored for mobile and iPad.
// Safari in iOS/iPad and Firefox/Android do not handle well a bottom:0 fixed-positioned element,
// especially while the software keyboard is visible.
#reply-control {
bottom: unset;
height: 0;
z-index: -1;
// these two properties below are equivalent to bottom: 0
top: calc(var(--composer-vh, 1vh) * 100);
transform: translateY(-100%);
&.open {
z-index: z("mobile-composer");
}
&.composer-action-edit,
&.edit-title {
height: calc(var(--composer-vh, 1vh) * 100);
}
&.draft,
&.saving {
z-index: z("ipad-header-nav") + 1;
@ -671,19 +655,6 @@ div.ac-wrap {
// this prevents touch events bubbling up to the browser, i.e. accidental scrolls
touch-action: none;
}
// When an element (input, textearea) gets focus, iOS Safari tries to put it in the center
// by scrolling and zooming. We handle zooming with a meta tag. We used to handle scrolling
// using a complicated JS hack.
//
// However, iOS Safari doesn't scroll when the input has opacity of 0 or is clipped.
// We use this quirk and temporarily hide the element on scroll and quickly show it again
//
// Source https://gist.github.com/kiding/72721a0553fa93198ae2bb6eefaa3299
input:focus,
textarea:focus {
animation: blink_input_opacity_to_prevent_scrolling_when_focus 0.01s;
}
}
&.keyboard-visible #reply-control.open {
@ -691,6 +662,32 @@ div.ac-wrap {
}
&.composer-open .with-topic-progress {
bottom: var(--d-min-composer-reply-height);
bottom: calc(var(--composer-height));
}
}
// Safari in iOS/iPad does not handle well a bottom:0 fixed-positioned element,
// especially while the software keyboard is visible, so we top-anchor it here
// and shift it using transform
.ipados-device,
.ios-device {
#reply-control {
// the two properties below are equivalent to bottom: 0
top: calc(var(--composer-vh, 1vh) * 100);
transform: translateY(-100%);
bottom: unset;
}
// When an element (input, textearea) gets focus, iOS Safari tries to put it in the center
// by scrolling and zooming. We handle zooming with a meta tag. We used to handle scrolling
// using a complicated JS hack.
//
// However, iOS Safari doesn't scroll when the input has opacity of 0 or is clipped.
// We use this quirk and temporarily hide the element on scroll and quickly show it again
//
// Source https://gist.github.com/kiding/72721a0553fa93198ae2bb6eefaa3299
input:focus,
textarea:focus {
animation: blink_input_opacity_to_prevent_scrolling_when_focus 0.01s;
}
}

View File

@ -8,7 +8,7 @@
<%- end %>
<%= discourse_theme_color_meta_tags %>
<%= discourse_color_scheme_meta_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover, interactive-widget=resizes-visual">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover, interactive-widget=resizes-content">
<%- if Discourse.base_path.present? %>
<meta name="discourse-base-uri" content="<%= Discourse.base_path %>">
<% end %>