From d1aaf3c2b93cece76189811d23c2a74f82cd5d1d Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 May 2026 20:59:17 +0000 Subject: [PATCH] feat(quit): close-window button + manual hint on the farewell overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The farewell overlay already attempted ``window.top.close()`` after a Close click — but browsers only honour that for tabs that JS opened (Chrome --app windows qualify; a regular browser tab does not). For users whose Chrome wasn't auto-detected and who fall back to ``webbrowser.open``, the overlay stays put and they had no in-page way to close. Add to the overlay HTML: - A "Close this window" button (uses the user-gesture path, which has slightly looser browser rules than auto-close). - A hidden hint paragraph that reveals itself 250 ms after the button is clicked IF the window is still here, telling the user to press Ctrl+W (⌘W on Mac). Wired through the existing _farewell_script template + ``_js_html_safe`` escaping so neither label can break out of the JS string literal. New i18n keys (en + es): ``quit.close_window_button`` and ``quit.close_hint``. The existing auto-close attempt remains — Chrome --app users still get their window closed without touching the button. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/components/_legacy.py | 42 +++++++++++++++++++++++++++++++---- src/i18n/packs/en.json | 4 +++- src/i18n/packs/es.json | 4 +++- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index 9a9bcb5..aeb8d9b 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -128,6 +128,13 @@ _FAREWELL_SCRIPT_TEMPLATE = """ // // We don't navigate to a data: URL here because Chrome blocks // top-frame navigation to data: URLs (anti-phishing, Chrome 60+). + // + // The overlay carries a manual "Close this window" button as a + // fallback for browsers that block the auto-close. ``window.close()`` + // only succeeds when the tab was JS-opened (Chrome --app windows + // qualify; regular browser tabs do not) — the button click counts + // as a user gesture and gives one more chance. A short timeout + // reveals a Ctrl+W hint if the browser still refused. function buildOverlay(doc) { var overlay = doc.createElement('div'); overlay.id = 'datatools-farewell-overlay'; @@ -140,22 +147,47 @@ _FAREWELL_SCRIPT_TEMPLATE = """ 'border-radius:12px;background:#161922;max-width:480px;">' + '

' + '__TITLE__

' + - '

__SUBTITLE__

' + + '

__SUBTITLE__

' + + '' + + '

' + + '__CLOSE_HINT__

' + ''; return overlay; } + function wireClose(doc, win) { + var btn = doc.getElementById('datatools-close-btn'); + if (!btn) return; + btn.onclick = function () { + try { win.close(); } catch (e) {} + try { win.top.close(); } catch (e) {} + // If after 250ms the window is still here, the browser + // blocked the close — show the manual-close hint. + setTimeout(function () { + var hint = doc.getElementById('datatools-close-hint'); + if (hint) hint.style.display = 'block'; + }, 250); + }; + } try { var doc = window.top.document; + var win = window.top; if (!doc.getElementById('datatools-farewell-overlay')) { doc.body.appendChild(buildOverlay(doc)); } - // Try to close the tab outright too — browsers only honour this - // for tabs a script opened, so it usually no-ops, but it's free. - try { window.top.close(); } catch (e) {} + wireClose(doc, win); + // Try to close the tab outright too — succeeds in Chrome --app + // windows, no-ops on regular tabs. + try { win.close(); } catch (e) {} } catch (e) { // Cross-origin access denied (shouldn't happen given Streamlit's // sandbox flags, but fall back gracefully): cover this iframe. document.body.appendChild(buildOverlay(document)); + wireClose(document, window); } })(); @@ -184,6 +216,8 @@ def _farewell_script() -> str: _FAREWELL_SCRIPT_TEMPLATE .replace("__TITLE__", _js_html_safe(_t("quit.farewell_title"))) .replace("__SUBTITLE__", _js_html_safe(_t("quit.farewell_subtitle"))) + .replace("__CLOSE_BTN__", _js_html_safe(_t("quit.close_window_button"))) + .replace("__CLOSE_HINT__", _js_html_safe(_t("quit.close_hint"))) ) diff --git a/src/i18n/packs/en.json b/src/i18n/packs/en.json index 4ac853d..69c525f 100644 --- a/src/i18n/packs/en.json +++ b/src/i18n/packs/en.json @@ -51,7 +51,9 @@ "button": "Quit app", "shutting_down": "Shutting down… you can close this window.", "farewell_title": "DataTools has shut down", - "farewell_subtitle": "You can close this window." + "farewell_subtitle": "You can close this window.", + "close_window_button": "Close this window", + "close_hint": "Your browser blocked the close. Press Ctrl+W (or ⌘W on Mac) to close this tab manually." }, "close_page": { "page_title": "DataTools — Close", diff --git a/src/i18n/packs/es.json b/src/i18n/packs/es.json index 75a20c6..63279b9 100644 --- a/src/i18n/packs/es.json +++ b/src/i18n/packs/es.json @@ -51,7 +51,9 @@ "button": "Cerrar app", "shutting_down": "Cerrando… ya puedes cerrar esta ventana.", "farewell_title": "DataTools se ha cerrado", - "farewell_subtitle": "Ya puedes cerrar esta ventana." + "farewell_subtitle": "Ya puedes cerrar esta ventana.", + "close_window_button": "Cerrar esta ventana", + "close_hint": "Tu navegador bloqueó el cierre. Presiona Ctrl+W (o ⌘W en Mac) para cerrar esta pestaña manualmente." }, "close_page": { "page_title": "DataTools — Cerrar",