From 2adf7fd2a2ef5844a5c97bd74122fb674a50c97f Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 10 Oct 2024 09:25:14 -0700 Subject: [PATCH] Support aria and data props --- reflex/components/component.py | 14 +++++++--- reflex/constants/compiler.py | 26 +++++++++++++++++++ tests/units/components/test_component.py | 33 ++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 26ea2fd3f..3ced51794 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -1,4 +1,4 @@ -"""Base component definitions.""" +"," "Base component definitions." "" from __future__ import annotations @@ -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,15 @@ 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 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 +503,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..238d16da0 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -160,3 +160,29 @@ 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 = "data" + ARIA = "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. + """ + for value in cls: + if attr.startswith(f"{value.value}-") or attr.startswith(f"{value.value}_"): + return True + return False diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index b54ce1bbe..517eb8887 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -2215,3 +2215,36 @@ class TriggerState(rx.State): ) def test_has_state_event_triggers(component, output): assert component._has_stateful_event_triggers() == output + + +@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"}, + {}, + ), + ], +) +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 = rx.box(**component_kwargs) + assert component.custom_attrs == exp_custom_attrs + assert component.style == exp_style