소스 검색

Add high-level API for accordion (#2285)

Nikhil Rao 1 년 전
부모
커밋
5d21f0ca60

+ 17 - 1
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,

+ 1 - 1
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)

+ 1 - 1
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

+ 31 - 64
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",
-                        ),
-                    ),
-                ),
-                accordion_content(
-                    panel,
+    # 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",
                 ),
-                value=f"item-{i}",
-            )
-            for i, (label, panel) in enumerate(items)
-        ],
+            ),
+        ),
+        AccordionContent.create(
+            content,
+        ),
+        value=value,
         **props,
     )
+
+
+accordion = AccordionRoot.create

+ 4 - 88
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.
-
-        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_item(header: Component, content: Component, **props) -> Component: ...
 
-def accordion(items: list[tuple[str, str]], **props) -> Component: ...
+accordion = AccordionRoot.create

+ 2 - 2
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)

+ 1 - 1
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)

+ 5 - 3
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

+ 1 - 1
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"}},
     ),
 ]