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

View File

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

View File

@ -1,14 +1,10 @@
html.composer-open.not-mobile-device { html.composer-open {
#main-outlet { #main-outlet {
padding-bottom: var(--composer-height); padding-bottom: var(--composer-height);
transition: padding-bottom 250ms ease; transition: padding-bottom 250ms ease;
} }
} }
html.composer-open {
--d-min-composer-reply-height: 35vh;
}
#reply-control { #reply-control {
position: fixed; position: fixed;
display: flex; display: flex;
@ -44,7 +40,7 @@ html.composer-open {
} }
z-index: z("composer", "content"); z-index: z("composer", "content");
transition: height 0.2s, max-width 0.2s, padding-bottom 0.2s, top 0.2s, 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); background-color: var(--secondary);
box-shadow: var(--shadow-composer); box-shadow: var(--shadow-composer);
@ -71,7 +67,6 @@ html.composer-open {
&.open { &.open {
box-sizing: border-box; box-sizing: border-box;
height: var(--composer-height); height: var(--composer-height);
min-height: var(--d-min-composer-reply-height);
max-height: calc(100vh - var(--header-offset, 4em)); max-height: calc(100vh - var(--header-offset, 4em));
padding-bottom: var(--composer-ipad-padding); 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, .ipados-device,
.mobile-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 { #reply-control {
bottom: unset;
height: 0;
z-index: -1; z-index: -1;
// these two properties below are equivalent to bottom: 0
top: calc(var(--composer-vh, 1vh) * 100);
transform: translateY(-100%);
&.open { &.open {
z-index: z("mobile-composer"); z-index: z("mobile-composer");
} }
&.composer-action-edit,
&.edit-title {
height: calc(var(--composer-vh, 1vh) * 100);
}
&.draft, &.draft,
&.saving { &.saving {
z-index: z("ipad-header-nav") + 1; 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 // this prevents touch events bubbling up to the browser, i.e. accidental scrolls
touch-action: none; 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 { &.keyboard-visible #reply-control.open {
@ -691,6 +662,32 @@ div.ac-wrap {
} }
&.composer-open .with-topic-progress { &.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 %> <%- end %>
<%= discourse_theme_color_meta_tags %> <%= discourse_theme_color_meta_tags %>
<%= discourse_color_scheme_meta_tag %> <%= 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? %> <%- if Discourse.base_path.present? %>
<meta name="discourse-base-uri" content="<%= Discourse.base_path %>"> <meta name="discourse-base-uri" content="<%= Discourse.base_path %>">
<% end %> <% end %>