Skip to content

Commit 32c0af4

Browse files
committed
refactor(useEventListener): refactor for wider use cases.
1 parent 6d4580b commit 32c0af4

21 files changed

+341
-222
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767

6868
# Step 8: Publish to JSR
6969
- name: 🌟 Publish to JSR
70-
run: pnpm dlx jsr publish --allow-dirty
70+
run: pnpm dlx jsr publish --allow-dirty || true
7171

7272
deploy-docs:
7373
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @zl-asica/react
22

3+
## 0.3.7
4+
5+
### Patch Changes
6+
7+
- Refactor useEventListener for wider use cases
8+
39
## 0.3.6
410

511
### Patch Changes

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export default [
77
'unicorn/filename-case': 'off',
88
'import/group-exports': 'off',
99
'unicorn/consistent-function-scoping': 'off',
10+
'unicorn/prefer-global-this': 'off',
11+
'unicorn/no-useless-undefined': 'off',
12+
'unicorn/no-object-as-default-parameter': 'off',
1013
},
1114
},
1215
];

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zl-asica/react",
3-
"version": "0.3.6",
3+
"version": "0.3.7",
44
"license": "MIT",
55
"exports": "./src/index.ts",
66
"importMap": "./import_map.json",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zl-asica/react",
3-
"version": "0.3.6",
3+
"version": "0.3.7",
44
"description": "A library of reusable React hooks, components, and utilities built by ZL Asica.",
55
"keywords": [
66
"react",
Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { renderHook } from '@testing-library/react';
1+
import { renderHook, act } from '@testing-library/react';
22

33
import { useEventListener } from '@/hooks/dom';
44

@@ -11,29 +11,123 @@ describe('useEventListener', () => {
1111
const removeSpy = vi.spyOn(element, 'removeEventListener');
1212

1313
const { unmount } = renderHook(() =>
14-
useEventListener('click', handler, element)
14+
useEventListener('click', handler, { current: element })
1515
);
1616

1717
// Trigger the event to verify the handler is called
1818
const clickEvent = new MouseEvent('click');
1919
element.dispatchEvent(clickEvent);
2020

2121
expect(handler).toHaveBeenCalledWith(clickEvent);
22-
expect(addSpy).toHaveBeenCalledWith('click', expect.any(Function));
22+
expect(addSpy).toHaveBeenCalledWith(
23+
'click',
24+
expect.any(Function),
25+
undefined
26+
);
2327

2428
unmount();
2529

26-
expect(removeSpy).toHaveBeenCalledWith('click', expect.any(Function));
30+
expect(removeSpy).toHaveBeenCalledWith(
31+
'click',
32+
expect.any(Function),
33+
undefined
34+
);
2735
});
2836

2937
it('should handle null element gracefully', () => {
3038
const handler = vi.fn();
3139
const { unmount } = renderHook(() =>
32-
useEventListener('click', handler, null)
40+
useEventListener('click', handler, undefined)
3341
);
3442

3543
// No listener should be attached or removed
3644
expect(handler).not.toHaveBeenCalled();
3745
unmount();
3846
});
47+
48+
it('should handle debounce functionality correctly', () => {
49+
vi.useFakeTimers();
50+
const element = document.createElement('div');
51+
const handler = vi.fn();
52+
53+
const { unmount } = renderHook(() =>
54+
useEventListener('click', handler, { current: element }, undefined, 200)
55+
);
56+
57+
// Trigger the event multiple times
58+
const clickEvent = new MouseEvent('click');
59+
act(() => {
60+
element.dispatchEvent(clickEvent);
61+
element.dispatchEvent(clickEvent);
62+
element.dispatchEvent(clickEvent);
63+
});
64+
65+
// Handler should not be called immediately due to debounce
66+
expect(handler).not.toHaveBeenCalled();
67+
68+
// Fast-forward time to trigger the debounce
69+
act(() => {
70+
vi.advanceTimersByTime(200);
71+
});
72+
73+
expect(handler).toHaveBeenCalledTimes(1);
74+
expect(handler).toHaveBeenCalledWith(clickEvent);
75+
76+
unmount();
77+
vi.useRealTimers();
78+
});
79+
80+
it('should update handler when it changes', () => {
81+
const element = document.createElement('div');
82+
let handler = vi.fn();
83+
84+
const { rerender } = renderHook(() =>
85+
useEventListener('click', handler, { current: element })
86+
);
87+
88+
// Trigger the event with the initial handler
89+
const clickEvent1 = new MouseEvent('click');
90+
element.dispatchEvent(clickEvent1);
91+
expect(handler).toHaveBeenCalledWith(clickEvent1);
92+
93+
// Update the handler
94+
handler = vi.fn();
95+
rerender();
96+
97+
// Trigger the event with the updated handler
98+
const clickEvent2 = new MouseEvent('click');
99+
element.dispatchEvent(clickEvent2);
100+
expect(handler).toHaveBeenCalledWith(clickEvent2);
101+
});
102+
103+
it('should support default window target', () => {
104+
const handler = vi.fn();
105+
106+
const addSpy = vi.spyOn(window, 'addEventListener');
107+
108+
const removeSpy = vi.spyOn(window, 'removeEventListener');
109+
110+
const { unmount } = renderHook(() => useEventListener('resize', handler));
111+
112+
// Trigger the event
113+
const resizeEvent = new Event('resize');
114+
act(() => {
115+
window.dispatchEvent(resizeEvent);
116+
});
117+
118+
expect(handler).toHaveBeenCalledWith(resizeEvent);
119+
expect(addSpy).toHaveBeenCalledWith(
120+
'resize',
121+
expect.any(Function),
122+
undefined
123+
);
124+
125+
unmount();
126+
127+
expect(removeSpy).toHaveBeenCalledWith(
128+
'resize',
129+
expect.any(Function),
130+
undefined
131+
);
132+
});
39133
});

