@matthesketh/react-guidetour 1.0.0 → 1.1.1

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.
@@ -0,0 +1,384 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import createStore from '../store';
3
+ import { ACTIONS, LIFECYCLE, STATUS } from '~/literals';
4
+
5
+ describe('Store', () => {
6
+ describe('initialization', () => {
7
+ it('creates with default state when no options', () => {
8
+ const store = createStore();
9
+ const state = store.getState();
10
+
11
+ expect(state.action).toBe(ACTIONS.INIT);
12
+ expect(state.controlled).toBe(false);
13
+ expect(state.index).toBe(0);
14
+ expect(state.lifecycle).toBe(LIFECYCLE.INIT);
15
+ expect(state.origin).toBeNull();
16
+ expect(state.size).toBe(0);
17
+ expect(state.status).toBe(STATUS.IDLE);
18
+ });
19
+
20
+ it('creates with READY status when steps provided', () => {
21
+ const store = createStore({
22
+ steps: [{ target: '.a', content: 'A' }],
23
+ } as any);
24
+ expect(store.getState().status).toBe(STATUS.READY);
25
+ expect(store.getState().size).toBe(1);
26
+ });
27
+
28
+ it('creates in controlled mode when stepIndex is a number', () => {
29
+ const store = createStore({
30
+ stepIndex: 0,
31
+ steps: [{ target: '.a', content: 'A' }],
32
+ } as any);
33
+ expect(store.getState().controlled).toBe(true);
34
+ });
35
+
36
+ it('sets initial index from stepIndex', () => {
37
+ const store = createStore({
38
+ stepIndex: 2,
39
+ steps: [
40
+ { target: '.a', content: 'A' },
41
+ { target: '.b', content: 'B' },
42
+ { target: '.c', content: 'C' },
43
+ ],
44
+ } as any);
45
+ expect(store.getState().index).toBe(2);
46
+ });
47
+ });
48
+
49
+ describe('start', () => {
50
+ it('transitions from READY to RUNNING', () => {
51
+ const store = createStore({
52
+ steps: [{ target: '.a', content: 'A' }],
53
+ } as any);
54
+
55
+ store.start();
56
+ expect(store.getState().status).toBe(STATUS.RUNNING);
57
+ expect(store.getState().action).toBe(ACTIONS.START);
58
+ });
59
+
60
+ it('transitions to WAITING when no steps', () => {
61
+ const store = createStore();
62
+ store.start();
63
+ expect(store.getState().status).toBe(STATUS.WAITING);
64
+ });
65
+
66
+ it('starts at a specific index', () => {
67
+ const store = createStore({
68
+ steps: [{ target: '.a', content: 'A' }, { target: '.b', content: 'B' }],
69
+ } as any);
70
+
71
+ store.start(1);
72
+ expect(store.getState().index).toBe(1);
73
+ });
74
+ });
75
+
76
+ describe('next', () => {
77
+ it('increments index', () => {
78
+ const store = createStore({
79
+ steps: [
80
+ { target: '.a', content: 'A' },
81
+ { target: '.b', content: 'B' },
82
+ { target: '.c', content: 'C' },
83
+ ],
84
+ } as any);
85
+
86
+ store.start();
87
+ expect(store.getState().index).toBe(0);
88
+
89
+ store.next();
90
+ expect(store.getState().index).toBe(1);
91
+ expect(store.getState().action).toBe(ACTIONS.NEXT);
92
+ });
93
+
94
+ it('does nothing when not running', () => {
95
+ const store = createStore({
96
+ steps: [{ target: '.a', content: 'A' }],
97
+ } as any);
98
+
99
+ store.next();
100
+ expect(store.getState().index).toBe(0);
101
+ });
102
+
103
+ it('finishes when reaching the end', () => {
104
+ const store = createStore({
105
+ steps: [{ target: '.a', content: 'A' }],
106
+ } as any);
107
+
108
+ store.start();
109
+ store.next();
110
+ expect(store.getState().status).toBe(STATUS.FINISHED);
111
+ });
112
+ });
113
+
114
+ describe('prev', () => {
115
+ it('decrements index', () => {
116
+ const store = createStore({
117
+ steps: [
118
+ { target: '.a', content: 'A' },
119
+ { target: '.b', content: 'B' },
120
+ ],
121
+ } as any);
122
+
123
+ store.start(1);
124
+ store.prev();
125
+ expect(store.getState().index).toBe(0);
126
+ expect(store.getState().action).toBe(ACTIONS.PREV);
127
+ });
128
+
129
+ it('clamps at 0', () => {
130
+ const store = createStore({
131
+ steps: [{ target: '.a', content: 'A' }],
132
+ } as any);
133
+
134
+ store.start();
135
+ store.prev();
136
+ expect(store.getState().index).toBe(0);
137
+ });
138
+ });
139
+
140
+ describe('go', () => {
141
+ it('jumps to specific index', () => {
142
+ const store = createStore({
143
+ steps: [
144
+ { target: '.a', content: 'A' },
145
+ { target: '.b', content: 'B' },
146
+ { target: '.c', content: 'C' },
147
+ ],
148
+ } as any);
149
+
150
+ store.start();
151
+ store.go(2);
152
+ expect(store.getState().index).toBe(2);
153
+ expect(store.getState().action).toBe(ACTIONS.GO);
154
+ });
155
+
156
+ it('does nothing in controlled mode', () => {
157
+ const store = createStore({
158
+ stepIndex: 0,
159
+ steps: [
160
+ { target: '.a', content: 'A' },
161
+ { target: '.b', content: 'B' },
162
+ ],
163
+ } as any);
164
+
165
+ store.start();
166
+ store.go(1);
167
+ expect(store.getState().index).toBe(0);
168
+ });
169
+ });
170
+
171
+ describe('close', () => {
172
+ it('increments index and sets CLOSE action', () => {
173
+ const store = createStore({
174
+ steps: [
175
+ { target: '.a', content: 'A' },
176
+ { target: '.b', content: 'B' },
177
+ ],
178
+ } as any);
179
+
180
+ store.start();
181
+ store.close();
182
+ expect(store.getState().action).toBe(ACTIONS.CLOSE);
183
+ expect(store.getState().index).toBe(1);
184
+ });
185
+
186
+ it('accepts an origin parameter', () => {
187
+ const store = createStore({
188
+ steps: [
189
+ { target: '.a', content: 'A' },
190
+ { target: '.b', content: 'B' },
191
+ ],
192
+ } as any);
193
+
194
+ store.start();
195
+ store.close('keyboard');
196
+ expect(store.getState().origin).toBe('keyboard');
197
+ });
198
+
199
+ it('does nothing when not running', () => {
200
+ const store = createStore({
201
+ steps: [{ target: '.a', content: 'A' }],
202
+ } as any);
203
+
204
+ store.close();
205
+ expect(store.getState().action).toBe(ACTIONS.INIT);
206
+ });
207
+ });
208
+
209
+ describe('skip', () => {
210
+ it('sets status to SKIPPED', () => {
211
+ const store = createStore({
212
+ steps: [{ target: '.a', content: 'A' }],
213
+ } as any);
214
+
215
+ store.start();
216
+ store.skip();
217
+ expect(store.getState().status).toBe(STATUS.SKIPPED);
218
+ expect(store.getState().action).toBe(ACTIONS.SKIP);
219
+ });
220
+ });
221
+
222
+ describe('reset', () => {
223
+ it('resets index to 0 with READY status', () => {
224
+ const store = createStore({
225
+ steps: [
226
+ { target: '.a', content: 'A' },
227
+ { target: '.b', content: 'B' },
228
+ ],
229
+ } as any);
230
+
231
+ store.start();
232
+ store.next();
233
+ store.reset();
234
+ expect(store.getState().index).toBe(0);
235
+ expect(store.getState().status).toBe(STATUS.READY);
236
+ expect(store.getState().action).toBe(ACTIONS.RESET);
237
+ });
238
+
239
+ it('resets with RUNNING status when restart=true', () => {
240
+ const store = createStore({
241
+ steps: [{ target: '.a', content: 'A' }],
242
+ } as any);
243
+
244
+ store.start();
245
+ store.reset(true);
246
+ expect(store.getState().status).toBe(STATUS.RUNNING);
247
+ });
248
+
249
+ it('does nothing in controlled mode', () => {
250
+ const store = createStore({
251
+ stepIndex: 0,
252
+ steps: [{ target: '.a', content: 'A' }],
253
+ } as any);
254
+
255
+ store.start();
256
+ store.reset();
257
+ expect(store.getState().index).toBe(0);
258
+ expect(store.getState().status).toBe(STATUS.RUNNING);
259
+ });
260
+ });
261
+
262
+ describe('stop', () => {
263
+ it('pauses the tour', () => {
264
+ const store = createStore({
265
+ steps: [{ target: '.a', content: 'A' }],
266
+ } as any);
267
+
268
+ store.start();
269
+ store.stop();
270
+ expect(store.getState().status).toBe(STATUS.PAUSED);
271
+ expect(store.getState().action).toBe(ACTIONS.STOP);
272
+ });
273
+
274
+ it('advances index when advance=true', () => {
275
+ const store = createStore({
276
+ steps: [
277
+ { target: '.a', content: 'A' },
278
+ { target: '.b', content: 'B' },
279
+ ],
280
+ } as any);
281
+
282
+ store.start();
283
+ store.stop(true);
284
+ expect(store.getState().index).toBe(1);
285
+ });
286
+
287
+ it('does nothing when already finished', () => {
288
+ const store = createStore({
289
+ steps: [{ target: '.a', content: 'A' }],
290
+ } as any);
291
+
292
+ store.start();
293
+ store.next(); // finishes
294
+ store.stop();
295
+ expect(store.getState().status).toBe(STATUS.FINISHED);
296
+ });
297
+ });
298
+
299
+ describe('listeners', () => {
300
+ it('notifies listener on state changes', () => {
301
+ const listener = vi.fn();
302
+ const store = createStore({
303
+ steps: [{ target: '.a', content: 'A' }],
304
+ } as any);
305
+
306
+ store.addListener(listener);
307
+ store.start();
308
+
309
+ expect(listener).toHaveBeenCalledTimes(1);
310
+ expect(listener).toHaveBeenCalledWith(
311
+ expect.objectContaining({ status: STATUS.RUNNING }),
312
+ );
313
+ });
314
+
315
+ it('does not notify when state has not changed', () => {
316
+ const listener = vi.fn();
317
+ const store = createStore({
318
+ steps: [{ target: '.a', content: 'A' }],
319
+ } as any);
320
+
321
+ store.addListener(listener);
322
+ // Update with same state - should not notify
323
+ store.update({ action: ACTIONS.INIT });
324
+
325
+ // The listener may or may not be called depending on whether state actually changed
326
+ // The point is the store works without error
327
+ });
328
+ });
329
+
330
+ describe('setSteps', () => {
331
+ it('updates the size', () => {
332
+ const store = createStore();
333
+ store.setSteps([
334
+ { target: '.a', content: 'A' },
335
+ { target: '.b', content: 'B' },
336
+ ] as any);
337
+ expect(store.getState().size).toBe(2);
338
+ });
339
+
340
+ it('transitions from WAITING to RUNNING when steps arrive', () => {
341
+ const store = createStore();
342
+ store.start(); // WAITING since no steps
343
+ expect(store.getState().status).toBe(STATUS.WAITING);
344
+
345
+ store.setSteps([{ target: '.a', content: 'A' }] as any);
346
+ expect(store.getState().status).toBe(STATUS.RUNNING);
347
+ });
348
+ });
349
+
350
+ describe('getHelpers', () => {
351
+ it('returns helper functions', () => {
352
+ const store = createStore();
353
+ const helpers = store.getHelpers();
354
+
355
+ expect(typeof helpers.close).toBe('function');
356
+ expect(typeof helpers.go).toBe('function');
357
+ expect(typeof helpers.info).toBe('function');
358
+ expect(typeof helpers.next).toBe('function');
359
+ expect(typeof helpers.open).toBe('function');
360
+ expect(typeof helpers.prev).toBe('function');
361
+ expect(typeof helpers.reset).toBe('function');
362
+ expect(typeof helpers.skip).toBe('function');
363
+ });
364
+ });
365
+
366
+ describe('update', () => {
367
+ it('updates state with valid keys', () => {
368
+ const store = createStore({
369
+ steps: [{ target: '.a', content: 'A' }],
370
+ } as any);
371
+
372
+ store.start();
373
+ store.update({ lifecycle: LIFECYCLE.TOOLTIP });
374
+ expect(store.getState().lifecycle).toBe(LIFECYCLE.TOOLTIP);
375
+ });
376
+
377
+ it('throws on invalid keys', () => {
378
+ const store = createStore();
379
+ expect(() => {
380
+ store.update({ invalid: 'key' } as any);
381
+ }).toThrow('State is not valid');
382
+ });
383
+ });
384
+ });
@@ -1,12 +1,11 @@
1
1
  import deepmerge from 'deepmerge';
