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 scrollToTarget = (target, { yOffset = 0, additionalYOffset = 0 } = {}) => {
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
- if (targetElement) {
19
- let verticalOffset = 0;
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
- let y = 0;
33
- const targetTop = targetElement.getBoundingClientRect().top;
34
- if(targetTop > 0) y =
35
- targetElement.getBoundingClientRect().top +
36
- window.pageYOffset +
37
- verticalOffset;
38
- window.scrollTo({ top: y, behavior: 'smooth' });
39
- history.scrollRestoration = 'manual';
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
- handleScrollToClick(options),
144
+ (e) => {
145
+ e.preventDefault();
146
+ handleScrollTo();
147
+ },
148
+ { signal: clickEventController.signal }
71
149
  );
72
150
  });
73
151
 
74
- const { target, yOffsetFromURLParams } = getURLParams(window.location.hash);
75
- scrollToTarget(target, {
76
- ...options,
77
- additionalYOffset: yOffsetFromURLParams,
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homeflowjs",
3
- "version": "1.0.69",
3
+ "version": "1.0.70",
4
4
  "sideEffects": [
5
5
  "modal/**/*",
6
6
  "user/default-profile/**/*",