homeflowjs 1.0.69 → 1.0.70
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,50 @@
|
|
1
|
+
import { useEffect, useState } from 'react';
|
2
|
+
import { isFunction } from '../utils';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Custom hook to observe the intersection of a target element with the viewport.
|
6
|
+
*
|
7
|
+
* @param {Object} params - Parameters for the hook.
|
8
|
+
* @param {HTMLElement|React.RefObject} params.target - The target element or a React ref to observe.
|
9
|
+
* @param {IntersectionObserverInit} [params.options={}] - Options for the IntersectionObserver (e.g., root, rootMargin, threshold).
|
10
|
+
* @param {Function} [params.onIntersect] - Callback function to execute when the target element intersects with the viewport.
|
11
|
+
*
|
12
|
+
* @returns {boolean} - A boolean indicating whether the target element is currently intersecting with the viewport.
|
13
|
+
*/
|
14
|
+
const useIntersectionObserver = ({ target, options = {}, onIntersect }) => {
|
15
|
+
const [isIntersecting, setIsIntersecting] = useState(false);
|
16
|
+
|
17
|
+
useEffect(() => {
|
18
|
+
if (!target) return;
|
19
|
+
|
20
|
+
let targetElement = target;
|
21
|
+
// If `target` is a ref, get the current DOM element
|
22
|
+
if (target?.current) {
|
23
|
+
targetElement = target.current;
|
24
|
+
}
|
25
|
+
|
26
|
+
const observer = new IntersectionObserver(
|
27
|
+
([entry]) => {
|
28
|
+
const intersecting = entry.isIntersecting;
|
29
|
+
setIsIntersecting(intersecting);
|
30
|
+
|
31
|
+
// Call the callback if provided and isIntersecting is true
|
32
|
+
if (intersecting && isFunction(onIntersect)) {
|
33
|
+
onIntersect();
|
34
|
+
}
|
35
|
+
},
|
36
|
+
options
|
37
|
+
);
|
38
|
+
|
39
|
+
observer.observe(targetElement);
|
40
|
+
|
41
|
+
// Cleanup observer on component unmount
|
42
|
+
return () => {
|
43
|
+
observer.disconnect();
|
44
|
+
};
|
45
|
+
}, [target, options, onIntersect]);
|
46
|
+
|
47
|
+
return isIntersecting;
|
48
|
+
};
|
49
|
+
|
50
|
+
export default useIntersectionObserver;
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import { useEffect } from 'react';
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
2
2
|
import { isFunction } from '../utils';
|
3
|
+
import useIntersectionObserver from './use-intersection-observer';;
|
3
4
|
|
4
|
-
const
|
5
|
-
|
5
|
+
const getTargetElementHelper = (target) => {
|
6
6
|
// If target is chunk ID
|
7
7
|
let targetElement;
|
8
8
|
if (!isNaN(target)) {
|
@@ -15,29 +15,38 @@ const scrollToTarget = (target, { yOffset = 0, additionalYOffset = 0 } = {}) =>
|
|
15
15
|
if (!targetElement) targetElement = document.querySelector(`.${target}`);
|
16
16
|
if (!targetElement) targetElement = document.querySelector(`#${target}`);
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
if (typeof yOffset === 'number') {
|
21
|
-
verticalOffset = yOffset;
|
22
|
-
} else if (isFunction(yOffset)) {
|
23
|
-
const value = yOffset();
|
24
|
-
if (typeof value === 'number') {
|
25
|
-
verticalOffset = value;
|
26
|
-
}
|
27
|
-
}
|
28
|
-
if(additionalYOffset) {
|
29
|
-
verticalOffset = verticalOffset + parseInt(additionalYOffset, 10);
|
30
|
-
}
|
18
|
+
return targetElement;
|
19
|
+
}
|
31
20
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
21
|
+
const scrollToTarget = ({ targetElement, yOffset = 0, additionalYOffset = 0 } = {}, signal) => {
|
22
|
+
if(!targetElement) return;
|
23
|
+
|
24
|
+
let verticalOffset = 0;
|
25
|
+
if (typeof yOffset === 'number') {
|
26
|
+
verticalOffset = yOffset;
|
27
|
+
} else if (isFunction(yOffset)) {
|
28
|
+
const value = yOffset();
|
29
|
+
if (typeof value === 'number') {
|
30
|
+
verticalOffset = value;
|
31
|
+
}
|
40
32
|
}
|
33
|
+
if(additionalYOffset) {
|
34
|
+
verticalOffset = verticalOffset + parseInt(additionalYOffset, 10);
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Scroll to the target element
|
39
|
+
* Note: when using getBoundingClientRect(), the position of the div will
|
40
|
+
* change whenever you scroll on the page. To get the absolute position of
|
41
|
+
* the element on the page, not relative to the window,we need to add the
|
42
|
+
* number of scrolled pixels to the element’s window location using scrollY:
|
43
|
+
*/
|
44
|
+
const rect = targetElement.getBoundingClientRect();
|
45
|
+
window.scroll({
|
46
|
+
top: rect.top + window.scrollY + verticalOffset,
|
47
|
+
behavior: 'smooth',
|
48
|
+
...(signal && { signal: signal }), // This is used to abort the scroll via a trigger in the useIntersectionObserver
|
49
|
+
});
|
41
50
|
};
|
42
51
|
|
43
52
|
const getURLParams = (hash) => {
|
@@ -46,37 +55,111 @@ const getURLParams = (hash) => {
|
|
46
55
|
return { target, yOffsetFromURLParams: yOffset };
|
47
56
|
};
|
48
57
|
|
49
|
-
function handleScrollToClick(options) {
|
50
|
-
// eslint-disable-next-line func-names
|
51
|
-
return function () {
|
52
|
-
const { target, yOffsetFromURLParams } = getURLParams(this.hash);
|
53
|
-
if (target) {
|
54
|
-
scrollToTarget(target, {
|
55
|
-
...options,
|
56
|
-
additionalYOffset: yOffsetFromURLParams,
|
57
|
-
});
|
58
|
-
}
|
59
|
-
};
|
60
|
-
}
|
61
58
|
|
59
|
+
/**
|
60
|
+
* Custom hook that provides functionality to scroll to a specific target element
|
61
|
+
* on the page, either based on URL hash parameters or click events to links with
|
62
|
+
* href="#scroll-to?target=". It also handles layout shifts caused by lazy-loaded components
|
63
|
+
* using an intersection observer to ensure accurate scrolling.
|
64
|
+
*
|
65
|
+
* Features:
|
66
|
+
* - Scrolls to a target element specified by a URL hash or click event.
|
67
|
+
* - Supports additional vertical offsets (static or dynamic).
|
68
|
+
* - Uses an AbortController to manage and stop scrolling when necessary.
|
69
|
+
* - Resolves layout shift issues by re-calculating the target position using an intersection observer.
|
70
|
+
* - Automatically attaches click event listeners to links with href="#scroll-to?target=",
|
71
|
+
* target can either be a element id, class name or chunk id.
|
72
|
+
* - Handles URL navigation with scroll-to hash parameters.
|
73
|
+
*
|
74
|
+
* @param {Object} options - Configuration options for scrolling behavior.
|
75
|
+
* @param {number|function} [options.yOffset] - Vertical offset for scrolling. Can be a number or a function.
|
76
|
+
* @param {number} [options.additionalYOffset] - Additional vertical offset for scrolling.
|
77
|
+
*/
|
62
78
|
const useScrollTo = (options) => {
|
79
|
+
const [currentTarget, setCurrentTarget] = useState(null);
|
80
|
+
const scrollAbortControllerRef = useRef(null);
|
81
|
+
|
82
|
+
const handleScrollTo = () => {
|
83
|
+
const { target, yOffsetFromURLParams } = getURLParams(window.location.hash);
|
84
|
+
const targetElement = getTargetElementHelper(target);
|
85
|
+
|
86
|
+
if(!targetElement) return;
|
87
|
+
|
88
|
+
const targetObj = {
|
89
|
+
targetElement,
|
90
|
+
additionalYOffset: yOffsetFromURLParams,
|
91
|
+
...options,
|
92
|
+
}
|
93
|
+
|
94
|
+
// Update state so that the intersection observer can pick it up
|
95
|
+
setCurrentTarget(targetObj);
|
96
|
+
|
97
|
+
// AbortController to manage the scroll
|
98
|
+
const controller = new AbortController();
|
99
|
+
scrollAbortControllerRef.current = controller;
|
100
|
+
|
101
|
+
// Scroll to the target element;
|
102
|
+
scrollToTarget({ ...targetObj }, controller.signal);
|
103
|
+
}
|
104
|
+
|
105
|
+
/**
|
106
|
+
* How this useIntersectionObserver is being used:
|
107
|
+
* This fixes a bug with the scrollToTarget function
|
108
|
+
* where the getBoundingClientRect() method returns
|
109
|
+
* a different value between scroll to's. This is caused
|
110
|
+
* because of layout shift with lazy loaded components
|
111
|
+
* while scrolling.
|
112
|
+
*
|
113
|
+
* How it works:
|
114
|
+
* When the scrollToTarget is fired, the useIntersectionObserver
|
115
|
+
* will check if the target element is in the viewport. If it is
|
116
|
+
* it stops the window.scroll method in the scrollToTarget function
|
117
|
+
* stopping the scroll. This is done by using the AbortController.
|
118
|
+
* Then it scrollToTarget again, this in turn gets the most up to date
|
119
|
+
* getBoundingClientRect() value and scrolls to the target element.
|
120
|
+
*
|
121
|
+
*/
|
122
|
+
useIntersectionObserver({
|
123
|
+
target: currentTarget?.targetElement,
|
124
|
+
onIntersect: () => {
|
125
|
+
if (scrollAbortControllerRef.current) {
|
126
|
+
scrollAbortControllerRef.current.abort(); // Stop the scroll
|
127
|
+
scrollAbortControllerRef.current = null; // Reset the controller
|
128
|
+
scrollToTarget({ ...currentTarget }); // Fire the scrollToTarget function again
|
129
|
+
}
|
130
|
+
}
|
131
|
+
});
|
132
|
+
|
133
|
+
|
134
|
+
// Handle onclick scroll to events
|
63
135
|
useEffect(() => {
|
136
|
+
const clickEventController = new AbortController()
|
64
137
|
const scrollToEls = document.querySelectorAll(
|
65
138
|
'[href*="#scroll-to"], [href*="#/scroll-to"]'
|
66
139
|
);
|
140
|
+
|
67
141
|
scrollToEls.forEach((el) => {
|
68
142
|
el.addEventListener(
|
69
143
|
'click',
|
70
|
-
|
144
|
+
(e) => {
|
145
|
+
e.preventDefault();
|
146
|
+
handleScrollTo();
|
147
|
+
},
|
148
|
+
{ signal: clickEventController.signal }
|
71
149
|
);
|
72
150
|
});
|
73
151
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
});
|
152
|
+
//On click element event listeners cleanup
|
153
|
+
return () => {
|
154
|
+
clickEventController.abort()
|
155
|
+
}
|
79
156
|
}, []);
|
157
|
+
|
158
|
+
// Handle when a user navigates to a URL with a scroll to hash
|
159
|
+
useEffect(() => {
|
160
|
+
handleScrollTo();
|
161
|
+
}, [])
|
162
|
+
|
80
163
|
};
|
81
164
|
|
82
165
|
export default useScrollTo;
|