@numidev/react-joyride 1.0.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.
- package/LICENSE +9 -0
- package/README.md +93 -0
- package/dist/index.cjs +2677 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +753 -0
- package/dist/index.d.mts +753 -0
- package/dist/index.mjs +2638 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +158 -0
- package/src/components/Arrow.tsx +138 -0
- package/src/components/Beacon.tsx +141 -0
- package/src/components/Floater.tsx +381 -0
- package/src/components/Loader.tsx +80 -0
- package/src/components/Overlay.tsx +167 -0
- package/src/components/Portal.tsx +17 -0
- package/src/components/Step.tsx +72 -0
- package/src/components/Tooltip/CloseButton.tsx +29 -0
- package/src/components/Tooltip/DefaultTooltip.tsx +82 -0
- package/src/components/Tooltip/index.tsx +163 -0
- package/src/components/TourRenderer.tsx +157 -0
- package/src/defaults.ts +64 -0
- package/src/global.d.ts +8 -0
- package/src/hooks/useControls.ts +219 -0
- package/src/hooks/useDebugLogger.ts +58 -0
- package/src/hooks/useEventEmitter.ts +55 -0
- package/src/hooks/useFocusTrap.ts +72 -0
- package/src/hooks/useJoyride.tsx +32 -0
- package/src/hooks/useLifecycleEffect.ts +512 -0
- package/src/hooks/usePortalElement.ts +49 -0
- package/src/hooks/usePropSync.ts +84 -0
- package/src/hooks/useScrollEffect.ts +217 -0
- package/src/hooks/useTargetPosition.ts +154 -0
- package/src/hooks/useTourEngine.ts +106 -0
- package/src/index.tsx +23 -0
- package/src/literals/index.ts +61 -0
- package/src/modules/changes.ts +20 -0
- package/src/modules/dom.ts +359 -0
- package/src/modules/helpers.tsx +230 -0
- package/src/modules/step.ts +156 -0
- package/src/modules/store.ts +215 -0
- package/src/modules/svg.ts +40 -0
- package/src/styles.ts +183 -0
- package/src/types/common.ts +315 -0
- package/src/types/components.ts +84 -0
- package/src/types/events.ts +89 -0
- package/src/types/floating.ts +60 -0
- package/src/types/index.ts +8 -0
- package/src/types/props.ts +124 -0
- package/src/types/state.ts +49 -0
- package/src/types/step.ts +108 -0
- package/src/types/utilities.ts +26 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { usePortalElement } from '~/hooks/usePortalElement';
|
|
4
|
+
import type { MergedProps } from '~/hooks/useTourEngine';
|
|
5
|
+
import { ACTIONS, LIFECYCLE, ORIGIN, STATUS } from '~/literals';
|
|
6
|
+
import createStore from '~/modules/store';
|
|
7
|
+
import type { StoreState } from '~/modules/store';
|
|
8
|
+
|
|
9
|
+
import Loader from '~/components/Loader';
|
|
10
|
+
import Overlay from '~/components/Overlay';
|
|
11
|
+
import Portal from '~/components/Portal';
|
|
12
|
+
import Step from '~/components/Step';
|
|
13
|
+
|
|
14
|
+
import type { Controls, StepMerged } from '~/types';
|
|
15
|
+
|
|
16
|
+
interface TourRendererProps {
|
|
17
|
+
controls: Controls;
|
|
18
|
+
mergedProps: MergedProps;
|
|
19
|
+
state: StoreState;
|
|
20
|
+
step: StepMerged | null;
|
|
21
|
+
store: RefObject<ReturnType<typeof createStore>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function TourRenderer({
|
|
25
|
+
controls,
|
|
26
|
+
mergedProps,
|
|
27
|
+
state,
|
|
28
|
+
step,
|
|
29
|
+
store,
|
|
30
|
+
}: TourRendererProps) {
|
|
31
|
+
const { continuous, debug, nonce, portalElement, scrollToFirstStep } = mergedProps;
|
|
32
|
+
|
|
33
|
+
const element = usePortalElement(portalElement);
|
|
34
|
+
|
|
35
|
+
const { index, lifecycle, status } = state;
|
|
36
|
+
const isRunning = status === STATUS.RUNNING;
|
|
37
|
+
|
|
38
|
+
const [showLoader, setShowLoader] = useState(false);
|
|
39
|
+
const loaderTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
40
|
+
const loaderDelay = step?.loaderDelay ?? 0;
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (state.waiting) {
|
|
44
|
+
if (loaderDelay === 0) {
|
|
45
|
+
setShowLoader(true);
|
|
46
|
+
} else {
|
|
47
|
+
loaderTimerRef.current = setTimeout(() => {
|
|
48
|
+
setShowLoader(true);
|
|
49
|
+
}, loaderDelay);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
setShowLoader(false);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
if (loaderTimerRef.current) {
|
|
57
|
+
clearTimeout(loaderTimerRef.current);
|
|
58
|
+
loaderTimerRef.current = null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}, [loaderDelay, state.waiting]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!isRunning) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const handleKeyboard = (event: KeyboardEvent) => {
|
|
69
|
+
if (!step || lifecycle !== LIFECYCLE.TOOLTIP) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (event.key === 'Escape' && step.dismissKeyAction) {
|
|
74
|
+
if (step.dismissKeyAction === 'next') {
|
|
75
|
+
controls.next(ORIGIN.KEYBOARD);
|
|
76
|
+
} else if (step.dismissKeyAction === 'replay') {
|
|
77
|
+
controls.replay(ORIGIN.KEYBOARD);
|
|
78
|
+
} else {
|
|
79
|
+
controls.close(ORIGIN.KEYBOARD);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
document.body.addEventListener('keydown', handleKeyboard, { passive: true });
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
document.body.removeEventListener('keydown', handleKeyboard);
|
|
88
|
+
};
|
|
89
|
+
}, [controls, isRunning, lifecycle, step]);
|
|
90
|
+
|
|
91
|
+
const handleClickOverlay = useCallback(() => {
|
|
92
|
+
switch (step?.overlayClickAction) {
|
|
93
|
+
case 'close': {
|
|
94
|
+
controls.close(ORIGIN.OVERLAY);
|
|
95
|
+
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'next': {
|
|
99
|
+
controls.next(ORIGIN.OVERLAY);
|
|
100
|
+
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'replay': {
|
|
104
|
+
controls.replay(ORIGIN.OVERLAY);
|
|
105
|
+
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
// No default
|
|
109
|
+
}
|
|
110
|
+
}, [controls, step?.overlayClickAction]);
|
|
111
|
+
|
|
112
|
+
if (!step || !isRunning) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
Hide the overlay when the tour starts, and a beacon will be shown.
|
|
118
|
+
Prevent the overlay from flashing before the beacon is rendered.
|
|
119
|
+
*/
|
|
120
|
+
const hideOverlay =
|
|
121
|
+
state.action === ACTIONS.START && !step.skipBeacon && step.placement !== 'center';
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
{lifecycle !== LIFECYCLE.INIT && (
|
|
126
|
+
<Step
|
|
127
|
+
{...state}
|
|
128
|
+
continuous={continuous}
|
|
129
|
+
controls={controls}
|
|
130
|
+
debug={debug}
|
|
131
|
+
nonce={nonce}
|
|
132
|
+
portalElement={element}
|
|
133
|
+
setPositionData={store.current.setPositionData}
|
|
134
|
+
shouldScroll={!step.skipScroll && (index !== 0 || scrollToFirstStep)}
|
|
135
|
+
step={step}
|
|
136
|
+
updateState={store.current.updateState}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
<Portal element={element}>
|
|
140
|
+
<>
|
|
141
|
+
{showLoader && <Loader nonce={nonce} step={step} />}
|
|
142
|
+
{!hideOverlay && (
|
|
143
|
+
<Overlay
|
|
144
|
+
{...step}
|
|
145
|
+
continuous={continuous}
|
|
146
|
+
lifecycle={lifecycle}
|
|
147
|
+
onClickOverlay={handleClickOverlay}
|
|
148
|
+
portalElement={portalElement ? element : null}
|
|
149
|
+
scrolling={state.scrolling}
|
|
150
|
+
waiting={state.waiting}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
</>
|
|
154
|
+
</Portal>
|
|
155
|
+
</>
|
|
156
|
+
);
|
|
157
|
+
}
|
package/src/defaults.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { FloatingOptions, Locale, Options, Props, Step } from '~/types';
|
|
2
|
+
|
|
3
|
+
export const defaultOptions: Required<Omit<Options, 'after' | 'before'>> = {
|
|
4
|
+
arrowBase: 32,
|
|
5
|
+
arrowColor: '#ffffff',
|
|
6
|
+
arrowSize: 16,
|
|
7
|
+
arrowSpacing: 12,
|
|
8
|
+
backgroundColor: '#ffffff',
|
|
9
|
+
beaconSize: 36,
|
|
10
|
+
beaconTrigger: 'click',
|
|
11
|
+
beforeTimeout: 5000,
|
|
12
|
+
blockTargetInteraction: false,
|
|
13
|
+
buttons: ['back', 'close', 'primary'],
|
|
14
|
+
closeButtonAction: 'close',
|
|
15
|
+
disableFocusTrap: false,
|
|
16
|
+
dismissKeyAction: 'close',
|
|
17
|
+
hideOverlay: false,
|
|
18
|
+
loaderDelay: 300,
|
|
19
|
+
offset: 10,
|
|
20
|
+
overlayClickAction: 'close',
|
|
21
|
+
overlayColor: '#00000080',
|
|
22
|
+
primaryColor: '#000000',
|
|
23
|
+
scrollDuration: 300,
|
|
24
|
+
scrollOffset: 20,
|
|
25
|
+
showProgress: false,
|
|
26
|
+
skipBeacon: false,
|
|
27
|
+
skipScroll: false,
|
|
28
|
+
spotlightPadding: 10,
|
|
29
|
+
spotlightRadius: 4,
|
|
30
|
+
targetWaitTimeout: 1000,
|
|
31
|
+
textColor: '#000000',
|
|
32
|
+
width: 380,
|
|
33
|
+
zIndex: 100,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const defaultFloatingOptions: FloatingOptions = {
|
|
37
|
+
beaconOptions: {
|
|
38
|
+
offset: -18,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const defaultLocale: Locale = {
|
|
43
|
+
back: 'Back',
|
|
44
|
+
close: 'Close',
|
|
45
|
+
last: 'Last',
|
|
46
|
+
next: 'Next',
|
|
47
|
+
nextWithProgress: 'Next ({current} of {total})',
|
|
48
|
+
open: 'Open the dialog',
|
|
49
|
+
skip: 'Skip',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const defaultStep = {
|
|
53
|
+
isFixed: false,
|
|
54
|
+
locale: defaultLocale,
|
|
55
|
+
placement: 'bottom',
|
|
56
|
+
} satisfies Omit<Step, 'content' | 'target'>;
|
|
57
|
+
|
|
58
|
+
export const defaultProps = {
|
|
59
|
+
continuous: false,
|
|
60
|
+
debug: false,
|
|
61
|
+
run: false,
|
|
62
|
+
scrollToFirstStep: false,
|
|
63
|
+
steps: [],
|
|
64
|
+
} satisfies Props;
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { type RefObject, useMemo, useRef } from 'react';
|
|
2
|
+
import is from 'is-lite';
|
|
3
|
+
|
|
4
|
+
import { ACTIONS, LIFECYCLE, STATUS } from '~/literals';
|
|
5
|
+
import { log, omit } from '~/modules/helpers';
|
|
6
|
+
import createStore from '~/modules/store';
|
|
7
|
+
import type { StoreState } from '~/modules/store';
|
|
8
|
+
|
|
9
|
+
import type { Controls, Origin, Status } from '~/types';
|
|
10
|
+
|
|
11
|
+
function getUpdatedIndex(nextIndex: number, size: number): number {
|
|
12
|
+
return Math.min(Math.max(nextIndex, 0), size);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function useControls(
|
|
16
|
+
store: RefObject<ReturnType<typeof createStore>>,
|
|
17
|
+
debug: boolean,
|
|
18
|
+
clearFailures: () => void,
|
|
19
|
+
): Controls {
|
|
20
|
+
const debugRef = useRef(debug);
|
|
21
|
+
const clearFailuresRef = useRef(clearFailures);
|
|
22
|
+
|
|
23
|
+
debugRef.current = debug;
|
|
24
|
+
clearFailuresRef.current = clearFailures;
|
|
25
|
+
|
|
26
|
+
return useMemo(() => {
|
|
27
|
+
const getState = (): StoreState => store.current.getSnapshot();
|
|
28
|
+
|
|
29
|
+
const close = (origin: Origin | null = null) => {
|
|
30
|
+
const { index, status } = getState();
|
|
31
|
+
|
|
32
|
+
if (status !== STATUS.RUNNING) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
store.current.updateState({
|
|
37
|
+
action: ACTIONS.CLOSE,
|
|
38
|
+
index: index + 1,
|
|
39
|
+
origin,
|
|
40
|
+
lifecycle: LIFECYCLE.COMPLETE,
|
|
41
|
+
positioned: false,
|
|
42
|
+
scrolling: false,
|
|
43
|
+
waiting: false,
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const go = (nextIndex: number) => {
|
|
48
|
+
const { controlled, size, status } = getState();
|
|
49
|
+
|
|
50
|
+
if (controlled) {
|
|
51
|
+
log(debugRef.current, 'tour', 'go() is not supported in controlled mode');
|
|
52
|
+
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (status !== STATUS.RUNNING) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
store.current.updateState({
|
|
61
|
+
action: ACTIONS.GO,
|
|
62
|
+
index: nextIndex,
|
|
63
|
+
lifecycle: LIFECYCLE.COMPLETE,
|
|
64
|
+
positioned: false,
|
|
65
|
+
scrolling: false,
|
|
66
|
+
status: nextIndex < size ? status : STATUS.FINISHED,
|
|
67
|
+
waiting: false,
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const info = () => omit(store.current.getSnapshot(), 'positioned');
|
|
72
|
+
|
|
73
|
+
const next = (origin?: Origin | null) => {
|
|
74
|
+
const { index, size, status } = getState();
|
|
75
|
+
|
|
76
|
+
if (status !== STATUS.RUNNING) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
store.current.updateState({
|
|
81
|
+
action: ACTIONS.NEXT,
|
|
82
|
+
index: getUpdatedIndex(index + 1, size),
|
|
83
|
+
lifecycle: LIFECYCLE.COMPLETE,
|
|
84
|
+
origin,
|
|
85
|
+
positioned: false,
|
|
86
|
+
scrolling: false,
|
|
87
|
+
waiting: false,
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const open = () => {
|
|
92
|
+
const { status } = getState();
|
|
93
|
+
|
|
94
|
+
if (status !== STATUS.RUNNING) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
store.current.updateState({
|
|
99
|
+
action: ACTIONS.UPDATE,
|
|
100
|
+
lifecycle: LIFECYCLE.TOOLTIP_BEFORE,
|
|
101
|
+
positioned: false,
|
|
102
|
+
scrolling: false,
|
|
103
|
+
waiting: false,
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const previous = (origin?: Origin | null) => {
|
|
108
|
+
const { index, size, status } = getState();
|
|
109
|
+
|
|
110
|
+
if (status !== STATUS.RUNNING) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
store.current.updateState({
|
|
115
|
+
action: ACTIONS.PREV,
|
|
116
|
+
index: getUpdatedIndex(index - 1, size),
|
|
117
|
+
lifecycle: LIFECYCLE.COMPLETE,
|
|
118
|
+
origin,
|
|
119
|
+
positioned: false,
|
|
120
|
+
scrolling: false,
|
|
121
|
+
waiting: false,
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const replay = (origin?: Origin | null) => {
|
|
126
|
+
const { lifecycle, status } = getState();
|
|
127
|
+
|
|
128
|
+
if (status !== STATUS.RUNNING || lifecycle !== LIFECYCLE.TOOLTIP) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
store.current.updateState({
|
|
133
|
+
action: ACTIONS.REPLAY,
|
|
134
|
+
lifecycle: LIFECYCLE.COMPLETE,
|
|
135
|
+
origin,
|
|
136
|
+
positioned: false,
|
|
137
|
+
scrolling: false,
|
|
138
|
+
waiting: false,
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const reset = (restart = false) => {
|
|
143
|
+
const { controlled } = getState();
|
|
144
|
+
|
|
145
|
+
if (controlled) {
|
|
146
|
+
log(debugRef.current, 'tour', 'reset() is not supported in controlled mode');
|
|
147
|
+
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
clearFailuresRef.current();
|
|
152
|
+
store.current.updateState({
|
|
153
|
+
action: ACTIONS.RESET,
|
|
154
|
+
index: 0,
|
|
155
|
+
lifecycle: LIFECYCLE.INIT,
|
|
156
|
+
positioned: false,
|
|
157
|
+
scrolling: false,
|
|
158
|
+
status: restart ? STATUS.RUNNING : STATUS.READY,
|
|
159
|
+
waiting: false,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const skip = (origin?: Extract<Origin, 'button_close' | 'button_skip'> | null) => {
|
|
164
|
+
const { status } = getState();
|
|
165
|
+
|
|
166
|
+
if (status !== STATUS.RUNNING) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
store.current.updateState({
|
|
171
|
+
action: ACTIONS.SKIP,
|
|
172
|
+
lifecycle: LIFECYCLE.COMPLETE,
|
|
173
|
+
origin,
|
|
174
|
+
positioned: false,
|
|
175
|
+
scrolling: false,
|
|
176
|
+
status: STATUS.SKIPPED,
|
|
177
|
+
waiting: false,
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const start = (nextIndex?: number) => {
|
|
182
|
+
const { index, size } = getState();
|
|
183
|
+
|
|
184
|
+
clearFailuresRef.current();
|
|
185
|
+
store.current.updateState(
|
|
186
|
+
{
|
|
187
|
+
action: ACTIONS.START,
|
|
188
|
+
index: is.number(nextIndex) ? nextIndex : index,
|
|
189
|
+
lifecycle: LIFECYCLE.INIT,
|
|
190
|
+
positioned: false,
|
|
191
|
+
scrolling: false,
|
|
192
|
+
status: size ? STATUS.RUNNING : STATUS.WAITING,
|
|
193
|
+
waiting: false,
|
|
194
|
+
},
|
|
195
|
+
true,
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const stop = (advance = false) => {
|
|
200
|
+
const { index, status } = getState();
|
|
201
|
+
|
|
202
|
+
if (([STATUS.FINISHED, STATUS.SKIPPED] as Array<Status>).includes(status)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
store.current.updateState({
|
|
207
|
+
action: ACTIONS.STOP,
|
|
208
|
+
index: index + (advance ? 1 : 0),
|
|
209
|
+
lifecycle: LIFECYCLE.COMPLETE,
|
|
210
|
+
positioned: false,
|
|
211
|
+
scrolling: false,
|
|
212
|
+
status: STATUS.PAUSED,
|
|
213
|
+
waiting: false,
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return { close, go, info, next, open, prev: previous, replay, reset, skip, start, stop };
|
|
218
|
+
}, [store]);
|
|
219
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type RefObject, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { log } from '~/modules/helpers';
|
|
4
|
+
import type createStore from '~/modules/store';
|
|
5
|
+
import type { StoreState } from '~/modules/store';
|
|
6
|
+
|
|
7
|
+
const skipFields = new Set(['origin', 'positioned']);
|
|
8
|
+
|
|
9
|
+
export default function useDebugLogger(
|
|
10
|
+
store: RefObject<ReturnType<typeof createStore>>,
|
|
11
|
+
debug: boolean,
|
|
12
|
+
): void {
|
|
13
|
+
const previousRef = useRef<StoreState | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!debug) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const current = store.current.getSnapshot();
|
|
21
|
+
|
|
22
|
+
log(true, 'tour', 'init', current);
|
|
23
|
+
previousRef.current = current;
|
|
24
|
+
|
|
25
|
+
return store.current.subscribe(state => {
|
|
26
|
+
const previous = previousRef.current;
|
|
27
|
+
|
|
28
|
+
previousRef.current = state;
|
|
29
|
+
|
|
30
|
+
if (!previous) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const changes: Record<string, { from: unknown; to: unknown }> = {};
|
|
35
|
+
let isTourLevel = false;
|
|
36
|
+
|
|
37
|
+
for (const key of Object.keys(state) as Array<keyof StoreState>) {
|
|
38
|
+
if (state[key] !== previous[key] && !skipFields.has(key)) {
|
|
39
|
+
changes[key] = { from: previous[key], to: state[key] };
|
|
40
|
+
|
|
41
|
+
if (key === 'status' || key === 'size') {
|
|
42
|
+
isTourLevel = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Object.keys(changes).length) {
|
|
48
|
+
const outOfBounds = !isTourLevel && state.index >= state.size;
|
|
49
|
+
|
|
50
|
+
if (!outOfBounds) {
|
|
51
|
+
const scope = isTourLevel ? 'tour' : `step:${state.index}`;
|
|
52
|
+
|
|
53
|
+
log(true, scope, 'state', changes);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}, [debug, store]);
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type RefObject, useCallback, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import createStore from '~/modules/store';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Actions,
|
|
7
|
+
Controls,
|
|
8
|
+
EventData,
|
|
9
|
+
EventHandler,
|
|
10
|
+
Events,
|
|
11
|
+
Lifecycle,
|
|
12
|
+
ScrollData,
|
|
13
|
+
StepMerged,
|
|
14
|
+
} from '~/types';
|
|
15
|
+
|
|
16
|
+
export type EmitEvent = (
|
|
17
|
+
type: Events,
|
|
18
|
+
step: StepMerged,
|
|
19
|
+
overrides?: {
|
|
20
|
+
action?: Actions;
|
|
21
|
+
error?: Error | null;
|
|
22
|
+
index?: number;
|
|
23
|
+
lifecycle?: Lifecycle;
|
|
24
|
+
scroll?: ScrollData | null;
|
|
25
|
+
},
|
|
26
|
+
) => void;
|
|
27
|
+
|
|
28
|
+
export default function useEventEmitter(
|
|
29
|
+
onEvent: EventHandler | undefined,
|
|
30
|
+
controls: Controls,
|
|
31
|
+
store: RefObject<ReturnType<typeof createStore>>,
|
|
32
|
+
): EmitEvent {
|
|
33
|
+
const onEventRef = useRef(onEvent);
|
|
34
|
+
const controlsRef = useRef(controls);
|
|
35
|
+
|
|
36
|
+
onEventRef.current = onEvent;
|
|
37
|
+
controlsRef.current = controls;
|
|
38
|
+
|
|
39
|
+
return useCallback(
|
|
40
|
+
(type, step, overrides) => {
|
|
41
|
+
const data: EventData = {
|
|
42
|
+
...store.current.getEventState(),
|
|
43
|
+
error: null,
|
|
44
|
+
scroll: null,
|
|
45
|
+
step,
|
|
46
|
+
type,
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
onEventRef.current?.(data, controlsRef.current);
|
|
51
|
+
store.current.dispatch(data, controlsRef.current);
|
|
52
|
+
},
|
|
53
|
+
[store],
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { noop } from '~/modules/helpers';
|
|
4
|
+
|
|
5
|
+
const TABBABLE_SELECTOR =
|
|
6
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), area[href], [tabindex]:not([tabindex="-1"]), [contenteditable]';
|
|
7
|
+
|
|
8
|
+
export default function useFocusTrap(element: HTMLElement | null, selector?: string | null): void {
|
|
9
|
+
const previousFocus = useRef<HTMLElement | null>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!element) {
|
|
13
|
+
return noop;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
previousFocus.current = document.activeElement as HTMLElement | null;
|
|
17
|
+
|
|
18
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
19
|
+
if (event.key !== 'Tab') {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const elements = [...element.querySelectorAll<HTMLElement>(TABBABLE_SELECTOR)];
|
|
24
|
+
const { shiftKey } = event;
|
|
25
|
+
|
|
26
|
+
if (!elements.length) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
|
|
32
|
+
let index = document.activeElement
|
|
33
|
+
? elements.indexOf(document.activeElement as HTMLElement)
|
|
34
|
+
: 0;
|
|
35
|
+
|
|
36
|
+
if (index === -1 || (!shiftKey && index + 1 === elements.length)) {
|
|
37
|
+
index = 0;
|
|
38
|
+
} else if (shiftKey && index === 0) {
|
|
39
|
+
index = elements.length - 1;
|
|
40
|
+
} else {
|
|
41
|
+
index += shiftKey ? -1 : 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
elements[index].focus();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
element.addEventListener('keydown', handleKeyDown, false);
|
|
48
|
+
|
|
49
|
+
let timerId: ReturnType<typeof setTimeout> | undefined;
|
|
50
|
+
|
|
51
|
+
if (selector) {
|
|
52
|
+
const target = element.querySelector<HTMLElement>(selector);
|
|
53
|
+
|
|
54
|
+
if (target) {
|
|
55
|
+
// Delay focus to allow Floater's CSS transition to complete
|
|
56
|
+
timerId = setTimeout(() => {
|
|
57
|
+
target.focus({ preventScroll: true });
|
|
58
|
+
}, 100);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
element.removeEventListener('keydown', handleKeyDown);
|
|
64
|
+
|
|
65
|
+
if (timerId !== undefined) {
|
|
66
|
+
clearTimeout(timerId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
previousFocus.current?.focus({ preventScroll: true });
|
|
70
|
+
};
|
|
71
|
+
}, [element, selector]);
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import useTourEngine from '~/hooks/useTourEngine';
|
|
4
|
+
import { canUseDOM } from '~/modules/dom';
|
|
5
|
+
import { omit } from '~/modules/helpers';
|
|
6
|
+
|
|
7
|
+
import TourRenderer from '~/components/TourRenderer';
|
|
8
|
+
|
|
9
|
+
import type { EventHandler, Events, Props, UseJoyrideReturn } from '~/types';
|
|
10
|
+
|
|
11
|
+
export function useJoyride(props: Props): UseJoyrideReturn {
|
|
12
|
+
const { controls, failures, mergedProps, state, step, store } = useTourEngine(props);
|
|
13
|
+
|
|
14
|
+
const on = useCallback(
|
|
15
|
+
(eventType: Events, handler: EventHandler) => store.current.on(eventType, handler),
|
|
16
|
+
[store],
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const publicState = useMemo(() => omit(state, 'positioned'), [state]);
|
|
20
|
+
|
|
21
|
+
const Tour = canUseDOM() ? (
|
|
22
|
+
<TourRenderer
|
|
23
|
+
controls={controls}
|
|
24
|
+
mergedProps={mergedProps}
|
|
25
|
+
state={state}
|
|
26
|
+
step={step}
|
|
27
|
+
store={store}
|
|
28
|
+
/>
|
|
29
|
+
) : null;
|
|
30
|
+
|
|
31
|
+
return { controls, failures, on, state: publicState, step, Tour };
|
|
32
|
+
}
|