Skip to content

Commit ea78c12

Browse files
committed
main 🧊 add use auto scroll
1 parent 6d6a3db commit ea78c12

File tree

20 files changed

+364
-49
lines changed

20 files changed

+364
-49
lines changed

‎packages/core/src/bundle/hooks/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './useActiveElement/useActiveElement';
22
export * from './useAsync/useAsync';
33
export * from './useAudio/useAudio';
4+
export * from './useAutoScroll/useAutoScroll';
45
export * from './useBattery/useBattery';
56
export * from './useBluetooth/useBluetooth';
67
export * from './useBoolean/useBoolean';
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useEffect } from 'react';
2+
import { getElement } from '@/utils/helpers';
3+
import { useRefState } from '../useRefState/useRefState';
4+
/**
5+
* @name useAutoScroll
6+
* @description - Hook that automatically scrolls a list element to the bottom
7+
* @category Sensors
8+
*
9+
* @overload
10+
* @param {HookTarget} target The target element to auto-scroll
11+
* @param {boolean} [options.enabled] Whether auto-scrolling is enabled
12+
* @returns {void}
13+
*
14+
* @example
15+
* useAutoScroll(ref);
16+
*
17+
* @overload
18+
* @template Target
19+
* @param {boolean} [options.enabled] Whether auto-scrolling is enabled
20+
* @returns {StateRef<Target>} A React ref to attach to the list element
21+
*
22+
* @example
23+
* const ref = useAutoScroll();
24+
*/
25+
export const useAutoScroll = (...params) => {
26+
const target = typeof params[0] !== 'object' || 'current' in params[0] ? params[0] : undefined;
27+
const options = params[1] || (typeof params[0] === 'object' ? params[0] : {});
28+
const { enabled = true } = options;
29+
const internalRef = useRefState();
30+
useEffect(() => {
31+
if (!enabled || (!target && !internalRef.state)) return;
32+
const element = target ? getElement(target) : internalRef.state;
33+
if (!element) return;
34+
let shouldAutoScroll = true;
35+
let touchStartY = 0;
36+
let lastScrollTop = 0;
37+
const onCheckScrollPosition = () => {
38+
const { scrollHeight, clientHeight, scrollTop } = element;
39+
const maxScrollHeight = scrollHeight - clientHeight;
40+
const scrollThreshold = maxScrollHeight / 2;
41+
if (scrollTop < lastScrollTop) shouldAutoScroll = false;
42+
else if (maxScrollHeight - scrollTop <= scrollThreshold) shouldAutoScroll = true;
43+
lastScrollTop = scrollTop;
44+
};
45+
const onWheel = (event) => {
46+
if (event.deltaY < 0) shouldAutoScroll = false;
47+
else onCheckScrollPosition();
48+
};
49+
const onTouchStart = (event) => {
50+
touchStartY = event.touches[0].clientY;
51+
};
52+
const onTouchMove = (event) => {
53+
const touchEndY = event.touches[0].clientY;
54+
const deltaY = touchStartY - touchEndY;
55+
if (deltaY < 0) shouldAutoScroll = false;
56+
else onCheckScrollPosition();
57+
touchStartY = touchEndY;
58+
};
59+
const onMutation = () => {
60+
if (!shouldAutoScroll) return;
61+
element.scrollTo({ top: element.scrollHeight });
62+
};
63+
element.addEventListener('wheel', onWheel);
64+
element.addEventListener('touchstart', onTouchStart);
65+
element.addEventListener('touchmove', onTouchMove);
66+
const observer = new MutationObserver(onMutation);
67+
observer.observe(element, {
68+
childList: true,
69+
subtree: true,
70+
characterData: true
71+
});
72+
return () => {
73+
observer.disconnect();
74+
element.removeEventListener('wheel', onWheel);
75+
element.removeEventListener('touchstart', onTouchStart);
76+
element.removeEventListener('touchmove', onTouchMove);
77+
};
78+
}, [enabled, target, internalRef.state]);
79+
if (target) return;
80+
return internalRef;
81+
};

