diff --git a/templates/zerver/app/index.html b/templates/zerver/app/index.html
index 6f501c4998..8118c6e256 100644
--- a/templates/zerver/app/index.html
+++ b/templates/zerver/app/index.html
@@ -104,6 +104,7 @@
text-decoration: underline;
}
+
{% endblock %}
{% block content %}
@@ -202,8 +203,10 @@
-
diff --git a/web/src/compose_actions.js b/web/src/compose_actions.js
index 484db7f140..beefdd526a 100644
--- a/web/src/compose_actions.js
+++ b/web/src/compose_actions.js
@@ -128,7 +128,8 @@ export function maybe_scroll_up_selected_message() {
return;
}
- const cover = $selected_row.offset().top + $selected_row.height() - $("#compose").offset().top;
+ const cover =
+ $selected_row.get_offset_to_window().bottom - $("#compose").get_offset_to_window().top;
if (cover > 0) {
message_viewport.user_initiated_animate_scroll(cover + 20);
}
diff --git a/web/src/compose_recipient.js b/web/src/compose_recipient.js
index 30559ad27e..ec3a84a24e 100644
--- a/web/src/compose_recipient.js
+++ b/web/src/compose_recipient.js
@@ -286,7 +286,8 @@ function compose_recipient_dropdown_on_show(dropdown) {
const window_height = window.innerHeight;
const search_box_and_padding_height = 50;
// pixels above compose box.
- const recipient_input_top = $("#compose_recipient_selection_dropdown").offset().top;
+ const recipient_input_top = $("#compose_recipient_selection_dropdown").get_offset_to_window()
+ .top;
const top_space = recipient_input_top - top_offset - search_box_and_padding_height;
// pixels below compose starting from top of compose box.
const bottom_space = window_height - recipient_input_top - search_box_and_padding_height;
diff --git a/web/src/compose_ui.js b/web/src/compose_ui.js
index a61d0192ce..83292df3c0 100644
--- a/web/src/compose_ui.js
+++ b/web/src/compose_ui.js
@@ -260,10 +260,7 @@ export function set_compose_box_top(set_top) {
// using CSS. If that wasn't the case, we could have somehow
// refactored the HTML so as to consider only the space below
// below the `#navbar_alerts` as `height: 100%` of `#compose`.
- const compose_top =
- $("#navbar_alerts_wrapper").height() +
- $(".header").height() +
- Number.parseInt($(".header").css("paddingBottom"), 10);
+ const compose_top = $("#navbar-fixed-container").height();
$("#compose").css("top", compose_top + "px");
} else {
$("#compose").css("top", "");
diff --git a/web/src/message_edit.js b/web/src/message_edit.js
index a4ffb0f5fd..ce556f5491 100644
--- a/web/src/message_edit.js
+++ b/web/src/message_edit.js
@@ -566,8 +566,8 @@ function edit_message($row, raw_content) {
function start_edit_maintaining_scroll($row, content) {
edit_message($row, content);
- const row_bottom = $row.height() + $row.offset().top;
- const composebox_top = $("#compose").offset().top;
+ const row_bottom = $row.get_offset_to_window().bottom;
+ const composebox_top = $("#compose").get_offset_to_window().top;
if (row_bottom > composebox_top) {
message_viewport.scrollTop(message_viewport.scrollTop() + row_bottom - composebox_top);
}
diff --git a/web/src/message_list_view.js b/web/src/message_list_view.js
index a66a800344..a8f56bfa4f 100644
--- a/web/src/message_list_view.js
+++ b/web/src/message_list_view.js
@@ -778,7 +778,7 @@ export class MessageListView {
const save_scroll_position = () => {
if (orig_scrolltop_offset === undefined && this.selected_row().length > 0) {
- orig_scrolltop_offset = this.selected_row().offset().top;
+ orig_scrolltop_offset = this.selected_row().get_offset_to_window().top;
}
};
@@ -977,7 +977,7 @@ export class MessageListView {
// it's the max amount that we can scroll down (or "skooch
// up" the messages) before knocking the selected message
// out of the feed.
- const selected_row_top = $selected_row.offset().top;
+ const selected_row_top = $selected_row.get_offset_to_window().top;
let scroll_limit = selected_row_top - viewport_info.visible_top;
if (scroll_limit < 0) {
@@ -1146,7 +1146,7 @@ export class MessageListView {
const $selected_row = this.selected_row();
const selected_in_view = $selected_row.length > 0;
if (selected_in_view) {
- old_offset = $selected_row.offset().top;
+ old_offset = $selected_row.get_offset_to_window().top;
}
if (discard_rendering_state) {
// If we know that the existing render is invalid way
@@ -1160,7 +1160,9 @@ export class MessageListView {
set_message_offset(offset) {
const $msg = this.selected_row();
- message_viewport.scrollTop(message_viewport.scrollTop() + $msg.offset().top - offset);
+ message_viewport.scrollTop(
+ message_viewport.scrollTop() + $msg.get_offset_to_window().top - offset,
+ );
}
rerender_with_target_scrolltop(selected_row, target_offset) {
diff --git a/web/src/message_scroll.js b/web/src/message_scroll.js
index 292ff71b6d..191a3fe32d 100644
--- a/web/src/message_scroll.js
+++ b/web/src/message_scroll.js
@@ -204,7 +204,7 @@ function scroll_finish() {
}
export function initialize() {
- message_viewport.$message_pane.on(
+ $(document).on(
"scroll",
_.throttle(() => {
unread_ops.process_visible();
diff --git a/web/src/message_viewport.js b/web/src/message_viewport.js
index 4fd801ea92..cf8743e34f 100644
--- a/web/src/message_viewport.js
+++ b/web/src/message_viewport.js
@@ -56,11 +56,10 @@ export function message_viewport_info() {
const res = {};
- const $element_just_above_us = $("#navbar-container .header");
+ const $element_just_above_us = $("#navbar-fixed-container");
const $element_just_below_us = $("#compose");
- res.visible_top =
- $element_just_above_us.offset().top + $element_just_above_us.safeOuterHeight();
+ res.visible_top = $element_just_above_us.safeOuterHeight();
const $sticky_header = $(".sticky_header");
if ($sticky_header.length) {
@@ -119,7 +118,7 @@ export function offset_from_bottom($last_row) {
// A positive return value here means the last row is
// below the bottom of the feed (i.e. obscured by the compose
// box or even further below the bottom).
- const message_bottom = $last_row.offset().top + $last_row.height();
+ const message_bottom = $last_row.get_offset_to_window().bottom;
const info = message_viewport_info();
return message_bottom - info.visible_bottom;
@@ -188,8 +187,8 @@ function add_to_visible(
const top_of_feed = new util.CachedValue({
compute_value() {
- const $header = $("#navbar-container .header");
- let visible_top = $header.offset().top + $header.safeOuterHeight();
+ const $header = $("#navbar-fixed-container");
+ let visible_top = $header.safeOuterHeight();
const $sticky_header = $(".sticky_header");
if ($sticky_header.length) {
@@ -290,7 +289,7 @@ export function scrollTop(target_scrollTop) {
}
let $ret = $message_pane.scrollTop(target_scrollTop);
const new_scrollTop = $message_pane.scrollTop();
- const space_to_scroll = $("#bottom_whitespace").offset().top - height();
+ const space_to_scroll = $("#bottom_whitespace").get_offset_to_window().top - height();
// Check whether our scrollTop didn't move even though one could have scrolled down
if (
@@ -362,7 +361,7 @@ export function recenter_view($message, {from_scroll = false, force_center = fal
const bottom_threshold = viewport_info.visible_bottom;
- const message_top = $message.offset().top;
+ const message_top = $message.get_offset_to_window().top;
const message_height = $message.safeOuterHeight(true);
const message_bottom = message_top + message_height;
@@ -395,7 +394,7 @@ export function maybe_scroll_to_show_message_top() {
// Only applies if the top of the message is out of view above the visible area.
const $selected_message = message_lists.current.selected_row();
const viewport_info = message_viewport_info();
- const message_top = $selected_message.offset().top;
+ const message_top = $selected_message.get_offset_to_window().top;
const message_height = $selected_message.safeOuterHeight(true);
if (message_top < viewport_info.visible_top) {
set_message_position(message_top, message_height, viewport_info, 0);
@@ -405,7 +404,7 @@ export function maybe_scroll_to_show_message_top() {
export function is_message_below_viewport($message_row) {
const info = message_viewport_info();
- const offset = $message_row.offset();
+ const offset = $message_row.get_offset_to_window();
return offset.top >= info.visible_bottom;
}
@@ -432,7 +431,7 @@ export function keep_pointer_in_view() {
return true;
}
- const message_top = $next_row.offset().top;
+ const message_top = $next_row.get_offset_to_window().top;
// If the message starts after the very top of the screen, we just
// leave it alone. This avoids bugs like #1608, where overzealousness
@@ -453,7 +452,7 @@ export function keep_pointer_in_view() {
}
function message_is_far_enough_up() {
- return at_bottom() || $next_row.offset().top <= bottom_threshold;
+ return at_bottom() || $next_row.get_offset_to_window().top <= bottom_threshold;
}
function adjust(in_view, get_next_row) {
@@ -481,7 +480,7 @@ export function keep_pointer_in_view() {
export function initialize() {
$jwindow = $(window);
- $message_pane = $(".app");
+ $message_pane = $("html");
// This handler must be placed before all resize handlers in our application
$jwindow.on("resize", () => {
dimensions.height.reset();
diff --git a/web/src/narrow.js b/web/src/narrow.js
index 2ba9460d20..164359526e 100644
--- a/web/src/narrow.js
+++ b/web/src/narrow.js
@@ -64,7 +64,9 @@ export function save_pre_narrow_offset_for_reload() {
render_end: message_lists.current.view._render_win_end,
});
}
- message_lists.current.pre_narrow_offset = message_lists.current.selected_row().offset().top;
+ message_lists.current.pre_narrow_offset = message_lists.current
+ .selected_row()
+ .get_offset_to_window().top;
}
}
@@ -409,7 +411,7 @@ export function activate(raw_operators, opts) {
if (opts.then_select_offset === undefined) {
const $row = message_lists.current.get_row(opts.then_select_id);
if ($row.length > 0) {
- opts.then_select_offset = $row.offset().top;
+ opts.then_select_offset = $row.get_offset_to_window().top;
}
}
}
diff --git a/web/src/overlays.ts b/web/src/overlays.ts
index 8e5ed601f4..b5f1053ef0 100644
--- a/web/src/overlays.ts
+++ b/web/src/overlays.ts
@@ -137,7 +137,7 @@ export function open_overlay(opts: OverlayOptions): void {
opts.$overlay.addClass("show");
opts.$overlay.attr("aria-hidden", "false");
$(".app").attr("aria-hidden", "true");
- $(".header").attr("aria-hidden", "true");
+ $("#navbar-fixed-container").attr("aria-hidden", "true");
}
// If conf.autoremove is true, the modal element will be removed from the DOM
@@ -240,7 +240,6 @@ export function open_modal(
}
close_modal(modal_id);
});
-
Micromodal.show(modal_id, {
disableFocus: true,
openClass: "modal--opening",
@@ -272,7 +271,7 @@ export function close_overlay(name: string): void {
active_overlay.$element.attr("aria-hidden", "true");
$(".app").attr("aria-hidden", "false");
- $(".header").attr("aria-hidden", "false");
+ $("#navbar-fixed-container").attr("aria-hidden", "false");
active_overlay.close_handler();
}
diff --git a/web/src/popovers.js b/web/src/popovers.js
index b2538d7314..0278887a24 100644
--- a/web/src/popovers.js
+++ b/web/src/popovers.js
@@ -163,7 +163,7 @@ function load_medium_avatar(user, $elt) {
}
function calculate_info_popover_placement(size, $elt) {
- const ypos = $elt.offset().top;
+ const ypos = $elt.get_offset_to_window().top;
if (!(ypos + size / 2 < message_viewport.height() && ypos > size / 2)) {
if (ypos + size < message_viewport.height()) {
@@ -303,10 +303,7 @@ function render_user_info_popover(
const $popover_content = $(render_user_info_popover_content(args));
popover_element.popover({
content: $popover_content.get(0),
- // TODO: Determine whether `fixed` should be applied
- // unconditionally. Right now, we only do it for the user
- // sidebar version of the popover.
- fixed: template_class === "user_popover",
+ fixed: true,
placement: popover_placement,
template: render_no_arrow_popover({class: template_class}),
title: render_user_info_popover_title({
@@ -316,7 +313,7 @@ function render_user_info_popover(
}),
html: true,
trigger: "manual",
- top_offset: $("#userlist-title").offset().top + 15,
+ top_offset: $("#userlist-title").get_offset_to_window().top + 15,
fix_positions: true,
});
popover_element.popover("show");
@@ -709,7 +706,7 @@ export function toggle_playground_link_popover(element, playground_info) {
}
const $elt = $(element);
if ($elt.data("popover") === undefined) {
- const ypos = $elt.offset().top;
+ const ypos = $elt.get_offset_to_window().top;
$elt.popover({
// It's unlikely we'll have more than 3-4 playground links
// for one language, so it should be OK to hardcode 120 here.
diff --git a/web/src/reload.js b/web/src/reload.js
index f809ed7ba2..d15310b26c 100644
--- a/web/src/reload.js
+++ b/web/src/reload.js
@@ -79,7 +79,7 @@ function preserve_state(send_after_reload, save_pointer, save_narrow, save_compo
const $row = message_lists.home.selected_row();
if (!narrow_state.active()) {
if ($row.length > 0) {
- url += "+offset=" + $row.offset().top;
+ url += "+offset=" + $row.get_offset_to_window().top;
}
} else {
url += "+offset=" + message_lists.home.pre_narrow_offset;
@@ -92,7 +92,7 @@ function preserve_state(send_after_reload, save_pointer, save_narrow, save_compo
}
const $narrow_row = message_lists.current.selected_row();
if ($narrow_row.length > 0) {
- url += "+narrow_offset=" + $narrow_row.offset().top;
+ url += "+narrow_offset=" + $narrow_row.get_offset_to_window().top;
}
}
}
diff --git a/web/src/resize.js b/web/src/resize.js
index 3e1e8727cc..d5dba39bd9 100644
--- a/web/src/resize.js
+++ b/web/src/resize.js
@@ -145,8 +145,19 @@ export function resize_sidebars() {
return h;
}
+export function reposition_message_header() {
+ // Since `navbar_alerts_wrapper`'s height can vary based on text / language, we
+ // need to adjust at what `top` position do `message-header` becomes `sticky`.
+ // Best way to do this is via adding custom CSS to the DOM instead of running endless
+ // javascript queries to find and update them on various re-renders.
+ const navbar_fixed_height = $("#navbar-fixed-container").safeOuterHeight(true);
+ const style = document.querySelector("#sticky_message_header_styles");
+ style.textContent = `.message_list .message_header { top: ${navbar_fixed_height}px !important; }`;
+}
+
export function resize_page_components() {
navbar_alerts.resize_app();
+ reposition_message_header();
const h = resize_sidebars();
resize_bottom_whitespace(h);
}
diff --git a/web/src/scroll_bar.ts b/web/src/scroll_bar.ts
index cdcd180185..8d898a49e2 100644
--- a/web/src/scroll_bar.ts
+++ b/web/src/scroll_bar.ts
@@ -2,37 +2,6 @@ import $ from "jquery";
import {user_settings} from "./user_settings";
-// A few of our width properties in Zulip depend on the width of the
-// browser scrollbar that is generated at the far right side of the
-// page, which unfortunately varies depending on the browser and
-// cannot be detected directly using CSS. As a result, we adjust a
-// number of element widths based on the value detected here.
-//
-// From https://stackoverflow.com/questions/13382516/getting-scroll-bar-width-using-javascript
-function getScrollbarWidth(): number {
- const outer = document.createElement("div");
- outer.style.visibility = "hidden";
- outer.style.width = "100px";
-
- document.body.append(outer);
-
- const widthNoScroll = outer.offsetWidth;
- // force scrollbars
- outer.style.overflow = "scroll";
-
- // add inner div
- const inner = document.createElement("div");
- inner.style.width = "100%";
- outer.append(inner);
-
- const widthWithScroll = inner.offsetWidth;
-
- // remove divs
- outer.remove();
-
- return widthNoScroll - widthWithScroll;
-}
-
export function set_layout_width(): void {
if (user_settings.fluid_layout_width) {
$(".header-main, .app .app-main, #compose-container").css("max-width", "inherit");
@@ -41,14 +10,6 @@ export function set_layout_width(): void {
}
}
-let sbWidth: number;
-
export function initialize(): void {
- // Workaround for browsers with fixed scrollbars
- sbWidth = getScrollbarWidth();
- if (sbWidth > 0) {
- // Reduce width of screen-wide parent containers, whose width doesn't vary with scrollbar width, by scrollbar width.
- $("#navbar-container .header, #compose").css("width", `calc(100% - ${sbWidth}px)`);
- }
set_layout_width();
}
diff --git a/web/src/setup.js b/web/src/setup.js
index 6c1ccd5dab..726deef7b7 100644
--- a/web/src/setup.js
+++ b/web/src/setup.js
@@ -31,6 +31,10 @@ $(() => {
return this.outerHeight(...args) || 0;
};
+ $.fn.get_offset_to_window = function () {
+ return this[0].getBoundingClientRect();
+ };
+
$.fn.safeOuterWidth = function (...args) {
return this.outerWidth(...args) || 0;
};
diff --git a/web/src/ui_init.js b/web/src/ui_init.js
index cf5e685213..e91f420568 100644
--- a/web/src/ui_init.js
+++ b/web/src/ui_init.js
@@ -221,7 +221,7 @@ function initialize_navbar() {
search_pills_enabled: page_params.search_pills_enabled,
});
- $("#navbar-container").html(rendered_navbar);
+ $("#header-container").html(rendered_navbar);
}
function initialize_compose_box() {
diff --git a/web/styles/zulip.css b/web/styles/zulip.css
index ca72800abe..c5c53f1585 100644
--- a/web/styles/zulip.css
+++ b/web/styles/zulip.css
@@ -1,10 +1,13 @@
+html {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ overscroll-behavior-y: none;
+}
+
body,
html {
width: 100%;
height: 100%;
- overflow-x: hidden;
- overflow-y: hidden;
-
touch-action: manipulation;
}
@@ -359,9 +362,14 @@ p.n-margin {
opacity: 1;
}
-.header {
+#navbar-fixed-container {
position: fixed;
- z-index: 102; /* Needs to be higher than .alert-bar-container */
+ top: 0;
+ z-index: 102;
+ width: 100%;
+}
+
+.header {
width: 100%;
height: var(--header-height);
/* Since the headers are sticky, we need non-transparent background. */
@@ -485,9 +493,7 @@ p.n-margin {
.app {
width: 100%;
height: 100%;
- overflow-y: scroll;
z-index: 99;
- -webkit-overflow-scrolling: touch;
}
.app-main,
diff --git a/web/templates/navbar.hbs b/web/templates/navbar.hbs
index 8de0679aa5..49fcdf2bd7 100644
--- a/web/templates/navbar.hbs
+++ b/web/templates/navbar.hbs
@@ -1,5 +1,5 @@