react-dismiss 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 react-dismiss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # react-dismiss
2
+
3
+ A tiny React hook for dismissing UI on outside interaction or Escape.
4
+
5
+ Tiny, dependency-free, SSR-safe, StrictMode-safe, TypeScript-first. Ideal for dropdowns, popovers, modals, menus, and overlays.
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
+ Requires `react >= 18` (peer dependency).
22
+
23
+ ## Quick Start
24
+
25
+ ```tsx
26
+ import { useRef, useState } from "react";
27
+ import { useDismiss } from "react-dismiss";
28
+
29
+ function Dropdown() {
30
+ const [open, setOpen] = useState(false);
31
+ const ref = useRef<HTMLDivElement>(null);
32
+
33
+ useDismiss(ref, () => {
34
+ setOpen(false);
35
+ });
36
+
37
+ return (
38
+ <div ref={ref}>
39
+ <button onClick={() => setOpen((v) => !v)}>Menu</button>
40
+ {open && <div>Dropdown content</div>}
41
+ </div>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## How It Works
47
+
48
+ ```
49
+ Interaction inside element → ignore
50
+ Interaction outside element → onDismiss()
51
+ Escape key → onDismiss()
52
+ ```
53
+
54
+ - Outside detection uses `element.contains(event.target)` — descendants are considered inside. Safe when `ref.current === null`.
55
+ - Escape handling listens for `keydown` with `key === "Escape"`.
56
+ - Listeners are attached to `document` inside `useEffect` (no SSR side-effects).
57
+
58
+ ## API
59
+
60
+ ```ts
61
+ useDismiss(ref, onDismiss, options?)
62
+ ```
63
+
64
+ ### `ref`
65
+
66
+ `React.RefObject<T extends HTMLElement>` — the element considered "inside". Attach it to the root of the dismissible UI.
67
+
68
+ ### `onDismiss`
69
+
70
+ `() => void` — called on dismissal. Latest callback is always used (ref-stable). Errors are not swallowed.
71
+
72
+ ### `options`
73
+
74
+ | Option | Type | Default | Description |
75
+ | --------- | ---------------------------- | --------------- | ----------------------------------------- |
76
+ | `enabled` | `boolean` | `true` | Master toggle. `false` removes listeners. |
77
+ | `outside` | `boolean` | `true` | Enable outside interaction dismissal. |
78
+ | `escape` | `boolean` | `true` | Enable Escape key dismissal. |
79
+ | `event` | `"pointerdown"` \| `"click"` | `"pointerdown"` | DOM event for outside detection. |
80
+
81
+ All options are optional and can be toggled dynamically.
82
+
83
+ ## Options
84
+
85
+ ```tsx
86
+ // Disabled
87
+ useDismiss(ref, close, { enabled: false });
88
+
89
+ // Escape only
90
+ useDismiss(ref, close, { outside: false });
91
+
92
+ // Outside only
93
+ useDismiss(ref, close, { escape: false });
94
+
95
+ // Use click instead of pointerdown
96
+ useDismiss(ref, close, { event: "click" });
97
+ ```
98
+
99
+ ## Examples
100
+
101
+ ### Dropdown
102
+
103
+ ```tsx
104
+ const ref = useRef<HTMLDivElement>(null);
105
+ useDismiss(ref, () => setOpen(false));
106
+ ```
107
+
108
+ ### Modal
109
+
110
+ ```tsx
111
+ const ref = useRef<HTMLDivElement>(null);
112
+ useDismiss(ref, closeModal);
113
+ // Renders inside an overlay; Escape and outside click both close.
114
+ ```
115
+
116
+ ### Popover
117
+
118
+ ```tsx
119
+ const ref = useRef<HTMLDivElement>(null);
120
+ useDismiss(ref, () => setPopoverOpen(false));
121
+ ```
122
+
123
+ ### Search suggestions
124
+
125
+ ```tsx
126
+ const ref = useRef<HTMLDivElement>(null);
127
+ useDismiss(ref, () => setSuggestions([]));
128
+ ```
129
+
130
+ ### Escape-only (e.g., controlled modal where backdrop handles clicks)
131
+
132
+ ```tsx
133
+ useDismiss(ref, close, { outside: false });
134
+ ```
135
+
136
+ ### Click event (when pointerdown is too eager)
137
+
138
+ ```tsx
139
+ useDismiss(ref, close, { event: "click" });
140
+ ```
141
+
142
+ See `examples/basic.tsx` for runnable demos.
143
+
144
+ ## TypeScript
145
+
146
+ Fully typed, strict `no any`.
147
+
148
+ ```tsx
149
+ const divRef = useRef<HTMLDivElement>(null);
150
+ useDismiss(divRef, onDismiss); // infers HTMLDivElement
151
+
152
+ const btnRef = useRef<HTMLButtonElement>(null);
153
+ useDismiss(btnRef, onDismiss); // infers HTMLButtonElement
154
+ ```
155
+
156
+ ## SSR
157
+
158
+ Safe for Next.js, Remix, Vite SSR, React Router. No browser globals are accessed at import time; listeners are registered inside `useEffect` only on the client.
159
+
160
+ ```tsx
161
+ // This import never touches `document` at module init:
162
+ import { useDismiss } from "react-dismiss";
163
+ ```
164
+
165
+ ## Accessibility
166
+
167
+ > `react-dismiss` provides interaction detection, not complete accessibility behavior.
168
+
169
+ You still need to handle:
170
+
171
+ - Focus management / focus trapping for dialogs
172
+ - `aria-expanded`, `aria-controls`, correct `role`
173
+ - Keyboard navigation (arrow keys, tab order)
174
+ - Screen-reader announcements
175
+
176
+ Escape handling in this hook complements but does not replace those patterns. Pair it with proper ARIA:
177
+
178
+ ```tsx
179
+ <button aria-expanded={open} aria-controls="menu">
180
+ Menu
181
+ </button>
182
+ <div id="menu" role="menu">{/* ... */}</div>
183
+ ```
184
+
185
+ ## Browser Support
186
+
187
+ - Outside detection uses `PointerEvent` (`pointerdown`). All modern browsers support it (Chrome 55+, Firefox 59+, Safari 13+).
188
+ - Fallback: pass `{ event: "click" }` if you must target older browsers.
189
+ - Escape handling uses `keydown` + `key === "Escape"` (universally supported).
190
+
191
+ ## Bundle Size
192
+
193
+ - ~1.3 KB uncompressed (`dist/index.js`), < 1 KB minified + gzipped.
194
+ - Zero runtime dependencies.
195
+ - Tree-shakeable (`sideEffects: false`).
196
+
197
+ ## Portals
198
+
199
+ By default "inside" means descendants of `ref.current` (`element.contains`). Content rendered in a React portal outside that subtree is considered outside and will dismiss. If you need multiple "inside" roots, compose refs or lift the dismiss root to a common ancestor. A dedicated multi-root API is intentionally out of scope for v1.
200
+
201
+ ## Development
202
+
203
+ ```bash
204
+ npm install
205
+ npm run typecheck
206
+ npm run lint
207
+ npm run test:run
208
+ npm run build
209
+ ```
210
+
211
+ ## Testing
212
+
213
+ ```bash
214
+ npm run test # watch
215
+ npm run test:run # single run
216
+ ```
217
+
218
+ Tests cover outside/inside, Escape, other keys, null ref, enabled/outside/escape toggles, dynamic enable, event switching, stale closures, unmount cleanup, StrictMode, nested elements, multiple instances, and error propagation.
219
+
220
+ ## Building
221
+
222
+ ```bash
223
+ npm run build # tsup → dist/index.js, dist/index.cjs, dist/index.d.ts
224
+ npm pack --dry-run # inspect published contents
225
+ ```
226
+
227
+ ## Publishing
228
+
229
+ ```bash
230
+ npm run check # typecheck + lint + tests
231
+ npm run build
232
+ npm pack --dry-run # verify files: dist/, README.md, LICENSE
233
+ npm publish # or OIDC trusted publishing via GitHub Actions
234
+ ```
235
+
236
+ Never commit secrets. If using GitHub Actions publishing, prefer OIDC over long-lived tokens.
237
+
238
+ ## License
239
+
240
+ MIT — see [LICENSE](./LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/useDismiss.ts
6
+ function useDismiss(ref, onDismiss, options = {}) {
7
+ const {
8
+ enabled = true,
9
+ outside = true,
10
+ escape = true,
11
+ event = "pointerdown"
12
+ } = options;
13
+ const onDismissRef = react.useRef(onDismiss);
14
+ react.useEffect(() => {
15
+ onDismissRef.current = onDismiss;
16
+ }, [onDismiss]);
17
+ react.useEffect(() => {
18
+ if (!enabled) return;
19
+ if (!outside && !escape) return;
20
+ if (typeof document === "undefined") return;
21
+ const handleOutside = (e) => {
22
+ const el = ref.current;
23
+ if (!el) return;
24
+ const target = e.target;
25
+ if (!(target instanceof Node)) return;
26
+ if (el.contains(target)) return;
27
+ onDismissRef.current();
28
+ };
29
+ const handleKeyDown = (e) => {
30
+ if (e.key === "Escape") {
31
+ onDismissRef.current();
32
+ }
33
+ };
34
+ if (outside) {
35
+ document.addEventListener(event, handleOutside);
36
+ }
37
+ if (escape) {
38
+ document.addEventListener("keydown", handleKeyDown);
39
+ }
40
+ return () => {
41
+ if (outside) {
42
+ document.removeEventListener(event, handleOutside);
43
+ }
44
+ if (escape) {
45
+ document.removeEventListener("keydown", handleKeyDown);
46
+ }
47
+ };
48
+ }, [ref, enabled, outside, escape, event]);
49
+ }
50
+
51
+ exports.useDismiss = useDismiss;
52
+ //# sourceMappingURL=index.cjs.map
53
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useDismiss.ts"],"names":["useRef","useEffect"],"mappings":";;;;;AAGO,SAAS,UAAA,CACd,GAAA,EACA,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,GACV,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;AAErC,IAAA,MAAM,aAAA,GAAgB,CAAC,CAAA,KAAa;AAClC,MAAA,MAAM,KAAK,GAAA,CAAI,OAAA;AACf,MAAA,IAAI,CAAC,EAAA,EAAI;AACT,MAAA,MAAM,SAAS,CAAA,CAAE,MAAA;AACjB,MAAA,IAAI,EAAE,kBAAkB,IAAA,CAAA,EAAO;AAC/B,MAAA,IAAI,EAAA,CAAG,QAAA,CAAS,MAAM,CAAA,EAAG;AACzB,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,GAAG,CAAC,GAAA,EAAK,SAAS,OAAA,EAAS,MAAA,EAAQ,KAAK,CAAC,CAAA;AAC3C","file":"index.cjs","sourcesContent":["import { useEffect, useRef } from \"react\";\nimport type { UseDismissOptions } from \"./types.js\";\n\nexport function useDismiss<T extends HTMLElement>(\n ref: React.RefObject<T | null>,\n onDismiss: () => void,\n options: UseDismissOptions = {},\n): void {\n const {\n enabled = true,\n outside = true,\n escape = true,\n event = \"pointerdown\",\n } = options;\n\n const onDismissRef = useRef(onDismiss);\n useEffect(() => {\n onDismissRef.current = onDismiss;\n }, [onDismiss]);\n\n useEffect(() => {\n if (!enabled) return;\n if (!outside && !escape) return;\n\n // SSR guard: document is unavailable on server. Effect only runs on client,\n // but guard keeps logic safe if environment is non-DOM.\n if (typeof document === \"undefined\") return;\n\n const handleOutside = (e: Event) => {\n const el = ref.current;\n if (!el) return;\n const target = e.target;\n if (!(target instanceof Node)) return;\n if (el.contains(target)) return;\n onDismissRef.current();\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n onDismissRef.current();\n }\n };\n\n if (outside) {\n document.addEventListener(event, handleOutside);\n }\n if (escape) {\n document.addEventListener(\"keydown\", handleKeyDown);\n }\n\n return () => {\n if (outside) {\n document.removeEventListener(event, handleOutside);\n }\n if (escape) {\n document.removeEventListener(\"keydown\", handleKeyDown);\n }\n };\n }, [ref, enabled, outside, escape, event]);\n}\n"]}
@@ -0,0 +1,26 @@
1
+ interface UseDismissOptions {
2
+ /**
3
+ * Whether dismissal listeners are active.
4
+ * @default true
5
+ */
6
+ enabled?: boolean;
7
+ /**
8
+ * Whether outside interaction triggers dismissal.
9
+ * @default true
10
+ */
11
+ outside?: boolean;
12
+ /**
13
+ * Whether pressing Escape triggers dismissal.
14
+ * @default true
15
+ */
16
+ escape?: boolean;
17
+ /**
18
+ * DOM event used to detect outside interaction.
19
+ * @default "pointerdown"
20
+ */
21
+ event?: "pointerdown" | "click";
22
+ }
23
+
24
+ declare function useDismiss<T extends HTMLElement>(ref: React.RefObject<T | null>, onDismiss: () => void, options?: UseDismissOptions): void;
25
+
26
+ export { type UseDismissOptions, useDismiss };
@@ -0,0 +1,26 @@
1
+ interface UseDismissOptions {
2
+ /**
3
+ * Whether dismissal listeners are active.
4
+ * @default true
5
+ */
6
+ enabled?: boolean;
7
+ /**
8
+ * Whether outside interaction triggers dismissal.
9
+ * @default true
10
+ */
11
+ outside?: boolean;
12
+ /**
13
+ * Whether pressing Escape triggers dismissal.
14
+ * @default true
15
+ */
16
+ escape?: boolean;
17
+ /**
18
+ * DOM event used to detect outside interaction.
19
+ * @default "pointerdown"
20
+ */
21
+ event?: "pointerdown" | "click";
22
+ }
23
+
24
+ declare function useDismiss<T extends HTMLElement>(ref: React.RefObject<T | null>, onDismiss: () => void, options?: UseDismissOptions): void;
25
+
26
+ export { type UseDismissOptions, useDismiss };
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ import { useRef, useEffect } from 'react';
2
+
3
+ // src/useDismiss.ts
4
+ function useDismiss(ref, onDismiss, options = {}) {
5
+ const {
6
+ enabled = true,
7
+ outside = true,
8
+ escape = true,
9
+ event = "pointerdown"
10
+ } = options;
11
+ const onDismissRef = useRef(onDismiss);
12
+ useEffect(() => {
13
+ onDismissRef.current = onDismiss;
14
+ }, [onDismiss]);
15
+ useEffect(() => {
16
+ if (!enabled) return;
17
+ if (!outside && !escape) return;
18
+ if (typeof document === "undefined") return;
19
+ const handleOutside = (e) => {
20
+ const el = ref.current;
21
+ if (!el) return;
22
+ const target = e.target;
23
+ if (!(target instanceof Node)) return;
24
+ if (el.contains(target)) return;
25
+ onDismissRef.current();
26
+ };
27
+ const handleKeyDown = (e) => {
28
+ if (e.key === "Escape") {
29
+ onDismissRef.current();
30
+ }
31
+ };
32
+ if (outside) {
33
+ document.addEventListener(event, handleOutside);
34
+ }
35
+ if (escape) {
36
+ document.addEventListener("keydown", handleKeyDown);
37
+ }
38
+ return () => {
39
+ if (outside) {
40
+ document.removeEventListener(event, handleOutside);
41
+ }
42
+ if (escape) {
43
+ document.removeEventListener("keydown", handleKeyDown);
44
+ }
45
+ };
46
+ }, [ref, enabled, outside, escape, event]);
47
+ }
48
+
49
+ export { useDismiss };
50
+ //# sourceMappingURL=index.js.map
51
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useDismiss.ts"],"names":[],"mappings":";;;AAGO,SAAS,UAAA,CACd,GAAA,EACA,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,GACV,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;AAErC,IAAA,MAAM,aAAA,GAAgB,CAAC,CAAA,KAAa;AAClC,MAAA,MAAM,KAAK,GAAA,CAAI,OAAA;AACf,MAAA,IAAI,CAAC,EAAA,EAAI;AACT,MAAA,MAAM,SAAS,CAAA,CAAE,MAAA;AACjB,MAAA,IAAI,EAAE,kBAAkB,IAAA,CAAA,EAAO;AAC/B,MAAA,IAAI,EAAA,CAAG,QAAA,CAAS,MAAM,CAAA,EAAG;AACzB,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,GAAG,CAAC,GAAA,EAAK,SAAS,OAAA,EAAS,MAAA,EAAQ,KAAK,CAAC,CAAA;AAC3C","file":"index.js","sourcesContent":["import { useEffect, useRef } from \"react\";\nimport type { UseDismissOptions } from \"./types.js\";\n\nexport function useDismiss<T extends HTMLElement>(\n ref: React.RefObject<T | null>,\n onDismiss: () => void,\n options: UseDismissOptions = {},\n): void {\n const {\n enabled = true,\n outside = true,\n escape = true,\n event = \"pointerdown\",\n } = options;\n\n const onDismissRef = useRef(onDismiss);\n useEffect(() => {\n onDismissRef.current = onDismiss;\n }, [onDismiss]);\n\n useEffect(() => {\n if (!enabled) return;\n if (!outside && !escape) return;\n\n // SSR guard: document is unavailable on server. Effect only runs on client,\n // but guard keeps logic safe if environment is non-DOM.\n if (typeof document === \"undefined\") return;\n\n const handleOutside = (e: Event) => {\n const el = ref.current;\n if (!el) return;\n const target = e.target;\n if (!(target instanceof Node)) return;\n if (el.contains(target)) return;\n onDismissRef.current();\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n onDismissRef.current();\n }\n };\n\n if (outside) {\n document.addEventListener(event, handleOutside);\n }\n if (escape) {\n document.addEventListener(\"keydown\", handleKeyDown);\n }\n\n return () => {\n if (outside) {\n document.removeEventListener(event, handleOutside);\n }\n if (escape) {\n document.removeEventListener(\"keydown\", handleKeyDown);\n }\n };\n }, [ref, enabled, outside, escape, event]);\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "react-dismiss",
3
+ "version": "0.1.0",
4
+ "description": "A tiny React hook for dismissing UI when users interact outside or press Escape.",
5
+ "keywords": [
6
+ "react",
7
+ "react-hook",
8
+ "dismiss",
9
+ "click-outside",
10
+ "outside-click",
11
+ "popover",
12
+ "dropdown",
13
+ "modal",
14
+ "typescript"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/parasraju/react-dismiss.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/parasraju/react-dismiss/issues"
24
+ },
25
+ "homepage": "https://github.com/parasraju/react-dismiss#readme",
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs"
36
+ }
37
+ },
38
+ "main": "./dist/index.cjs",
39
+ "module": "./dist/index.js",
40
+ "types": "./dist/index.d.ts",
41
+ "sideEffects": false,
42
+ "type": "module",
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "test": "vitest",
46
+ "test:run": "vitest run",
47
+ "test:coverage": "vitest run --coverage",
48
+ "lint": "eslint .",
49
+ "format": "prettier --write .",
50
+ "format:check": "prettier --check .",
51
+ "typecheck": "tsc --noEmit",
52
+ "check": "npm run typecheck && npm run lint && npm run test:run",
53
+ "prepublishOnly": "npm run check && npm run build"
54
+ },
55
+ "peerDependencies": {
56
+ "react": ">=18"
57
+ },
58
+ "devDependencies": {
59
+ "@testing-library/react": "^16.0.1",
60
+ "@types/node": "^22.0.0",
61
+ "@types/react": "^18.3.12",
62
+ "@types/react-dom": "^18.3.1",
63
+ "eslint": "^9.18.0",
64
+ "@eslint/js": "^9.18.0",
65
+ "eslint-plugin-react-hooks": "^5.1.0",
66
+ "globals": "^15.14.0",
67
+ "jsdom": "^25.0.1",
68
+ "prettier": "^3.4.2",
69
+ "react": "^18.3.1",
70
+ "react-dom": "^18.3.1",
71
+ "tsup": "^8.3.6",
72
+ "typescript": "^5.7.3",
73
+ "typescript-eslint": "^8.19.0",
74
+ "vitest": "^3.1.1"
75
+ }
76
+ }