Skip to content

Commit 517681e

Browse files
Add parent-child validation for Radix Dialog, AlertDialog, Drawer, Popover, and HoverCard components
- Add _valid_parents ClassVar to Dialog components (primitives and themes) - Add _valid_parents ClassVar to AlertDialog components - Add _valid_parents ClassVar to Drawer components - Add _valid_parents ClassVar to Popover and HoverCard components - Add comprehensive test suite for Dialog validation - Fix the error: DialogTrigger must be used within Dialog This prevents runtime React errors by validating component hierarchy at compile time. Note: Pre-existing pyright error in dialog.py (RadixPrimitiveTriggerComponent import) exists on main branch and is not introduced by these changes. Co-Authored-By: Alek <[email protected]>
1 parent fc0a6ca commit 517681e

File tree

8 files changed

+238
-12
lines changed

8 files changed

+238
-12
lines changed

pyi_hashes.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,16 @@
4747
"reflex/components/radix/primitives/__init__.pyi": "01c388fe7a1f5426a16676404344edf6",
4848
"reflex/components/radix/primitives/accordion.pyi": "19484eca0ad53f538f5db04c09921738",
4949
"reflex/components/radix/primitives/base.pyi": "9ef34884fb6028dc017df5e2db639c81",
50-
"reflex/components/radix/primitives/dialog.pyi": "1bc7533791b07928ad1ade5f616b81d4",
51-
"reflex/components/radix/primitives/drawer.pyi": "921e45dfaf5b9131ef27c561c3acca2e",
50+
"reflex/components/radix/primitives/dialog.pyi": "13ec2d6d93e925427c68aa17adb1fdb1",
51+
"reflex/components/radix/primitives/drawer.pyi": "b25a02ff81c03bcb08bf851f435fb84c",
5252
"reflex/components/radix/primitives/form.pyi": "78055e820703c98c3b838aa889566365",
5353
"reflex/components/radix/primitives/progress.pyi": "c917952d57ddb3e138a40c4005120d5e",
5454
"reflex/components/radix/primitives/slider.pyi": "4ff06f0025d47f166132909b09ab96f8",
5555
"reflex/components/radix/themes/__init__.pyi": "582b4a7ead62b2ae8605e17fa084c063",
5656
"reflex/components/radix/themes/base.pyi": "3e1ccd5ce5fef0b2898025193ee3d069",
5757
"reflex/components/radix/themes/color_mode.pyi": "dda570583355d8c0d8f607be457ba7a1",
5858
"reflex/components/radix/themes/components/__init__.pyi": "efa279ee05479d7bb8a64d49da808d03",
59-
"reflex/components/radix/themes/components/alert_dialog.pyi": "eed422fcc1ff5ccf3dbf6934699bd0b1",
59+
"reflex/components/radix/themes/components/alert_dialog.pyi": "dada6f9fd295509a94e4f6daa8b11072",
6060
"reflex/components/radix/themes/components/aspect_ratio.pyi": "71de4160d79840561c48b570197a4152",
6161
"reflex/components/radix/themes/components/avatar.pyi": "e40c2f0fda6d2c028d83681a27f3fb96",
6262
"reflex/components/radix/themes/components/badge.pyi": "58fd1a9c5d2f8762e2a0370311731ff5",
@@ -68,12 +68,12 @@
6868
"reflex/components/radix/themes/components/checkbox_group.pyi": "8638582a623036f8893a3fa6080f2672",
6969
"reflex/components/radix/themes/components/context_menu.pyi": "b9499d8bdd2c5565621fea5fe7d7a25a",
7070
"reflex/components/radix/themes/components/data_list.pyi": "6f8d9c582e084c23966b992158193b72",
71-
"reflex/components/radix/themes/components/dialog.pyi": "d2615f1a68c80ff930444d054b598c13",
71+
"reflex/components/radix/themes/components/dialog.pyi": "4a2ab11b4cdde3762797526fd7e5322e",
7272
"reflex/components/radix/themes/components/dropdown_menu.pyi": "43f8770c9adf93c73398d68f79048424",
73-
"reflex/components/radix/themes/components/hover_card.pyi": "a96f4433237f9994decf935deff9f269",
73+
"reflex/components/radix/themes/components/hover_card.pyi": "ded8d6f868311c55812bb056907ef7c7",
7474
"reflex/components/radix/themes/components/icon_button.pyi": "e930911d8ecbe61e5447e61c76a28ab6",
7575
"reflex/components/radix/themes/components/inset.pyi": "bd7a2186b553bd4c86d83ff50c784066",
76-
"reflex/components/radix/themes/components/popover.pyi": "91f8edefeb232cc6d48690b1838144c2",
76+
"reflex/components/radix/themes/components/popover.pyi": "9c8c1fe5571d4fb85c2f033d3262900a",
7777
"reflex/components/radix/themes/components/progress.pyi": "0e59587d5b3c8fe0d0067587f144e5b0",
7878
"reflex/components/radix/themes/components/radio.pyi": "f375aa5ac746679618ea7dad257e3224",
7979
"reflex/components/radix/themes/components/radio_cards.pyi": "9dc34a1ce2a1924eb1f41438ef84e80b",

