diff --git a/reflex/components/chakra/disclosure/tabs.py b/reflex/components/chakra/disclosure/tabs.py index 4e573281f..4600384b4 100644 --- a/reflex/components/chakra/disclosure/tabs.py +++ b/reflex/components/chakra/disclosure/tabs.py @@ -90,20 +90,28 @@ class Tab(ChakraComponent): # The id of the panel. panel_id: Var[str] + _valid_parents: List[str] = ["TabList"] + class TabList(ChakraComponent): """Wrapper for the Tab components.""" tag = "TabList" + _valid_parents: List[str] = ["Tabs"] + class TabPanels(ChakraComponent): """Wrapper for the Tab components.""" tag = "TabPanels" + _valid_parents: List[str] = ["Tabs"] + class TabPanel(ChakraComponent): """An element that contains the content associated with a tab.""" tag = "TabPanel" + + _valid_parents: List[str] = ["TabPanels"] diff --git a/reflex/components/component.py b/reflex/components/component.py index 6fc6e047f..428b4b64f 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -155,6 +155,9 @@ class Component(BaseComponent, ABC): # only components that are allowed as children _valid_children: List[str] = [] + # only components that are allowed as parent + _valid_parents: List[str] = [] + # custom attribute custom_attrs: Dict[str, Union[Var, str]] = {} @@ -651,7 +654,8 @@ class Component(BaseComponent, ABC): children: The children of the component. """ - if not self._invalid_children and not self._valid_children: + skip_parentable = all(child._valid_parents == [] for child in children) + if not self._invalid_children and not self._valid_children and skip_parentable: return comp_name = type(self).__name__ @@ -671,6 +675,15 @@ class Component(BaseComponent, ABC): f"The component `{comp_name}` only allows the components: {valid_child_list} as children. Got `{child_name}` instead." ) + def validate_vaild_parent(child_name, valid_parents): + if comp_name not in valid_parents: + valid_parent_list = ", ".join( + [f"`{v_parent}`" for v_parent in valid_parents] + ) + raise ValueError( + f"The component `{child_name}` can only be a child of the components: {valid_parent_list}. Got `{comp_name}` instead." + ) + for child in children: name = type(child).__name__ @@ -680,6 +693,9 @@ class Component(BaseComponent, ABC): if self._valid_children: validate_valid_child(name) + if child._valid_parents: + validate_vaild_parent(name, child._valid_parents) + @staticmethod def _get_vars_from_event_triggers( event_triggers: dict[str, EventChain | Var], diff --git a/scripts/pyi_generator.py b/scripts/pyi_generator.py index a6f55281d..cd0a5f551 100644 --- a/scripts/pyi_generator.py +++ b/scripts/pyi_generator.py @@ -55,6 +55,7 @@ EXCLUDED_PROPS = [ "_invalid_children", "_memoization_mode", "_valid_children", + "_valid_parents", ] DEFAULT_TYPING_IMPORTS = { diff --git a/tests/components/test_component.py b/tests/components/test_component.py index 7bdd6cc9f..e4bad40db 100644 --- a/tests/components/test_component.py +++ b/tests/components/test_component.py @@ -137,6 +137,8 @@ def component5() -> Type[Component]: _valid_children: List[str] = ["Text"] + _valid_parents: List[str] = ["Text"] + return TestComponent5 @@ -569,6 +571,20 @@ def test_unsupported_child_components(fixture, request): ) +def test_unsupported_parent_components(component5): + """Test that a value error is raised when an component is not in _valid_parents of one of its children. + + Args: + component5: component with valid parent of "Text" only + """ + with pytest.raises(ValueError) as err: + rx.Box(children=[component5.create()]) + assert ( + err.value.args[0] + == f"The component `{component5.__name__}` can only be a child of the components: `{component5._valid_parents[0]}`. Got `Box` instead." + ) + + @pytest.mark.parametrize("fixture", ["component5", "component7"]) def test_component_with_only_valid_children(fixture, request): """Test that a value error is raised when an unsupported component (a child component not found in the