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 +21 -0
- package/README.md +240 -0
- package/dist/index.cjs +53 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|