reflex/components/radix/primitives/dialog.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Interactive components provided by @radix-ui/react-dialog."""
22

3-
from typing import Any
3+
from typing import Any, ClassVar
44

55
from reflex.components.component import ComponentNamespace
66
from reflex.components.el import elements
@@ -48,6 +48,8 @@ class DialogPortal(DialogElement):
4848
# Specify a container element to portal the content into.
4949
container: Var[Any]
5050

51+
_valid_parents: ClassVar[list[str]] = ["DialogRoot"]
52+
5153

5254
class DialogOverlay(DialogElement):
5355
"""A layer that covers the inert portion of the view when the dialog is open."""
@@ -61,6 +63,8 @@ class DialogOverlay(DialogElement):
6163
# Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. It inherits from Dialog.Portal.
6264
force_mount: Var[bool]
6365

66+
_valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogPortal"]
67+
6468

6569
class DialogTrigger(DialogElement, RadixPrimitiveTriggerComponent):
6670
"""Trigger an action or event, to open a Dialog modal."""
@@ -73,6 +77,8 @@ class DialogTrigger(DialogElement, RadixPrimitiveTriggerComponent):
7377

7478
_memoization_mode = MemoizationMode(recursive=False)
7579

80+
_valid_parents: ClassVar[list[str]] = ["DialogRoot"]
81+
7682

7783
class DialogContent(elements.Div, DialogElement):
7884
"""Content component to display inside a Dialog modal."""
@@ -101,6 +107,8 @@ class DialogContent(elements.Div, DialogElement):
101107
# Fired when the pointer interacts outside the dialog.
102108
on_interact_outside: EventHandler[no_args_event_spec]
103109

110+
_valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogPortal"]
111+
104112

105113
class DialogTitle(DialogElement):
106114
"""Title component to display inside a Dialog modal."""
@@ -111,6 +119,12 @@ class DialogTitle(DialogElement):
111119
# Change the default rendered element for the one passed as a child, merging their props and behavior.
112120
as_child: Var[bool]
113121

122+
_valid_parents: ClassVar[list[str]] = [
123+
"DialogRoot",
124+
"DialogPortal",
125+
"DialogContent",
126+
]
127+
114128

115129
class DialogDescription(DialogElement):
116130
"""Description component to display inside a Dialog modal."""
@@ -121,6 +135,12 @@ class DialogDescription(DialogElement):
121135
# Change the default rendered element for the one passed as a child, merging their props and behavior.
122136
as_child: Var[bool]
123137

138+
_valid_parents: ClassVar[list[str]] = [
139+
"DialogRoot",
140+
"DialogPortal",
141+
"DialogContent",
142+
]
143+
124144

125145
class DialogClose(DialogElement, RadixPrimitiveTriggerComponent):
126146
"""Close button component to close an open Dialog modal."""
@@ -131,6 +151,12 @@ class DialogClose(DialogElement, RadixPrimitiveTriggerComponent):
131151
# Change the default rendered element for the one passed as a child, merging their props and behavior.
132152
as_child: Var[bool]
133153

154+
_valid_parents: ClassVar[list[str]] = [
155+
"DialogRoot",
156+
"DialogPortal",
157+
"DialogContent",
158+
]
159+
134160

135161
class Dialog(ComponentNamespace):
136162
"""Dialog components namespace."""

reflex/components/radix/primitives/drawer.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from __future__ import annotations
66

77
from collections.abc import Sequence
8-
from typing import Any, Literal
8+
from typing import Any, ClassVar, Literal
99

1010
from reflex.components.component import Component, ComponentNamespace
1111
from reflex.components.radix.primitives.base import RadixPrimitiveComponent
@@ -89,6 +89,8 @@ class DrawerTrigger(DrawerComponent):
8989

9090
_memoization_mode = MemoizationMode(recursive=False)
9191

92+
_valid_parents: ClassVar[list[str]] = ["DrawerRoot"]
93+
9294
@classmethod
9395
def create(cls, *children: Any, **props: Any) -> Component:
9496
"""Create a new DrawerTrigger instance.
@@ -114,6 +116,8 @@ class DrawerPortal(DrawerComponent):
114116

