Skip to content

Commit faca182

Browse files
devin-ai-integration[bot]carlosreflexcarlosabadia
authored
Add Base UI accordion component with high-level wrapper (#36)
* Add Base UI accordion component with high-level wrapper - Create accordion component following established reflex-ui patterns - Implement AccordionRoot, AccordionItem, AccordionHeader, AccordionTrigger, AccordionPanel components - Add HighLevelAccordion wrapper with trigger and content props as requested - Follow BaseUIComponent pattern with proper Base UI integration - Add component to lazy loader system in __init__.py - Add accordion example to demo application - All prop descriptions sourced from Base UI documentation JSON files - Component tested and working correctly with expand/collapse functionality Co-Authored-By: Carlos Cutillas <[email protected]> * updates and bump base ui --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Carlos Cutillas <[email protected]> Co-authored-by: carlosabadia <[email protected]>
1 parent f324bde commit faca182

File tree

5 files changed

+314
-11
lines changed

5 files changed

+314
-11
lines changed

reflex_ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from reflex.utils import lazy_loader
44

55
_REFLEX_UI_MAPPING = {
6+
"components.base.accordion": ["accordion"],
67
"components.base.avatar": ["avatar"],
78
"components.base.badge": ["badge"],
89
"components.base.button": ["button"],
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
"""Custom Accordion component."""
2+
3+
from typing import Literal
4+
5+
from reflex.components.component import Component, ComponentNamespace
6+
from reflex.components.core.foreach import foreach
7+
from reflex.components.el import Div
8+
from reflex.event import EventHandler, passthrough_event_spec
9+
from reflex.utils.imports import ImportVar
10+
from reflex.vars.base import Var
11+
from reflex.vars.object import ObjectVar
12+
13+
from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent
14+
from reflex_ui.components.icons.hugeicon import icon
15+
16+
LiteralOrientation = Literal["horizontal", "vertical"]
17+
18+
ITEMS_TYPE = list[dict[str, str | Component]]
19+
20+
21+
class ClassNames:
22+
"""Class names for accordion components."""
23+
24+
ROOT = "flex flex-col justify-center shadow-small border border-secondary-a4 divide-y divide-secondary-a4 overflow-hidden rounded-xl"
25+
ITEM = ""
26+
HEADER = ""
27+
TRIGGER = "group relative flex w-full items-center justify-between gap-4 bg-secondary-1 hover:bg-secondary-3 px-6 py-4 text-md font-semibold text-secondary-12 transition-colors disabled:cursor-not-allowed disabled:bg-secondary-3 disabled:text-secondary-8 disabled:[&_svg]:text-secondary-8 [&_svg]:text-secondary-11"
28+
PANEL = "h-[var(--accordion-panel-height)] overflow-hidden text-base text-secondary-11 font-medium transition-[height] ease-out data-[ending-style]:h-0 data-[starting-style]:h-0 border-t border-secondary-a4"
29+
PANEL_DIV = "py-4 px-6"
30+
TRIGGER_ICON = "size-4 shrink-0 transition-all ease-out group-data-[panel-open]:scale-110 group-data-[panel-open]:rotate-45"
31+
32+
33+
class AccordionBaseComponent(BaseUIComponent):
34+
"""Base component for accordion components."""
35+
36+
library = f"{PACKAGE_NAME}/accordion"
37+
38+
@property
39+
def import_var(self):
40+
"""Return the import variable for the accordion component."""
41+
return ImportVar(tag="Accordion", package_path="", install=False)
42+
43+
44+
class AccordionRoot(AccordionBaseComponent):
45+
"""Groups all parts of the accordion."""
46+
47+
tag = "Accordion.Root"
48+
49+
# The uncontrolled value of the item(s) that should be initially expanded. To render a controlled accordion, use the `value` prop instead.
50+
default_value: Var[list[str]]
51+
52+
# The controlled value of the item(s) that should be expanded. To render an uncontrolled accordion, use the `default_value` prop instead.
53+
value: Var[list[str]]
54+
55+
# Event handler called when an accordion item is expanded or collapsed. Provides the new value as an argument.
56+
on_value_change: EventHandler[passthrough_event_spec(list[str])]
57+
58+
# Allows the browser's built-in page search to find and expand the panel contents. Overrides the `keep_mounted` prop and uses `hidden="until-found"` to hide the element without removing it from the DOM. Defaults to False.
59+
hidden_until_found: Var[bool]
60+
61+
# Whether multiple items can be open at the same time. Defaults to True.
62+
open_multiple: Var[bool]
63+
64+
# Whether the component should ignore user interaction. Defaults to False.
65+
disabled: Var[bool]
66+
67+
# Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. Defaults to True.
68+
loop: Var[bool]
69+
70+
# The visual orientation of the accordion. Controls whether roving focus uses left/right or up/down arrow keys. Defaults to 'vertical'.
71+
orientation: Var[LiteralOrientation]
72+
73+
# Whether to keep the element in the DOM while the panel is closed. This prop is ignored when hidden_until_found is used. Defaults to False.
74+
keep_mounted: Var[bool]
75+
76+
# The render prop.
77+
render_: Var[Component]
78+
79+
@classmethod
80+
def create(cls, *children, **props) -> BaseUIComponent:
81+
"""Create the accordion root component."""
82+
props["data-slot"] = "accordion"
83+
cls.set_class_name(ClassNames.ROOT, props)
84+
return super().create(*children, **props)
85+
86+
87+
class AccordionItem(AccordionBaseComponent):
88+
"""Groups an accordion header with the corresponding panel."""
89+
90+
tag = "Accordion.Item"
91+
92+
# The value that identifies this item.
93+
value: Var[str]
94+
95+
# Event handler called when the panel is opened or closed.
96+
on_open_change: EventHandler[passthrough_event_spec(bool)]
97+
98+
# Whether the component should ignore user interaction. Defaults to False.
99+
disabled: Var[bool]
100+
101+
# The render prop.
102+
render_: Var[Component]
103+
104+
@classmethod
105+
def create(cls, *children, **props) -> BaseUIComponent:
106+
"""Create the accordion item component."""
107+
props["data-slot"] = "accordion-item"
108+
cls.set_class_name(ClassNames.ITEM, props)
109+
return super().create(*children, **props)
110+
111+
112+
class AccordionHeader(AccordionBaseComponent):
113+
"""A heading that labels the corresponding panel."""
114+
115+
tag = "Accordion.Header"
116+
117+
# The render prop.
118+
render_: Var[Component]
119+
120+
@classmethod
121+
def create(cls, *children, **props) -> BaseUIComponent:
122+
"""Create the accordion header component."""
123+
props["data-slot"] = "accordion-header"
124+
cls.set_class_name(ClassNames.HEADER, props)
125+
return super().create(*children, **props)
126+
127+
128+
class AccordionTrigger(AccordionBaseComponent):
129+
"""A button that opens and closes the corresponding panel."""
130+
131+
tag = "Accordion.Trigger"
132+
133+
# Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (e.g. `<div>`). Defaults to True.
134+
native_button: Var[bool]
135+
136+
# The render prop.
137+
render_: Var[Component]
138+
139+
@classmethod
140+
def create(cls, *children, **props) -> BaseUIComponent:
141+
"""Create the accordion trigger component."""
142+
props["data-slot"] = "accordion-trigger"
143+
cls.set_class_name(ClassNames.TRIGGER, props)
144+
return super().create(*children, **props)
145+
146+
147+
class AccordionPanel(AccordionBaseComponent):
148+
"""A collapsible panel with the accordion item contents."""
149+
150+
tag = "Accordion.Panel"
151+
152+
# Allows the browser's built-in page search to find and expand the panel contents. Overrides the `keep_mounted` prop and uses `hidden="until-found"` to hide the element without removing it from the DOM. Defaults to False.
153+
hidden_until_found: Var[bool]
154+
155+
# Whether to keep the element in the DOM while the panel is closed. This prop is ignored when `hidden_until_found` is used. Defaults to False.
156+
keep_mounted: Var[bool]
157+
158+
# The render prop.
159+
render_: Var[Component]
160+
161+
@classmethod
162+
def create(cls, *children, **props) -> BaseUIComponent:
163+
"""Create the accordion panel component."""
164+
props["data-slot"] = "accordion-panel"
165+
cls.set_class_name(ClassNames.PANEL, props)
166+
return super().create(*children, **props)
167+
168+
169+
class HighLevelAccordion(AccordionRoot):
170+
"""High level wrapper for the Accordion component."""
171+
172+
items: Var[ITEMS_TYPE] | ITEMS_TYPE
173+
174+
_item_props = {"on_open_change", "disabled"}
175+
_trigger_props = {"native_button"}
176+
_panel_props = {"hidden_until_found", "keep_mounted"}
177+
178+
@classmethod
179+
def create(
180+
cls,
181+
items: Var[ITEMS_TYPE] | ITEMS_TYPE,
182+
**props,
183+
) -> BaseUIComponent:
184+
"""Create a high level accordion component.
185+
186+
Args:
187+
items: List of dictionaries with 'trigger', 'content', and optional 'value' and 'disabled' keys.
188+
**props: Additional properties to apply to the accordion component.
189+
190+
Returns:
191+
The accordion component with all necessary subcomponents.
192+
"""
193+
# Extract props for different parts
194+
item_props = {k: props.pop(k) for k in cls._item_props & props.keys()}
195+
trigger_props = {k: props.pop(k) for k in cls._trigger_props & props.keys()}
196+
panel_props = {k: props.pop(k) for k in cls._panel_props & props.keys()}
197+
198+
if isinstance(items, Var):
199+
accordion_items = foreach(
200+
items,
201+
lambda item: cls._create_accordion_item_dynamic(
202+
item, item_props, trigger_props, panel_props
203+
),
204+
)
205+
return AccordionRoot.create(accordion_items, **props)
206+
accordion_items = [
207+
cls._create_accordion_item(
208+
item, index, item_props, trigger_props, panel_props
209+
)
210+
for index, item in enumerate(items)
211+
]
212+
return AccordionRoot.create(*accordion_items, **props)
213+
214+
@classmethod
215+
def _create_trigger_icon(cls) -> Component:
216+
"""Create the accordion trigger icon."""
217+
return icon(
218+
"PlusSignIcon",
219+
class_name=ClassNames.TRIGGER_ICON,
220+
data_slot="accordion-trigger-icon",
221+
)
222+
223+
@classmethod
224+
def _create_accordion_item(
225+
cls,
226+
item: dict[str, str | Component],
227+
index: int,
228+
item_props: dict,
229+
trigger_props: dict,
230+
panel_props: dict,
231+
) -> BaseUIComponent:
232+
"""Create a single accordion item from a dictionary (for normal lists)."""
233+
return AccordionItem.create(
234+
AccordionHeader.create(
235+
AccordionTrigger.create(
236+
item.get("trigger"),
237+
cls._create_trigger_icon(),
238+
**trigger_props,
239+
),
240+
),
241+
AccordionPanel.create(
242+
Div.create(
243+
item.get("content"),
244+
class_name=ClassNames.PANEL_DIV,
245+
data_slot="accordion-panel-div",
246+
),
247+
**panel_props,
248+
),
249+
value=item.get("value", f"item-{index + 1}"),
250+
disabled=item.get("disabled", False),
251+
**item_props,
252+
)
253+
254+
@classmethod
255+
def _create_accordion_item_dynamic(
256+
cls,
257+
item: ObjectVar[dict[str, str | Component]],
258+
item_props: dict,
259+
trigger_props: dict,
260+
panel_props: dict,
261+
) -> BaseUIComponent:
262+
"""Create a single accordion item from a dictionary (for Var items)."""
263+
return AccordionItem.create(
264+
AccordionHeader.create(
265+
AccordionTrigger.create(
266+
item["trigger"],
267+
cls._create_trigger_icon(),
268+
**trigger_props,
269+
),
270+
),
271+
AccordionPanel.create(
272+
Div.create(
273+
item["content"],
274+
class_name=ClassNames.PANEL_DIV,
275+
data_slot="accordion-panel-div",
276+
),
277+
**panel_props,
278+
),
279+
value=item.get("value", ""),
280+
disabled=item.get("disabled", False).bool(),
281+
**item_props,
282+
)
283+
284+
285+
class Accordion(ComponentNamespace):
286+
"""Namespace for Accordion components."""
287+
288+
root = staticmethod(AccordionRoot.create)
289+
item = staticmethod(AccordionItem.create)
290+
header = staticmethod(AccordionHeader.create)
291+
trigger = staticmethod(AccordionTrigger.create)
292+
panel = staticmethod(AccordionPanel.create)
293+
__call__ = staticmethod(HighLevelAccordion.create)
294+
295+
296+
accordion = Accordion()

reflex_ui/components/base/dialog.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ class DialogBackdrop(DialogBaseComponent):
106106

107107
tag = "Dialog.Backdrop"
108108

109+
# Whether the backdrop is forced to render even when nested. Defaults to False.
110+
force_render: Var[bool]
111+
109112
# The render prop
110113
render_: Var[Component]
111114

reflex_ui/components/base/select.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,20 @@ class SelectRoot(SelectBaseComponent):
8484

8585
# Determines if the select enters a modal state when open.
8686
# - True: user interaction is limited to the select: document page scroll is locked and pointer interactions on outside elements are disabled.
87-
# - False: user interaction with the rest of the document is allowed.
88-
modal: Var[bool] = Var.create(True)
87+
# - False: user interaction with the rest of the document is allowed. Defaults to True.
88+
modal: Var[bool]
8989

90-
# Whether the component should ignore user interaction
91-
disabled: Var[bool] = Var.create(False)
90+
# Whether multiple items can be selected. Defaults to False.
91+
multiple: Var[bool]
9292

93-
# Whether the user should be unable to choose a different option from the select menu
94-
read_only: Var[bool] = Var.create(False)
93+
# Whether the component should ignore user interaction. Defaults to False.
94+
disabled: Var[bool]
95+
96+
# Whether the user should be unable to choose a different option from the select menu. Defaults to False.
97+
read_only: Var[bool]
9598

96-
# Whether the user must choose a value before submitting a form
97-
required: Var[bool] = Var.create(False)
99+
# Whether the user must choose a value before submitting a form. Defaults to False.
100+
required: Var[bool]
98101

99102
@classmethod
100103
def create(cls, *children, **props) -> BaseUIComponent:
@@ -108,8 +111,8 @@ class SelectTrigger(SelectBaseComponent):
108111

109112
tag = "Select.Trigger"
110113

111-
# Whether the component should ignore user interaction
112-
disabled: Var[bool] = Var.create(False)
114+
# Whether the component should ignore user interaction. Defaults to False.
115+
disabled: Var[bool]
113116

114117
# The render prop
115118
render_: Var[Component]

reflex_ui/components/base_ui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from reflex_ui.components.component import CoreComponent
66

77
PACKAGE_NAME = "@base-ui-components/react"
8-
PACKAGE_VERSION = "1.0.0-beta.1"
8+
PACKAGE_VERSION = "1.0.0-beta.2"
99

1010

1111
class BaseUIComponent(CoreComponent):

0 commit comments

Comments
 (0)