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
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,

View File

@ -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)

View File

@ -1,3 +1,3 @@
"""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 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

View File

@ -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

View File

@ -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)

View File

@ -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)

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("-", "_")
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

View File

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