aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/ask.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/ask.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/ask.js70
1 files changed, 70 insertions, 0 deletions
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`
+ <div class="video-overlay" onClick=${(e) => {
+ if (e.target.classList.contains('video-overlay')) onDone(!cancellable);
+ }}>
+ <form class="music-detail playlist-modal" role="alertdialog" aria-modal="true"
+ onKeyDown=${onKeyDown}
+ onSubmit=${(e) => { e.preventDefault(); onDone(true); }}>
+ <div class="playlist-modal-body">
+ <div class="ask-message">${message}</div>
+ <div class="playlist-modal-actions">
+ ${cancellable && html`
+ <button type="button" class="tb-btn" onClick=${() => onDone(false)}>
+ ${t('dialog.cancel')}</button>`}
+ <button type="submit" class="admin-btn" ref=${okRef}>${t('dialog.ok')}</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ `;
+}
+
+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);