Skip to content

Commit 500361b

Browse files
authored
ENG-6152: Dialog component (#16)
* ENG-6174: Card component * ENG-6152: Dialog component
1 parent 41491a7 commit 500361b

File tree

3 files changed

+297
-0
lines changed

3 files changed

+297
-0
lines changed

demo/demo/demo.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,35 @@ def index() -> rx.Component:
1919
),
2020
content="Seriously, click me",
2121
),
22+
ui.dialog(
23+
trigger=ui.button(
24+
ui.icon("Add02Icon"),
25+
"Open dialog",
26+
variant="secondary",
27+
),
28+
title="Welcome to My App",
29+
description="This dialog provides an overview of the application features and functionality.",
30+
content=rx.el.div(
31+
rx.el.p(
32+
"Did you click the button?",
33+
class_name="text-secondary-11 text-sm font-medium",
34+
),
35+
rx.el.div(
36+
ui.dialog.close(
37+
render_=ui.button(
38+
"Cancel",
39+
variant="outline",
40+
),
41+
),
42+
ui.button(
43+
"Click me",
44+
on_click=rx.toast.success("You are cool :)"),
45+
),
46+
class_name="flex flex-row gap-2 justify-end",
47+
),
48+
class_name="flex flex-col gap-2 w-full",
49+
),
50+
),
2251
ui.checkbox(
2352
label="Click me",
2453
on_checked_change=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
@@ -8,6 +8,7 @@
88
"components.base.button": ["button"],
99
"components.base.card": ["card"],
1010
"components.base.checkbox": ["checkbox"],
11+
"components.base.dialog": ["dialog"],
1112
"components.base.scroll_area": ["scroll_area"],
1213
"components.base.select": ["select"],
1314
"components.base.skeleton": ["skeleton"],
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
"""Custom dialog component."""
2+
3+
from typing import Literal
4+
5+
from reflex.components.component import Component, ComponentNamespace
6+
from reflex.components.el import Div
7+
from reflex.event import EventHandler, passthrough_event_spec
8+
from reflex.utils.imports import ImportVar
9+
from reflex.vars import Var
10+
11+
from reflex_ui.components.base.button import button
12+
from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent
13+
from reflex_ui.components.icons.hugeicon import hi
14+
15+
16+
class ClassNames:
17+
"""Class names for dialog components."""
18+
19+
BACKDROP = "fixed inset-0 bg-black opacity-40 transition-all duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 dark:opacity-80"
20+
POPUP = "fixed top-1/2 left-1/2 -mt-8 w-[31rem] max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-secondary-a4 bg-secondary-1 shadow-large transition-all duration-150 data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0"
21+
TITLE = "text-2xl font-semibold text-secondary-12"
22+
DESCRIPTION = "text-sm text-secondary-11 font-[450]"
23+
HEADER = "flex flex-col gap-2 px-6 pt-6 pb-4"
24+
CONTENT = "flex flex-col gap-4 px-6 pb-6"
25+
TRIGGER = ""
26+
CLOSE = ""
27+
28+
29+
class DialogBaseComponent(BaseUIComponent):
30+
"""Base component for dialog components."""
31+
32+
library = f"{PACKAGE_NAME}/dialog"
33+
34+
@property
35+
def import_var(self):
36+
"""Return the import variable for the dialog component."""
37+
return ImportVar(tag="Dialog", package_path="", install=False)
38+
39+
40+
class DialogRoot(DialogBaseComponent):
41+
"""Groups all parts of the dialog. Doesn't render its own HTML element."""
42+
43+
tag = "Dialog.Root"
44+
45+
# Whether the dialog is initially open. To render a controlled dialog, use the open prop instead.
46+
default_open: Var[bool]
47+
48+
# Whether the dialog is currently open.
49+
open: Var[bool]
50+
51+
# Event handler called when the dialog is opened or closed
52+
on_open_change: EventHandler[passthrough_event_spec(bool, dict, str)]
53+
54+
# Determines whether the dialog should close on outside clicks. Defaults to True.
55+
dismissible: Var[bool]
56+
57+
# Determines if the dialog enters a modal state when open.
58+
# - True: user interaction is limited to just the dialog: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled.
59+
# - False: user interaction with the rest of the document is allowed.
60+
# - 'trap-focus': focus is trapped inside the dialog, but document page scroll is not locked and pointer interactions outside of it remain enabled.
61+
modal: Var[bool | Literal["trap-focus"]]
62+
63+
# Event handler called after any animations complete when the dialog is opened or closed.
64+
on_open_change_complete: EventHandler[passthrough_event_spec(bool)]
65+
66+
@classmethod
67+
def create(cls, *children, **props) -> Component:
68+
"""Create the dialog root component."""
69+
props["data-slot"] = "dialog"
70+
return super().create(*children, **props)
71+
72+
73+
class DialogTrigger(DialogBaseComponent):
74+
"""A button that opens the dialog. Renders a <button> element."""
75+
76+
tag = "Dialog.Trigger"
77+
78+
# 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.
79+
native_button: Var[bool]
80+
81+
# The render prop
82+
render_: Var[Component]
83+
84+
@classmethod
85+
def create(cls, *children, **props) -> Component:
86+
"""Create the dialog trigger component."""
87+
props["data-slot"] = "dialog-trigger"
88+
cls.set_class_name(ClassNames.TRIGGER, props)
89+
return super().create(*children, **props)
90+
91+
92+
class DialogPortal(DialogBaseComponent):
93+
"""A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>."""
94+
95+
tag = "Dialog.Portal"
96+
97+
# A parent element to render the portal element into.
98+
container: Var[str]
99+
100+
# Whether to keep the portal mounted in the DOM while the popup is hidden. Defaults to False.
101+
keep_mounted: Var[bool]
102+
103+
104+
class DialogBackdrop(DialogBaseComponent):
105+
"""An overlay displayed beneath the popup. Renders a <div> element."""
106+
107+
tag = "Dialog.Backdrop"
108+
109+
# The render prop
110+
render_: Var[Component]
111+
112+
@classmethod
113+
def create(cls, *children, **props) -> Component:
114+
"""Create the dialog backdrop component."""
115+
props["data-slot"] = "dialog-backdrop"
116+
cls.set_class_name(ClassNames.BACKDROP, props)
117+
return super().create(*children, **props)
118+
119+
120+
class DialogPopup(DialogBaseComponent):
121+
"""A container for the dialog contents. Renders a <div> element."""
122+
123+
tag = "Dialog.Popup"
124+
125+
# Determines the element to focus when the dialog is opened. By default, the first focusable element is focused.
126+
initial_focus: Var[str]
127+
128+
# Determines the element to focus when the dialog is closed. By default, focus returns to the trigger.
129+
final_focus: Var[str]
130+
131+
# The render prop
132+
render_: Var[Component]
133+
134+
@classmethod
135+
def create(cls, *children, **props) -> Component:
136+
"""Create the dialog popup component."""
137+
props["data-slot"] = "dialog-popup"
138+
cls.set_class_name(ClassNames.POPUP, props)
139+
return super().create(*children, **props)
140+
141+
142+
class DialogTitle(DialogBaseComponent):
143+
"""A heading that labels the dialog. Renders an <h2> element."""
144+
145+
tag = "Dialog.Title"
146+
147+
# The render prop
148+
render_: Var[Component]
149+
150+
@classmethod
151+
def create(cls, *children, **props) -> Component:
152+
"""Create the dialog title component."""
153+
props["data-slot"] = "dialog-title"
154+
cls.set_class_name(ClassNames.TITLE, props)
155+
return super().create(*children, **props)
156+
157+
158+
class DialogDescription(DialogBaseComponent):
159+
"""A paragraph with additional information about the dialog. Renders a <p> element.."""
160+
161+
tag = "Dialog.Description"
162+
163+
# The render prop
164+
render_: Var[Component]
165+
166+
@classmethod
167+
def create(cls, *children, **props) -> Component:
168+
"""Create the dialog description component."""
169+
props["data-slot"] = "dialog-description"
170+
cls.set_class_name(ClassNames.DESCRIPTION, props)
171+
return super().create(*children, **props)
172+
173+
174+
class DialogClose(DialogBaseComponent):
175+
"""A paragraph with additional information about the dialog. Renders a <p> element."""
176+
177+
tag = "Dialog.Close"
178+
179+
# 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.
180+
native_button: Var[bool]
181+
182+
# The render prop
183+
render_: Var[Component]
184+
185+
@classmethod
186+
def create(cls, *children, **props) -> Component:
187+
"""Create the dialog close component."""
188+
props["data-slot"] = "dialog-close"
189+
cls.set_class_name(ClassNames.CLOSE, props)
190+
return super().create(*children, **props)
191+
192+
193+
class HighLevelDialog(DialogRoot):
194+
"""High level dialog component."""
195+
196+
# Dialog props
197+
trigger: Var[Component | None]
198+
content: Var[str | Component | None]
199+
title: Var[str | Component | None]
200+
description: Var[str | Component | None]
201+
202+
@classmethod
203+
def create(cls, *children, **props) -> Component:
204+
"""Create the dialog component."""
205+
trigger = props.pop("trigger", None)
206+
content = props.pop("content", None)
207+
title = props.pop("title", None)
208+
description = props.pop("description", None)
209+
class_name = props.pop("class_name", "")
210+
211+
return DialogRoot.create(
212+
DialogTrigger.create(render_=trigger) if trigger else None,
213+
DialogPortal.create(
214+
DialogBackdrop.create(),
215+
DialogPopup.create(
216+
Div.create(
217+
Div.create(
218+
DialogTitle.create(title) if title else None,
219+
DialogClose.create(
220+
render_=button(
221+
hi("Cancel01Icon"),
222+
variant="ghost",
223+
size="icon-sm",
224+
class_name="text-secondary-11",
225+
),
226+
),
227+
class_name="flex flex-row justify-between items-baseline gap-1",
228+
),
229+
DialogDescription.create(description) if description else None,
230+
class_name=ClassNames.HEADER,
231+
),
232+
Div.create(
233+
content,
234+
class_name=ClassNames.CONTENT,
235+
),
236+
*children,
237+
class_name=class_name,
238+
),
239+
),
240+
**props,
241+
)
242+
243+
def _exclude_props(self) -> list[str]:
244+
return [
245+
*super()._exclude_props(),
246+
"trigger",
247+
"content",
248+
"title",
249+
"description",
250+
]
251+
252+
253+
class Dialog(ComponentNamespace):
254+
"""Namespace for Dialog components."""
255+
256+
root = staticmethod(DialogRoot.create)
257+
trigger = staticmethod(DialogTrigger.create)
258+
portal = staticmethod(DialogPortal.create)
259+
backdrop = staticmethod(DialogBackdrop.create)
260+
popup = staticmethod(DialogPopup.create)
261+
title = staticmethod(DialogTitle.create)
262+
description = staticmethod(DialogDescription.create)
263+
close = staticmethod(DialogClose.create)
264+
__call__ = staticmethod(HighLevelDialog.create)
265+
266+
267+
dialog = Dialog()

0 commit comments

Comments
 (0)