@os-design/use-auto-scroll 1.0.11 → 1.0.13

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 (2) hide show
  1. package/package.json +11 -4
  2. package/src/index.ts +216 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-design/use-auto-scroll",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "license": "UNLICENSED",
5
5
  "repository": "git@gitlab.com:os-team/libs/os-design.git",
6
6
  "main": "dist/cjs/index.js",
@@ -14,7 +14,14 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "src",
19
+ "!**/*.test.ts",
20
+ "!**/*.test.tsx",
21
+ "!**/__tests__",
22
+ "!**/*.stories.tsx",
23
+ "!**/*.stories.mdx",
24
+ "!**/*.example.tsx"
18
25
  ],
19
26
  "sideEffects": false,
20
27
  "scripts": {
@@ -29,10 +36,10 @@
29
36
  "access": "public"
30
37
  },
31
38
  "dependencies": {
32
- "@os-design/use-cursor-position": "^1.0.10"
39
+ "@os-design/use-cursor-position": "^1.0.12"
33
40
  },
34
41
  "peerDependencies": {
35
42
  "react": ">=18"
36
43
  },
37
- "gitHead": "0e88d3afc41e36cee61222a039ef1aa4d08115b5"
44
+ "gitHead": "e5d8409760608145d2c738aa5789d0465ae5416f"
38
45
  }