115117
alias = "Vaul" + tag
116118

119+
_valid_parents: ClassVar[list[str]] = ["DrawerRoot"]
120+
117121

118122
# Based on https://www.radix-ui.com/primitives/docs/components/dialog#content
119123
class DrawerContent(DrawerComponent):
@@ -155,6 +159,8 @@ def add_style(self) -> dict:
155159
# Fired when interacting outside the drawer content.
156160
on_interact_outside: EventHandler[no_args_event_spec]
157161

162+
_valid_parents: ClassVar[list[str]] = ["DrawerRoot", "DrawerPortal"]
163+
158164
@classmethod
159165
def create(cls, *children, **props):
160166
"""Create a Drawer Content.
@@ -182,6 +188,8 @@ class DrawerOverlay(DrawerComponent):
182188

183189
alias = "Vaul" + tag
184190

191+
_valid_parents: ClassVar[list[str]] = ["DrawerRoot", "DrawerPortal"]
192+
185193
# Style set based on the source code at https://ui.shadcn.com/docs/components/drawer
186194
def add_style(self) -> dict:
187195
"""Get the style for the component.
@@ -207,6 +215,12 @@ class DrawerClose(DrawerTrigger):
207215

208216
alias = "Vaul" + tag
209217

218+
_valid_parents: ClassVar[list[str]] = [
219+
"DrawerRoot",
220+
"DrawerPortal",
221+
"DrawerContent",
222+
]
223+
210224

211225
class DrawerTitle(DrawerComponent):
212226
"""A title for the drawer."""
@@ -215,6 +229,12 @@ class DrawerTitle(DrawerComponent):
215229

216230
alias = "Vaul" + tag
217231

232+
_valid_parents: ClassVar[list[str]] = [
233+
"DrawerRoot",
234+
"DrawerPortal",
235+
"DrawerContent",
236+
]
237+
218238
# Style set based on the source code at https://ui.shadcn.com/docs/components/drawer
219239
def add_style(self) -> dict:
220240
"""Get the style for the component.
@@ -237,6 +257,12 @@ class DrawerDescription(DrawerComponent):
237257

238258
alias = "Vaul" + tag
239259

260+
_valid_parents: ClassVar[list[str]] = [
261+
"DrawerRoot",
262+
"DrawerPortal",
263+
"DrawerContent",
264+
]
265+
240266
# Style set based on the source code at https://ui.shadcn.com/docs/components/drawer
241267
def add_style(self) -> dict:
242268
"""Get the style for the component.
@@ -256,6 +282,12 @@ class DrawerHandle(DrawerComponent):
256282

