@usefy/use-click-any-where 0.0.8 → 0.0.11
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 +463 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
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-click-any-where</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>A lightweight React hook for detecting document-wide click events</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@usefy/use-click-any-where">
|
|
13
|
+
<img src="https://img.shields.io/npm/v/@usefy/use-click-any-where.svg?style=flat-square&color=007acc" alt="npm version" />
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@usefy/use-click-any-where">
|
|
16
|
+
<img src="https://img.shields.io/npm/dm/@usefy/use-click-any-where.svg?style=flat-square&color=007acc" alt="npm downloads" />
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://bundlephobia.com/package/@usefy/use-click-any-where">
|
|
19
|
+
<img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-click-any-where?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-click-any-where.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-click-any-where` detects clicks anywhere on the document and calls your handler. Perfect for closing dropdowns, modals, context menus, or any component that should respond to outside clicks.
|
|
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-click-any-where?
|
|
43
|
+
|
|
44
|
+
- **Zero Dependencies** — Pure React implementation with no external dependencies
|
|
45
|
+
- **TypeScript First** — Full type safety with exported interfaces
|
|
46
|
+
- **Conditional Activation** — Enable/disable via the `enabled` option
|
|
47
|
+
- **Event Capture Support** — Choose between capture and bubble phase
|
|
48
|
+
- **Passive Listeners** — Performance-optimized with passive listeners by default
|
|
49
|
+
- **Handler Stability** — No re-registration when handler changes
|
|
50
|
+
- **SSR Compatible** — Works seamlessly with Next.js, Remix, and other SSR frameworks
|
|
51
|
+
- **Lightweight** — Minimal bundle footprint (~200B minified + gzipped)
|
|
52
|
+
- **Well Tested** — Comprehensive test coverage with Vitest
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# npm
|
|
60
|
+
npm install @usefy/use-click-any-where
|
|
61
|
+
|
|
62
|
+
# yarn
|
|
63
|
+
yarn add @usefy/use-click-any-where
|
|
64
|
+
|
|
65
|
+
# pnpm
|
|
66
|
+
pnpm add @usefy/use-click-any-where
|
|
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 { useClickAnyWhere } from "@usefy/use-click-any-where";
|
|
87
|
+
|
|
88
|
+
function ClickTracker() {
|
|
89
|
+
const [lastClick, setLastClick] = useState({ x: 0, y: 0 });
|
|
90
|
+
|
|
91
|
+
useClickAnyWhere((event) => {
|
|
92
|
+
setLastClick({ x: event.clientX, y: event.clientY });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
Last click: ({lastClick.x}, {lastClick.y})
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## API Reference
|
|
106
|
+
|
|
107
|
+
### `useClickAnyWhere(handler, options?)`
|
|
108
|
+
|
|
109
|
+
A hook that listens for document-wide click events.
|
|
110
|
+
|
|
111
|
+
#### Parameters
|
|
112
|
+
|
|
113
|
+
| Parameter | Type | Description |
|
|
114
|
+
| --------- | ----------------------------- | ------------------------------------------------ |
|
|
115
|
+
| `handler` | `(event: MouseEvent) => void` | Callback function called on every document click |
|
|
116
|
+
| `options` | `UseClickAnyWhereOptions` | Configuration options |
|
|
117
|
+
|
|
118
|
+
#### Options
|
|
119
|
+
|
|
120
|
+
| Option | Type | Default | Description |
|
|
121
|
+
| --------- | --------- | ------- | ------------------------------------------ |
|
|
122
|
+
| `enabled` | `boolean` | `true` | Whether the event listener is active |
|
|
123
|
+
| `capture` | `boolean` | `false` | Use event capture phase instead of bubble |
|
|
124
|
+
| `passive` | `boolean` | `true` | Use passive event listener for performance |
|
|
125
|
+
|
|
126
|
+
#### Returns
|
|
127
|
+
|
|
128
|
+
`void`
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Examples
|
|
133
|
+
|
|
134
|
+
### Close Dropdown on Outside Click
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { useClickAnyWhere } from "@usefy/use-click-any-where";
|
|
138
|
+
import { useRef, useState } from "react";
|
|
139
|
+
|
|
140
|
+
function Dropdown() {
|
|
141
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
142
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
143
|
+
|
|
144
|
+
useClickAnyWhere(
|
|
145
|
+
(event) => {
|
|
146
|
+
// Close if clicked outside the dropdown
|
|
147
|
+
if (
|
|
148
|
+
dropdownRef.current &&
|
|
149
|
+
!dropdownRef.current.contains(event.target as Node)
|
|
150
|
+
) {
|
|
151
|
+
setIsOpen(false);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{ enabled: isOpen }
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div ref={dropdownRef}>
|
|
159
|
+
<button onClick={() => setIsOpen(!isOpen)}>Toggle Menu</button>
|
|
160
|
+
{isOpen && (
|
|
161
|
+
<ul className="dropdown-menu">
|
|
162
|
+
<li>Option 1</li>
|
|
163
|
+
<li>Option 2</li>
|
|
164
|
+
<li>Option 3</li>
|
|
165
|
+
</ul>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Modal with Click Outside to Close
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { useClickAnyWhere } from "@usefy/use-click-any-where";
|
|
176
|
+
import { useRef } from "react";
|
|
177
|
+
|
|
178
|
+
function Modal({ isOpen, onClose, children }: ModalProps) {
|
|
179
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
180
|
+
|
|
181
|
+
useClickAnyWhere(
|
|
182
|
+
(event) => {
|
|
183
|
+
if (
|
|
184
|
+
modalRef.current &&
|
|
185
|
+
!modalRef.current.contains(event.target as Node)
|
|
186
|
+
) {
|
|
187
|
+
onClose();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{ enabled: isOpen }
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (!isOpen) return null;
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className="modal-overlay">
|
|
197
|
+
<div ref={modalRef} className="modal-content">
|
|
198
|
+
{children}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Context Menu
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { useClickAnyWhere } from "@usefy/use-click-any-where";
|
|
209
|
+
import { useState } from "react";
|
|
210
|
+
|
|
211
|
+
function ContextMenu() {
|
|
212
|
+
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
|
|
213
|
+
|
|
214
|
+
// Close menu on any click
|
|
215
|
+
useClickAnyWhere(() => setMenu(null), { enabled: menu !== null });
|
|
216
|
+
|
|
217
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
setMenu({ x: e.clientX, y: e.clientY });
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div onContextMenu={handleContextMenu} className="context-area">
|
|
224
|
+
Right-click anywhere
|
|
225
|
+
{menu && (
|
|
226
|
+
<div
|
|
227
|
+
className="context-menu"
|
|
228
|
+
style={{ position: "fixed", left: menu.x, top: menu.y }}
|
|
229
|
+
>
|
|
230
|
+
<button>Cut</button>
|
|
231
|
+
<button>Copy</button>
|
|
232
|
+
<button>Paste</button>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Click Coordinate Logger
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
import { useClickAnyWhere } from "@usefy/use-click-any-where";
|
|
244
|
+
import { useState } from "react";
|
|
245
|
+
|
|
246
|
+
function ClickLogger() {
|
|
247
|
+
const [clicks, setClicks] = useState<
|
|
248
|
+
Array<{ x: number; y: number; time: Date }>
|
|
249
|
+
>([]);
|
|
250
|
+
|
|
251
|
+
useClickAnyWhere((event) => {
|
|
252
|
+
setClicks((prev) => [
|
|
253
|
+
...prev.slice(-9), // Keep last 10 clicks
|
|
254
|
+
{ x: event.clientX, y: event.clientY, time: new Date() },
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div>
|
|
260
|
+
<h3>Recent Clicks</h3>
|
|
261
|
+
<ul>
|
|
262
|
+
{clicks.map((click, i) => (
|
|
263
|
+
<li key={i}>
|
|
264
|
+
({click.x}, {click.y}) at {click.time.toLocaleTimeString()}
|
|
265
|
+
</li>
|
|
266
|
+
))}
|
|
267
|
+
</ul>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### With Capture Phase
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
import { useClickAnyWhere } from "@usefy/use-click-any-where";
|
|
277
|
+
|
|
278
|
+
function CapturePhaseHandler() {
|
|
279
|
+
// Handle click before it reaches any element
|
|
280
|
+
useClickAnyWhere(
|
|
281
|
+
(event) => {
|
|
282
|
+
console.log("Click captured (before bubble):", event.target);
|
|
283
|
+
},
|
|
284
|
+
{ capture: true }
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return <div>Clicks are captured in capture phase</div>;
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Tooltip Dismissal
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
import { useClickAnyWhere } from "@usefy/use-click-any-where";
|
|
295
|
+
import { useState, useRef } from "react";
|
|
296
|
+
|
|
297
|
+
function TooltipTrigger({ content }: { content: string }) {
|
|
298
|
+
const [showTooltip, setShowTooltip] = useState(false);
|
|
299
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
300
|
+
|
|
301
|
+
useClickAnyWhere(
|
|
302
|
+
(event) => {
|
|
303
|
+
if (
|
|
304
|
+
triggerRef.current &&
|
|
305
|
+
!triggerRef.current.contains(event.target as Node)
|
|
306
|
+
) {
|
|
307
|
+
setShowTooltip(false);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
{ enabled: showTooltip }
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div className="tooltip-container">
|
|
315
|
+
<button ref={triggerRef} onClick={() => setShowTooltip(!showTooltip)}>
|
|
316
|
+
Show Info
|
|
317
|
+
</button>
|
|
318
|
+
{showTooltip && <div className="tooltip">{content}</div>}
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## TypeScript
|
|
327
|
+
|
|
328
|
+
This hook is written in TypeScript with exported types.
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
import {
|
|
332
|
+
useClickAnyWhere,
|
|
333
|
+
type UseClickAnyWhereOptions,
|
|
334
|
+
type ClickAnyWhereHandler,
|
|
335
|
+
} from "@usefy/use-click-any-where";
|
|
336
|
+
|
|
337
|
+
// Handler type
|
|
338
|
+
const handleClick: ClickAnyWhereHandler = (event) => {
|
|
339
|
+
console.log("Clicked at:", event.clientX, event.clientY);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Options type
|
|
343
|
+
const options: UseClickAnyWhereOptions = {
|
|
344
|
+
enabled: true,
|
|
345
|
+
capture: false,
|
|
346
|
+
passive: true,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
useClickAnyWhere(handleClick, options);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Testing
|
|
355
|
+
|
|
356
|
+
This package maintains comprehensive test coverage to ensure reliability and stability.
|
|
357
|
+
|
|
358
|
+
### Test Coverage
|
|
359
|
+
|
|
360
|
+
| Category | Tests | Coverage |
|
|
361
|
+
| ------------------- | ------ | --------- |
|
|
362
|
+
| Basic Functionality | 4 | 100% |
|
|
363
|
+
| Enabled Option | 4 | 100% |
|
|
364
|
+
| Capture Option | 3 | 100% |
|
|
365
|
+
| Passive Option | 2 | 100% |
|
|
366
|
+
| Cleanup | 2 | 100% |
|
|
367
|
+
| Handler Stability | 2 | 100% |
|
|
368
|
+
| Multiple Instances | 2 | 100% |
|
|
369
|
+
| Options Changes | 2 | 100% |
|
|
370
|
+
| Edge Cases | 2 | 100% |
|
|
371
|
+
| **Total** | **23** | **92.3%** |
|
|
372
|
+
|
|
373
|
+
### Test Categories
|
|
374
|
+
|
|
375
|
+
<details>
|
|
376
|
+
<summary><strong>Basic Functionality Tests</strong></summary>
|
|
377
|
+
|
|
378
|
+
- Call handler when document is clicked
|
|
379
|
+
- Pass MouseEvent with correct properties
|
|
380
|
+
- Handle multiple clicks
|
|
381
|
+
- Register event listener on mount
|
|
382
|
+
|
|
383
|
+
</details>
|
|
384
|
+
|
|
385
|
+
<details>
|
|
386
|
+
<summary><strong>Enabled Option Tests</strong></summary>
|
|
387
|
+
|
|
388
|
+
- Not call handler when enabled is false
|
|
389
|
+
- Not register event listener when disabled
|
|
390
|
+
- Toggle listener when enabled changes
|
|
391
|
+
- Default enabled to true
|
|
392
|
+
|
|
393
|
+
</details>
|
|
394
|
+
|
|
395
|
+
<details>
|
|
396
|
+
<summary><strong>Handler Stability Tests</strong></summary>
|
|
397
|
+
|
|
398
|
+
- Not re-register listener when handler changes
|
|
399
|
+
- Call the latest handler after update
|
|
400
|
+
|
|
401
|
+
</details>
|
|
402
|
+
|
|
403
|
+
### Running Tests
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
# Run all tests
|
|
407
|
+
pnpm test
|
|
408
|
+
|
|
409
|
+
# Run tests in watch mode
|
|
410
|
+
pnpm test:watch
|
|
411
|
+
|
|
412
|
+
# Run tests with coverage report
|
|
413
|
+
pnpm test --coverage
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Related Packages
|
|
419
|
+
|
|
420
|
+
Explore other hooks in the **@usefy** collection:
|
|
421
|
+
|
|
422
|
+
| Package | Description |
|
|
423
|
+
| ------------------------------------------------------------------------------------------ | ------------------------ |
|
|
424
|
+
| [@usefy/use-toggle](https://www.npmjs.com/package/@usefy/use-toggle) | Boolean state management |
|
|
425
|
+
| [@usefy/use-counter](https://www.npmjs.com/package/@usefy/use-counter) | Counter state management |
|
|
426
|
+
| [@usefy/use-debounce](https://www.npmjs.com/package/@usefy/use-debounce) | Value debouncing |
|
|
427
|
+
| [@usefy/use-debounce-callback](https://www.npmjs.com/package/@usefy/use-debounce-callback) | Debounced callbacks |
|
|
428
|
+
| [@usefy/use-throttle](https://www.npmjs.com/package/@usefy/use-throttle) | Value throttling |
|
|
429
|
+
| [@usefy/use-throttle-callback](https://www.npmjs.com/package/@usefy/use-throttle-callback) | Throttled callbacks |
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Contributing
|
|
434
|
+
|
|
435
|
+
We welcome contributions! Please see our [Contributing Guide](https://github.com/geon0529/usefy/blob/master/CONTRIBUTING.md) for details.
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
# Clone the repository
|
|
439
|
+
git clone https://github.com/geon0529/usefy.git
|
|
440
|
+
|
|
441
|
+
# Install dependencies
|
|
442
|
+
pnpm install
|
|
443
|
+
|
|
444
|
+
# Run tests
|
|
445
|
+
pnpm test
|
|
446
|
+
|
|
447
|
+
# Build
|
|
448
|
+
pnpm build
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## License
|
|
454
|
+
|
|
455
|
+
MIT © [mirunamu](https://github.com/geon0529)
|
|
456
|
+
|
|
457
|
+
This package is part of the [usefy](https://github.com/geon0529/usefy) monorepo.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
<p align="center">
|
|
462
|
+
<sub>Built with care by the usefy team</sub>
|
|
463
|
+
</p>
|