2
2
  import is from 'is-lite';
3
3
 
4
- import { defaultFloaterProps, defaultLocale, defaultStep } from '~/defaults';
4
+ import { defaultLocale, defaultStep } from '~/defaults';
5
5
  import getStyles from '~/styles';
6
6
  import { Props, Step, StepMerged } from '~/types';
7
7
 
8
- import { getElement, hasCustomScrollParent } from './dom';
9
- import { log, omit, pick } from './helpers';
8
+ import { log, pick } from './helpers';
10
9
 
11
10
  function getTourProps(props: Props) {
12
11
  return pick(
@@ -17,7 +16,6 @@ function getTourProps(props: Props) {
17
16
  'disableOverlayClose',
18
17
  'disableScrolling',
19
18
  'disableScrollParentFix',
20
- 'floaterProps',
21
19
  'hideBackButton',
22
20
  'hideCloseButton',
23
21
  'locale',
@@ -37,38 +35,11 @@ export function getMergedStep(props: Props, currentStep?: Step): StepMerged {
37
35
  }) as StepMerged;
38
36
 
39
37
  const mergedStyles = getStyles(props, mergedStep);
40
- const scrollParent = hasCustomScrollParent(
41
- getElement(mergedStep.target),
42
- mergedStep.disableScrollParentFix,
43
- );
44
- const floaterProps = deepmerge.all([
45
- defaultFloaterProps,
46
- props.floaterProps ?? {},
47
- mergedStep.floaterProps ?? {},
48
- ]) as any;
49
-
50
- // Set react-floater props
51
- floaterProps.offset = mergedStep.offset;
52
- floaterProps.styles = deepmerge(floaterProps.styles ?? {}, mergedStyles.floaterStyles);
53
-
54
- floaterProps.offset += props.spotlightPadding ?? mergedStep.spotlightPadding ?? 0;
55
-
56
- if (mergedStep.placementBeacon && floaterProps.wrapperOptions) {
57
- floaterProps.wrapperOptions.placement = mergedStep.placementBeacon;
58
- }
59
-
60
- if (scrollParent && floaterProps.modifiers?.preventOverflow) {
61
- floaterProps.modifiers.preventOverflow.options = {
62
- ...floaterProps.modifiers.preventOverflow.options,
63
- boundary: 'window',
64
- };
65
- }
66
38
 
67
39
  return {
68
40
  ...mergedStep,
69
41
  locale: deepmerge.all([defaultLocale, props.locale ?? {}, mergedStep.locale || {}]),
70
- floaterProps,
71
- styles: omit(mergedStyles, 'floaterStyles'),
42
+ styles: mergedStyles,
72
43
  };
73
44
  }
74
45
 
@@ -8,7 +8,6 @@ import { hasValidKeys, objectKeys, omit } from './helpers';
8
8
 
9
9
  type StateWithContinuous = State & { continuous: boolean };
10
10
  type Listener = (state: State) => void;
11
- type PopperData = any;
12
11
 
13
12
  const defaultState: State = {
14
13
  action: 'init',
@@ -22,8 +21,6 @@ const defaultState: State = {
22
21
  const validKeys = objectKeys(omit(defaultState, 'controlled', 'size'));
23
22
 
24
23
  class Store {
25
- private beaconPopper: PopperData | null;
26
- private tooltipPopper: PopperData | null;
27
24
  private data: Map<string, any> = new Map();
28
25
  private listener: Listener | null;
29
26
  private store: Map<string, any> = new Map();
@@ -44,8 +41,6 @@ class Store {
44
41
  true,
45
42
  );
46
43
 
47
- this.beaconPopper = null;
48
- this.tooltipPopper = null;
49
44
  this.listener = null;
50
45
  this.setSteps(steps);
51
46
  }
@@ -160,27 +155,6 @@ class Store {
160
155
  };
161
156
  }
162
157
 
163
- public getPopper = (name: 'beacon' | 'tooltip'): PopperData | null => {
164
- if (name === 'beacon') {
165
- return this.beaconPopper;
166
- }
167
-
168
- return this.tooltipPopper;
169
- };
170
-
171
- public setPopper = (name: 'beacon' | 'tooltip', popper: PopperData) => {
172
- if (name === 'beacon') {
173
- this.beaconPopper = popper;
174
- } else {
175
- this.tooltipPopper = popper;
176
- }
177
- };
178
-
179
- public cleanupPoppers = () => {
180
- this.beaconPopper = null;
181
- this.tooltipPopper = null;
182
- };
183
-
184
158
  public close = (origin: Origin | null = null) => {
185
159
  const { index, status } = this.getState();
186
160
 
package/src/styles.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import deepmerge from 'deepmerge';
2
2
 
3
3
  import { hexToRGB } from './modules/helpers';
4
- import { Props, StepMerged, StylesOptions, StylesWithFloaterStyles } from './types';
4
+ import { Props, StepMerged, Styles, StylesOptions } from './types';
5
5
 
6
6
  const defaultOptions = {
7
7
  arrowColor: '#fff',
@@ -33,8 +33,7 @@ const spotlight = {
33
33
  };
34
34
 
35
35
  export default function getStyles(props: Props, step: StepMerged) {
36
- const { floaterProps, styles } = props;
37
- const mergedFloaterProps = deepmerge(step.floaterProps ?? {}, floaterProps ?? {});
36
+ const { styles } = props;
38
37
  const mergedStyles = deepmerge(styles ?? {}, step.styles ?? {});
39
38
  const options = deepmerge(defaultOptions, mergedStyles.options || {}) satisfies StylesOptions;
40
39
  const hideBeacon = step.placement === 'center' || step.disableBeacon;
@@ -176,16 +175,8 @@ export default function getStyles(props: Props, step: StepMerged) {
176
175
  ...spotlight,
177
176
  boxShadow: `0 0 0 9999px ${options.overlayColor}, ${options.spotlightShadow}`,
178
177
  },
179
- floaterStyles: {
180
- arrow: {
181
- color: mergedFloaterProps?.styles?.arrow?.color ?? options.arrowColor,
182
- },
183
- options: {
184
- zIndex: options.zIndex + 100,
185
- },
186
- },
187
178
  options,
188
179
  };
189
180
 
190
- return deepmerge(defaultStyles, mergedStyles) as StylesWithFloaterStyles;
181
+ return deepmerge(defaultStyles, mergedStyles) as Styles;
191
182
  }
@@ -1,5 +1,4 @@
1
1
  import { CSSProperties, ReactNode } from 'react';
2
- import { Styles as FloaterStyles } from 'react-floater';
3
2
  import { ValueOf } from 'type-fest';
4
3
 
5
4
  import { ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS } from '~/literals';
@@ -93,10 +92,6 @@ export interface Styles {
93
92
  tooltipTitle: CSSProperties;
94
93
  }
95
94
 
96
- export interface StylesWithFloaterStyles extends Styles {
97
- floaterStyles: FloaterStyles;
98
- }
99
-
100
95
  export interface StylesOptions {
101
96
  arrowColor: string;
102
97
  backgroundColor: string;
@@ -1,5 +1,4 @@
1
1
  import { ElementType, MouseEventHandler, ReactNode, RefCallback } from 'react';
2
- import { Props as FloaterProps } from 'react-floater';
3
2
  import { PartialDeep, SetRequired, Simplify } from 'type-fest';
4
3
 
5
4
  import type { StoreInstance } from '~/modules/store';
@@ -35,10 +34,6 @@ export type BaseProps = {
35
34
  * @default false
36
35
  */
37
36
  disableScrolling?: boolean;
38
- /**
39
- * Options to be passed to react-floater
40
- */
41
- floaterProps?: Partial<FloaterProps>;
42
37
  /**
43
38
  * Hide the Back button.
44
39
  * @default false
@@ -236,10 +231,6 @@ export type Step = Simplify<
236
231
  * @default click
237
232
  */
238
233
  event?: 'click' | 'hover';
239
- /**
240
- * Options to be passed to react-floater
241
- */
242
- floaterProps?: FloaterProps;
243
234
  /**
244
235
  * Hide the tooltip's footer.
245
236
  * @default false
@@ -379,4 +370,3 @@ export type TooltipRenderProps = Simplify<
379
370
  }
380
371
  >;
381
372
 
382
- export type { Props as FloaterProps } from 'react-floater';