diff --git a/app/assets/javascripts/discourse/app/components/composer-body.js b/app/assets/javascripts/discourse/app/components/composer-body.js index 24e560b67d0..7891a218f53 100644 --- a/app/assets/javascripts/discourse/app/components/composer-body.js +++ b/app/assets/javascripts/discourse/app/components/composer-body.js @@ -8,7 +8,7 @@ import Composer from "discourse/models/composer"; import KeyEnterEscape from "discourse/mixins/key-enter-escape"; import afterTransition from "discourse/lib/after-transition"; import discourseDebounce from "discourse-common/lib/debounce"; -import { headerOffset } from "discourse/components/site-header"; +import { headerOffset } from "discourse/lib/offset-calculator"; import positioningWorkaround from "discourse/lib/safari-hacks"; const START_DRAG_EVENTS = ["touchstart", "mousedown"]; diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js index febba56a795..6035a5b5830 100644 --- a/app/assets/javascripts/discourse/app/components/site-header.js +++ b/app/assets/javascripts/discourse/app/components/site-header.js @@ -7,6 +7,7 @@ import Docking from "discourse/mixins/docking"; import MountWidget from "discourse/components/mount-widget"; import ItsATrap from "@discourse/itsatrap"; import RerenderOnDoNotDisturbChange from "discourse/mixins/rerender-on-do-not-disturb-change"; +import { headerOffset } from "discourse/lib/offset-calculator"; import { observes } from "discourse-common/utils/decorators"; import { topicTitleDecorators } from "discourse/components/topic-title"; @@ -438,15 +439,6 @@ export default SiteHeaderComponent.extend({ classNames: ["d-header-wrap"], }); -export function headerOffset() { - return ( - parseInt( - document.documentElement.style.getPropertyValue("--header-offset"), - 10 - ) || 0 - ); -} - export function headerTop() { const header = document.querySelector("header.d-header"); const headerOffsetTop = header.offsetTop ? header.offsetTop : 0; diff --git a/app/assets/javascripts/discourse/app/components/topic-navigation.js b/app/assets/javascripts/discourse/app/components/topic-navigation.js index b6a15b6e1f3..f40882cf87e 100644 --- a/app/assets/javascripts/discourse/app/components/topic-navigation.js +++ b/app/assets/javascripts/discourse/app/components/topic-navigation.js @@ -5,7 +5,7 @@ import PanEvents, { import Component from "@ember/component"; import EmberObject from "@ember/object"; import discourseDebounce from "discourse-common/lib/debounce"; -import { headerOffset } from "discourse/components/site-header"; +import { headerOffset } from "discourse/lib/offset-calculator"; import { later, next } from "@ember/runloop"; import { observes } from "discourse-common/utils/decorators"; import showModal from "discourse/lib/show-modal"; diff --git a/app/assets/javascripts/discourse/app/components/topic-timeline.js b/app/assets/javascripts/discourse/app/components/topic-timeline.js index 5a60bc9be27..d37ced63530 100644 --- a/app/assets/javascripts/discourse/app/components/topic-timeline.js +++ b/app/assets/javascripts/discourse/app/components/topic-timeline.js @@ -1,6 +1,6 @@ import Docking from "discourse/mixins/docking"; import MountWidget from "discourse/components/mount-widget"; -import { headerOffset } from "discourse/components/site-header"; +import { headerOffset } from "discourse/lib/offset-calculator"; import { next } from "@ember/runloop"; import { observes } from "discourse-common/utils/decorators"; import optionalService from "discourse/lib/optional-service"; diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js index 42e3a0ad1f3..16f7f9a5b7b 100644 --- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js @@ -1,3 +1,5 @@ +import { bind } from "discourse-common/utils/decorators"; +import discourseDebounce from "discourse-common/lib/debounce"; import { isAppWebview } from "discourse/lib/utilities"; import { later, run, schedule, throttle } from "@ember/runloop"; import { @@ -6,9 +8,10 @@ import { } from "discourse/lib/topic-list-tracker"; import Composer from "discourse/models/composer"; import DiscourseURL from "discourse/lib/url"; +import domUtils from "discourse-common/utils/dom-utils"; import { INPUT_DELAY } from "discourse-common/config/environment"; import { ajax } from "discourse/lib/ajax"; -import { minimumOffset } from "discourse/lib/offset-calculator"; +import { headerOffset } from "discourse/lib/offset-calculator"; const DEFAULT_BINDINGS = { "!": { postAction: "showFlags" }, @@ -608,7 +611,7 @@ export default { // Discard selection if it is not in viewport, so users can combine // keyboard shortcuts with mouse scrolling. if ($selected.length !== 0 && !fast) { - const offset = minimumOffset(); + const offset = headerOffset(); const beginScreen = $(window).scrollTop() - offset; const endScreen = beginScreen + window.innerHeight + offset; const beginArticle = $selected.offset().top; @@ -621,7 +624,7 @@ export default { // If still nothing is selected, select the first post that is // visible and cancel move operation. if (!$selected || $selected.length === 0) { - const offset = minimumOffset(); + const offset = headerOffset(); $selected = $articles .toArray() .find((article) => @@ -636,32 +639,32 @@ export default { } const index = $articles.index($selected); - let $article = $articles.eq(index); + let article = $articles.eq(index)[0]; // Try doing a page scroll in the context of current post. - if (!fast && direction !== 0 && $article.length > 0) { + if (!fast && direction !== 0 && article) { // The beginning of first article is the beginning of the page. const beginArticle = - $article.is(".topic-post") && $article.find("#post_1").length + article.classList.contains("topic-post") && + article.querySelector("#post_1") ? 0 - : $article.offset().top; - const endArticle = - $article.offset().top + $article[0].getBoundingClientRect().height; + : domUtils.offset(article).top; + const endArticle = domUtils.offset(article).top + article.offsetHeight; - const beginScreen = $(window).scrollTop(); + const beginScreen = window.scrollY; const endScreen = beginScreen + window.innerHeight; if (direction < 0 && beginScreen > beginArticle) { return this._scrollTo( Math.max( - beginScreen - window.innerHeight + 3 * minimumOffset(), // page up - beginArticle - minimumOffset() // beginning of article + beginScreen - window.innerHeight + 3 * headerOffset(), // page up + beginArticle - headerOffset() // beginning of article ) ); - } else if (direction > 0 && endScreen < endArticle - minimumOffset()) { + } else if (direction > 0 && endScreen < endArticle - headerOffset()) { return this._scrollTo( Math.min( - endScreen - 3 * minimumOffset(), // page down + endScreen - 3 * headerOffset(), // page down endArticle - window.innerHeight // end of article ) ); @@ -678,68 +681,59 @@ export default { } } - $article = $articles.eq(index + direction); - if ($article.length > 0) { - $articles.removeClass("selected"); - $article.addClass("selected"); - this.appEvents.trigger("keyboard:move-selection", { - articles: $articles.get(), - selectedArticle: $article.get(0), - }); - - const articleRect = $article[0].getBoundingClientRect(); - if (!fast && direction < 0 && articleRect.height > window.innerHeight) { - // Scrolling to the last "page" of the previous post if post has multiple - // "pages" (if its height does not fit in the screen). - return this._scrollTo( - $article.offset().top + articleRect.height - window.innerHeight - ); - } else if ($article.is(".topic-post")) { - return this._scrollTo( - $article.find("#post_1").length > 0 - ? 0 - : $article.offset().top - minimumOffset(), - () => $("a.tabLoc", $article).focus() - ); - } - - // Otherwise scroll through the suggested topic list. - this._scrollList($article, direction); + article = $articles.eq(index + direction)[0]; + if (!article) { + return; } + + $articles.removeClass("selected"); + article.classList.add("selected"); + + this.appEvents.trigger("keyboard:move-selection", { + articles: $articles.get(), + selectedArticle: article, + }); + + const articleTop = domUtils.offset(article).top; + if (!fast && direction < 0 && article.offsetHeight > window.innerHeight) { + // Scrolling to the last "page" of the previous post if post has multiple + // "pages" (if its height does not fit in the screen). + return this._scrollTo( + articleTop + article.offsetHeight - window.innerHeight + ); + } else if (article.classList.contains("topic-post")) { + return this._scrollTo( + article.querySelector("#post_1") ? 0 : articleTop - headerOffset(), + { focusTabLoc: true } + ); + } + + // Otherwise scroll through the suggested topic list. + article.scrollIntoView({ + behavior: "smooth", + block: "center", + }); }, - _scrollTo(scrollTop) { + _scrollTo(scrollTop, opts = {}) { window.scrollTo({ top: scrollTop, behavior: "smooth", }); + + if (opts.focusTabLoc) { + window.addEventListener("scroll", this._onScrollEnds, { passive: true }); + } }, - _scrollList($article) { - // Try to keep the article on screen - const pos = $article.offset(); - const height = $article.height(); - const headerHeight = $("header.d-header").height(); - const scrollTop = $(window).scrollTop(); - const windowHeight = $(window).height(); + @bind + _onScrollEnds() { + window.removeEventListener("scroll", this._onScrollEnds, { passive: true }); + discourseDebounce(this, this._onScrollEndsCallback, animationDuration); + }, - // skip if completely on screen - if ( - pos.top - headerHeight > scrollTop && - pos.top + height < scrollTop + windowHeight - ) { - return; - } - - let scrollPos = pos.top + height / 2 - windowHeight * 0.5; - if (height > windowHeight - headerHeight) { - scrollPos = pos.top - headerHeight; - } - if (scrollPos < 0) { - scrollPos = 0; - } - - this._scrollTo(scrollPos); + _onScrollEndsCallback() { + document.querySelector(".topic-post.selected a.tabLoc")?.focus(); }, categoriesTopicsList() { diff --git a/app/assets/javascripts/discourse/app/lib/lock-on.js b/app/assets/javascripts/discourse/app/lib/lock-on.js index 32a92b342bd..9c77de68d02 100644 --- a/app/assets/javascripts/discourse/app/lib/lock-on.js +++ b/app/assets/javascripts/discourse/app/lib/lock-on.js @@ -1,5 +1,5 @@ import { bind } from "discourse-common/utils/decorators"; -import { minimumOffset } from "discourse/lib/offset-calculator"; +import { headerOffset } from "discourse/lib/offset-calculator"; // Dear traveller, you are entering a zone where we are at war with the browser. // The browser is insisting on positioning scrollTop per the location it was in @@ -50,7 +50,7 @@ export default class LockOn { } } - return offset - minimumOffset(); + return offset - headerOffset(); } clearLock() { diff --git a/app/assets/javascripts/discourse/app/lib/offset-calculator.js b/app/assets/javascripts/discourse/app/lib/offset-calculator.js index 9ce8c234e24..976ac56b78a 100644 --- a/app/assets/javascripts/discourse/app/lib/offset-calculator.js +++ b/app/assets/javascripts/discourse/app/lib/offset-calculator.js @@ -1,8 +1,18 @@ +import deprecated from "discourse-common/lib/deprecated"; + export function scrollTopFor(y) { return y - offsetCalculator(); } export function minimumOffset() { + deprecated( + "The minimumOffset() helper is deprecated, please use headerOffset() instead.", + { + since: "2.8.0.beta10", + dropFrom: "2.9.0.beta2", + } + ); + const header = document.querySelector("header.d-header"), iPadNav = document.querySelector(".footer-nav-ipad .footer-nav"), iPadNavHeight = iPadNav ? iPadNav.offsetHeight : 0; @@ -17,8 +27,17 @@ export function minimumOffset() { : 0; } +export function headerOffset() { + return ( + parseInt( + document.documentElement.style.getPropertyValue("--header-offset"), + 10 + ) || 0 + ); +} + export default function offsetCalculator() { - const min = minimumOffset(); + const min = headerOffset(); // on mobile, just use the header if (document.querySelector("html").classList.contains("mobile-view")) { diff --git a/app/assets/javascripts/discourse/app/lib/sticky-avatars.js b/app/assets/javascripts/discourse/app/lib/sticky-avatars.js index 0ab533b97a9..65083ae6176 100644 --- a/app/assets/javascripts/discourse/app/lib/sticky-avatars.js +++ b/app/assets/javascripts/discourse/app/lib/sticky-avatars.js @@ -1,7 +1,7 @@ import { addWidgetCleanCallback } from "discourse/components/mount-widget"; import Site from "discourse/models/site"; import { bind } from "discourse-common/utils/decorators"; -import { headerOffset } from "discourse/components/site-header"; +import { headerOffset } from "discourse/lib/offset-calculator"; import { schedule } from "@ember/runloop"; export default class StickyAvatars { @@ -79,6 +79,9 @@ export default class StickyAvatars { @bind _initIntersectionObserver() { schedule("afterRender", () => { + const headerOffsetInPx = + headerOffset() <= 0 ? "0px" : `-${headerOffset()}px`; + this.intersectionObserver = new IntersectionObserver( (entries) => { entries.forEach((entry) => { @@ -99,7 +102,7 @@ export default class StickyAvatars { }, { threshold: [0.0, 1.0], - rootMargin: `-${headerOffset()}px 0px 0px 0px`, + rootMargin: `${headerOffsetInPx} 0px 0px 0px`, } ); }); diff --git a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js index 272d4930c3b..2eb5f26f269 100644 --- a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js +++ b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js @@ -1,6 +1,6 @@ import { ajax } from "discourse/lib/ajax"; import discourseDebounce from "discourse-common/lib/debounce"; -import { headerOffset } from "discourse/components/site-header"; +import { headerOffset } from "discourse/lib/offset-calculator"; import isElementInViewport from "discourse/lib/is-element-in-viewport"; import { withPluginApi } from "discourse/lib/plugin-api";