diff --git a/web/src/buddy_list.ts b/web/src/buddy_list.ts index 5195be1ed3..8e13604a61 100644 --- a/web/src/buddy_list.ts +++ b/web/src/buddy_list.ts @@ -12,7 +12,6 @@ import render_presence_rows from "../templates/presence_rows.hbs"; import * as blueslip from "./blueslip.ts"; import * as buddy_data from "./buddy_data.ts"; import type {BuddyUserInfo} from "./buddy_data.ts"; -import {media_breakpoints_num} from "./css_variables.ts"; import type {Filter} from "./filter.ts"; import * as hash_util from "./hash_util.ts"; import {$t} from "./i18n.ts"; @@ -26,6 +25,7 @@ import {current_user} from "./state_data.ts"; import * as stream_data from "./stream_data.ts"; import type {StreamSubscription} from "./sub_store.ts"; import {INTERACTIVE_HOVER_DELAY} from "./tippyjs.ts"; +import * as ui_util from "./ui_util.ts"; import {user_settings} from "./user_settings.ts"; import * as util from "./util.ts"; @@ -174,7 +174,7 @@ export class BuddyList extends BuddyListConf { e.stopPropagation(); const $elem = $(this); let placement: "left" | "auto" = "left"; - if (window.innerWidth < media_breakpoints_num.md) { + if (ui_util.matches_viewport_state("lt_md_min")) { // On small devices display tooltips based on available space. // This will default to "bottom" placement for this tooltip. placement = "auto"; diff --git a/web/src/click_handlers.ts b/web/src/click_handlers.ts index a11e335efa..d9ad291d3e 100644 --- a/web/src/click_handlers.ts +++ b/web/src/click_handlers.ts @@ -13,7 +13,6 @@ import * as buddy_data from "./buddy_data.ts"; import * as compose_actions from "./compose_actions.ts"; import * as compose_reply from "./compose_reply.ts"; import * as compose_state from "./compose_state.ts"; -import {media_breakpoints_num} from "./css_variables.ts"; import * as emoji_picker from "./emoji_picker.ts"; import * as hash_util from "./hash_util.ts"; import * as hashchange from "./hashchange.ts"; @@ -484,7 +483,7 @@ export function initialize(): void { ): void { let placement: tippy.Placement = "left"; let observer: MutationObserver; - if (window.innerWidth < media_breakpoints_num.md) { + if (ui_util.matches_viewport_state("lt_md_min")) { // On small devices display tooltips based on available space. // This will default to "bottom" placement for this tooltip. placement = "auto"; diff --git a/web/src/popover_menus.ts b/web/src/popover_menus.ts index 595567a34b..6bf7351040 100644 --- a/web/src/popover_menus.ts +++ b/web/src/popover_menus.ts @@ -6,11 +6,11 @@ import $ from "jquery"; import * as tippy from "tippy.js"; import * as blueslip from "./blueslip.ts"; -import {media_breakpoints_num} from "./css_variables.ts"; import * as message_viewport from "./message_viewport.ts"; import * as modals from "./modals.ts"; import * as overlays from "./overlays.ts"; import * as popovers from "./popovers.ts"; +import * as ui_util from "./ui_util.ts"; import * as util from "./util.ts"; type PopoverName = @@ -418,7 +418,7 @@ export function toggle_popover_menu( // popover centered on the screen as an overlay. let show_as_overlay = (options?.show_as_overlay_on_mobile === true && - window.innerWidth <= media_breakpoints_num.md) || + ui_util.matches_viewport_state("lt_md_min")) || options?.show_as_overlay_always === true; // Show the popover as over if the reference element is hidden. if (!show_as_overlay && options?.show_as_overlay_if_reference_hidden_at_trigger) { diff --git a/web/src/sidebar_ui.ts b/web/src/sidebar_ui.ts index dfd23be05f..04092fb78d 100644 --- a/web/src/sidebar_ui.ts +++ b/web/src/sidebar_ui.ts @@ -7,7 +7,6 @@ import render_right_sidebar from "../templates/right_sidebar.hbs"; import {buddy_list} from "./buddy_list.ts"; import * as channel from "./channel.ts"; import * as compose_ui from "./compose_ui.ts"; -import {media_breakpoints_num} from "./css_variables.ts"; import {reorder_left_sidebar_navigation_list} from "./left_sidebar_navigation_area.ts"; import {localstorage} from "./localstorage.ts"; import * as message_lists from "./message_lists.ts"; @@ -56,7 +55,7 @@ export function show_userlist_sidebar(): void { return; } - if (window.innerWidth >= media_breakpoints_num.xl) { + if (ui_util.matches_viewport_state("gte_xl_min")) { $("body").removeClass("hide-right-sidebar"); fix_invite_user_button_flicker(); return; @@ -146,7 +145,7 @@ export function initialize(): void { e.preventDefault(); e.stopPropagation(); - if (window.innerWidth >= media_breakpoints_num.xl) { + if (ui_util.matches_viewport_state("gte_xl_min")) { $("body").toggleClass("hide-right-sidebar"); if (!$("body").hasClass("hide-right-sidebar")) { fix_invite_user_button_flicker(); @@ -172,11 +171,11 @@ export function initialize(): void { e.preventDefault(); e.stopPropagation(); - if (window.innerWidth >= media_breakpoints_num.md) { + if (ui_util.matches_viewport_state("gte_md_min")) { $("body").toggleClass("hide-left-sidebar"); if ( message_lists.current !== undefined && - window.innerWidth <= media_breakpoints_num.xl + !ui_util.matches_viewport_state("gte_xl_min") ) { // We expand the middle column width between md and xl breakpoints when the // left sidebar is hidden. This can cause the pointer to move out of view. diff --git a/web/src/ui_util.ts b/web/src/ui_util.ts index b7cddf4f65..a1c457b30a 100644 --- a/web/src/ui_util.ts +++ b/web/src/ui_util.ts @@ -258,3 +258,16 @@ export function show_left_sidebar_menu_icon(element: Element): void { export function hide_left_sidebar_menu_icon(): void { $(".left_sidebar_menu_icon_visible").removeClass("left_sidebar_menu_icon_visible"); } + +export function matches_viewport_state(state_string: string): boolean { + const app_main = document.querySelector(".app-main"); + if (app_main instanceof HTMLElement) { + const app_main_after_content = getComputedStyle(app_main, ":after").content ?? ""; + /* The .content property includes the quotation marks, so we + strip them before splitting on the empty space. */ + const app_main_after_content_array = app_main_after_content.replaceAll('"', "").split(" "); + + return app_main_after_content_array.includes(state_string); + } + return false; +} diff --git a/web/styles/zulip.css b/web/styles/zulip.css index 7c36236bf1..d5d9ba371b 100644 --- a/web/styles/zulip.css +++ b/web/styles/zulip.css @@ -495,6 +495,15 @@ body.has-overlay-scrollbar { height: 100%; min-height: 100%; + &::after { + /* We use `content` values to determine + viewport state from JavaScript; this + hides the values without disrupting + the document flow. */ + position: absolute; + display: none; + } + .column-left .left-sidebar, .column-right .right-sidebar { position: fixed; @@ -1861,6 +1870,55 @@ body:not(.hide-left-sidebar) { /* End fallback media queries. */ +/* Begin viewport state queries. */ + +/* We sometimes need the state of the viewport in JavaScript; + by setting values on `content:`, we can query instead for + those, independent of whether it's a container or media query + responsible for layout. See for example `sidebar_ui.ts`. + + These queries for greater than or equal to should be ordered + smallest to largest. */ + +/* Adjustments for smaller viewports are critical + regardless of info-density settings, so we + don't include a container-query check here. */ +@media (width < $md_min) { + .app-main::after { + content: "lt_md_min"; + } +} + +@container app (width >= $cq_md_min) { + .app-main::after { + content: "gte_md_min"; + } +} + +@media (width >= $md_min) { + .without-container-query-support { + .app-main::after { + content: "gte_md_min"; + } + } +} + +@container app (width >= $cq_xl_min) { + .app-main::after { + content: "gte_md_min gte_xl_min"; + } +} + +@media (width >= $xl_min) { + .without-container-query-support { + .app-main::after { + content: "gte_md_min gte_xl_min"; + } + } +} + +/* End viewport state queries. */ + @media (height < $short_navbar_cutoff_height) { .app-main .column-right.expanded .right-sidebar, .app-main .column-left.expanded .left-sidebar {