fix(footer): restore soft-nav for Close (no page reload on shutdown)

Footer Close was using ``<a href="./close">`` which triggers a
browser hard-nav. That's a visible page-reload flash, websocket
churn, and slower shutdown than the previous sidebar Close —
which used ``st.navigation``'s soft nav.

Restore the soft-nav path:

- ``render_sticky_footer`` now renders a hidden ``st.page_link``
  pointing at ``pages/99_Close.py``. Positioned off-screen via
  CSS (``stElementContainer:has(a[data-testid=stPageLink]
  [href$=/close])``) so it occupies no layout space but stays in
  the DOM, reachable + clickable.
- Footer's Close <button> click handler now dispatches a
  programmatic click on that hidden page_link. Streamlit's React
  handler picks it up and runs the soft nav (same code path the
  old sidebar entry used). Falls back to ``window.location.href``
  if the helper link hasn't rendered yet so the button is never
  a no-op.
- The page_link call is wrapped in try/except: ``AppTest`` doesn't
  populate the page-nav session keys it needs and raises
  ``KeyError('url_pathname')``. Failure costs only the soft-nav
  optimization — Close still works via the hard-nav fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 15:52:00 +00:00
parent b2449d3139
commit 4a7f99f0ec

View File

@@ -728,11 +728,59 @@ def render_sticky_footer() -> None:
#datatools-help-popover .dt-help-dismiss:hover {
color: rgb(38, 39, 48) !important;
}
/* Hide the sticky-footer's helper st.page_link off-screen but
keep it in the DOM + clickable. The footer's Close button
dispatches a programmatic click on this link so navigation uses
Streamlit's soft nav (preserves the websocket, no visible page
reload) instead of the browser hard-nav an ``<a href="./close">``
would trigger. Off-screen (rather than ``display:none``) so
React event delegation works reliably across browsers. */
[data-testid="stElementContainer"]:has(a[data-testid="stPageLink"][href$="/close"]),
[data-testid="stElementContainer"]:has(a[data-testid="stPageLink"][href$="/close/"]) {
position: absolute !important;
left: -9999px !important;
top: -9999px !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* Defensive fallback for browsers without :has() — at least
shrink the inline page_link so it doesn't render a visible row. */
a[data-testid="stPageLink"][href$="/close"],
a[data-testid="stPageLink"][href$="/close/"] {
visibility: hidden !important;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
</style>
""",
unsafe_allow_html=True,
)
# Hidden Streamlit page_link to the close page. The footer's
# Close button programmatically clicks the anchor this renders,
# which triggers Streamlit's soft navigation (same code path
# the previous sidebar Close entry used). The link is positioned
# off-screen via the CSS above so it doesn't take page space
# but remains reachable to the JS click dispatch.
#
# Wrapped because ``st.page_link`` raises ``KeyError('url_pathname')``
# under ``AppTest`` (the test harness does not populate the page-nav
# session keys ``page_link`` needs to mark itself active/inactive).
# The JS click handler has a hard-nav fallback when this helper
# link isn't present, so a failure here only costs the soft-nav
# optimization — Close still works.
try:
st.page_link(
"pages/99_Close.py",
label=_t("footer.close"),
)
except Exception:
pass
from streamlit.components.v1 import html as _components_html
_components_html(
f"""
@@ -757,14 +805,32 @@ def render_sticky_footer() -> None:
helpBtn.className = 'datatools-footer-btn help';
helpBtn.textContent = labels.help;
var closeLink = doc.createElement('a');
closeLink.className = 'datatools-footer-btn close';
closeLink.href = './close';
closeLink.target = '_self';
closeLink.textContent = labels.close;
var closeBtn = doc.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'datatools-footer-btn close';
closeBtn.textContent = labels.close;
// Soft-nav via the hidden ``st.page_link`` that
// ``render_sticky_footer`` injects. Streamlit owns its click
// handler and will route through ``st.switch_page`` (same
// code path the old sidebar Close entry used) — no full-page
// reload, no websocket churn. Fall back to a hard nav if the
// helper link hasn't rendered yet (first paint race) so the
// button is never a no-op.
closeBtn.addEventListener('click', function (e) {{
e.preventDefault();
var helper = doc.querySelector(
'a[data-testid="stPageLink"][href$="/close"], ' +
'a[data-testid="stPageLink"][href$="/close/"]'
);
if (helper) {{
helper.click();
}} else {{
window.location.href = './close';
}}
}});
div.appendChild(helpBtn);
div.appendChild(closeLink);
div.appendChild(closeBtn);
var pop = doc.createElement('div');
pop.id = 'datatools-help-popover';