@usefy/use-click-any-where 0.0.8 → 0.0.10

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.
Files changed (2) hide show
  1. package/README.md +462 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,462 @@
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 (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
148
+ setIsOpen(false);
149
+ }
150
+ },
151
+ { enabled: isOpen }
152
+ );
153
+
154
+ return (
155
+ <div ref={dropdownRef}>
156
+ <button onClick={() => setIsOpen(!isOpen)}>
157
+ Toggle Menu
158
+ </button>
159
+ {isOpen && (
160
+ <ul className="dropdown-menu">
161
+ <li>Option 1</li>
162
+ <li>Option 2</li>
163
+ <li>Option 3</li>
164
+ </ul>
165
+ )}
166
+ </div>
167
+ );
168
+ }
169
+ ```
170
+
171
+ ### Modal with Click Outside to Close
172
+
173
+ ```tsx
174
+ import { useClickAnyWhere } from '@usefy/use-click-any-where';
175
+ import { useRef } from 'react';
176
+
177
+ function Modal({ isOpen, onClose, children }: ModalProps) {
178
+ const modalRef = useRef<HTMLDivElement>(null);
179
+
180
+ useClickAnyWhere(
181
+ (event) => {
182
+ if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
183
+ onClose();
184
+ }
185
+ },
186
+ { enabled: isOpen }
187
+ );
188
+
189
+ if (!isOpen) return null;
190
+
191
+ return (
192
+ <div className="modal-overlay">
193
+ <div ref={modalRef} className="modal-content">
194
+ {children}
195
+ </div>
196
+ </div>
197
+ );
198
+ }
199
+ ```
200
+
201
+ ### Context Menu
202
+
203
+ ```tsx
204
+ import { useClickAnyWhere } from '@usefy/use-click-any-where';
205
+ import { useState } from 'react';
206
+
207
+ function ContextMenu() {
208
+ const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
209
+
210
+ // Close menu on any click
211
+ useClickAnyWhere(
212
+ () => setMenu(null),
213
+ { enabled: menu !== null }
214
+ );
215
+
216
+ const handleContextMenu = (e: React.MouseEvent) => {
217
+ e.preventDefault();
218
+ setMenu({ x: e.clientX, y: e.clientY });
219
+ };
220
+
221
+ return (
222
+ <div onContextMenu={handleContextMenu} className="context-area">
223
+ Right-click anywhere
224
+ {menu && (
225
+ <div
226
+ className="context-menu"
227
+ style={{ position: 'fixed', left: menu.x, top: menu.y }}
228
+ >
229
+ <button>Cut</button>
230
+ <button>Copy</button>
231
+ <button>Paste</button>
232
+ </div>
233
+ )}
234
+ </div>
235
+ );
236
+ }
237
+ ```
238
+
239
+ ### Click Coordinate Logger
240
+
241
+ ```tsx
242
+ import { useClickAnyWhere } from '@usefy/use-click-any-where';
243
+ import { useState } from 'react';
244
+
245
+ function ClickLogger() {
246
+ const [clicks, setClicks] = useState<Array<{ x: number; y: number; time: Date }>>([]);
247
+
248
+ useClickAnyWhere((event) => {
249
+ setClicks((prev) => [
250
+ ...prev.slice(-9), // Keep last 10 clicks
251
+ { x: event.clientX, y: event.clientY, time: new Date() },
252
+ ]);
253
+ });
254
+
255
+ return (
256
+ <div>
257
+ <h3>Recent Clicks</h3>
258
+ <ul>
259
+ {clicks.map((click, i) => (
260
+ <li key={i}>
261
+ ({click.x}, {click.y}) at {click.time.toLocaleTimeString()}
262
+ </li>
263
+ ))}
264
+ </ul>
265
+ </div>
266
+ );
267
+ }
268
+ ```
269
+
270
+ ### With Capture Phase
271
+
272
+ ```tsx
273
+ import { useClickAnyWhere } from '@usefy/use-click-any-where';
274
+
275
+ function CapturePhaseHandler() {
276
+ // Handle click before it reaches any element
277
+ useClickAnyWhere(
278
+ (event) => {
279
+ console.log('Click captured (before bubble):', event.target);
280
+ },
281
+ { capture: true }
282
+ );
283
+
284
+ return <div>Clicks are captured in capture phase</div>;
285
+ }
286
+ ```
287
+
288
+ ### Tooltip Dismissal
289
+
290
+ ```tsx
291
+ import { useClickAnyWhere } from '@usefy/use-click-any-where';
292
+ import { useState, useRef } from 'react';
293
+
294
+ function TooltipTrigger({ content }: { content: string }) {
295
+ const [showTooltip, setShowTooltip] = useState(false);
296
+ const triggerRef = useRef<HTMLButtonElement>(null);
297
+
298
+ useClickAnyWhere(
299
+ (event) => {
300
+ if (triggerRef.current && !triggerRef.current.contains(event.target as Node)) {
301
+ setShowTooltip(false);
302
+ }
303
+ },
304
+ { enabled: showTooltip }
305
+ );
306
+
307
+ return (
308
+ <div className="tooltip-container">
309
+ <button
310
+ ref={triggerRef}
311
+ onClick={() => setShowTooltip(!showTooltip)}
312
+ >
313
+ Show Info
314
+ </button>
315
+ {showTooltip && (
316
+ <div className="tooltip">{content}</div>
317
+ )}
318
+ </div>
319
+ );
320
+ }
321
+ ```
322
+
323
+ ---
324
+
325
+ ## TypeScript
326
+
327
+ This hook is written in TypeScript with exported types.
328
+
329
+ ```tsx
330
+ import {
331
+ useClickAnyWhere,
332
+ type UseClickAnyWhereOptions,
333
+ type ClickAnyWhereHandler,
334
+ } from '@usefy/use-click-any-where';
335
+
336
+ // Handler type
337
+ const handleClick: ClickAnyWhereHandler = (event) => {
338
+ console.log('Clicked at:', event.clientX, event.clientY);
339
+ };
340
+
341
+ // Options type
342
+ const options: UseClickAnyWhereOptions = {
343
+ enabled: true,
344
+ capture: false,
345
+ passive: true,
346
+ };
347
+
348
+ useClickAnyWhere(handleClick, options);
349
+ ```
350
+
351
+ ---
352
+
353
+ ## Testing
354
+
355
+ This package maintains comprehensive test coverage to ensure reliability and stability.
356
+
357
+ ### Test Coverage
358
+
359
+ | Category | Tests | Coverage |
360
+ |----------|-------|----------|
361
+ | Basic Functionality | 4 | 100% |
362
+ | Enabled Option | 4 | 100% |
363
+ | Capture Option | 3 | 100% |
364
+ | Passive Option | 2 | 100% |
365
+ | Cleanup | 2 | 100% |
366
+ | Handler Stability | 2 | 100% |
367
+ | Multiple Instances | 2 | 100% |
368
+ | Options Changes | 2 | 100% |
369
+ | Edge Cases | 2 | 100% |
370
+ | **Total** | **23** | **92.3%** |
371
+
372
+ ### Test Categories
373
+
374
+ <details>
375
+ <summary><strong>Basic Functionality Tests</strong></summary>
376
+
377
+ - Call handler when document is clicked
378
+ - Pass MouseEvent with correct properties
379
+ - Handle multiple clicks
380
+ - Register event listener on mount
381
+
382
+ </details>
383
+
384
+ <details>
385
+ <summary><strong>Enabled Option Tests</strong></summary>
386
+
387
+ - Not call handler when enabled is false
388
+ - Not register event listener when disabled
389
+ - Toggle listener when enabled changes
390
+ - Default enabled to true
391
+
392
+ </details>
393
+
394
+ <details>
395
+ <summary><strong>Handler Stability Tests</strong></summary>
396
+
397
+ - Not re-register listener when handler changes
398
+ - Call the latest handler after update
399
+
400
+ </details>
401
+
402
+ ### Running Tests
403
+
404
+ ```bash
405
+ # Run all tests
406
+ pnpm test
407
+
408
+ # Run tests in watch mode
409
+ pnpm test:watch
410
+
411
+ # Run tests with coverage report
412
+ pnpm test --coverage
413
+ ```
414
+
415
+ ---
416
+
417
+ ## Related Packages
418
+
419
+ Explore other hooks in the **@usefy** collection:
420
+
421
+ | Package | Description |
422
+ |---------|-------------|
423
+ | [@usefy/use-toggle](https://www.npmjs.com/package/@usefy/use-toggle) | Boolean state management |
424
+ | [@usefy/use-counter](https://www.npmjs.com/package/@usefy/use-counter) | Counter state management |
425
+ | [@usefy/use-debounce](https://www.npmjs.com/package/@usefy/use-debounce) | Value debouncing |
426
+ | [@usefy/use-debounce-callback](https://www.npmjs.com/package/@usefy/use-debounce-callback) | Debounced callbacks |
427
+ | [@usefy/use-throttle](https://www.npmjs.com/package/@usefy/use-throttle) | Value throttling |
428
+ | [@usefy/use-throttle-callback](https://www.npmjs.com/package/@usefy/use-throttle-callback) | Throttled callbacks |
429
+
430
+ ---
431
+
432
+ ## Contributing
433
+
434
+ We welcome contributions! Please see our [Contributing Guide](https://github.com/geon0529/usefy/blob/master/CONTRIBUTING.md) for details.
435
+
436
+ ```bash
437
+ # Clone the repository
438
+ git clone https://github.com/geon0529/usefy.git
439
+
440
+ # Install dependencies
441
+ pnpm install
442
+
443
+ # Run tests
444
+ pnpm test
445
+
446
+ # Build
447
+ pnpm build
448
+ ```
449
+
450
+ ---
451
+
452
+ ## License
453
+
454
+ MIT © [mirunamu](https://github.com/geon0529)
455
+
456
+ This package is part of the [usefy](https://github.com/geon0529/usefy) monorepo.
457
+
458
+ ---
459
+
460
+ <p align="center">
461
+ <sub>Built with care by the usefy team</sub>
462
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefy/use-click-any-where",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "A React hook for detecting document-wide click events",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",