feat(quit): close-window button + manual hint on the farewell overlay

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 20:59:17 +00:00
parent 27f0648093
commit d1aaf3c2b9
3 changed files with 44 additions and 6 deletions

View File

@@ -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;">' +
'<h1 style="margin:0 0 8px 0;font-weight:600;letter-spacing:-0.01em;">' +
'__TITLE__</h1>' +
'<p style="opacity:0.7;margin:0;">__SUBTITLE__</p>' +
'<p style="opacity:0.7;margin:0 0 20px 0;">__SUBTITLE__</p>' +
'<button id="datatools-close-btn" style="' +
'background:#6ee7b7;color:#052e1a;font-weight:600;' +
'padding:10px 20px;border-radius:8px;border:none;' +
'font-size:15px;cursor:pointer;font-family:inherit;">' +
'__CLOSE_BTN__</button>' +
'<p id="datatools-close-hint" style="' +
'display:none;font-size:13px;opacity:0.6;margin:14px 0 0 0;">' +
'__CLOSE_HINT__</p>' +
'</div>';
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);
}
})();
</script>
@@ -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")))
)