reflex-react-joyride 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reflex_react_joyride-0.1.0/LICENSE +21 -0
- reflex_react_joyride-0.1.0/PKG-INFO +341 -0
- reflex_react_joyride-0.1.0/README.md +313 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/__init__.py +81 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/actions.py +104 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/constants.py +188 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/events.py +73 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/joyride.py +282 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/joyride.pyi +149 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/joyride_tour.jsx +277 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/props.py +252 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/py.typed +0 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/PKG-INFO +341 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/SOURCES.txt +21 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/dependency_links.txt +1 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/requires.txt +1 -0
- reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/top_level.txt +1 -0
- reflex_react_joyride-0.1.0/pyproject.toml +85 -0
- reflex_react_joyride-0.1.0/setup.cfg +4 -0
- reflex_react_joyride-0.1.0/tests/test_actions.py +49 -0
- reflex_react_joyride-0.1.0/tests/test_component.py +122 -0
- reflex_react_joyride-0.1.0/tests/test_constants.py +28 -0
- reflex_react_joyride-0.1.0/tests/test_props.py +64 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ernesto Crespo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reflex-react-joyride
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reflex custom component for react-joyride: guided product tours and onboarding walkthroughs, with controlled mode, theming, custom tooltips and Python-driven controls
|
|
5
|
+
Author-email: Ernesto Crespo <ecrespo@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ecrespo/reflex-react-joyride
|
|
8
|
+
Project-URL: Source, https://github.com/ecrespo/reflex-react-joyride
|
|
9
|
+
Project-URL: Issues, https://github.com/ecrespo/reflex-react-joyride/issues
|
|
10
|
+
Keywords: reflex,reflex-custom-components,react-joyride,joyride,tour,onboarding,walkthrough,product-tour
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: reflex>=0.9.11
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# reflex-react-joyride
|
|
30
|
+
|
|
31
|
+
Guided product tours and onboarding walkthroughs for [Reflex](https://reflex.dev), powered by [react-joyride](https://react-joyride.com) v3.
|
|
32
|
+
|
|
33
|
+
- Wraps **react-joyride 3.2.0** through its `useJoyride` hook, so the tour's controls are reachable from Python
|
|
34
|
+
- Typed props for everything: `Step`, `TourOptions`, `Locale`, `TourStyles`, `FloatingOptions` — `snake_case` in Python, `camelCase` in the browser
|
|
35
|
+
- **Uncontrolled** (`run`) or **controlled** (`step_index`) mode, whichever fits your State
|
|
36
|
+
- **Python actions**: `start`, `next_step`, `prev_step`, `go_to`, `skip`, `reset`, `replay`, `stop`, `open_tooltip`, `get_state`
|
|
37
|
+
- One catch-all `on_event` plus a dedicated trigger per event (`on_tour_start`, `on_step_after`, `on_target_not_found`, ...) with a flat, JSON-safe payload
|
|
38
|
+
- **Custom tooltip and beacon** written as ordinary Reflex components
|
|
39
|
+
- Step content and titles accept Reflex components, not just strings
|
|
40
|
+
- Constants mirroring react-joyride's literals: `EVENTS`, `ACTIONS`, `STATUS`, `LIFECYCLE`, `ORIGIN`
|
|
41
|
+
|
|
42
|
+
## Screenshots
|
|
43
|
+
|
|
44
|
+
The four pages of the demo app, in order:
|
|
45
|
+
|
|
46
|
+
| | |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
|  |  |
|
|
49
|
+
| A continuous tour over a small dashboard, with the event log filling up | Controlled mode: the step index lives in the State and external buttons drive it |
|
|
50
|
+
|  |  |
|
|
51
|
+
| `locale`, `options` and `styles` restyling the built-in tooltip | The same step rendered by a Reflex component through the `tooltip` render prop |
|
|
52
|
+
|  |  |
|
|
53
|
+
| `placement="center"` on `body` for a modal-like opening step | A spotlight over a different element than the one the tooltip points at |
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install reflex-react-joyride
|
|
59
|
+
# or
|
|
60
|
+
uv add reflex-react-joyride
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import reflex as rx
|
|
67
|
+
import reflex_react_joyride as rjr
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TourState(rx.State):
|
|
71
|
+
run: bool = False
|
|
72
|
+
|
|
73
|
+
@rx.event
|
|
74
|
+
def start(self):
|
|
75
|
+
self.run = True
|
|
76
|
+
|
|
77
|
+
@rx.event
|
|
78
|
+
def finished(self, event: rjr.JoyrideEvent):
|
|
79
|
+
self.run = False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def index() -> rx.Component:
|
|
83
|
+
return rx.vstack(
|
|
84
|
+
rjr.joyride(
|
|
85
|
+
tour_id="welcome",
|
|
86
|
+
run=TourState.run,
|
|
87
|
+
continuous=True,
|
|
88
|
+
steps=[
|
|
89
|
+
rjr.Step(target="#logo", title="Welcome", content="This is the app."),
|
|
90
|
+
rjr.Step(target="#cta", content="Start here.", placement="top"),
|
|
91
|
+
],
|
|
92
|
+
options=rjr.TourOptions(show_progress=True, primary_color="#7c3aed"),
|
|
93
|
+
on_tour_end=TourState.finished,
|
|
94
|
+
),
|
|
95
|
+
rx.heading("Acme", id="logo"),
|
|
96
|
+
rx.button("Get started", id="cta"),
|
|
97
|
+
rx.button("Take the tour", on_click=TourState.start),
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Steps
|
|
102
|
+
|
|
103
|
+
A step needs a `target` (any CSS selector) and some `content`. Everything in
|
|
104
|
+
`TourOptions` can be overridden per step:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
rjr.Step(
|
|
108
|
+
target="#sidebar",
|
|
109
|
+
title="Navigation",
|
|
110
|
+
content=rx.vstack(rx.text("Every section lives here."), rx.code("Cmd+K")),
|
|
111
|
+
placement="right", # top | bottom | left | right | auto | center (+ -start / -end)
|
|
112
|
+
spotlight_padding=16,
|
|
113
|
+
show_progress=True,
|
|
114
|
+
id="sidebar", # echoed back as event["step_id"]
|
|
115
|
+
data={"section": "nav"}, # echoed back as event["step_data"], keys untouched
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`target="body"` with `placement="center"` gives a modal-like step, which is a
|
|
120
|
+
good opener. Plain dicts work as well as `Step` objects, and so do steps kept in
|
|
121
|
+
a State var:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
class State(rx.State):
|
|
125
|
+
steps: list[dict] = [{"target": "#logo", "content": "Hi", "show_progress": True}]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
rjr.joyride(steps=State.steps)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`snake_case` keys are converted to the `camelCase` react-joyride expects, both
|
|
132
|
+
for literal props (in Python) and for values coming from the State (in the
|
|
133
|
+
browser), so you never have to write `spotlightPadding` yourself. Keys that are
|
|
134
|
+
already `camelCase` pass through untouched.
|
|
135
|
+
|
|
136
|
+
## Theming
|
|
137
|
+
|
|
138
|
+
`options` sets behaviour and colors for the whole tour, `styles` overrides the
|
|
139
|
+
CSS of every element react-joyride renders, and `locale` replaces the strings:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
rjr.joyride(
|
|
143
|
+
steps=STEPS,
|
|
144
|
+
options=rjr.TourOptions(
|
|
145
|
+
primary_color="#059669",
|
|
146
|
+
background_color="#f2fbf7",
|
|
147
|
+
overlay_color="rgba(4, 47, 36, 0.55)",
|
|
148
|
+
show_progress=True,
|
|
149
|
+
skip_beacon=True, # go straight to the tooltip, no beacon
|
|
150
|
+
spotlight_radius=12,
|
|
151
|
+
buttons=["back", "skip", "primary"],
|
|
152
|
+
width=420,
|
|
153
|
+
z_index=1000,
|
|
154
|
+
),
|
|
155
|
+
styles=rjr.TourStyles(
|
|
156
|
+
tooltip={"border_radius": "14px", "box_shadow": "0 18px 40px rgba(15, 23, 42, 0.18)"},
|
|
157
|
+
button_primary={"border_radius": "999px", "padding": "8px 18px"},
|
|
158
|
+
),
|
|
159
|
+
locale=rjr.Locale(
|
|
160
|
+
back="Atrás",
|
|
161
|
+
close="Cerrar",
|
|
162
|
+
last="Finalizar",
|
|
163
|
+
next="Siguiente",
|
|
164
|
+
next_with_progress="Siguiente ({current} de {total})",
|
|
165
|
+
skip="Saltar",
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
CSS properties inside `styles` are converted too: `border_radius` becomes
|
|
171
|
+
`borderRadius`.
|
|
172
|
+
|
|
173
|
+
## Controlled mode
|
|
174
|
+
|
|
175
|
+
Pass `step_index` and react-joyride stops advancing on its own — it renders
|
|
176
|
+
exactly the step your State asks for. Move the index from `on_step_after`:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
class State(rx.State):
|
|
180
|
+
run: bool = False
|
|
181
|
+
index: int = 0
|
|
182
|
+
|
|
183
|
+
@rx.event
|
|
184
|
+
def advance(self, event: rjr.JoyrideEvent):
|
|
185
|
+
self.index = max(0, event["index"] + (-1 if event["action"] == rjr.ACTIONS.PREV else 1))
|
|
186
|
+
|
|
187
|
+
@rx.event
|
|
188
|
+
def skip_missing(self, event: rjr.JoyrideEvent):
|
|
189
|
+
self.index = event["index"] + 1
|
|
190
|
+
|
|
191
|
+
@rx.event
|
|
192
|
+
def finished(self, _event: rjr.JoyrideEvent):
|
|
193
|
+
self.run, self.index = False, 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
rjr.joyride(
|
|
197
|
+
steps=STEPS,
|
|
198
|
+
run=State.run,
|
|
199
|
+
step_index=State.index,
|
|
200
|
+
continuous=True,
|
|
201
|
+
on_step_after=State.advance,
|
|
202
|
+
on_target_not_found=State.skip_missing,
|
|
203
|
+
on_tour_end=State.finished,
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
This is what you want when a step should only open after your own code did
|
|
208
|
+
something — finished a request, opened a drawer, switched a tab.
|
|
209
|
+
|
|
210
|
+
## Events
|
|
211
|
+
|
|
212
|
+
Every trigger receives the same flat dict:
|
|
213
|
+
|
|
214
|
+
| key | meaning |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `type` | the event, e.g. `tour:start`, `step:after`, `error:target_not_found` |
|
|
217
|
+
| `action` | what triggered it: `start`, `next`, `prev`, `close`, `skip`, `update`, ... |
|
|
218
|
+
| `index`, `size`, `is_last_step` | where we are in the tour |
|
|
219
|
+
| `status` | `idle`, `ready`, `waiting`, `running`, `paused`, `skipped`, `finished` |
|
|
220
|
+
| `lifecycle` | `init`, `ready`, `beacon`, `tooltip`, `complete`, ... |
|
|
221
|
+
| `origin` | which control fired it: `button_primary`, `overlay`, `keyboard`, ... |
|
|
222
|
+
| `controlled`, `waiting`, `scrolling` | tour flags |
|
|
223
|
+
| `step_id`, `step_target`, `step_title`, `step_data` | about the current step |
|
|
224
|
+
| `error` | the message for `error` events |
|
|
225
|
+
|
|
226
|
+
Triggers: `on_event` (all of them), `on_tour_start`, `on_tour_end`,
|
|
227
|
+
`on_status_change`, `on_step_before`, `on_step_after`, `on_beacon`,
|
|
228
|
+
`on_tooltip`, `on_scroll_start`, `on_scroll_end`, `on_target_not_found`,
|
|
229
|
+
`on_error`.
|
|
230
|
+
|
|
231
|
+
Compare against the constants instead of raw strings:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
@rx.event
|
|
235
|
+
def on_tour_event(self, event: rjr.JoyrideEvent):
|
|
236
|
+
if event["type"] == rjr.EVENTS.TOUR_END and event["status"] in rjr.FINISHED_STATUSES:
|
|
237
|
+
self.onboarding_done = True
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Python actions
|
|
241
|
+
|
|
242
|
+
Give the tour a `tour_id` and you can drive it from anywhere, without keeping
|
|
243
|
+
the index in your State:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
rx.button("Start", on_click=rjr.start("welcome"))
|
|
247
|
+
rx.button("Step 3", on_click=rjr.go_to("welcome", 2))
|
|
248
|
+
rx.button("Next", on_click=rjr.next_step("welcome"))
|
|
249
|
+
rx.button("Replay this step", on_click=rjr.replay("welcome"))
|
|
250
|
+
rx.button("Skip", on_click=rjr.skip("welcome"))
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`start`, `stop`, `next_step`, `prev_step`, `go_to`, `close`, `skip`, `reset`,
|
|
254
|
+
`replay`, `open_tooltip`, and `get_state(tour_id, callback)` which sends the
|
|
255
|
+
current tour state to an event handler. They are no-ops (with a console warning)
|
|
256
|
+
when no tour with that id is mounted.
|
|
257
|
+
|
|
258
|
+
The tour still needs `run=True` to be running; the actions drive a running tour,
|
|
259
|
+
they do not switch it on.
|
|
260
|
+
|
|
261
|
+
## Custom tooltip and beacon
|
|
262
|
+
|
|
263
|
+
Pass a function returning a Reflex component. It receives what react-joyride
|
|
264
|
+
passes to a custom tooltip, plus `current`, `total`, `progress`, `content`,
|
|
265
|
+
`title`, `stepId`, `stepData`, `isFirstStep` and `isLastStep` (these are
|
|
266
|
+
JavaScript props, so their keys stay `camelCase`):
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
def tooltip(props):
|
|
270
|
+
return rx.card(
|
|
271
|
+
rx.vstack(
|
|
272
|
+
rx.badge(props["progress"], radius="full"),
|
|
273
|
+
rx.heading(props["title"], size="4"),
|
|
274
|
+
rx.text(props["content"], size="2"),
|
|
275
|
+
rx.hstack(
|
|
276
|
+
rx.button(
|
|
277
|
+
"Back",
|
|
278
|
+
variant="soft",
|
|
279
|
+
on_click=rjr.prev_step("welcome"),
|
|
280
|
+
disabled=props["isFirstStep"].to(bool),
|
|
281
|
+
),
|
|
282
|
+
rx.button(
|
|
283
|
+
rx.cond(props["isLastStep"].to(bool), "Done", "Next"), on_click=rjr.next_step("welcome")
|
|
284
|
+
),
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
width="360px",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
rjr.joyride(steps=STEPS, tour_id="welcome", tooltip=tooltip, beacon=lambda _p: rx.box(...))
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The component is rendered inside react-joyride's own wrapper, so the ARIA
|
|
295
|
+
attributes and positioning are handled for you. It can read your State like any
|
|
296
|
+
other component.
|
|
297
|
+
|
|
298
|
+
## Rendering above the overlay
|
|
299
|
+
|
|
300
|
+
react-joyride portals the overlay into `document.body`, and Reflex's Radix theme
|
|
301
|
+
root is a stacking context (`z-index: 0`), so the overlay paints above the whole
|
|
302
|
+
app no matter how large a `z-index` you give your own elements. If you need
|
|
303
|
+
something clickable while a tour runs — a control bar, a chat widget — render
|
|
304
|
+
the tour inside the app instead and give your element a higher `z-index` than
|
|
305
|
+
the tour's:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
(rx.box(id="tour-root"),)
|
|
309
|
+
(rjr.joyride(steps=STEPS, portal_element="#tour-root", options=rjr.TourOptions(z_index=1000)),)
|
|
310
|
+
(rx.box(my_controls, position="fixed", bottom="1rem", left="1rem", z_index="1200"),)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Otherwise anything under the overlay is inert while a step is open, which is
|
|
314
|
+
usually exactly what you want.
|
|
315
|
+
|
|
316
|
+
## Demo app
|
|
317
|
+
|
|
318
|
+
The `react_joyride_demo/` folder is a four page Reflex app covering the basics,
|
|
319
|
+
controlled mode, theming with a Reflex tooltip, and the awkward cases (targets
|
|
320
|
+
that appear late or never, spotlight targets, scrolling, modal steps, blocked
|
|
321
|
+
interaction):
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
uv sync
|
|
325
|
+
cd react_joyride_demo
|
|
326
|
+
uv run reflex run
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Development
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
uv sync # creates .venv and installs the package in editable mode
|
|
333
|
+
uv run pytest
|
|
334
|
+
uv run ruff check .
|
|
335
|
+
uv run ruff format --check .
|
|
336
|
+
uv run reflex component build # generates the .pyi stubs and builds dist/
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## License
|
|
340
|
+
|
|
341
|
+
MIT © Ernesto Crespo
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# reflex-react-joyride
|
|
2
|
+
|
|
3
|
+
Guided product tours and onboarding walkthroughs for [Reflex](https://reflex.dev), powered by [react-joyride](https://react-joyride.com) v3.
|
|
4
|
+
|
|
5
|
+
- Wraps **react-joyride 3.2.0** through its `useJoyride` hook, so the tour's controls are reachable from Python
|
|
6
|
+
- Typed props for everything: `Step`, `TourOptions`, `Locale`, `TourStyles`, `FloatingOptions` — `snake_case` in Python, `camelCase` in the browser
|
|
7
|
+
- **Uncontrolled** (`run`) or **controlled** (`step_index`) mode, whichever fits your State
|
|
8
|
+
- **Python actions**: `start`, `next_step`, `prev_step`, `go_to`, `skip`, `reset`, `replay`, `stop`, `open_tooltip`, `get_state`
|
|
9
|
+
- One catch-all `on_event` plus a dedicated trigger per event (`on_tour_start`, `on_step_after`, `on_target_not_found`, ...) with a flat, JSON-safe payload
|
|
10
|
+
- **Custom tooltip and beacon** written as ordinary Reflex components
|
|
11
|
+
- Step content and titles accept Reflex components, not just strings
|
|
12
|
+
- Constants mirroring react-joyride's literals: `EVENTS`, `ACTIONS`, `STATUS`, `LIFECYCLE`, `ORIGIN`
|
|
13
|
+
|
|
14
|
+
## Screenshots
|
|
15
|
+
|
|
16
|
+
The four pages of the demo app, in order:
|
|
17
|
+
|
|
18
|
+
| | |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
|  |  |
|
|
21
|
+
| A continuous tour over a small dashboard, with the event log filling up | Controlled mode: the step index lives in the State and external buttons drive it |
|
|
22
|
+
|  |  |
|
|
23
|
+
| `locale`, `options` and `styles` restyling the built-in tooltip | The same step rendered by a Reflex component through the `tooltip` render prop |
|
|
24
|
+
|  |  |
|
|
25
|
+
| `placement="center"` on `body` for a modal-like opening step | A spotlight over a different element than the one the tooltip points at |
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install reflex-react-joyride
|
|
31
|
+
# or
|
|
32
|
+
uv add reflex-react-joyride
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import reflex as rx
|
|
39
|
+
import reflex_react_joyride as rjr
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TourState(rx.State):
|
|
43
|
+
run: bool = False
|
|
44
|
+
|
|
45
|
+
@rx.event
|
|
46
|
+
def start(self):
|
|
47
|
+
self.run = True
|
|
48
|
+
|
|
49
|
+
@rx.event
|
|
50
|
+
def finished(self, event: rjr.JoyrideEvent):
|
|
51
|
+
self.run = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def index() -> rx.Component:
|
|
55
|
+
return rx.vstack(
|
|
56
|
+
rjr.joyride(
|
|
57
|
+
tour_id="welcome",
|
|
58
|
+
run=TourState.run,
|
|
59
|
+
continuous=True,
|
|
60
|
+
steps=[
|
|
61
|
+
rjr.Step(target="#logo", title="Welcome", content="This is the app."),
|
|
62
|
+
rjr.Step(target="#cta", content="Start here.", placement="top"),
|
|
63
|
+
],
|
|
64
|
+
options=rjr.TourOptions(show_progress=True, primary_color="#7c3aed"),
|
|
65
|
+
on_tour_end=TourState.finished,
|
|
66
|
+
),
|
|
67
|
+
rx.heading("Acme", id="logo"),
|
|
68
|
+
rx.button("Get started", id="cta"),
|
|
69
|
+
rx.button("Take the tour", on_click=TourState.start),
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Steps
|
|
74
|
+
|
|
75
|
+
A step needs a `target` (any CSS selector) and some `content`. Everything in
|
|
76
|
+
`TourOptions` can be overridden per step:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
rjr.Step(
|
|
80
|
+
target="#sidebar",
|
|
81
|
+
title="Navigation",
|
|
82
|
+
content=rx.vstack(rx.text("Every section lives here."), rx.code("Cmd+K")),
|
|
83
|
+
placement="right", # top | bottom | left | right | auto | center (+ -start / -end)
|
|
84
|
+
spotlight_padding=16,
|
|
85
|
+
show_progress=True,
|
|
86
|
+
id="sidebar", # echoed back as event["step_id"]
|
|
87
|
+
data={"section": "nav"}, # echoed back as event["step_data"], keys untouched
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`target="body"` with `placement="center"` gives a modal-like step, which is a
|
|
92
|
+
good opener. Plain dicts work as well as `Step` objects, and so do steps kept in
|
|
93
|
+
a State var:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
class State(rx.State):
|
|
97
|
+
steps: list[dict] = [{"target": "#logo", "content": "Hi", "show_progress": True}]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
rjr.joyride(steps=State.steps)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`snake_case` keys are converted to the `camelCase` react-joyride expects, both
|
|
104
|
+
for literal props (in Python) and for values coming from the State (in the
|
|
105
|
+
browser), so you never have to write `spotlightPadding` yourself. Keys that are
|
|
106
|
+
already `camelCase` pass through untouched.
|
|
107
|
+
|
|
108
|
+
## Theming
|
|
109
|
+
|
|
110
|
+
`options` sets behaviour and colors for the whole tour, `styles` overrides the
|
|
111
|
+
CSS of every element react-joyride renders, and `locale` replaces the strings:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
rjr.joyride(
|
|
115
|
+
steps=STEPS,
|
|
116
|
+
options=rjr.TourOptions(
|
|
117
|
+
primary_color="#059669",
|
|
118
|
+
background_color="#f2fbf7",
|
|
119
|
+
overlay_color="rgba(4, 47, 36, 0.55)",
|
|
120
|
+
show_progress=True,
|
|
121
|
+
skip_beacon=True, # go straight to the tooltip, no beacon
|
|
122
|
+
spotlight_radius=12,
|
|
123
|
+
buttons=["back", "skip", "primary"],
|
|
124
|
+
width=420,
|
|
125
|
+
z_index=1000,
|
|
126
|
+
),
|
|
127
|
+
styles=rjr.TourStyles(
|
|
128
|
+
tooltip={"border_radius": "14px", "box_shadow": "0 18px 40px rgba(15, 23, 42, 0.18)"},
|
|
129
|
+
button_primary={"border_radius": "999px", "padding": "8px 18px"},
|
|
130
|
+
),
|
|
131
|
+
locale=rjr.Locale(
|
|
132
|
+
back="Atrás",
|
|
133
|
+
close="Cerrar",
|
|
134
|
+
last="Finalizar",
|
|
135
|
+
next="Siguiente",
|
|
136
|
+
next_with_progress="Siguiente ({current} de {total})",
|
|
137
|
+
skip="Saltar",
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
CSS properties inside `styles` are converted too: `border_radius` becomes
|
|
143
|
+
`borderRadius`.
|
|
144
|
+
|
|
145
|
+
## Controlled mode
|
|
146
|
+
|
|
147
|
+
Pass `step_index` and react-joyride stops advancing on its own — it renders
|
|
148
|
+
exactly the step your State asks for. Move the index from `on_step_after`:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
class State(rx.State):
|
|
152
|
+
run: bool = False
|
|
153
|
+
index: int = 0
|
|
154
|
+
|
|
155
|
+
@rx.event
|
|
156
|
+
def advance(self, event: rjr.JoyrideEvent):
|
|
157
|
+
self.index = max(0, event["index"] + (-1 if event["action"] == rjr.ACTIONS.PREV else 1))
|
|
158
|
+
|
|
159
|
+
@rx.event
|
|
160
|
+
def skip_missing(self, event: rjr.JoyrideEvent):
|
|
161
|
+
self.index = event["index"] + 1
|
|
162
|
+
|
|
163
|
+
@rx.event
|
|
164
|
+
def finished(self, _event: rjr.JoyrideEvent):
|
|
165
|
+
self.run, self.index = False, 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
rjr.joyride(
|
|
169
|
+
steps=STEPS,
|
|
170
|
+
run=State.run,
|
|
171
|
+
step_index=State.index,
|
|
172
|
+
continuous=True,
|
|
173
|
+
on_step_after=State.advance,
|
|
174
|
+
on_target_not_found=State.skip_missing,
|
|
175
|
+
on_tour_end=State.finished,
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This is what you want when a step should only open after your own code did
|
|
180
|
+
something — finished a request, opened a drawer, switched a tab.
|
|
181
|
+
|
|
182
|
+
## Events
|
|
183
|
+
|
|
184
|
+
Every trigger receives the same flat dict:
|
|
185
|
+
|
|
186
|
+
| key | meaning |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `type` | the event, e.g. `tour:start`, `step:after`, `error:target_not_found` |
|
|
189
|
+
| `action` | what triggered it: `start`, `next`, `prev`, `close`, `skip`, `update`, ... |
|
|
190
|
+
| `index`, `size`, `is_last_step` | where we are in the tour |
|
|
191
|
+
| `status` | `idle`, `ready`, `waiting`, `running`, `paused`, `skipped`, `finished` |
|
|
192
|
+
| `lifecycle` | `init`, `ready`, `beacon`, `tooltip`, `complete`, ... |
|
|
193
|
+
| `origin` | which control fired it: `button_primary`, `overlay`, `keyboard`, ... |
|
|
194
|
+
| `controlled`, `waiting`, `scrolling` | tour flags |
|
|
195
|
+
| `step_id`, `step_target`, `step_title`, `step_data` | about the current step |
|
|
196
|
+
| `error` | the message for `error` events |
|
|
197
|
+
|
|
198
|
+
Triggers: `on_event` (all of them), `on_tour_start`, `on_tour_end`,
|
|
199
|
+
`on_status_change`, `on_step_before`, `on_step_after`, `on_beacon`,
|
|
200
|
+
`on_tooltip`, `on_scroll_start`, `on_scroll_end`, `on_target_not_found`,
|
|
201
|
+
`on_error`.
|
|
202
|
+
|
|
203
|
+
Compare against the constants instead of raw strings:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
@rx.event
|
|
207
|
+
def on_tour_event(self, event: rjr.JoyrideEvent):
|
|
208
|
+
if event["type"] == rjr.EVENTS.TOUR_END and event["status"] in rjr.FINISHED_STATUSES:
|
|
209
|
+
self.onboarding_done = True
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Python actions
|
|
213
|
+
|
|
214
|
+
Give the tour a `tour_id` and you can drive it from anywhere, without keeping
|
|
215
|
+
the index in your State:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
rx.button("Start", on_click=rjr.start("welcome"))
|
|
219
|
+
rx.button("Step 3", on_click=rjr.go_to("welcome", 2))
|
|
220
|
+
rx.button("Next", on_click=rjr.next_step("welcome"))
|
|
221
|
+
rx.button("Replay this step", on_click=rjr.replay("welcome"))
|
|
222
|
+
rx.button("Skip", on_click=rjr.skip("welcome"))
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`start`, `stop`, `next_step`, `prev_step`, `go_to`, `close`, `skip`, `reset`,
|
|
226
|
+
`replay`, `open_tooltip`, and `get_state(tour_id, callback)` which sends the
|
|
227
|
+
current tour state to an event handler. They are no-ops (with a console warning)
|
|
228
|
+
when no tour with that id is mounted.
|
|
229
|
+
|
|
230
|
+
The tour still needs `run=True` to be running; the actions drive a running tour,
|
|
231
|
+
they do not switch it on.
|
|
232
|
+
|
|
233
|
+
## Custom tooltip and beacon
|
|
234
|
+
|
|
235
|
+
Pass a function returning a Reflex component. It receives what react-joyride
|
|
236
|
+
passes to a custom tooltip, plus `current`, `total`, `progress`, `content`,
|
|
237
|
+
`title`, `stepId`, `stepData`, `isFirstStep` and `isLastStep` (these are
|
|
238
|
+
JavaScript props, so their keys stay `camelCase`):
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
def tooltip(props):
|
|
242
|
+
return rx.card(
|
|
243
|
+
rx.vstack(
|
|
244
|
+
rx.badge(props["progress"], radius="full"),
|
|
245
|
+
rx.heading(props["title"], size="4"),
|
|
246
|
+
rx.text(props["content"], size="2"),
|
|
247
|
+
rx.hstack(
|
|
248
|
+
rx.button(
|
|
249
|
+
"Back",
|
|
250
|
+
variant="soft",
|
|
251
|
+
on_click=rjr.prev_step("welcome"),
|
|
252
|
+
disabled=props["isFirstStep"].to(bool),
|
|
253
|
+
),
|
|
254
|
+
rx.button(
|
|
255
|
+
rx.cond(props["isLastStep"].to(bool), "Done", "Next"), on_click=rjr.next_step("welcome")
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
),
|
|
259
|
+
width="360px",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
rjr.joyride(steps=STEPS, tour_id="welcome", tooltip=tooltip, beacon=lambda _p: rx.box(...))
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The component is rendered inside react-joyride's own wrapper, so the ARIA
|
|
267
|
+
attributes and positioning are handled for you. It can read your State like any
|
|
268
|
+
other component.
|
|
269
|
+
|
|
270
|
+
## Rendering above the overlay
|
|
271
|
+
|
|
272
|
+
react-joyride portals the overlay into `document.body`, and Reflex's Radix theme
|
|
273
|
+
root is a stacking context (`z-index: 0`), so the overlay paints above the whole
|
|
274
|
+
app no matter how large a `z-index` you give your own elements. If you need
|
|
275
|
+
something clickable while a tour runs — a control bar, a chat widget — render
|
|
276
|
+
the tour inside the app instead and give your element a higher `z-index` than
|
|
277
|
+
the tour's:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
(rx.box(id="tour-root"),)
|
|
281
|
+
(rjr.joyride(steps=STEPS, portal_element="#tour-root", options=rjr.TourOptions(z_index=1000)),)
|
|
282
|
+
(rx.box(my_controls, position="fixed", bottom="1rem", left="1rem", z_index="1200"),)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Otherwise anything under the overlay is inert while a step is open, which is
|
|
286
|
+
usually exactly what you want.
|
|
287
|
+
|
|
288
|
+
## Demo app
|
|
289
|
+
|
|
290
|
+
The `react_joyride_demo/` folder is a four page Reflex app covering the basics,
|
|
291
|
+
controlled mode, theming with a Reflex tooltip, and the awkward cases (targets
|
|
292
|
+
that appear late or never, spotlight targets, scrolling, modal steps, blocked
|
|
293
|
+
interaction):
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
uv sync
|
|
297
|
+
cd react_joyride_demo
|
|
298
|
+
uv run reflex run
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Development
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
uv sync # creates .venv and installs the package in editable mode
|
|
305
|
+
uv run pytest
|
|
306
|
+
uv run ruff check .
|
|
307
|
+
uv run ruff format --check .
|
|
308
|
+
uv run reflex component build # generates the .pyi stubs and builds dist/
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## License
|
|
312
|
+
|
|
313
|
+
MIT © Ernesto Crespo
|