-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add useScroll hook with tests * Add format rules * chore * docs(changeset): Add use-scroll hook
- Loading branch information
Showing
6 changed files
with
204 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@codiume/hooks": patch | ||
--- | ||
|
||
Add use-scroll hook |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# EditorConfig is awesome: https://editorconfig.org | ||
|
||
# top-most EditorConfig file | ||
root = true | ||
|
||
# Unix-style newlines with a newline ending every file | ||
[*] | ||
end_of_line = lf | ||
insert_final_newline = true | ||
charset = utf-8 | ||
indent_style = space | ||
indent_size = 2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,13 +3,7 @@ | |
"version": "0.0.5", | ||
"description": "A collection of reusable react hooks for state and UI management", | ||
"homepage": "https://github.com/codiume/hooks.git", | ||
"keywords": [ | ||
"hooks", | ||
"library", | ||
"react", | ||
"react-hooks", | ||
"state" | ||
], | ||
"keywords": ["hooks", "library", "react", "react-hooks", "state"], | ||
"author": "MHD <[email protected]>", | ||
"contributors": [ | ||
{ | ||
|
@@ -21,9 +15,7 @@ | |
"main": "./dist/index.cjs", | ||
"module": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"files": [ | ||
"dist" | ||
], | ||
"files": ["dist"], | ||
"exports": { | ||
".": { | ||
"import": { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export { useQueue } from './use-queue/use-queue'; | ||
export { useScroll } from './use-scroll/use-scroll'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import { act, renderHook } from '@testing-library/react'; | ||
import type { RefObject } from 'react'; | ||
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { useScroll } from './use-scroll'; | ||
|
||
interface MockElement extends Partial<HTMLElement> { | ||
scrollLeft: number; | ||
scrollTop: number; | ||
addEventListener: Mock; | ||
removeEventListener: Mock; | ||
scrollTo: Mock; | ||
} | ||
|
||
describe('useScroll', () => { | ||
let ref: RefObject<HTMLElement>; | ||
let mockElement: MockElement; | ||
|
||
beforeEach(() => { | ||
mockElement = { | ||
scrollLeft: 0, | ||
scrollTop: 0, | ||
addEventListener: vi.fn(), | ||
removeEventListener: vi.fn(), | ||
scrollTo: vi.fn() | ||
}; | ||
ref = { current: mockElement as HTMLElement }; | ||
}); | ||
|
||
it('should initialize with scroll position (0, 0)', () => { | ||
const { result } = renderHook(() => useScroll(ref)); | ||
const [scrollPosition] = result.current; | ||
expect(scrollPosition).toEqual({ x: 0, y: 0 }); | ||
}); | ||
|
||
it('should update scroll position on scroll', () => { | ||
const { result } = renderHook(() => useScroll(ref)); | ||
|
||
act(() => { | ||
mockElement.scrollLeft = 100; | ||
mockElement.scrollTop = 200; | ||
/* @ts-ignore */ | ||
mockElement.addEventListener.mock.calls[0][1](); | ||
}); | ||
|
||
const [scrollPosition] = result.current; | ||
expect(scrollPosition).toEqual({ x: 100, y: 200 }); | ||
}); | ||
|
||
it('should add scroll event listeners', () => { | ||
renderHook(() => useScroll(ref)); | ||
expect(mockElement.addEventListener).toHaveBeenCalledWith( | ||
'scroll', | ||
expect.any(Function), | ||
expect.objectContaining({ | ||
passive: true, | ||
signal: expect.any(AbortSignal) | ||
}) | ||
); | ||
}); | ||
|
||
it('should clean up event listeners on unmount', () => { | ||
const { unmount } = renderHook(() => useScroll(ref)); | ||
unmount(); | ||
expect(mockElement.removeEventListener).not.toHaveBeenCalled(); // AbortController is used instead | ||
}); | ||
|
||
it('should provide a scrollTo function', () => { | ||
const { result } = renderHook(() => useScroll(ref)); | ||
const [, scrollTo] = result.current; | ||
|
||
act(() => { | ||
scrollTo({ x: 50, y: 100 }); | ||
}); | ||
|
||
expect(mockElement.scrollTo).toHaveBeenCalledWith({ | ||
left: 50, | ||
top: 100, | ||
behavior: 'smooth' | ||
}); | ||
}); | ||
|
||
it('should allow partial scroll with only x', () => { | ||
const { result } = renderHook(() => useScroll(ref)); | ||
const [, scrollTo] = result.current; | ||
|
||
act(() => { | ||
scrollTo({ x: 50 }); | ||
}); | ||
|
||
expect(mockElement.scrollTo).toHaveBeenCalledWith({ | ||
left: 50, | ||
behavior: 'smooth' | ||
}); | ||
}); | ||
|
||
it('should allow partial scroll with only y', () => { | ||
const { result } = renderHook(() => useScroll(ref)); | ||
const [, scrollTo] = result.current; | ||
|
||
act(() => { | ||
scrollTo({ y: 100 }); | ||
}); | ||
|
||
expect(mockElement.scrollTo).toHaveBeenCalledWith({ | ||
top: 100, | ||
behavior: 'smooth' | ||
}); | ||
}); | ||
|
||
it('should allow custom scroll options', () => { | ||
const { result } = renderHook(() => useScroll(ref)); | ||
const [, scrollTo] = result.current; | ||
|
||
act(() => { | ||
scrollTo({ x: 50, y: 100 }, { behavior: 'auto' }); | ||
}); | ||
|
||
expect(mockElement.scrollTo).toHaveBeenCalledWith({ | ||
left: 50, | ||
top: 100, | ||
behavior: 'auto' | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { type RefObject, useCallback, useEffect, useState } from 'react'; | ||
|
||
type ScrollPosition = { | ||
x: number; | ||
y: number; | ||
}; | ||
|
||
export function useScroll(ref: RefObject<HTMLElement>) { | ||
const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({ | ||
x: 0, | ||
y: 0 | ||
}); | ||
|
||
const scrollTo = useCallback( | ||
( | ||
{ x, y }: Partial<ScrollPosition>, | ||
options: ScrollOptions = { behavior: 'smooth' } | ||
) => { | ||
const el = ref.current; | ||
if (!el) return; | ||
|
||
const scrollOptions: ScrollToOptions = { ...options }; | ||
|
||
if (typeof x === 'number') { | ||
scrollOptions.left = x; | ||
} | ||
|
||
if (typeof y === 'number') { | ||
scrollOptions.top = y; | ||
} | ||
|
||
el.scrollTo(scrollOptions); | ||
}, | ||
[ref] | ||
); | ||
|
||
useEffect(() => { | ||
const el = ref.current; | ||
if (!el) return; | ||
|
||
const abortController = new AbortController(); | ||
|
||
const handler = () => | ||
setScrollPosition({ | ||
x: el.scrollLeft, | ||
y: el.scrollTop | ||
}); | ||
|
||
el.addEventListener('scroll', handler, { | ||
passive: true, | ||
signal: abortController.signal | ||
}); | ||
|
||
return () => { | ||
abortController.abort(); | ||
}; | ||
}, [ref]); | ||
|
||
return [scrollPosition, scrollTo] as const; | ||
} |