-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsidebar_toggle.py
More file actions
201 lines (172 loc) · 8.08 KB
/
Copy pathsidebar_toggle.py
File metadata and controls
201 lines (172 loc) · 8.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"""Browser-side repair for Streamlit's persisted sidebar state.
WebQA uses Streamlit's native sidebar. The browser bridge has two jobs:
1. On the first visit in a browser tab, clear stale sidebar layout preferences
that can leave a hosted/fullscreen Streamlit app collapsed. The page is
reloaded once so ``initial_sidebar_state="expanded"`` can take effect.
2. Guarantee a working reopen affordance once the sidebar is collapsed.
On (2): earlier versions of this fix tried to force Streamlit's own native
reopen control visible via CSS/JS style overrides. That approach depends on
guessing which exact data-testid and DOM ancestry the installed Streamlit
build uses for that control, and silently breaks if the guess is wrong or an
ancestor element (not just the control itself) is what's hidden/clipped —
which is very plausibly what's happening here, since two rounds of
selector/!important fixes on the control itself did not resolve it.
Instead, this renders a small button that WE fully own (appended directly to
the parent document body, entirely outside Streamlit's own component tree,
so Streamlit's re-renders never touch or remove it) and is always visible
when the sidebar is collapsed, regardless of anything wrong with Streamlit's
native control. Clicking it fires a synthetic click on whichever native
toggle element currently exists in the DOM. A synthetic .click() on a
<button> fires its handlers even if that button is currently invisible,
zero-size, or nested inside a hidden ancestor — as long as it is still
present in the DOM and not disabled — so this works even if the native
control's visibility is broken for a reason we haven't identified.
"""
from __future__ import annotations
import streamlit.components.v1 as components
_TOGGLE_HTML = r"""
<script>
(() => {
const parentWindow = window.parent;
const doc = parentWindow.document;
// Streamlit persists sidebar/layout preferences in browser storage. A stale
// collapsed preference can survive a deployment and can reproduce the
// hosted-only behaviour where a fresh local run starts expanded but the same
// public URL starts collapsed. Reset sidebar-related preferences once per
// browser-tab session, then reload so Streamlit applies initial_sidebar_state.
const RECOVERY_FLAG = "webqa-sidebar-recovery-v2";
const recoverPersistedSidebarState = () => {
try {
if (parentWindow.sessionStorage.getItem(RECOVERY_FLAG) === "1") {
return;
}
const staleKeys = [];
for (let i = 0; i < parentWindow.localStorage.length; i += 1) {
const key = parentWindow.localStorage.key(i);
if (key && /sidebar/i.test(key)) {
staleKeys.push(key);
}
}
parentWindow.sessionStorage.setItem(RECOVERY_FLAG, "1");
if (staleKeys.length > 0) {
staleKeys.forEach((key) => parentWindow.localStorage.removeItem(key));
parentWindow.location.reload();
return true;
}
} catch (_) {
// Storage access can be blocked by privacy settings. In that case the
// native control repair below still runs and the app remains usable.
}
return false;
};
if (recoverPersistedSidebarState()) {
return;
}
const NATIVE_SELECTORS = [
'[data-testid="stSidebarCollapsedControl"] button',
'[data-testid="stSidebarCollapsedControl"]',
'[data-testid="stSidebarCollapseButton"] button',
'[data-testid="stSidebarCollapseButton"]',
'[data-testid="collapsedControl"] button',
'[data-testid="collapsedControl"]'
];
const findNativeToggle = () => {
for (const selector of NATIVE_SELECTORS) {
const el = doc.querySelector(selector);
if (el) return el;
}
return null;
};
const isSidebarCollapsed = () => {
const sidebar = doc.querySelector('section[data-testid="stSidebar"]');
// aria-expanded is the accessibility attribute Streamlit sets on the
// sidebar itself to reflect collapsed/expanded state; it is far less
// likely to change across Streamlit versions than internal testids.
if (sidebar && sidebar.getAttribute('aria-expanded') === 'false') return true;
if (sidebar && sidebar.getAttribute('aria-expanded') === 'true') return false;
// Fallback: no sidebar element found in the DOM at all reads as collapsed.
return !sidebar;
};
const BUTTON_ID = 'webqa-sidebar-reopen';
const ensureCustomButton = () => {
let btn = doc.getElementById(BUTTON_ID);
const collapsed = isSidebarCollapsed();
if (!collapsed) {
if (btn) btn.style.setProperty('display', 'none', 'important');
return;
}
if (!btn) {
btn = doc.createElement('button');
btn.id = BUTTON_ID;
btn.type = 'button';
btn.setAttribute('aria-label', 'Open sidebar');
btn.innerHTML = '›';
btn.addEventListener('click', () => {
const native = findNativeToggle();
const sidebarBefore = doc.querySelector('section[data-testid=\"stSidebar\"]');
const wasCollapsed = !sidebarBefore || sidebarBefore.getAttribute('aria-expanded') !== 'true';
if (native) native.click();
// Streamlit can occasionally consume the synthetic click without
// changing the collapsed state. Verify the result and recover by
// clearing the persisted sidebar preference and reloading once.
window.setTimeout(() => {
const sidebarAfter = doc.querySelector('section[data-testid=\"stSidebar\"]');
const expanded = sidebarAfter && sidebarAfter.getAttribute('aria-expanded') === 'true';
if (expanded || !wasCollapsed) return;
try {
const staleKeys = [];
for (let i = 0; i < parentWindow.localStorage.length; i += 1) {
const key = parentWindow.localStorage.key(i);
if (key && /sidebar/i.test(key)) staleKeys.push(key);
}
staleKeys.forEach((key) => parentWindow.localStorage.removeItem(key));
} catch (_) {}
parentWindow.location.reload();
}, 250);
});
doc.body.appendChild(btn);
}
// Inline styles here always take priority: this element is not managed
// by Streamlit's own stylesheet, so there is nothing else in the cascade
// to lose to, and !important keeps it that way defensively regardless.
btn.style.setProperty('position', 'fixed', 'important');
btn.style.setProperty('top', '0.6rem', 'important');
btn.style.setProperty('left', '0.6rem', 'important');
btn.style.setProperty('z-index', '999999', 'important');
btn.style.setProperty('display', 'flex', 'important');
btn.style.setProperty('align-items', 'center', 'important');
btn.style.setProperty('justify-content', 'center', 'important');
btn.style.setProperty('width', '2.2rem', 'important');
btn.style.setProperty('height', '2.2rem', 'important');
btn.style.setProperty('border-radius', '0.6rem', 'important');
btn.style.setProperty('border', '1px solid rgba(120,120,120,0.35)', 'important');
btn.style.setProperty('background', 'rgba(38,39,48,0.85)', 'important');
btn.style.setProperty('color', '#fafafa', 'important');
btn.style.setProperty('font-size', '1.2rem', 'important');
btn.style.setProperty('line-height', '1', 'important');
btn.style.setProperty('cursor', 'pointer', 'important');
btn.style.setProperty('box-shadow', '0 4px 14px rgba(0,0,0,0.25)', 'important');
btn.style.setProperty('opacity', '1', 'important');
btn.style.setProperty('visibility', 'visible', 'important');
btn.style.setProperty('pointer-events', 'auto', 'important');
};
const repairNative = () => {
NATIVE_SELECTORS.forEach((selector) => {
const target = doc.querySelector(selector);
if (!target) return;
target.style.setProperty('pointer-events', 'auto', 'important');
target.style.setProperty('z-index', '100001', 'important');
});
};
const tick = () => {
repairNative();
ensureCustomButton();
};
tick();
window.setInterval(tick, 800);
})();
</script>
"""
def render_sidebar_toggle() -> None:
"""Repair stale hosted sidebar state and guarantee a working reopen control."""
components.html(_TOGGLE_HTML, height=1, width=1)