diff --git a/pyi_hashes.json b/pyi_hashes.json index 30289779ee3..4da29e75127 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -47,8 +47,8 @@ "reflex/components/radix/primitives/__init__.pyi": "01c388fe7a1f5426a16676404344edf6", "reflex/components/radix/primitives/accordion.pyi": "19484eca0ad53f538f5db04c09921738", "reflex/components/radix/primitives/base.pyi": "9ef34884fb6028dc017df5e2db639c81", - "reflex/components/radix/primitives/dialog.pyi": "1bc7533791b07928ad1ade5f616b81d4", - "reflex/components/radix/primitives/drawer.pyi": "921e45dfaf5b9131ef27c561c3acca2e", + "reflex/components/radix/primitives/dialog.pyi": "13ec2d6d93e925427c68aa17adb1fdb1", + "reflex/components/radix/primitives/drawer.pyi": "b25a02ff81c03bcb08bf851f435fb84c", "reflex/components/radix/primitives/form.pyi": "78055e820703c98c3b838aa889566365", "reflex/components/radix/primitives/progress.pyi": "c917952d57ddb3e138a40c4005120d5e", "reflex/components/radix/primitives/slider.pyi": "4ff06f0025d47f166132909b09ab96f8", @@ -56,7 +56,7 @@ "reflex/components/radix/themes/base.pyi": "3e1ccd5ce5fef0b2898025193ee3d069", "reflex/components/radix/themes/color_mode.pyi": "dda570583355d8c0d8f607be457ba7a1", "reflex/components/radix/themes/components/__init__.pyi": "efa279ee05479d7bb8a64d49da808d03", - "reflex/components/radix/themes/components/alert_dialog.pyi": "eed422fcc1ff5ccf3dbf6934699bd0b1", + "reflex/components/radix/themes/components/alert_dialog.pyi": "dada6f9fd295509a94e4f6daa8b11072", "reflex/components/radix/themes/components/aspect_ratio.pyi": "71de4160d79840561c48b570197a4152", "reflex/components/radix/themes/components/avatar.pyi": "e40c2f0fda6d2c028d83681a27f3fb96", "reflex/components/radix/themes/components/badge.pyi": "58fd1a9c5d2f8762e2a0370311731ff5", @@ -68,12 +68,12 @@ "reflex/components/radix/themes/components/checkbox_group.pyi": "8638582a623036f8893a3fa6080f2672", "reflex/components/radix/themes/components/context_menu.pyi": "b9499d8bdd2c5565621fea5fe7d7a25a", "reflex/components/radix/themes/components/data_list.pyi": "6f8d9c582e084c23966b992158193b72", - "reflex/components/radix/themes/components/dialog.pyi": "d2615f1a68c80ff930444d054b598c13", + "reflex/components/radix/themes/components/dialog.pyi": "4a2ab11b4cdde3762797526fd7e5322e", "reflex/components/radix/themes/components/dropdown_menu.pyi": "43f8770c9adf93c73398d68f79048424", - "reflex/components/radix/themes/components/hover_card.pyi": "a96f4433237f9994decf935deff9f269", + "reflex/components/radix/themes/components/hover_card.pyi": "ded8d6f868311c55812bb056907ef7c7", "reflex/components/radix/themes/components/icon_button.pyi": "e930911d8ecbe61e5447e61c76a28ab6", "reflex/components/radix/themes/components/inset.pyi": "bd7a2186b553bd4c86d83ff50c784066", - "reflex/components/radix/themes/components/popover.pyi": "91f8edefeb232cc6d48690b1838144c2", + "reflex/components/radix/themes/components/popover.pyi": "9c8c1fe5571d4fb85c2f033d3262900a", "reflex/components/radix/themes/components/progress.pyi": "0e59587d5b3c8fe0d0067587f144e5b0", "reflex/components/radix/themes/components/radio.pyi": "f375aa5ac746679618ea7dad257e3224", "reflex/components/radix/themes/components/radio_cards.pyi": "9dc34a1ce2a1924eb1f41438ef84e80b", diff --git a/reflex/components/radix/primitives/dialog.py b/reflex/components/radix/primitives/dialog.py index 6a4cb24018f..a97b1c5e288 100644 --- a/reflex/components/radix/primitives/dialog.py +++ b/reflex/components/radix/primitives/dialog.py @@ -1,6 +1,6 @@ """Interactive components provided by @radix-ui/react-dialog.""" -from typing import Any +from typing import Any, ClassVar from reflex.components.component import ComponentNamespace from reflex.components.el import elements @@ -48,6 +48,8 @@ class DialogPortal(DialogElement): # Specify a container element to portal the content into. container: Var[Any] + _valid_parents: ClassVar[list[str]] = ["DialogRoot"] + class DialogOverlay(DialogElement): """A layer that covers the inert portion of the view when the dialog is open.""" @@ -61,6 +63,8 @@ class DialogOverlay(DialogElement): # Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. It inherits from Dialog.Portal. force_mount: Var[bool] + _valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogPortal"] + class DialogTrigger(DialogElement, RadixPrimitiveTriggerComponent): """Trigger an action or event, to open a Dialog modal.""" @@ -73,6 +77,8 @@ class DialogTrigger(DialogElement, RadixPrimitiveTriggerComponent): _memoization_mode = MemoizationMode(recursive=False) + _valid_parents: ClassVar[list[str]] = ["DialogRoot"] + class DialogContent(elements.Div, DialogElement): """Content component to display inside a Dialog modal.""" @@ -101,6 +107,8 @@ class DialogContent(elements.Div, DialogElement): # Fired when the pointer interacts outside the dialog. on_interact_outside: EventHandler[no_args_event_spec] + _valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogPortal"] + class DialogTitle(DialogElement): """Title component to display inside a Dialog modal.""" @@ -111,6 +119,12 @@ class DialogTitle(DialogElement): # Change the default rendered element for the one passed as a child, merging their props and behavior. as_child: Var[bool] + _valid_parents: ClassVar[list[str]] = [ + "DialogRoot", + "DialogPortal", + "DialogContent", + ] + class DialogDescription(DialogElement): """Description component to display inside a Dialog modal.""" @@ -121,6 +135,12 @@ class DialogDescription(DialogElement): # Change the default rendered element for the one passed as a child, merging their props and behavior. as_child: Var[bool] + _valid_parents: ClassVar[list[str]] = [ + "DialogRoot", + "DialogPortal", + "DialogContent", + ] + class DialogClose(DialogElement, RadixPrimitiveTriggerComponent): """Close button component to close an open Dialog modal.""" @@ -131,6 +151,12 @@ class DialogClose(DialogElement, RadixPrimitiveTriggerComponent): # Change the default rendered element for the one passed as a child, merging their props and behavior. as_child: Var[bool] + _valid_parents: ClassVar[list[str]] = [ + "DialogRoot", + "DialogPortal", + "DialogContent", + ] + class Dialog(ComponentNamespace): """Dialog components namespace.""" diff --git a/reflex/components/radix/primitives/drawer.py b/reflex/components/radix/primitives/drawer.py index 0087686aa47..1fad4ee4341 100644 --- a/reflex/components/radix/primitives/drawer.py +++ b/reflex/components/radix/primitives/drawer.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Any, Literal +from typing import Any, ClassVar, Literal from reflex.components.component import Component, ComponentNamespace from reflex.components.radix.primitives.base import RadixPrimitiveComponent @@ -89,6 +89,8 @@ class DrawerTrigger(DrawerComponent): _memoization_mode = MemoizationMode(recursive=False) + _valid_parents: ClassVar[list[str]] = ["DrawerRoot"] + @classmethod def create(cls, *children: Any, **props: Any) -> Component: """Create a new DrawerTrigger instance. @@ -114,6 +116,8 @@ class DrawerPortal(DrawerComponent): alias = "Vaul" + tag + _valid_parents: ClassVar[list[str]] = ["DrawerRoot"] + # Based on https://www.radix-ui.com/primitives/docs/components/dialog#content class DrawerContent(DrawerComponent): @@ -155,6 +159,8 @@ def add_style(self) -> dict: # Fired when interacting outside the drawer content. on_interact_outside: EventHandler[no_args_event_spec] + _valid_parents: ClassVar[list[str]] = ["DrawerRoot", "DrawerPortal"] + @classmethod def create(cls, *children, **props): """Create a Drawer Content. @@ -182,6 +188,8 @@ class DrawerOverlay(DrawerComponent): alias = "Vaul" + tag + _valid_parents: ClassVar[list[str]] = ["DrawerRoot", "DrawerPortal"] + # Style set based on the source code at https://ui.shadcn.com/docs/components/drawer def add_style(self) -> dict: """Get the style for the component. @@ -207,6 +215,12 @@ class DrawerClose(DrawerTrigger): alias = "Vaul" + tag + _valid_parents: ClassVar[list[str]] = [ + "DrawerRoot", + "DrawerPortal", + "DrawerContent", + ] + class DrawerTitle(DrawerComponent): """A title for the drawer.""" @@ -215,6 +229,12 @@ class DrawerTitle(DrawerComponent): alias = "Vaul" + tag + _valid_parents: ClassVar[list[str]] = [ + "DrawerRoot", + "DrawerPortal", + "DrawerContent", + ] + # Style set based on the source code at https://ui.shadcn.com/docs/components/drawer def add_style(self) -> dict: """Get the style for the component. @@ -237,6 +257,12 @@ class DrawerDescription(DrawerComponent): alias = "Vaul" + tag + _valid_parents: ClassVar[list[str]] = [ + "DrawerRoot", + "DrawerPortal", + "DrawerContent", + ] + # Style set based on the source code at https://ui.shadcn.com/docs/components/drawer def add_style(self) -> dict: """Get the style for the component. @@ -256,6 +282,12 @@ class DrawerHandle(DrawerComponent): alias = "Vaul" + tag + _valid_parents: ClassVar[list[str]] = [ + "DrawerRoot", + "DrawerPortal", + "DrawerContent", + ] + class Drawer(ComponentNamespace): """A namespace for Drawer components.""" diff --git a/reflex/components/radix/themes/components/alert_dialog.py b/reflex/components/radix/themes/components/alert_dialog.py index 947f4d3a12f..dbf3c4ab69d 100644 --- a/reflex/components/radix/themes/components/alert_dialog.py +++ b/reflex/components/radix/themes/components/alert_dialog.py @@ -1,6 +1,6 @@ """Interactive components provided by @radix-ui/themes.""" -from typing import Literal +from typing import ClassVar, Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive @@ -38,6 +38,8 @@ class AlertDialogTrigger(RadixThemesTriggerComponent): _memoization_mode = MemoizationMode(recursive=False) + _valid_parents: ClassVar[list[str]] = ["AlertDialogRoot"] + class AlertDialogContent(elements.Div, RadixThemesComponent): """Contains the content of the dialog. This component is based on the div element.""" @@ -59,6 +61,8 @@ class AlertDialogContent(elements.Div, RadixThemesComponent): # Fired when the escape key is pressed. on_escape_key_down: EventHandler[no_args_event_spec] + _valid_parents: ClassVar[list[str]] = ["AlertDialogRoot"] + class AlertDialogTitle(RadixThemesComponent): """An accessible title that is announced when the dialog is opened. @@ -68,6 +72,8 @@ class AlertDialogTitle(RadixThemesComponent): tag = "AlertDialog.Title" + _valid_parents: ClassVar[list[str]] = ["AlertDialogRoot", "AlertDialogContent"] + class AlertDialogDescription(RadixThemesComponent): """An optional accessible description that is announced when the dialog is opened. @@ -76,6 +82,8 @@ class AlertDialogDescription(RadixThemesComponent): tag = "AlertDialog.Description" + _valid_parents: ClassVar[list[str]] = ["AlertDialogRoot", "AlertDialogContent"] + class AlertDialogAction(RadixThemesTriggerComponent): """Wraps the control that will close the dialog. This should be distinguished @@ -84,6 +92,8 @@ class AlertDialogAction(RadixThemesTriggerComponent): tag = "AlertDialog.Action" + _valid_parents: ClassVar[list[str]] = ["AlertDialogRoot", "AlertDialogContent"] + class AlertDialogCancel(RadixThemesTriggerComponent): """Wraps the control that will close the dialog. This should be distinguished @@ -92,6 +102,8 @@ class AlertDialogCancel(RadixThemesTriggerComponent): tag = "AlertDialog.Cancel" + _valid_parents: ClassVar[list[str]] = ["AlertDialogRoot", "AlertDialogContent"] + class AlertDialog(ComponentNamespace): """AlertDialog components namespace.""" diff --git a/reflex/components/radix/themes/components/dialog.py b/reflex/components/radix/themes/components/dialog.py index 4d4a62bc4ee..ba944f55cde 100644 --- a/reflex/components/radix/themes/components/dialog.py +++ b/reflex/components/radix/themes/components/dialog.py @@ -1,6 +1,6 @@ """Interactive components provided by @radix-ui/themes.""" -from typing import Literal +from typing import ClassVar, Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive @@ -36,12 +36,16 @@ class DialogTrigger(RadixThemesTriggerComponent): _memoization_mode = MemoizationMode(recursive=False) + _valid_parents: ClassVar[list[str]] = ["DialogRoot"] + class DialogTitle(RadixThemesComponent): """Title component to display inside a Dialog modal.""" tag = "Dialog.Title" + _valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogContent"] + class DialogContent(elements.Div, RadixThemesComponent): """Content component to display inside a Dialog modal.""" @@ -66,18 +70,24 @@ class DialogContent(elements.Div, RadixThemesComponent): # Fired when the pointer interacts outside the dialog. on_interact_outside: EventHandler[no_args_event_spec] + _valid_parents: ClassVar[list[str]] = ["DialogRoot"] + class DialogDescription(RadixThemesComponent): """Description component to display inside a Dialog modal.""" tag = "Dialog.Description" + _valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogContent"] + class DialogClose(RadixThemesTriggerComponent): """Close button component to close an open Dialog modal.""" tag = "Dialog.Close" + _valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogContent"] + class Dialog(ComponentNamespace): """Dialog components namespace.""" diff --git a/reflex/components/radix/themes/components/hover_card.py b/reflex/components/radix/themes/components/hover_card.py index 110113427cb..cbb24f9315c 100644 --- a/reflex/components/radix/themes/components/hover_card.py +++ b/reflex/components/radix/themes/components/hover_card.py @@ -1,6 +1,6 @@ """Interactive components provided by @radix-ui/themes.""" -from typing import Literal +from typing import ClassVar, Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive @@ -42,6 +42,8 @@ class HoverCardTrigger(RadixThemesTriggerComponent): _memoization_mode = MemoizationMode(recursive=False) + _valid_parents: ClassVar[list[str]] = ["HoverCardRoot"] + class HoverCardContent(elements.Div, RadixThemesComponent): """Contains the content of the open hover card.""" @@ -75,6 +77,8 @@ class HoverCardContent(elements.Div, RadixThemesComponent): # Hovercard size "1" - "3" size: Var[Responsive[Literal["1", "2", "3"]]] + _valid_parents: ClassVar[list[str]] = ["HoverCardRoot"] + class HoverCard(ComponentNamespace): """For sighted users to preview content available behind a link.""" diff --git a/reflex/components/radix/themes/components/popover.py b/reflex/components/radix/themes/components/popover.py index 650a8a5af8d..039349e65aa 100644 --- a/reflex/components/radix/themes/components/popover.py +++ b/reflex/components/radix/themes/components/popover.py @@ -1,6 +1,6 @@ """Interactive components provided by @radix-ui/themes.""" -from typing import Literal +from typing import ClassVar, Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive @@ -39,6 +39,8 @@ class PopoverTrigger(RadixThemesTriggerComponent): _memoization_mode = MemoizationMode(recursive=False) + _valid_parents: ClassVar[list[str]] = ["PopoverRoot"] + class PopoverContent(elements.Div, RadixThemesComponent): """Contains content to be rendered in the open popover.""" @@ -90,12 +92,16 @@ class PopoverContent(elements.Div, RadixThemesComponent): # Fired when the pointer interacts outside the dialog. on_interact_outside: EventHandler[no_args_event_spec] + _valid_parents: ClassVar[list[str]] = ["PopoverRoot"] + class PopoverClose(RadixThemesTriggerComponent): """Wraps the control that will close the popover.""" tag = "Popover.Close" + _valid_parents: ClassVar[list[str]] = ["PopoverRoot", "PopoverContent"] + class Popover(ComponentNamespace): """Floating element for displaying rich content, triggered by a button.""" diff --git a/tests/units/components/radix/test_dialog_validation.py b/tests/units/components/radix/test_dialog_validation.py new file mode 100644 index 00000000000..53d2074f29b --- /dev/null +++ b/tests/units/components/radix/test_dialog_validation.py @@ -0,0 +1,136 @@ +"""Tests for Dialog component parent-child validation.""" + +import pytest + +from reflex.components.radix.primitives.dialog import ( + DialogClose, + DialogContent, + DialogDescription, + DialogOverlay, + DialogPortal, + DialogRoot, + DialogTitle, + DialogTrigger, +) +from reflex.components.radix.themes.components.dialog import ( + DialogClose as ThemeDialogClose, +) +from reflex.components.radix.themes.components.dialog import ( + DialogContent as ThemeDialogContent, +) +from reflex.components.radix.themes.components.dialog import ( + DialogDescription as ThemeDialogDescription, +) +from reflex.components.radix.themes.components.dialog import ( + DialogRoot as ThemeDialogRoot, +) +from reflex.components.radix.themes.components.dialog import ( + DialogTitle as ThemeDialogTitle, +) +from reflex.components.radix.themes.components.dialog import ( + DialogTrigger as ThemeDialogTrigger, +) +from reflex.components.radix.themes.layout.box import Box + + +class TestDialogPrimitivesValidation: + """Test validation of Dialog primitives components.""" + + def test_dialog_trigger_requires_dialog_root(self): + """DialogTrigger must be used within DialogRoot.""" + with pytest.raises(ValueError) as err: + Box.create(DialogTrigger.create()) + assert "DialogTrigger" in str(err.value) + assert "DialogRoot" in str(err.value) + + def test_dialog_trigger_valid_with_dialog_root(self): + """DialogTrigger is valid within DialogRoot.""" + DialogRoot.create(DialogTrigger.create()) + + def test_dialog_portal_requires_dialog_root(self): + """DialogPortal must be used within DialogRoot.""" + with pytest.raises(ValueError) as err: + Box.create(DialogPortal.create()) + assert "DialogPortal" in str(err.value) + assert "DialogRoot" in str(err.value) + + def test_dialog_overlay_requires_dialog_root_or_portal(self): + """DialogOverlay must be used within DialogRoot or DialogPortal.""" + with pytest.raises(ValueError) as err: + Box.create(DialogOverlay.create()) + assert "DialogOverlay" in str(err.value) + + def test_dialog_content_requires_dialog_root_or_portal(self): + """DialogContent must be used within DialogRoot or DialogPortal.""" + with pytest.raises(ValueError) as err: + Box.create(DialogContent.create()) + assert "DialogContent" in str(err.value) + + def test_dialog_title_requires_valid_parent(self): + """DialogTitle must be used within DialogRoot, DialogPortal, or DialogContent.""" + with pytest.raises(ValueError) as err: + Box.create(DialogTitle.create()) + assert "DialogTitle" in str(err.value) + + def test_dialog_description_requires_valid_parent(self): + """DialogDescription must be used within DialogRoot, DialogPortal, or DialogContent.""" + with pytest.raises(ValueError) as err: + Box.create(DialogDescription.create()) + assert "DialogDescription" in str(err.value) + + def test_dialog_close_requires_valid_parent(self): + """DialogClose must be used within DialogRoot, DialogPortal, or DialogContent.""" + with pytest.raises(ValueError) as err: + Box.create(DialogClose.create()) + assert "DialogClose" in str(err.value) + + +class TestDialogThemesValidation: + """Test validation of Dialog themes components.""" + + def test_theme_dialog_trigger_requires_dialog_root(self): + """Theme DialogTrigger must be used within DialogRoot.""" + with pytest.raises(ValueError) as err: + Box.create(ThemeDialogTrigger.create()) + assert "DialogTrigger" in str(err.value) + assert "DialogRoot" in str(err.value) + + def test_theme_dialog_trigger_valid_with_dialog_root(self): + """Theme DialogTrigger is valid within DialogRoot.""" + ThemeDialogRoot.create(ThemeDialogTrigger.create()) + + def test_theme_dialog_content_requires_dialog_root(self): + """Theme DialogContent must be used within DialogRoot.""" + with pytest.raises(ValueError) as err: + Box.create(ThemeDialogContent.create()) + assert "DialogContent" in str(err.value) + assert "DialogRoot" in str(err.value) + + def test_theme_dialog_title_requires_valid_parent(self): + """Theme DialogTitle must be used within DialogRoot or DialogContent.""" + with pytest.raises(ValueError) as err: + Box.create(ThemeDialogTitle.create()) + assert "DialogTitle" in str(err.value) + + def test_theme_dialog_description_requires_valid_parent(self): + """Theme DialogDescription must be used within DialogRoot or DialogContent.""" + with pytest.raises(ValueError) as err: + Box.create(ThemeDialogDescription.create()) + assert "DialogDescription" in str(err.value) + + def test_theme_dialog_close_requires_valid_parent(self): + """Theme DialogClose must be used within DialogRoot or DialogContent.""" + with pytest.raises(ValueError) as err: + Box.create(ThemeDialogClose.create()) + assert "DialogClose" in str(err.value) + + def test_valid_theme_dialog_structure(self): + """Test a valid theme dialog structure.""" + ThemeDialogRoot.create( + ThemeDialogTrigger.create("Open"), + ThemeDialogContent.create( + ThemeDialogTitle.create("Dialog Title"), + ThemeDialogDescription.create("Dialog description"), + ThemeDialogClose.create("Close"), + ), + )