@usefy/use-event-listener 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +505 -0
- package/dist/index.d.mts +99 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +54 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/geon0529/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@usefy/use-event-listener</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>A React hook for adding event listeners to DOM elements with automatic cleanup</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@usefy/use-event-listener">
|
|
13
|
+
<img src="https://img.shields.io/npm/v/@usefy/use-event-listener.svg?style=flat-square&color=007acc" alt="npm version" />
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@usefy/use-event-listener">
|
|
16
|
+
<img src="https://img.shields.io/npm/dm/@usefy/use-event-listener.svg?style=flat-square&color=007acc" alt="npm downloads" />
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://bundlephobia.com/package/@usefy/use-event-listener">
|
|
19
|
+
<img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-event-listener?style=flat-square&color=007acc" alt="bundle size" />
|
|
20
|
+
</a>
|
|
21
|
+
<a href="https://github.com/geon0529/usefy/blob/master/LICENSE">
|
|
22
|
+
<img src="https://img.shields.io/npm/l/@usefy/use-event-listener.svg?style=flat-square&color=007acc" alt="license" />
|
|
23
|
+
</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="#installation">Installation</a> •
|
|
28
|
+
<a href="#quick-start">Quick Start</a> •
|
|
29
|
+
<a href="#api-reference">API Reference</a> •
|
|
30
|
+
<a href="#examples">Examples</a> •
|
|
31
|
+
<a href="#license">License</a>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Overview
|
|
37
|
+
|
|
38
|
+
`@usefy/use-event-listener` provides a simple way to add event listeners to DOM elements with automatic cleanup on unmount. It supports window, document, HTMLElement, and RefObject targets with full TypeScript type inference.
|
|
39
|
+
|
|
40
|
+
**Part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem** — a collection of production-ready React hooks designed for modern applications.
|
|
41
|
+
|
|
42
|
+
### Why use-event-listener?
|
|
43
|
+
|
|
44
|
+
- **Zero Dependencies** — Pure React implementation with no external dependencies
|
|
45
|
+
- **TypeScript First** — Full type safety with automatic event type inference
|
|
46
|
+
- **Multiple Targets** — Support for window, document, HTMLElement, and RefObject
|
|
47
|
+
- **Automatic Cleanup** — Event listeners are removed on unmount
|
|
48
|
+
- **Handler Stability** — No re-registration when handler changes
|
|
49
|
+
- **Conditional Activation** — Enable/disable via the `enabled` option
|
|
50
|
+
- **Performance Options** — Support for `passive`, `capture`, and `once` options
|
|
51
|
+
- **SSR Compatible** — Works seamlessly with Next.js, Remix, and other SSR frameworks
|
|
52
|
+
- **Well Tested** — Comprehensive test coverage with Vitest
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# npm
|
|
60
|
+
npm install @usefy/use-event-listener
|
|
61
|
+
|
|
62
|
+
# yarn
|
|
63
|
+
yarn add @usefy/use-event-listener
|
|
64
|
+
|
|
65
|
+
# pnpm
|
|
66
|
+
pnpm add @usefy/use-event-listener
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Peer Dependencies
|
|
70
|
+
|
|
71
|
+
This package requires React 18 or 19:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
87
|
+
|
|
88
|
+
function WindowResizeTracker() {
|
|
89
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
90
|
+
|
|
91
|
+
useEventListener("resize", () => {
|
|
92
|
+
setSize({ width: window.innerWidth, height: window.innerHeight });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
Window size: {size.width} × {size.height}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## API Reference
|
|
106
|
+
|
|
107
|
+
### `useEventListener(eventName, handler, element?, options?)`
|
|
108
|
+
|
|
109
|
+
A hook that adds an event listener to the specified target.
|
|
110
|
+
|
|
111
|
+
#### Parameters
|
|
112
|
+
|
|
113
|
+
| Parameter | Type | Description |
|
|
114
|
+
| ----------- | ------------------------- | ---------------------------------------------------- |
|
|
115
|
+
| `eventName` | `string` | The event type to listen for (e.g., "click", "resize") |
|
|
116
|
+
| `handler` | `(event: Event) => void` | Callback function called when the event fires |
|
|
117
|
+
| `element` | `EventTargetType` | Target element (defaults to window) |
|
|
118
|
+
| `options` | `UseEventListenerOptions` | Configuration options |
|
|
119
|
+
|
|
120
|
+
#### Options
|
|
121
|
+
|
|
122
|
+
| Option | Type | Default | Description |
|
|
123
|
+
| --------- | --------- | ----------- | ------------------------------------------------ |
|
|
124
|
+
| `enabled` | `boolean` | `true` | Whether the event listener is active |
|
|
125
|
+
| `capture` | `boolean` | `false` | Use event capture phase instead of bubble |
|
|
126
|
+
| `passive` | `boolean` | `undefined` | Use passive event listener for performance |
|
|
127
|
+
| `once` | `boolean` | `false` | Handler is invoked once and then removed |
|
|
128
|
+
|
|
129
|
+
#### Supported Target Types
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
type EventTargetType<T extends HTMLElement = HTMLElement> =
|
|
133
|
+
| Window // window object
|
|
134
|
+
| Document // document object
|
|
135
|
+
| HTMLElement // any HTML element
|
|
136
|
+
| React.RefObject<T> // React ref
|
|
137
|
+
| null // no listener
|
|
138
|
+
| undefined; // defaults to window
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Returns
|
|
142
|
+
|
|
143
|
+
`void`
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Examples
|
|
148
|
+
|
|
149
|
+
### Window Events (Default)
|
|
150
|
+
|
|
151
|
+
When no element is provided, events are attached to the window:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
155
|
+
|
|
156
|
+
function ResizeHandler() {
|
|
157
|
+
useEventListener("resize", (e) => {
|
|
158
|
+
console.log("Window resized:", window.innerWidth, window.innerHeight);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return <div>Resize the window</div>;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Document Events
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
169
|
+
|
|
170
|
+
function KeyboardHandler() {
|
|
171
|
+
useEventListener(
|
|
172
|
+
"keydown",
|
|
173
|
+
(e) => {
|
|
174
|
+
if (e.key === "Escape") {
|
|
175
|
+
console.log("Escape pressed");
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
document
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return <div>Press Escape</div>;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### HTMLElement Events
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
189
|
+
|
|
190
|
+
function ElementClickHandler() {
|
|
191
|
+
const button = document.getElementById("myButton");
|
|
192
|
+
|
|
193
|
+
useEventListener(
|
|
194
|
+
"click",
|
|
195
|
+
(e) => {
|
|
196
|
+
console.log("Button clicked");
|
|
197
|
+
},
|
|
198
|
+
button
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return <button id="myButton">Click me</button>;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### RefObject Events
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
209
|
+
import { useRef } from "react";
|
|
210
|
+
|
|
211
|
+
function ScrollableBox() {
|
|
212
|
+
const boxRef = useRef<HTMLDivElement>(null);
|
|
213
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
214
|
+
|
|
215
|
+
useEventListener(
|
|
216
|
+
"scroll",
|
|
217
|
+
() => {
|
|
218
|
+
if (boxRef.current) {
|
|
219
|
+
setScrollTop(boxRef.current.scrollTop);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
boxRef,
|
|
223
|
+
{ passive: true }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div ref={boxRef} style={{ height: 200, overflow: "auto" }}>
|
|
228
|
+
<div style={{ height: 1000 }}>Scroll position: {scrollTop}px</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Conditional Activation
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
238
|
+
|
|
239
|
+
function ConditionalListener() {
|
|
240
|
+
const [isListening, setIsListening] = useState(true);
|
|
241
|
+
|
|
242
|
+
useEventListener(
|
|
243
|
+
"click",
|
|
244
|
+
() => {
|
|
245
|
+
console.log("Clicked!");
|
|
246
|
+
},
|
|
247
|
+
document,
|
|
248
|
+
{ enabled: isListening }
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<button onClick={() => setIsListening(!isListening)}>
|
|
253
|
+
{isListening ? "Disable" : "Enable"} Listener
|
|
254
|
+
</button>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Passive Scroll Listener
|
|
260
|
+
|
|
261
|
+
Use `passive: true` for better scroll performance:
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
265
|
+
|
|
266
|
+
function OptimizedScrollHandler() {
|
|
267
|
+
useEventListener(
|
|
268
|
+
"scroll",
|
|
269
|
+
(e) => {
|
|
270
|
+
// Handle scroll without blocking
|
|
271
|
+
console.log("Scroll position:", window.scrollY);
|
|
272
|
+
},
|
|
273
|
+
window,
|
|
274
|
+
{ passive: true }
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
return <div>Scroll the page</div>;
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### One-time Event Handler
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
285
|
+
|
|
286
|
+
function OneTimeHandler() {
|
|
287
|
+
useEventListener(
|
|
288
|
+
"click",
|
|
289
|
+
() => {
|
|
290
|
+
console.log("This only fires once!");
|
|
291
|
+
},
|
|
292
|
+
document,
|
|
293
|
+
{ once: true }
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return <div>Click anywhere (only works once)</div>;
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Multiple Event Listeners
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
304
|
+
|
|
305
|
+
function MultipleListeners() {
|
|
306
|
+
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
|
307
|
+
const [isPressed, setIsPressed] = useState(false);
|
|
308
|
+
|
|
309
|
+
useEventListener("mousemove", (e) => {
|
|
310
|
+
setMousePos({ x: e.clientX, y: e.clientY });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
useEventListener("mousedown", () => {
|
|
314
|
+
setIsPressed(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
useEventListener("mouseup", () => {
|
|
318
|
+
setIsPressed(false);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div>
|
|
323
|
+
Position: ({mousePos.x}, {mousePos.y})
|
|
324
|
+
<br />
|
|
325
|
+
{isPressed ? "Mouse down" : "Mouse up"}
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Network Status
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
335
|
+
|
|
336
|
+
function NetworkStatus() {
|
|
337
|
+
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
338
|
+
|
|
339
|
+
useEventListener("online", () => setIsOnline(true));
|
|
340
|
+
useEventListener("offline", () => setIsOnline(false));
|
|
341
|
+
|
|
342
|
+
return <div>Network: {isOnline ? "Online" : "Offline"}</div>;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## TypeScript
|
|
349
|
+
|
|
350
|
+
This hook provides full type inference for event types:
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
import { useEventListener } from "@usefy/use-event-listener";
|
|
354
|
+
import type { UseEventListenerOptions, EventTargetType } from "@usefy/use-event-listener";
|
|
355
|
+
|
|
356
|
+
// MouseEvent is automatically inferred
|
|
357
|
+
useEventListener("click", (e) => {
|
|
358
|
+
console.log(e.clientX, e.clientY); // e is MouseEvent
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// KeyboardEvent is automatically inferred
|
|
362
|
+
useEventListener("keydown", (e) => {
|
|
363
|
+
console.log(e.key); // e is KeyboardEvent
|
|
364
|
+
}, document);
|
|
365
|
+
|
|
366
|
+
// FocusEvent is automatically inferred
|
|
367
|
+
useEventListener("focus", (e) => {
|
|
368
|
+
console.log(e.relatedTarget); // e is FocusEvent
|
|
369
|
+
}, inputRef);
|
|
370
|
+
|
|
371
|
+
// Options type
|
|
372
|
+
const options: UseEventListenerOptions = {
|
|
373
|
+
enabled: true,
|
|
374
|
+
capture: false,
|
|
375
|
+
passive: true,
|
|
376
|
+
once: false,
|
|
377
|
+
};
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Testing
|
|
383
|
+
|
|
384
|
+
This package maintains comprehensive test coverage to ensure reliability and stability.
|
|
385
|
+
|
|
386
|
+
### Test Coverage
|
|
387
|
+
|
|
388
|
+
| Category | Coverage |
|
|
389
|
+
| ---------- | -------- |
|
|
390
|
+
| Statements | 96.29% |
|
|
391
|
+
| Branches | 91.66% |
|
|
392
|
+
| Functions | 100% |
|
|
393
|
+
| Lines | 96.29% |
|
|
394
|
+
|
|
395
|
+
### Test Categories
|
|
396
|
+
|
|
397
|
+
<details>
|
|
398
|
+
<summary><strong>Basic Functionality Tests</strong></summary>
|
|
399
|
+
|
|
400
|
+
- Add event listener to window by default
|
|
401
|
+
- Call handler when event fires
|
|
402
|
+
- Add event listener to document
|
|
403
|
+
- Add event listener to HTMLElement
|
|
404
|
+
- Add event listener to RefObject
|
|
405
|
+
- Handle multiple events
|
|
406
|
+
|
|
407
|
+
</details>
|
|
408
|
+
|
|
409
|
+
<details>
|
|
410
|
+
<summary><strong>Enabled Option Tests</strong></summary>
|
|
411
|
+
|
|
412
|
+
- Not add listener when enabled is false
|
|
413
|
+
- Add listener when enabled changes to true
|
|
414
|
+
- Remove listener when enabled changes to false
|
|
415
|
+
- Default enabled to true
|
|
416
|
+
|
|
417
|
+
</details>
|
|
418
|
+
|
|
419
|
+
<details>
|
|
420
|
+
<summary><strong>Capture Option Tests</strong></summary>
|
|
421
|
+
|
|
422
|
+
- Use capture phase when capture is true
|
|
423
|
+
- Use bubble phase when capture is false
|
|
424
|
+
- Re-register listener when capture changes
|
|
425
|
+
|
|
426
|
+
</details>
|
|
427
|
+
|
|
428
|
+
<details>
|
|
429
|
+
<summary><strong>Passive Option Tests</strong></summary>
|
|
430
|
+
|
|
431
|
+
- Pass passive: true to addEventListener
|
|
432
|
+
- Pass passive: false to addEventListener
|
|
433
|
+
- Use browser default when passive is undefined
|
|
434
|
+
|
|
435
|
+
</details>
|
|
436
|
+
|
|
437
|
+
<details>
|
|
438
|
+
<summary><strong>Once Option Tests</strong></summary>
|
|
439
|
+
|
|
440
|
+
- Pass once: true to addEventListener
|
|
441
|
+
- Default once to false
|
|
442
|
+
|
|
443
|
+
</details>
|
|
444
|
+
|
|
445
|
+
<details>
|
|
446
|
+
<summary><strong>Handler Stability Tests</strong></summary>
|
|
447
|
+
|
|
448
|
+
- Not re-register listener when handler changes
|
|
449
|
+
- Call updated handler after change
|
|
450
|
+
|
|
451
|
+
</details>
|
|
452
|
+
|
|
453
|
+
<details>
|
|
454
|
+
<summary><strong>Cleanup Tests</strong></summary>
|
|
455
|
+
|
|
456
|
+
- Remove event listener on unmount
|
|
457
|
+
- Not call handler after unmount
|
|
458
|
+
- Remove listener with correct capture option
|
|
459
|
+
|
|
460
|
+
</details>
|
|
461
|
+
|
|
462
|
+
### Running Tests
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# Run all tests
|
|
466
|
+
pnpm test
|
|
467
|
+
|
|
468
|
+
# Run tests in watch mode
|
|
469
|
+
pnpm test:watch
|
|
470
|
+
|
|
471
|
+
# Run tests with coverage report
|
|
472
|
+
pnpm test --coverage
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Related Packages
|
|
478
|
+
|
|
479
|
+
Explore other hooks in the **@usefy** collection:
|
|
480
|
+
|
|
481
|
+
| Package | Description |
|
|
482
|
+
| ------------------------------------------------------------------------------------------ | ----------------------------------- |
|
|
483
|
+
| [@usefy/use-click-any-where](https://www.npmjs.com/package/@usefy/use-click-any-where) | Document-wide click detection |
|
|
484
|
+
| [@usefy/use-on-click-outside](https://www.npmjs.com/package/@usefy/use-on-click-outside) | Outside click detection |
|
|
485
|
+
| [@usefy/use-toggle](https://www.npmjs.com/package/@usefy/use-toggle) | Boolean state management |
|
|
486
|
+
| [@usefy/use-counter](https://www.npmjs.com/package/@usefy/use-counter) | Counter state management |
|
|
487
|
+
| [@usefy/use-debounce](https://www.npmjs.com/package/@usefy/use-debounce) | Value debouncing |
|
|
488
|
+
| [@usefy/use-throttle](https://www.npmjs.com/package/@usefy/use-throttle) | Value throttling |
|
|
489
|
+
| [@usefy/use-local-storage](https://www.npmjs.com/package/@usefy/use-local-storage) | localStorage state synchronization |
|
|
490
|
+
| [@usefy/use-session-storage](https://www.npmjs.com/package/@usefy/use-session-storage) | sessionStorage state synchronization|
|
|
491
|
+
| [@usefy/use-copy-to-clipboard](https://www.npmjs.com/package/@usefy/use-copy-to-clipboard) | Clipboard operations |
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## License
|
|
496
|
+
|
|
497
|
+
MIT © [mirunamu](https://github.com/geon0529)
|
|
498
|
+
|
|
499
|
+
This package is part of the [usefy](https://github.com/geon0529/usefy) monorepo.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
<p align="center">
|
|
504
|
+
<sub>Built with care by the usefy team</sub>
|
|
505
|
+
</p>
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for useEventListener hook
|
|
3
|
+
*/
|
|
4
|
+
interface UseEventListenerOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the event listener is enabled
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether to use event capture phase
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
capture?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to use passive event listener for performance optimization
|
|
17
|
+
* Useful for scroll/touch events
|
|
18
|
+
* @default undefined (browser default)
|
|
19
|
+
*/
|
|
20
|
+
passive?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the handler should be invoked only once and then removed
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
once?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Supported event target types
|
|
29
|
+
*/
|
|
30
|
+
type EventTargetType<T extends HTMLElement = HTMLElement> = Window | Document | HTMLElement | React.RefObject<T | null> | null | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Adds an event listener to the window (default) or specified element.
|
|
33
|
+
*
|
|
34
|
+
* @param eventName - The event type to listen for
|
|
35
|
+
* @param handler - The callback function called when the event fires
|
|
36
|
+
* @param element - The target element (defaults to window)
|
|
37
|
+
* @param options - Configuration options
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* // Window resize event
|
|
42
|
+
* useEventListener("resize", (e) => {
|
|
43
|
+
* console.log("Window resized:", window.innerWidth);
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
declare function useEventListener<K extends keyof WindowEventMap>(eventName: K, handler: (event: WindowEventMap[K]) => void, element?: Window | null, options?: UseEventListenerOptions): void;
|
|
48
|
+
/**
|
|
49
|
+
* Adds an event listener to the document.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* // Document keydown event
|
|
54
|
+
* useEventListener("keydown", (e) => {
|
|
55
|
+
* console.log("Key pressed:", e.key);
|
|
56
|
+
* }, document);
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
declare function useEventListener<K extends keyof DocumentEventMap>(eventName: K, handler: (event: DocumentEventMap[K]) => void, element: Document, options?: UseEventListenerOptions): void;
|
|
60
|
+
/**
|
|
61
|
+
* Adds an event listener to an HTMLElement.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* // HTMLElement click event
|
|
66
|
+
* const button = document.getElementById("myButton");
|
|
67
|
+
* useEventListener("click", (e) => {
|
|
68
|
+
* console.log("Button clicked");
|
|
69
|
+
* }, button);
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function useEventListener<K extends keyof HTMLElementEventMap>(eventName: K, handler: (event: HTMLElementEventMap[K]) => void, element: HTMLElement | null, options?: UseEventListenerOptions): void;
|
|
73
|
+
/**
|
|
74
|
+
* Adds an event listener to an element referenced by a RefObject.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* // RefObject event
|
|
79
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
80
|
+
* useEventListener("scroll", (e) => {
|
|
81
|
+
* console.log("Element scrolled");
|
|
82
|
+
* }, ref);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function useEventListener<K extends keyof HTMLElementEventMap, T extends HTMLElement>(eventName: K, handler: (event: HTMLElementEventMap[K]) => void, element: React.RefObject<T | null>, options?: UseEventListenerOptions): void;
|
|
86
|
+
/**
|
|
87
|
+
* Adds an event listener for custom events.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* // Custom event
|
|
92
|
+
* useEventListener("myCustomEvent", (e) => {
|
|
93
|
+
* console.log("Custom event fired");
|
|
94
|
+
* }, document);
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
declare function useEventListener(eventName: string, handler: (event: Event) => void, element?: EventTargetType, options?: UseEventListenerOptions): void;
|
|
98
|
+
|
|
99
|
+
export { type EventTargetType, type UseEventListenerOptions, useEventListener };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for useEventListener hook
|
|
3
|
+
*/
|
|
4
|
+
interface UseEventListenerOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the event listener is enabled
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether to use event capture phase
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
capture?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to use passive event listener for performance optimization
|
|
17
|
+
* Useful for scroll/touch events
|
|
18
|
+
* @default undefined (browser default)
|
|
19
|
+
*/
|
|
20
|
+
passive?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the handler should be invoked only once and then removed
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
once?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Supported event target types
|
|
29
|
+
*/
|
|
30
|
+
type EventTargetType<T extends HTMLElement = HTMLElement> = Window | Document | HTMLElement | React.RefObject<T | null> | null | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Adds an event listener to the window (default) or specified element.
|
|
33
|
+
*
|
|
34
|
+
* @param eventName - The event type to listen for
|
|
35
|
+
* @param handler - The callback function called when the event fires
|
|
36
|
+
* @param element - The target element (defaults to window)
|
|
37
|
+
* @param options - Configuration options
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* // Window resize event
|
|
42
|
+
* useEventListener("resize", (e) => {
|
|
43
|
+
* console.log("Window resized:", window.innerWidth);
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
declare function useEventListener<K extends keyof WindowEventMap>(eventName: K, handler: (event: WindowEventMap[K]) => void, element?: Window | null, options?: UseEventListenerOptions): void;
|
|
48
|
+
/**
|
|
49
|
+
* Adds an event listener to the document.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* // Document keydown event
|
|
54
|
+
* useEventListener("keydown", (e) => {
|
|
55
|
+
* console.log("Key pressed:", e.key);
|
|
56
|
+
* }, document);
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
declare function useEventListener<K extends keyof DocumentEventMap>(eventName: K, handler: (event: DocumentEventMap[K]) => void, element: Document, options?: UseEventListenerOptions): void;
|
|
60
|
+
/**
|
|
61
|
+
* Adds an event listener to an HTMLElement.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* // HTMLElement click event
|
|
66
|
+
* const button = document.getElementById("myButton");
|
|
67
|
+
* useEventListener("click", (e) => {
|
|
68
|
+
* console.log("Button clicked");
|
|
69
|
+
* }, button);
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function useEventListener<K extends keyof HTMLElementEventMap>(eventName: K, handler: (event: HTMLElementEventMap[K]) => void, element: HTMLElement | null, options?: UseEventListenerOptions): void;
|
|
73
|
+
/**
|
|
74
|
+
* Adds an event listener to an element referenced by a RefObject.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* // RefObject event
|
|
79
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
80
|
+
* useEventListener("scroll", (e) => {
|
|
81
|
+
* console.log("Element scrolled");
|
|
82
|
+
* }, ref);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function useEventListener<K extends keyof HTMLElementEventMap, T extends HTMLElement>(eventName: K, handler: (event: HTMLElementEventMap[K]) => void, element: React.RefObject<T | null>, options?: UseEventListenerOptions): void;
|
|
86
|
+
/**
|
|
87
|
+
* Adds an event listener for custom events.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* // Custom event
|
|
92
|
+
* useEventListener("myCustomEvent", (e) => {
|
|
93
|
+
* console.log("Custom event fired");
|
|
94
|
+
* }, document);
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
declare function useEventListener(eventName: string, handler: (event: Event) => void, element?: EventTargetType, options?: UseEventListenerOptions): void;
|
|
98
|
+
|
|
99
|
+
export { type EventTargetType, type UseEventListenerOptions, useEventListener };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
useEventListener: () => useEventListener
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/useEventListener.ts
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
function isRefObject(target) {
|
|
30
|
+
return target !== null && target !== void 0 && typeof target === "object" && "current" in target;
|
|
31
|
+
}
|
|
32
|
+
function getTargetElement(target) {
|
|
33
|
+
if (target === void 0) {
|
|
34
|
+
return typeof window !== "undefined" ? window : null;
|
|
35
|
+
}
|
|
36
|
+
if (target === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (isRefObject(target)) {
|
|
40
|
+
return target.current;
|
|
41
|
+
}
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
function useEventListener(eventName, handler, element, options = {}) {
|
|
45
|
+
const { enabled = true, capture = false, passive, once = false } = options;
|
|
46
|
+
const handlerRef = (0, import_react.useRef)(handler);
|
|
47
|
+
handlerRef.current = handler;
|
|
48
|
+
(0, import_react.useEffect)(() => {
|
|
49
|
+
if (typeof window === "undefined") {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!enabled) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const targetElement = getTargetElement(element);
|
|
56
|
+
if (!targetElement) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const internalHandler = (event) => {
|
|
60
|
+
handlerRef.current(event);
|
|
61
|
+
};
|
|
62
|
+
const eventOptions = {
|
|
63
|
+
capture,
|
|
64
|
+
once
|
|
65
|
+
};
|
|
66
|
+
if (passive !== void 0) {
|
|
67
|
+
eventOptions.passive = passive;
|
|
68
|
+
}
|
|
69
|
+
targetElement.addEventListener(eventName, internalHandler, eventOptions);
|
|
70
|
+
return () => {
|
|
71
|
+
targetElement.removeEventListener(eventName, internalHandler, {
|
|
72
|
+
capture
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
}, [eventName, element, enabled, capture, passive, once]);
|
|
76
|
+
}
|
|
77
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
78
|
+
0 && (module.exports = {
|
|
79
|
+
useEventListener
|
|
80
|
+
});
|
|
81
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useEventListener.ts"],"sourcesContent":["export { useEventListener } from \"./useEventListener\";\nexport type {\n UseEventListenerOptions,\n EventTargetType,\n} from \"./useEventListener\";\n","import { useEffect, useRef } from \"react\";\n\n/**\n * Options for useEventListener hook\n */\nexport interface UseEventListenerOptions {\n /**\n * Whether the event listener is enabled\n * @default true\n */\n enabled?: boolean;\n\n /**\n * Whether to use event capture phase\n * @default false\n */\n capture?: boolean;\n\n /**\n * Whether to use passive event listener for performance optimization\n * Useful for scroll/touch events\n * @default undefined (browser default)\n */\n passive?: boolean;\n\n /**\n * Whether the handler should be invoked only once and then removed\n * @default false\n */\n once?: boolean;\n}\n\n/**\n * Supported event target types\n */\nexport type EventTargetType<T extends HTMLElement = HTMLElement> =\n | Window\n | Document\n | HTMLElement\n | React.RefObject<T | null>\n | null\n | undefined;\n\n/**\n * Check if target is a RefObject\n */\nfunction isRefObject<T extends HTMLElement>(\n target: EventTargetType<T>\n): target is React.RefObject<T | null> {\n return (\n target !== null &&\n target !== undefined &&\n typeof target === \"object\" &&\n \"current\" in target\n );\n}\n\n/**\n * Extract actual DOM element from target\n */\nfunction getTargetElement<T extends HTMLElement>(\n target: EventTargetType<T>\n): Window | Document | HTMLElement | null {\n // Default to window if target is undefined (not provided)\n if (target === undefined) {\n return typeof window !== \"undefined\" ? window : null;\n }\n\n // If null is passed, return null (no listener)\n if (target === null) {\n return null;\n }\n\n // Extract element from RefObject\n if (isRefObject(target)) {\n return target.current;\n }\n\n return target;\n}\n\n// Overload 1: Window events (default when element is omitted or Window)\n/**\n * Adds an event listener to the window (default) or specified element.\n *\n * @param eventName - The event type to listen for\n * @param handler - The callback function called when the event fires\n * @param element - The target element (defaults to window)\n * @param options - Configuration options\n *\n * @example\n * ```tsx\n * // Window resize event\n * useEventListener(\"resize\", (e) => {\n * console.log(\"Window resized:\", window.innerWidth);\n * });\n * ```\n */\nexport function useEventListener<K extends keyof WindowEventMap>(\n eventName: K,\n handler: (event: WindowEventMap[K]) => void,\n element?: Window | null,\n options?: UseEventListenerOptions\n): void;\n\n// Overload 2: Document events\n/**\n * Adds an event listener to the document.\n *\n * @example\n * ```tsx\n * // Document keydown event\n * useEventListener(\"keydown\", (e) => {\n * console.log(\"Key pressed:\", e.key);\n * }, document);\n * ```\n */\nexport function useEventListener<K extends keyof DocumentEventMap>(\n eventName: K,\n handler: (event: DocumentEventMap[K]) => void,\n element: Document,\n options?: UseEventListenerOptions\n): void;\n\n// Overload 3: HTMLElement events\n/**\n * Adds an event listener to an HTMLElement.\n *\n * @example\n * ```tsx\n * // HTMLElement click event\n * const button = document.getElementById(\"myButton\");\n * useEventListener(\"click\", (e) => {\n * console.log(\"Button clicked\");\n * }, button);\n * ```\n */\nexport function useEventListener<K extends keyof HTMLElementEventMap>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n element: HTMLElement | null,\n options?: UseEventListenerOptions\n): void;\n\n// Overload 4: RefObject<HTMLElement>\n/**\n * Adds an event listener to an element referenced by a RefObject.\n *\n * @example\n * ```tsx\n * // RefObject event\n * const ref = useRef<HTMLDivElement>(null);\n * useEventListener(\"scroll\", (e) => {\n * console.log(\"Element scrolled\");\n * }, ref);\n * ```\n */\nexport function useEventListener<\n K extends keyof HTMLElementEventMap,\n T extends HTMLElement\n>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n element: React.RefObject<T | null>,\n options?: UseEventListenerOptions\n): void;\n\n// Overload 5: Custom events (fallback for non-standard events)\n/**\n * Adds an event listener for custom events.\n *\n * @example\n * ```tsx\n * // Custom event\n * useEventListener(\"myCustomEvent\", (e) => {\n * console.log(\"Custom event fired\");\n * }, document);\n * ```\n */\nexport function useEventListener(\n eventName: string,\n handler: (event: Event) => void,\n element?: EventTargetType,\n options?: UseEventListenerOptions\n): void;\n\n/**\n * A React hook for adding event listeners to DOM elements with automatic cleanup.\n * Supports window, document, HTMLElement, and RefObject targets.\n *\n * Features:\n * - Type-safe event handling with TypeScript inference\n * - Automatic cleanup on unmount\n * - Handler stability (no re-registration on handler change)\n * - SSR compatible\n *\n * @param eventName - The event type to listen for\n * @param handler - The callback function called when the event fires\n * @param element - The target element (defaults to window)\n * @param options - Configuration options\n *\n * @example\n * ```tsx\n * // Window resize event (default target)\n * useEventListener(\"resize\", (e) => {\n * console.log(\"Window resized:\", window.innerWidth);\n * });\n *\n * // Document keydown event\n * useEventListener(\"keydown\", (e) => {\n * if (e.key === \"Escape\") closeModal();\n * }, document);\n *\n * // Element click event with ref\n * const buttonRef = useRef<HTMLButtonElement>(null);\n * useEventListener(\"click\", handleClick, buttonRef);\n *\n * // With options\n * useEventListener(\"scroll\", handleScroll, window, {\n * passive: true,\n * capture: false,\n * });\n *\n * // Conditional activation\n * useEventListener(\"mousemove\", handleMouseMove, document, {\n * enabled: isTracking,\n * });\n * ```\n */\nexport function useEventListener(\n eventName: string,\n handler: (event: Event) => void,\n element?: EventTargetType,\n options: UseEventListenerOptions = {}\n): void {\n const { enabled = true, capture = false, passive, once = false } = options;\n\n // Store handler in ref to avoid re-registering event listeners\n const handlerRef = useRef(handler);\n\n // Update handler ref on each render to always call the latest handler\n handlerRef.current = handler;\n\n useEffect(() => {\n // SSR check\n if (typeof window === \"undefined\") {\n return;\n }\n\n // Don't add listener if disabled\n if (!enabled) {\n return;\n }\n\n // Get the target element\n const targetElement = getTargetElement(element);\n\n // Don't add listener if element is null\n if (!targetElement) {\n return;\n }\n\n // Internal handler that calls the ref (always latest handler)\n const internalHandler = (event: Event) => {\n handlerRef.current(event);\n };\n\n // Build event listener options\n const eventOptions: AddEventListenerOptions = {\n capture,\n once,\n };\n\n // Only set passive if explicitly provided (respect browser defaults)\n if (passive !== undefined) {\n eventOptions.passive = passive;\n }\n\n // Add event listener\n targetElement.addEventListener(eventName, internalHandler, eventOptions);\n\n // Cleanup\n return () => {\n targetElement.removeEventListener(eventName, internalHandler, {\n capture,\n });\n };\n }, [eventName, element, enabled, capture, passive, once]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAkC;AA8ClC,SAAS,YACP,QACqC;AACrC,SACE,WAAW,QACX,WAAW,UACX,OAAO,WAAW,YAClB,aAAa;AAEjB;AAKA,SAAS,iBACP,QACwC;AAExC,MAAI,WAAW,QAAW;AACxB,WAAO,OAAO,WAAW,cAAc,SAAS;AAAA,EAClD;AAGA,MAAI,WAAW,MAAM;AACnB,WAAO;AAAA,EACT;AAGA,MAAI,YAAY,MAAM,GAAG;AACvB,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO;AACT;AAsJO,SAAS,iBACd,WACA,SACA,SACA,UAAmC,CAAC,GAC9B;AACN,QAAM,EAAE,UAAU,MAAM,UAAU,OAAO,SAAS,OAAO,MAAM,IAAI;AAGnE,QAAM,iBAAa,qBAAO,OAAO;AAGjC,aAAW,UAAU;AAErB,8BAAU,MAAM;AAEd,QAAI,OAAO,WAAW,aAAa;AACjC;AAAA,IACF;AAGA,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAGA,UAAM,gBAAgB,iBAAiB,OAAO;AAG9C,QAAI,CAAC,eAAe;AAClB;AAAA,IACF;AAGA,UAAM,kBAAkB,CAAC,UAAiB;AACxC,iBAAW,QAAQ,KAAK;AAAA,IAC1B;AAGA,UAAM,eAAwC;AAAA,MAC5C;AAAA,MACA;AAAA,IACF;AAGA,QAAI,YAAY,QAAW;AACzB,mBAAa,UAAU;AAAA,IACzB;AAGA,kBAAc,iBAAiB,WAAW,iBAAiB,YAAY;AAGvE,WAAO,MAAM;AACX,oBAAc,oBAAoB,WAAW,iBAAiB;AAAA,QAC5D;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,WAAW,SAAS,SAAS,SAAS,SAAS,IAAI,CAAC;AAC1D;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/useEventListener.ts
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
function isRefObject(target) {
|
|
4
|
+
return target !== null && target !== void 0 && typeof target === "object" && "current" in target;
|
|
5
|
+
}
|
|
6
|
+
function getTargetElement(target) {
|
|
7
|
+
if (target === void 0) {
|
|
8
|
+
return typeof window !== "undefined" ? window : null;
|
|
9
|
+
}
|
|
10
|
+
if (target === null) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (isRefObject(target)) {
|
|
14
|
+
return target.current;
|
|
15
|
+
}
|
|
16
|
+
return target;
|
|
17
|
+
}
|
|
18
|
+
function useEventListener(eventName, handler, element, options = {}) {
|
|
19
|
+
const { enabled = true, capture = false, passive, once = false } = options;
|
|
20
|
+
const handlerRef = useRef(handler);
|
|
21
|
+
handlerRef.current = handler;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (typeof window === "undefined") {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!enabled) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const targetElement = getTargetElement(element);
|
|
30
|
+
if (!targetElement) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const internalHandler = (event) => {
|
|
34
|
+
handlerRef.current(event);
|
|
35
|
+
};
|
|
36
|
+
const eventOptions = {
|
|
37
|
+
capture,
|
|
38
|
+
once
|
|
39
|
+
};
|
|
40
|
+
if (passive !== void 0) {
|
|
41
|
+
eventOptions.passive = passive;
|
|
42
|
+
}
|
|
43
|
+
targetElement.addEventListener(eventName, internalHandler, eventOptions);
|
|
44
|
+
return () => {
|
|
45
|
+
targetElement.removeEventListener(eventName, internalHandler, {
|
|
46
|
+
capture
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
}, [eventName, element, enabled, capture, passive, once]);
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
useEventListener
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useEventListener.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/**\n * Options for useEventListener hook\n */\nexport interface UseEventListenerOptions {\n /**\n * Whether the event listener is enabled\n * @default true\n */\n enabled?: boolean;\n\n /**\n * Whether to use event capture phase\n * @default false\n */\n capture?: boolean;\n\n /**\n * Whether to use passive event listener for performance optimization\n * Useful for scroll/touch events\n * @default undefined (browser default)\n */\n passive?: boolean;\n\n /**\n * Whether the handler should be invoked only once and then removed\n * @default false\n */\n once?: boolean;\n}\n\n/**\n * Supported event target types\n */\nexport type EventTargetType<T extends HTMLElement = HTMLElement> =\n | Window\n | Document\n | HTMLElement\n | React.RefObject<T | null>\n | null\n | undefined;\n\n/**\n * Check if target is a RefObject\n */\nfunction isRefObject<T extends HTMLElement>(\n target: EventTargetType<T>\n): target is React.RefObject<T | null> {\n return (\n target !== null &&\n target !== undefined &&\n typeof target === \"object\" &&\n \"current\" in target\n );\n}\n\n/**\n * Extract actual DOM element from target\n */\nfunction getTargetElement<T extends HTMLElement>(\n target: EventTargetType<T>\n): Window | Document | HTMLElement | null {\n // Default to window if target is undefined (not provided)\n if (target === undefined) {\n return typeof window !== \"undefined\" ? window : null;\n }\n\n // If null is passed, return null (no listener)\n if (target === null) {\n return null;\n }\n\n // Extract element from RefObject\n if (isRefObject(target)) {\n return target.current;\n }\n\n return target;\n}\n\n// Overload 1: Window events (default when element is omitted or Window)\n/**\n * Adds an event listener to the window (default) or specified element.\n *\n * @param eventName - The event type to listen for\n * @param handler - The callback function called when the event fires\n * @param element - The target element (defaults to window)\n * @param options - Configuration options\n *\n * @example\n * ```tsx\n * // Window resize event\n * useEventListener(\"resize\", (e) => {\n * console.log(\"Window resized:\", window.innerWidth);\n * });\n * ```\n */\nexport function useEventListener<K extends keyof WindowEventMap>(\n eventName: K,\n handler: (event: WindowEventMap[K]) => void,\n element?: Window | null,\n options?: UseEventListenerOptions\n): void;\n\n// Overload 2: Document events\n/**\n * Adds an event listener to the document.\n *\n * @example\n * ```tsx\n * // Document keydown event\n * useEventListener(\"keydown\", (e) => {\n * console.log(\"Key pressed:\", e.key);\n * }, document);\n * ```\n */\nexport function useEventListener<K extends keyof DocumentEventMap>(\n eventName: K,\n handler: (event: DocumentEventMap[K]) => void,\n element: Document,\n options?: UseEventListenerOptions\n): void;\n\n// Overload 3: HTMLElement events\n/**\n * Adds an event listener to an HTMLElement.\n *\n * @example\n * ```tsx\n * // HTMLElement click event\n * const button = document.getElementById(\"myButton\");\n * useEventListener(\"click\", (e) => {\n * console.log(\"Button clicked\");\n * }, button);\n * ```\n */\nexport function useEventListener<K extends keyof HTMLElementEventMap>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n element: HTMLElement | null,\n options?: UseEventListenerOptions\n): void;\n\n// Overload 4: RefObject<HTMLElement>\n/**\n * Adds an event listener to an element referenced by a RefObject.\n *\n * @example\n * ```tsx\n * // RefObject event\n * const ref = useRef<HTMLDivElement>(null);\n * useEventListener(\"scroll\", (e) => {\n * console.log(\"Element scrolled\");\n * }, ref);\n * ```\n */\nexport function useEventListener<\n K extends keyof HTMLElementEventMap,\n T extends HTMLElement\n>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n element: React.RefObject<T | null>,\n options?: UseEventListenerOptions\n): void;\n\n// Overload 5: Custom events (fallback for non-standard events)\n/**\n * Adds an event listener for custom events.\n *\n * @example\n * ```tsx\n * // Custom event\n * useEventListener(\"myCustomEvent\", (e) => {\n * console.log(\"Custom event fired\");\n * }, document);\n * ```\n */\nexport function useEventListener(\n eventName: string,\n handler: (event: Event) => void,\n element?: EventTargetType,\n options?: UseEventListenerOptions\n): void;\n\n/**\n * A React hook for adding event listeners to DOM elements with automatic cleanup.\n * Supports window, document, HTMLElement, and RefObject targets.\n *\n * Features:\n * - Type-safe event handling with TypeScript inference\n * - Automatic cleanup on unmount\n * - Handler stability (no re-registration on handler change)\n * - SSR compatible\n *\n * @param eventName - The event type to listen for\n * @param handler - The callback function called when the event fires\n * @param element - The target element (defaults to window)\n * @param options - Configuration options\n *\n * @example\n * ```tsx\n * // Window resize event (default target)\n * useEventListener(\"resize\", (e) => {\n * console.log(\"Window resized:\", window.innerWidth);\n * });\n *\n * // Document keydown event\n * useEventListener(\"keydown\", (e) => {\n * if (e.key === \"Escape\") closeModal();\n * }, document);\n *\n * // Element click event with ref\n * const buttonRef = useRef<HTMLButtonElement>(null);\n * useEventListener(\"click\", handleClick, buttonRef);\n *\n * // With options\n * useEventListener(\"scroll\", handleScroll, window, {\n * passive: true,\n * capture: false,\n * });\n *\n * // Conditional activation\n * useEventListener(\"mousemove\", handleMouseMove, document, {\n * enabled: isTracking,\n * });\n * ```\n */\nexport function useEventListener(\n eventName: string,\n handler: (event: Event) => void,\n element?: EventTargetType,\n options: UseEventListenerOptions = {}\n): void {\n const { enabled = true, capture = false, passive, once = false } = options;\n\n // Store handler in ref to avoid re-registering event listeners\n const handlerRef = useRef(handler);\n\n // Update handler ref on each render to always call the latest handler\n handlerRef.current = handler;\n\n useEffect(() => {\n // SSR check\n if (typeof window === \"undefined\") {\n return;\n }\n\n // Don't add listener if disabled\n if (!enabled) {\n return;\n }\n\n // Get the target element\n const targetElement = getTargetElement(element);\n\n // Don't add listener if element is null\n if (!targetElement) {\n return;\n }\n\n // Internal handler that calls the ref (always latest handler)\n const internalHandler = (event: Event) => {\n handlerRef.current(event);\n };\n\n // Build event listener options\n const eventOptions: AddEventListenerOptions = {\n capture,\n once,\n };\n\n // Only set passive if explicitly provided (respect browser defaults)\n if (passive !== undefined) {\n eventOptions.passive = passive;\n }\n\n // Add event listener\n targetElement.addEventListener(eventName, internalHandler, eventOptions);\n\n // Cleanup\n return () => {\n targetElement.removeEventListener(eventName, internalHandler, {\n capture,\n });\n };\n }, [eventName, element, enabled, capture, passive, once]);\n}\n"],"mappings":";AAAA,SAAS,WAAW,cAAc;AA8ClC,SAAS,YACP,QACqC;AACrC,SACE,WAAW,QACX,WAAW,UACX,OAAO,WAAW,YAClB,aAAa;AAEjB;AAKA,SAAS,iBACP,QACwC;AAExC,MAAI,WAAW,QAAW;AACxB,WAAO,OAAO,WAAW,cAAc,SAAS;AAAA,EAClD;AAGA,MAAI,WAAW,MAAM;AACnB,WAAO;AAAA,EACT;AAGA,MAAI,YAAY,MAAM,GAAG;AACvB,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO;AACT;AAsJO,SAAS,iBACd,WACA,SACA,SACA,UAAmC,CAAC,GAC9B;AACN,QAAM,EAAE,UAAU,MAAM,UAAU,OAAO,SAAS,OAAO,MAAM,IAAI;AAGnE,QAAM,aAAa,OAAO,OAAO;AAGjC,aAAW,UAAU;AAErB,YAAU,MAAM;AAEd,QAAI,OAAO,WAAW,aAAa;AACjC;AAAA,IACF;AAGA,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAGA,UAAM,gBAAgB,iBAAiB,OAAO;AAG9C,QAAI,CAAC,eAAe;AAClB;AAAA,IACF;AAGA,UAAM,kBAAkB,CAAC,UAAiB;AACxC,iBAAW,QAAQ,KAAK;AAAA,IAC1B;AAGA,UAAM,eAAwC;AAAA,MAC5C;AAAA,MACA;AAAA,IACF;AAGA,QAAI,YAAY,QAAW;AACzB,mBAAa,UAAU;AAAA,IACzB;AAGA,kBAAc,iBAAiB,WAAW,iBAAiB,YAAY;AAGvE,WAAO,MAAM;AACX,oBAAc,oBAAoB,WAAW,iBAAiB;AAAA,QAC5D;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,WAAW,SAAS,SAAS,SAAS,SAAS,IAAI,CAAC;AAC1D;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usefy/use-event-listener",
|
|
3
|
+
"version": "0.0.16",
|
|
4
|
+
"description": "A React hook for adding event listeners to DOM elements with automatic cleanup",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
24
|
+
"@testing-library/react": "^16.3.1",
|
|
25
|
+
"@testing-library/user-event": "^14.6.1",
|
|
26
|
+
"@types/react": "^19.0.0",
|
|
27
|
+
"jsdom": "^27.3.0",
|
|
28
|
+
"react": "^19.0.0",
|
|
29
|
+
"rimraf": "^6.0.1",
|
|
30
|
+
"tsup": "^8.0.0",
|
|
31
|
+
"typescript": "^5.0.0",
|
|
32
|
+
"vitest": "^4.0.16"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/geon0529/usefy.git",
|
|
40
|
+
"directory": "packages/use-event-listener"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react",
|
|
45
|
+
"hooks",
|
|
46
|
+
"event-listener",
|
|
47
|
+
"addEventListener",
|
|
48
|
+
"window",
|
|
49
|
+
"document",
|
|
50
|
+
"dom",
|
|
51
|
+
"useEventListener"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"dev": "tsup --watch",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"clean": "rimraf dist"
|
|
60
|
+
}
|
|
61
|
+
}
|