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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user