fix(close): Edge fallback + better tryClose + honest hint

There is no JavaScript override for browser tab-close security:
``window.close()`` only succeeds on windows JS opened (Chrome --app
windows qualify; a regular browser tab does not). What we can do is
make the --app path easier to hit and the failure case more
actionable.

Three changes:

1. ``src/gui/__main__.py`` — extend browser detection. PATH lookup
   now also looks for ``msedge`` / ``microsoft-edge``; Windows install
   candidates include the Edge install path; macOS candidates include
   Edge and Chromium. Edge is Chromium-based, supports ``--app``, and
   ships on every Windows 10+ machine — so users without Chrome no
   longer fall through to the regular browser tab. When the fallback
   IS hit, print a warning to stderr explaining why Close-from-page
   will require Ctrl+W. Renamed ``_find_chrome`` to
   ``_find_app_browser`` to reflect the broader scope.

2. ``_FAREWELL_SCRIPT_TEMPLATE`` in ``components/_legacy.py`` —
   factor close attempts into a ``tryClose`` helper that runs three
   escalating tries: standard ``win.close()``, the
   ``win.open('', '_self')`` history-rewrite trick (no-op in modern
   Chrome but free), and ``win.top.close()``. Auto-close on paint AND
   the manual button now both call this helper. Skip the manual hint
   if the close eventually succeeded between the click and the 250 ms
   timeout.

3. ``quit.close_hint`` in en/es i18n packs — rewrite the message to
   tell the user honestly that this is a browser security restriction,
   tell them the Ctrl+W keystroke that works, and point them at
   ``python -m src.gui`` for the auto-closing app-mode experience.

2008 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 21:17:18 +00:00
parent aeead05e4c
commit ef9f8b5de4
4 changed files with 78 additions and 22 deletions

View File

@@ -159,15 +159,33 @@ _FAREWELL_SCRIPT_TEMPLATE = """
'</div>';
return overlay;
}
function tryClose(win) {
// Three escalating attempts. Modern browsers only honour close()
// for windows that JS opened (Chrome --app windows qualify; a
// regular browser tab does not) — there is no JS override for
// that policy. The window.open('', '_self') trick used to rewrite
// a window's "opener" lineage and let close() through in older
// Chrome; modern Chrome patched it but it costs nothing to try.
try { win.close(); } catch (e) {}
if (win.closed) return;
try {
var w = win.open('', '_self', '');
if (w) {
try { w.close(); } catch (e) {}
}
} catch (e) {}
if (win.closed) return;
try { win.top.close(); } catch (e) {}
}
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) {}
tryClose(win);
// If after 250ms the window is still here, the browser
// blocked the close — show the manual-close hint.
setTimeout(function () {
if (win.closed) return;
var hint = doc.getElementById('datatools-close-hint');
if (hint) hint.style.display = 'block';
}, 250);
@@ -180,9 +198,9 @@ _FAREWELL_SCRIPT_TEMPLATE = """
doc.body.appendChild(buildOverlay(doc));
}
wireClose(doc, win);
// Try to close the tab outright too — succeeds in Chrome --app
// Auto-close attempt on first paint — succeeds in Chrome --app
// windows, no-ops on regular tabs.
try { win.close(); } catch (e) {}
tryClose(win);
} catch (e) {
// Cross-origin access denied (shouldn't happen given Streamlit's
// sandbox flags, but fall back gracefully): cover this iframe.