diff --git a/app/assets/javascripts/discourse/app/components/d-composer-position.gjs b/app/assets/javascripts/discourse/app/components/d-composer-position.gjs index f3ab33d601e..2c0a5ffc783 100644 --- a/app/assets/javascripts/discourse/app/components/d-composer-position.gjs +++ b/app/assets/javascripts/discourse/app/components/d-composer-position.gjs @@ -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 = diff --git a/app/assets/javascripts/discourse/app/components/d-virtual-height.gjs b/app/assets/javascripts/discourse/app/components/d-virtual-height.gjs index 1b35197f209..06dcd36f184 100644 --- a/app/assets/javascripts/discourse/app/components/d-virtual-height.gjs +++ b/app/assets/javascripts/discourse/app/components/d-virtual-height.gjs @@ -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); diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index aa5ab336061..83b4af9cc1c 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -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; } } diff --git a/app/views/layouts/_head.html.erb b/app/views/layouts/_head.html.erb index 177feecfecf..abd3add7373 100644 --- a/app/views/layouts/_head.html.erb +++ b/app/views/layouts/_head.html.erb @@ -8,7 +8,7 @@ <%- end %> <%= discourse_theme_color_meta_tags %> <%= discourse_color_scheme_meta_tag %> - + <%- if Discourse.base_path.present? %> <% end %>