‎packages/core/src/bundle/hooks/useClickOutside/useClickOutside.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export const useClickOutside = (...params) => {
3131
const internalCallbackRef = useRef(callback);
3232
internalCallbackRef.current = callback;
3333
useEffect(() => {
34-
console.log('target', target);
3534
if (!target && !internalRef.state) return;
3635
const onClick = (event) => {
3736
const element = target ? getElement(target) : internalRef.current;

‎packages/core/src/bundle/hooks/useEvent/useEvent.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import { useCallback, useRef } from 'react';
1313
* const onClick = useEvent(() => console.log('clicked'));
1414
*/
1515
export const useEvent = (callback) => {
16-
const callbackRef = useRef(callback);
17-
callbackRef.current = callback;
16+
const internalCallbackRef = useRef(callback);
17+
internalCallbackRef.current = callback;
1818
return useCallback((...args) => {
19-
const fn = callbackRef.current;
19+
const fn = internalCallbackRef.current;
2020
return fn(...args);
2121
}, []);
2222
};

‎packages/core/src/bundle/hooks/useKeyPress/useKeyPress.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useRefState } from '../useRefState/useRefState';
1313
* @returns {boolean} The pressed state of the key
1414
*
1515
* @example
16-
* const isKeyPressed = useKeyPress('a', window);
16+
* const isKeyPressed = useKeyPress(ref, 'a');
1717
*
1818
* @overload
1919
* @template Target The target element type
@@ -32,8 +32,8 @@ export const useKeyPress = (...params) => {
3232
const internalRef = useRefState(window);
3333
const keyRef = useRef(key);
3434
keyRef.current = key;
35-
const callbackRef = useRef(callback);
36-
callbackRef.current = callback;
35+
const internalCallbackRef = useRef(callback);
36+
internalCallbackRef.current = callback;
3737
useEffect(() => {
3838
if (!target && !internalRef.state) return;
3939
const element = target ? getElement(target) : internalRef.current;
@@ -46,7 +46,7 @@ export const useKeyPress = (...params) => {
4646
: keyboardEvent.key === keyRef.current
4747
) {
4848
setPressed(true);
49-
callbackRef.current?.(true, keyboardEvent);
49+
internalCallbackRef.current?.(true, keyboardEvent);
5050
}
5151
};
5252
const onKeyUp = (event) => {
@@ -57,7 +57,7 @@ export const useKeyPress = (...params) => {
5757
: keyboardEvent.key === keyRef.current
5858
) {
5959
setPressed(false);
60-
callbackRef.current?.(false, keyboardEvent);
60+
internalCallbackRef.current?.(false, keyboardEvent);
6161
}
6262
};
6363
element.addEventListener('keydown', onKeyDown);

‎packages/core/src/bundle/hooks/useKeyboard/useKeyboard.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useRef } from 'react';
22
import { getElement, isTarget } from '@/utils/helpers';
33
import { useRefState } from '../useRefState/useRefState';
44
/**
@@ -7,31 +7,55 @@ import { useRefState } from '../useRefState/useRefState';
77
* @category Sensors
88
*
99
* @overload
10-
* @param {HookTarget | Window} target The target to attach the event listeners to
11-
* @param {UseKeyboardOptions} [options] The keyboard event options
10+
* @param {HookTarget} target The target to attach the event listeners to
11+
* @param {KeyboardEventHandler} callback The callback function to be invoked on key down
12+
* @returns {void}
13+
*
14+
* @example
15+
* useKeyboard(ref, (event) => console.log('key down'));
16+
*
17+
* @overload
18+
* @param {HookTarget} target The target to attach the event listeners to
19+
* @param {UseKeyboardEventOptions} [options] The keyboard event options
1220
* @returns {void}
1321
*
1422
* @example
1523
* useKeyboard(ref, { onKeyDown: (event) => console.log('key down'), onKeyUp: (event) => console.log('key up') });
1624
*
1725
* @overload
1826
* @template Target The target element type
19-
* @param {UseKeyboardOptions} [options] The keyboard event options
27+
* @param {KeyboardEventHandler} callback The callback function to be invoked on key down
28+
* @returns {{ ref: StateRef<Target> }} An object containing the ref
29+
*
30+
* @example
31+
* const ref = useKeyboard((event) => console.log('key down'));
32+
*
33+
* @overload
34+
* @template Target The target element type
35+
* @param {UseKeyboardEventOptions} [options] The keyboard event options
2036
* @returns {{ ref: StateRef<Target> }} An object containing the ref
2137
*
2238
* @example
2339
* const ref = useKeyboard({ onKeyDown: (event) => console.log('key down'), onKeyUp: (event) => console.log('key up') });
2440
*/
2541
export const useKeyboard = (...params) => {
2642
const target = isTarget(params[0]) ? params[0] : undefined;
27-
const options = target ? params[1] : params[0];
43+
const options = target
44+
? typeof params[1] === 'object'
45+
? params[1]
46+
: { onKeyDown: params[1] }
47+
: typeof params[0] === 'object'
48+
? params[0]
49+
: { onKeyDown: params[0] };
2850
const internalRef = useRefState(window);
51+
const internalOptionsRef = useRef(options);
52+
internalOptionsRef.current = options;
2953
useEffect(() => {
3054
if (!target && !internalRef.state) return;
3155
const element = target ? getElement(target) : internalRef.current;
3256
if (!element) return;
33-
const onKeyDown = (event) => options?.onKeyDown?.(event);
34-
const onKeyUp = (event) => options?.onKeyUp?.(event);
57+
const onKeyDown = (event) => internalOptionsRef.current?.onKeyDown?.(event);
58+
const onKeyUp = (event) => internalOptionsRef.current?.onKeyUp?.(event);
3559
element.addEventListener('keydown', onKeyDown);
3660
element.addEventListener('keyup', onKeyUp);
3761
return () => {

‎packages/core/src/bundle/hooks/useLockCallback/useLockCallback.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import { useRef } from 'react';
1212
*/
1313
export const useLockCallback = (callback) => {
1414
const lockRef = useRef(false);
15-
const callbackRef = useRef(callback);
16-
callbackRef.current = callback;
15+
const internalCallbackRef = useRef(callback);
16+
internalCallbackRef.current = callback;
1717
return async (...args) => {
1818
if (lockRef.current) return;
1919
lockRef.current = true;
2020
try {
21-
return await callbackRef.current(...args);
21+
return await internalCallbackRef.current(...args);
2222
} finally {
2323
lockRef.current = false;
2424
}

‎packages/core/src/bundle/hooks/useWindowEvent/useWindowEvent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useEventListener } from '../useEventListener/useEventListener';
33
/**
44
* @name useWindowEvent
55
* @description - Hook attaches an event listener to the window object for the specified event
6-
* @category Browser
6+
* @category Events
77
*
88
* @template Event Key of window event map.
99
* @param {Event} event The event to listen for.

‎packages/core/src/bundle/hooks/useWindowScroll/useWindowScroll.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const scrollTo = ({ x, y, behavior = 'smooth' }) => {
88
/**
99
* @name useWindowScroll
1010
* @description - Hook that manages the window scroll position
11-
* @category Browser
11+
* @category Sensors
1212
*
1313
* @returns {UseWindowScrollReturn} An object containing the current window scroll position
1414
*

‎packages/core/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './useActiveElement/useActiveElement';
22
export * from './useAsync/useAsync';
33
export * from './useAudio/useAudio';
4+
export * from './useAutoScroll/useAutoScroll';
45
export * from './useBattery/useBattery';
56
export * from './useBluetooth/useBluetooth';
67
export * from './useBoolean/useBoolean';

0 commit comments

Comments
 (0)