diff --git a/reflex/components/layout/foreach.py b/reflex/components/layout/foreach.py index f469cb7c0..ef61a1acc 100644 --- a/reflex/components/layout/foreach.py +++ b/reflex/components/layout/foreach.py @@ -3,7 +3,7 @@ from __future__ import annotations import inspect from hashlib import md5 -from typing import Any, Callable, Iterable +from typing import Any, Callable, Iterable, Optional from reflex.components.component import Component from reflex.components.layout.fragment import Fragment @@ -23,6 +23,17 @@ class Foreach(Component): # A function from the render args to the component. render_fn: Callable = Fragment.create + # The theme if set. + theme: Optional[Component] = None + + def _apply_theme(self, theme: Component): + """Apply the theme to this component. + + Args: + theme: The theme to apply. + """ + self.theme = theme + @classmethod def create(cls, iterable: Var[Iterable], render_fn: Callable, **props) -> Foreach: """Create a foreach component. @@ -85,6 +96,11 @@ class Foreach(Component): """ tag = self._render() component = tag.render_component() + + # Apply the theme to the children. + if self.theme is not None: + component.apply_theme(self.theme) + return dict( tag.add_props( **self.event_triggers, diff --git a/reflex/components/media/icon.py b/reflex/components/media/icon.py index 18fc9edc6..c10e67896 100644 --- a/reflex/components/media/icon.py +++ b/reflex/components/media/icon.py @@ -41,7 +41,7 @@ class Icon(ChakraIconComponent): raise AttributeError("Missing 'tag' keyword-argument for Icon") if type(props["tag"]) != str or props["tag"].lower() not in ICON_LIST: raise ValueError( - f"Invalid icon tag: {props['tag']}. Please use one of the following: {ICON_LIST}" + f"Invalid icon tag: {props['tag']}. Please use one of the following: {sorted(ICON_LIST)}" ) props["tag"] = format.to_title_case(props["tag"]) + "Icon" return super().create(*children, **props) diff --git a/reflex/components/radix/primitives/__init__.py b/reflex/components/radix/primitives/__init__.py index 82e4c55e4..7e23d792d 100644 --- a/reflex/components/radix/primitives/__init__.py +++ b/reflex/components/radix/primitives/__init__.py @@ -1,3 +1,3 @@ """Radix primitive components (https://www.radix-ui.com/primitives).""" -from .accordion import accordion +from .accordion import accordion, accordion_item diff --git a/reflex/components/radix/primitives/accordion.py b/reflex/components/radix/primitives/accordion.py index c3cb66286..705083fba 100644 --- a/reflex/components/radix/primitives/accordion.py +++ b/reflex/components/radix/primitives/accordion.py @@ -3,9 +3,9 @@ from typing import Literal from reflex.components.component import Component -from reflex.components.tags import Tag +from reflex.components.radix.themes.components.icons import Icon from reflex.style import Style -from reflex.utils import format, imports +from reflex.utils import imports from reflex.vars import Var LiteralAccordionType = Literal["single", "multiple"] @@ -24,17 +24,6 @@ class AccordionComponent(Component): # Change the default rendered element for the one passed as a child. as_child: Var[bool] - def _render(self) -> Tag: - return ( - super() - ._render() - .add_props( - **{ - "class_name": format.to_title_case(self.tag or ""), - } - ) - ) - class AccordionRoot(AccordionComponent): """An accordion component.""" @@ -152,6 +141,10 @@ class AccordionTrigger(AccordionComponent): "&:hover": { "background_color": "var(--gray-2)", }, + "& > .AccordionChevron": { + "color": "var(--accent-10)", + "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)", + }, "&[data-state='open'] > .AccordionChevron": { "transform": "rotate(180deg)", }, @@ -218,62 +211,36 @@ to { """ -# TODO: Remove this once the radix-icons PR is merged in. -class ChevronDownIcon(Component): - """A chevron down icon.""" - - library = "@radix-ui/react-icons" - - tag = "ChevronDownIcon" - - def _apply_theme(self, theme: Component): - self.style = Style( - { - "color": "var(--accent-10)", - "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)", - **self.style, - } - ) - - -accordion_root = AccordionRoot.create -accordion_item = AccordionItem.create -accordion_trigger = AccordionTrigger.create -accordion_content = AccordionContent.create -accordion_header = AccordionHeader.create -chevron_down_icon = ChevronDownIcon.create - - -def accordion(items: list[tuple[str, str]], **props) -> Component: - """High level API for the Radix accordion. - - #TODO: We need to handle taking in state here. This is just for a POC. - +def accordion_item(header: Component, content: Component, **props) -> Component: + """Create an accordion item. Args: - items: The items of the accordion component: list of tuples (label,panel) - **props: The properties of the component. + header: The header of the accordion item. + content: The content of the accordion item. + **props: Additional properties to apply to the accordion item. Returns: - The accordion component. + The accordion item. """ - return accordion_root( - *[ - accordion_item( - accordion_header( - accordion_trigger( - label, - chevron_down_icon( - class_name="AccordionChevron", - ), - ), + # The item requires a value to toggle (use the header as the default value). + value = props.pop("value", str(header)) + + return AccordionItem.create( + AccordionHeader.create( + AccordionTrigger.create( + header, + Icon.create( + tag="chevron_down", + class_name="AccordionChevron", ), - accordion_content( - panel, - ), - value=f"item-{i}", - ) - for i, (label, panel) in enumerate(items) - ], + ), + ), + AccordionContent.create( + content, + ), + value=value, **props, ) + + +accordion = AccordionRoot.create diff --git a/reflex/components/radix/primitives/accordion.pyi b/reflex/components/radix/primitives/accordion.pyi index 119220e56..4da48f2d2 100644 --- a/reflex/components/radix/primitives/accordion.pyi +++ b/reflex/components/radix/primitives/accordion.pyi @@ -9,9 +9,9 @@ from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style from typing import Literal from reflex.components.component import Component -from reflex.components.tags import Tag +from reflex.components.radix.themes.components.icons import Icon from reflex.style import Style -from reflex.utils import format, imports +from reflex.utils import imports from reflex.vars import Var LiteralAccordionType = Literal["single", "multiple"] @@ -530,90 +530,6 @@ class AccordionContent(AccordionComponent): """ ... -class ChevronDownIcon(Component): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "ChevronDownIcon": - """Create the component. +def accordion_item(header: Component, content: Component, **props) -> Component: ... - Args: - *children: The children of the component. - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The props of the component. - - Returns: - The component. - - Raises: - TypeError: If an invalid child is passed. - """ - ... - -accordion_root = AccordionRoot.create -accordion_item = AccordionItem.create -accordion_trigger = AccordionTrigger.create -accordion_content = AccordionContent.create -accordion_header = AccordionHeader.create -chevron_down_icon = ChevronDownIcon.create - -def accordion(items: list[tuple[str, str]], **props) -> Component: ... +accordion = AccordionRoot.create diff --git a/reflex/components/radix/themes/components/icons.py b/reflex/components/radix/themes/components/icons.py index 91f165d50..88530bbc6 100644 --- a/reflex/components/radix/themes/components/icons.py +++ b/reflex/components/radix/themes/components/icons.py @@ -10,7 +10,7 @@ from reflex.utils import format class RadixIconComponent(Component): """A component used as basis for Radix icons.""" - library = "@radix-ui/react-icons" + library = "@radix-ui/react-icons@^1.3.0" class Icon(RadixIconComponent): @@ -43,7 +43,7 @@ class Icon(RadixIconComponent): raise AttributeError("Missing 'tag' keyword-argument for Icon") if type(props["tag"]) != str or props["tag"].lower() not in ICON_LIST: raise ValueError( - f"Invalid icon tag: {props['tag']}. Please use one of the following: {ICON_LIST}" + f"Invalid icon tag: {props['tag']}. Please use one of the following: {sorted(ICON_LIST)}" ) props["tag"] = format.to_title_case(props["tag"]) + "Icon" return super().create(*children, **props) diff --git a/reflex/style.py b/reflex/style.py index 96bb1ebdb..35591dc75 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -108,7 +108,7 @@ def convert(style_dict): var_data = None # Track import/hook data from any Vars in the style dict. out = {} for key, value in style_dict.items(): - key = format.to_camel_case(key) + key = format.to_camel_case(key, allow_hyphens=True) if isinstance(value, dict): # Recursively format nested style dictionaries. out[key], new_var_data = convert(value) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 84364b600..b42ed2b10 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -125,7 +125,7 @@ def to_snake_case(text: str) -> str: return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower().replace("-", "_") -def to_camel_case(text: str) -> str: +def to_camel_case(text: str, allow_hyphens: bool = False) -> str: """Convert a string to camel case. The first word in the text is converted to lowercase and @@ -133,12 +133,14 @@ def to_camel_case(text: str) -> str: Args: text: The string to convert. + allow_hyphens: Whether to allow hyphens in the string. Returns: The camel case string. """ - words = re.split("[_-]", text.lstrip("-_")) - leading_underscores_or_hyphens = "".join(re.findall(r"^[_-]+", text)) + char = "_" if allow_hyphens else "-_" + words = re.split(f"[{char}]", text.lstrip(char)) + leading_underscores_or_hyphens = "".join(re.findall(rf"^[{char}]+", text)) # Capitalize the first letter of each word except the first one converted_word = words[0] + "".join(x.capitalize() for x in words[1:]) return leading_underscores_or_hyphens + converted_word diff --git a/tests/test_style.py b/tests/test_style.py index 40473b43b..bc8faa205 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -17,7 +17,7 @@ test_style = [ ({"::test_case": {"a": 1}}, {"::testCase": {"a": 1}}), ( {"::-webkit-scrollbar": {"display": "none"}}, - {"::WebkitScrollbar": {"display": "none"}}, + {"::-webkit-scrollbar": {"display": "none"}}, ), ]