@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.
Files changed (51) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +93 -0
  3. package/dist/index.cjs +2677 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +753 -0
  6. package/dist/index.d.mts +753 -0
  7. package/dist/index.mjs +2638 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +158 -0
  10. package/src/components/Arrow.tsx +138 -0
  11. package/src/components/Beacon.tsx +141 -0
  12. package/src/components/Floater.tsx +381 -0
  13. package/src/components/Loader.tsx +80 -0
  14. package/src/components/Overlay.tsx +167 -0
  15. package/src/components/Portal.tsx +17 -0
  16. package/src/components/Step.tsx +72 -0
  17. package/src/components/Tooltip/CloseButton.tsx +29 -0
  18. package/src/components/Tooltip/DefaultTooltip.tsx +82 -0
  19. package/src/components/Tooltip/index.tsx +163 -0
  20. package/src/components/TourRenderer.tsx +157 -0
  21. package/src/defaults.ts +64 -0
  22. package/src/global.d.ts +8 -0
  23. package/src/hooks/useControls.ts +219 -0
  24. package/src/hooks/useDebugLogger.ts +58 -0
  25. package/src/hooks/useEventEmitter.ts +55 -0
  26. package/src/hooks/useFocusTrap.ts +72 -0
  27. package/src/hooks/useJoyride.tsx +32 -0
  28. package/src/hooks/useLifecycleEffect.ts +512 -0
  29. package/src/hooks/usePortalElement.ts +49 -0
  30. package/src/hooks/usePropSync.ts +84 -0
  31. package/src/hooks/useScrollEffect.ts +217 -0
  32. package/src/hooks/useTargetPosition.ts +154 -0
  33. package/src/hooks/useTourEngine.ts +106 -0
  34. package/src/index.tsx +23 -0
  35. package/src/literals/index.ts +61 -0
  36. package/src/modules/changes.ts +20 -0
  37. package/src/modules/dom.ts +359 -0
  38. package/src/modules/helpers.tsx +230 -0
  39. package/src/modules/step.ts +156 -0
  40. package/src/modules/store.ts +215 -0
  41. package/src/modules/svg.ts +40 -0
  42. package/src/styles.ts +183 -0
  43. package/src/types/common.ts +315 -0
  44. package/src/types/components.ts +84 -0
  45. package/src/types/events.ts +89 -0
  46. package/src/types/floating.ts +60 -0
  47. package/src/types/index.ts +8 -0
  48. package/src/types/props.ts +124 -0
  49. package/src/types/state.ts +49 -0
  50. package/src/types/step.ts +108 -0
  51. package/src/types/utilities.ts +26 -0
