Skip to content

Commit c532ba8

Browse files
authored
ENG-6159: Add scroll area component (#9)
1 parent 2d638a4 commit c532ba8

File tree

3 files changed

+203
-1
lines changed

3 files changed

+203
-1
lines changed

reflex_ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"components.base.avatar": ["avatar"],
77
"components.base.badge": ["badge"],
88
"components.base.button": ["button"],
9+
"components.base.scroll_area": ["scroll_area"],
910
"components.base.select": ["select"],
1011
"components.base.skeleton": ["skeleton"],
1112
"components.base.theme_switcher": ["theme_switcher"],
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
"""Custom scroll area component."""
2+
3+
from typing import Literal
4+
5+
from reflex.components.component import Component, ComponentNamespace
6+
from reflex.components.core.cond import cond
7+
from reflex.utils.imports import ImportVar
8+
from reflex.vars import Var
9+
10+
from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent
11+
from reflex_ui.utils.twmerge import cn
12+
13+
LiteralOrientation = Literal["horizontal", "vertical"]
14+
15+
16+
class ClassNames:
17+
"""Class names for scroll area components."""
18+
19+
ROOT = "h-full outline-none focus:outline-none"
20+
VIEWPORT = "h-full overscroll-contain"
21+
CONTENT = ""
22+
SCROLLBAR_BASE = "flex touch-none p-0.5 opacity-0 transition-[colors,opacity] delay-200 select-none data-hovering:opacity-100 data-hovering:delay-0 data-hovering:duration-100 data-scrolling:opacity-100 data-scrolling:delay-0 data-scrolling:duration-100"
23+
SCROLLBAR_VERTICAL = "w-2"
24+
SCROLLBAR_HORIZONTAL = "h-2"
25+
THUMB = "w-full rounded-full bg-secondary-a5"
26+
CORNER = "bg-secondary-a3"
27+
28+
29+
class ScrollAreaBaseComponent(BaseUIComponent):
30+
"""Base component for scroll area components."""
31+
32+
library = f"{PACKAGE_NAME}/scroll-area"
33+
34+
@property
35+
def import_var(self):
36+
"""Return the import variable for the scroll area component."""
37+
return ImportVar(tag="ScrollArea", package_path="", install=False)
38+
39+
40+
class ScrollAreaRoot(ScrollAreaBaseComponent):
41+
"""The root of the scroll area."""
42+
43+
tag = "ScrollArea.Root"
44+
45+
# Render prop
46+
render_: Var[Component]
47+
48+
@classmethod
49+
def create(cls, *children, **props) -> Component:
50+
"""Create the scroll area root component."""
51+
cls.set_class_name(ClassNames.ROOT, props)
52+
return super().create(*children, **props)
53+
54+
55+
class ScrollAreaViewport(ScrollAreaBaseComponent):
56+
"""The viewport of the scroll area."""
57+
58+
tag = "ScrollArea.Viewport"
59+
60+
# Render prop
61+
render_: Var[Component]
62+
63+
@classmethod
64+
def create(cls, *children, **props) -> Component:
65+
"""Create the scroll area viewport component."""
66+
cls.set_class_name(ClassNames.VIEWPORT, props)
67+
return super().create(*children, **props)
68+
69+
70+
class ScrollAreaContent(ScrollAreaBaseComponent):
71+
"""A container for the content of the scroll area."""
72+
73+
tag = "ScrollArea.Content"
74+
75+
# Render prop
76+
render_: Var[Component]
77+
78+
@classmethod
79+
def create(cls, *children, **props) -> Component:
80+
"""Create the scroll area content component."""
81+
cls.set_class_name(ClassNames.CONTENT, props)
82+
return super().create(*children, **props)
83+
84+
85+
class ScrollAreaScrollbar(ScrollAreaBaseComponent):
86+
"""The scrollbar of the scroll area."""
87+
88+
tag = "ScrollArea.Scrollbar"
89+
90+
# Orientation of the scrollbar
91+
orientation: Var[LiteralOrientation] = Var.create("vertical")
92+
93+
# Whether to keep the HTML element in the DOM when the viewport isn't scrollable
94+
keep_mounted: Var[bool] = Var.create(False)
95+
96+
# Render prop
97+
render_: Var[Component]
98+
99+
@classmethod
100+
def create(cls, *children, **props) -> Component:
101+
"""Create the scroll area scrollbar component."""
102+
orientation = props.get("orientation", "vertical")
103+
104+
scrollbar_classes = cn(
105+
ClassNames.SCROLLBAR_BASE,
106+
cond(
107+
orientation == "vertical",
108+
ClassNames.SCROLLBAR_VERTICAL,
109+
ClassNames.SCROLLBAR_HORIZONTAL,
110+
),
111+
)
112+
113+
cls.set_class_name(scrollbar_classes, props)
114+
return super().create(*children, **props)
115+
116+
117+
class ScrollAreaThumb(ScrollAreaBaseComponent):
118+
"""The thumb of the scrollbar."""
119+
120+
tag = "ScrollArea.Thumb"
121+
122+
# Render prop
123+
render_: Var[Component]
124+
125+
@classmethod
126+
def create(cls, *children, **props) -> Component:
127+
"""Create the scroll area thumb component."""
128+
cls.set_class_name(ClassNames.THUMB, props)
129+
return super().create(*children, **props)
130+
131+
132+
class ScrollAreaCorner(ScrollAreaBaseComponent):
133+
"""A small rectangular area that appears at the intersection of horizontal and vertical scrollbars."""
134+
135+
tag = "ScrollArea.Corner"
136+
137+
# Render prop
138+
render_: Var[Component]
139+
140+
@classmethod
141+
def create(cls, *children, **props) -> Component:
142+
"""Create the scroll area corner component."""
143+
cls.set_class_name(ClassNames.CORNER, props)
144+
return super().create(*children, **props)
145+
146+
147+
class HighLevelScrollArea(ScrollAreaRoot):
148+
"""High level wrapper for the Scroll Area component."""
149+
150+
# Orientation of the scroll area
151+
orientation: Var[LiteralOrientation] = Var.create("vertical")
152+
153+
# Whether to keep the HTML element in the DOM when the viewport isn't scrollable
154+
keep_mounted: Var[bool] = Var.create(False)
155+
156+
# Props for different component parts
157+
_scrollbar_props = {"orientation", "keep_mounted"}
158+
159+
@classmethod
160+
def create(cls, *children, **props) -> Component:
161+
"""Create a high level scroll area component.
162+
163+
Args:
164+
*children: The content to be scrollable.
165+
**props: Additional properties to apply to the scroll area component.
166+
167+
Returns:
168+
The scroll area component.
169+
"""
170+
# Extract props for different parts
171+
scrollbar_props = {k: props.pop(k) for k in cls._scrollbar_props & props.keys()}
172+
173+
return ScrollAreaRoot.create(
174+
ScrollAreaViewport.create(
175+
ScrollAreaContent.create(
176+
*children,
177+
),
178+
),
179+
ScrollAreaScrollbar.create(
180+
ScrollAreaThumb.create(),
181+
**scrollbar_props,
182+
),
183+
**props,
184+
)
185+
186+
187+
class ScrollArea(ComponentNamespace):
188+
"""Namespace for Scroll Area components."""
189+
190+
root = staticmethod(ScrollAreaRoot.create)
191+
viewport = staticmethod(ScrollAreaViewport.create)
192+
content = staticmethod(ScrollAreaContent.create)
193+
scrollbar = staticmethod(ScrollAreaScrollbar.create)
194+
thumb = staticmethod(ScrollAreaThumb.create)
195+
corner = staticmethod(ScrollAreaCorner.create)
196+
__call__ = staticmethod(HighLevelScrollArea.create)
197+
198+
199+
scroll_area = ScrollArea()

reflex_ui/components/component.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ class CoreComponent(Component):
1515
unstyled: Var[bool]
1616

1717
@classmethod
18-
def set_class_name(cls, default_class_name: str, props: dict[str, Any]) -> None:
18+
def set_class_name(
19+
cls, default_class_name: str | Var[str], props: dict[str, Any]
20+
) -> None:
1921
"""Set the class name in props, merging with the default if necessary.
2022
2123
Args:

0 commit comments

Comments
 (0)