From 9f3445d03f106ee3ebd8b4b1bd546a08d9169af7 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 21 Sep 2026 19:53:33 +0200 Subject: fix: ask in the page instead of native confirm/alert A native confirm() or alert() leaves the desktop client unable to type until the window is refocused. ask.js draws both in the page; the SPA test now bans all three browser dialogs. Co-Authored-By: Claude Opus 5 --- packages/meshbay-hub/src/meshbay_hub/static/ask.js | 70 ++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/ask.js (limited to 'packages/meshbay-hub/src/meshbay_hub/static/ask.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/ask.js b/packages/meshbay-hub/src/meshbay_hub/static/ask.js new file mode 100644 index 0000000..8a8b895 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/ask.js @@ -0,0 +1,70 @@ +import { html, render, useEffect, useRef } from './vendor/htm-preact.js'; +import { t } from './i18n.js'; + +/** + * `confirm` and `alert`, drawn by the page instead of the browser. + * + * In the desktop client a native `confirm()` leaves the window unable to type: + * once it closes, `document.hasFocus()` stays false, a click still moves + * `activeElement` to a field, and every keystroke after it goes nowhere — until + * the window loses and regains focus. Found as "the Create group fields are + * frozen" right after removing a member, and reproduced against Electron 44 by + * real X input events (`test_no_native_dialogs_in_the_spa.py`). `prompt` was + * already banned for throwing; the other two fail more quietly, and later. + * + * Both return a promise, so a call site reads as it did: `if (!(await + * ask(msg))) return;`. + */ + +function Dialog({ message, cancellable, onDone }) { + const okRef = useRef(null); + useEffect(() => { if (okRef.current) okRef.current.focus(); }, []); + + const onKeyDown = (e) => { + if (e.key === 'Escape') { e.preventDefault(); onDone(!cancellable); } + }; + + return html` +
{ + if (e.target.classList.contains('video-overlay')) onDone(!cancellable); + }}> +
{ e.preventDefault(); onDone(true); }}> +
+
${message}
+
+ ${cancellable && html` + `} + +
+
+
+
+ `; +} + +function open(message, cancellable) { + return new Promise((resolve) => { + const host = document.createElement('div'); + document.body.appendChild(host); + const previous = document.activeElement; + let settled = false; + const onDone = (value) => { + if (settled) return; + settled = true; + render(null, host); + host.remove(); + if (previous && previous.isConnected && typeof previous.focus === 'function') { + previous.focus(); + } + resolve(value); + }; + render(html`<${Dialog} message=${String(message)} cancellable=${cancellable} + onDone=${onDone} />`, host); + }); +} + +export const ask = (message) => open(message, true); +export const tell = (message) => open(message, false); -- cgit v1.2.3