\n",
"
\n",
" \n",
@@ -52,6 +55,7 @@
" \n",
" \n",
"
\n",
+ " \n",
" ''').tag(sync=True)\n",
" \n",
" components=Dict({\n",
@@ -113,9 +117,11 @@
" label = Unicode().tag(sync=True)\n",
" \n",
" template = Unicode('''\n",
+ "
\n",
" \n",
" [{{ label }}] Db URL: {{ info }}\n",
"
\n",
+ " \n",
" ''').tag(sync=True)\n",
" \n",
" @observe('db')\n",
@@ -135,6 +141,7 @@
" supplier_db_collection = Dict().tag(sync_ref=True)\n",
" \n",
" template = Unicode('''\n",
+ "
\n",
" \n",
" \n",
" \n",
@@ -146,6 +153,7 @@
" \n",
" \n",
"
\n",
+ " \n",
" ''').tag(sync=True)\n",
" \n",
" components = Dict({\n",
diff --git a/examples/DeepWatch.ipynb b/examples/DeepWatch.ipynb
index 7fcc66a..f090e7c 100644
--- a/examples/DeepWatch.ipynb
+++ b/examples/DeepWatch.ipynb
@@ -15,6 +15,7 @@
" deep_array = traitlets.List(['array']).tag(sync=True)\n",
" deep_array2 = traitlets.List([{'prop': 'array2'}]).tag(sync=True)\n",
" template = traitlets.Unicode('''\n",
+ "
\n",
" \n",
" \n",
" \n",
@@ -27,6 +28,7 @@
" {{ deep_array[0] }} |\n",
" {{ deep_array2[0].prop }}\n",
"
\n",
+ " \n",
" ''').tag(sync=True)\n",
" \n",
"md = MyDeep()\n",
diff --git a/examples/EmbeddingJupyterWidgetsInVueTemplate.ipynb b/examples/EmbeddingJupyterWidgetsInVueTemplate.ipynb
index fd03f42..1d4f73f 100644
--- a/examples/EmbeddingJupyterWidgetsInVueTemplate.ipynb
+++ b/examples/EmbeddingJupyterWidgetsInVueTemplate.ipynb
@@ -29,12 +29,14 @@
"\n",
"\n",
" template=Unicode(\"\"\"\n",
- "
\n",
- "
\n",
- " {{ item.title }}:
\n",
+ "
\n",
+ " \n",
+ "
\n",
+ " {{ item.title }}: \n",
+ "
\n",
+ " Single widget:
\n",
"
\n",
- " Single widget: \n",
- " \n",
+ " \n",
" \"\"\").tag(sync=True)\n",
"\n",
"my_component = MyComponent()\n",
diff --git a/examples/ScopedCSS.ipynb b/examples/ScopedCSS.ipynb
index 3f04f0e..3f4ec3b 100644
--- a/examples/ScopedCSS.ipynb
+++ b/examples/ScopedCSS.ipynb
@@ -6,9 +6,9 @@
"source": [
"# Scoped CSS\n",
"\n",
- "By default, CSS in ipyvue templates is **global** — it affects all elements on the page with matching selectors. Scoped CSS limits styles to the component that defines them.\n",
+ "CSS in ipyvue templates is global unless it is marked with `
+ """
+
+
+def test_template_style_update_replaces_old_styles(
+ solara_test, page_session: playwright.sync_api.Page
+):
+ widget = StyledTemplate()
+
+ display(widget)
+
+ target = page_session.locator(".style-leak-target")
+ target.wait_for()
+ expect_red = """
+ () => {
+ const el = document.querySelector('.style-leak-target');
+ return el && getComputedStyle(el).color === 'rgb(255, 0, 0)';
+ }
+ """
+ page_session.wait_for_function(expect_red)
+
+ def update_template():
+ widget.template = """
+
+ Unstyled
+
+ """
+
+ threading.Timer(0.5, update_template).start()
+
+ page_session.locator("text=Unstyled").wait_for()
+ page_session.wait_for_function(
+ """
+ () => {
+ const el = document.querySelector('.style-leak-target');
+ return el && getComputedStyle(el).color !== 'rgb(255, 0, 0)';
+ }
+ """
+ )
+
+
class MyTemplateScript(vue.VueTemplate):
clicks = Int(0).tag(sync=True)
@@ -124,8 +208,6 @@ def test_template_script(
ipywidgets_runner, page_session: playwright.sync_api.Page, template_class_name
):
def kernel_code(template_class_name=template_class_name):
- # this import is need so when this code executes in the kernel,
- # the class is imported
from test_template import MyTemplateScript, MyTemplateScriptOld
template_class = {
@@ -167,7 +249,7 @@ def kernel_code():
import ipywidgets as widgets
from IPython.display import display
- scoped = ScopedStyleTemplate(scoped_css_support=True)
+ scoped = ScopedStyleTemplate()
unscoped = vue.Html(
tag="span",
children=["Unscoped text"],
@@ -187,46 +269,3 @@ def kernel_code():
)
assert scoped_color == "rgb(255, 0, 0)"
assert unscoped_color != "rgb(255, 0, 0)"
-
-
-class ScopedCssTemplate(vue.VueTemplate):
- @default("template")
- def _default_vue_template(self):
- return """
-
- Scoped css text
-
- """
-
-
-def test_template_scoped_css_trait(
- ipywidgets_runner, page_session: playwright.sync_api.Page
-):
- def kernel_code():
- from test_template import ScopedCssTemplate
- import ipyvue as vue
- import ipywidgets as widgets
- from IPython.display import display
-
- scoped = ScopedCssTemplate(
- css=".scoped-css-text { color: rgb(0, 128, 0); }", scoped=True
- )
- unscoped = vue.Html(
- tag="span",
- children=["Unscoped css text"],
- class_="scoped-css-text",
- attributes={"id": "unscoped-css-text"},
- )
- display(widgets.VBox([scoped, unscoped]))
-
- ipywidgets_runner(kernel_code)
- page_session.locator("#scoped-css-text").wait_for()
- page_session.locator("#unscoped-css-text").wait_for()
- scoped_color = page_session.eval_on_selector(
- "#scoped-css-text", "el => getComputedStyle(el).color"
- )
- unscoped_color = page_session.eval_on_selector(
- "#unscoped-css-text", "el => getComputedStyle(el).color"
- )
- assert scoped_color == "rgb(0, 128, 0)"
- assert unscoped_color != "rgb(0, 128, 0)"
diff --git a/tests/ui/test_v_bind.py b/tests/ui/test_v_bind.py
new file mode 100644
index 0000000..779f60e
--- /dev/null
+++ b/tests/ui/test_v_bind.py
@@ -0,0 +1,41 @@
+import pytest
+import sys
+
+if sys.version_info < (3, 7):
+ pytest.skip("requires python3.7 or higher", allow_module_level=True)
+
+import playwright.sync_api
+
+from IPython.display import display
+
+
+@pytest.mark.parametrize("ipywidgets_runner", ["solara"], indirect=True)
+def test_v_bind_supports_vuetify3_activator_props(
+ ipywidgets_runner,
+ page_session: playwright.sync_api.Page,
+):
+ def kernel_code():
+ import ipyvuetify as v
+
+ tooltip = v.Tooltip(
+ location="bottom",
+ v_slots=[
+ {
+ "name": "activator",
+ "variable": "tooltip",
+ "children": v.Btn(
+ v_bind="tooltip.props",
+ children=["Hover me"],
+ class_="v-bind-tooltip-button",
+ ),
+ }
+ ],
+ children=["Tooltip via v_bind"],
+ )
+
+ display(tooltip)
+
+ ipywidgets_runner(kernel_code)
+ page_session.locator(".v-bind-tooltip-button").wait_for()
+ page_session.locator(".v-bind-tooltip-button").hover()
+ page_session.get_by_text("Tooltip via v_bind", exact=True).wait_for()
diff --git a/tests/ui/test_v_on.py b/tests/ui/test_v_on.py
new file mode 100644
index 0000000..0479517
--- /dev/null
+++ b/tests/ui/test_v_on.py
@@ -0,0 +1,59 @@
+import pytest
+import sys
+
+if sys.version_info < (3, 7):
+ pytest.skip("requires python3.7 or higher", allow_module_level=True)
+
+import playwright.sync_api
+
+
+@pytest.mark.parametrize("ipywidgets_runner", ["solara"], indirect=True)
+def test_v_on_supports_nested_slot_scope_paths_and_vuetify3_props_fallback(
+ ipywidgets_runner,
+ page_session: playwright.sync_api.Page,
+):
+ ipywidgets_runner(lambda: None)
+ page_session.wait_for_function("window.requirejs !== undefined")
+
+ resolved = page_session.evaluate(
+ """() => new Promise((resolve, reject) => {
+ requirejs(["jupyter-vue"], (jupyterVue) => {
+ try {
+ const nested = jupyterVue.getScope("scopeData.nested", {
+ scopeData: {
+ nested: {
+ onClick: "nested-click",
+ onMouseenter: "nested-hover",
+ },
+ },
+ });
+ const tooltip = jupyterVue.getScope("tooltip.on", {
+ tooltip: {
+ props: {
+ onMouseenter: "tooltip-hover",
+ id: "ignored-non-event-prop",
+ },
+ },
+ });
+
+ resolve({
+ nestedKeys: Object.keys(nested || {}).sort(),
+ nestedClick: nested && nested.onClick,
+ tooltipKeys: Object.keys(tooltip || {}).sort(),
+ tooltipHover: tooltip && tooltip.onMouseenter,
+ tooltipId: tooltip && tooltip.id,
+ });
+ } catch (error) {
+ reject(error);
+ }
+ }, reject);
+ })"""
+ )
+
+ assert resolved == {
+ "nestedKeys": ["onClick", "onMouseenter"],
+ "nestedClick": "nested-click",
+ "tooltipKeys": ["onMouseenter"],
+ "tooltipHover": "tooltip-hover",
+ "tooltipId": None,
+ }
diff --git a/tests/unit/test_vue_widget.py b/tests/unit/test_vue_widget.py
index 9e3b52c..35bc963 100644
--- a/tests/unit/test_vue_widget.py
+++ b/tests/unit/test_vue_widget.py
@@ -77,3 +77,11 @@ def test_event_handling():
event_handler.reset_mock()
button._handle_event(None, dict(event="click.stop", data={"foo": "bar"}), [])
event_handler.assert_called_once_with(button, "click.stop", {"foo": "bar"})
+
+ event_handler.reset_mock()
+ button._handle_event(None, dict(event="click.stop", data=0), [])
+ event_handler.assert_called_once_with(button, "click.stop", 0)
+
+ event_handler.reset_mock()
+ button.click(False)
+ event_handler.assert_called_once_with(button, "click.stop", False)