Skip to content

Commit ed104a6

Browse files
Add collapsible component wrapper for Base UI
- Implement CollapsibleRoot, CollapsibleTrigger, CollapsiblePanel components - Add HighLevelCollapsible wrapper with trigger and content props - Follow existing component patterns from dialog and menu components - Add to lazy loading system in __init__.py - Include component in demo app for testing - All pre-commit hooks pass (ruff, codespell, pyright) Co-Authored-By: Carlos Cutillas <carlos@reflex.dev>
1 parent 9d97154 commit ed104a6

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

demo/demo/demo.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ def index() -> rx.Component:
5858
on_value_change=lambda value: rx.toast.success(f"Value: {value}"),
5959
on_open_change=lambda value: rx.toast.success(f"Open: {value}"),
6060
),
61+
ui.collapsible(
62+
trigger=ui.button("Toggle Content", variant="outline"),
63+
content=rx.el.div(
64+
"This is the collapsible content! It can contain any components.",
65+
rx.el.p("Here's another paragraph inside the collapsible panel."),
66+
class_name="p-4 bg-secondary-2 rounded-md mt-2",
67+
),
68+
on_open_change=lambda value: rx.toast.success(f"Collapsible open: {value}"),
69+
),
6170
ui.theme_switcher(class_name="absolute top-4 right-4"),
6271
class_name=ui.cn(
6372
"flex flex-col gap-6 items-center justify-center h-screen", "bg-secondary-1"

reflex_ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"components.base.button": ["button"],
99
"components.base.card": ["card"],
1010
"components.base.checkbox": ["checkbox"],
11+
"components.base.collapsible": ["collapsible"],
1112
"components.base.dialog": ["dialog"],
1213
"components.base.gradient_profile": ["gradient_profile"],
1314
"components.base.input": ["input"],
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Custom collapsible component."""
2+
3+
from typing import Literal
4+
5+
from reflex.components.component import Component, ComponentNamespace
6+
from reflex.event import EventHandler, passthrough_event_spec
7+
from reflex.utils.imports import ImportVar
8+
from reflex.vars.base import Var
9+
10+
from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent
11+
12+
13+
class ClassNames:
14+
"""Class names for collapsible components."""
15+
16+
ROOT = ""
17+
TRIGGER = "cursor-pointer focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-4"
18+
PANEL = "overflow-hidden transition-all duration-200 data-[state=closed]:animate-collapse-up data-[state=open]:animate-collapse-down"
19+
20+
21+
class CollapsibleBaseComponent(BaseUIComponent):
22+
"""Base component for collapsible components."""
23+
24+
library = f"{PACKAGE_NAME}/collapsible"
25+
26+
@property
27+
def import_var(self):
28+
"""Return the import variable for the collapsible component."""
29+
return ImportVar(tag="Collapsible", package_path="", install=False)
30+
31+
32+
class CollapsibleRoot(CollapsibleBaseComponent):
33+
"""Groups all parts of the collapsible. Doesn't render its own HTML element."""
34+
35+
tag = "Collapsible.Root"
36+
37+
default_open: Var[bool]
38+
39+
open: Var[bool]
40+
41+
on_open_change: EventHandler[passthrough_event_spec(bool)]
42+
43+
# Whether the component should ignore user interaction.
44+
disabled: Var[bool]
45+
46+
@classmethod
47+
def create(cls, *children, **props) -> BaseUIComponent:
48+
"""Create the collapsible root component."""
49+
props["data-slot"] = "collapsible"
50+
cls.set_class_name(ClassNames.ROOT, props)
51+
return super().create(*children, **props)
52+
53+
54+
class CollapsibleTrigger(CollapsibleBaseComponent):
55+
"""A button that opens and closes the collapsible panel. Renders a <button> element."""
56+
57+
tag = "Collapsible.Trigger"
58+
59+
# 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.
60+
native_button: Var[bool]
61+
62+
# The render prop.
63+
render_: Var[Component]
64+
65+
@classmethod
66+
def create(cls, *children, **props) -> BaseUIComponent:
67+
"""Create the collapsible trigger component."""
68+
props["data-slot"] = "collapsible-trigger"
69+
cls.set_class_name(ClassNames.TRIGGER, props)
70+
return super().create(*children, **props)
71+
72+
73+
class CollapsiblePanel(CollapsibleBaseComponent):
74+
"""A panel with the collapsible contents. Renders a <div> element."""
75+
76+
tag = "Collapsible.Panel"
77+
78+
hidden_until_found: Var[bool]
79+
80+
class_name: Var[str]
81+
82+
keep_mounted: Var[bool]
83+
84+
# The render prop.
85+
render_: Var[Component]
86+
87+
@classmethod
88+
def create(cls, *children, **props) -> BaseUIComponent:
89+
"""Create the collapsible panel component."""
90+
props["data-slot"] = "collapsible-panel"
91+
cls.set_class_name(ClassNames.PANEL, props)
92+
return super().create(*children, **props)
93+
94+
95+
class HighLevelCollapsible(CollapsibleRoot):
96+
"""High level collapsible component."""
97+
98+
trigger: Var[Component | None]
99+
content: Var[str | Component | None]
100+
101+
@classmethod
102+
def create(cls, *children, **props) -> BaseUIComponent:
103+
"""Create the collapsible component."""
104+
trigger = props.pop("trigger", None)
105+
content = props.pop("content", None)
106+
class_name = props.pop("class_name", "")
107+
108+
return CollapsibleRoot.create(
109+
CollapsibleTrigger.create(render_=trigger) if trigger else None,
110+
CollapsiblePanel.create(
111+
content,
112+
*children,
113+
class_name=class_name,
114+
),
115+
**props,
116+
)
117+
118+
def _exclude_props(self) -> list[str]:
119+
return [
120+
*super()._exclude_props(),
121+
"trigger",
122+
"content",
123+
]
124+
125+
126+
class Collapsible(ComponentNamespace):
127+
"""Namespace for Collapsible components."""
128+
129+
root = staticmethod(CollapsibleRoot.create)
130+
trigger = staticmethod(CollapsibleTrigger.create)
131+
panel = staticmethod(CollapsiblePanel.create)
132+
__call__ = staticmethod(HighLevelCollapsible.create)
133+
134+
135+
collapsible = Collapsible()
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Type stubs for collapsible component."""
2+
3+
from typing import Any, Literal
4+
5+
from reflex.components.component import Component, ComponentNamespace
6+
from reflex.event import EventType
7+
from reflex.vars.base import Var
8+
9+
from reflex_ui.components.base_ui import BaseUIComponent
10+
11+
class ClassNames:
12+
ROOT: str
13+
TRIGGER: str
14+
PANEL: str
15+
16+
class CollapsibleBaseComponent(BaseUIComponent):
17+
library: str
18+
@property
19+
def import_var(self): ...
20+
21+
class CollapsibleRoot(CollapsibleBaseComponent):
22+
tag: str
23+
default_open: Var[bool]
24+
open: Var[bool]
25+
on_open_change: EventType[bool]
26+
disabled: Var[bool]
27+
@classmethod
28+
def create(
29+
cls,
30+
*children,
31+
default_open: bool | Var[bool] | None = None,
32+
open: bool | Var[bool] | None = None,
33+
on_open_change: EventType[bool] | None = None,
34+
disabled: bool | Var[bool] | None = None,
35+
**props,
36+
) -> CollapsibleRoot: ...
37+
38+
class CollapsibleTrigger(CollapsibleBaseComponent):
39+
tag: str
40+
native_button: Var[bool]
41+
render_: Var[Component]
42+
@classmethod
43+
def create(
44+
cls,
45+
*children,
46+
native_button: bool | Var[bool] | None = None,
47+
render_: Component | Var[Component] | None = None,
48+
**props,
49+
) -> CollapsibleTrigger: ...
50+
51+
class CollapsiblePanel(CollapsibleBaseComponent):
52+
tag: str
53+
hidden_until_found: Var[bool]
54+
class_name: Var[str]
55+
keep_mounted: Var[bool]
56+
render_: Var[Component]
57+
@classmethod
58+
def create(
59+
cls,
60+
*children,
61+
hidden_until_found: bool | Var[bool] | None = None,
62+
class_name: str | Var[str] | None = None,
63+
keep_mounted: bool | Var[bool] | None = None,
64+
render_: Component | Var[Component] | None = None,
65+
**props,
66+
) -> CollapsiblePanel: ...
67+
68+
class HighLevelCollapsible(CollapsibleRoot):
69+
trigger: Var[Component | None]
70+
content: Var[str | Component | None]
71+
@classmethod
72+
def create(
73+
cls,
74+
*children,
75+
trigger: Component | Var[Component] | None = None,
76+
content: str | Component | Var[str | Component] | None = None,
77+
default_open: bool | Var[bool] | None = None,
78+
open: bool | Var[bool] | None = None,
79+
on_open_change: EventType[bool] | None = None,
80+
disabled: bool | Var[bool] | None = None,
81+
**props,
82+
) -> HighLevelCollapsible: ...
83+
84+
class Collapsible(ComponentNamespace):
85+
root: staticmethod
86+
trigger: staticmethod
87+
panel: staticmethod
88+
__call__: staticmethod
89+
90+
collapsible: Collapsible

0 commit comments

Comments
 (0)