src/__tests__/hooks/dom/useInViewport.test.ts

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -92,40 +92,6 @@ describe('useInViewport', () => {
9292
expect(result.current).toBe(true); // Initial visibility check
9393
});
9494

95-
it('should recheck visibility on scroll', () => {
96-
referenceMock.getBoundingClientRect = vi.fn(
97-
() =>
98-
({
99-
top: 400,
100-
left: 0,
101-
bottom: 500,
102-
right: 100,
103-
width: 100,
104-
height: 100,
105-
}) as DOMRect
106-
);
107-
108-
const reference = { current: referenceMock as HTMLElement };
109-
const { result } = renderHook(() => useInViewport(reference, 0));
110-
111-
act(() => {
112-
referenceMock.getBoundingClientRect = vi.fn(
113-
() =>
114-
({
115-
top: 900,
116-
left: 0,
117-
bottom: 1000,
118-
right: 100,
119-
width: 100,
120-
height: 100,
121-
}) as DOMRect
122-
);
123-
globalThis.dispatchEvent(new Event('scroll'));
124-
});
125-
126-
expect(result.current).toBe(false); // Updated visibility check
127-
});
128-
12995
it('should handle missing ref', () => {
13096
const reference = { current: null };
13197
const { result } = renderHook(() => useInViewport(reference, 0));

src/__tests__/hooks/dom/useIsBottom.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ describe('useIsBottom', () => {
5555
it('should handle target without scrollTop', () => {
5656
const mockElement = document.createElement('div');
5757
Object.defineProperty(mockElement, 'scrollTop', {
58-
// eslint-disable-next-line unicorn/no-useless-undefined
5958
get: () => undefined, // Simulate no scrollTop
6059
configurable: true,
6160
});
@@ -84,7 +83,6 @@ describe('useIsBottom', () => {
8483
configurable: true,
8584
});
8685
Object.defineProperty(mockElement, 'scrollHeight', {
87-
// eslint-disable-next-line unicorn/no-useless-undefined
8886
get: () => undefined, // Simulate no scrollHeight
8987
configurable: true,
9088
});
@@ -113,7 +111,6 @@ describe('useIsBottom', () => {
113111
configurable: true,
114112
});
115113
Object.defineProperty(mockElement, 'clientHeight', {
116-
// eslint-disable-next-line unicorn/no-useless-undefined
117114
get: () => undefined, // Simulate no clientHeight
118115
configurable: true,
119116
});

src/__tests__/hooks/dom/useIsTop.test.ts

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,19 @@ describe('useIsTop', () => {
1919
});
2020

2121
it('should return false when not at the top', () => {
22-
const { result } = renderHook(() => useIsTop(50));
23-
22+
const { result } = renderHook(() => useIsTop());
23+
act(() => {
24+
document.documentElement.scrollTop = 10;
25+
globalThis.dispatchEvent(new Event('scroll'));
26+
});
2427
expect(result.current).toBe(false);
2528
});
2629

2730
it('should return true when scrolled to the top', () => {
2831
const { result } = renderHook(() => useIsTop(50));
2932

3033
act(() => {
31-
document.documentElement.scrollTop = 40; // Within 50px offset
34+
document.documentElement.scrollTop = 40; // Close enough with offset
3235
globalThis.dispatchEvent(new Event('scroll'));
3336
});
3437

@@ -39,75 +42,89 @@ describe('useIsTop', () => {
3942
const { result } = renderHook(() => useIsTop(100));
4043

4144
act(() => {
42-
document.documentElement.scrollTop = 110; // 10px away from top with 100px offset
45+
document.documentElement.scrollTop = 110; // Beyond the offset
4346
globalThis.dispatchEvent(new Event('scroll'));
4447
});
4548

4649
expect(result.current).toBe(false);
4750

4851
act(() => {
49-
document.documentElement.scrollTop = 90; // Exactly at the offset
52+
document.documentElement.scrollTop = 90; // Exactly within offset
5053
globalThis.dispatchEvent(new Event('scroll'));
5154
});
5255

5356
expect(result.current).toBe(true);
5457
});
5558

56-
it('should initialize isTop state on mount', () => {
57-
Object.defineProperty(document.documentElement, 'scrollTop', { value: 0 });
58-
59-
const { result } = renderHook(() => useIsTop());
60-
expect(result.current).toBe(true);
61-
});
62-
63-
it('should handle custom HTMLElement as target', () => {
59+
it('should handle target without scrollTop', () => {
6460
const mockElement = document.createElement('div');
6561
Object.defineProperty(mockElement, 'scrollTop', {
66-
value: 60,
67-
writable: true,
62+
get: () => undefined, // Simulate no scrollTop
63+
configurable: true,
64+
});
65+
Object.defineProperty(mockElement, 'scrollHeight', {
66+
value: 2000,
67+
configurable: true,
68+
});
69+
Object.defineProperty(mockElement, 'clientHeight', {
70+
value: 1000,
71+
configurable: true,
6872
});
6973

7074
const { result } = renderHook(() => useIsTop(50, mockElement));
7175

72-
expect(result.current).toBe(false);
73-
7476
act(() => {
75-
mockElement.scrollTop = 40; // Within 50px offset
7677
mockElement.dispatchEvent(new Event('scroll'));
7778
});
7879

79-
expect(result.current).toBe(true);
80+
expect(result.current).toBe(true); // Defaults to 0 for scrollTop
8081
});
8182

82-
it('should default to globalThis when no element is provided', () => {
83-
const { result } = renderHook(() => useIsTop(50));
83+
it('should handle target without scrollHeight', () => {
84+
const mockElement = document.createElement('div');
85+
Object.defineProperty(mockElement, 'scrollTop', {
86+
value: 0, // Simulate at the top
87+
configurable: true,
88+
});
89+
Object.defineProperty(mockElement, 'scrollHeight', {
90+
get: () => undefined, // Simulate no scrollHeight
91+
configurable: true,
92+
});
93+
Object.defineProperty(mockElement, 'clientHeight', {
94+
value: 1000,
95+
configurable: true,
96+
});
97+
98+
const { result } = renderHook(() => useIsTop(50, mockElement));
8499

85100
act(() => {
86-
document.documentElement.scrollTop = 40; // Within 50px offset
87-
globalThis.dispatchEvent(new Event('scroll'));
101+
mockElement.dispatchEvent(new Event('scroll'));
88102
});
89103

90-
expect(result.current).toBe(true);
104+
expect(result.current).toBe(true); // Defaults to 0 for scrollHeight, so isTop should be true
91105
});
92106

93-
it('should correctly handle HTMLElement with scrollTop', () => {
107+
it('should handle target without clientHeight', () => {
94108
const mockElement = document.createElement('div');
95109
Object.defineProperty(mockElement, 'scrollTop', {
96-
value: 100,
97-
writable: true,
110+
value: 0, // Simulate at the top
111+
configurable: true,
112+
});
113+
Object.defineProperty(mockElement, 'scrollHeight', {
114+
value: 2000,
115+
configurable: true,
116+
});
117+
Object.defineProperty(mockElement, 'clientHeight', {
118+
get: () => undefined, // Simulate no clientHeight
119+
configurable: true,
98120
});
99121

100122
const { result } = renderHook(() => useIsTop(50, mockElement));
101123

102-
// `scrollTop` is greater than `offset`, so it should return false
103-
expect(result.current).toBe(false);
104-
105124
act(() => {
106-
mockElement.scrollTop = 40;
107125
mockElement.dispatchEvent(new Event('scroll'));
108126
});
109127

110-
// `scrollTop` is now within the `offset`, so it should return true
111-
expect(result.current).toBe(true);
128+
expect(result.current).toBe(true); // Defaults to 0 for clientHeight, so isTop should be true
112129
});
113130
});

src/hooks/dom/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// * This file is the entry point for all DOM hooks.
44
//**
55

6+
export { useAdaptiveEffect } from './useAdaptiveEffect';
67
export { useClickOutside } from './useClickOutside';
78
export { useEventListener } from './useEventListener';
89
export { useHover } from './useHover';

0 commit comments

Comments
 (0)