@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.
Files changed (2) hide show
  1. package/README.md +463 -0
  2. 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>
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.11",
4
4
  "description": "A React hook for detecting document-wide click events",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",