Add high-level API for accordion (#2285)
This commit is contained in:
parent
7388617b72
commit
5d21f0ca60
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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: ...
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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"}},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user