From e4a2e6143b918601f161a1aded164b601942b3e0 Mon Sep 17 00:00:00 2001 From: elvis kahoro Date: Sat, 24 Oct 2026 13:06:16 +0000 Subject: [PATCH] fix(dialog): declare on_close/on_cancel triggers so dismissible=False and on_open_change build --- assets/docs/components/dialog.md | 12 ++++++-- components/ui/dialog.py | 12 ++++++-- tests/test_dialog.py | 53 ++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 tests/test_dialog.py diff --git a/assets/docs/components/dialog.md b/assets/docs/components/dialog.md index c11199fd..28a7c03a 100644 --- a/assets/docs/components/dialog.md +++ b/assets/docs/components/dialog.md @@ -66,7 +66,10 @@ import uuid import reflex as rx from reflex.components.component import ComponentNamespace -from reflex_components_core.el import Div +from reflex_components_core.el import ( + Dialog as BaseDialog, + Div, +) from ..core.core import CoreComponent, cn @@ -130,7 +133,10 @@ class DialogTrigger(CoreComponent): return rx.el.div(*children, **props) -class DialogPopup(Div, CoreComponent): +class DialogPopup(BaseDialog, CoreComponent): + on_close: rx.EventHandler = None + on_cancel: rx.EventHandler = None + @classmethod def create( cls, @@ -177,7 +183,7 @@ class DialogPopup(Div, CoreComponent): else: props["on_mount"] = close_on_backdrop_script - return rx.el.dialog(*children, **props) + return super().create(*children, **props) class DialogClose(CoreComponent): diff --git a/components/ui/dialog.py b/components/ui/dialog.py index a1274f36..d7d8a1ee 100644 --- a/components/ui/dialog.py +++ b/components/ui/dialog.py @@ -2,7 +2,10 @@ import reflex as rx from reflex.components.component import ComponentNamespace -from reflex_components_core.el import Div +from reflex_components_core.el import ( + Dialog as BaseDialog, + Div, +) from ..core.core import CoreComponent, cn @@ -66,7 +69,10 @@ def create(cls, *children, **props) -> rx.Component: return rx.el.div(*children, **props) -class DialogPopup(Div, CoreComponent): +class DialogPopup(BaseDialog, CoreComponent): + on_close: rx.EventHandler = None + on_cancel: rx.EventHandler = None + @classmethod def create( cls, @@ -113,7 +119,7 @@ def create( else: props["on_mount"] = close_on_backdrop_script - return rx.el.dialog(*children, **props) + return super().create(*children, **props) class DialogClose(CoreComponent): diff --git a/tests/test_dialog.py b/tests/test_dialog.py new file mode 100644 index 00000000..6d50402c --- /dev/null +++ b/tests/test_dialog.py @@ -0,0 +1,53 @@ +"""Regression coverage for the dialog popup crash when ``dismissible=False`` +or ``on_open_change=...`` was passed to ``dialog.popup(...)``. + +Both options previously raised at component-construction time: + + ValueError: The Dialog does not take in an 'on_cancel' event trigger + ValueError: The Dialog does not take in an 'on_close' event trigger + +because ``DialogPopup`` wired ``on_cancel`` / ``on_close`` into ``props`` and +passed them to ``rx.el.dialog(...)``, whose underlying element does not declare +those triggers. The tests pin the four contracts the fix restored: +``DialogPopup`` declares ``on_close`` / ``on_cancel`` as class-level triggers, +the popup renders a native ```` with ``data-slot="dialog-content"``, +``dismissible=False`` wires ``on_cancel`` only, and ``on_open_change=`` wires +``on_close`` only. See bug #4 and PR #23. +""" + +import reflex as rx +from reflex_components_core.el.elements.other import Dialog + +from components.ui.dialog import DialogPopup, dialog + + +class _DialogState(rx.State): # pyright: ignore [reportUntypedBaseClass] + open: bool = True + + def set_open(self, *, value: bool) -> None: + self.open = value + + +def test_dialog_popup_declares_on_close_and_on_cancel_triggers(): + triggers = DialogPopup.get_event_triggers() + assert "on_close" in triggers + assert "on_cancel" in triggers + + +def test_popup_is_a_dialog_element_with_data_slot(): + popup = dialog.popup(dialog.title("title"), dismissible=False) + assert isinstance(popup, Dialog) + assert popup.tag == "dialog" + assert popup.custom_attrs.get("data-slot") == "dialog-content" + + +def test_dismissible_false_wires_on_cancel(): + popup = dialog.popup(dialog.title("title"), dismissible=False) + assert "on_cancel" in popup.event_triggers + assert "on_close" not in popup.event_triggers + + +def test_on_open_change_wires_on_close(): + popup = dialog.popup(dialog.title("title"), on_open_change=_DialogState.set_open) + assert "on_close" in popup.event_triggers + assert "on_cancel" not in popup.event_triggers