react-dismiss 0.2.0 → 0.2.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 +126 -125
- package/dist/index.cjs +10 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/package.json +82 -82
package/README.md
CHANGED
|
@@ -1,125 +1,126 @@
|
|
|
1
|
-
# react-dismiss
|
|
2
|
-
|
|
3
|
-
A tiny React hook for dismissing UI when users interact outside or press Escape.
|
|
4
|
-
|
|
5
|
-
**[Live Demo on CodeSandbox](https://codesandbox.io/s/xzwc87)**
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install react-dismiss
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
pnpm add react-dismiss
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
yarn add react-dismiss
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Quick Start
|
|
22
|
-
|
|
23
|
-
```tsx
|
|
24
|
-
import { useRef, useState } from "react";
|
|
25
|
-
import { useDismiss } from "react-dismiss";
|
|
26
|
-
|
|
27
|
-
export function Dropdown() {
|
|
28
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
29
|
-
const menuRef = useRef<HTMLDivElement>(null);
|
|
30
|
-
|
|
31
|
-
useDismiss(menuRef, () => setIsOpen(false));
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div ref={menuRef} style={{ position: "relative", display: "inline-block" }}>
|
|
35
|
-
<button onClick={() => setIsOpen((prev) => !prev)}>Toggle Menu</button>
|
|
36
|
-
{isOpen && (
|
|
37
|
-
<ul
|
|
38
|
-
style={{
|
|
39
|
-
position: "absolute",
|
|
40
|
-
top: "100%",
|
|
41
|
-
left: 0,
|
|
42
|
-
background: "#fff",
|
|
43
|
-
border: "1px solid #ccc",
|
|
44
|
-
padding: "8px",
|
|
45
|
-
listStyle: "none",
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
<li>Profile</li>
|
|
49
|
-
<li>Settings</li>
|
|
50
|
-
<li>Log out</li>
|
|
51
|
-
</ul>
|
|
52
|
-
)}
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## API
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
useDismiss<T extends HTMLElement>(
|
|
62
|
-
ref: React.RefObject<T | null
|
|
63
|
-
onDismiss: () => void,
|
|
64
|
-
options?: UseDismissOptions
|
|
65
|
-
): void
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Arguments
|
|
69
|
-
|
|
70
|
-
- `ref`: `React.RefObject<T | null>` — React ref attached to the element considered "inside".
|
|
71
|
-
- `onDismiss`: `() => void` — Callback invoked when dismissal triggers. Ref-stable internally so inline callbacks do not re-bind listeners.
|
|
72
|
-
- `options`: `UseDismissOptions` (optional object):
|
|
73
|
-
- `enabled` (`boolean`, default: `true`): Master toggle to enable or disable dismissal listeners.
|
|
74
|
-
- `outside` (`boolean`, default: `true`): Whether outside pointer/click interactions trigger dismissal.
|
|
75
|
-
- `escape` (`boolean`, default: `true`): Whether pressing the `Escape` key triggers dismissal.
|
|
76
|
-
- `event` (`"pointerdown" | "click"`, default: `"pointerdown"`): DOM event used for outside interaction detection.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
- **
|
|
118
|
-
- **
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
- **
|
|
125
|
-
- **
|
|
1
|
+
# react-dismiss
|
|
2
|
+
|
|
3
|
+
A tiny React hook for dismissing UI when users interact outside or press Escape.
|
|
4
|
+
|
|
5
|
+
**[Live Demo on CodeSandbox](https://codesandbox.io/s/xzwc87)**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install react-dismiss
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add react-dismiss
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yarn add react-dismiss
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { useRef, useState } from "react";
|
|
25
|
+
import { useDismiss } from "react-dismiss";
|
|
26
|
+
|
|
27
|
+
export function Dropdown() {
|
|
28
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
29
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
useDismiss(menuRef, () => setIsOpen(false));
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div ref={menuRef} style={{ position: "relative", display: "inline-block" }}>
|
|
35
|
+
<button onClick={() => setIsOpen((prev) => !prev)}>Toggle Menu</button>
|
|
36
|
+
{isOpen && (
|
|
37
|
+
<ul
|
|
38
|
+
style={{
|
|
39
|
+
position: "absolute",
|
|
40
|
+
top: "100%",
|
|
41
|
+
left: 0,
|
|
42
|
+
background: "#fff",
|
|
43
|
+
border: "1px solid #ccc",
|
|
44
|
+
padding: "8px",
|
|
45
|
+
listStyle: "none",
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<li>Profile</li>
|
|
49
|
+
<li>Settings</li>
|
|
50
|
+
<li>Log out</li>
|
|
51
|
+
</ul>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
useDismiss<T extends HTMLElement>(
|
|
62
|
+
ref: React.RefObject<T | null> | React.RefObject<T | null>[],
|
|
63
|
+
onDismiss: () => void,
|
|
64
|
+
options?: UseDismissOptions
|
|
65
|
+
): void
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Arguments
|
|
69
|
+
|
|
70
|
+
- `ref`: `React.RefObject<T | null>` or an array of them — React ref(s) attached to the element(s) considered "inside". Dismissal runs only when the interaction is outside all of them.
|
|
71
|
+
- `onDismiss`: `() => void` — Callback invoked when dismissal triggers. Ref-stable internally so inline callbacks do not re-bind listeners.
|
|
72
|
+
- `options`: `UseDismissOptions` (optional object):
|
|
73
|
+
- `enabled` (`boolean`, default: `true`): Master toggle to enable or disable dismissal listeners.
|
|
74
|
+
- `outside` (`boolean`, default: `true`): Whether outside pointer/click interactions trigger dismissal.
|
|
75
|
+
- `escape` (`boolean`, default: `true`): Whether pressing the `Escape` key triggers dismissal.
|
|
76
|
+
- `event` (`"pointerdown" | "click"`, default: `"pointerdown"`): DOM event used for outside interaction detection.
|
|
77
|
+
- `ignore` (`React.RefObject<HTMLElement | null>` or an array of them): Refs whose clicks never trigger dismissal, e.g. a toggle button outside the dismissed element.
|
|
78
|
+
|
|
79
|
+
### Returns
|
|
80
|
+
|
|
81
|
+
`void`
|
|
82
|
+
|
|
83
|
+
### Multiple refs — dismiss only outside all of them
|
|
84
|
+
|
|
85
|
+
The first argument accepts one ref or several. Dismissal runs only when the event is outside all of them. A click inside any ref does not dismiss.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
useDismiss([menuRef, panelRef], () => setIsOpen(false));
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Ignore option — clicks on ignored refs never dismiss
|
|
92
|
+
|
|
93
|
+
`options.ignore` accepts one ref or several. Clicks on ignored refs never dismiss.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
useDismiss(menuRef, () => setIsOpen(false), {
|
|
97
|
+
ignore: toggleRef,
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## What It Handles
|
|
102
|
+
|
|
103
|
+
- Outside interactions (`pointerdown` or `click`) while ignoring clicks on descendants inside `ref`.
|
|
104
|
+
- Multiple refs: dismissal runs only when the interaction is outside all of them.
|
|
105
|
+
- Ignore option: clicks on ignored refs never dismiss.
|
|
106
|
+
- Escape key presses (`keydown` where `event.key === "Escape"`).
|
|
107
|
+
- Null or unmounted refs without throwing errors.
|
|
108
|
+
- Automatic event listener cleanup on component unmount or when options change.
|
|
109
|
+
- SSR safe: guards document access and attaches listeners only inside client-side `useEffect`.
|
|
110
|
+
|
|
111
|
+
## TypeScript
|
|
112
|
+
|
|
113
|
+
Ships with built-in TypeScript declarations and generics (`dist/index.d.ts`), no extra `@types` packages required.
|
|
114
|
+
|
|
115
|
+
## Why This One
|
|
116
|
+
|
|
117
|
+
- **Zero dependencies**: Only requires `react` (>= 18) as a peer dependency.
|
|
118
|
+
- **Tiny footprint**: ~1.3 KB uncompressed ESM/CJS build (< 1 KB minified + gzipped).
|
|
119
|
+
- **Ref-stable callback**: Passing inline callback functions will not re-bind document listeners on re-render.
|
|
120
|
+
- One hook, not a hook library — no need to install a whole utilities package for a single outside-click handler.
|
|
121
|
+
|
|
122
|
+
## Links
|
|
123
|
+
|
|
124
|
+
- **Live Demo**: [codesandbox.io/s/xzwc87](https://codesandbox.io/s/xzwc87)
|
|
125
|
+
- **npm**: [npmjs.com/package/react-dismiss](https://www.npmjs.com/package/react-dismiss)
|
|
126
|
+
- **GitHub**: [github.com/parasraju/react-dismiss](https://github.com/parasraju/react-dismiss)
|
package/dist/index.cjs
CHANGED
|
@@ -8,7 +8,8 @@ function useDismiss(ref, onDismiss, options = {}) {
|
|
|
8
8
|
enabled = true,
|
|
9
9
|
outside = true,
|
|
10
10
|
escape = true,
|
|
11
|
-
event = "pointerdown"
|
|
11
|
+
event = "pointerdown",
|
|
12
|
+
ignore
|
|
12
13
|
} = options;
|
|
13
14
|
const onDismissRef = react.useRef(onDismiss);
|
|
14
15
|
react.useEffect(() => {
|
|
@@ -18,12 +19,16 @@ function useDismiss(ref, onDismiss, options = {}) {
|
|
|
18
19
|
if (!enabled) return;
|
|
19
20
|
if (!outside && !escape) return;
|
|
20
21
|
if (typeof document === "undefined") return;
|
|
22
|
+
const refs = Array.isArray(ref) ? ref : [ref];
|
|
23
|
+
const ignoreRefs = ignore === void 0 ? [] : Array.isArray(ignore) ? ignore : [ignore];
|
|
21
24
|
const handleOutside = (e) => {
|
|
22
|
-
const el = ref.current;
|
|
23
|
-
if (!el) return;
|
|
24
25
|
const target = e.target;
|
|
25
26
|
if (!(target instanceof Node)) return;
|
|
26
|
-
|
|
27
|
+
const elements = refs.map((r) => r.current).filter((el) => el !== null);
|
|
28
|
+
if (elements.length === 0) return;
|
|
29
|
+
if (elements.some((el) => el.contains(target))) return;
|
|
30
|
+
const ignored = ignoreRefs.map((r) => r.current).filter((el) => el !== null);
|
|
31
|
+
if (ignored.some((el) => el.contains(target))) return;
|
|
27
32
|
onDismissRef.current();
|
|
28
33
|
};
|
|
29
34
|
const handleKeyDown = (e) => {
|
|
@@ -45,7 +50,7 @@ function useDismiss(ref, onDismiss, options = {}) {
|
|
|
45
50
|
document.removeEventListener("keydown", handleKeyDown);
|
|
46
51
|
}
|
|
47
52
|
};
|
|
48
|
-
}, [ref, enabled, outside, escape, event]);
|
|
53
|
+
}, [ref, ignore, enabled, outside, escape, event]);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
exports.useDismiss = useDismiss;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useDismiss.ts"],"names":["useRef","useEffect"],"mappings":";;;;;AAGO,SAAS,UAAA,CACd,GAAA,
|
|
1
|
+
{"version":3,"sources":["../src/useDismiss.ts"],"names":["useRef","useEffect"],"mappings":";;;;;AAGO,SAAS,UAAA,CACd,GAAA,EAGA,SAAA,EACA,OAAA,GAA6B,EAAC,EACxB;AACN,EAAA,MAAM;AAAA,IACJ,OAAA,GAAU,IAAA;AAAA,IACV,OAAA,GAAU,IAAA;AAAA,IACV,MAAA,GAAS,IAAA;AAAA,IACT,KAAA,GAAQ,aAAA;AAAA,IACR;AAAA,GACF,GAAI,OAAA;AAEJ,EAAA,MAAM,YAAA,GAAeA,aAAO,SAAS,CAAA;AACrC,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,YAAA,CAAa,OAAA,GAAU,SAAA;AAAA,EACzB,CAAA,EAAG,CAAC,SAAS,CAAC,CAAA;AAEd,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,MAAA,EAAQ;AAIzB,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AAKrC,IAAA,MAAM,OAAO,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,GAAI,GAAA,GAAM,CAAC,GAAG,CAAA;AAC5C,IAAA,MAAM,UAAA,GACJ,MAAA,KAAW,MAAA,GACP,EAAC,GACD,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAClB,MAAA,GACA,CAAC,MAAM,CAAA;AAEf,IAAA,MAAM,aAAA,GAAgB,CAAC,CAAA,KAAa;AAClC,MAAA,MAAM,SAAS,CAAA,CAAE,MAAA;AACjB,MAAA,IAAI,EAAE,kBAAkB,IAAA,CAAA,EAAO;AAG/B,MAAA,MAAM,QAAA,GAAW,IAAA,CACd,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAA,CACpB,MAAA,CAAO,CAAC,EAAA,KAAgB,EAAA,KAAO,IAAI,CAAA;AACtC,MAAA,IAAI,QAAA,CAAS,WAAW,CAAA,EAAG;AAE3B,MAAA,IAAI,QAAA,CAAS,KAAK,CAAC,EAAA,KAAO,GAAG,QAAA,CAAS,MAAM,CAAC,CAAA,EAAG;AAEhD,MAAA,MAAM,OAAA,GAAU,UAAA,CACb,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAA,CACpB,MAAA,CAAO,CAAC,EAAA,KAA0B,EAAA,KAAO,IAAI,CAAA;AAChD,MAAA,IAAI,OAAA,CAAQ,KAAK,CAAC,EAAA,KAAO,GAAG,QAAA,CAAS,MAAM,CAAC,CAAA,EAAG;AAC/C,MAAA,YAAA,CAAa,OAAA,EAAQ;AAAA,IACvB,CAAA;AAEA,IAAA,MAAM,aAAA,GAAgB,CAAC,CAAA,KAAqB;AAC1C,MAAA,IAAI,CAAA,CAAE,QAAQ,QAAA,EAAU;AACtB,QAAA,YAAA,CAAa,OAAA,EAAQ;AAAA,MACvB;AAAA,IACF,CAAA;AAEA,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,QAAA,CAAS,gBAAA,CAAiB,OAAO,aAAa,CAAA;AAAA,IAChD;AACA,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,QAAA,CAAS,gBAAA,CAAiB,WAAW,aAAa,CAAA;AAAA,IACpD;AAEA,IAAA,OAAO,MAAM;AACX,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,QAAA,CAAS,mBAAA,CAAoB,OAAO,aAAa,CAAA;AAAA,MACnD;AACA,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,QAAA,CAAS,mBAAA,CAAoB,WAAW,aAAa,CAAA;AAAA,MACvD;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,GAAA,EAAK,MAAA,EAAQ,SAAS,OAAA,EAAS,MAAA,EAAQ,KAAK,CAAC,CAAA;AACnD","file":"index.cjs","sourcesContent":["import { useEffect, useRef } from \"react\";\r\nimport type { UseDismissOptions } from \"./types.js\";\r\n\r\nexport function useDismiss<T extends HTMLElement>(\r\n ref:\r\n | React.RefObject<T | null>\r\n | React.RefObject<T | null>[],\r\n onDismiss: () => void,\r\n options: UseDismissOptions = {},\r\n): void {\r\n const {\r\n enabled = true,\r\n outside = true,\r\n escape = true,\r\n event = \"pointerdown\",\r\n ignore,\r\n } = options;\r\n\r\n const onDismissRef = useRef(onDismiss);\r\n useEffect(() => {\r\n onDismissRef.current = onDismiss;\r\n }, [onDismiss]);\r\n\r\n useEffect(() => {\r\n if (!enabled) return;\r\n if (!outside && !escape) return;\r\n\r\n // SSR guard: document is unavailable on server. Effect only runs on client,\r\n // but guard keeps logic safe if environment is non-DOM.\r\n if (typeof document === \"undefined\") return;\r\n\r\n // Normalized inside the effect so the dep array stays honest: callers\r\n // passing an inline array literal re-bind listeners on re-render, which\r\n // is correct; stable useRef objects do not.\r\n const refs = Array.isArray(ref) ? ref : [ref];\r\n const ignoreRefs =\r\n ignore === undefined\r\n ? []\r\n : Array.isArray(ignore)\r\n ? ignore\r\n : [ignore];\r\n\r\n const handleOutside = (e: Event) => {\r\n const target = e.target;\r\n if (!(target instanceof Node)) return;\r\n // Only mounted refs count. With no mounted ref, there is no \"inside\",\r\n // so nothing to dismiss from (same as the old single null-ref behavior).\r\n const elements = refs\r\n .map((r) => r.current)\r\n .filter((el): el is T => el !== null);\r\n if (elements.length === 0) return;\r\n // Inside any ref: not a dismissal.\r\n if (elements.some((el) => el.contains(target))) return;\r\n // Inside an ignored ref: never dismiss.\r\n const ignored = ignoreRefs\r\n .map((r) => r.current)\r\n .filter((el): el is HTMLElement => el !== null);\r\n if (ignored.some((el) => el.contains(target))) return;\r\n onDismissRef.current();\r\n };\r\n\r\n const handleKeyDown = (e: KeyboardEvent) => {\r\n if (e.key === \"Escape\") {\r\n onDismissRef.current();\r\n }\r\n };\r\n\r\n if (outside) {\r\n document.addEventListener(event, handleOutside);\r\n }\r\n if (escape) {\r\n document.addEventListener(\"keydown\", handleKeyDown);\r\n }\r\n\r\n return () => {\r\n if (outside) {\r\n document.removeEventListener(event, handleOutside);\r\n }\r\n if (escape) {\r\n document.removeEventListener(\"keydown\", handleKeyDown);\r\n }\r\n };\r\n }, [ref, ignore, enabled, outside, escape, event]);\r\n}\r\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -19,8 +19,13 @@ interface UseDismissOptions {
|
|
|
19
19
|
* @default "pointerdown"
|
|
20
20
|
*/
|
|
21
21
|
event?: "pointerdown" | "click";
|
|
22
|
+
/**
|
|
23
|
+
* Refs whose clicks never trigger dismissal. Accepts one ref or several.
|
|
24
|
+
* Useful for toggle buttons that live outside the dismissed element.
|
|
25
|
+
*/
|
|
26
|
+
ignore?: React.RefObject<HTMLElement | null> | React.RefObject<HTMLElement | null>[];
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
declare function useDismiss<T extends HTMLElement>(ref: React.RefObject<T | null
|
|
29
|
+
declare function useDismiss<T extends HTMLElement>(ref: React.RefObject<T | null> | React.RefObject<T | null>[], onDismiss: () => void, options?: UseDismissOptions): void;
|
|
25
30
|
|
|
26
31
|
export { type UseDismissOptions, useDismiss };
|
package/dist/index.d.ts
CHANGED
|
@@ -19,8 +19,13 @@ interface UseDismissOptions {
|
|
|
19
19
|
* @default "pointerdown"
|
|
20
20
|
*/
|
|
21
21
|
event?: "pointerdown" | "click";
|
|
22
|
+
/**
|
|
23
|
+
* Refs whose clicks never trigger dismissal. Accepts one ref or several.
|
|
24
|
+
* Useful for toggle buttons that live outside the dismissed element.
|
|
25
|
+
*/
|
|
26
|
+
ignore?: React.RefObject<HTMLElement | null> | React.RefObject<HTMLElement | null>[];
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
declare function useDismiss<T extends HTMLElement>(ref: React.RefObject<T | null
|
|
29
|
+
declare function useDismiss<T extends HTMLElement>(ref: React.RefObject<T | null> | React.RefObject<T | null>[], onDismiss: () => void, options?: UseDismissOptions): void;
|
|
25
30
|
|
|
26
31
|
export { type UseDismissOptions, useDismiss };
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@ function useDismiss(ref, onDismiss, options = {}) {
|
|
|
6
6
|
enabled = true,
|
|
7
7
|
outside = true,
|
|
8
8
|
escape = true,
|
|
9
|
-
event = "pointerdown"
|
|
9
|
+
event = "pointerdown",
|
|
10
|
+
ignore
|
|
10
11
|
} = options;
|
|
11
12
|
const onDismissRef = useRef(onDismiss);
|
|
12
13
|
useEffect(() => {
|
|
@@ -16,12 +17,16 @@ function useDismiss(ref, onDismiss, options = {}) {
|
|
|
16
17
|
if (!enabled) return;
|
|
17
18
|
if (!outside && !escape) return;
|
|
18
19
|
if (typeof document === "undefined") return;
|
|
20
|
+
const refs = Array.isArray(ref) ? ref : [ref];
|
|
21
|
+
const ignoreRefs = ignore === void 0 ? [] : Array.isArray(ignore) ? ignore : [ignore];
|
|
19
22
|
const handleOutside = (e) => {
|
|
20
|
-
const el = ref.current;
|
|
21
|
-
if (!el) return;
|
|
22
23
|
const target = e.target;
|
|
23
24
|
if (!(target instanceof Node)) return;
|
|
24
|
-
|
|
25
|
+
const elements = refs.map((r) => r.current).filter((el) => el !== null);
|
|
26
|
+
if (elements.length === 0) return;
|
|
27
|
+
if (elements.some((el) => el.contains(target))) return;
|
|
28
|
+
const ignored = ignoreRefs.map((r) => r.current).filter((el) => el !== null);
|
|
29
|
+
if (ignored.some((el) => el.contains(target))) return;
|
|
25
30
|
onDismissRef.current();
|
|
26
31
|
};
|
|
27
32
|
const handleKeyDown = (e) => {
|
|
@@ -43,7 +48,7 @@ function useDismiss(ref, onDismiss, options = {}) {
|
|
|
43
48
|
document.removeEventListener("keydown", handleKeyDown);
|
|
44
49
|
}
|
|
45
50
|
};
|
|
46
|
-
}, [ref, enabled, outside, escape, event]);
|
|
51
|
+
}, [ref, ignore, enabled, outside, escape, event]);
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
export { useDismiss };
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useDismiss.ts"],"names":[],"mappings":";;;AAGO,SAAS,UAAA,CACd,GAAA,
|
|
1
|
+
{"version":3,"sources":["../src/useDismiss.ts"],"names":[],"mappings":";;;AAGO,SAAS,UAAA,CACd,GAAA,EAGA,SAAA,EACA,OAAA,GAA6B,EAAC,EACxB;AACN,EAAA,MAAM;AAAA,IACJ,OAAA,GAAU,IAAA;AAAA,IACV,OAAA,GAAU,IAAA;AAAA,IACV,MAAA,GAAS,IAAA;AAAA,IACT,KAAA,GAAQ,aAAA;AAAA,IACR;AAAA,GACF,GAAI,OAAA;AAEJ,EAAA,MAAM,YAAA,GAAe,OAAO,SAAS,CAAA;AACrC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,YAAA,CAAa,OAAA,GAAU,SAAA;AAAA,EACzB,CAAA,EAAG,CAAC,SAAS,CAAC,CAAA;AAEd,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,MAAA,EAAQ;AAIzB,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AAKrC,IAAA,MAAM,OAAO,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,GAAI,GAAA,GAAM,CAAC,GAAG,CAAA;AAC5C,IAAA,MAAM,UAAA,GACJ,MAAA,KAAW,MAAA,GACP,EAAC,GACD,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAClB,MAAA,GACA,CAAC,MAAM,CAAA;AAEf,IAAA,MAAM,aAAA,GAAgB,CAAC,CAAA,KAAa;AAClC,MAAA,MAAM,SAAS,CAAA,CAAE,MAAA;AACjB,MAAA,IAAI,EAAE,kBAAkB,IAAA,CAAA,EAAO;AAG/B,MAAA,MAAM,QAAA,GAAW,IAAA,CACd,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAA,CACpB,MAAA,CAAO,CAAC,EAAA,KAAgB,EAAA,KAAO,IAAI,CAAA;AACtC,MAAA,IAAI,QAAA,CAAS,WAAW,CAAA,EAAG;AAE3B,MAAA,IAAI,QAAA,CAAS,KAAK,CAAC,EAAA,KAAO,GAAG,QAAA,CAAS,MAAM,CAAC,CAAA,EAAG;AAEhD,MAAA,MAAM,OAAA,GAAU,UAAA,CACb,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAA,CACpB,MAAA,CAAO,CAAC,EAAA,KAA0B,EAAA,KAAO,IAAI,CAAA;AAChD,MAAA,IAAI,OAAA,CAAQ,KAAK,CAAC,EAAA,KAAO,GAAG,QAAA,CAAS,MAAM,CAAC,CAAA,EAAG;AAC/C,MAAA,YAAA,CAAa,OAAA,EAAQ;AAAA,IACvB,CAAA;AAEA,IAAA,MAAM,aAAA,GAAgB,CAAC,CAAA,KAAqB;AAC1C,MAAA,IAAI,CAAA,CAAE,QAAQ,QAAA,EAAU;AACtB,QAAA,YAAA,CAAa,OAAA,EAAQ;AAAA,MACvB;AAAA,IACF,CAAA;AAEA,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,QAAA,CAAS,gBAAA,CAAiB,OAAO,aAAa,CAAA;AAAA,IAChD;AACA,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,QAAA,CAAS,gBAAA,CAAiB,WAAW,aAAa,CAAA;AAAA,IACpD;AAEA,IAAA,OAAO,MAAM;AACX,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,QAAA,CAAS,mBAAA,CAAoB,OAAO,aAAa,CAAA;AAAA,MACnD;AACA,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,QAAA,CAAS,mBAAA,CAAoB,WAAW,aAAa,CAAA;AAAA,MACvD;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,GAAA,EAAK,MAAA,EAAQ,SAAS,OAAA,EAAS,MAAA,EAAQ,KAAK,CAAC,CAAA;AACnD","file":"index.js","sourcesContent":["import { useEffect, useRef } from \"react\";\r\nimport type { UseDismissOptions } from \"./types.js\";\r\n\r\nexport function useDismiss<T extends HTMLElement>(\r\n ref:\r\n | React.RefObject<T | null>\r\n | React.RefObject<T | null>[],\r\n onDismiss: () => void,\r\n options: UseDismissOptions = {},\r\n): void {\r\n const {\r\n enabled = true,\r\n outside = true,\r\n escape = true,\r\n event = \"pointerdown\",\r\n ignore,\r\n } = options;\r\n\r\n const onDismissRef = useRef(onDismiss);\r\n useEffect(() => {\r\n onDismissRef.current = onDismiss;\r\n }, [onDismiss]);\r\n\r\n useEffect(() => {\r\n if (!enabled) return;\r\n if (!outside && !escape) return;\r\n\r\n // SSR guard: document is unavailable on server. Effect only runs on client,\r\n // but guard keeps logic safe if environment is non-DOM.\r\n if (typeof document === \"undefined\") return;\r\n\r\n // Normalized inside the effect so the dep array stays honest: callers\r\n // passing an inline array literal re-bind listeners on re-render, which\r\n // is correct; stable useRef objects do not.\r\n const refs = Array.isArray(ref) ? ref : [ref];\r\n const ignoreRefs =\r\n ignore === undefined\r\n ? []\r\n : Array.isArray(ignore)\r\n ? ignore\r\n : [ignore];\r\n\r\n const handleOutside = (e: Event) => {\r\n const target = e.target;\r\n if (!(target instanceof Node)) return;\r\n // Only mounted refs count. With no mounted ref, there is no \"inside\",\r\n // so nothing to dismiss from (same as the old single null-ref behavior).\r\n const elements = refs\r\n .map((r) => r.current)\r\n .filter((el): el is T => el !== null);\r\n if (elements.length === 0) return;\r\n // Inside any ref: not a dismissal.\r\n if (elements.some((el) => el.contains(target))) return;\r\n // Inside an ignored ref: never dismiss.\r\n const ignored = ignoreRefs\r\n .map((r) => r.current)\r\n .filter((el): el is HTMLElement => el !== null);\r\n if (ignored.some((el) => el.contains(target))) return;\r\n onDismissRef.current();\r\n };\r\n\r\n const handleKeyDown = (e: KeyboardEvent) => {\r\n if (e.key === \"Escape\") {\r\n onDismissRef.current();\r\n }\r\n };\r\n\r\n if (outside) {\r\n document.addEventListener(event, handleOutside);\r\n }\r\n if (escape) {\r\n document.addEventListener(\"keydown\", handleKeyDown);\r\n }\r\n\r\n return () => {\r\n if (outside) {\r\n document.removeEventListener(event, handleOutside);\r\n }\r\n if (escape) {\r\n document.removeEventListener(\"keydown\", handleKeyDown);\r\n }\r\n };\r\n }, [ref, ignore, enabled, outside, escape, event]);\r\n}\r\n"]}
|
package/package.json
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "react-dismiss",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Tiny React hook that dismisses dropdowns, popovers and modals on outside click or Escape — multiple refs and an ignore option, zero dependencies.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"react",
|
|
7
|
-
"react-hook",
|
|
8
|
-
"dismiss",
|
|
9
|
-
"click-outside",
|
|
10
|
-
"outside-click",
|
|
11
|
-
"popover",
|
|
12
|
-
"dropdown",
|
|
13
|
-
"modal",
|
|
14
|
-
"typescript",
|
|
15
|
-
"click-away",
|
|
16
|
-
"useClickAway",
|
|
17
|
-
"react-hooks",
|
|
18
|
-
"useDismiss",
|
|
19
|
-
"useClickOutside",
|
|
20
|
-
"escape"
|
|
21
|
-
],
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"author": "",
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/parasraju/react-dismiss.git"
|
|
27
|
-
},
|
|
28
|
-
"bugs": {
|
|
29
|
-
"url": "https://github.com/parasraju/react-dismiss/issues"
|
|
30
|
-
},
|
|
31
|
-
"homepage": "https://github.com/parasraju/react-dismiss#readme",
|
|
32
|
-
"files": [
|
|
33
|
-
"dist",
|
|
34
|
-
"README.md",
|
|
35
|
-
"LICENSE"
|
|
36
|
-
],
|
|
37
|
-
"exports": {
|
|
38
|
-
".": {
|
|
39
|
-
"types": "./dist/index.d.ts",
|
|
40
|
-
"import": "./dist/index.js",
|
|
41
|
-
"require": "./dist/index.cjs"
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"main": "./dist/index.cjs",
|
|
45
|
-
"module": "./dist/index.js",
|
|
46
|
-
"types": "./dist/index.d.ts",
|
|
47
|
-
"sideEffects": false,
|
|
48
|
-
"type": "module",
|
|
49
|
-
"scripts": {
|
|
50
|
-
"build": "tsup",
|
|
51
|
-
"test": "vitest",
|
|
52
|
-
"test:run": "vitest run",
|
|
53
|
-
"test:coverage": "vitest run --coverage",
|
|
54
|
-
"lint": "eslint .",
|
|
55
|
-
"format": "prettier --write .",
|
|
56
|
-
"format:check": "prettier --check .",
|
|
57
|
-
"typecheck": "tsc --noEmit",
|
|
58
|
-
"check": "npm run typecheck && npm run lint && npm run test:run",
|
|
59
|
-
"prepublishOnly": "npm run check && npm run build"
|
|
60
|
-
},
|
|
61
|
-
"peerDependencies": {
|
|
62
|
-
"react": ">=18"
|
|
63
|
-
},
|
|
64
|
-
"devDependencies": {
|
|
65
|
-
"@testing-library/react": "^16.0.1",
|
|
66
|
-
"@types/node": "^22.0.0",
|
|
67
|
-
"@types/react": "^18.3.12",
|
|
68
|
-
"@types/react-dom": "^18.3.1",
|
|
69
|
-
"eslint": "^9.18.0",
|
|
70
|
-
"@eslint/js": "^9.18.0",
|
|
71
|
-
"eslint-plugin-react-hooks": "^5.1.0",
|
|
72
|
-
"globals": "^15.14.0",
|
|
73
|
-
"jsdom": "^25.0.1",
|
|
74
|
-
"prettier": "^3.4.2",
|
|
75
|
-
"react": "^18.3.1",
|
|
76
|
-
"react-dom": "^18.3.1",
|
|
77
|
-
"tsup": "^8.3.6",
|
|
78
|
-
"typescript": "^5.7.3",
|
|
79
|
-
"typescript-eslint": "^8.19.0",
|
|
80
|
-
"vitest": "^3.1.1"
|
|
81
|
-
}
|
|
82
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "react-dismiss",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Tiny React hook that dismisses dropdowns, popovers and modals on outside click or Escape — multiple refs and an ignore option, zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"react-hook",
|
|
8
|
+
"dismiss",
|
|
9
|
+
"click-outside",
|
|
10
|
+
"outside-click",
|
|
11
|
+
"popover",
|
|
12
|
+
"dropdown",
|
|
13
|
+
"modal",
|
|
14
|
+
"typescript",
|
|
15
|
+
"click-away",
|
|
16
|
+
"useClickAway",
|
|
17
|
+
"react-hooks",
|
|
18
|
+
"useDismiss",
|
|
19
|
+
"useClickOutside",
|
|
20
|
+
"escape"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/parasraju/react-dismiss.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/parasraju/react-dismiss/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/parasraju/react-dismiss#readme",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.js",
|
|
41
|
+
"require": "./dist/index.cjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"main": "./dist/index.cjs",
|
|
45
|
+
"module": "./dist/index.js",
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"type": "module",
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"test": "vitest",
|
|
52
|
+
"test:run": "vitest run",
|
|
53
|
+
"test:coverage": "vitest run --coverage",
|
|
54
|
+
"lint": "eslint .",
|
|
55
|
+
"format": "prettier --write .",
|
|
56
|
+
"format:check": "prettier --check .",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"check": "npm run typecheck && npm run lint && npm run test:run",
|
|
59
|
+
"prepublishOnly": "npm run check && npm run build"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"react": ">=18"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@testing-library/react": "^16.0.1",
|
|
66
|
+
"@types/node": "^22.0.0",
|
|
67
|
+
"@types/react": "^18.3.12",
|
|
68
|
+
"@types/react-dom": "^18.3.1",
|
|
69
|
+
"eslint": "^9.18.0",
|
|
70
|
+
"@eslint/js": "^9.18.0",
|
|
71
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
72
|
+
"globals": "^15.14.0",
|
|
73
|
+
"jsdom": "^25.0.1",
|
|
74
|
+
"prettier": "^3.4.2",
|
|
75
|
+
"react": "^18.3.1",
|
|
76
|
+
"react-dom": "^18.3.1",
|
|
77
|
+
"tsup": "^8.3.6",
|
|
78
|
+
"typescript": "^5.7.3",
|
|
79
|
+
"typescript-eslint": "^8.19.0",
|
|
80
|
+
"vitest": "^3.1.1"
|
|
81
|
+
}
|
|
82
|
+
}
|