Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement accordion component #2262

Merged
merged 10 commits into from
Dec 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions reflex/.templates/jinja/web/pages/base_page.js.jinja2
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% import 'web/pages/utils.js.jinja2' as utils %}

/** @jsxImportSource @emotion/react */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why, but in my app this is rendering as

/** @jsxImportSource @emotion/react */import { Fragment } from "react"

It still seems to work, somehow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah not sure I added an extra blank line but doesn't seem to care


{%- block imports_libs %}
{% for module in imports%}
{{- utils.get_import(module) }}
Expand Down
3 changes: 3 additions & 0 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,9 @@ def mark_complete(_=None):
# Merge the component style with the app style.
component.add_style(self.style)

if self.theme is not None:
component.apply_theme(self.theme)

# Add component.get_imports() to all_imports.
all_imports.update(component.get_imports())

Expand Down
23 changes: 22 additions & 1 deletion reflex/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ def __init__(self, *args, **kwargs):

kwargs["style"] = Style(
{
**self.get_fields()["style"].default,
**style,
**{attr: value for attr, value in kwargs.items() if attr not in fields},
}
Expand Down Expand Up @@ -445,6 +446,26 @@ def __str__(self) -> str:

return _compile_component(self)

def _apply_theme(self, theme: Component):
"""Apply the theme to this component.

Args:
theme: The theme to apply.
"""
pass

def apply_theme(self, theme: Component):
"""Apply a theme to the component and its children.

Args:
theme: The theme to apply.
"""
self._apply_theme(theme)
for child in self.children:
if not isinstance(child, Component):
continue
child.apply_theme(theme)

def _render(self, props: dict[str, Any] | None = None) -> Tag:
"""Define how to render the component in React.

Expand Down Expand Up @@ -603,7 +624,7 @@ def _get_style(self) -> dict:
Returns:
The dictionary of the component style as value and the style notation as key.
"""
return {"style": self.style}
return {"css": self.style}

def render(self) -> Dict:
"""Render the component.
Expand Down
3 changes: 3 additions & 0 deletions reflex/components/radix/primitives/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Radix primitive components (https://www.radix-ui.com/primitives)."""

from .accordion import accordion
279 changes: 279 additions & 0 deletions reflex/components/radix/primitives/accordion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
"""Radix accordion components."""

from typing import Literal

from reflex.components.component import Component
from reflex.components.tags import Tag
from reflex.style import Style
from reflex.utils import format, imports
from reflex.vars import Var

LiteralAccordionType = Literal["single", "multiple"]
LiteralAccordionDir = Literal["ltr", "rtl"]
LiteralAccordionOrientation = Literal["vertical", "horizontal"]


DEFAULT_ANIMATION_DURATION = 250


class AccordionComponent(Component):
"""Base class for all @radix-ui/accordion components."""

library = "@radix-ui/react-accordion@^1.1.2"

# 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."""

tag = "Root"

alias = "RadixAccordionRoot"

# The type of accordion (single or multiple).
type_: Var[LiteralAccordionType]

# The value of the item to expand.
value: Var[str]

# The default value of the item to expand.
default_value: Var[str]

# Whether or not the accordion is collapsible.
collapsible: Var[bool]

# Whether or not the accordion is disabled.
disabled: Var[bool]

# The reading direction of the accordion when applicable.
dir: Var[LiteralAccordionDir]

# The orientation of the accordion.
orientation: Var[LiteralAccordionOrientation]

def _apply_theme(self, theme: Component):
self.style = Style(
{
"border_radius": "6px",
"background_color": "var(--accent-6)",
"box_shadow": "0 2px 10px var(--black-a4)",
**self.style,
}
)


class AccordionItem(AccordionComponent):
"""An accordion component."""

tag = "Item"

alias = "RadixAccordionItem"

# A unique identifier for the item.
value: Var[str]

# When true, prevents the user from interacting with the item.
disabled: Var[bool]

def _apply_theme(self, theme: Component):
self.style = Style(
{
"overflow": "hidden",
"margin_top": "1px",
"&:first-child": {
"margin_top": 0,
"border_top_left_radius": "4px",
"border_top_right_radius": "4px",
},
"&:last-child": {
"border_bottom_left_radius": "4px",
"border_bottom_right_radius": "4px",
},
"&:focus-within": {
"position": "relative",
"z_index": 1,
"box_shadow": "0 0 0 2px var(--accent-7)",
},
**self.style,
}
)


class AccordionHeader(AccordionComponent):
"""An accordion component."""

tag = "Header"

alias = "RadixAccordionHeader"

def _apply_theme(self, theme: Component):
self.style = Style(
{
"display": "flex",
**self.style,
}
)


class AccordionTrigger(AccordionComponent):
"""An accordion component."""

tag = "Trigger"

alias = "RadixAccordionTrigger"

def _apply_theme(self, theme: Component):
self.style = Style(
{
"font_family": "inherit",
"padding": "0 20px",
"height": "45px",
"flex": 1,
"display": "flex",
"align_items": "center",
"justify_content": "space-between",
"font_size": "15px",
"line_height": 1,
"color": "var(--accent-11)",
"box_shadow": "0 1px 0 var(--accent-6)",
"&:hover": {
"background_color": "var(--gray-2)",
},
"&[data-state='open'] > .AccordionChevron": {
"transform": "rotate(180deg)",
},
**self.style,
}
)


class AccordionContent(AccordionComponent):
"""An accordion component."""

tag = "Content"

alias = "RadixAccordionContent"

def _apply_theme(self, theme: Component):
self.style = Style(
{
"overflow": "hidden",
"fontSize": "15px",
"color": "var(--accent-11)",
"backgroundColor": "var(--accent-2)",
"padding": "15px, 20px",
"&[data-state='open']": {
"animation": Var.create(
f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
_var_is_string=True,
),
},
"&[data-state='closed']": {
"animation": Var.create(
f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
_var_is_string=True,
),
},
**self.style,
}
)

def _get_imports(self):
return {
**super()._get_imports(),
"@emotion/react": [imports.ImportVar(tag="keyframes")],
}

def _get_custom_code(self) -> str:
return """
const slideDown = keyframes`
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
`
const slideUp = keyframes`
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
`
"""


# 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.


Args:
items: The items of the accordion component: list of tuples (label,panel)
**props: The properties of the component.

Returns:
The accordion component.
"""
return accordion_root(
*[
accordion_item(
accordion_header(
accordion_trigger(
label,
chevron_down_icon(
class_name="AccordionChevron",
),
),
),
accordion_content(
panel,
),
value=f"item-{i}",
)
for i, (label, panel) in enumerate(items)
],
**props,
)
Loading
Loading