package/src/index.ts ADDED
@@ -0,0 +1,216 @@
1
+ import useCursorPosition from '@os-design/use-cursor-position';
2
+ import { useCallback, useEffect, useRef } from 'react';
3
+
4
+ const FPS = 60;
5
+ const FRAME_TIMEOUT = 1000 / FPS;
6
+
7
+ export interface UseAutoScrollProps {
8
+ /**
9
+ * Whether the auto scroll is enabled.
10
+ * @default true
11
+ */
12
+ enabled?: boolean;
13
+ /**
14
+ * The distance to the border at which the container starts to scroll automatically (in percent).
15
+ * @default 20
16
+ */
17
+ distPercent?: number;
18
+ /**
19
+ * The max auto scroll speed (in px).
20
+ * @default 100
21
+ */
22
+ maxSpeedPx?: number;
23
+ }
24
+
25
+ const getProps = (props: UseAutoScrollProps): Required<UseAutoScrollProps> => ({
26
+ enabled: props.enabled !== undefined ? props.enabled : true,
27
+ distPercent: 20,
28
+ maxSpeedPx: 100,
29
+ });
30
+
31
+ const getRect = (el: Element) => {
32
+ if (el === document.body) {
33
+ return {
34
+ x: 0,
35
+ y: 0,
36
+ width: window.innerWidth,
37
+ height: window.innerHeight,
38
+ };
39
+ }
40
+ const rect = el.getBoundingClientRect();
41
+ return {
42
+ x: rect.x,
43
+ y: rect.y,
44
+ width: rect.width,
45
+ height: rect.height,
46
+ };
47
+ };
48
+
49
+ /**
50
+ * Detects whether the element is scrollable.
51
+ */
52
+ const isScrollable = (el: Element) => {
53
+ const style = getComputedStyle(el);
54
+ if (
55
+ el !== document.body &&
56
+ !/(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX)
57
+ ) {
58
+ return false;
59
+ }
60
+ const { width, height } = getRect(el);
61
+ return el.scrollWidth > width || el.scrollHeight > height;
62
+ };
63
+
64
+ /**
65
+ * Returns an array of the scrollable elements located under the specified coordinates. The first one is the topmost.
66
+ */
67
+ export const getScrollableElements = (x: number, y: number): Element[] => {
68
+ const elementsFromPoint = document.elementsFromPoint(x, y);
69
+ const elements: Element[] = [];
70
+ // eslint-disable-next-line no-restricted-syntax
71
+ for (const element of elementsFromPoint) {
72
+ const el = element === document.documentElement ? document.body : element;
73
+ if (isScrollable(el)) {
74
+ elements.push(el);
75
+ }
76
+ if (el === document.body) break;
77
+ }
78
+ return elements;
79
+ };
80
+
81
+ const getScrollOffset = (el: Element) => {
82
+ if (el === document.body) {
83
+ return {
84
+ scrollLeft: window.scrollX,
85
+ scrollTop: window.scrollY,
86
+ };
87
+ }
88
+ return {
89
+ scrollLeft: el.scrollLeft,
90
+ scrollTop: el.scrollTop,
91
+ };
92
+ };
93
+
94
+ const useAutoScroll = (props: UseAutoScrollProps = {}) => {
95
+ const cursorPosition = useCursorPosition();
96
+ const cursorPositionRef = useRef(cursorPosition);
97
+ const propsRef = useRef<Required<UseAutoScrollProps>>(getProps(props));
98
+ const isScrollingRef = useRef(false);
99
+ const frameRef = useRef<number>();
100
+ const timeoutRef = useRef<NodeJS.Timeout>();
101
+
102
+ // Update the ref to the cursor position if it changes
103
+ useEffect(() => {
104
+ cursorPositionRef.current = cursorPosition;
105
+ }, [cursorPosition]);
106
+
107
+ // Update the props if it changes
108
+ useEffect(() => {
109
+ propsRef.current = getProps(props);
110
+ }, [props]);
111
+
112
+ // Cancel the animation frame request and clear the timeout if the component was unmounted
113
+ useEffect(
114
+ () => () => {
115
+ if (frameRef.current) window.cancelAnimationFrame(frameRef.current);
116
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
117
+ },
118
+ []
119
+ );
120
+
121
+ // Returns the max distance from the border of the specified element at which auto scrolling is enabled (in px)
122
+ const getMaxDist = useCallback((el: Element, axis: 'x' | 'y') => {
123
+ const { distPercent } = propsRef.current;
124
+ const { width, height } = getRect(el);
125
+ const size = axis === 'x' ? width : height;
126
+ return Math.round((size * distPercent) / 100);
127
+ }, []);
128
+
129
+ // Returns the distance by which the scroll position should be changed
130
+ const getScrollStep = useCallback((dist: number, maxDist: number) => {
131
+ if (dist < 0 || dist > maxDist) return 0;
132
+ const { maxSpeedPx } = propsRef.current;
133
+ const divisor = maxDist / Math.log(maxSpeedPx);
134
+ return Math.round(Math.exp((maxDist - dist) / divisor));
135
+ }, []);
136
+
137
+ // Scrolls the element to the specified position
138
+ const scrollTo = useCallback((element: Element, options: ScrollToOptions) => {
139
+ frameRef.current = window.requestAnimationFrame(() => {
140
+ const el = element === document.body ? window : element;
141
+ el.scrollTo(options);
142
+ });
143
+ }, []);
144
+
145
+ const scroll = useCallback(() => {
146
+ const { enabled } = propsRef.current;
147
+ const { x, y } = cursorPositionRef.current;
148
+ const scrollableElements = getScrollableElements(x, y).reverse();
149
+
150
+ if (!enabled) {
151
+ isScrollingRef.current = false;
152
+ return;
153
+ }
154
+
155
+ isScrollingRef.current = true;
156
+
157
+ // eslint-disable-next-line no-restricted-syntax
158
+ for (const el of scrollableElements) {
159
+ const rect = getRect(el);
160
+ const { scrollLeft, scrollTop } = getScrollOffset(el);
161
+
162
+ const xMaxDist = getMaxDist(el, 'x');
163
+ const yMaxDist = getMaxDist(el, 'y');
164
+
165
+ const leftDist = x - rect.x;
166
+ const topDist = y - rect.y;
167
+ const rightDist = rect.x + rect.width - x;
168
+ const bottomDist = rect.y + rect.height - y;
169
+
170
+ const leftScrollStep = getScrollStep(leftDist, xMaxDist);
171
+ const topScrollStep = getScrollStep(topDist, yMaxDist);
172
+ const rightScrollStep = getScrollStep(rightDist, xMaxDist);
173
+ const bottomScrollStep = getScrollStep(bottomDist, yMaxDist);
174
+
175
+ const canScrollLeft = scrollLeft > 0;
176
+ const canScrollTop = scrollTop > 0;
177
+ const canScrollRight = el.scrollWidth - scrollLeft > rect.width;
178
+ const canScrollBottom = el.scrollHeight - scrollTop > rect.height;
179
+
180
+ let left = scrollLeft;
181
+ if (canScrollLeft && leftScrollStep > 0) {
182
+ left = Math.max(scrollLeft - leftScrollStep, 0);
183
+ } else if (canScrollRight && rightScrollStep > 0) {
184
+ left = Math.min(
185
+ scrollLeft + rightScrollStep,
186
+ el.scrollWidth - rect.width
187
+ );
188
+ }
189
+
190
+ let top = scrollTop;
191
+ if (canScrollTop && topScrollStep > 0) {
192
+ top = Math.max(scrollTop - topScrollStep, 0);
193
+ } else if (canScrollBottom && bottomScrollStep > 0) {
194
+ top = Math.min(
195
+ scrollTop + bottomScrollStep,
196
+ el.scrollHeight - rect.height
197
+ );
198
+ }
199
+
200
+ if (left !== scrollLeft || top !== scrollTop) {
201
+ scrollTo(el, { left, top });
202
+ timeoutRef.current = setTimeout(scroll, FRAME_TIMEOUT);
203
+ return;
204
+ }
205
+ }
206
+
207
+ isScrollingRef.current = false;
208
+ }, [getMaxDist, getScrollStep, scrollTo]);
209
+
210
+ // Start auto scrolling when the cursor position changes only if it is not already running
211
+ useEffect(() => {
212
+ if (!isScrollingRef.current && props.enabled) scroll();
213
+ }, [scroll, cursorPosition, props.enabled]);
214
+ };
215
+
216
+ export default useAutoScroll;