Add high-level API for accordion (#2285)

This commit is contained in:
Nikhil Rao 2023-12-18 17:21:49 -08:00 committed by GitHub
parent 7388617b72
commit 5d21f0ca60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 63 additions and 162 deletions

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import inspect import inspect
from hashlib import md5 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.component import Component
from reflex.components.layout.fragment import Fragment from reflex.components.layout.fragment import Fragment
@ -23,6 +23,17 @@ class Foreach(Component):
# A function from the render args to the component. # A function from the render args to the component.
render_fn: Callable = Fragment.create 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 @classmethod
def create(cls, iterable: Var[Iterable], render_fn: Callable, **props) -> Foreach: def create(cls, iterable: Var[Iterable], render_fn: Callable, **props) -> Foreach:
"""Create a foreach component. """Create a foreach component.
@ -85,6 +96,11 @@ class Foreach(Component):
""" """
tag = self._render() tag = self._render()
component = tag.render_component() component = tag.render_component()
# Apply the theme to the children.
if self.theme is not None:
component.apply_theme(self.theme)
return dict( return dict(
tag.add_props( tag.add_props(
**self.event_triggers, **self.event_triggers,

View File

@ -41,7 +41,7 @@ class Icon(ChakraIconComponent):
raise AttributeError("Missing 'tag' keyword-argument for Icon") raise AttributeError("Missing 'tag' keyword-argument for Icon")
if type(props["tag"]) != str or props["tag"].lower() not in ICON_LIST: if type(props["tag"]) != str or props["tag"].lower() not in ICON_LIST:
raise ValueError( 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" props["tag"] = format.to_title_case(props["tag"]) + "Icon"
return super().create(*children, **props) return super().create(*children, **props)

View File

@ -1,3 +1,3 @@
"""Radix primitive components (https://www.radix-ui.com/primitives).""" """Radix primitive components (https://www.radix-ui.com/primitives)."""
from .accordion import accordion from .accordion import accordion, accordion_item

View File

@ -3,9 +3,9 @@
from typing import Literal from typing import Literal
from reflex.components.component import Component 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.style import Style
from reflex.utils import format, imports from reflex.utils import imports
from reflex.vars import Var from reflex.vars import Var
LiteralAccordionType = Literal["single", "multiple"] LiteralAccordionType = Literal["single", "multiple"]
@ -24,17 +24,6 @@ class AccordionComponent(Component):
# Change the default rendered element for the one passed as a child. # Change the default rendered element for the one passed as a child.
as_child: Var[bool] 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): class AccordionRoot(AccordionComponent):
"""An accordion component.""" """An accordion component."""
@ -152,6 +141,10 @@ class AccordionTrigger(AccordionComponent):
"&:hover": { "&:hover": {
"background_color": "var(--gray-2)", "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": { "&[data-state='open'] > .AccordionChevron": {
"transform": "rotate(180deg)", "transform": "rotate(180deg)",
}, },
@ -218,62 +211,36 @@ to {
""" """
# TODO: Remove this once the radix-icons PR is merged in. def accordion_item(header: Component, content: Component, **props) -> Component:
class ChevronDownIcon(Component): """Create an accordion item.
"""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.
Args: Args:
items: The items of the accordion component: list of tuples (label,panel) header: The header of the accordion item.
**props: The properties of the component. content: The content of the accordion item.
**props: Additional properties to apply to the accordion item.
Returns: Returns:
The accordion component. The accordion item.
""" """
return accordion_root( # The item requires a value to toggle (use the header as the default value).
*[ value = props.pop("value", str(header))
accordion_item(
accordion_header( return AccordionItem.create(
accordion_trigger( AccordionHeader.create(
label, AccordionTrigger.create(
chevron_down_icon( header,
class_name="AccordionChevron", Icon.create(
), tag="chevron_down",
), class_name="AccordionChevron",
), ),
accordion_content( ),
panel, ),
), AccordionContent.create(
value=f"item-{i}", content,
) ),
for i, (label, panel) in enumerate(items) value=value,
],
**props, **props,
) )
accordion = AccordionRoot.create

View File

@ -9,9 +9,9 @@ from reflex.event import EventChain, EventHandler, EventSpec
from reflex.style import Style from reflex.style import Style
from typing import Literal from typing import Literal
from reflex.components.component import Component 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.style import Style
from reflex.utils import format, imports from reflex.utils import imports
from reflex.vars import Var from reflex.vars import Var
LiteralAccordionType = Literal["single", "multiple"] LiteralAccordionType = Literal["single", "multiple"]
@ -530,90 +530,6 @@ class AccordionContent(AccordionComponent):
""" """
... ...
class ChevronDownIcon(Component): def accordion_item(header: Component, content: Component, **props) -> 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: accordion = AccordionRoot.create
*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: ...

View File

@ -10,7 +10,7 @@ from reflex.utils import format
class RadixIconComponent(Component): class RadixIconComponent(Component):
"""A component used as basis for Radix icons.""" """A component used as basis for Radix icons."""
library = "@radix-ui/react-icons" library = "@radix-ui/react-icons@^1.3.0"
class Icon(RadixIconComponent): class Icon(RadixIconComponent):
@ -43,7 +43,7 @@ class Icon(RadixIconComponent):
raise AttributeError("Missing 'tag' keyword-argument for Icon") raise AttributeError("Missing 'tag' keyword-argument for Icon")
if type(props["tag"]) != str or props["tag"].lower() not in ICON_LIST: if type(props["tag"]) != str or props["tag"].lower() not in ICON_LIST:
raise ValueError( 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" props["tag"] = format.to_title_case(props["tag"]) + "Icon"
return super().create(*children, **props) return super().create(*children, **props)

View File

@ -108,7 +108,7 @@ def convert(style_dict):
var_data = None # Track import/hook data from any Vars in the style dict. var_data = None # Track import/hook data from any Vars in the style dict.
out = {} out = {}
for key, value in style_dict.items(): 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): if isinstance(value, dict):
# Recursively format nested style dictionaries. # Recursively format nested style dictionaries.
out[key], new_var_data = convert(value) out[key], new_var_data = convert(value)

View File

@ -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("-", "_") 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. """Convert a string to camel case.
The first word in the text is converted to lowercase and The first word in the text is converted to lowercase and
@ -133,12 +133,14 @@ def to_camel_case(text: str) -> str:
Args: Args:
text: The string to convert. text: The string to convert.
allow_hyphens: Whether to allow hyphens in the string.
Returns: Returns:
The camel case string. The camel case string.
""" """
words = re.split("[_-]", text.lstrip("-_")) char = "_" if allow_hyphens else "-_"
leading_underscores_or_hyphens = "".join(re.findall(r"^[_-]+", text)) 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 # Capitalize the first letter of each word except the first one
converted_word = words[0] + "".join(x.capitalize() for x in words[1:]) converted_word = words[0] + "".join(x.capitalize() for x in words[1:])
return leading_underscores_or_hyphens + converted_word return leading_underscores_or_hyphens + converted_word

View File

@ -17,7 +17,7 @@ test_style = [
({"::test_case": {"a": 1}}, {"::testCase": {"a": 1}}), ({"::test_case": {"a": 1}}, {"::testCase": {"a": 1}}),
( (
{"::-webkit-scrollbar": {"display": "none"}}, {"::-webkit-scrollbar": {"display": "none"}},
{"::WebkitScrollbar": {"display": "none"}}, {"::-webkit-scrollbar": {"display": "none"}},
), ),
] ]