Skip to content

Commit a26d3af

Browse files
committed
feat: Add support for target elements in usePosition hook
1 parent 636eccb commit a26d3af

File tree

1 file changed

+57
-18
lines changed

1 file changed

+57
-18
lines changed

src/hooks/usePosition.ts

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { type DependencyList, useLayoutEffect, useRef } from 'react'
2-
import { Point, Rect } from 'spase'
1+
import { type DependencyList, type RefObject, useLayoutEffect, useRef } from 'react'
2+
import { Point } from 'spase'
33

44
import { useLatest } from './useLatest.js'
55

@@ -10,23 +10,41 @@ export type ScrollPositionInfo = {
1010
step: Point
1111
}
1212

13+
type Target = HTMLElement | null | RefObject<HTMLElement> | RefObject<HTMLElement | null> | RefObject<HTMLElement | undefined> | undefined
14+
1315
/**
14-
* Hook for tracking the scroll position of the viewport.
16+
* Hook for tracking the scroll position of a target element or the viewport.
1517
*
18+
* @param target Optional target element or ref to track. If not provided, the
19+
* viewport is tracked instead.
1620
* @param onChange Handler invoked when the scroll position changes.
1721
* @param deps Optional dependency list to control when the hook should re-run.
1822
*/
23+
export function usePosition(
24+
target: Target,
25+
onChange: (newInfo: ScrollPositionInfo, oldInfo: ScrollPositionInfo | undefined) => void,
26+
deps?: DependencyList,
27+
): void
1928
export function usePosition(
2029
onChange: (newInfo: ScrollPositionInfo, oldInfo: ScrollPositionInfo | undefined) => void,
21-
deps: DependencyList = [],
22-
) {
30+
deps?: DependencyList,
31+
): void
32+
export function usePosition(...args:
33+
| [(newInfo: ScrollPositionInfo, oldInfo: ScrollPositionInfo | undefined) => void, DependencyList?]
34+
| [Target, (newInfo: ScrollPositionInfo, oldInfo: ScrollPositionInfo | undefined) => void, DependencyList?]) {
35+
const target = typeof args[0] === 'function' ? undefined : args[0]
36+
const onChange = typeof args[0] === 'function' ? args[0] : args[1] as (newInfo: ScrollPositionInfo, oldInfo: ScrollPositionInfo | undefined) => void
37+
const deps = typeof args[0] === 'function' ? (args[1] as DependencyList | undefined) ?? [] : (args[2] as DependencyList | undefined) ?? []
38+
2339
const changeHandlerRef = useLatest(onChange)
2440
const prevInfoRef = useRef<ScrollPositionInfo>(undefined)
2541
const isTickingRef = useRef(false)
2642

2743
useLayoutEffect(() => {
44+
const el = _resolveTarget(target)
45+
2846
const handler = () => {
29-
const newInfo = _getScrollPositionInfo()
47+
const newInfo = el ? _getElementScrollPositionInfo(el) : _getViewportScrollPositionInfo()
3048
if (!newInfo) return
3149

3250
changeHandlerRef.current(newInfo, prevInfoRef.current)
@@ -45,32 +63,53 @@ export function usePosition(
4563
})
4664
}
4765

48-
window.addEventListener('scroll', tick, { passive: true })
66+
const scrollTarget = el ?? window
67+
68+
scrollTarget.addEventListener('scroll', tick, { passive: true })
4969
window.addEventListener('resize', tick)
5070
window.addEventListener('orientationchange', tick)
5171

5272
tick()
5373

5474
return () => {
55-
window.removeEventListener('scroll', tick)
75+
scrollTarget.removeEventListener('scroll', tick)
5676
window.removeEventListener('resize', tick)
5777
window.removeEventListener('orientationchange', tick)
5878
}
5979
}, [...deps])
6080
}
6181

62-
function _getScrollPositionInfo(): ScrollPositionInfo | undefined {
63-
const refRect = Rect.fromViewport()
64-
const refRectMin = Rect.clone(refRect, { x: 0, y: 0 })
65-
const refRectFull = Rect.from(window, { overflow: true })
82+
function _resolveTarget(target: Target): HTMLElement | undefined {
83+
if (target == null) return undefined
84+
if (target instanceof HTMLElement) return target
85+
86+
return target.current ?? undefined
87+
}
88+
89+
function _getViewportScrollPositionInfo(): ScrollPositionInfo | undefined {
90+
const scrollLeft = window.scrollX
91+
const scrollTop = window.scrollY
92+
const maxScrollLeft = document.documentElement.scrollWidth - window.innerWidth
93+
const maxScrollTop = document.documentElement.scrollHeight - window.innerHeight
94+
95+
return {
96+
maxPos: Point.make(maxScrollLeft, maxScrollTop),
97+
minPos: Point.make(0, 0),
98+
pos: Point.make(scrollLeft, scrollTop),
99+
step: Point.make(maxScrollLeft > 0 ? scrollLeft / maxScrollLeft : 0, maxScrollTop > 0 ? scrollTop / maxScrollTop : 0),
100+
}
101+
}
66102

67-
const refRectMax = Rect.clone(refRectMin, { x: refRectFull.width - refRect.width, y: refRectFull.height - refRect.height })
68-
const step = Point.make(refRect.left / refRectMax.left, refRect.top / refRectMax.top)
103+
function _getElementScrollPositionInfo(element: HTMLElement): ScrollPositionInfo | undefined {
104+
const scrollLeft = element.scrollLeft
105+
const scrollTop = element.scrollTop
106+
const maxScrollLeft = element.scrollWidth - element.clientWidth
107+
const maxScrollTop = element.scrollHeight - element.clientHeight
69108

70109
return {
71-
maxPos: Point.make(refRectMax.left, refRectMax.top),
72-
minPos: Point.make(refRectMin.left, refRectMin.top),
73-
pos: Point.make(refRect.left, refRect.top),
74-
step,
110+
maxPos: Point.make(maxScrollLeft, maxScrollTop),
111+
minPos: Point.make(0, 0),
112+
pos: Point.make(scrollLeft, scrollTop),
113+
step: Point.make(maxScrollLeft > 0 ? scrollLeft / maxScrollLeft : 0, maxScrollTop > 0 ? scrollTop / maxScrollTop : 0),
75114
}
76115
}

0 commit comments

Comments
 (0)