From 2e7f97c8fe074939c74d474ee73003e4bab492c8 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Wed, 22 Apr 2026 20:13:23 +0530 Subject: [PATCH] Fix BarWidget CSS leakage by scoping selectors Scope BarWidget CSS classes under a widget-specific container and localize control button queries to the widget container. Add regression tests to prevent global selector and document-level query regressions. Closes #26. Signed-off-by: Mridankan Mandal --- drawdata/static/bar_widget.css | 24 ++++++++++++------------ drawdata/static/bar_widget.js | 15 ++++++++------- js/bar_widget.js | 15 ++++++++------- tests/test_bar_widget_scoping.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 26 deletions(-) create mode 100644 tests/test_bar_widget_scoping.py diff --git a/drawdata/static/bar_widget.css b/drawdata/static/bar_widget.css index 8a97741..e2afe73 100644 --- a/drawdata/static/bar_widget.css +++ b/drawdata/static/bar_widget.css @@ -1,54 +1,54 @@ -.container { +.dd-bar-container { max-width: 800px; margin: 20px auto; font-family: Arial, sans-serif; } -.controls { +.dd-bar-controls { margin-bottom: 20px; } -.chart-container { +.dd-bar-container .chart-container { border: 1px solid #ccc; margin-bottom: 20px; } -button.control { +button.dd-bar-control { padding: 8px 16px; margin: 0 5px; cursor: pointer; background-color: #eee; border: 1px solid #ccc; } -button.reset { +button.dd-bar-reset { padding: 8px 16px; margin: 0 5px; cursor: pointer; background-color: #eee; border: 1px solid #ccc; } -.active { +.dd-bar-active { border: 4px solid #333; background-color: #ddd; font-weight: bold; } -input[type="number"] { +.dd-bar-container input[type="number"] { width: 80px; padding: 5px; margin: 0 5px; } -label { +.dd-bar-container label { margin-right: 10px; } -.bar { +.dd-bar-container .bar { opacity: 0.6; transition: opacity 0.2s; } -.bar:hover { +.dd-bar-container .bar:hover { opacity: 0.8; } -.grid line { +.dd-bar-container .grid line { stroke: #ddd; stroke-opacity: 0.7; } -.domain { +.dd-bar-container .domain { stroke: #000; stroke-width: 1px; } diff --git a/drawdata/static/bar_widget.js b/drawdata/static/bar_widget.js index 3b47cf5..60b4757 100644 --- a/drawdata/static/bar_widget.js +++ b/drawdata/static/bar_widget.js @@ -9557,12 +9557,13 @@ function render({ model, el }) { updateDataOut(); getBins(); let container = document.createElement("div"); + container.setAttribute("class", "dd-bar-container"); let controls = document.createElement("div"); - controls.setAttribute("class", "controls"); + controls.setAttribute("class", "dd-bar-controls"); if (Object.keys(collections).length > 1) { Object.keys(collections).forEach((key) => { let btn = document.createElement("button"); - btn.setAttribute("class", "control"); + btn.setAttribute("class", "dd-bar-control"); btn.style.position = "relative"; btn.style.paddingLeft = "30px"; let circle = document.createElement("span"); @@ -9587,7 +9588,7 @@ function render({ model, el }) { }); } let clear_btn = document.createElement("button"); - clear_btn.setAttribute("class", "reset"); + clear_btn.setAttribute("class", "dd-bar-reset"); clear_btn.innerHTML = "Clear"; if (Object.keys(collections).length > 1) { controls.appendChild(clear_btn); @@ -9683,10 +9684,10 @@ function render({ model, el }) { chartArea.on("mouseleave", () => { isDrawing = false; }); - document.querySelectorAll("button.control").forEach((button) => { + container.querySelectorAll("button.dd-bar-control").forEach((button) => { button.addEventListener("click", () => { - document.querySelectorAll("button.control").forEach((b) => b.classList.remove("active")); - button.classList.add("active"); + container.querySelectorAll("button.dd-bar-control").forEach((b) => b.classList.remove("dd-bar-active")); + button.classList.add("dd-bar-active"); activeCollection = button.querySelector("span").textContent; }); }); @@ -9697,7 +9698,7 @@ function render({ model, el }) { updateChart(); }); if (Object.keys(collections).length > 1) { - document.querySelector("button.control").click(); + container.querySelector("button.dd-bar-control").click(); } updateChart(); } diff --git a/js/bar_widget.js b/js/bar_widget.js index 43d4008..be4b92b 100644 --- a/js/bar_widget.js +++ b/js/bar_widget.js @@ -30,15 +30,16 @@ function render({ model, el }) { // Create SVG getBins(); let container = document.createElement("div"); + container.setAttribute("class", "dd-bar-container"); let controls = document.createElement("div"); - controls.setAttribute("class", "controls"); + controls.setAttribute("class", "dd-bar-controls"); // Only add collection buttons if there are multiple collections if (Object.keys(collections).length > 1) { Object.keys(collections).forEach(key => { let btn = document.createElement("button"); - btn.setAttribute("class", "control") + btn.setAttribute("class", "dd-bar-control") btn.style.position = "relative"; btn.style.paddingLeft = "30px"; let circle = document.createElement("span"); @@ -64,7 +65,7 @@ function render({ model, el }) { } // Add a button to clear the chart let clear_btn = document.createElement("button"); - clear_btn.setAttribute("class", "reset"); + clear_btn.setAttribute("class", "dd-bar-reset"); clear_btn.innerHTML = "Clear"; if (Object.keys(collections).length > 1) { controls.appendChild(clear_btn); @@ -249,10 +250,10 @@ function render({ model, el }) { }); // Handle collection selection - document.querySelectorAll('button.control').forEach(button => { + container.querySelectorAll('button.dd-bar-control').forEach(button => { button.addEventListener('click', () => { - document.querySelectorAll('button.control').forEach(b => b.classList.remove('active')); - button.classList.add('active'); + container.querySelectorAll('button.dd-bar-control').forEach(b => b.classList.remove('dd-bar-active')); + button.classList.add('dd-bar-active'); activeCollection = button.querySelector('span').textContent; }); }); @@ -266,7 +267,7 @@ function render({ model, el }) { }); if (Object.keys(collections).length > 1) { - document.querySelector('button.control').click(); + container.querySelector('button.dd-bar-control').click(); } // Initialize chart diff --git a/tests/test_bar_widget_scoping.py b/tests/test_bar_widget_scoping.py new file mode 100644 index 0000000..94ea39e --- /dev/null +++ b/tests/test_bar_widget_scoping.py @@ -0,0 +1,29 @@ +from pathlib import Path +import re + + +def test_bar_widget_css_is_scoped_to_widget_container(): + css = Path("drawdata/static/bar_widget.css").read_text(encoding="utf-8") + + # Ensure we expose a widget-specific root class for all styling. + assert ".dd-bar-container {" in css + + leaked_global_selectors = [ + r"^\.container\s*\{", + r"^\.controls\s*\{", + r"^\.active\s*\{", + r"^label\s*\{", + r"^input\[type=\"number\"\]\s*\{", + ] + + for pattern in leaked_global_selectors: + assert re.search(pattern, css, flags=re.MULTILINE) is None + + +def test_bar_widget_button_selection_is_container_scoped(): + js = Path("drawdata/static/bar_widget.js").read_text(encoding="utf-8") + + assert 'container.querySelectorAll("button.dd-bar-control")' in js + assert 'container.querySelector("button.dd-bar-control")' in js + assert 'document.querySelectorAll("button.control")' not in js + assert 'document.querySelector("button.control")' not in js