rx.accordion customizability overhaul (#3266)

* rx.accordion customizability overhaul

* fix low contrast font in "classic" variant
* relative font sizing, so setting font_size on the root works now
* use CSS vars wherever possible to make downstream styling easier
* optional dividers between accordion items
* support `radius`
* support `duration` and `easing` for controlling the animation
* fix animation jank while keeping padding_y
* lookup `data-variant` via CSS instead of passing props in python
* fix "surface" variant to use `--accent-surface` as radix intended

* Restore default variant: "classic"

* Fix accordion primitive radius
This commit is contained in:
Masen Furer 2024-05-10 16:50:02 -07:00 committed by GitHub
parent 2789f32134
commit b95e02a805
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 155 additions and 108 deletions

View File

@ -6,9 +6,10 @@ from typing import Any, Dict, List, Literal, Optional, Union
from reflex.components.component import Component, ComponentNamespace
from reflex.components.core.colors import color
from reflex.components.core.cond import cond
from reflex.components.lucide.icon import Icon
from reflex.components.radix.primitives.base import RadixPrimitiveComponent
from reflex.components.radix.themes.base import LiteralAccentColor
from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius
from reflex.style import Style
from reflex.utils import imports
from reflex.vars import Var, get_uuid_string_var
@ -19,6 +20,32 @@ LiteralAccordionOrientation = Literal["vertical", "horizontal"]
LiteralAccordionVariant = Literal["classic", "soft", "surface", "outline", "ghost"]
DEFAULT_ANIMATION_DURATION = 250
DEFAULT_ANIMATION_EASING = "cubic-bezier(0.87, 0, 0.13, 1)"
def _inherited_variant_selector(
variant: Var[LiteralAccordionVariant] | LiteralAccordionVariant,
*selectors: str,
) -> str:
"""Create a multi CSS selector for targeting variant against the given selectors.
Args:
variant: The variant to target.
selectors: The selectors to apply the variant to (default &)
Returns:
A CSS selector that is more specific on elements that directly set the variant.
"""
if not selectors:
selectors = ("&",)
# Prefer the `data-variant` that is set directly on the selector,
# but also inherit the `data-variant` from any parent element.
return ", ".join(
[
f"{selector}[data-variant='{variant}'], *:where([data-variant='{variant}']) {selector}"
for selector in selectors
]
)
class AccordionComponent(RadixPrimitiveComponent):
@ -30,14 +57,14 @@ class AccordionComponent(RadixPrimitiveComponent):
color_scheme: Var[LiteralAccentColor]
# The variant of the component.
variant: Var[LiteralAccordionVariant] = Var.create_safe("classic")
variant: Var[LiteralAccordionVariant]
def add_style(self) -> Style | None:
"""Add style to the component."""
if self.color_scheme is not None:
self.custom_attrs["data-accent-color"] = self.color_scheme
self.custom_attrs["data-variant"] = self.variant
if self.variant is not None:
self.custom_attrs["data-variant"] = self.variant
def _exclude_props(self) -> list[str]:
return ["color_scheme", "variant"]
@ -71,28 +98,27 @@ class AccordionRoot(AccordionComponent):
# The orientation of the accordion.
orientation: Var[LiteralAccordionOrientation]
# The variant of the accordion.
variant: Var[LiteralAccordionVariant] = Var.create_safe("classic")
# The radius of the accordion corners.
radius: Var[LiteralRadius]
# The time in milliseconds to animate open and close
duration: Var[int] = Var.create_safe(DEFAULT_ANIMATION_DURATION)
# The easing function to use for the animation.
easing: Var[str] = Var.create_safe(DEFAULT_ANIMATION_EASING)
# Whether to show divider lines between items.
show_dividers: Var[bool]
_valid_children: List[str] = ["AccordionItem"]
@classmethod
def create(cls, *children, **props) -> Component:
"""Create the Accordion root component.
Args:
*children: The children of the component.
**props: The properties of the component.
Returns:
The Accordion root Component.
"""
for child in children:
if isinstance(child, AccordionItem):
child.color_scheme = props.get("color_scheme") # type: ignore
child.variant = props.get("variant") # type: ignore
return super().create(*children, **props)
def _exclude_props(self) -> list[str]:
return super()._exclude_props() + [
"radius",
"duration",
"easing",
"show_dividers",
]
def get_event_triggers(self) -> Dict[str, Any]:
"""Get the events triggers signatures for the component.
@ -111,30 +137,42 @@ class AccordionRoot(AccordionComponent):
Returns:
The style of the component.
"""
return Style(
{
"border_radius": "6px",
"box_shadow": f"0 2px 10px {color('black', 1, alpha=True)}",
"&[data-variant='classic']": {
"background_color": color("accent", 9),
"box_shadow": f"0 2px 10px {color('black', 4, alpha=True)}",
},
"&[data-variant='soft']": {
"background_color": color("accent", 3),
},
"&[data-variant='outline']": {
"border": f"1px solid {color('accent', 6)}",
},
"&[data-variant='surface']": {
"border": f"1px solid {color('accent', 6)}",
"background_color": color("accent", 3),
},
"&[data-variant='ghost']": {
"background_color": "none",
"box_shadow": "None",
},
}
)
if self.radius is not None:
self.custom_attrs["data-radius"] = self.radius
if self.variant is None:
# The default variant is classic
self.custom_attrs["data-variant"] = "classic"
style = {
"border_radius": "var(--radius-4)",
"box_shadow": f"0 2px 10px {color('black', 1, alpha=True)}",
"&[data-variant='classic']": {
"background_color": color("accent", 9),
"box_shadow": f"0 2px 10px {color('black', 4, alpha=True)}",
},
"&[data-variant='soft']": {
"background_color": color("accent", 3),
},
"&[data-variant='outline']": {
"border": f"1px solid {color('accent', 6)}",
},
"&[data-variant='surface']": {
"border": f"1px solid {color('accent', 6)}",
"background_color": "var(--accent-surface)",
},
"&[data-variant='ghost']": {
"background_color": "none",
"box_shadow": "None",
},
"--animation-duration": f"{self.duration}ms",
"--animation-easing": self.easing,
}
if self.show_dividers is not None:
style["--divider-px"] = cond(self.show_dividers, "1px", "0")
else:
style["&[data-variant='outline']"]["--divider-px"] = "1px"
style["&[data-variant='surface']"]["--divider-px"] = "1px"
return Style(style)
class AccordionItem(AccordionComponent):
@ -185,23 +223,28 @@ class AccordionItem(AccordionComponent):
):
cls_name = f"{cls_name} AccordionItem"
color_scheme = props.get("color_scheme")
variant = props.get("variant")
if (header is not None) and (content is not None):
children = [
AccordionHeader.create(
AccordionTrigger.create(
header,
AccordionIcon.create(
color_scheme=props.get("color_scheme"),
variant=props.get("variant"),
color_scheme=color_scheme,
variant=variant,
),
color_scheme=props.get("color_scheme"),
variant=props.get("variant"),
color_scheme=color_scheme,
variant=variant,
),
color_scheme=props.get("color_scheme"),
variant=props.get("variant"),
color_scheme=color_scheme,
variant=variant,
),
AccordionContent.create(
content, color_scheme=props.get("color_scheme")
content,
color_scheme=color_scheme,
variant=variant,
),
]
@ -213,29 +256,35 @@ class AccordionItem(AccordionComponent):
Returns:
The style of the component.
"""
for child in self.children:
if isinstance(child, (AccordionHeader, AccordionContent)):
child.color_scheme = self.color_scheme
child.variant = self.variant
divider_style = f"var(--divider-px) solid {color('gray', 6, alpha=True)}"
return Style(
{
"overflow": "hidden",
"width": "100%",
"margin_top": "1px",
"border_top": divider_style,
"&:first-child": {
"margin_top": 0,
"border_top_left_radius": "4px",
"border_top_right_radius": "4px",
"border_top": 0,
"border_top_left_radius": "max(var(--radius-2), var(--radius-6))",
"border_top_right_radius": "max(var(--radius-2), var(--radius-6))",
},
"&:last-child": {
"border_bottom_left_radius": "4px",
"border_bottom_right_radius": "4px",
"border_bottom_left_radius": "max(var(--radius-2), var(--radius-6))",
"border_bottom_right_radius": "max(var(--radius-2), var(--radius-6))",
},
"&:focus-within": {
"position": "relative",
"z_index": 1,
},
_inherited_variant_selector("ghost", "&:first-child"): {
"border_radius": 0,
"border_top": divider_style,
},
_inherited_variant_selector("ghost", "&:last-child"): {
"border_radius": 0,
"border_bottom": divider_style,
},
}
)
@ -271,17 +320,9 @@ class AccordionHeader(AccordionComponent):
Returns:
The style of the component.
"""
for child in self.children:
if isinstance(child, AccordionTrigger):
child.color_scheme = self.color_scheme
child.variant = self.variant
return Style({"display": "flex"})
cubic_bezier = "cubic-bezier(0.87, 0, 0.13, 1)"
class AccordionTrigger(AccordionComponent):
"""An accordion component."""
@ -313,24 +354,18 @@ class AccordionTrigger(AccordionComponent):
Returns:
The style of the component.
"""
for child in self.children:
if isinstance(child, AccordionIcon):
child.color_scheme = self.color_scheme
child.variant = self.variant
return Style(
{
"color": color("accent", 11),
"font_size": "1.1em",
"line_height": 1,
"font_size": "15px",
"justify_content": "space-between",
"align_items": "center",
"flex": 1,
"display": "flex",
"padding": "0 20px",
"height": "45px",
"font_family": "inherit",
"padding": "var(--space-3) var(--space-4)",
"width": "100%",
"box_shadow": f"0 var(--divider-px) 0 {color('gray', 6, alpha=True)}",
"&[data-state='open'] > .AccordionChevron": {
"transform": "rotate(180deg)",
},
@ -338,17 +373,15 @@ class AccordionTrigger(AccordionComponent):
"background_color": color("accent", 4),
},
"& > .AccordionChevron": {
"transition": f"transform {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
"transition": f"transform var(--animation-duration) var(--animation-easing)",
},
"&[data-variant='classic']": {
"color": color("accent", 12),
"box_shadow": color("accent", 11),
_inherited_variant_selector("classic"): {
"color": "var(--accent-contrast)",
"&:hover": {
"background_color": color("accent", 10),
},
"& > .AccordionChevron": {
"color": color("accent", 12),
"transition": f"transform {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
"color": "var(--accent-contrast)",
},
},
}
@ -444,30 +477,31 @@ to {
The style of the component.
"""
slideDown = Var.create(
f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
f"${{slideDown}} var(--animation-duration) var(--animation-easing)",
_var_is_string=True,
)
slideUp = Var.create(
f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
f"${{slideUp}} var(--animation-duration) var(--animation-easing)",
_var_is_string=True,
)
return Style(
{
"overflow": "hidden",
"font_size": "10px",
"color": color("accent", 11),
"background_color": color("accent", 3),
"padding": "0 15px",
"padding_x": "var(--space-4)",
# Apply before and after content to avoid height animation jank.
"&:before, &:after": {
"content": "' '",
"display": "block",
"height": "var(--space-3)",
},
"&[data-state='open']": {"animation": slideDown},
"&[data-state='closed']": {"animation": slideUp},
"&[data-variant='classic']": {
"color": color("accent", 12),
"background_color": color("accent", 9),
_inherited_variant_selector("classic"): {
"color": "var(--accent-contrast)",
},
"&[data-variant='outline']": {"background_color": "transparent"},
"&[data-variant='ghost']": {"background_color": "transparent"},
}
)

View File

@ -10,9 +10,10 @@ from reflex.style import Style
from typing import Any, Dict, List, Literal, Optional, Union
from reflex.components.component import Component, ComponentNamespace
from reflex.components.core.colors import color
from reflex.components.core.cond import cond
from reflex.components.lucide.icon import Icon
from reflex.components.radix.primitives.base import RadixPrimitiveComponent
from reflex.components.radix.themes.base import LiteralAccentColor
from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius
from reflex.style import Style
from reflex.utils import imports
from reflex.vars import Var, get_uuid_string_var
@ -22,6 +23,7 @@ LiteralAccordionDir = Literal["ltr", "rtl"]
LiteralAccordionOrientation = Literal["vertical", "horizontal"]
LiteralAccordionVariant = Literal["classic", "soft", "surface", "outline", "ghost"]
DEFAULT_ANIMATION_DURATION = 250
DEFAULT_ANIMATION_EASING = "cubic-bezier(0.87, 0, 0.13, 1)"
class AccordionComponent(RadixPrimitiveComponent):
def add_style(self) -> Style | None: ...
@ -173,6 +175,8 @@ class AccordionComponent(RadixPrimitiveComponent):
...
class AccordionRoot(AccordionComponent):
def get_event_triggers(self) -> Dict[str, Any]: ...
def add_style(self): ...
@overload
@classmethod
def create( # type: ignore
@ -196,12 +200,15 @@ class AccordionRoot(AccordionComponent):
Literal["vertical", "horizontal"],
]
] = None,
variant: Optional[
radius: Optional[
Union[
Var[Literal["classic", "soft", "surface", "outline", "ghost"]],
Literal["classic", "soft", "surface", "outline", "ghost"],
Var[Literal["none", "small", "medium", "large", "full"]],
Literal["none", "small", "medium", "large", "full"],
]
] = None,
duration: Optional[Union[Var[int], int]] = None,
easing: Optional[Union[Var[str], str]] = None,
show_dividers: Optional[Union[Var[bool], bool]] = None,
color_scheme: Optional[
Union[
Var[
@ -264,6 +271,12 @@ class AccordionRoot(AccordionComponent):
],
]
] = None,
variant: Optional[
Union[
Var[Literal["classic", "soft", "surface", "outline", "ghost"]],
Literal["classic", "soft", "surface", "outline", "ghost"],
]
] = None,
as_child: Optional[Union[Var[bool], bool]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
@ -321,7 +334,7 @@ class AccordionRoot(AccordionComponent):
] = None,
**props
) -> "AccordionRoot":
"""Create the Accordion root component.
"""Create the component.
Args:
*children: The children of the component.
@ -332,8 +345,12 @@ class AccordionRoot(AccordionComponent):
disabled: Whether or not the accordion is disabled.
dir: The reading direction of the accordion when applicable.
orientation: The orientation of the accordion.
variant: The variant of the component.
radius: The radius of the accordion corners.
duration: The time in milliseconds to animate open and close
easing: The easing function to use for the animation.
show_dividers: Whether to show divider lines between items.
color_scheme: The color scheme of the component.
variant: The variant of the component.
as_child: Change the default rendered element for the one passed as a child.
style: The style of the component.
key: A unique key for the component.
@ -341,14 +358,12 @@ class AccordionRoot(AccordionComponent):
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 properties of the component.
**props: The props of the component.
Returns:
The Accordion root Component.
The component.
"""
...
def get_event_triggers(self) -> Dict[str, Any]: ...
def add_style(self): ...
class AccordionItem(AccordionComponent):
@overload
@ -656,8 +671,6 @@ class AccordionHeader(AccordionComponent):
...
def add_style(self) -> Style | None: ...
cubic_bezier = "cubic-bezier(0.87, 0, 0.13, 1)"
class AccordionTrigger(AccordionComponent):
@overload
@classmethod