Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#267 Add useScroll #278

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export * from './useRerender/useRerender';
export * from './useResizeObserver/useResizeObserver';
export * from './useScreenOrientation/useScreenOrientation';
export * from './useScript/useScript';
export * from './useScroll/useScroll';
export * from './useSessionStorage/useSessionStorage';
export * from './useSet/useSet';
export * from './useShare/useShare';
Expand Down
130 changes: 130 additions & 0 deletions src/hooks/useScroll/useScroll.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { CSSProperties } from 'react';

import { useMemo, useRef, useState } from 'react';

import { useScroll } from './useScroll';

const styles: Record<string, CSSProperties> = {
container: {
width: '300px',
height: '300px',
margin: 'auto',
overflow: 'scroll',
backgroundColor: 'rgba(128, 128, 128, 0.05)',
borderRadius: '8px'
},
inner: {
width: '500px',
height: '400px',
position: 'relative'
},
topLeft: {
position: 'absolute',
left: 0,
top: 0,
backgroundColor: 'rgba(128, 128, 128, 0.05)',
padding: '4px 8px'
},
bottomLeft: {
position: 'absolute',
left: 0,
bottom: 0,
backgroundColor: 'rgba(128, 128, 128, 0.05)',
padding: '4px 8px'
},
topRight: {
position: 'absolute',
right: 0,
top: 0,
backgroundColor: 'rgba(128, 128, 128, 0.05)',
padding: '4px 8px'
},
bottomRight: {
position: 'absolute',
right: 0,
bottom: 0,
backgroundColor: 'rgba(128, 128, 128, 0.05)',
padding: '4px 8px'
},
center: {
position: 'absolute',
left: '33.33%',
top: '33.33%',
backgroundColor: 'rgba(128, 128, 128, 0.05)',
padding: '4px 8px'
},
containerInfo: {
width: 280,
margin: 'auto',
paddingLeft: '1rem',
display: 'flex',
flexDirection: 'column',
gap: 5
}
};

const Demo = () => {
const elementRef = useRef<HTMLDivElement>(null);

const [scrollX, setScrollX] = useState(0);
const [scrollY, setScrollY] = useState(0);
const [behavior, setBehavior] = useState<ScrollBehavior>('auto');

const { x, y, isScrolling, arrivedState, directions } = useScroll(elementRef, {
x: scrollX,
y: scrollY,
behavior
});

const { left, right, top, bottom } = useMemo(() => arrivedState, [arrivedState]);

const {
left: toLeft,
right: toRight,
top: toTop,
bottom: toBottom
} = useMemo(() => directions, [directions]);

return (
<div style={{ display: 'flex' }}>
<div ref={elementRef} style={styles.container}>
<div style={styles.inner}>
<div style={styles.topLeft}>TopLeft</div>
<div style={styles.bottomLeft}>BottomLeft</div>
<div style={styles.topRight}>TopRight</div>
<div style={styles.bottomRight}>BottomRight</div>
<div style={styles.center}>Scroll Me</div>
</div>
</div>
<div style={styles.containerInfo}>
<div>
X Position:
<input value={x} onChange={(event) => setScrollX(+event.target.value)} />
</div>
<div>
Y Position:
<input value={y} onChange={(event) => setScrollY(+event.target.value)} />
</div>
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
Smooth scrolling:{' '}
<input
type='checkbox'
value={behavior}
onChange={(event) => setBehavior(event.target.checked ? 'smooth' : 'auto')}
/>
</div>
<div>isScrolling: {JSON.stringify(isScrolling)}</div>
<div>Top Arrived: {JSON.stringify(top)}</div>
<div>Right Arrived: {JSON.stringify(right)}</div>
<div>Bottom Arrived: {JSON.stringify(bottom)}</div>
<div>Left Arrived: {JSON.stringify(left)}</div>
<div>Scrolling Up: {JSON.stringify(toTop)}</div>
<div>Scrolling Right: {JSON.stringify(toRight)}</div>
<div>Scrolling Down: {JSON.stringify(toBottom)}</div>
<div>Scrolling Left: {JSON.stringify(toLeft)}</div>
</div>
</div>
);
};

export default Demo;
202 changes: 202 additions & 0 deletions src/hooks/useScroll/useScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { type RefObject, useEffect, useRef, useState } from 'react';

import { debounce as DebounceFn, throttle as ThrottleFn } from '@/utils/helpers';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

с маленькой буквы фанки

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

даже можешь заюзать так чтобы не было повторных неймов * as helpers from '@/utils/helpers'

и ниже helper.debounce() и тд


import { useEvent } from '../useEvent/useEvent';
import { useEventListener } from '../useEventListener/useEventListener';

