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
44import { 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
1928export 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