fix(sidebar): correct testid + JS swap so +/− actually renders

The prior attempt used data-testid=stSidebarNavSectionHeader, which is
not what Streamlit 1.57 emits — the correct testid is stNavSectionHeader
(verified against the bundled JS in streamlit/static/static/js/).
The section header is also a <div> with onClick, not a <button>, and
the React component keeps the expanded state in a prop without
surfacing aria-expanded on the DOM. Pure CSS can therefore neither
locate the header nor switch the glyph by state, which is why the
chevron was unchanged in the rendered UI.

Switch strategies:
- CSS now targets the correct stNavSectionHeader / stIconMaterial
  selectors, drops the Material Symbols font from the icon span, and
  restyles it so a plain ascii character reads as proper typography
  (size, weight, color, hover).
- Add _SWAP_NAV_SECTION_INDICATOR_JS — small inline script that
  rewrites the icon's text node from "expand_more"/"expand_less" to
  "+"/"−" (U+2212), throttled via requestAnimationFrame, re-applied
  on every DOM mutation by a MutationObserver. Bundled into the same
  iframe injection as the existing brand/upload/findings scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 17:52:47 +00:00
parent 209b5fb1aa
commit fe4b5dc755

View File

@@ -336,51 +336,36 @@ body, .stApp {
}
/* ---------- Section header expand indicator ----------
Swap Streamlit's default ``expand_more`` chevron in each sidebar
section header for a soft ``+`` / ```` pair. ``+`` when collapsed,
```` (U+2212, visually balanced with +) when expanded.
We hide any built-in icon Streamlit injects (svg or Material
Symbols span — exact element differs by Streamlit version) and
render our own glyph as a right-aligned pseudo-element keyed off
``aria-expanded``. The button itself gets a touch of right padding
so the label doesn't run into the indicator. */
[data-testid="stSidebarNavSectionHeader"] {
Streamlit's nav section header uses a Material Symbols ligature
icon (``expand_more`` / ``expand_less``) and does NOT expose
``aria-expanded`` on the header — the React component keeps that
state internally. Pure CSS therefore can't switch the glyph based
on state, so the visible swap is performed by
``_SWAP_NAV_SECTION_INDICATOR_JS`` (rewrites the icon's text node
to ``+`` / ```` and re-applies on mutation). This block only
handles the static styling so the rewritten glyph reads as a
normal typographic plus/minus instead of a Material font ligature
that would still try to resolve ``+`` as an icon name. */
[data-testid="stNavSectionHeader"] {
position: relative !important;
}
[data-testid="stSidebarNavSectionHeader"] button {
width: 100% !important;
padding-right: 22px !important;
background: transparent !important;
border: none !important;
[data-testid="stNavSectionHeader"] [data-testid="stIconMaterial"] {
/* Drop the Material Symbols font so the JS-swapped ``+`` / ````
characters render as plain typography. ``font-feature-settings``
is reset so no ligature kicks in. */
font-family: var(--font-sans) !important;
font-feature-settings: normal !important;
-webkit-font-feature-settings: normal !important;
-moz-font-feature-settings: normal !important;
font-weight: 500 !important;
font-size: 16px !important;
line-height: 1 !important;
color: var(--ink-tertiary) !important;
width: auto !important;
height: auto !important;
transition: color 0.15s ease !important;
}
[data-testid="stSidebarNavSectionHeader"] button svg,
[data-testid="stSidebarNavSectionHeader"] button [data-testid="stIconMaterial"],
[data-testid="stSidebarNavSectionHeader"] button span.material-symbols-rounded,
[data-testid="stSidebarNavSectionHeader"] button span.material-symbols-outlined,
[data-testid="stSidebarNavSectionHeader"] button span.material-icons,
[data-testid="stSidebarNavSectionHeader"] button span.material-icons-outlined {
display: none !important;
}
[data-testid="stSidebarNavSectionHeader"] button[aria-expanded]::after {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
font-weight: 400;
line-height: 1;
color: var(--ink-tertiary);
transition: color 0.15s ease;
pointer-events: none;
}
[data-testid="stSidebarNavSectionHeader"] button[aria-expanded="false"]::after {
content: "+";
}
[data-testid="stSidebarNavSectionHeader"] button[aria-expanded="true"]::after {
content: "\2212";
}
[data-testid="stSidebarNavSectionHeader"] button:hover::after {
[data-testid="stNavSectionHeader"]:hover [data-testid="stIconMaterial"] {
color: var(--ink) !important;
}
@@ -1161,6 +1146,47 @@ _WIRE_COLLAPSIBLE_FINDINGS_JS = """
"""
_SWAP_NAV_SECTION_INDICATOR_JS = """
<script>
(function () {
// Replace Streamlit's ``expand_more`` / ``expand_less`` Material
// ligature in sidebar nav section headers with plain ``+`` / ````.
// The section header isn't a button and doesn't carry
// ``aria-expanded``, so a pure-CSS swap can't switch the glyph
// based on state — we walk the icon's text node directly.
function swap(doc) {
var headers = doc.querySelectorAll('[data-testid="stNavSectionHeader"]');
headers.forEach(function (h) {
var icon = h.querySelector('[data-testid="stIconMaterial"]');
if (!icon) return;
var text = (icon.textContent || '').trim();
var glyph = null;
if (text === 'expand_more') glyph = '+';
else if (text === 'expand_less') glyph = ''; // U+2212
else if (text === '+' || text === '') return; // already swapped
else return;
icon.textContent = glyph;
});
}
var doc;
try { doc = window.parent.document; }
catch (e) { doc = document; }
swap(doc);
var win = doc.defaultView || window.parent || window;
if ('MutationObserver' in win) {
var raf = 0;
try {
new win.MutationObserver(function () {
if (raf) return;
raf = win.requestAnimationFrame(function () { raf = 0; swap(doc); });
}).observe(doc.body, { childList: true, subtree: true, characterData: true });
} catch (e) {}
}
})();
</script>
"""
_RENAME_UPLOAD_BUTTON_JS = """
<script>
(function () {
@@ -1234,7 +1260,8 @@ def hide_streamlit_chrome(*, gate_license: bool = True) -> None:
st.iframe(
_INJECT_BRAND_JS
+ _RENAME_UPLOAD_BUTTON_JS
+ _WIRE_COLLAPSIBLE_FINDINGS_JS,
+ _WIRE_COLLAPSIBLE_FINDINGS_JS
+ _SWAP_NAV_SECTION_INDICATOR_JS,
height=1,
)
# Stamp a session-start record into the audit log the first time