interface UseScrollOptions {
/** Behavior of scrolling */
behavior?: ScrollBehavior;

/** Listener options for scroll event. */
eventListenerOptions?: boolean | AddEventListenerOptions;

/** The check time when scrolling ends. */
idle?: number;

/** Throttle time for scroll event, it’s disabled by default. */
throttle?: number;

/** The initial x position. */
x?: number;

/** The initial y position. */
y?: number;

/** On error callback. */
onError?: (error: unknown) => void;

/** Trigger it when scrolling. */
onScroll?: (e: Event) => void;

/** Trigger it when scrolling ends. */
onStop?: (e: Event) => void;

/** Offset arrived states by x pixels. */
offset?: {
left?: number;
right?: number;
top?: number;
bottom?: number;
};
}

interface useScrollReturn {
/** State of scrolling. */
isScrolling: boolean;

/** The initial x position. */
x: number;

/** The initial y position. */
y: number;

/** State of arrived scroll. */
arrivedState: {
left: boolean;
right: boolean;
top: boolean;
bottom: boolean;
};

/** State of scroll direction. */
directions: {
left: boolean;
right: boolean;
top: boolean;
bottom: boolean;
};
}

/**
* @name useScroll
* @category Sensors
*
* @description Reactive scroll position and state.
*
* @param {RefObject<HTMLElement>} ref - React ref object pointing to a scrollable element.
* @param {UseScrollOptions} [options] - Optional configuration options for the hook.
*
* @returns {useScrollReturn} An object containing the current scroll position, scrolling state, and scroll direction.
*
* @example
* const { x, y, isScrolling, arrivedState, directions } = useScroll(ref);
*/

const ARRIVED_STATE_THRESHOLD_PIXELS = 1;

export const useScroll = (
element: RefObject<HTMLElement>,
options?: UseScrollOptions
): useScrollReturn => {
const {
throttle = 0,
idle = 200,
x = 0,
y = 0,
onStop = () => {},
onScroll = () => {},
offset = {
left: 0,
right: 0,
top: 0,
bottom: 0
},
eventListenerOptions = {
capture: false,
passive: true
},
behavior = 'auto',
onError = (e: unknown) => {
console.error(e);
}
} = options ?? {};

const [scrollX, setScrollX] = useState(x);
const [scrollY, setScrollY] = useState(y);

const [isScrolling, setIsScrolling] = useState(false);
const lastScrollTime = useRef<number>(Date.now());

const [arrivedState, setArrivedState] = useState({
left: true,
right: false,
top: true,
bottom: false
});

const [directions, setDirections] = useState({
left: false,
right: false,
top: false,
bottom: false
});

useEffect(() => {
if (element.current) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

скобки лишние

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

даже бахни так - if(!element.current) return и ниже что внутри скобок

element.current.scrollTo({
left: x,
top: y,
behavior
});
}
}, [x, y, element, behavior]);

const onScrollEnd = DebounceFn((e: Event) => {
const currentTime = Date.now();
if (currentTime - lastScrollTime.current >= idle) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут можно тоже предусмотреть чтобы сразу выйти типо if(currentTime - lastScrollTime.current < idle) return и ниже делать сеты

setIsScrolling(false);
setDirections({ left: false, right: false, top: false, bottom: false });
onStop(e);
}
}, throttle + idle);

const onScrollHandler = useEvent((e: Event) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не e а event - буквы бесплатны

try {
const eventTarget = (
e.target === document ? (e.target as Document).documentElement : e.target
) as HTMLElement;
const scrollLeft = eventTarget.scrollLeft;
let scrollTop = eventTarget.scrollTop;

if (e.target === document && !scrollTop) scrollTop = document.body.scrollTop;

setScrollX(scrollLeft);
setScrollY(scrollTop);
setDirections({
left: scrollLeft < scrollX,
right: scrollLeft > scrollX,
top: scrollTop < scrollY,
bottom: scrollTop > scrollY
});
setArrivedState({
left: scrollLeft <= 0 + (offset.left || 0),
right:
scrollLeft + eventTarget.clientWidth >=
eventTarget.scrollWidth - (offset.right || 0) - ARRIVED_STATE_THRESHOLD_PIXELS,
top: scrollTop <= 0 + (offset.top || 0),
bottom:
scrollTop + eventTarget.clientHeight >=
eventTarget.scrollHeight - (offset.bottom || 0) - ARRIVED_STATE_THRESHOLD_PIXELS
});
setIsScrolling(true);
lastScrollTime.current = Date.now();
onScrollEnd(e);
onScroll(e);
} catch (error) {
onError(error);
}
});

const throttleOnScroll = ThrottleFn(onScrollHandler, throttle);

useEventListener(
element,
'scroll',
throttle ? throttleOnScroll : onScrollHandler,
eventListenerOptions
);

return { x: scrollX, y: scrollY, isScrolling, arrivedState, directions };
};