Compare commits
12 Commits
main
...
reflex-0.5
Author | SHA1 | Date | |
---|---|---|---|
![]() |
25c1406e7f | ||
![]() |
7d4609ba60 | ||
![]() |
52e0507c40 | ||
![]() |
5e1fe07e0b | ||
![]() |
d520b34762 | ||
![]() |
7727c14758 | ||
![]() |
7e6fd44732 | ||
![]() |
69d5c02cc5 | ||
![]() |
09dfd031d7 | ||
![]() |
4e58716378 | ||
![]() |
4f7ccb57bc | ||
![]() |
601391fca9 |
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "reflex"
|
||||
version = "0.4.9"
|
||||
version = "0.5.0.post1"
|
||||
description = "Web apps in pure Python."
|
||||
license = "Apache-2.0"
|
||||
authors = [
|
||||
|
@ -44,7 +44,7 @@ def template(main_content: Callable[[], rx.Component]) -> rx.Component:
|
||||
),
|
||||
rx.chakra.menu_item(
|
||||
rx.chakra.link(
|
||||
"Contact", href="mailto:founders@=reflex.dev", width="100%"
|
||||
"Contact", href="mailto:founders@reflex.dev", width="100%"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
@ -17,4 +17,16 @@ module.exports = {
|
||||
{% if darkMode is defined %}
|
||||
darkMode: {{darkMode|json_dumps}},
|
||||
{% endif %}
|
||||
{% if corePlugins is defined %}
|
||||
corePlugins: {{corePlugins|json_dumps}},
|
||||
{% endif %}
|
||||
{% if important is defined %}
|
||||
important: {{important|json_dumps}},
|
||||
{% endif %}
|
||||
{% if prefix is defined %}
|
||||
prefix: {{prefix|json_dumps}},
|
||||
{% endif %}
|
||||
{% if separator is defined %}
|
||||
separator: {{separator|json_dumps}},
|
||||
{% endif %}
|
||||
};
|
||||
|
@ -781,7 +781,7 @@ class Component(BaseComponent, ABC):
|
||||
|
||||
return cls(children=children, **props)
|
||||
|
||||
def add_style(self) -> Style | None:
|
||||
def add_style(self) -> dict[str, Any] | None:
|
||||
"""Add style to the component.
|
||||
|
||||
Downstream components can override this method to return a style dict
|
||||
@ -801,20 +801,16 @@ class Component(BaseComponent, ABC):
|
||||
The style to add.
|
||||
"""
|
||||
styles = []
|
||||
vars = []
|
||||
|
||||
# Walk the MRO to call all `add_style` methods.
|
||||
for base in self._iter_parent_classes_with_method("add_style"):
|
||||
s = base.add_style(self) # type: ignore
|
||||
if s is not None:
|
||||
styles.append(s)
|
||||
vars.append(s._var_data)
|
||||
|
||||
_style = Style()
|
||||
for s in reversed(styles):
|
||||
_style.update(s)
|
||||
|
||||
_style._var_data = VarData.merge(*vars)
|
||||
return _style
|
||||
|
||||
def _get_component_style(self, styles: ComponentStyle) -> Style | None:
|
||||
|
@ -2,16 +2,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from hashlib import md5
|
||||
from typing import Any, Callable, Iterable
|
||||
|
||||
from reflex.components.base.fragment import Fragment
|
||||
from reflex.components.component import Component
|
||||
from reflex.components.tags import IterTag
|
||||
from reflex.constants import MemoizationMode
|
||||
from reflex.utils import console
|
||||
from reflex.vars import Var
|
||||
|
||||
|
||||
class ForeachVarError(TypeError):
|
||||
"""Raised when the iterable type is Any."""
|
||||
|
||||
|
||||
class ForeachRenderError(TypeError):
|
||||
"""Raised when there is an error with the foreach render function."""
|
||||
|
||||
|
||||
class Foreach(Component):
|
||||
"""A component that takes in an iterable and a render function and renders a list of components."""
|
||||
|
||||
@ -24,56 +32,84 @@ class Foreach(Component):
|
||||
render_fn: Callable = Fragment.create
|
||||
|
||||
@classmethod
|
||||
def create(cls, iterable: Var[Iterable], render_fn: Callable, **props) -> Foreach:
|
||||
def create(
|
||||
cls,
|
||||
iterable: Var[Iterable] | Iterable,
|
||||
render_fn: Callable,
|
||||
**props,
|
||||
) -> Foreach:
|
||||
"""Create a foreach component.
|
||||
|
||||
Args:
|
||||
iterable: The iterable to create components from.
|
||||
render_fn: A function from the render args to the component.
|
||||
**props: The attributes to pass to each child component.
|
||||
**props: The attributes to pass to each child component (deprecated).
|
||||
|
||||
Returns:
|
||||
The foreach component.
|
||||
|
||||
Raises:
|
||||
TypeError: If the iterable is of type Any.
|
||||
ForeachVarError: If the iterable is of type Any.
|
||||
"""
|
||||
iterable = Var.create(iterable) # type: ignore
|
||||
if props:
|
||||
console.deprecate(
|
||||
feature_name="Passing props to rx.foreach",
|
||||
reason="it does not have the intended effect and may be confusing",
|
||||
deprecation_version="0.5.0",
|
||||
removal_version="0.6.0",
|
||||
)
|
||||
iterable = Var.create_safe(iterable)
|
||||
if iterable._var_type == Any:
|
||||
raise TypeError(
|
||||
f"Could not foreach over var of type Any. (If you are trying to foreach over a state var, add a type annotation to the var.)"
|
||||
raise ForeachVarError(
|
||||
f"Could not foreach over var `{iterable._var_full_name}` of type Any. "
|
||||
"(If you are trying to foreach over a state var, add a type annotation to the var). "
|
||||
"See https://reflex.dev/docs/library/layout/foreach/"
|
||||
)
|
||||
component = cls(
|
||||
iterable=iterable,
|
||||
render_fn=render_fn,
|
||||
**props,
|
||||
)
|
||||
# Keep a ref to a rendered component to determine correct imports.
|
||||
component.children = [
|
||||
component._render(props=dict(index_var_name="i")).render_component()
|
||||
]
|
||||
# Keep a ref to a rendered component to determine correct imports/hooks/styles.
|
||||
component.children = [component._render().render_component()]
|
||||
return component
|
||||
|
||||
def _render(self, props: dict[str, Any] | None = None) -> IterTag:
|
||||
props = {} if props is None else props.copy()
|
||||
def _render(self) -> IterTag:
|
||||
props = {}
|
||||
|
||||
# Determine the arg var name based on the params accepted by render_fn.
|
||||
render_sig = inspect.signature(self.render_fn)
|
||||
params = list(render_sig.parameters.values())
|
||||
if len(params) >= 1:
|
||||
props.setdefault("arg_var_name", params[0].name)
|
||||
|
||||
if len(params) >= 2:
|
||||
# Validate the render function signature.
|
||||
if len(params) == 0 or len(params) > 2:
|
||||
raise ForeachRenderError(
|
||||
"Expected 1 or 2 parameters in foreach render function, got "
|
||||
f"{[p.name for p in params]}. See https://reflex.dev/docs/library/layout/foreach/"
|
||||
)
|
||||
|
||||
if len(params) >= 1:
|
||||
# Determine the arg var name based on the params accepted by render_fn.
|
||||
props["arg_var_name"] = params[0].name
|
||||
|
||||
if len(params) == 2:
|
||||
# Determine the index var name based on the params accepted by render_fn.
|
||||
props.setdefault("index_var_name", params[1].name)
|
||||
elif "index_var_name" not in props:
|
||||
# Otherwise, use a deterministic index, based on the rendered code.
|
||||
code_hash = md5(str(self.children[0].render()).encode("utf-8")).hexdigest()
|
||||
props.setdefault("index_var_name", f"index_{code_hash}")
|
||||
props["index_var_name"] = params[1].name
|
||||
else:
|
||||
# Otherwise, use a deterministic index, based on the render function bytecode.
|
||||
code_hash = (
|
||||
hash(self.render_fn.__code__)
|
||||
.to_bytes(
|
||||
length=8,
|
||||
byteorder="big",
|
||||
signed=True,
|
||||
)
|
||||
.hex()
|
||||
)
|
||||
props["index_var_name"] = f"index_{code_hash}"
|
||||
|
||||
return IterTag(
|
||||
iterable=self.iterable,
|
||||
render_fn=self.render_fn,
|
||||
children=self.children,
|
||||
**props,
|
||||
)
|
||||
|
||||
@ -84,19 +120,9 @@ class Foreach(Component):
|
||||
The dictionary for template of component.
|
||||
"""
|
||||
tag = self._render()
|
||||
component = tag.render_component()
|
||||
|
||||
return dict(
|
||||
tag.add_props(
|
||||
**self.event_triggers,
|
||||
key=self.key,
|
||||
sx=self.style,
|
||||
id=self.id,
|
||||
class_name=self.class_name,
|
||||
).set(
|
||||
children=[component.render()],
|
||||
props=tag.format_props(),
|
||||
),
|
||||
tag,
|
||||
iterable_state=tag.iterable._var_full_name,
|
||||
arg_name=tag.arg_var_name,
|
||||
arg_index=tag.get_index_var_arg(),
|
||||
|
@ -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": "var(--radius-4)",
|
||||
"border_top_right_radius": "var(--radius-4)",
|
||||
},
|
||||
"&:last-child": {
|
||||
"border_bottom_left_radius": "4px",
|
||||
"border_bottom_right_radius": "4px",
|
||||
"border_bottom_left_radius": "var(--radius-4)",
|
||||
"border_bottom_right_radius": "var(--radius-4)",
|
||||
},
|
||||
"&: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"},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -44,7 +44,7 @@ class FormRoot(FormComponent, HTMLForm):
|
||||
Returns:
|
||||
The style of the component.
|
||||
"""
|
||||
return Style({"width": "260px"})
|
||||
return Style({"width": "100%"})
|
||||
|
||||
|
||||
class FormField(FormComponent):
|
||||
|
@ -23,7 +23,8 @@ from typing import Literal, get_args
|
||||
from reflex.components.component import BaseComponent
|
||||
from reflex.components.core.cond import Cond, color_mode_cond, cond
|
||||
from reflex.components.lucide.icon import Icon
|
||||
from reflex.style import color_mode, toggle_color_mode
|
||||
from reflex.components.radix.themes.components.switch import Switch
|
||||
from reflex.style import LIGHT_COLOR_MODE, color_mode, toggle_color_mode
|
||||
from reflex.utils import console
|
||||
from reflex.vars import BaseVar, Var
|
||||
|
||||
@ -143,11 +144,34 @@ class ColorModeIconButton(IconButton):
|
||||
)
|
||||
|
||||
|
||||
class ColorModeSwitch(Switch):
|
||||
"""Switch for toggling light / dark mode via toggle_color_mode."""
|
||||
|
||||
@classmethod
|
||||
def create(cls, *children, **props):
|
||||
"""Create a switch component bound to color_mode.
|
||||
|
||||
Args:
|
||||
*children: The children of the component.
|
||||
**props: The props to pass to the component.
|
||||
|
||||
Returns:
|
||||
The switch component.
|
||||
"""
|
||||
return Switch.create(
|
||||
*children,
|
||||
checked=color_mode != LIGHT_COLOR_MODE,
|
||||
on_change=toggle_color_mode,
|
||||
**props,
|
||||
)
|
||||
|
||||
|
||||
class ColorModeNamespace(BaseVar):
|
||||
"""Namespace for color mode components."""
|
||||
|
||||
icon = staticmethod(ColorModeIcon.create)
|
||||
button = staticmethod(ColorModeIconButton.create)
|
||||
switch = staticmethod(ColorModeSwitch.create)
|
||||
|
||||
|
||||
color_mode_var_and_namespace = ColorModeNamespace(**dataclasses.asdict(color_mode))
|
||||
|
@ -12,7 +12,8 @@ from typing import Literal, get_args
|
||||
from reflex.components.component import BaseComponent
|
||||
from reflex.components.core.cond import Cond, color_mode_cond, cond
|
||||
from reflex.components.lucide.icon import Icon
|
||||
from reflex.style import color_mode, toggle_color_mode
|
||||
from reflex.components.radix.themes.components.switch import Switch
|
||||
from reflex.style import LIGHT_COLOR_MODE, color_mode, toggle_color_mode
|
||||
from reflex.utils import console
|
||||
from reflex.vars import BaseVar, Var
|
||||
from .components.icon_button import IconButton
|
||||
@ -362,8 +363,184 @@ class ColorModeIconButton(IconButton):
|
||||
"""
|
||||
...
|
||||
|
||||
class ColorModeSwitch(Switch):
|
||||
@overload
|
||||
@classmethod
|
||||
def create( # type: ignore
|
||||
cls,
|
||||
*children,
|
||||
as_child: Optional[Union[Var[bool], bool]] = None,
|
||||
default_checked: Optional[Union[Var[bool], bool]] = None,
|
||||
checked: Optional[Union[Var[bool], bool]] = None,
|
||||
disabled: Optional[Union[Var[bool], bool]] = None,
|
||||
required: Optional[Union[Var[bool], bool]] = None,
|
||||
name: Optional[Union[Var[str], str]] = None,
|
||||
value: Optional[Union[Var[str], str]] = None,
|
||||
size: Optional[
|
||||
Union[Var[Literal["1", "2", "3"]], Literal["1", "2", "3"]]
|
||||
] = None,
|
||||
variant: Optional[
|
||||
Union[
|
||||
Var[Literal["classic", "surface", "soft"]],
|
||||
Literal["classic", "surface", "soft"],
|
||||
]
|
||||
] = None,
|
||||
color_scheme: Optional[
|
||||
Union[
|
||||
Var[
|
||||
Literal[
|
||||
"tomato",
|
||||
"red",
|
||||
"ruby",
|
||||
"crimson",
|
||||
"pink",
|
||||
"plum",
|
||||
"purple",
|
||||
"violet",
|
||||
"iris",
|
||||
"indigo",
|
||||
"blue",
|
||||
"cyan",
|
||||
"teal",
|
||||
"jade",
|
||||
"green",
|
||||
"grass",
|
||||
"brown",
|
||||
"orange",
|
||||
"sky",
|
||||
"mint",
|
||||
"lime",
|
||||
"yellow",
|
||||
"amber",
|
||||
"gold",
|
||||
"bronze",
|
||||
"gray",
|
||||
]
|
||||
],
|
||||
Literal[
|
||||
"tomato",
|
||||
"red",
|
||||
"ruby",
|
||||
"crimson",
|
||||
"pink",
|
||||
"plum",
|
||||
"purple",
|
||||
"violet",
|
||||
"iris",
|
||||
"indigo",
|
||||
"blue",
|
||||
"cyan",
|
||||
"teal",
|
||||
"jade",
|
||||
"green",
|
||||
"grass",
|
||||
"brown",
|
||||
"orange",
|
||||
"sky",
|
||||
"mint",
|
||||
"lime",
|
||||
"yellow",
|
||||
"amber",
|
||||
"gold",
|
||||
"bronze",
|
||||
"gray",
|
||||
],
|
||||
]
|
||||
] = None,
|
||||
high_contrast: Optional[Union[Var[bool], bool]] = None,
|
||||
radius: Optional[
|
||||
Union[
|
||||
Var[Literal["none", "small", "full"]], Literal["none", "small", "full"]
|
||||
]
|
||||
] = None,
|
||||
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_change: 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
|
||||
) -> "ColorModeSwitch":
|
||||
"""Create a switch component bound to color_mode.
|
||||
|
||||
Args:
|
||||
*children: The children of the component.
|
||||
as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
|
||||
default_checked: Whether the switch is checked by default
|
||||
checked: Whether the switch is checked
|
||||
disabled: If true, prevent the user from interacting with the switch
|
||||
required: If true, the user must interact with the switch to submit the form
|
||||
name: The name of the switch (when submitting a form)
|
||||
value: The value associated with the "on" position
|
||||
size: Switch size "1" - "4"
|
||||
variant: Variant of switch: "classic" | "surface" | "soft"
|
||||
color_scheme: Override theme color for switch
|
||||
high_contrast: Whether to render the switch with higher contrast color against background
|
||||
radius: Override theme radius for switch: "none" | "small" | "full"
|
||||
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 to pass to the component.
|
||||
|
||||
Returns:
|
||||
The switch component.
|
||||
"""
|
||||
...
|
||||
|
||||
class ColorModeNamespace(BaseVar):
|
||||
icon = staticmethod(ColorModeIcon.create)
|
||||
button = staticmethod(ColorModeIconButton.create)
|
||||
switch = staticmethod(ColorModeSwitch.create)
|
||||
|
||||
color_mode_var_and_namespace = ColorModeNamespace(**dataclasses.asdict(color_mode))
|
||||
|
@ -20,7 +20,6 @@ from reflex import constants
|
||||
from reflex.config import get_config
|
||||
from reflex.constants import CustomComponents
|
||||
from reflex.utils import console
|
||||
from reflex.utils.pyi_generator import PyiGenerator
|
||||
|
||||
config = get_config()
|
||||
custom_components_cli = typer.Typer()
|
||||
@ -438,6 +437,8 @@ def _run_commands_in_subprocess(cmds: list[str]) -> bool:
|
||||
|
||||
def _make_pyi_files():
|
||||
"""Create pyi files for the custom component."""
|
||||
from reflex.utils.pyi_generator import PyiGenerator
|
||||
|
||||
package_name = _get_package_config()["project"]["name"]
|
||||
|
||||
for dir, _, _ in os.walk(f"./{package_name}"):
|
||||
|
@ -180,12 +180,15 @@ class Style(dict):
|
||||
style_dict: The style dictionary.
|
||||
kwargs: Other key value pairs to apply to the dict update.
|
||||
"""
|
||||
if kwargs:
|
||||
style_dict = {**(style_dict or {}), **kwargs}
|
||||
if not isinstance(style_dict, Style):
|
||||
converted_dict = type(self)(style_dict)
|
||||
else:
|
||||
converted_dict = style_dict
|
||||
if kwargs:
|
||||
if converted_dict is None:
|
||||
converted_dict = type(self)(kwargs)
|
||||
else:
|
||||
converted_dict.update(kwargs)
|
||||
# Combine our VarData with that of any Vars in the style_dict that was passed.
|
||||
self._var_data = VarData.merge(self._var_data, converted_dict._var_data)
|
||||
super().update(converted_dict)
|
||||
|
@ -181,7 +181,12 @@ def get_install_package_manager() -> str | None:
|
||||
Returns:
|
||||
The path to the package manager.
|
||||
"""
|
||||
if constants.IS_WINDOWS and not is_windows_bun_supported():
|
||||
if (
|
||||
constants.IS_WINDOWS
|
||||
and not is_windows_bun_supported()
|
||||
or windows_check_onedrive_in_path()
|
||||
or windows_npm_escape_hatch()
|
||||
):
|
||||
return get_package_manager()
|
||||
return get_config().bun_path
|
||||
|
||||
@ -199,6 +204,24 @@ def get_package_manager() -> str | None:
|
||||
return npm_path
|
||||
|
||||
|
||||
def windows_check_onedrive_in_path() -> bool:
|
||||
"""For windows, check if oneDrive is present in the project dir path.
|
||||
|
||||
Returns:
|
||||
If oneDrive is in the path of the project directory.
|
||||
"""
|
||||
return "onedrive" in str(Path.cwd()).lower()
|
||||
|
||||
|
||||
def windows_npm_escape_hatch() -> bool:
|
||||
"""For windows, if the user sets REFLEX_USE_NPM, use npm instead of bun.
|
||||
|
||||
Returns:
|
||||
If the user has set REFLEX_USE_NPM.
|
||||
"""
|
||||
return os.environ.get("REFLEX_USE_NPM", "").lower() in ["true", "1", "yes"]
|
||||
|
||||
|
||||
def get_app(reload: bool = False) -> ModuleType:
|
||||
"""Get the app module based on the default config.
|
||||
|
||||
@ -737,10 +760,17 @@ def install_bun():
|
||||
Raises:
|
||||
FileNotFoundError: If required packages are not found.
|
||||
"""
|
||||
if constants.IS_WINDOWS and not is_windows_bun_supported():
|
||||
console.warn(
|
||||
"Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm."
|
||||
)
|
||||
win_supported = is_windows_bun_supported()
|
||||
one_drive_in_path = windows_check_onedrive_in_path()
|
||||
if constants.IS_WINDOWS and not win_supported or one_drive_in_path:
|
||||
if not win_supported:
|
||||
console.warn(
|
||||
"Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm."
|
||||
)
|
||||
if one_drive_in_path:
|
||||
console.warn(
|
||||
"Creating project directories in OneDrive is not recommended for bun usage on windows. This will fallback to npm."
|
||||
)
|
||||
|
||||
# Skip if bun is already installed.
|
||||
if os.path.exists(get_config().bun_path) and get_bun_version() == version.parse(
|
||||
@ -843,6 +873,7 @@ def install_frontend_packages(packages: set[str], config: Config):
|
||||
if not constants.IS_WINDOWS
|
||||
or constants.IS_WINDOWS
|
||||
and is_windows_bun_supported()
|
||||
and not windows_check_onedrive_in_path()
|
||||
else None
|
||||
)
|
||||
processes.run_process_with_fallback(
|
||||
@ -929,6 +960,11 @@ def needs_reinit(frontend: bool = True) -> bool:
|
||||
console.warn(
|
||||
f"""On Python < 3.12, `uvicorn==0.20.0` is recommended for improved hot reload times. Found {uvi_ver} instead."""
|
||||
)
|
||||
|
||||
if windows_check_onedrive_in_path():
|
||||
console.warn(
|
||||
"Creating project directories in OneDrive may lead to performance issues. For optimal performance, It is recommended to avoid using OneDrive for your reflex app."
|
||||
)
|
||||
# No need to reinitialize if the app is already initialized.
|
||||
return False
|
||||
|
||||
|
@ -2,17 +2,11 @@ from typing import Dict, List, Set, Tuple, Union
|
||||
|
||||
import pytest
|
||||
|
||||
from reflex.components import box, foreach, text
|
||||
from reflex.components.core import Foreach
|
||||
from reflex.components import box, el, foreach, text
|
||||
from reflex.components.core.foreach import Foreach, ForeachRenderError, ForeachVarError
|
||||
from reflex.state import BaseState
|
||||
from reflex.vars import Var
|
||||
|
||||
try:
|
||||
# When pydantic v2 is installed
|
||||
from pydantic.v1 import ValidationError # type: ignore
|
||||
except ImportError:
|
||||
from pydantic import ValidationError
|
||||
|
||||
|
||||
class ForEachState(BaseState):
|
||||
"""A state for testing the ForEach component."""
|
||||
@ -84,12 +78,12 @@ def display_nested_color_with_shades_v2(color):
|
||||
|
||||
def display_color_tuple(color):
|
||||
assert color._var_type == str
|
||||
return box(text(color, "tuple"))
|
||||
return box(text(color))
|
||||
|
||||
|
||||
def display_colors_set(color):
|
||||
assert color._var_type == str
|
||||
return box(text(color, "set"))
|
||||
return box(text(color))
|
||||
|
||||
|
||||
def display_nested_list_element(element: Var[str], index: Var[int]):
|
||||
@ -100,7 +94,7 @@ def display_nested_list_element(element: Var[str], index: Var[int]):
|
||||
|
||||
def display_color_index_tuple(color):
|
||||
assert color._var_type == Union[int, str]
|
||||
return box(text(color, "index_tuple"))
|
||||
return box(text(color))
|
||||
|
||||
|
||||
seen_index_vars = set()
|
||||
@ -215,24 +209,46 @@ def test_foreach_render(state_var, render_fn, render_dict):
|
||||
|
||||
# Make sure the index vars are unique.
|
||||
arg_index = rend["arg_index"]
|
||||
assert isinstance(arg_index, Var)
|
||||
assert arg_index._var_name not in seen_index_vars
|
||||
assert arg_index._var_type == int
|
||||
seen_index_vars.add(arg_index._var_name)
|
||||
|
||||
|
||||
def test_foreach_bad_annotations():
|
||||
"""Test that the foreach component raises a TypeError if the iterable is of type Any."""
|
||||
with pytest.raises(TypeError):
|
||||
"""Test that the foreach component raises a ForeachVarError if the iterable is of type Any."""
|
||||
with pytest.raises(ForeachVarError):
|
||||
Foreach.create(
|
||||
ForEachState.bad_annotation_list, # type: ignore
|
||||
ForEachState.bad_annotation_list,
|
||||
lambda sublist: Foreach.create(sublist, lambda color: text(color)),
|
||||
)
|
||||
|
||||
|
||||
def test_foreach_no_param_in_signature():
|
||||
"""Test that the foreach component raises a TypeError if no parameters are passed."""
|
||||
with pytest.raises(ValidationError):
|
||||
"""Test that the foreach component raises a ForeachRenderError if no parameters are passed."""
|
||||
with pytest.raises(ForeachRenderError):
|
||||
Foreach.create(
|
||||
ForEachState.colors_list, # type: ignore
|
||||
ForEachState.colors_list,
|
||||
lambda: text("color"),
|
||||
)
|
||||
|
||||
|
||||
def test_foreach_too_many_params_in_signature():
|
||||
"""Test that the foreach component raises a ForeachRenderError if too many parameters are passed."""
|
||||
with pytest.raises(ForeachRenderError):
|
||||
Foreach.create(
|
||||
ForEachState.colors_list,
|
||||
lambda color, index, extra: text(color),
|
||||
)
|
||||
|
||||
|
||||
def test_foreach_component_styles():
|
||||
"""Test that the foreach component works with global component styles."""
|
||||
component = el.div(
|
||||
foreach(
|
||||
ForEachState.colors_list,
|
||||
display_color,
|
||||
)
|
||||
)
|
||||
component._add_style_recursive({box: {"color": "red"}})
|
||||
assert 'css={{"color": "red"}}' in str(component)
|
||||
|
@ -1951,3 +1951,73 @@ def test_component_add_custom_code():
|
||||
"const custom_code5 = 46",
|
||||
"const custom_code6 = 47",
|
||||
}
|
||||
|
||||
|
||||
def test_add_style_embedded_vars(test_state: BaseState):
|
||||
"""Test that add_style works with embedded vars when returning a plain dict.
|
||||
|
||||
Args:
|
||||
test_state: A test state.
|
||||
"""
|
||||
v0 = Var.create_safe("parent")._replace(
|
||||
merge_var_data=VarData(hooks={"useParent": None}), # type: ignore
|
||||
)
|
||||
v1 = rx.color("plum", 10)
|
||||
v2 = Var.create_safe("text")._replace(
|
||||
merge_var_data=VarData(hooks={"useText": None}), # type: ignore
|
||||
)
|
||||
|
||||
class ParentComponent(Component):
|
||||
def add_style(self):
|
||||
return Style(
|
||||
{
|
||||
"fake_parent": v0,
|
||||
}
|
||||
)
|
||||
|
||||
class StyledComponent(ParentComponent):
|
||||
tag = "StyledComponent"
|
||||
|
||||
def add_style(self):
|
||||
return {
|
||||
"color": v1,
|
||||
"fake": v2,
|
||||
"margin": f"{test_state.num}%",
|
||||
}
|
||||
|
||||
page = rx.vstack(StyledComponent.create())
|
||||
page._add_style_recursive(Style())
|
||||
|
||||
assert (
|
||||
"const test_state = useContext(StateContexts.test_state)"
|
||||
in page._get_all_hooks_internal()
|
||||
)
|
||||
assert "useText" in page._get_all_hooks_internal()
|
||||
assert "useParent" in page._get_all_hooks_internal()
|
||||
assert (
|
||||
str(page).count(
|
||||
'css={{"fakeParent": "parent", "color": "var(--plum-10)", "fake": "text", "margin": `${test_state.num}%`}}'
|
||||
)
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
def test_add_style_foreach():
|
||||
class StyledComponent(Component):
|
||||
tag = "StyledComponent"
|
||||
ix: Var[int]
|
||||
|
||||
def add_style(self):
|
||||
return Style({"color": "red"})
|
||||
|
||||
page = rx.vstack(rx.foreach(Var.range(3), lambda i: StyledComponent.create(i)))
|
||||
page._add_style_recursive(Style())
|
||||
|
||||
# Expect only a single child of the foreach on the python side
|
||||
assert len(page.children[0].children) == 1
|
||||
|
||||
# Expect the style to be added to the child of the foreach
|
||||
assert 'css={{"color": "red"}}' in str(page.children[0].children[0])
|
||||
|
||||
# Expect only one instance of this CSS dict in the rendered page
|
||||
assert str(page).count('css={{"color": "red"}}') == 1
|
||||
|
@ -8,7 +8,7 @@ import reflex as rx
|
||||
from reflex import style
|
||||
from reflex.components.component import evaluate_style_namespaces
|
||||
from reflex.style import Style
|
||||
from reflex.vars import Var
|
||||
from reflex.vars import Var, VarData
|
||||
|
||||
test_style = [
|
||||
({"a": 1}, {"a": 1}),
|
||||
@ -503,3 +503,25 @@ def test_evaluate_style_namespaces():
|
||||
assert rx.text.__call__ not in style_dict
|
||||
style_dict = evaluate_style_namespaces(style_dict) # type: ignore
|
||||
assert rx.text.__call__ in style_dict
|
||||
|
||||
|
||||
def test_style_update_with_var_data():
|
||||
"""Test that .update with a Style containing VarData works."""
|
||||
red_var = Var.create_safe("red")._replace(
|
||||
merge_var_data=VarData(hooks={"const red = true": None}), # type: ignore
|
||||
)
|
||||
blue_var = Var.create_safe("blue", _var_is_local=False)._replace(
|
||||
merge_var_data=VarData(hooks={"const blue = true": None}), # type: ignore
|
||||
)
|
||||
|
||||
s1 = Style(
|
||||
{
|
||||
"color": red_var,
|
||||
}
|
||||
)
|
||||
s2 = Style()
|
||||
s2.update(s1, background_color=f"{blue_var}ish")
|
||||
assert s2 == {"color": "red", "backgroundColor": "`${blue}ish`"}
|
||||
assert s2._var_data is not None
|
||||
assert "const red = true" in s2._var_data.hooks
|
||||
assert "const blue = true" in s2._var_data.hooks
|
||||
|
Loading…
Reference in New Issue
Block a user