@usefy/use-on-click-outside 0.0.1
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 +536 -0
- package/dist/index.d.mts +160 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +118 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +91 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
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-on-click-outside</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>A React hook for detecting clicks outside of specified elements</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@usefy/use-on-click-outside">
|
|
13
|
+
<img src="https://img.shields.io/npm/v/@usefy/use-on-click-outside.svg?style=flat-square&color=007acc" alt="npm version" />
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@usefy/use-on-click-outside">
|
|
16
|
+
<img src="https://img.shields.io/npm/dm/@usefy/use-on-click-outside.svg?style=flat-square&color=007acc" alt="npm downloads" />
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://bundlephobia.com/package/@usefy/use-on-click-outside">
|
|
19
|
+
<img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-on-click-outside?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-on-click-outside.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-on-click-outside` detects clicks outside of specified element(s) and calls your handler. Perfect for closing modals, dropdowns, popovers, tooltips, and any UI component that should dismiss when clicking elsewhere.
|
|
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-on-click-outside?
|
|
43
|
+
|
|
44
|
+
- **Zero Dependencies** — Pure React implementation with no external dependencies
|
|
45
|
+
- **TypeScript First** — Full type safety with exported interfaces
|
|
46
|
+
- **Multiple Refs Support** — Pass a single ref or an array of refs (e.g., button + dropdown)
|
|
47
|
+
- **Exclude Elements** — Exclude specific elements from triggering the handler via `excludeRefs` or `shouldExclude`
|
|
48
|
+
- **Mouse + Touch Support** — Handles both `mousedown` and `touchstart` events for mobile compatibility
|
|
49
|
+
- **Capture Phase** — Uses capture phase by default to avoid `stopPropagation` issues
|
|
50
|
+
- **Conditional Activation** — Enable/disable via the `enabled` option
|
|
51
|
+
- **Handler Stability** — No re-registration when handler changes
|
|
52
|
+
- **SSR Compatible** — Works seamlessly with Next.js, Remix, and other SSR frameworks
|
|
53
|
+
- **Well Tested** — 97.61% test coverage with Vitest
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# npm
|
|
61
|
+
npm install @usefy/use-on-click-outside
|
|
62
|
+
|
|
63
|
+
# yarn
|
|
64
|
+
yarn add @usefy/use-on-click-outside
|
|
65
|
+
|
|
66
|
+
# pnpm
|
|
67
|
+
pnpm add @usefy/use-on-click-outside
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Peer Dependencies
|
|
71
|
+
|
|
72
|
+
This package requires React 18 or 19:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { useOnClickOutside } from "@usefy/use-on-click-outside";
|
|
88
|
+
import { useRef, useState } from "react";
|
|
89
|
+
|
|
90
|
+
function Modal({ onClose }: { onClose: () => void }) {
|
|
91
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
92
|
+
|
|
93
|
+
useOnClickOutside(modalRef, () => onClose());
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="overlay">
|
|
97
|
+
<div ref={modalRef} className="modal">
|
|
98
|
+
Click outside to close
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## API Reference
|
|
108
|
+
|
|
109
|
+
### `useOnClickOutside(ref, handler, options?)`
|
|
110
|
+
|
|
111
|
+
A hook that detects clicks outside of specified element(s).
|
|
112
|
+
|
|
113
|
+
#### Parameters
|
|
114
|
+
|
|
115
|
+
| Parameter | Type | Description |
|
|
116
|
+
| --------- | ------------------------ | -------------------------------------------------- |
|
|
117
|
+
| `ref` | `RefTarget<T>` | Single ref or array of refs to detect outside clicks for |
|
|
118
|
+
| `handler` | `OnClickOutsideHandler` | Callback function called when a click outside is detected |
|
|
119
|
+
| `options` | `UseOnClickOutsideOptions` | Configuration options |
|
|
120
|
+
|
|
121
|
+
#### Options
|
|
122
|
+
|
|
123
|
+
| Option | Type | Default | Description |
|
|
124
|
+
| ---------------- | --------------------------- | -------------- | ----------------------------------------------------- |
|
|
125
|
+
| `enabled` | `boolean` | `true` | Whether the event listener is active |
|
|
126
|
+
| `capture` | `boolean` | `true` | Use event capture phase (immune to stopPropagation) |
|
|
127
|
+
| `eventType` | `MouseEventType` | `"mousedown"` | Mouse event type to listen for |
|
|
128
|
+
| `touchEventType` | `TouchEventType` | `"touchstart"` | Touch event type to listen for |
|
|
129
|
+
| `detectTouch` | `boolean` | `true` | Whether to detect touch events (mobile support) |
|
|
130
|
+
| `excludeRefs` | `RefObject<HTMLElement>[]` | `[]` | Refs to exclude from outside click detection |
|
|
131
|
+
| `shouldExclude` | `(target: Node) => boolean` | `undefined` | Custom function to determine if target should be excluded |
|
|
132
|
+
| `eventTarget` | `Document \| HTMLElement \| Window` | `document` | The event target to attach listeners to |
|
|
133
|
+
|
|
134
|
+
#### Types
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
type ClickOutsideEvent = MouseEvent | TouchEvent;
|
|
138
|
+
type OnClickOutsideHandler = (event: ClickOutsideEvent) => void;
|
|
139
|
+
type MouseEventType = "mousedown" | "mouseup" | "click" | "pointerdown" | "pointerup";
|
|
140
|
+
type TouchEventType = "touchstart" | "touchend";
|
|
141
|
+
type RefTarget<T extends HTMLElement> =
|
|
142
|
+
| React.RefObject<T | null>
|
|
143
|
+
| Array<React.RefObject<HTMLElement | null>>;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Returns
|
|
147
|
+
|
|
148
|
+
`void`
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Examples
|
|
153
|
+
|
|
154
|
+
### Basic Modal
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { useOnClickOutside } from "@usefy/use-on-click-outside";
|
|
158
|
+
import { useRef, useState } from "react";
|
|
159
|
+
|
|
160
|
+
function Modal({ isOpen, onClose, children }: ModalProps) {
|
|
161
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
162
|
+
|
|
163
|
+
useOnClickOutside(modalRef, onClose, { enabled: isOpen });
|
|
164
|
+
|
|
165
|
+
if (!isOpen) return null;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="overlay">
|
|
169
|
+
<div ref={modalRef} className="modal">
|
|
170
|
+
{children}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Dropdown with Multiple Refs
|
|
178
|
+
|
|
179
|
+
When you have a button that toggles a dropdown, you want clicks on both the button and dropdown to be considered "inside":
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import { useOnClickOutside } from "@usefy/use-on-click-outside";
|
|
183
|
+
import { useRef, useState } from "react";
|
|
184
|
+
|
|
185
|
+
function Dropdown() {
|
|
186
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
187
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
188
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
189
|
+
|
|
190
|
+
// Both button and menu are considered "inside"
|
|
191
|
+
useOnClickOutside(
|
|
192
|
+
[buttonRef, menuRef],
|
|
193
|
+
() => setIsOpen(false),
|
|
194
|
+
{ enabled: isOpen }
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<>
|
|
199
|
+
<button ref={buttonRef} onClick={() => setIsOpen(!isOpen)}>
|
|
200
|
+
Toggle Menu
|
|
201
|
+
</button>
|
|
202
|
+
{isOpen && (
|
|
203
|
+
<div ref={menuRef} className="dropdown-menu">
|
|
204
|
+
<button>Option 1</button>
|
|
205
|
+
<button>Option 2</button>
|
|
206
|
+
<button>Option 3</button>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Exclude Specific Elements
|
|
215
|
+
|
|
216
|
+
Use `excludeRefs` to prevent certain elements from triggering the outside click handler:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import { useOnClickOutside } from "@usefy/use-on-click-outside";
|
|
220
|
+
import { useRef, useState } from "react";
|
|
221
|
+
|
|
222
|
+
function ModalWithToast() {
|
|
223
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
224
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
225
|
+
const toastRef = useRef<HTMLDivElement>(null);
|
|
226
|
+
|
|
227
|
+
useOnClickOutside(modalRef, () => setIsOpen(false), {
|
|
228
|
+
enabled: isOpen,
|
|
229
|
+
excludeRefs: [toastRef], // Clicks on toast won't close modal
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<>
|
|
234
|
+
{isOpen && (
|
|
235
|
+
<div className="overlay">
|
|
236
|
+
<div ref={modalRef} className="modal">
|
|
237
|
+
Modal Content
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
<div ref={toastRef} className="toast">
|
|
242
|
+
This toast won't close the modal when clicked
|
|
243
|
+
</div>
|
|
244
|
+
</>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Custom Exclude Logic with shouldExclude
|
|
250
|
+
|
|
251
|
+
Use `shouldExclude` for dynamic exclusion based on element properties:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { useOnClickOutside } from "@usefy/use-on-click-outside";
|
|
255
|
+
import { useRef, useState } from "react";
|
|
256
|
+
|
|
257
|
+
function MenuWithIgnoredElements() {
|
|
258
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
259
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
260
|
+
|
|
261
|
+
useOnClickOutside(menuRef, () => setIsOpen(false), {
|
|
262
|
+
enabled: isOpen,
|
|
263
|
+
shouldExclude: (target) => {
|
|
264
|
+
// Ignore clicks on elements with specific class
|
|
265
|
+
return (target as Element).closest?.(".ignore-outside-click") !== null;
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<>
|
|
271
|
+
{isOpen && (
|
|
272
|
+
<div ref={menuRef} className="menu">
|
|
273
|
+
Menu Content
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
<button className="ignore-outside-click">
|
|
277
|
+
This button won't close the menu
|
|
278
|
+
</button>
|
|
279
|
+
</>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Popover with Touch Support
|
|
285
|
+
|
|
286
|
+
Touch events are enabled by default for mobile compatibility:
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
import { useOnClickOutside } from "@usefy/use-on-click-outside";
|
|
290
|
+
import { useRef, useState } from "react";
|
|
291
|
+
|
|
292
|
+
function Popover({ trigger, content }: PopoverProps) {
|
|
293
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
294
|
+
const popoverRef = useRef<HTMLDivElement>(null);
|
|
295
|
+
|
|
296
|
+
// Handles both mouse and touch events
|
|
297
|
+
useOnClickOutside(popoverRef, () => setIsOpen(false), {
|
|
298
|
+
enabled: isOpen,
|
|
299
|
+
detectTouch: true, // default
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div ref={popoverRef}>
|
|
304
|
+
<button onClick={() => setIsOpen(!isOpen)}>{trigger}</button>
|
|
305
|
+
{isOpen && <div className="popover-content">{content}</div>}
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Context Menu
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
import { useOnClickOutside } from "@usefy/use-on-click-outside";
|
|
315
|
+
import { useRef, useState } from "react";
|
|
316
|
+
|
|
317
|
+
function ContextMenu() {
|
|
318
|
+
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
|
|
319
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
320
|
+
|
|
321
|
+
useOnClickOutside(menuRef, () => setMenu(null), {
|
|
322
|
+
enabled: menu !== null,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
326
|
+
e.preventDefault();
|
|
327
|
+
setMenu({ x: e.clientX, y: e.clientY });
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div onContextMenu={handleContextMenu} className="context-area">
|
|
332
|
+
Right-click anywhere
|
|
333
|
+
{menu && (
|
|
334
|
+
<div
|
|
335
|
+
ref={menuRef}
|
|
336
|
+
className="context-menu"
|
|
337
|
+
style={{ position: "fixed", left: menu.x, top: menu.y }}
|
|
338
|
+
>
|
|
339
|
+
<button>Cut</button>
|
|
340
|
+
<button>Copy</button>
|
|
341
|
+
<button>Paste</button>
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Different Event Types
|
|
350
|
+
|
|
351
|
+
You can customize which mouse/touch events trigger the handler:
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
import { useOnClickOutside } from "@usefy/use-on-click-outside";
|
|
355
|
+
|
|
356
|
+
// Use 'click' instead of 'mousedown' (fires after full click completes)
|
|
357
|
+
useOnClickOutside(ref, handler, {
|
|
358
|
+
eventType: "click",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Use 'touchend' instead of 'touchstart'
|
|
362
|
+
useOnClickOutside(ref, handler, {
|
|
363
|
+
touchEventType: "touchend",
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Disable touch detection entirely
|
|
367
|
+
useOnClickOutside(ref, handler, {
|
|
368
|
+
detectTouch: false,
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## TypeScript
|
|
375
|
+
|
|
376
|
+
This hook is written in TypeScript with exported types.
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
import {
|
|
380
|
+
useOnClickOutside,
|
|
381
|
+
type UseOnClickOutsideOptions,
|
|
382
|
+
type OnClickOutsideHandler,
|
|
383
|
+
type ClickOutsideEvent,
|
|
384
|
+
type RefTarget,
|
|
385
|
+
type MouseEventType,
|
|
386
|
+
type TouchEventType,
|
|
387
|
+
} from "@usefy/use-on-click-outside";
|
|
388
|
+
|
|
389
|
+
// Handler type
|
|
390
|
+
const handleOutsideClick: OnClickOutsideHandler = (event) => {
|
|
391
|
+
console.log("Clicked outside at:", event.clientX, event.clientY);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Options type
|
|
395
|
+
const options: UseOnClickOutsideOptions = {
|
|
396
|
+
enabled: true,
|
|
397
|
+
capture: true,
|
|
398
|
+
eventType: "mousedown",
|
|
399
|
+
detectTouch: true,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
useOnClickOutside(ref, handleOutsideClick, options);
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Testing
|
|
408
|
+
|
|
409
|
+
This package maintains comprehensive test coverage to ensure reliability and stability.
|
|
410
|
+
|
|
411
|
+
### Test Coverage
|
|
412
|
+
|
|
413
|
+
| Category | Coverage |
|
|
414
|
+
| ---------- | ------------------ |
|
|
415
|
+
| Statements | 97.61% (41/42) |
|
|
416
|
+
| Branches | 93.93% (31/33) |
|
|
417
|
+
| Functions | 100% (7/7) |
|
|
418
|
+
| Lines | 97.61% (41/42) |
|
|
419
|
+
|
|
420
|
+
### Test Categories
|
|
421
|
+
|
|
422
|
+
<details>
|
|
423
|
+
<summary><strong>Basic Functionality Tests</strong></summary>
|
|
424
|
+
|
|
425
|
+
- Call handler when clicked outside the element
|
|
426
|
+
- Not call handler when clicked inside the element
|
|
427
|
+
- Not call handler when target is not connected to DOM
|
|
428
|
+
- Handle events with correct type (MouseEvent/TouchEvent)
|
|
429
|
+
|
|
430
|
+
</details>
|
|
431
|
+
|
|
432
|
+
<details>
|
|
433
|
+
<summary><strong>Multiple Refs Tests</strong></summary>
|
|
434
|
+
|
|
435
|
+
- Support array of refs
|
|
436
|
+
- Not call handler when clicking inside any of the refs
|
|
437
|
+
- Call handler only when clicking outside all refs
|
|
438
|
+
|
|
439
|
+
</details>
|
|
440
|
+
|
|
441
|
+
<details>
|
|
442
|
+
<summary><strong>Enabled Option Tests</strong></summary>
|
|
443
|
+
|
|
444
|
+
- Not call handler when enabled is false
|
|
445
|
+
- Not register event listener when disabled
|
|
446
|
+
- Toggle listener when enabled changes
|
|
447
|
+
- Default enabled to true
|
|
448
|
+
|
|
449
|
+
</details>
|
|
450
|
+
|
|
451
|
+
<details>
|
|
452
|
+
<summary><strong>Exclude Refs Tests</strong></summary>
|
|
453
|
+
|
|
454
|
+
- Not call handler when clicking on excluded element
|
|
455
|
+
- Support multiple exclude refs
|
|
456
|
+
- Handle dynamically added exclude refs
|
|
457
|
+
|
|
458
|
+
</details>
|
|
459
|
+
|
|
460
|
+
<details>
|
|
461
|
+
<summary><strong>shouldExclude Function Tests</strong></summary>
|
|
462
|
+
|
|
463
|
+
- Not call handler when shouldExclude returns true
|
|
464
|
+
- Pass correct target to shouldExclude function
|
|
465
|
+
|
|
466
|
+
</details>
|
|
467
|
+
|
|
468
|
+
<details>
|
|
469
|
+
<summary><strong>Touch Events Tests</strong></summary>
|
|
470
|
+
|
|
471
|
+
- Call handler on touchstart when detectTouch is true
|
|
472
|
+
- Not listen for touch events when detectTouch is false
|
|
473
|
+
|
|
474
|
+
</details>
|
|
475
|
+
|
|
476
|
+
<details>
|
|
477
|
+
<summary><strong>Handler Stability Tests</strong></summary>
|
|
478
|
+
|
|
479
|
+
- Not re-register listener when handler changes
|
|
480
|
+
- Call the latest handler after update
|
|
481
|
+
|
|
482
|
+
</details>
|
|
483
|
+
|
|
484
|
+
<details>
|
|
485
|
+
<summary><strong>Cleanup Tests</strong></summary>
|
|
486
|
+
|
|
487
|
+
- Remove event listeners on unmount
|
|
488
|
+
- Not call handler after unmount
|
|
489
|
+
|
|
490
|
+
</details>
|
|
491
|
+
|
|
492
|
+
### Running Tests
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
# Run all tests
|
|
496
|
+
pnpm test
|
|
497
|
+
|
|
498
|
+
# Run tests in watch mode
|
|
499
|
+
pnpm test:watch
|
|
500
|
+
|
|
501
|
+
# Run tests with coverage report
|
|
502
|
+
pnpm test --coverage
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## Related Packages
|
|
508
|
+
|
|
509
|
+
Explore other hooks in the **@usefy** collection:
|
|
510
|
+
|
|
511
|
+
| Package | Description |
|
|
512
|
+
| ------------------------------------------------------------------------------------------ | ----------------------------------- |
|
|
513
|
+
| [@usefy/use-click-any-where](https://www.npmjs.com/package/@usefy/use-click-any-where) | Document-wide click detection |
|
|
514
|
+
| [@usefy/use-toggle](https://www.npmjs.com/package/@usefy/use-toggle) | Boolean state management |
|
|
515
|
+
| [@usefy/use-counter](https://www.npmjs.com/package/@usefy/use-counter) | Counter state management |
|
|
516
|
+
| [@usefy/use-debounce](https://www.npmjs.com/package/@usefy/use-debounce) | Value debouncing |
|
|
517
|
+
| [@usefy/use-debounce-callback](https://www.npmjs.com/package/@usefy/use-debounce-callback) | Debounced callbacks |
|
|
518
|
+
| [@usefy/use-throttle](https://www.npmjs.com/package/@usefy/use-throttle) | Value throttling |
|
|
519
|
+
| [@usefy/use-throttle-callback](https://www.npmjs.com/package/@usefy/use-throttle-callback) | Throttled callbacks |
|
|
520
|
+
| [@usefy/use-local-storage](https://www.npmjs.com/package/@usefy/use-local-storage) | localStorage state synchronization |
|
|
521
|
+
| [@usefy/use-session-storage](https://www.npmjs.com/package/@usefy/use-session-storage) | sessionStorage state synchronization|
|
|
522
|
+
| [@usefy/use-copy-to-clipboard](https://www.npmjs.com/package/@usefy/use-copy-to-clipboard) | Clipboard operations |
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## License
|
|
527
|
+
|
|
528
|
+
MIT © [mirunamu](https://github.com/geon0529)
|
|
529
|
+
|
|
530
|
+
This package is part of the [usefy](https://github.com/geon0529/usefy) monorepo.
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
<p align="center">
|
|
535
|
+
<sub>Built with care by the usefy team</sub>
|
|
536
|
+
</p>
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event types for click outside detection (mouse + touch)
|
|
3
|
+
*/
|
|
4
|
+
type ClickOutsideEvent = MouseEvent | TouchEvent;
|
|
5
|
+
/**
|
|
6
|
+
* Handler function type for click outside events
|
|
7
|
+
*/
|
|
8
|
+
type OnClickOutsideHandler = (event: ClickOutsideEvent) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Mouse event type options
|
|
11
|
+
*/
|
|
12
|
+
type MouseEventType = "mousedown" | "mouseup" | "click" | "pointerdown" | "pointerup";
|
|
13
|
+
/**
|
|
14
|
+
* Touch event type options
|
|
15
|
+
*/
|
|
16
|
+
type TouchEventType = "touchstart" | "touchend";
|
|
17
|
+
/**
|
|
18
|
+
* Ref target type - supports single ref or array of refs
|
|
19
|
+
* Array accepts mixed element types (e.g., [buttonRef, divRef])
|
|
20
|
+
*/
|
|
21
|
+
type RefTarget<T extends HTMLElement = HTMLElement> = React.RefObject<T | null> | Array<React.RefObject<HTMLElement | null>>;
|
|
22
|
+
/**
|
|
23
|
+
* Options for useOnClickOutside hook
|
|
24
|
+
*/
|
|
25
|
+
interface UseOnClickOutsideOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Whether the event listener is enabled
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether to use event capture phase.
|
|
33
|
+
* When true, the handler is called before the event reaches the target element,
|
|
34
|
+
* making it immune to stopPropagation calls.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
capture?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Mouse event type to listen for
|
|
40
|
+
* @default 'mousedown'
|
|
41
|
+
*/
|
|
42
|
+
eventType?: MouseEventType;
|
|
43
|
+
/**
|
|
44
|
+
* Touch event type to listen for
|
|
45
|
+
* @default 'touchstart'
|
|
46
|
+
*/
|
|
47
|
+
touchEventType?: TouchEventType;
|
|
48
|
+
/**
|
|
49
|
+
* Whether to detect touch events (for mobile support)
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
detectTouch?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Array of refs to exclude from outside click detection.
|
|
55
|
+
* Clicks on these elements will not trigger the handler.
|
|
56
|
+
*/
|
|
57
|
+
excludeRefs?: Array<React.RefObject<HTMLElement | null>>;
|
|
58
|
+
/**
|
|
59
|
+
* Custom function to determine if a target should be excluded.
|
|
60
|
+
* Return true to ignore clicks on the target element.
|
|
61
|
+
* @param target - The clicked element
|
|
62
|
+
* @returns Whether to exclude this element from triggering the handler
|
|
63
|
+
*/
|
|
64
|
+
shouldExclude?: (target: Node) => boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The event target to attach listeners to
|
|
67
|
+
* @default document
|
|
68
|
+
*/
|
|
69
|
+
eventTarget?: Document | HTMLElement | Window | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Detects clicks outside of specified element(s) and calls the provided handler.
|
|
73
|
+
* Useful for closing modals, dropdowns, popovers, and similar UI components.
|
|
74
|
+
*
|
|
75
|
+
* @param ref - Single ref or array of refs to detect outside clicks for
|
|
76
|
+
* @param handler - Callback function called when a click outside is detected
|
|
77
|
+
* @param options - Configuration options for the event listener
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* // Basic usage - close modal on outside click
|
|
82
|
+
* function Modal({ isOpen, onClose }) {
|
|
83
|
+
* const modalRef = useRef<HTMLDivElement>(null);
|
|
84
|
+
*
|
|
85
|
+
* useOnClickOutside(modalRef, () => onClose(), { enabled: isOpen });
|
|
86
|
+
*
|
|
87
|
+
* if (!isOpen) return null;
|
|
88
|
+
*
|
|
89
|
+
* return (
|
|
90
|
+
* <div className="overlay">
|
|
91
|
+
* <div ref={modalRef} className="modal">
|
|
92
|
+
* Modal content
|
|
93
|
+
* </div>
|
|
94
|
+
* </div>
|
|
95
|
+
* );
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* // Multiple refs - button and dropdown menu
|
|
102
|
+
* function Dropdown() {
|
|
103
|
+
* const [isOpen, setIsOpen] = useState(false);
|
|
104
|
+
* const buttonRef = useRef<HTMLButtonElement>(null);
|
|
105
|
+
* const menuRef = useRef<HTMLDivElement>(null);
|
|
106
|
+
*
|
|
107
|
+
* useOnClickOutside(
|
|
108
|
+
* [buttonRef, menuRef],
|
|
109
|
+
* () => setIsOpen(false),
|
|
110
|
+
* { enabled: isOpen }
|
|
111
|
+
* );
|
|
112
|
+
*
|
|
113
|
+
* return (
|
|
114
|
+
* <>
|
|
115
|
+
* <button ref={buttonRef} onClick={() => setIsOpen(!isOpen)}>
|
|
116
|
+
* Toggle
|
|
117
|
+
* </button>
|
|
118
|
+
* {isOpen && (
|
|
119
|
+
* <div ref={menuRef}>Dropdown content</div>
|
|
120
|
+
* )}
|
|
121
|
+
* </>
|
|
122
|
+
* );
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```tsx
|
|
128
|
+
* // With exclude refs - ignore specific elements
|
|
129
|
+
* function ModalWithPortal({ isOpen, onClose }) {
|
|
130
|
+
* const modalRef = useRef<HTMLDivElement>(null);
|
|
131
|
+
* const toastRef = useRef<HTMLDivElement>(null);
|
|
132
|
+
*
|
|
133
|
+
* useOnClickOutside(modalRef, onClose, {
|
|
134
|
+
* enabled: isOpen,
|
|
135
|
+
* excludeRefs: [toastRef], // Clicks on toast won't close modal
|
|
136
|
+
* });
|
|
137
|
+
*
|
|
138
|
+
* return (
|
|
139
|
+
* <>
|
|
140
|
+
* {isOpen && <div ref={modalRef}>Modal</div>}
|
|
141
|
+
* <div ref={toastRef}>Toast notification</div>
|
|
142
|
+
* </>
|
|
143
|
+
* );
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```tsx
|
|
149
|
+
* // With custom exclude function
|
|
150
|
+
* useOnClickOutside(ref, handleClose, {
|
|
151
|
+
* shouldExclude: (target) => {
|
|
152
|
+
* // Ignore clicks on elements with specific class
|
|
153
|
+
* return (target as Element).closest?.('.ignore-outside-click') !== null;
|
|
154
|
+
* },
|
|
155
|
+
* });
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
declare function useOnClickOutside<T extends HTMLElement = HTMLElement>(ref: RefTarget<T>, handler: OnClickOutsideHandler, options?: UseOnClickOutsideOptions): void;
|
|
159
|
+
|
|
160
|
+
export { type ClickOutsideEvent, type MouseEventType, type OnClickOutsideHandler, type RefTarget, type TouchEventType, type UseOnClickOutsideOptions, useOnClickOutside };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event types for click outside detection (mouse + touch)
|
|
3
|
+
*/
|
|
4
|
+
type ClickOutsideEvent = MouseEvent | TouchEvent;
|
|
5
|
+
/**
|
|
6
|
+
* Handler function type for click outside events
|
|
7
|
+
*/
|
|
8
|
+
type OnClickOutsideHandler = (event: ClickOutsideEvent) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Mouse event type options
|
|
11
|
+
*/
|
|
12
|
+
type MouseEventType = "mousedown" | "mouseup" | "click" | "pointerdown" | "pointerup";
|
|
13
|
+
/**
|
|
14
|
+
* Touch event type options
|
|
15
|
+
*/
|
|
16
|
+
type TouchEventType = "touchstart" | "touchend";
|
|
17
|
+
/**
|
|
18
|
+
* Ref target type - supports single ref or array of refs
|
|
19
|
+
* Array accepts mixed element types (e.g., [buttonRef, divRef])
|
|
20
|
+
*/
|
|
21
|
+
type RefTarget<T extends HTMLElement = HTMLElement> = React.RefObject<T | null> | Array<React.RefObject<HTMLElement | null>>;
|
|
22
|
+
/**
|
|
23
|
+
* Options for useOnClickOutside hook
|
|
24
|
+
*/
|
|
25
|
+
interface UseOnClickOutsideOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Whether the event listener is enabled
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether to use event capture phase.
|
|
33
|
+
* When true, the handler is called before the event reaches the target element,
|
|
34
|
+
* making it immune to stopPropagation calls.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
capture?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Mouse event type to listen for
|
|
40
|
+
* @default 'mousedown'
|
|
41
|
+
*/
|
|
42
|
+
eventType?: MouseEventType;
|
|
43
|
+
/**
|
|
44
|
+
* Touch event type to listen for
|
|
45
|
+
* @default 'touchstart'
|
|
46
|
+
*/
|
|
47
|
+
touchEventType?: TouchEventType;
|
|
48
|
+
/**
|
|
49
|
+
* Whether to detect touch events (for mobile support)
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
detectTouch?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Array of refs to exclude from outside click detection.
|
|
55
|
+
* Clicks on these elements will not trigger the handler.
|
|
56
|
+
*/
|
|
57
|
+
excludeRefs?: Array<React.RefObject<HTMLElement | null>>;
|
|
58
|
+
/**
|
|
59
|
+
* Custom function to determine if a target should be excluded.
|
|
60
|
+
* Return true to ignore clicks on the target element.
|
|
61
|
+
* @param target - The clicked element
|
|
62
|
+
* @returns Whether to exclude this element from triggering the handler
|
|
63
|
+
*/
|
|
64
|
+
shouldExclude?: (target: Node) => boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The event target to attach listeners to
|
|
67
|
+
* @default document
|
|
68
|
+
*/
|
|
69
|
+
eventTarget?: Document | HTMLElement | Window | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Detects clicks outside of specified element(s) and calls the provided handler.
|
|
73
|
+
* Useful for closing modals, dropdowns, popovers, and similar UI components.
|
|
74
|
+
*
|
|
75
|
+
* @param ref - Single ref or array of refs to detect outside clicks for
|
|
76
|
+
* @param handler - Callback function called when a click outside is detected
|
|
77
|
+
* @param options - Configuration options for the event listener
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* // Basic usage - close modal on outside click
|
|
82
|
+
* function Modal({ isOpen, onClose }) {
|
|
83
|
+
* const modalRef = useRef<HTMLDivElement>(null);
|
|
84
|
+
*
|
|
85
|
+
* useOnClickOutside(modalRef, () => onClose(), { enabled: isOpen });
|
|
86
|
+
*
|
|
87
|
+
* if (!isOpen) return null;
|
|
88
|
+
*
|
|
89
|
+
* return (
|
|
90
|
+
* <div className="overlay">
|
|
91
|
+
* <div ref={modalRef} className="modal">
|
|
92
|
+
* Modal content
|
|
93
|
+
* </div>
|
|
94
|
+
* </div>
|
|
95
|
+
* );
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* // Multiple refs - button and dropdown menu
|
|
102
|
+
* function Dropdown() {
|
|
103
|
+
* const [isOpen, setIsOpen] = useState(false);
|
|
104
|
+
* const buttonRef = useRef<HTMLButtonElement>(null);
|
|
105
|
+
* const menuRef = useRef<HTMLDivElement>(null);
|
|
106
|
+
*
|
|
107
|
+
* useOnClickOutside(
|
|
108
|
+
* [buttonRef, menuRef],
|
|
109
|
+
* () => setIsOpen(false),
|
|
110
|
+
* { enabled: isOpen }
|
|
111
|
+
* );
|
|
112
|
+
*
|
|
113
|
+
* return (
|
|
114
|
+
* <>
|
|
115
|
+
* <button ref={buttonRef} onClick={() => setIsOpen(!isOpen)}>
|
|
116
|
+
* Toggle
|
|
117
|
+
* </button>
|
|
118
|
+
* {isOpen && (
|
|
119
|
+
* <div ref={menuRef}>Dropdown content</div>
|
|
120
|
+
* )}
|
|
121
|
+
* </>
|
|
122
|
+
* );
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```tsx
|
|
128
|
+
* // With exclude refs - ignore specific elements
|
|
129
|
+
* function ModalWithPortal({ isOpen, onClose }) {
|
|
130
|
+
* const modalRef = useRef<HTMLDivElement>(null);
|
|
131
|
+
* const toastRef = useRef<HTMLDivElement>(null);
|
|
132
|
+
*
|
|
133
|
+
* useOnClickOutside(modalRef, onClose, {
|
|
134
|
+
* enabled: isOpen,
|
|
135
|
+
* excludeRefs: [toastRef], // Clicks on toast won't close modal
|
|
136
|
+
* });
|
|
137
|
+
*
|
|
138
|
+
* return (
|
|
139
|
+
* <>
|
|
140
|
+
* {isOpen && <div ref={modalRef}>Modal</div>}
|
|
141
|
+
* <div ref={toastRef}>Toast notification</div>
|
|
142
|
+
* </>
|
|
143
|
+
* );
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```tsx
|
|
149
|
+
* // With custom exclude function
|
|
150
|
+
* useOnClickOutside(ref, handleClose, {
|
|
151
|
+
* shouldExclude: (target) => {
|
|
152
|
+
* // Ignore clicks on elements with specific class
|
|
153
|
+
* return (target as Element).closest?.('.ignore-outside-click') !== null;
|
|
154
|
+
* },
|
|
155
|
+
* });
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
declare function useOnClickOutside<T extends HTMLElement = HTMLElement>(ref: RefTarget<T>, handler: OnClickOutsideHandler, options?: UseOnClickOutsideOptions): void;
|
|
159
|
+
|
|
160
|
+
export { type ClickOutsideEvent, type MouseEventType, type OnClickOutsideHandler, type RefTarget, type TouchEventType, type UseOnClickOutsideOptions, useOnClickOutside };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
useOnClickOutside: () => useOnClickOutside
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/useOnClickOutside.ts
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
function normalizeRefs(ref) {
|
|
30
|
+
return Array.isArray(ref) ? ref : [ref];
|
|
31
|
+
}
|
|
32
|
+
function isClickOutside(event, refs, excludeRefs, shouldExclude) {
|
|
33
|
+
const target = event.target;
|
|
34
|
+
if (!target || !target.isConnected) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (shouldExclude?.(target)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
for (const excludeRef of excludeRefs) {
|
|
41
|
+
if (excludeRef.current?.contains(target)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const ref of refs) {
|
|
46
|
+
if (ref.current?.contains(target)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
function useOnClickOutside(ref, handler, options = {}) {
|
|
53
|
+
const {
|
|
54
|
+
enabled = true,
|
|
55
|
+
capture = true,
|
|
56
|
+
eventType = "mousedown",
|
|
57
|
+
touchEventType = "touchstart",
|
|
58
|
+
detectTouch = true,
|
|
59
|
+
excludeRefs = [],
|
|
60
|
+
shouldExclude,
|
|
61
|
+
eventTarget
|
|
62
|
+
} = options;
|
|
63
|
+
const handlerRef = (0, import_react.useRef)(handler);
|
|
64
|
+
const shouldExcludeRef = (0, import_react.useRef)(shouldExclude);
|
|
65
|
+
const excludeRefsRef = (0, import_react.useRef)(excludeRefs);
|
|
66
|
+
const refRef = (0, import_react.useRef)(ref);
|
|
67
|
+
handlerRef.current = handler;
|
|
68
|
+
shouldExcludeRef.current = shouldExclude;
|
|
69
|
+
excludeRefsRef.current = excludeRefs;
|
|
70
|
+
refRef.current = ref;
|
|
71
|
+
(0, import_react.useEffect)(() => {
|
|
72
|
+
if (typeof document === "undefined") {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!enabled) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const normalizedRefs = normalizeRefs(refRef.current);
|
|
79
|
+
const target = eventTarget ?? document;
|
|
80
|
+
const handleMouseEvent = (event) => {
|
|
81
|
+
if (isClickOutside(
|
|
82
|
+
event,
|
|
83
|
+
normalizedRefs,
|
|
84
|
+
excludeRefsRef.current,
|
|
85
|
+
shouldExcludeRef.current
|
|
86
|
+
)) {
|
|
87
|
+
handlerRef.current(event);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const handleTouchEvent = (event) => {
|
|
91
|
+
if (isClickOutside(
|
|
92
|
+
event,
|
|
93
|
+
normalizedRefs,
|
|
94
|
+
excludeRefsRef.current,
|
|
95
|
+
shouldExcludeRef.current
|
|
96
|
+
)) {
|
|
97
|
+
handlerRef.current(event);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
target.addEventListener(eventType, handleMouseEvent, { capture });
|
|
101
|
+
if (detectTouch) {
|
|
102
|
+
target.addEventListener(touchEventType, handleTouchEvent, { capture });
|
|
103
|
+
}
|
|
104
|
+
return () => {
|
|
105
|
+
target.removeEventListener(eventType, handleMouseEvent, { capture });
|
|
106
|
+
if (detectTouch) {
|
|
107
|
+
target.removeEventListener(touchEventType, handleTouchEvent, {
|
|
108
|
+
capture
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}, [enabled, capture, eventType, touchEventType, detectTouch, eventTarget]);
|
|
113
|
+
}
|
|
114
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
115
|
+
0 && (module.exports = {
|
|
116
|
+
useOnClickOutside
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useOnClickOutside.ts"],"sourcesContent":["export { useOnClickOutside } from \"./useOnClickOutside\";\nexport type {\n UseOnClickOutsideOptions,\n OnClickOutsideHandler,\n ClickOutsideEvent,\n RefTarget,\n MouseEventType,\n TouchEventType,\n} from \"./useOnClickOutside\";\n","import { useEffect, useRef } from \"react\";\n\n/**\n * Event types for click outside detection (mouse + touch)\n */\nexport type ClickOutsideEvent = MouseEvent | TouchEvent;\n\n/**\n * Handler function type for click outside events\n */\nexport type OnClickOutsideHandler = (event: ClickOutsideEvent) => void;\n\n/**\n * Mouse event type options\n */\nexport type MouseEventType =\n | \"mousedown\"\n | \"mouseup\"\n | \"click\"\n | \"pointerdown\"\n | \"pointerup\";\n\n/**\n * Touch event type options\n */\nexport type TouchEventType = \"touchstart\" | \"touchend\";\n\n/**\n * Ref target type - supports single ref or array of refs\n * Array accepts mixed element types (e.g., [buttonRef, divRef])\n */\nexport type RefTarget<T extends HTMLElement = HTMLElement> =\n | React.RefObject<T | null>\n | Array<React.RefObject<HTMLElement | null>>;\n\n/**\n * Options for useOnClickOutside hook\n */\nexport interface UseOnClickOutsideOptions {\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 * When true, the handler is called before the event reaches the target element,\n * making it immune to stopPropagation calls.\n * @default true\n */\n capture?: boolean;\n\n /**\n * Mouse event type to listen for\n * @default 'mousedown'\n */\n eventType?: MouseEventType;\n\n /**\n * Touch event type to listen for\n * @default 'touchstart'\n */\n touchEventType?: TouchEventType;\n\n /**\n * Whether to detect touch events (for mobile support)\n * @default true\n */\n detectTouch?: boolean;\n\n /**\n * Array of refs to exclude from outside click detection.\n * Clicks on these elements will not trigger the handler.\n */\n excludeRefs?: Array<React.RefObject<HTMLElement | null>>;\n\n /**\n * Custom function to determine if a target should be excluded.\n * Return true to ignore clicks on the target element.\n * @param target - The clicked element\n * @returns Whether to exclude this element from triggering the handler\n */\n shouldExclude?: (target: Node) => boolean;\n\n /**\n * The event target to attach listeners to\n * @default document\n */\n eventTarget?: Document | HTMLElement | Window | null;\n}\n\n/**\n * Normalizes ref input to always return an array of refs\n */\nfunction normalizeRefs<T extends HTMLElement>(\n ref: RefTarget<T>\n): Array<React.RefObject<HTMLElement | null>> {\n return Array.isArray(ref) ? ref : [ref];\n}\n\n/**\n * Checks if a click event occurred outside of all specified elements\n */\nfunction isClickOutside(\n event: ClickOutsideEvent,\n refs: Array<React.RefObject<HTMLElement | null>>,\n excludeRefs: Array<React.RefObject<HTMLElement | null>>,\n shouldExclude?: (target: Node) => boolean\n): boolean {\n const target = event.target as Node;\n\n // Check if target exists in DOM\n if (!target || !target.isConnected) {\n return false;\n }\n\n // Check custom exclude function\n if (shouldExclude?.(target)) {\n return false;\n }\n\n // Check exclude refs\n for (const excludeRef of excludeRefs) {\n if (excludeRef.current?.contains(target)) {\n return false;\n }\n }\n\n // Check target refs - if clicked inside any of them, it's not an outside click\n for (const ref of refs) {\n if (ref.current?.contains(target)) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Detects clicks outside of specified element(s) and calls the provided handler.\n * Useful for closing modals, dropdowns, popovers, and similar UI components.\n *\n * @param ref - Single ref or array of refs to detect outside clicks for\n * @param handler - Callback function called when a click outside is detected\n * @param options - Configuration options for the event listener\n *\n * @example\n * ```tsx\n * // Basic usage - close modal on outside click\n * function Modal({ isOpen, onClose }) {\n * const modalRef = useRef<HTMLDivElement>(null);\n *\n * useOnClickOutside(modalRef, () => onClose(), { enabled: isOpen });\n *\n * if (!isOpen) return null;\n *\n * return (\n * <div className=\"overlay\">\n * <div ref={modalRef} className=\"modal\">\n * Modal content\n * </div>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Multiple refs - button and dropdown menu\n * function Dropdown() {\n * const [isOpen, setIsOpen] = useState(false);\n * const buttonRef = useRef<HTMLButtonElement>(null);\n * const menuRef = useRef<HTMLDivElement>(null);\n *\n * useOnClickOutside(\n * [buttonRef, menuRef],\n * () => setIsOpen(false),\n * { enabled: isOpen }\n * );\n *\n * return (\n * <>\n * <button ref={buttonRef} onClick={() => setIsOpen(!isOpen)}>\n * Toggle\n * </button>\n * {isOpen && (\n * <div ref={menuRef}>Dropdown content</div>\n * )}\n * </>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With exclude refs - ignore specific elements\n * function ModalWithPortal({ isOpen, onClose }) {\n * const modalRef = useRef<HTMLDivElement>(null);\n * const toastRef = useRef<HTMLDivElement>(null);\n *\n * useOnClickOutside(modalRef, onClose, {\n * enabled: isOpen,\n * excludeRefs: [toastRef], // Clicks on toast won't close modal\n * });\n *\n * return (\n * <>\n * {isOpen && <div ref={modalRef}>Modal</div>}\n * <div ref={toastRef}>Toast notification</div>\n * </>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With custom exclude function\n * useOnClickOutside(ref, handleClose, {\n * shouldExclude: (target) => {\n * // Ignore clicks on elements with specific class\n * return (target as Element).closest?.('.ignore-outside-click') !== null;\n * },\n * });\n * ```\n */\nexport function useOnClickOutside<T extends HTMLElement = HTMLElement>(\n ref: RefTarget<T>,\n handler: OnClickOutsideHandler,\n options: UseOnClickOutsideOptions = {}\n): void {\n const {\n enabled = true,\n capture = true,\n eventType = \"mousedown\",\n touchEventType = \"touchstart\",\n detectTouch = true,\n excludeRefs = [],\n shouldExclude,\n eventTarget,\n } = options;\n\n // Store handler in ref to avoid re-registering event listeners\n const handlerRef = useRef<OnClickOutsideHandler>(handler);\n\n // Store shouldExclude in ref to avoid re-registering event listeners\n const shouldExcludeRef = useRef(shouldExclude);\n\n // Store excludeRefs in ref to avoid re-registering event listeners\n const excludeRefsRef = useRef(excludeRefs);\n\n // Store ref in a ref to avoid re-registering when array is passed inline\n const refRef = useRef(ref);\n\n // Update refs when values change\n handlerRef.current = handler;\n shouldExcludeRef.current = shouldExclude;\n excludeRefsRef.current = excludeRefs;\n refRef.current = ref;\n\n useEffect(() => {\n // SSR check\n if (typeof document === \"undefined\") {\n return;\n }\n\n // Don't add listener if disabled\n if (!enabled) {\n return;\n }\n\n // Normalize refs to array (use refRef.current to get latest value)\n const normalizedRefs = normalizeRefs(refRef.current);\n\n // Get the event target (default to document)\n const target = eventTarget ?? document;\n\n // Internal handler for mouse events\n const handleMouseEvent = (event: Event) => {\n if (\n isClickOutside(\n event as MouseEvent,\n normalizedRefs,\n excludeRefsRef.current,\n shouldExcludeRef.current\n )\n ) {\n handlerRef.current(event as MouseEvent);\n }\n };\n\n // Internal handler for touch events\n const handleTouchEvent = (event: Event) => {\n if (\n isClickOutside(\n event as TouchEvent,\n normalizedRefs,\n excludeRefsRef.current,\n shouldExcludeRef.current\n )\n ) {\n handlerRef.current(event as TouchEvent);\n }\n };\n\n // Add event listeners\n target.addEventListener(eventType, handleMouseEvent, { capture });\n\n if (detectTouch) {\n target.addEventListener(touchEventType, handleTouchEvent, { capture });\n }\n\n // Cleanup\n return () => {\n target.removeEventListener(eventType, handleMouseEvent, { capture });\n\n if (detectTouch) {\n target.removeEventListener(touchEventType, handleTouchEvent, {\n capture,\n });\n }\n };\n }, [enabled, capture, eventType, touchEventType, detectTouch, eventTarget]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAkC;AA+FlC,SAAS,cACP,KAC4C;AAC5C,SAAO,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,GAAG;AACxC;AAKA,SAAS,eACP,OACA,MACA,aACA,eACS;AACT,QAAM,SAAS,MAAM;AAGrB,MAAI,CAAC,UAAU,CAAC,OAAO,aAAa;AAClC,WAAO;AAAA,EACT;AAGA,MAAI,gBAAgB,MAAM,GAAG;AAC3B,WAAO;AAAA,EACT;AAGA,aAAW,cAAc,aAAa;AACpC,QAAI,WAAW,SAAS,SAAS,MAAM,GAAG;AACxC,aAAO;AAAA,IACT;AAAA,EACF;AAGA,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,SAAS,SAAS,MAAM,GAAG;AACjC,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;AAyFO,SAAS,kBACd,KACA,SACA,UAAoC,CAAC,GAC/B;AACN,QAAM;AAAA,IACJ,UAAU;AAAA,IACV,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,cAAc;AAAA,IACd,cAAc,CAAC;AAAA,IACf;AAAA,IACA;AAAA,EACF,IAAI;AAGJ,QAAM,iBAAa,qBAA8B,OAAO;AAGxD,QAAM,uBAAmB,qBAAO,aAAa;AAG7C,QAAM,qBAAiB,qBAAO,WAAW;AAGzC,QAAM,aAAS,qBAAO,GAAG;AAGzB,aAAW,UAAU;AACrB,mBAAiB,UAAU;AAC3B,iBAAe,UAAU;AACzB,SAAO,UAAU;AAEjB,8BAAU,MAAM;AAEd,QAAI,OAAO,aAAa,aAAa;AACnC;AAAA,IACF;AAGA,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAGA,UAAM,iBAAiB,cAAc,OAAO,OAAO;AAGnD,UAAM,SAAS,eAAe;AAG9B,UAAM,mBAAmB,CAAC,UAAiB;AACzC,UACE;AAAA,QACE;AAAA,QACA;AAAA,QACA,eAAe;AAAA,QACf,iBAAiB;AAAA,MACnB,GACA;AACA,mBAAW,QAAQ,KAAmB;AAAA,MACxC;AAAA,IACF;AAGA,UAAM,mBAAmB,CAAC,UAAiB;AACzC,UACE;AAAA,QACE;AAAA,QACA;AAAA,QACA,eAAe;AAAA,QACf,iBAAiB;AAAA,MACnB,GACA;AACA,mBAAW,QAAQ,KAAmB;AAAA,MACxC;AAAA,IACF;AAGA,WAAO,iBAAiB,WAAW,kBAAkB,EAAE,QAAQ,CAAC;AAEhE,QAAI,aAAa;AACf,aAAO,iBAAiB,gBAAgB,kBAAkB,EAAE,QAAQ,CAAC;AAAA,IACvE;AAGA,WAAO,MAAM;AACX,aAAO,oBAAoB,WAAW,kBAAkB,EAAE,QAAQ,CAAC;AAEnE,UAAI,aAAa;AACf,eAAO,oBAAoB,gBAAgB,kBAAkB;AAAA,UAC3D;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,SAAS,WAAW,gBAAgB,aAAa,WAAW,CAAC;AAC5E;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/useOnClickOutside.ts
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
function normalizeRefs(ref) {
|
|
4
|
+
return Array.isArray(ref) ? ref : [ref];
|
|
5
|
+
}
|
|
6
|
+
function isClickOutside(event, refs, excludeRefs, shouldExclude) {
|
|
7
|
+
const target = event.target;
|
|
8
|
+
if (!target || !target.isConnected) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (shouldExclude?.(target)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
for (const excludeRef of excludeRefs) {
|
|
15
|
+
if (excludeRef.current?.contains(target)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const ref of refs) {
|
|
20
|
+
if (ref.current?.contains(target)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
function useOnClickOutside(ref, handler, options = {}) {
|
|
27
|
+
const {
|
|
28
|
+
enabled = true,
|
|
29
|
+
capture = true,
|
|
30
|
+
eventType = "mousedown",
|
|
31
|
+
touchEventType = "touchstart",
|
|
32
|
+
detectTouch = true,
|
|
33
|
+
excludeRefs = [],
|
|
34
|
+
shouldExclude,
|
|
35
|
+
eventTarget
|
|
36
|
+
} = options;
|
|
37
|
+
const handlerRef = useRef(handler);
|
|
38
|
+
const shouldExcludeRef = useRef(shouldExclude);
|
|
39
|
+
const excludeRefsRef = useRef(excludeRefs);
|
|
40
|
+
const refRef = useRef(ref);
|
|
41
|
+
handlerRef.current = handler;
|
|
42
|
+
shouldExcludeRef.current = shouldExclude;
|
|
43
|
+
excludeRefsRef.current = excludeRefs;
|
|
44
|
+
refRef.current = ref;
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (typeof document === "undefined") {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!enabled) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const normalizedRefs = normalizeRefs(refRef.current);
|
|
53
|
+
const target = eventTarget ?? document;
|
|
54
|
+
const handleMouseEvent = (event) => {
|
|
55
|
+
if (isClickOutside(
|
|
56
|
+
event,
|
|
57
|
+
normalizedRefs,
|
|
58
|
+
excludeRefsRef.current,
|
|
59
|
+
shouldExcludeRef.current
|
|
60
|
+
)) {
|
|
61
|
+
handlerRef.current(event);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const handleTouchEvent = (event) => {
|
|
65
|
+
if (isClickOutside(
|
|
66
|
+
event,
|
|
67
|
+
normalizedRefs,
|
|
68
|
+
excludeRefsRef.current,
|
|
69
|
+
shouldExcludeRef.current
|
|
70
|
+
)) {
|
|
71
|
+
handlerRef.current(event);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
target.addEventListener(eventType, handleMouseEvent, { capture });
|
|
75
|
+
if (detectTouch) {
|
|
76
|
+
target.addEventListener(touchEventType, handleTouchEvent, { capture });
|
|
77
|
+
}
|
|
78
|
+
return () => {
|
|
79
|
+
target.removeEventListener(eventType, handleMouseEvent, { capture });
|
|
80
|
+
if (detectTouch) {
|
|
81
|
+
target.removeEventListener(touchEventType, handleTouchEvent, {
|
|
82
|
+
capture
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}, [enabled, capture, eventType, touchEventType, detectTouch, eventTarget]);
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
useOnClickOutside
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useOnClickOutside.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/**\n * Event types for click outside detection (mouse + touch)\n */\nexport type ClickOutsideEvent = MouseEvent | TouchEvent;\n\n/**\n * Handler function type for click outside events\n */\nexport type OnClickOutsideHandler = (event: ClickOutsideEvent) => void;\n\n/**\n * Mouse event type options\n */\nexport type MouseEventType =\n | \"mousedown\"\n | \"mouseup\"\n | \"click\"\n | \"pointerdown\"\n | \"pointerup\";\n\n/**\n * Touch event type options\n */\nexport type TouchEventType = \"touchstart\" | \"touchend\";\n\n/**\n * Ref target type - supports single ref or array of refs\n * Array accepts mixed element types (e.g., [buttonRef, divRef])\n */\nexport type RefTarget<T extends HTMLElement = HTMLElement> =\n | React.RefObject<T | null>\n | Array<React.RefObject<HTMLElement | null>>;\n\n/**\n * Options for useOnClickOutside hook\n */\nexport interface UseOnClickOutsideOptions {\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 * When true, the handler is called before the event reaches the target element,\n * making it immune to stopPropagation calls.\n * @default true\n */\n capture?: boolean;\n\n /**\n * Mouse event type to listen for\n * @default 'mousedown'\n */\n eventType?: MouseEventType;\n\n /**\n * Touch event type to listen for\n * @default 'touchstart'\n */\n touchEventType?: TouchEventType;\n\n /**\n * Whether to detect touch events (for mobile support)\n * @default true\n */\n detectTouch?: boolean;\n\n /**\n * Array of refs to exclude from outside click detection.\n * Clicks on these elements will not trigger the handler.\n */\n excludeRefs?: Array<React.RefObject<HTMLElement | null>>;\n\n /**\n * Custom function to determine if a target should be excluded.\n * Return true to ignore clicks on the target element.\n * @param target - The clicked element\n * @returns Whether to exclude this element from triggering the handler\n */\n shouldExclude?: (target: Node) => boolean;\n\n /**\n * The event target to attach listeners to\n * @default document\n */\n eventTarget?: Document | HTMLElement | Window | null;\n}\n\n/**\n * Normalizes ref input to always return an array of refs\n */\nfunction normalizeRefs<T extends HTMLElement>(\n ref: RefTarget<T>\n): Array<React.RefObject<HTMLElement | null>> {\n return Array.isArray(ref) ? ref : [ref];\n}\n\n/**\n * Checks if a click event occurred outside of all specified elements\n */\nfunction isClickOutside(\n event: ClickOutsideEvent,\n refs: Array<React.RefObject<HTMLElement | null>>,\n excludeRefs: Array<React.RefObject<HTMLElement | null>>,\n shouldExclude?: (target: Node) => boolean\n): boolean {\n const target = event.target as Node;\n\n // Check if target exists in DOM\n if (!target || !target.isConnected) {\n return false;\n }\n\n // Check custom exclude function\n if (shouldExclude?.(target)) {\n return false;\n }\n\n // Check exclude refs\n for (const excludeRef of excludeRefs) {\n if (excludeRef.current?.contains(target)) {\n return false;\n }\n }\n\n // Check target refs - if clicked inside any of them, it's not an outside click\n for (const ref of refs) {\n if (ref.current?.contains(target)) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Detects clicks outside of specified element(s) and calls the provided handler.\n * Useful for closing modals, dropdowns, popovers, and similar UI components.\n *\n * @param ref - Single ref or array of refs to detect outside clicks for\n * @param handler - Callback function called when a click outside is detected\n * @param options - Configuration options for the event listener\n *\n * @example\n * ```tsx\n * // Basic usage - close modal on outside click\n * function Modal({ isOpen, onClose }) {\n * const modalRef = useRef<HTMLDivElement>(null);\n *\n * useOnClickOutside(modalRef, () => onClose(), { enabled: isOpen });\n *\n * if (!isOpen) return null;\n *\n * return (\n * <div className=\"overlay\">\n * <div ref={modalRef} className=\"modal\">\n * Modal content\n * </div>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Multiple refs - button and dropdown menu\n * function Dropdown() {\n * const [isOpen, setIsOpen] = useState(false);\n * const buttonRef = useRef<HTMLButtonElement>(null);\n * const menuRef = useRef<HTMLDivElement>(null);\n *\n * useOnClickOutside(\n * [buttonRef, menuRef],\n * () => setIsOpen(false),\n * { enabled: isOpen }\n * );\n *\n * return (\n * <>\n * <button ref={buttonRef} onClick={() => setIsOpen(!isOpen)}>\n * Toggle\n * </button>\n * {isOpen && (\n * <div ref={menuRef}>Dropdown content</div>\n * )}\n * </>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With exclude refs - ignore specific elements\n * function ModalWithPortal({ isOpen, onClose }) {\n * const modalRef = useRef<HTMLDivElement>(null);\n * const toastRef = useRef<HTMLDivElement>(null);\n *\n * useOnClickOutside(modalRef, onClose, {\n * enabled: isOpen,\n * excludeRefs: [toastRef], // Clicks on toast won't close modal\n * });\n *\n * return (\n * <>\n * {isOpen && <div ref={modalRef}>Modal</div>}\n * <div ref={toastRef}>Toast notification</div>\n * </>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With custom exclude function\n * useOnClickOutside(ref, handleClose, {\n * shouldExclude: (target) => {\n * // Ignore clicks on elements with specific class\n * return (target as Element).closest?.('.ignore-outside-click') !== null;\n * },\n * });\n * ```\n */\nexport function useOnClickOutside<T extends HTMLElement = HTMLElement>(\n ref: RefTarget<T>,\n handler: OnClickOutsideHandler,\n options: UseOnClickOutsideOptions = {}\n): void {\n const {\n enabled = true,\n capture = true,\n eventType = \"mousedown\",\n touchEventType = \"touchstart\",\n detectTouch = true,\n excludeRefs = [],\n shouldExclude,\n eventTarget,\n } = options;\n\n // Store handler in ref to avoid re-registering event listeners\n const handlerRef = useRef<OnClickOutsideHandler>(handler);\n\n // Store shouldExclude in ref to avoid re-registering event listeners\n const shouldExcludeRef = useRef(shouldExclude);\n\n // Store excludeRefs in ref to avoid re-registering event listeners\n const excludeRefsRef = useRef(excludeRefs);\n\n // Store ref in a ref to avoid re-registering when array is passed inline\n const refRef = useRef(ref);\n\n // Update refs when values change\n handlerRef.current = handler;\n shouldExcludeRef.current = shouldExclude;\n excludeRefsRef.current = excludeRefs;\n refRef.current = ref;\n\n useEffect(() => {\n // SSR check\n if (typeof document === \"undefined\") {\n return;\n }\n\n // Don't add listener if disabled\n if (!enabled) {\n return;\n }\n\n // Normalize refs to array (use refRef.current to get latest value)\n const normalizedRefs = normalizeRefs(refRef.current);\n\n // Get the event target (default to document)\n const target = eventTarget ?? document;\n\n // Internal handler for mouse events\n const handleMouseEvent = (event: Event) => {\n if (\n isClickOutside(\n event as MouseEvent,\n normalizedRefs,\n excludeRefsRef.current,\n shouldExcludeRef.current\n )\n ) {\n handlerRef.current(event as MouseEvent);\n }\n };\n\n // Internal handler for touch events\n const handleTouchEvent = (event: Event) => {\n if (\n isClickOutside(\n event as TouchEvent,\n normalizedRefs,\n excludeRefsRef.current,\n shouldExcludeRef.current\n )\n ) {\n handlerRef.current(event as TouchEvent);\n }\n };\n\n // Add event listeners\n target.addEventListener(eventType, handleMouseEvent, { capture });\n\n if (detectTouch) {\n target.addEventListener(touchEventType, handleTouchEvent, { capture });\n }\n\n // Cleanup\n return () => {\n target.removeEventListener(eventType, handleMouseEvent, { capture });\n\n if (detectTouch) {\n target.removeEventListener(touchEventType, handleTouchEvent, {\n capture,\n });\n }\n };\n }, [enabled, capture, eventType, touchEventType, detectTouch, eventTarget]);\n}\n"],"mappings":";AAAA,SAAS,WAAW,cAAc;AA+FlC,SAAS,cACP,KAC4C;AAC5C,SAAO,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,GAAG;AACxC;AAKA,SAAS,eACP,OACA,MACA,aACA,eACS;AACT,QAAM,SAAS,MAAM;AAGrB,MAAI,CAAC,UAAU,CAAC,OAAO,aAAa;AAClC,WAAO;AAAA,EACT;AAGA,MAAI,gBAAgB,MAAM,GAAG;AAC3B,WAAO;AAAA,EACT;AAGA,aAAW,cAAc,aAAa;AACpC,QAAI,WAAW,SAAS,SAAS,MAAM,GAAG;AACxC,aAAO;AAAA,IACT;AAAA,EACF;AAGA,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,SAAS,SAAS,MAAM,GAAG;AACjC,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;AAyFO,SAAS,kBACd,KACA,SACA,UAAoC,CAAC,GAC/B;AACN,QAAM;AAAA,IACJ,UAAU;AAAA,IACV,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,cAAc;AAAA,IACd,cAAc,CAAC;AAAA,IACf;AAAA,IACA;AAAA,EACF,IAAI;AAGJ,QAAM,aAAa,OAA8B,OAAO;AAGxD,QAAM,mBAAmB,OAAO,aAAa;AAG7C,QAAM,iBAAiB,OAAO,WAAW;AAGzC,QAAM,SAAS,OAAO,GAAG;AAGzB,aAAW,UAAU;AACrB,mBAAiB,UAAU;AAC3B,iBAAe,UAAU;AACzB,SAAO,UAAU;AAEjB,YAAU,MAAM;AAEd,QAAI,OAAO,aAAa,aAAa;AACnC;AAAA,IACF;AAGA,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAGA,UAAM,iBAAiB,cAAc,OAAO,OAAO;AAGnD,UAAM,SAAS,eAAe;AAG9B,UAAM,mBAAmB,CAAC,UAAiB;AACzC,UACE;AAAA,QACE;AAAA,QACA;AAAA,QACA,eAAe;AAAA,QACf,iBAAiB;AAAA,MACnB,GACA;AACA,mBAAW,QAAQ,KAAmB;AAAA,MACxC;AAAA,IACF;AAGA,UAAM,mBAAmB,CAAC,UAAiB;AACzC,UACE;AAAA,QACE;AAAA,QACA;AAAA,QACA,eAAe;AAAA,QACf,iBAAiB;AAAA,MACnB,GACA;AACA,mBAAW,QAAQ,KAAmB;AAAA,MACxC;AAAA,IACF;AAGA,WAAO,iBAAiB,WAAW,kBAAkB,EAAE,QAAQ,CAAC;AAEhE,QAAI,aAAa;AACf,aAAO,iBAAiB,gBAAgB,kBAAkB,EAAE,QAAQ,CAAC;AAAA,IACvE;AAGA,WAAO,MAAM;AACX,aAAO,oBAAoB,WAAW,kBAAkB,EAAE,QAAQ,CAAC;AAEnE,UAAI,aAAa;AACf,eAAO,oBAAoB,gBAAgB,kBAAkB;AAAA,UAC3D;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,SAAS,WAAW,gBAAgB,aAAa,WAAW,CAAC;AAC5E;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usefy/use-on-click-outside",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A React hook for detecting clicks outside of specified elements",
|
|
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-on-click-outside"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react",
|
|
45
|
+
"hooks",
|
|
46
|
+
"click-outside",
|
|
47
|
+
"outside-click",
|
|
48
|
+
"modal",
|
|
49
|
+
"dropdown",
|
|
50
|
+
"event-listener",
|
|
51
|
+
"useOnClickOutside"
|
|
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
|
+
}
|