Skip to content

Commit fa94fea

Browse files
committed
ENG-6165: Tooltip component
1 parent 69e4bfe commit fa94fea

File tree

4 files changed

+338
-6
lines changed

4 files changed

+338
-6
lines changed

demo/demo/demo.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
def index() -> rx.Component:
99
# Welcome Page (Index)
1010
return rx.el.div(
11-
ui.button(
12-
ui.icon("SmileIcon"),
13-
"Click me",
14-
on_click=rx.toast.success(
15-
"You are cool :)",
16-
position="top-center",
11+
ui.tooltip(
12+
ui.button(
13+
ui.icon("SmileIcon"),
14+
"Click me",
15+
on_click=rx.toast.success(
16+
"You are cool :)",
17+
position="top-center",
18+
),
1719
),
20+
content="Seriously, click me",
1821
),
1922
ui.slider(
2023
on_value_committed=lambda value: rx.toast.success(f"Value: {value}"),

reflex_ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"components.base.slider": ["slider"],
1313
"components.base.switch": ["switch"],
1414
"components.base.theme_switcher": ["theme_switcher"],
15+
"components.base.tooltip": ["tooltip"],
1516
}
1617

1718
_SUBMODULES = {"components", "utils"}
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
"""Tooltip component from base-ui components."""
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 import Var
9+
10+
from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent
11+
from reflex_ui.components.icons.others import arrow_svg
12+
13+
LiteralSide = Literal["top", "right", "bottom", "left", "inline-end", "inline-start"]
14+
LiteralAlign = Literal["start", "center", "end"]
15+
LiteralPositionMethod = Literal["absolute", "fixed"]
16+
LiteralTrackCursorAxis = Literal["none", "bottom", "x", "y"]
17+
18+
19+
# Constants for default class names
20+
class ClassNames:
21+
"""Class names for tooltip components."""
22+
23+
TRIGGER = "inline-flex items-center justify-center"
24+
POPUP = "z-50 rounded-sm bg-secondary-12 px-3 py-1.5 text-balance text-sm font-medium text-secondary-1 shadow-small transition-all duration-150 data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0"
25+
ARROW = "data-[side=bottom]:top-[-7.5px] data-[side=left]:right-[-12.5px] data-[side=left]:rotate-90 data-[side=right]:left-[-12.5px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-7.5px] data-[side=top]:rotate-180"
26+
27+
28+
class TooltipBaseComponent(BaseUIComponent):
29+
"""Base component for tooltip components."""
30+
31+
library = f"{PACKAGE_NAME}/tooltip"
32+
33+
@property
34+
def import_var(self):
35+
"""Return the import variable for the tooltip component."""
36+
return ImportVar(tag="Tooltip", package_path="", install=False)
37+
38+
39+
class TooltipProvider(TooltipBaseComponent):
40+
"""Provider component for tooltips."""
41+
42+
tag = "Tooltip.Provider"
43+
44+
# How long to wait before opening a tooltip. Specified in milliseconds.
45+
delay: Var[int]
46+
47+
# How long to wait before closing a tooltip. Specified in milliseconds.
48+
close_delay: Var[int]
49+
50+
# Another tooltip will open instantly if the previous tooltip is closed within this timeout. Specified in milliseconds. Defaults to 400.
51+
timeout: Var[int]
52+
53+
@classmethod
54+
def create(cls, *children, **props) -> Component:
55+
"""Create the tooltip provider component."""
56+
return super().create(*children, **props)
57+
58+
59+
class TooltipRoot(TooltipBaseComponent):
60+
"""Root component for a tooltip."""
61+
62+
tag = "Tooltip.Root"
63+
64+
# Whether the tooltip is currently open.
65+
open: Var[bool]
66+
67+
# Whether the tooltip is initially open. To render a controlled tooltip, use the open prop instead. Defaults to False.
68+
default_open: Var[bool]
69+
70+
# Event handler called when the tooltip is opened or closed.
71+
on_open_change: EventHandler[passthrough_event_spec(bool, dict, str)]
72+
73+
# Event handler called after any animations complete when the tooltip is opened or closed.
74+
on_open_change_complete: EventHandler[passthrough_event_spec(bool)]
75+
76+
# Determines which axis the tooltip should track the cursor on. Defaults to "None".
77+
track_cursor_axis: Var[LiteralTrackCursorAxis]
78+
79+
# Whether the tooltip is disabled. Defaults to False.
80+
disabled: Var[bool]
81+
82+
# How long to wait before opening the tooltip. Specified in milliseconds. Defaults to 600.
83+
delay: Var[int]
84+
85+
# How long to wait before closing the tooltip. Specified in milliseconds. Defaults to 0.
86+
close_delay: Var[int]
87+
88+
# Whether the tooltip contents can be hovered without closing the tooltip. Defaults to True.
89+
hoverable: Var[bool]
90+
91+
@classmethod
92+
def create(cls, *children, **props) -> Component:
93+
"""Create the tooltip root component."""
94+
return super().create(*children, **props)
95+
96+
97+
class TooltipTrigger(TooltipBaseComponent):
98+
"""Trigger element for the tooltip."""
99+
100+
tag = "Tooltip.Trigger"
101+
102+
# The render prop
103+
render_: Var[Component]
104+
105+
@classmethod
106+
def create(cls, *children, **props) -> Component:
107+
"""Create the tooltip trigger component."""
108+
cls.set_class_name(ClassNames.TRIGGER, props)
109+
return super().create(*children, **props)
110+
111+
112+
class TooltipPortal(TooltipBaseComponent):
113+
"""Portal that moves the tooltip to a different part of the DOM."""
114+
115+
tag = "Tooltip.Portal"
116+
117+
# A parent element to render the portal element into.
118+
container: Var[str]
119+
120+
# Whether to keep the portal mounted in the DOM while the popup is hidden. Defaults to False.
121+
keep_mounted: Var[bool]
122+
123+
124+
class TooltipPositioner(TooltipBaseComponent):
125+
"""Positions the tooltip relative to the trigger."""
126+
127+
tag = "Tooltip.Positioner"
128+
129+
# How to align the popup relative to the specified side. Defaults to "center".
130+
align: Var[LiteralAlign]
131+
132+
# Additional offset along the alignment axis in pixels. Defaults to 0.
133+
align_offset: Var[int]
134+
135+
# Which side of the anchor element to align the popup against. May automatically change to avoid collisions. Defaults to "top".
136+
side: Var[LiteralSide]
137+
138+
# Distance between the anchor and the popup in pixels. Defaults to 0.
139+
side_offset: Var[int]
140+
141+
# Minimum distance to maintain between the arrow and the edges of the popup. Use it to prevent the arrow element from hanging out of the rounded corners of a popup. Defaults to 5.
142+
arrow_padding: Var[int]
143+
144+
# An element to position the popup against. By default, the popup will be positioned against the trigger.
145+
anchor: Var[str]
146+
147+
# An element or a rectangle that delimits the area that the popup is confined to. Defaults to "clipping-ancestors".
148+
collision_boundary: Var[str]
149+
150+
# Additional space to maintain from the edge of the collision boundary. Defaults to 5.
151+
collision_padding: Var[int]
152+
153+
# Whether to maintain the popup in the viewport after the anchor element was scrolled out of view. Defaults to False.
154+
sticky: Var[bool]
155+
156+
# Determines which CSS position property to use. Defaults to "absolute".
157+
position_method: Var[LiteralPositionMethod]
158+
159+
# Indicates whether the tooltip should track the anchor's position
160+
track_anchor: Var[bool]
161+
162+
# Determines how to handle collisions when positioning the popup.
163+
collision_avoidance: Var[str]
164+
165+
# Render prop for the positioner
166+
render_: Var[Component]
167+
168+
@classmethod
169+
def create(cls, *children, **props) -> Component:
170+
"""Create the tooltip positioner component."""
171+
return super().create(*children, **props)
172+
173+
174+
class TooltipPopup(TooltipBaseComponent):
175+
"""Container for the tooltip content."""
176+
177+
tag = "Tooltip.Popup"
178+
179+
# Render prop for the popup
180+
render_: Var[Component]
181+
182+
@classmethod
183+
def create(cls, *children, **props) -> Component:
184+
"""Create the tooltip popup component."""
185+
cls.set_class_name(ClassNames.POPUP, props)
186+
return super().create(*children, **props)
187+
188+
189+
class TooltipArrow(TooltipBaseComponent):
190+
"""Arrow element for the tooltip."""
191+
192+
tag = "Tooltip.Arrow"
193+
194+
@classmethod
195+
def create(cls, *children, **props) -> Component:
196+
"""Create the tooltip arrow component."""
197+
cls.set_class_name(ClassNames.ARROW, props)
198+
return super().create(*children, **props)
199+
200+
201+
class HighLevelTooltip(TooltipRoot):
202+
"""High level wrapper for the Tooltip component."""
203+
204+
# Content to display in the tooltip
205+
content: Var[str] | Component
206+
207+
# Props for different component parts
208+
_root_props = {
209+
"open",
210+
"default_open",
211+
"on_open_change",
212+
"on_open_change_complete",
213+
"track_cursor_axis",
214+
"disabled",
215+
"delay",
216+
"close_delay",
217+
"hoverable",
218+
}
219+
_portal_props = {
220+
"container",
221+
"keep_mounted",
222+
}
223+
_positioner_props = {
224+
"align",
225+
"align_offset",
226+
"side",
227+
"side_offset",
228+
"arrow_padding",
229+
"anchor",
230+
"collision_boundary",
231+
"collision_padding",
232+
"sticky",
233+
"position_method",
234+
"track_anchor",
235+
"collision_avoidance",
236+
}
237+
238+
@classmethod
239+
def create(
240+
cls,
241+
trigger_component: Component,
242+
content: str | Component | None = None,
243+
**props,
244+
) -> Component:
245+
"""Create a high level tooltip component.
246+
247+
Args:
248+
trigger_component: The component that triggers the tooltip.
249+
content: The content to display in the tooltip.
250+
**props: Additional properties to apply to the tooltip component.
251+
252+
Returns:
253+
The tooltip component with all necessary subcomponents.
254+
"""
255+
# Extract content from props if provided there
256+
if content is None and "content" in props:
257+
content = props.pop("content")
258+
259+
# Extract props for different parts
260+
root_props = {k: props.pop(k) for k in cls._root_props & props.keys()}
261+
portal_props = {k: props.pop(k) for k in cls._portal_props & props.keys()}
262+
positioner_props = {
263+
k: props.pop(k) for k in cls._positioner_props & props.keys()
264+
}
265+
266+
# Set default values
267+
positioner_props.setdefault("side_offset", 8)
268+
root_props.setdefault("delay", 0)
269+
root_props.setdefault("close_delay", 0)
270+
271+
return TooltipRoot.create(
272+
TooltipTrigger.create(
273+
render_=trigger_component,
274+
),
275+
TooltipPortal.create(
276+
TooltipPositioner.create(
277+
TooltipPopup.create(
278+
TooltipArrow.create(arrow_svg()),
279+
content,
280+
),
281+
**positioner_props,
282+
),
283+
**portal_props,
284+
),
285+
**root_props,
286+
)
287+
288+
289+
class Tooltip(ComponentNamespace):
290+
"""Namespace for Tooltip components."""
291+
292+
provider = staticmethod(TooltipProvider.create)
293+
root = staticmethod(TooltipRoot.create)
294+
trigger = staticmethod(TooltipTrigger.create)
295+
portal = staticmethod(TooltipPortal.create)
296+
positioner = staticmethod(TooltipPositioner.create)
297+
popup = staticmethod(TooltipPopup.create)
298+
arrow = staticmethod(TooltipArrow.create)
299+
__call__ = staticmethod(HighLevelTooltip.create)
300+
301+
302+
tooltip = Tooltip()

reflex_ui/components/icons/others.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,29 @@ def select_arrow_icon(
6060

6161

6262
select_arrow = select_arrow_icon
63+
64+
65+
@memo
66+
def arrow_svg_component() -> Component:
67+
"""Create a tooltip arrow SVG icon.
68+
69+
The arrow SVG icon.
70+
"""
71+
return svg(
72+
svg.path(
73+
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V9H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z",
74+
class_name="fill-secondary-12",
75+
),
76+
svg.path(
77+
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z",
78+
class_name="fill-none",
79+
),
80+
width="20",
81+
height="10",
82+
xmlns="http://www.w3.org/2000/svg",
83+
custom_attrs={"viewBox": "0 0 20 10"},
84+
fill="none",
85+
)
86+
87+
88+
arrow_svg = arrow_svg_component

0 commit comments

Comments
 (0)