257283
alias = "Vaul" + tag
258284

285+
_valid_parents: ClassVar[list[str]] = [
286+
"DrawerRoot",
287+
"DrawerPortal",
288+
"DrawerContent",
289+
]
290+
259291

260292
class Drawer(ComponentNamespace):
261293
"""A namespace for Drawer components."""

reflex/components/radix/themes/components/alert_dialog.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Interactive components provided by @radix-ui/themes."""
22

3-
from typing import Literal
3+
from typing import ClassVar, Literal
44

55
from reflex.components.component import ComponentNamespace
66
from reflex.components.core.breakpoints import Responsive
@@ -38,6 +38,8 @@ class AlertDialogTrigger(RadixThemesTriggerComponent):
3838

3939
_memoization_mode = MemoizationMode(recursive=False)
4040

41+
_valid_parents: ClassVar[list[str]] = ["AlertDialogRoot"]
42+
4143

4244
class AlertDialogContent(elements.Div, RadixThemesComponent):
4345
"""Contains the content of the dialog. This component is based on the div element."""
@@ -59,6 +61,8 @@ class AlertDialogContent(elements.Div, RadixThemesComponent):
5961
# Fired when the escape key is pressed.
6062
on_escape_key_down: EventHandler[no_args_event_spec]
6163

64+
_valid_parents: ClassVar[list[str]] = ["AlertDialogRoot"]
65+
6266

6367
class AlertDialogTitle(RadixThemesComponent):
6468
"""An accessible title that is announced when the dialog is opened.
@@ -68,6 +72,8 @@ class AlertDialogTitle(RadixThemesComponent):
6872

6973
tag = "AlertDialog.Title"
7074

75+
_valid_parents: ClassVar[list[str]] = ["AlertDialogRoot", "AlertDialogContent"]
76+
7177

7278
class AlertDialogDescription(RadixThemesComponent):
7379
"""An optional accessible description that is announced when the dialog is opened.
@@ -76,6 +82,8 @@ class AlertDialogDescription(RadixThemesComponent):
7682

7783
tag = "AlertDialog.Description"
7884

85+
_valid_parents: ClassVar[list[str]] = ["AlertDialogRoot", "AlertDialogContent"]
86+
7987

8088
class AlertDialogAction(RadixThemesTriggerComponent):
8189
"""Wraps the control that will close the dialog. This should be distinguished
@@ -84,6 +92,8 @@ class AlertDialogAction(RadixThemesTriggerComponent):
8492

8593
tag = "AlertDialog.Action"
8694

95+
_valid_parents: ClassVar[list[str]] = ["AlertDialogRoot", "AlertDialogContent"]
96+
8797

8898
class AlertDialogCancel(RadixThemesTriggerComponent):
8999
"""Wraps the control that will close the dialog. This should be distinguished
@@ -92,6 +102,8 @@ class AlertDialogCancel(RadixThemesTriggerComponent):
92102

93103
tag = "AlertDialog.Cancel"
94104

105+
_valid_parents: ClassVar[list[str]] = ["AlertDialogRoot", "AlertDialogContent"]
106+
95107

96108
class AlertDialog(ComponentNamespace):
97109
"""AlertDialog components namespace."""

reflex/components/radix/themes/components/dialog.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Interactive components provided by @radix-ui/themes."""
22

3-
from typing import Literal
3+
from typing import ClassVar, Literal
44

55
from reflex.components.component import ComponentNamespace
66
from reflex.components.core.breakpoints import Responsive
@@ -36,12 +36,16 @@ class DialogTrigger(RadixThemesTriggerComponent):
3636

3737
_memoization_mode = MemoizationMode(recursive=False)
3838

39+
_valid_parents: ClassVar[list[str]] = ["DialogRoot"]
40+
3941

