Skip to content

Commit 5f185be

Browse files
Parvin MehrabaniParvin Mehrabani
authored andcommitted
fix(theme): fix theme leaks, race conditions, and expose public theme API
1 parent 0a73c31 commit 5f185be

9 files changed

Lines changed: 377 additions & 96 deletions

File tree

docs/theme-usage.md

Lines changed: 70 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The IntroJS theme system allows you to easily customize the look and feel of you
1212
-**Custom Themes** - Load any CSS file as a theme
1313
-**Theme Registration** - Register custom themes for easy reuse
1414
-**Tour Options Integration** - Set theme directly in tour options
15+
-**Live Theme Switching** - Change the theme of a running tour with `tour.setTheme()`
1516

1617
## Quick Start
1718

@@ -23,22 +24,22 @@ The simplest way to use themes is by setting the `theme` option when creating a
2324
import introJs from 'intro.js';
2425

2526
// Use dark theme
26-
introJs().setOptions({
27+
introJs.tour().setOptions({
2728
theme: 'dark'
2829
}).start();
2930

3031
// Use light theme
31-
introJs().setOptions({
32+
introJs.tour().setOptions({
3233
theme: 'light'
3334
}).start();
3435

3536
// Use system preference (default)
36-
introJs().setOptions({
37+
introJs.tour().setOptions({
3738
theme: 'auto'
3839
}).start();
3940

4041
// Use any pre-registered theme
41-
introJs().setOptions({
42+
introJs.tour().setOptions({
4243
theme: 'modern'
4344
}).start();
4445
```
@@ -51,13 +52,13 @@ You can load a custom CSS file by providing both `theme` and `themePath` options
5152
import introJs from 'intro.js';
5253

5354
// Load a custom theme from a CSS file
54-
introJs().setOptions({
55+
introJs.tour().setOptions({
5556
theme: 'ocean',
5657
themePath: 'path/to/themes/introjs-ocean.css'
5758
}).start();
5859

5960
// Load from CDN
60-
introJs().setOptions({
61+
introJs.tour().setOptions({
6162
theme: 'custom',
6263
themePath: 'https://cdn.example.com/intro-custom-theme.css'
6364
}).start();
@@ -69,79 +70,79 @@ The following themes are available out of the box:
6970

7071
| Theme Name | Description | CSS Path |
7172
|------------|-------------|----------|
72-
| `light` | Light theme (default style) | `themes/introjs-light.css` |
73-
| `dark` | Dark theme | `themes/introjs-dark.css` |
74-
| `auto` | System preference (switches automatically) | N/A (uses classes only) |
73+
| `light` | Light theme (default style) | N/A - uses CSS custom properties, no file to load |
74+
| `dark` | Dark theme | N/A - uses CSS custom properties, no file to load |
75+
| `auto` | Follows the system preference, switches automatically | N/A - uses CSS custom properties, no file to load |
7576
| `modern` | Modern theme | `themes/introjs-modern.css` |
7677
| `flattener` | Flat design theme | `themes/introjs-flattener.css` |
7778
| `nassim` | Nassim theme | `themes/introjs-nassim.css` |
7879
| `nazanin` | Nazanin theme (RTL support) | `themes/introjs-nazanin.css` |
7980
| `royal` | Royal theme | `themes/introjs-royal.css` |
8081

82+
> Note: `light`, `dark` and `auto` are implemented purely with CSS classes/custom properties on the tour's root element, so no extra network request happens when you use them. The other themes are loaded on demand from the CSS file shown above.
83+
8184
## Advanced Usage
8285

8386
### 1. Registering Custom Themes
8487

8588
You can register custom themes globally so they can be used by name:
8689

8790
```javascript
88-
import { registerTheme, registerThemes } from 'intro.js/theme';
8991
import introJs from 'intro.js';
9092

9193
// Register a single theme
92-
registerTheme('ocean', 'themes/introjs-ocean.css');
94+
introJs.registerTheme('ocean', 'themes/introjs-ocean.css');
9395

9496
// Now you can use it by name
95-
introJs().setOptions({
97+
introJs.tour().setOptions({
9698
theme: 'ocean'
9799
}).start();
98100

99101
// Register multiple themes at once
100-
registerThemes([
102+
introJs.registerThemes([
101103
{ name: 'sunset', cssPath: 'themes/introjs-sunset.css' },
102104
{ name: 'forest', cssPath: 'themes/introjs-forest.css' },
103105
{ name: 'corporate', cssPath: 'themes/introjs-corporate.css' }
104106
]);
105107
```
106108

107-
### 2. Using Theme API Directly
109+
### 2. Changing the Theme of a Running Tour
108110

109-
For more control, you can use the theme API directly:
111+
Every `Tour` instance exposes `setTheme()`/`getTheme()` so you can react to in-app theme toggles without restarting the tour:
110112

111113
```javascript
112-
import { applyTheme, getTheme } from 'intro.js/theme';
114+
import introJs from 'intro.js';
113115

114-
// Apply a theme to a specific root element
115-
const root = document.getElementById('my-tour-container');
116-
applyTheme({
117-
root: root,
118-
theme: 'dark'
119-
});
116+
const tour = introJs.tour().setOptions({ theme: 'light' });
117+
await tour.start();
120118

121-
// Get current theme instance
122-
const theme = getTheme();
123-
console.log(theme.currentTheme); // 'dark'
124-
console.log(theme.value); // 'dark' or 'light'
119+
console.log(tour.getTheme()); // 'light'
125120

126-
// Change theme dynamically
127-
await theme.setTheme('light');
121+
// Switch the running tour to dark mode
122+
await tour.setTheme('dark');
123+
console.log(tour.getTheme()); // 'dark'
128124

129-
// Clean up when done
130-
theme.destroy();
125+
// Switch to a custom CSS theme
126+
await tour.setTheme('ocean', 'themes/introjs-ocean.css');
131127
```
132128

129+
Calling `setTheme()` before the tour has started simply stores the choice; it's applied the next time `start()` runs.
130+
133131
### 3. Getting Theme Information
134132

135133
```javascript
136-
import { getThemePath, getRegisteredThemes } from 'intro.js/theme';
134+
import introJs from 'intro.js';
137135

138-
// Get CSS path for a specific theme
139-
const darkPath = getThemePath('dark');
140-
console.log(darkPath); // 'themes/introjs-dark.css'
136+
// Get CSS path for a registered theme
137+
const modernPath = introJs.getThemePath('modern');
138+
console.log(modernPath); // 'themes/introjs-modern.css'
139+
140+
// Built-in themes (light/dark/auto) are not file-based, so this is undefined
141+
console.log(introJs.getThemePath('dark')); // undefined
141142

142143
// Get all registered theme names
143-
const allThemes = getRegisteredThemes();
144-
console.log(allThemes); // ['dark', 'light', 'modern', ...]
144+
const allThemes = introJs.getRegisteredThemes();
145+
console.log(allThemes); // ['modern', 'flattener', 'nassim', 'nazanin', 'royal', ...]
145146
```
146147

147148
## Complete Examples
@@ -151,7 +152,7 @@ console.log(allThemes); // ['dark', 'light', 'modern', ...]
151152
```javascript
152153
import introJs from 'intro.js';
153154

154-
introJs().setOptions({
155+
introJs.tour().setOptions({
155156
steps: [
156157
{
157158
element: '#step1',
@@ -170,13 +171,12 @@ introJs().setOptions({
170171

171172
```javascript
172173
import introJs from 'intro.js';
173-
import { registerTheme } from 'intro.js/theme';
174174

175175
// Register your custom theme
176-
registerTheme('mycompany', 'assets/css/intro-mycompany-theme.css');
176+
introJs.registerTheme('mycompany', 'assets/css/intro-mycompany-theme.css');
177177

178178
// Use it in your tour
179-
introJs().setOptions({
179+
introJs.tour().setOptions({
180180
steps: [
181181
{
182182
element: '#welcome',
@@ -193,8 +193,9 @@ introJs().setOptions({
193193
import introJs from 'intro.js';
194194

195195
// This tour will automatically use dark theme in dark mode
196-
// and light theme in light mode
197-
introJs().setOptions({
196+
// and light theme in light mode, and keeps reacting if the
197+
// user's OS theme changes while the tour is open
198+
introJs.tour().setOptions({
198199
steps: [
199200
{
200201
intro: 'This tour adapts to your system theme!'
@@ -209,7 +210,7 @@ introJs().setOptions({
209210
```javascript
210211
import introJs from 'intro.js';
211212

212-
const tour = introJs().setOptions({
213+
const tour = introJs.tour().setOptions({
213214
steps: [
214215
{
215216
intro: 'Starting with light theme'
@@ -221,15 +222,10 @@ const tour = introJs().setOptions({
221222
theme: 'light'
222223
});
223224

224-
tour.onBeforeChange(function(targetElement) {
225+
tour.onBeforeChange(async function (targetElement) {
225226
if (this.getCurrentStep() === 1) {
226-
// Switch to dark theme on step 2
227-
this.setOption('theme', 'dark');
228-
// Re-initialize theme
229-
this.exit().then(() => {
230-
this.setCurrentStep(1);
231-
this.start();
232-
});
227+
// Switch the already-running tour to dark theme on step 2
228+
await this.setTheme('dark');
233229
}
234230
});
235231

@@ -285,7 +281,7 @@ Then use it:
285281
```javascript
286282
import introJs from 'intro.js';
287283

288-
introJs().setOptions({
284+
introJs.tour().setOptions({
289285
theme: 'ocean',
290286
themePath: 'themes/introjs-ocean.css'
291287
}).start();
@@ -296,17 +292,16 @@ introJs().setOptions({
296292
The theme system is fully typed:
297293

298294
```typescript
299-
import introJs from 'intro.js';
300-
import { ThemeType, registerTheme, applyTheme } from 'intro.js/theme';
295+
import introJs, { type ThemeType } from 'intro.js';
301296

302297
// Theme types
303298
const theme: ThemeType = 'dark'; // 'light' | 'dark' | 'auto' | string
304299

305300
// Register theme with types
306-
registerTheme('custom', 'path/to/custom.css');
301+
introJs.registerTheme('custom', 'path/to/custom.css');
307302

308303
// Use in tour with types
309-
introJs().setOptions({
304+
introJs.tour().setOptions({
310305
theme: 'dark',
311306
themePath: 'path/to/theme.css' // optional
312307
}).start();
@@ -316,15 +311,15 @@ introJs().setOptions({
316311

317312
1. **Use `auto` for Better UX**: Let users' system preferences determine the theme
318313
```javascript
319-
introJs().setOptions({ theme: 'auto' }).start();
314+
introJs.tour().setOptions({ theme: 'auto' }).start();
320315
```
321316

322317
2. **Register Themes Early**: Register all custom themes at app initialization
323318
```javascript
324319
// app-init.js
325-
import { registerThemes } from 'intro.js/theme';
326-
327-
registerThemes([
320+
import introJs from 'intro.js';
321+
322+
introJs.registerThemes([
328323
{ name: 'brand', cssPath: 'themes/brand.css' },
329324
{ name: 'seasonal', cssPath: 'themes/seasonal.css' }
330325
]);
@@ -353,7 +348,7 @@ introJs().setOptions({
353348
**Problem**: Theme is registered but not applying
354349

355350
**Solutions**:
356-
- Make sure you call `registerTheme()` before starting the tour
351+
- Make sure you call `introJs.registerTheme()` before starting the tour
357352
- Verify the theme name matches exactly
358353
- Check that the CSS selectors in your theme file are correct
359354
- Ensure no other CSS is overriding your theme styles
@@ -363,9 +358,9 @@ introJs().setOptions({
363358
**Problem**: Multiple theme CSS files are being loaded
364359

365360
**Solutions**:
366-
- The system automatically prevents duplicate loading
361+
- The system automatically prevents duplicate loading of the same CSS file
362+
- When you call `tour.setTheme()` with a different custom theme, the previous theme's `<link>` is removed automatically
367363
- Use the same theme name consistently
368-
- Call `theme.destroy()` when switching themes programmatically
369364

370365
## API Reference
371366

@@ -376,11 +371,18 @@ introJs().setOptions({
376371
| `theme` | `ThemeType` | `'auto'` | Theme name ('light', 'dark', 'auto', or custom) |
377372
| `themePath` | `string` | `undefined` | Path to custom CSS file |
378373

379-
### Theme Functions
374+
### Tour Instance Methods
375+
376+
| Method | Parameters | Returns | Description |
377+
|--------|------------|---------|-------------|
378+
| `tour.setTheme(theme, themePath?)` | `theme: ThemeType, themePath?: string` | `Promise<Tour>` | Change the theme; applies immediately if the tour is running |
379+
| `tour.getTheme()` | - | `'light' \| 'dark' \| undefined` | The currently resolved theme, or `undefined` before the tour starts |
380+
381+
### `introJs` Theme Functions
380382

381383
| Function | Parameters | Returns | Description |
382384
|----------|------------|---------|-------------|
383-
| `registerTheme(name, cssPath)` | `name: string, cssPath: string` | `void` | Register a custom theme |
384-
| `registerThemes(themes)` | `themes: ThemeRegistration[]` | `void` | Register multiple themes |
385-
| `getThemePath(name)` | `name: string` | `string \| undefined` | Get CSS path for a theme |
386-
| `getRegisteredThemes()` | -
385+
| `introJs.registerTheme(name, cssPath)` | `name: string, cssPath: string` | `void` | Register a custom theme |
386+
| `introJs.registerThemes(themes)` | `themes: ThemeRegistration[]` | `void` | Register multiple themes |
387+
| `introJs.getThemePath(name)` | `name: string` | `string \| undefined` | Get the CSS path for a theme (built-in themes return `undefined`) |
388+
| `introJs.getRegisteredThemes()` | - | `string[]` | List the names of all registered themes |

src/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,15 @@ describe("index", () => {
2626
// Assert
2727
expect(hintInstance).toBeInstanceOf(Hint);
2828
});
29+
30+
it("should expose theme registration and lookup helpers", () => {
31+
introJs.registerTheme("sunset", "/themes/sunset.css");
32+
introJs.registerThemes([{ name: "forest", cssPath: "/themes/forest.css" }]);
33+
34+
expect(introJs.getThemePath("sunset")).toBe("/themes/sunset.css");
35+
expect(introJs.getThemePath("forest")).toBe("/themes/forest.css");
36+
expect(introJs.getRegisteredThemes()).toEqual(
37+
expect.arrayContaining(["sunset", "forest"])
38+
);
39+
});
2940
});

src/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { version } from "../package.json";
22
import { Hint } from "./packages/hint";
33
import { Tour } from "./packages/tour";
4+
import {
5+
registerTheme,
6+
registerThemes,
7+
getThemePath,
8+
getRegisteredThemes,
9+
} from "./packages/tour/theme";
10+
export type { ThemeType, ThemeRegistration } from "./packages/tour/theme";
411

512
class LegacyIntroJs extends Tour {
613
/**
@@ -63,4 +70,24 @@ introJs.hint = (elementOrSelector?: string | HTMLElement) =>
6370
*/
6471
introJs.version = version;
6572

73+
/**
74+
* Register a custom theme so it can be referenced by name in the `theme` tour option
75+
*/
76+
introJs.registerTheme = registerTheme;
77+
78+
/**
79+
* Register multiple custom themes at once
80+
*/
81+
introJs.registerThemes = registerThemes;
82+
83+
/**
84+
* Get the CSS path registered for a given theme name, if any
85+
*/
86+
introJs.getThemePath = getThemePath;
87+
88+
/**
89+
* List the names of all currently registered themes
90+
*/
91+
introJs.getRegisteredThemes = getRegisteredThemes;
92+
6693
export default introJs;

0 commit comments

Comments
 (0)