React Higher Order Component that enables components to detect and handle clicks outside their DOM boundaries
npx @tessl/cli install tessl/npm-react-onclickoutside@6.13.00
# React OnClickOutside
1
2
React OnClickOutside is a Higher Order Component (HOC) that enables React components to detect and handle clicks that occur outside their DOM boundaries. It provides a clean, configurable solution for implementing dropdowns, modals, tooltips and other UI components that need to close when users click elsewhere.
3
4
## Package Information
5
6
- **Package Name**: react-onclickoutside
7
- **Package Type**: npm
8
- **Language**: JavaScript (with TypeScript support)
9
- **Installation**: `npm install react-onclickoutside`
10
11
## Core Imports
12
13
```javascript
14
import onClickOutside from "react-onclickoutside";
15
```
16
17
For CommonJS:
18
19
```javascript
20
const onClickOutside = require("react-onclickoutside").default;
21
```
22
23
Named import for constants:
24
25
```javascript
26
import onClickOutside, { IGNORE_CLASS_NAME } from "react-onclickoutside";
27
```
28
29
TypeScript imports with types:
30
31
```typescript
32
import onClickOutside, { IGNORE_CLASS_NAME } from "react-onclickoutside";
33
// Note: TypeScript types not officially exported, interfaces shown in this doc for reference
34
```
35
36
## Basic Usage
37
38
```javascript
39
import React, { Component } from "react";
40
import onClickOutside from "react-onclickoutside";
41
42
class Dropdown extends Component {
43
constructor(props) {
44
super(props);
45
this.state = { isOpen: false };
46
}
47
48
toggleDropdown = () => {
49
this.setState({ isOpen: !this.state.isOpen });
50
};
51
52
handleClickOutside = (event) => {
53
this.setState({ isOpen: false });
54
};
55
56
render() {
57
return (
58
<div>
59
<button onClick={this.toggleDropdown}>
60
Menu {this.state.isOpen ? "▲" : "▼"}
61
</button>
62
{this.state.isOpen && (
63
<ul>
64
<li>Option 1</li>
65
<li>Option 2</li>
66
<li>Option 3</li>
67
</ul>
68
)}
69
</div>
70
);
71
}
72
}
73
74
export default onClickOutside(Dropdown);
75
```
76
77
## Modern Functional Component Alternative
78
79
For modern React applications using hooks, you may not need this HOC. Here's a functional component approach:
80
81
```javascript
82
import React, { useEffect, useState, useRef } from "react";
83
84
function useClickOutside(handler) {
85
const ref = useRef(null);
86
const [listening, setListening] = useState(false);
87
88
useEffect(() => {
89
if (listening) return;
90
if (!ref.current) return;
91
92
setListening(true);
93
const clickHandler = (event) => {
94
if (!ref.current.contains(event.target)) {
95
handler(event);
96
}
97
};
98
99
['click', 'touchstart'].forEach((type) => {
100
document.addEventListener(type, clickHandler);
101
});
102
103
return () => {
104
['click', 'touchstart'].forEach((type) => {
105
document.removeEventListener(type, clickHandler);
106
});
107
setListening(false);
108
};
109
}, [handler, listening]);
110
111
return ref;
112
}
113
114
const Dropdown = () => {
115
const [isOpen, setIsOpen] = useState(false);
116
const dropdownRef = useClickOutside(() => setIsOpen(false));
117
118
return (
119
<div ref={dropdownRef}>
120
<button onClick={() => setIsOpen(!isOpen)}>
121
Menu {isOpen ? "▲" : "▼"}
122
</button>
123
{isOpen && (
124
<ul>
125
<li>Option 1</li>
126
<li>Option 2</li>
127
<li>Option 3</li>
128
</ul>
129
)}
130
</div>
131
);
132
};
133
```
134
135
## Architecture
136
137
React OnClickOutside uses a Higher Order Component pattern that wraps existing components without requiring significant code changes. Key components:
138
139
- **HOC Wrapper**: Creates enhanced component with event listening capabilities
140
- **Event Management**: Handles document-level event listeners with proper cleanup
141
- **DOM Traversal**: Uses efficient DOM node comparison to determine outside clicks
142
- **Configuration System**: Supports both props-based and config object customization
143
- **Lifecycle Integration**: Automatically manages event listeners through React lifecycle methods
144
145
## Capabilities
146
147
### Higher Order Component
148
149
The main HOC function that wraps React components to add outside click detection.
150
151
```typescript { .api }
152
/**
153
* Higher Order Component that adds outside click detection to React components
154
* @param WrappedComponent - React component class or functional component to wrap
155
* @param config - Optional configuration object
156
* @returns Enhanced React component with outside click functionality
157
*/
158
function onClickOutside<P = {}>(
159
WrappedComponent: React.ComponentType<P>,
160
config?: Config
161
): React.ComponentType<P & EnhancedProps>;
162
163
interface Config {
164
/** Function that returns the click handler for the component instance */
165
handleClickOutside?: (instance: any) => (event: Event) => void;
166
/** Default excludeScrollbar setting for all instances (default: false) */
167
excludeScrollbar?: boolean;
168
/** Function to determine which DOM node to use for outside click detection */
169
setClickOutsideRef?: () => (instance: any) => HTMLElement;
170
}
171
172
/** Props automatically added to wrapped components */
173
interface EnhancedProps {
174
/** Event types to listen for (default: ["mousedown", "touchstart"]) */
175
eventTypes?: string[] | string;
176
/** Whether to ignore clicks on the scrollbar (default: false) */
177
excludeScrollbar?: boolean;
178
/** CSS class name to ignore during outside click detection (default: "ignore-react-onclickoutside") */
179
outsideClickIgnoreClass?: string;
180
/** Whether to call preventDefault on outside click events (default: false) */
181
preventDefault?: boolean;
182
/** Whether to call stopPropagation on outside click events (default: false) */
183
stopPropagation?: boolean;
184
/** Whether to disable outside click detection initially (default: false) */
185
disableOnClickOutside?: boolean;
186
/** Optional click handler function passed as prop */
187
handleClickOutside?: (event: Event) => void;
188
/** Function to enable outside click detection */
189
enableOnClickOutside?: () => void;
190
/** Function to disable outside click detection */
191
disableOnClickOutside?: () => void;
192
}
193
194
/** Default props applied by the HOC */
195
const DEFAULT_PROPS = {
196
eventTypes: ['mousedown', 'touchstart'],
197
excludeScrollbar: false,
198
outsideClickIgnoreClass: 'ignore-react-onclickoutside',
199
preventDefault: false,
200
stopPropagation: false
201
};
202
```
203
204
**Usage with configuration:**
205
206
```javascript
207
import React, { Component } from "react";
208
import onClickOutside from "react-onclickoutside";
209
210
class MyComponent extends Component {
211
myClickOutsideHandler = (event) => {
212
console.log("Clicked outside!");
213
};
214
215
render() {
216
return <div>Content</div>;
217
}
218
}
219
220
const config = {
221
handleClickOutside: (instance) => instance.myClickOutsideHandler
222
};
223
224
export default onClickOutside(MyComponent, config);
225
```
226
227
### Event Type Configuration
228
229
Controls which DOM events trigger outside click detection.
230
231
```typescript { .api }
232
// From EnhancedProps interface above
233
eventTypes?: string[] | string; // default: ["mousedown", "touchstart"]
234
```
235
236
**Usage:**
237
238
```javascript
239
// Single event type
240
<WrappedComponent eventTypes="click" />
241
242
// Multiple event types
243
<WrappedComponent eventTypes={["click", "touchend"]} />
244
245
// Default is ["mousedown", "touchstart"]
246
```
247
248
### Scrollbar Exclusion
249
250
Controls whether clicks on the browser scrollbar should be ignored.
251
252
```typescript { .api }
253
// From EnhancedProps interface above
254
excludeScrollbar?: boolean; // default: false
255
```
256
257
**Usage:**
258
259
```javascript
260
<WrappedComponent excludeScrollbar={true} />
261
```
262
263
### Event Propagation Control
264
265
Controls event prevention and propagation behavior.
266
267
```typescript { .api }
268
// From EnhancedProps interface above
269
preventDefault?: boolean; // default: false
270
stopPropagation?: boolean; // default: false
271
```
272
273
**Usage:**
274
275
```javascript
276
<WrappedComponent
277
preventDefault={true}
278
stopPropagation={true}
279
/>
280
```
281
282
### Element Ignore Configuration
283
284
Controls which elements should be ignored during outside click detection.
285
286
```typescript { .api }
287
// From EnhancedProps interface above
288
outsideClickIgnoreClass?: string; // default: "ignore-react-onclickoutside"
289
290
/** Default CSS class name for elements to ignore */
291
export const IGNORE_CLASS_NAME: string = "ignore-react-onclickoutside";
292
```
293
294
**Usage:**
295
296
```javascript
297
import onClickOutside, { IGNORE_CLASS_NAME } from "react-onclickoutside";
298
299
// Use default ignore class
300
<div className={IGNORE_CLASS_NAME}>This won't trigger outside click</div>
301
302
// Use custom ignore class
303
<WrappedComponent outsideClickIgnoreClass="my-ignore-class" />
304
<div className="my-ignore-class">This won't trigger outside click</div>
305
```
306
307
### Manual Control
308
309
Methods for programmatically enabling/disabling outside click detection.
310
311
```typescript { .api }
312
// Methods available on the HOC wrapper component instance
313
interface WrapperInstance {
314
/** Returns reference to the wrapped component instance */
315
getInstance(): React.Component | any;
316
/** Explicitly enables outside click event listening */
317
enableOnClickOutside(): void;
318
/** Explicitly disables outside click event listening */
319
disableOnClickOutside(): void;
320
}
321
322
// Static method on the HOC wrapper component class
323
interface WrapperClass {
324
/** Returns the original wrapped component class */
325
getClass(): React.ComponentType;
326
}
327
```
328
329
**Usage - Props methods:**
330
331
```javascript
332
class Container extends Component {
333
handleToggle = () => {
334
if (this.dropdownRef) {
335
// Toggle outside click detection
336
if (this.outsideClickEnabled) {
337
this.dropdownRef.disableOnClickOutside();
338
} else {
339
this.dropdownRef.enableOnClickOutside();
340
}
341
this.outsideClickEnabled = !this.outsideClickEnabled;
342
}
343
};
344
345
render() {
346
return (
347
<div>
348
<button onClick={this.handleToggle}>Toggle Detection</button>
349
<WrappedDropdown
350
ref={ref => this.dropdownRef = ref}
351
disableOnClickOutside={true}
352
/>
353
</div>
354
);
355
}
356
}
357
```
358
359
**Usage - Access wrapped component:**
360
361
```javascript
362
class Container extends Component {
363
callWrappedMethod = () => {
364
if (this.wrapperRef) {
365
// Get reference to the actual wrapped component
366
const wrappedComponent = this.wrapperRef.getInstance();
367
// Call a method on the wrapped component
368
wrappedComponent.customMethod();
369
}
370
};
371
372
render() {
373
return (
374
<div>
375
<button onClick={this.callWrappedMethod}>Call Wrapped Method</button>
376
<WrappedComponent
377
ref={ref => this.wrapperRef = ref}
378
/>
379
</div>
380
);
381
}
382
}
383
```
384
385
### Initial Disable State
386
387
Controls whether outside click detection is initially disabled.
388
389
```typescript { .api }
390
// From EnhancedProps interface above
391
disableOnClickOutside?: boolean; // default: false
392
```
393
394
**Usage:**
395
396
```javascript
397
// Component starts with outside click detection disabled
398
<WrappedComponent disableOnClickOutside={true} />
399
```
400
401
### Required Handler Implementation
402
403
The wrapped component must implement a handler method for outside click events.
404
405
```typescript { .api }
406
// Required method on wrapped component (unless provided via config or props)
407
interface RequiredHandler {
408
/** Handler method called when outside click is detected */
409
handleClickOutside(event: Event): void;
410
}
411
412
// Alternative: handler can be provided via props
413
interface PropsHandler {
414
/** Handler function passed as prop */
415
handleClickOutside?: (event: Event) => void;
416
}
417
418
// Alternative: handler can be configured via config object
419
interface ConfigHandler {
420
/** Function that returns the click handler for the component instance */
421
handleClickOutside: (instance: any) => (event: Event) => void;
422
}
423
```
424
425
**Handler priority (first available method is used):**
426
1. Config `handleClickOutside` function result
427
2. Component prop `handleClickOutside` function
428
3. Component method `handleClickOutside`
429
430
If none are found, the HOC throws an error:
431
432
```javascript
433
// Error thrown when no handler is found
434
throw new Error(
435
`WrappedComponent: ${componentName} lacks a handleClickOutside(event) function for processing outside click events.`
436
);
437
```
438
439
## Error Handling
440
441
The HOC will throw errors in these situations:
442
443
- **Missing Handler**: When the wrapped component lacks a `handleClickOutside` method and no config handler is provided
444
- **Invalid Config Handler**: When the config `handleClickOutside` function doesn't return a function
445
446
```javascript
447
// Example error handling
448
try {
449
const WrappedComponent = onClickOutside(MyComponent);
450
} catch (error) {
451
console.error("HOC setup failed:", error.message);
452
}
453
```
454
455
## Browser Compatibility
456
457
- Requires `classList` property support (all modern browsers)
458
- IE11 requires classList polyfill for SVG elements
459
- Supports passive event listeners when available
460
- Handles both mouse and touch events for mobile compatibility
461
462
For IE11 SVG support, add this polyfill:
463
464
```javascript
465
if (!("classList" in SVGElement.prototype)) {
466
Object.defineProperty(SVGElement.prototype, "classList", {
467
get() {
468
return {
469
contains: className => {
470
return this.className.baseVal.split(" ").indexOf(className) !== -1;
471
}
472
};
473
}
474
});
475
}
476
```