-
Notifications
You must be signed in to change notification settings - Fork 46
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
base: main
Are you sure you want to change the base?
#267 Add useScroll #278
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; |
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'; | ||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. скобки лишние There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
с маленькой буквы фанки
There was a problem hiding this comment.
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() и тд