@@ -0,0 +1,359 @@
1
+ import scroll from 'scroll';
2
+ import scrollParent from 'scrollparent';
3
+
4
+ import { LIFECYCLE } from '~/literals';
5
+
6
+ import type { Lifecycle, StepMerged, StepTarget } from '~/types';
7
+
8
+ interface NeedsScrollingOptions {
9
+ isFirstStep: boolean;
10
+ scrollToFirstStep: boolean;
11
+ step: StepMerged;
12
+ target: HTMLElement | null;
13
+ targetLifecycle?: Lifecycle;
14
+ }
15
+
16
+ export function canUseDOM() {
17
+ return !!(typeof window !== 'undefined' && window.document?.createElement);
18
+ }
19
+
20
+ /**
21
+ * Get the absolute document-relative offset of an element by walking up the offsetParent chain.
22
+ */
23
+ export function getAbsoluteOffset(element: HTMLElement): { left: number; top: number } {
24
+ let top = 0;
25
+ let left = 0;
26
+ let current: HTMLElement | null = element;
27
+
28
+ while (current) {
29
+ top += current.offsetTop;
30
+ left += current.offsetLeft;
31
+ current = current.offsetParent as HTMLElement | null;
32
+ }
33
+
34
+ return { left, top };
35
+ }
36
+
37
+ /**
38
+ * Find the bounding client rect
39
+ */
40
+ export function getClientRect(element: HTMLElement | null) {
41
+ if (!element) {
42
+ return null;
43
+ }
44
+
45
+ return element.getBoundingClientRect();
46
+ }
47
+
48
+ /**
49
+ * Helper function to get the browser-normalized "document height"
50
+ */
51
+ export function getDocumentHeight(median = false): number {
52
+ const { body, documentElement } = document;
53
+
54
+ if (!body || !documentElement) {
55
+ return 0;
56
+ }
57
+
58
+ if (median) {
59
+ const heights = [
60
+ body.scrollHeight,
61
+ body.offsetHeight,
62
+ documentElement.clientHeight,
63
+ documentElement.scrollHeight,
64
+ documentElement.offsetHeight,
65
+ ].sort((a, b) => a - b);
66
+ const middle = Math.floor(heights.length / 2);
67
+
68
+ if (heights.length % 2 === 0) {
69
+ return (heights[middle - 1] + heights[middle]) / 2;
70
+ }
71
+
72
+ return heights[middle];
73
+ }
74
+
75
+ return Math.max(
76
+ body.scrollHeight,
77
+ body.offsetHeight,
78
+ documentElement.clientHeight,
79
+ documentElement.scrollHeight,
80
+ documentElement.offsetHeight,
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Find and return the target DOM element based on a step's 'target'.
86
+ */
87
+ export function getElement(element?: StepTarget): HTMLElement | null {
88
+ if (!element) {
89
+ return null;
90
+ }
91
+
92
+ if (typeof element === 'function') {
93
+ try {
94
+ return element();
95
+ } catch (error: any) {
96
+ if (process.env.NODE_ENV !== 'production') {
97
+ // eslint-disable-next-line no-console
98
+ console.error(error);
99
+ }
100
+
101
+ return null;
102
+ }
103
+ }
104
+
105
+ if (typeof element === 'object' && 'current' in element) {
106
+ return element.current;
107
+ }
108
+
109
+ if (typeof element === 'string') {
110
+ try {
111
+ return document.querySelector(element);
112
+ } catch (error: any) {
113
+ if (process.env.NODE_ENV !== 'production') {
114
+ // eslint-disable-next-line no-console
115
+ console.error(error);
116
+ }
117
+
118
+ return null;
119
+ }
120
+ }
121
+
122
+ return element;
123
+ }
124
+
125
+ /**
126
+ * Find and return the target DOM element based on a step's 'target'.
127
+ */
128
+ export function getElementPosition(
129
+ element: HTMLElement | null,
130
+ offset: number,
131
+ isFixed?: boolean,
132
+ ): number {
133
+ const elementRect = getClientRect(element);
134
+ const parent = getScrollParent(element);
135
+ const hasScrollParent = parent ? !parent.isSameNode(scrollDocument()) : false;
136
+ const isFixedTarget = isFixed ?? hasPosition(element);
137
+ let parentTop = 0;
138
+ let top = elementRect?.top ?? 0;
139
+
140
+ if (hasScrollParent && isFixedTarget) {
141
+ top = elementRect?.top ?? 0;
142
+ } else if (parent instanceof HTMLElement) {
143
+ parentTop = parent.scrollTop;
144
+
145
+ if (!hasScrollParent && !isFixedTarget) {
146
+ top += parentTop;
147
+ }
148
+
149
+ if (!parent.isSameNode(scrollDocument())) {
150
+ top += scrollDocument().scrollTop;
151
+ }
152
+ }
153
+
154
+ return Math.floor(top - offset);
155
+ }
156
+
157
+ /**
158
+ * Get the scroll parent of an element.
159
+ * If the detected parent doesn't actually scroll, fall back to the document.
160
+ */
161
+ export function getScrollParent(element: HTMLElement | null, forListener?: boolean) {
162
+ if (!element) {
163
+ return scrollDocument();
164
+ }
165
+
166
+ const parent = scrollParent(element) as HTMLElement;
167
+
168
+ if (parent) {
169
+ if (parent.isSameNode(scrollDocument())) {
170
+ if (forListener) {
171
+ return document;
172
+ }
173
+
174
+ return scrollDocument();
175
+ }
176
+
177
+ const hasScrolling = parent.scrollHeight > parent.offsetHeight;
178
+
179
+ if (!hasScrolling) {
180
+ return scrollDocument();
181
+ }
182
+ }
183
+
184
+ return parent;
185
+ }
186
+
187
+ export function getScrollTargetToCenter(element: Element): number {
188
+ const rect = element.getBoundingClientRect();
189
+ const documentElement = scrollDocument();
190
+ const containerCenter = rect.top + rect.height / 2;
191
+ const viewportCenter = window.innerHeight / 2;
192
+
193
+ return Math.max(0, documentElement.scrollTop + containerCenter - viewportCenter);
194
+ }
195
+
196
+ /**
197
+ * Get the scrollTop position
198
+ */
199
+ export function getScrollTo(element: HTMLElement | null, offset: number): number {
200
+ if (!element) {
201
+ return 0;
202
+ }
203
+
204
+ const parentElement = scrollParent(element) ?? (scrollDocument() as HTMLElement);
205
+ const scrollMarginTop = parseFloat(getComputedStyle(element).scrollMarginTop) || 0;
206
+
207
+ const parentRect = getClientRect(parentElement);
208
+ const parentScrollTop = parentElement.scrollTop ?? 0;
209
+
210
+ const { offsetTop = 0, scrollTop = 0 } = parentElement;
211
+ let top = element.getBoundingClientRect().top + scrollTop;
212
+
213
+ if (!!offsetTop && (hasCustomScrollParent(element) || hasCustomOffsetParent(element))) {
214
+ const elementRect = element.getBoundingClientRect();
215
+ const elementTopInContainer = elementRect.top - (parentRect?.top ?? 0);
216
+ const elementBottomInContainer = elementTopInContainer + elementRect.height;
217
+ const containerHeight = parentElement.clientHeight;
218
+ const margin = containerHeight * 0.2;
219
+
220
+ // eslint-disable-next-line unicorn/prefer-ternary
221
+ if (elementTopInContainer >= margin && elementBottomInContainer <= containerHeight - margin) {
222
+ top = parentScrollTop;
223
+ } else {
224
+ top = elementTopInContainer + parentScrollTop;
225
+ }
226
+ }
227
+
228
+ const output = Math.floor(top - offset - scrollMarginTop);
229
+
230
+ return output < 0 ? 0 : output;
231
+ }
232
+
233
+ /**
234
+ * Check if the element has custom offset parent
235
+ */
236
+ export function hasCustomOffsetParent(element: HTMLElement): boolean {
237
+ return element.offsetParent !== document.body;
238
+ }
239
+
240
+ /**
241
+ * Check if the element has custom scroll parent
242
+ */
243
+ export function hasCustomScrollParent(element: HTMLElement | null): boolean {
244
+ if (!element) {
245
+ return false;
246
+ }
247
+
248
+ const parent = getScrollParent(element);
249
+
250
+ return parent ? !parent.isSameNode(scrollDocument()) : false;
251
+ }
252
+
253
+ /**
254
+ * Check if an element has fixed/sticky position
255
+ */
256
+ export function hasPosition(el: Element | Node | null, type: string = 'fixed'): boolean {
257
+ if (!el || !(el instanceof Element)) {
258
+ return false;
259
+ }
260
+
261
+ const { nodeName } = el;
262
+
263
+ if (nodeName === 'BODY' || nodeName === 'HTML') {
264
+ return false;
265
+ }
266
+
267
+ if (getComputedStyle(el).position === type) {
268
+ return true;
269
+ }
270
+
271
+ if (!el.parentNode) {
272
+ return false;
273
+ }
274
+
275
+ return hasPosition(el.parentNode, type);
276
+ }
277
+
278
+ /**
279
+ * Check if the element is visible
280
+ */
281
+ export function isElementVisible(element: HTMLElement): boolean {
282
+ if (!element) {
283
+ return false;
284
+ }
285
+
286
+ let parentElement: HTMLElement | null = element;
287
+
288
+ while (parentElement) {
289
+ if (parentElement === document.body) {
290
+ break;
291
+ }
292
+
293
+ if (parentElement instanceof HTMLElement) {
294
+ const { display, visibility } = getComputedStyle(parentElement);
295
+
296
+ if (display === 'none' || visibility === 'hidden') {
297
+ return false;
298
+ }
299
+ }
300
+
301
+ parentElement = parentElement.parentElement ?? null;
302
+ }
303
+
304
+ return true;
305
+ }
306
+
307
+ export function needsScrolling(options: NeedsScrollingOptions): boolean {
308
+ const { isFirstStep, scrollToFirstStep, step, target, targetLifecycle } = options;
309
+
310
+ if (
311
+ step.skipScroll ||
312
+ (isFirstStep && !scrollToFirstStep && targetLifecycle !== LIFECYCLE.TOOLTIP) ||
313
+ step.placement === 'center'
314
+ ) {
315
+ return false;
316
+ }
317
+
318
+ const parent = (target?.isConnected ? getScrollParent(target) : scrollDocument()) as Element;
319
+ const isCustomScrollParent = parent ? !parent.isSameNode(scrollDocument()) : false;
320
+
321
+ if ((step.isFixed || hasPosition(target)) && !isCustomScrollParent) {
322
+ return false;
323
+ }
324
+
325
+ return parent.scrollHeight > parent.clientHeight;
326
+ }
327
+
328
+ export function scrollDocument(): Element | HTMLElement {
329
+ return document.scrollingElement ?? document.documentElement;
330
+ }
331
+
332
+ /**
333
+ * Scroll to position
334
+ */
335
+ export function scrollTo(
336
+ value: number,
337
+ options: { duration?: number; element: Element | HTMLElement },
338
+ ): { cancel: () => void; promise: Promise<void> } {
339
+ const { duration, element } = options;
340
+
341
+ let cancel: () => void = () => {};
342
+
343
+ const promise = new Promise<void>(resolve => {
344
+ const { scrollTop } = element;
345
+
346
+ const limit = value > scrollTop ? value - scrollTop : scrollTop - value;
347
+
348
+ cancel = scroll.top(
349
+ element as HTMLElement,
350
+ value,
351
+ { duration: limit < 100 ? 50 : duration },
352
+ () => {
353
+ resolve();
354
+ },
355
+ );
356
+ });
357
+
358
+ return { cancel, promise };
359
+ }
@@ -0,0 +1,230 @@
1
+ import { cloneElement, type FC, isValidElement, type ReactElement, type ReactNode } from 'react';
2
+ import innerText from 'react-innertext';
3
+ import deepmergeFactory from '@fastify/deepmerge';
4
+ import is from 'is-lite';
5
+
6
+ import type { AnyObject, NarrowPlainObject, PlainObject, Simplify } from '~/types';
7
+
8
+ type RemoveType<TObject, TExclude = undefined> = {
9
+ [Key in keyof TObject as TObject[Key] extends TExclude ? never : Key]: TObject[Key];
10
+ };
11
+
12
+ interface GetReactNodeTextOptions {
13
+ defaultValue?: any;
14
+ step?: number;
15
+ steps?: number;
16
+ }
17
+
18
+ /**
19
+ * Remove properties with undefined value from an object
20
+ */
21
+ export function cleanUpObject<T extends PlainObject>(input: T) {
22
+ const output: Record<string, unknown> = {};
23
+
24
+ for (const key in input) {
25
+ if (input[key] !== undefined) {
26
+ output[key] = input[key];
27
+ }
28
+ }
29
+
30
+ return output as RemoveType<T>;
31
+ }
32
+
33
+ export function deepMerge<T extends object>(...objects: object[]): T {
34
+ return deepmergeFactory({
35
+ all: true,
36
+ isMergeableObject: (value): value is object =>
37
+ !(!is.plainObject(value) || isValidElement(value)),
38
+ })(...objects) as T;
39
+ }
40
+
41
+ /**
42
+ * Get Object type
43
+ */
44
+ export function getObjectType(value: unknown): string {
45
+ return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
46
+ }
47
+
48
+ export function getReactNodeText(input: ReactNode, options: GetReactNodeTextOptions = {}): string {
49
+ const { defaultValue, step, steps } = options;
50
+ let text = innerText(input);
51
+
52
+ if (!text) {
53
+ if (
54
+ isValidElement(input) &&
55
+ !Object.values(input.props as Record<string, unknown>).length &&
56
+ getObjectType(input.type) === 'function'
57
+ ) {
58
+ try {
59
+ const component = (input.type as FC)({}) as ReactNode;
60
+
61
+ text = getReactNodeText(component, options);
62
+ } catch {
63
+ text = innerText(defaultValue);
64
+ }
65
+ } else {
66
+ text = innerText(defaultValue);
67
+ }
68
+ } else if ((text.includes('{current}') || text.includes('{total}')) && step && steps) {
69
+ text = text.replace('{current}', step.toString()).replace('{total}', steps.toString());
70
+ }
71
+
72
+ return text;
73
+ }
74
+
75
+ /**
76
+ * Log method calls if debug is enabled
77
+ */
78
+ export function log(debug: boolean, scope: string, title: string, ...data: unknown[]): void {
79
+ if (!debug) {
80
+ return;
81
+ }
82
+
83
+ const now = new Date();
84
+ const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}`;
85
+
86
+ // eslint-disable-next-line no-console
87
+ console.log(
88
+ `${scope} %c${title}%c ${time}`,
89
+ 'font-weight: bold',
90
+ 'color: gray; font-weight: normal',
91
+ ...data,
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Merges the defaultProps with literal values with the incoming props, removing undefined values from it that would override the defaultProps.
97
+ * The result is a type-safe object with the defaultProps as required properties.
98
+ */
99
+ export function mergeProps<TDefaultProps extends PlainObject<any>, TProps extends PlainObject<any>>(
100
+ defaultProps: TDefaultProps,
101
+ props: TProps,
102
+ ) {
103
+ const cleanProps = cleanUpObject(props);
104
+
105
+ return { ...defaultProps, ...cleanProps } as unknown as Simplify<
106
+ TProps & Required<Pick<TProps, keyof TDefaultProps & string>>
107
+ >;
108
+ }
109
+
110
+ /**
111
+ * A function that does nothing.
112
+ */
113
+ export function noop() {
114
+ return undefined;
115
+ }
116
+
117
+ /**
118
+ * Type-safe Object.keys()
119
+ */
120
+ export function objectKeys<T extends AnyObject>(input: T) {
121
+ return Object.keys(input) as Array<keyof T>;
122
+ }
123
+
124
+ /**
125
+ * Remove properties from an object
126
+ */
127
+ export function omit<T extends Record<string, any>, K extends keyof T>(
128
+ input: NarrowPlainObject<T>,
129
+ ...filter: K[]
130
+ ) {
131
+ if (!is.plainObject(input)) {
132
+ throw new TypeError('Expected an object');
133
+ }
134
+
135
+ const output: any = {};
136
+
137
+ for (const key in input) {
138
+ /* istanbul ignore else */
139
+ if ({}.hasOwnProperty.call(input, key) && !filter.includes(key as unknown as K)) {
140
+ output[key] = input[key];
141
+ }
142
+ }
143
+
144
+ return output as Omit<T, K>;
145
+ }
146
+
147
+ /**
148
+ * Select properties from an object
149
+ */
150
+ export function pick<T extends Record<string, any>, K extends keyof T>(
151
+ input: NarrowPlainObject<T>,
152
+ ...filter: K[]
153
+ ) {
154
+ if (!is.plainObject(input)) {
155
+ throw new TypeError('Expected an object');
156
+ }
157
+
158
+ if (!filter.length) {
159
+ return input;
160
+ }
161
+
162
+ const output: any = {};
163
+
164
+ for (const key in input) {
165
+ /* istanbul ignore else */
166
+ if ({}.hasOwnProperty.call(input, key) && filter.includes(key as unknown as K)) {
167
+ output[key] = input[key];
168
+ }
169
+ }
170
+
171
+ return output as Pick<T, K>;
172
+ }
173
+
174
+ export function replaceLocaleContent(input: ReactNode, step: number, steps: number): ReactNode {
175
+ const replacer = (text: string) =>
176
+ text.replace('{current}', String(step)).replace('{total}', String(steps));
177
+
178
+ if (getObjectType(input) === 'string') {
179
+ return replacer(input as string);
180
+ }
181
+
182
+ if (!isValidElement(input)) {
183
+ return input;
184
+ }
185
+
186
+ const { children } = input.props as { children?: ReactNode };
187
+
188
+ if (is.string(children) && children.includes('{current}')) {
189
+ return cloneElement(input as ReactElement<{ children?: ReactNode }>, {
190
+ children: replacer(children),
191
+ });
192
+ }
193
+
194
+ if (Array.isArray(children)) {
195
+ return cloneElement(input as ReactElement<{ children?: ReactNode }>, {
196
+ children: children.map((child: ReactNode) => {
197
+ if (typeof child === 'string') {
198
+ return replacer(child);
199
+ }
200
+
201
+ return replaceLocaleContent(child, step, steps);
202
+ }),
203
+ });
204
+ }
205
+
206
+ if (is.function(input.type) && !Object.values(input.props as Record<string, unknown>).length) {
207
+ try {
208
+ const component = (input.type as FC)({}) as ReactNode;
209
+
210
+ return replaceLocaleContent(component, step, steps);
211
+ } catch {
212
+ return input;
213
+ }
214
+ }
215
+
216
+ return input;
217
+ }
218
+
219
+ /**
220
+ * Sort object keys
221
+ */
222
+ export function sortObjectKeys<T extends PlainObject>(input: T) {
223
+ return objectKeys(input)
224
+ .sort()
225
+ .reduce((acc, key) => {
226
+ acc[key] = input[key];
227
+
228
+ return acc;
229
+ }, {} as T);
230
+ }