From 4e94324b9a3b33d5fd3ce22cec9673e958a1dcff Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Tue, 12 Aug 2025 17:22:09 -0700 Subject: [PATCH] overlays: Avoid closing when dragging resizers. Fixes most of #35663. --- tools/test-js-with-node | 1 + web/src/modals.ts | 3 ++- web/src/mouse_drag.ts | 30 ++++++++++++++++++++++++++++++ web/src/overlays.ts | 3 ++- web/src/ui_init.js | 2 ++ 5 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 web/src/mouse_drag.ts diff --git a/tools/test-js-with-node b/tools/test-js-with-node index cb3ace9588..63b7d12915 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -169,6 +169,7 @@ EXEMPT_FILES = make_set( "web/src/message_viewport.ts", "web/src/messages_overlay_ui.ts", "web/src/modals.ts", + "web/src/mouse_drag.ts", "web/src/muted_users_ui.ts", "web/src/narrow_history.ts", "web/src/narrow_title.ts", diff --git a/web/src/modals.ts b/web/src/modals.ts index 72b2b1020d..9300fa6055 100644 --- a/web/src/modals.ts +++ b/web/src/modals.ts @@ -3,6 +3,7 @@ import Micromodal from "micromodal"; import assert from "minimalistic-assert"; import * as blueslip from "./blueslip.ts"; +import * as mouse_drag from "./mouse_drag.ts"; import * as overlay_util from "./overlay_util.ts"; import * as overlays from "./overlays.ts"; @@ -157,7 +158,7 @@ export function open( input inside the modal too far will weirdly close the modal. See https://github.com/ghosh/Micromodal/issues/505. Work around this with our own implementation. */ - if (document.getSelection()?.type === "Range") { + if (mouse_drag.is_drag(e)) { return; } close(modal_id); diff --git a/web/src/mouse_drag.ts b/web/src/mouse_drag.ts new file mode 100644 index 0000000000..58473b1a4e --- /dev/null +++ b/web/src/mouse_drag.ts @@ -0,0 +1,30 @@ +import $ from "jquery"; + +let start_x = 0; +let start_y = 0; + +export function initialize(): void { + $(document).on("mousedown", (e) => { + start_x = e.pageX; + start_y = e.pageY; + }); +} + +export function is_drag(e: JQuery.ClickEvent): boolean { + // Used to prevent click handlers from firing when dragging a + // region, even if not actually selecting something. + + // Total distance the mouse has moved since the mouse went down. + const drag_distance = Math.abs(e.pageX - start_x) + Math.abs(e.pageY - start_y); + + const sel = window.getSelection(); + const has_selection = Boolean(sel?.type === "Range" && sel.toString().length > 0); + + // A very low drag_distance cutoff (2) can prevent a click after + // moving the mouse rapidly from registering. + // + // So we only use that low cutoff when the drag resulted in + // actually selecting something, and use a larger distance for + // non-selection drags, like resizing textareas. + return drag_distance > 20 || (drag_distance > 2 && has_selection); +} diff --git a/web/src/overlays.ts b/web/src/overlays.ts index 75b368d627..09bcaa2287 100644 --- a/web/src/overlays.ts +++ b/web/src/overlays.ts @@ -1,6 +1,7 @@ import $ from "jquery"; import * as blueslip from "./blueslip.ts"; +import * as mouse_drag from "./mouse_drag.ts"; import * as overlay_util from "./overlay_util.ts"; type Hook = () => void; @@ -177,7 +178,7 @@ export function initialize(): void { $("body").on("click", "div.overlay, div.overlay .exit", (e) => { let $target = $(e.target); - if (document.getSelection()?.type === "Range") { + if (mouse_drag.is_drag(e)) { return; } diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 4fba43b6aa..feb7fc9d71 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -80,6 +80,7 @@ import * as message_view from "./message_view.ts"; import * as message_view_header from "./message_view_header.ts"; import * as message_viewport from "./message_viewport.ts"; import * as modals from "./modals.ts"; +import * as mouse_drag from "./mouse_drag.ts"; import * as muted_users from "./muted_users.ts"; import * as narrow_history from "./narrow_history.ts"; import * as narrow_state from "./narrow_state.ts"; @@ -441,6 +442,7 @@ export async function initialize_everything(state_data) { user_settings before setting the theme. Because information density is so fundamental, we initialize that first, however. */ initialize_user_settings(state_data.user_settings); + mouse_drag.initialize(); sidebar_ui.restore_sidebar_toggle_status(); i18n.initialize({language_list: page_params.language_list}); timerender.initialize();