4042
class DialogTitle(RadixThemesComponent):
4143
"""Title component to display inside a Dialog modal."""
4244

4345
tag = "Dialog.Title"
4446

47+
_valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogContent"]
48+
4549

4650
class DialogContent(elements.Div, RadixThemesComponent):
4751
"""Content component to display inside a Dialog modal."""
@@ -66,18 +70,24 @@ class DialogContent(elements.Div, RadixThemesComponent):
6670
# Fired when the pointer interacts outside the dialog.
6771
on_interact_outside: EventHandler[no_args_event_spec]
6872

73+
_valid_parents: ClassVar[list[str]] = ["DialogRoot"]
74+
6975

7076
class DialogDescription(RadixThemesComponent):
7177
"""Description component to display inside a Dialog modal."""
7278

7379
tag = "Dialog.Description"
7480

81+
_valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogContent"]
82+
7583

7684
class DialogClose(RadixThemesTriggerComponent):
7785
"""Close button component to close an open Dialog modal."""
7886

7987
tag = "Dialog.Close"
8088

89+
_valid_parents: ClassVar[list[str]] = ["DialogRoot", "DialogContent"]
90+
8191

8292
class Dialog(ComponentNamespace):
8393
"""Dialog components namespace."""

reflex/components/radix/themes/components/hover_card.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Interactive components provided by @radix-ui/themes."""
22

3-
from typing import Literal
3+
from typing import ClassVar, Literal
44

55
from reflex.components.component import ComponentNamespace
66
from reflex.components.core.breakpoints import Responsive
@@ -42,6 +42,8 @@ class HoverCardTrigger(RadixThemesTriggerComponent):
4242

4343
_memoization_mode = MemoizationMode(recursive=False)
4444

45+
_valid_parents: ClassVar[list[str]] = ["HoverCardRoot"]
46+
4547

4648
class HoverCardContent(elements.Div, RadixThemesComponent):
4749
"""Contains the content of the open hover card."""
@@ -75,6 +77,8 @@ class HoverCardContent(elements.Div, RadixThemesComponent):
7577
# Hovercard size "1" - "3"
7678
size: Var[Responsive[Literal["1", "2", "3"]]]
7779

80+
_valid_parents: ClassVar[list[str]] = ["HoverCardRoot"]
81+
7882

7983
class HoverCard(ComponentNamespace):
8084
"""For sighted users to preview content available behind a link."""

reflex/components/radix/themes/components/popover.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Interactive components provided by @radix-ui/themes."""
22

3-
from typing import Literal
3+
from typing import ClassVar, Literal
44

55
from reflex.components.component import ComponentNamespace
66
from reflex.components.core.breakpoints import Responsive
@@ -39,6 +39,8 @@ class PopoverTrigger(RadixThemesTriggerComponent):
3939

4040
_memoization_mode = MemoizationMode(recursive=False)
4141

42+
_valid_parents: ClassVar[list[str]] = ["PopoverRoot"]
43+
4244

4345
class PopoverContent(elements.Div, RadixThemesComponent):
4446
"""Contains content to be rendered in the open popover."""
@@ -90,12 +92,16 @@ class PopoverContent(elements.Div, RadixThemesComponent):
9092
# Fired when the pointer interacts outside the dialog.
9193
on_interact_outside: EventHandler[no_args_event_spec]
9294

95+
_valid_parents: ClassVar[list[str]] = ["PopoverRoot"]
96+
9397

9498
class PopoverClose(RadixThemesTriggerComponent):
9599
"""Wraps the control that will close the popover."""
96100

97101
tag = "Popover.Close"
98102

103+
_valid_parents: ClassVar[list[str]] = ["PopoverRoot", "PopoverContent"]
104+
99105

100106
class Popover(ComponentNamespace):
101107
"""Floating element for displaying rich content, triggered by a button."""

0 commit comments

Comments
 (0)