diff --git a/reflex/components/component.py b/reflex/components/component.py index 26ea2fd3f..364353b9d 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -36,6 +36,7 @@ from reflex.constants import ( MemoizationMode, PageNames, ) +from reflex.constants.compiler import SpecialAttributes from reflex.event import ( EventChain, EventChainVar, @@ -474,6 +475,17 @@ class Component(BaseComponent, ABC): for key in kwargs["event_triggers"]: del kwargs[key] + # Place data_ and aria_ attributes into custom_attrs + special_attributes = tuple( + key + for key in kwargs + if key not in fields and SpecialAttributes.is_special(key) + ) + if special_attributes: + custom_attrs = kwargs.setdefault("custom_attrs", {}) + for key in special_attributes: + custom_attrs[format.to_kebab_case(key)] = kwargs.pop(key) + # Add style props to the component. style = kwargs.get("style", {}) if isinstance(style, List): @@ -493,8 +505,6 @@ class Component(BaseComponent, ABC): **{attr: value for attr, value in kwargs.items() if attr not in fields}, } ) - if "custom_attrs" not in kwargs: - kwargs["custom_attrs"] = {} # Convert class_name to str if it's list class_name = kwargs.get("class_name", "") diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index 1de3fc263..557a92092 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -160,3 +160,28 @@ class MemoizationMode(Base): # Whether children of this component should be memoized first. recursive: bool = True + + +class SpecialAttributes(enum.Enum): + """Special attributes for components. + + These are placed in custom_attrs and rendered as-is rather than converting + to a style prop. + """ + + DATA_UNDERSCORE = "data_" + DATA_DASH = "data-" + ARIA_UNDERSCORE = "aria_" + ARIA_DASH = "aria-" + + @classmethod + def is_special(cls, attr: str) -> bool: + """Check if the attribute is special. + + Args: + attr: the attribute to check + + Returns: + True if the attribute is special. + """ + return any(attr.startswith(value.value) for value in cls) diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index eda628df7..b7b721a92 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -2217,3 +2217,56 @@ class TriggerState(rx.State): ) def test_has_state_event_triggers(component, output): assert component._has_stateful_event_triggers() == output + + +class SpecialComponent(Box): + """A special component with custom attributes.""" + + data_prop: Var[str] + aria_prop: Var[str] + + +@pytest.mark.parametrize( + ("component_kwargs", "exp_custom_attrs", "exp_style"), + [ + ( + {"data_test": "test", "aria_test": "test"}, + {"data-test": "test", "aria-test": "test"}, + {}, + ), + ( + {"data-test": "test", "aria-test": "test"}, + {"data-test": "test", "aria-test": "test"}, + {}, + ), + ( + {"custom_attrs": {"data-existing": "test"}, "data_new": "test"}, + {"data-existing": "test", "data-new": "test"}, + {}, + ), + ( + {"data_test": "test", "data_prop": "prop"}, + {"data-test": "test"}, + {}, + ), + ( + {"aria_test": "test", "aria_prop": "prop"}, + {"aria-test": "test"}, + {}, + ), + ], +) +def test_special_props(component_kwargs, exp_custom_attrs, exp_style): + """Test that data_ and aria_ special props are correctly added to the component. + + Args: + component_kwargs: The component kwargs. + exp_custom_attrs: The expected custom attributes. + exp_style: The expected style. + """ + component = SpecialComponent.create(**component_kwargs) + assert component.custom_attrs == exp_custom_attrs + assert component.style == exp_style + for prop in SpecialComponent.get_props(): + if prop in component_kwargs: + assert getattr(component, prop)._var_value == component_kwargs[prop]