@mshafiqyajid/react-modal 0.0.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 +103 -0
- package/dist/chunk-BFH4N5GR.js +66 -0
- package/dist/chunk-BFH4N5GR.js.map +1 -0
- package/dist/index.cjs +159 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +96 -0
- package/dist/index.js.map +1 -0
- package/dist/styled.cjs +209 -0
- package/dist/styled.cjs.map +1 -0
- package/dist/styled.d.cts +31 -0
- package/dist/styled.d.ts +31 -0
- package/dist/styled.js +148 -0
- package/dist/styled.js.map +1 -0
- package/dist/styles.css +247 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shafiq Yajid
|
|
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,103 @@
|
|
|
1
|
+
# @mshafiqyajid/react-modal
|
|
2
|
+
|
|
3
|
+
Headless modal hook and styled component for React. Accessible, focus-trapped, scroll-locked, animated, SSR-safe, fully typed.
|
|
4
|
+
|
|
5
|
+
**[Full docs →](https://docs.shafiqyajid.com/react/modal/)**
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @mshafiqyajid/react-modal
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Headless usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { useModal } from "@mshafiqyajid/react-modal";
|
|
17
|
+
|
|
18
|
+
function Example() {
|
|
19
|
+
const { isOpen, open, close, modalProps, overlayProps } = useModal();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<button onClick={open}>Open modal</button>
|
|
24
|
+
{isOpen && (
|
|
25
|
+
<div {...overlayProps}>
|
|
26
|
+
<div {...modalProps} aria-labelledby="title">
|
|
27
|
+
<h2 id="title">Hello</h2>
|
|
28
|
+
<button onClick={close}>Close</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Styled usage
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { ModalStyled } from "@mshafiqyajid/react-modal/styled";
|
|
41
|
+
import "@mshafiqyajid/react-modal/styles.css";
|
|
42
|
+
|
|
43
|
+
function Example() {
|
|
44
|
+
const [open, setOpen] = React.useState(false);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<button onClick={() => setOpen(true)}>Open</button>
|
|
49
|
+
<ModalStyled
|
|
50
|
+
isOpen={open}
|
|
51
|
+
onClose={() => setOpen(false)}
|
|
52
|
+
title="My Modal"
|
|
53
|
+
size="md"
|
|
54
|
+
variant="dialog"
|
|
55
|
+
>
|
|
56
|
+
<p>Modal content goes here.</p>
|
|
57
|
+
</ModalStyled>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Props
|
|
64
|
+
|
|
65
|
+
### `useModal(options?)`
|
|
66
|
+
|
|
67
|
+
| Option | Type | Default | Description |
|
|
68
|
+
|--------|------|---------|-------------|
|
|
69
|
+
| `defaultOpen` | `boolean` | `false` | Initial open state |
|
|
70
|
+
| `onOpen` | `() => void` | — | Called when modal opens |
|
|
71
|
+
| `onClose` | `() => void` | — | Called when modal closes |
|
|
72
|
+
| `closeOnEsc` | `boolean` | `true` | Close on Escape key |
|
|
73
|
+
|
|
74
|
+
Returns: `isOpen`, `open()`, `close()`, `toggle()`, `modalProps`, `overlayProps`
|
|
75
|
+
|
|
76
|
+
### `ModalStyled`
|
|
77
|
+
|
|
78
|
+
| Prop | Type | Default | Description |
|
|
79
|
+
|------|------|---------|-------------|
|
|
80
|
+
| `isOpen` | `boolean` | — | Controls visibility |
|
|
81
|
+
| `onClose` | `() => void` | — | Called when closed |
|
|
82
|
+
| `title` | `string` | — | Dialog title (sets aria-labelledby) |
|
|
83
|
+
| `children` | `ReactNode` | — | Dialog body content |
|
|
84
|
+
| `footer` | `ReactNode` | — | Optional footer content |
|
|
85
|
+
| `size` | `"sm" \| "md" \| "lg" \| "full"` | `"md"` | Dialog size |
|
|
86
|
+
| `variant` | `"dialog" \| "drawer-left" \| "drawer-right" \| "drawer-bottom"` | `"dialog"` | Display variant |
|
|
87
|
+
| `closeOnOverlayClick` | `boolean` | `true` | Close when clicking the backdrop |
|
|
88
|
+
| `closeOnEsc` | `boolean` | `true` | Close on Escape key |
|
|
89
|
+
| `showCloseButton` | `boolean` | `true` | Show the X close button in the header |
|
|
90
|
+
|
|
91
|
+
## Dark mode
|
|
92
|
+
|
|
93
|
+
Set `data-theme="dark"` on any ancestor element — no `prefers-color-scheme` queries are used.
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<div data-theme="dark">
|
|
97
|
+
<!-- ModalStyled picks up dark tokens automatically -->
|
|
98
|
+
</div>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/useFocusTrap.ts
|
|
4
|
+
var FOCUSABLE_SELECTORS = [
|
|
5
|
+
"a[href]",
|
|
6
|
+
"button:not([disabled])",
|
|
7
|
+
"input:not([disabled])",
|
|
8
|
+
"select:not([disabled])",
|
|
9
|
+
"textarea:not([disabled])",
|
|
10
|
+
"[tabindex]:not([tabindex='-1'])",
|
|
11
|
+
"details > summary"
|
|
12
|
+
].join(", ");
|
|
13
|
+
function getFocusableElements(container) {
|
|
14
|
+
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTORS)).filter(
|
|
15
|
+
(el) => !el.closest("[inert]") && getComputedStyle(el).display !== "none"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
function useFocusTrap() {
|
|
19
|
+
const containerRef = useRef(null);
|
|
20
|
+
const previousFocusRef = useRef(null);
|
|
21
|
+
const activate = useCallback((container) => {
|
|
22
|
+
containerRef.current = container;
|
|
23
|
+
previousFocusRef.current = document.activeElement;
|
|
24
|
+
const focusable = getFocusableElements(container);
|
|
25
|
+
const first = focusable[0];
|
|
26
|
+
if (first) {
|
|
27
|
+
first.focus();
|
|
28
|
+
} else {
|
|
29
|
+
container.focus();
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
const deactivate = useCallback(() => {
|
|
33
|
+
containerRef.current = null;
|
|
34
|
+
const prev = previousFocusRef.current;
|
|
35
|
+
if (prev && typeof prev.focus === "function") {
|
|
36
|
+
prev.focus();
|
|
37
|
+
}
|
|
38
|
+
previousFocusRef.current = null;
|
|
39
|
+
}, []);
|
|
40
|
+
const handleKeyDown = useCallback((e) => {
|
|
41
|
+
if (e.key !== "Tab" || !containerRef.current) return;
|
|
42
|
+
const focusable = getFocusableElements(containerRef.current);
|
|
43
|
+
if (focusable.length === 0) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const first = focusable[0];
|
|
48
|
+
const last = focusable[focusable.length - 1];
|
|
49
|
+
if (e.shiftKey) {
|
|
50
|
+
if (document.activeElement === first) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
last.focus();
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (document.activeElement === last) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
first.focus();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}, []);
|
|
61
|
+
return { activate, deactivate, handleKeyDown };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { useFocusTrap };
|
|
65
|
+
//# sourceMappingURL=chunk-BFH4N5GR.js.map
|
|
66
|
+
//# sourceMappingURL=chunk-BFH4N5GR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useFocusTrap.ts"],"names":[],"mappings":";;;AAEA,IAAM,mBAAA,GAAsB;AAAA,EAC1B,SAAA;AAAA,EACA,wBAAA;AAAA,EACA,uBAAA;AAAA,EACA,wBAAA;AAAA,EACA,0BAAA;AAAA,EACA,iCAAA;AAAA,EACA;AACF,CAAA,CAAE,KAAK,IAAI,CAAA;AAEX,SAAS,qBAAqB,SAAA,EAAuC;AACnE,EAAA,OAAO,MAAM,IAAA,CAAK,SAAA,CAAU,gBAAA,CAA8B,mBAAmB,CAAC,CAAA,CAAE,MAAA;AAAA,IAC9E,CAAC,EAAA,KAAO,CAAC,EAAA,CAAG,OAAA,CAAQ,SAAS,CAAA,IAAK,gBAAA,CAAiB,EAAE,CAAA,CAAE,OAAA,KAAY;AAAA,GACrE;AACF;AAQO,SAAS,YAAA,GAAmC;AACjD,EAAA,MAAM,YAAA,GAAe,OAA2B,IAAI,CAAA;AACpD,EAAA,MAAM,gBAAA,GAAmB,OAA2B,IAAI,CAAA;AAExD,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,CAAC,SAAA,KAA2B;AACvD,IAAA,YAAA,CAAa,OAAA,GAAU,SAAA;AACvB,IAAA,gBAAA,CAAiB,UAAU,QAAA,CAAS,aAAA;AAEpC,IAAA,MAAM,SAAA,GAAY,qBAAqB,SAAS,CAAA;AAChD,IAAA,MAAM,KAAA,GAAQ,UAAU,CAAC,CAAA;AACzB,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,KAAA,CAAM,KAAA,EAAM;AAAA,IACd,CAAA,MAAO;AACL,MAAA,SAAA,CAAU,KAAA,EAAM;AAAA,IAClB;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,UAAA,GAAa,YAAY,MAAM;AACnC,IAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AACvB,IAAA,MAAM,OAAO,gBAAA,CAAiB,OAAA;AAC9B,IAAA,IAAI,IAAA,IAAQ,OAAO,IAAA,CAAK,KAAA,KAAU,UAAA,EAAY;AAC5C,MAAA,IAAA,CAAK,KAAA,EAAM;AAAA,IACb;AACA,IAAA,gBAAA,CAAiB,OAAA,GAAU,IAAA;AAAA,EAC7B,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,aAAA,GAAgB,WAAA,CAAY,CAAC,CAAA,KAAqB;AACtD,IAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,KAAA,IAAS,CAAC,aAAa,OAAA,EAAS;AAE9C,IAAA,MAAM,SAAA,GAAY,oBAAA,CAAqB,YAAA,CAAa,OAAO,CAAA;AAC3D,IAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,MAAA,CAAA,CAAE,cAAA,EAAe;AACjB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,KAAA,GAAQ,UAAU,CAAC,CAAA;AACzB,IAAA,MAAM,IAAA,GAAO,SAAA,CAAU,SAAA,CAAU,MAAA,GAAS,CAAC,CAAA;AAE3C,IAAA,IAAI,EAAE,QAAA,EAAU;AACd,MAAA,IAAI,QAAA,CAAS,kBAAkB,KAAA,EAAO;AACpC,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,IAAA,CAAK,KAAA,EAAM;AAAA,MACb;AAAA,IACF,CAAA,MAAO;AACL,MAAA,IAAI,QAAA,CAAS,kBAAkB,IAAA,EAAM;AACnC,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,KAAA,CAAM,KAAA,EAAM;AAAA,MACd;AAAA,IACF;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,EAAE,QAAA,EAAU,UAAA,EAAY,aAAA,EAAc;AAC/C","file":"chunk-BFH4N5GR.js","sourcesContent":["import { useCallback, useRef } from \"react\";\n\nconst FOCUSABLE_SELECTORS = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n \"details > summary\",\n].join(\", \");\n\nfunction getFocusableElements(container: HTMLElement): HTMLElement[] {\n return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS)).filter(\n (el) => !el.closest(\"[inert]\") && getComputedStyle(el).display !== \"none\",\n );\n}\n\nexport interface UseFocusTrapResult {\n activate: (container: HTMLElement) => void;\n deactivate: () => void;\n handleKeyDown: (e: KeyboardEvent) => void;\n}\n\nexport function useFocusTrap(): UseFocusTrapResult {\n const containerRef = useRef<HTMLElement | null>(null);\n const previousFocusRef = useRef<HTMLElement | null>(null);\n\n const activate = useCallback((container: HTMLElement) => {\n containerRef.current = container;\n previousFocusRef.current = document.activeElement as HTMLElement | null;\n\n const focusable = getFocusableElements(container);\n const first = focusable[0];\n if (first) {\n first.focus();\n } else {\n container.focus();\n }\n }, []);\n\n const deactivate = useCallback(() => {\n containerRef.current = null;\n const prev = previousFocusRef.current;\n if (prev && typeof prev.focus === \"function\") {\n prev.focus();\n }\n previousFocusRef.current = null;\n }, []);\n\n const handleKeyDown = useCallback((e: KeyboardEvent) => {\n if (e.key !== \"Tab\" || !containerRef.current) return;\n\n const focusable = getFocusableElements(containerRef.current);\n if (focusable.length === 0) {\n e.preventDefault();\n return;\n }\n\n const first = focusable[0]!;\n const last = focusable[focusable.length - 1]!;\n\n if (e.shiftKey) {\n if (document.activeElement === first) {\n e.preventDefault();\n last.focus();\n }\n } else {\n if (document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n }, []);\n\n return { activate, deactivate, handleKeyDown };\n}\n"]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
// src/useModal.ts
|
|
6
|
+
var FOCUSABLE_SELECTORS = [
|
|
7
|
+
"a[href]",
|
|
8
|
+
"button:not([disabled])",
|
|
9
|
+
"input:not([disabled])",
|
|
10
|
+
"select:not([disabled])",
|
|
11
|
+
"textarea:not([disabled])",
|
|
12
|
+
"[tabindex]:not([tabindex='-1'])",
|
|
13
|
+
"details > summary"
|
|
14
|
+
].join(", ");
|
|
15
|
+
function getFocusableElements(container) {
|
|
16
|
+
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTORS)).filter(
|
|
17
|
+
(el) => !el.closest("[inert]") && getComputedStyle(el).display !== "none"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
function useFocusTrap() {
|
|
21
|
+
const containerRef = react.useRef(null);
|
|
22
|
+
const previousFocusRef = react.useRef(null);
|
|
23
|
+
const activate = react.useCallback((container) => {
|
|
24
|
+
containerRef.current = container;
|
|
25
|
+
previousFocusRef.current = document.activeElement;
|
|
26
|
+
const focusable = getFocusableElements(container);
|
|
27
|
+
const first = focusable[0];
|
|
28
|
+
if (first) {
|
|
29
|
+
first.focus();
|
|
30
|
+
} else {
|
|
31
|
+
container.focus();
|
|
32
|
+
}
|
|
33
|
+
}, []);
|
|
34
|
+
const deactivate = react.useCallback(() => {
|
|
35
|
+
containerRef.current = null;
|
|
36
|
+
const prev = previousFocusRef.current;
|
|
37
|
+
if (prev && typeof prev.focus === "function") {
|
|
38
|
+
prev.focus();
|
|
39
|
+
}
|
|
40
|
+
previousFocusRef.current = null;
|
|
41
|
+
}, []);
|
|
42
|
+
const handleKeyDown = react.useCallback((e) => {
|
|
43
|
+
if (e.key !== "Tab" || !containerRef.current) return;
|
|
44
|
+
const focusable = getFocusableElements(containerRef.current);
|
|
45
|
+
if (focusable.length === 0) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const first = focusable[0];
|
|
50
|
+
const last = focusable[focusable.length - 1];
|
|
51
|
+
if (e.shiftKey) {
|
|
52
|
+
if (document.activeElement === first) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
last.focus();
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
if (document.activeElement === last) {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
first.focus();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
63
|
+
return { activate, deactivate, handleKeyDown };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/useModal.ts
|
|
67
|
+
function useModal(options = {}) {
|
|
68
|
+
const { defaultOpen = false, onOpen, onClose, closeOnEsc = true } = options;
|
|
69
|
+
const [isOpen, setIsOpen] = react.useState(defaultOpen);
|
|
70
|
+
const { activate, deactivate, handleKeyDown } = useFocusTrap();
|
|
71
|
+
const modalRef = react.useRef(null);
|
|
72
|
+
const originalOverflowRef = react.useRef("");
|
|
73
|
+
const hasOpenedRef = react.useRef(defaultOpen);
|
|
74
|
+
const open = react.useCallback(() => {
|
|
75
|
+
setIsOpen(true);
|
|
76
|
+
onOpen?.();
|
|
77
|
+
}, [onOpen]);
|
|
78
|
+
const close = react.useCallback(() => {
|
|
79
|
+
setIsOpen(false);
|
|
80
|
+
onClose?.();
|
|
81
|
+
}, [onClose]);
|
|
82
|
+
const toggle = react.useCallback(() => {
|
|
83
|
+
setIsOpen((prev) => {
|
|
84
|
+
if (prev) {
|
|
85
|
+
onClose?.();
|
|
86
|
+
} else {
|
|
87
|
+
onOpen?.();
|
|
88
|
+
}
|
|
89
|
+
return !prev;
|
|
90
|
+
});
|
|
91
|
+
}, [onOpen, onClose]);
|
|
92
|
+
react.useEffect(() => {
|
|
93
|
+
if (isOpen) {
|
|
94
|
+
hasOpenedRef.current = true;
|
|
95
|
+
originalOverflowRef.current = document.body.style.overflow;
|
|
96
|
+
document.body.style.overflow = "hidden";
|
|
97
|
+
return () => {
|
|
98
|
+
document.body.style.overflow = originalOverflowRef.current;
|
|
99
|
+
};
|
|
100
|
+
} else if (hasOpenedRef.current) {
|
|
101
|
+
document.body.style.overflow = originalOverflowRef.current;
|
|
102
|
+
return () => {
|
|
103
|
+
document.body.style.overflow = originalOverflowRef.current;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}, [isOpen]);
|
|
107
|
+
react.useEffect(() => {
|
|
108
|
+
if (!isOpen) return;
|
|
109
|
+
const onKeyDown = (e) => {
|
|
110
|
+
if (closeOnEsc && e.key === "Escape") {
|
|
111
|
+
close();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
handleKeyDown(e);
|
|
115
|
+
};
|
|
116
|
+
document.addEventListener("keydown", onKeyDown);
|
|
117
|
+
return () => {
|
|
118
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
119
|
+
};
|
|
120
|
+
}, [isOpen, closeOnEsc, close, handleKeyDown]);
|
|
121
|
+
react.useEffect(() => {
|
|
122
|
+
if (isOpen && modalRef.current) {
|
|
123
|
+
activate(modalRef.current);
|
|
124
|
+
} else if (!isOpen) {
|
|
125
|
+
deactivate();
|
|
126
|
+
}
|
|
127
|
+
}, [isOpen, activate, deactivate]);
|
|
128
|
+
const setModalRef = react.useCallback(
|
|
129
|
+
(el) => {
|
|
130
|
+
modalRef.current = el;
|
|
131
|
+
if (el && isOpen) {
|
|
132
|
+
activate(el);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
[isOpen, activate]
|
|
136
|
+
);
|
|
137
|
+
const modalProps = {
|
|
138
|
+
role: "dialog",
|
|
139
|
+
"aria-modal": true,
|
|
140
|
+
tabIndex: -1,
|
|
141
|
+
ref: setModalRef
|
|
142
|
+
};
|
|
143
|
+
const overlayProps = {
|
|
144
|
+
"data-rmod-overlay": "true"
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
isOpen,
|
|
148
|
+
open,
|
|
149
|
+
close,
|
|
150
|
+
toggle,
|
|
151
|
+
modalProps,
|
|
152
|
+
overlayProps
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
exports.useFocusTrap = useFocusTrap;
|
|
157
|
+
exports.useModal = useModal;
|
|
158
|
+
//# sourceMappingURL=index.cjs.map
|
|
159
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useFocusTrap.ts","../src/useModal.ts"],"names":["useRef","useCallback","useState","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import { useCallback, useRef } from \"react\";\n\nconst FOCUSABLE_SELECTORS = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n \"details > summary\",\n].join(\", \");\n\nfunction getFocusableElements(container: HTMLElement): HTMLElement[] {\n return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS)).filter(\n (el) => !el.closest(\"[inert]\") && getComputedStyle(el).display !== \"none\",\n );\n}\n\nexport interface UseFocusTrapResult {\n activate: (container: HTMLElement) => void;\n deactivate: () => void;\n handleKeyDown: (e: KeyboardEvent) => void;\n}\n\nexport function useFocusTrap(): UseFocusTrapResult {\n const containerRef = useRef<HTMLElement | null>(null);\n const previousFocusRef = useRef<HTMLElement | null>(null);\n\n const activate = useCallback((container: HTMLElement) => {\n containerRef.current = container;\n previousFocusRef.current = document.activeElement as HTMLElement | null;\n\n const focusable = getFocusableElements(container);\n const first = focusable[0];\n if (first) {\n first.focus();\n } else {\n container.focus();\n }\n }, []);\n\n const deactivate = useCallback(() => {\n containerRef.current = null;\n const prev = previousFocusRef.current;\n if (prev && typeof prev.focus === \"function\") {\n prev.focus();\n }\n previousFocusRef.current = null;\n }, []);\n\n const handleKeyDown = useCallback((e: KeyboardEvent) => {\n if (e.key !== \"Tab\" || !containerRef.current) return;\n\n const focusable = getFocusableElements(containerRef.current);\n if (focusable.length === 0) {\n e.preventDefault();\n return;\n }\n\n const first = focusable[0]!;\n const last = focusable[focusable.length - 1]!;\n\n if (e.shiftKey) {\n if (document.activeElement === first) {\n e.preventDefault();\n last.focus();\n }\n } else {\n if (document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n }, []);\n\n return { activate, deactivate, handleKeyDown };\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useFocusTrap } from \"./useFocusTrap\";\n\nexport interface UseModalOptions {\n defaultOpen?: boolean;\n onOpen?: () => void;\n onClose?: () => void;\n closeOnEsc?: boolean;\n}\n\nexport interface ModalProps {\n role: \"dialog\";\n \"aria-modal\": true;\n tabIndex: -1;\n}\n\nexport interface OverlayProps {\n \"data-rmod-overlay\": string;\n}\n\nexport interface UseModalResult {\n isOpen: boolean;\n open: () => void;\n close: () => void;\n toggle: () => void;\n modalProps: ModalProps;\n overlayProps: OverlayProps;\n}\n\nexport function useModal(options: UseModalOptions = {}): UseModalResult {\n const { defaultOpen = false, onOpen, onClose, closeOnEsc = true } = options;\n\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const { activate, deactivate, handleKeyDown } = useFocusTrap();\n const modalRef = useRef<HTMLElement | null>(null);\n const originalOverflowRef = useRef<string>(\"\");\n const hasOpenedRef = useRef(defaultOpen);\n\n const open = useCallback(() => {\n setIsOpen(true);\n onOpen?.();\n }, [onOpen]);\n\n const close = useCallback(() => {\n setIsOpen(false);\n onClose?.();\n }, [onClose]);\n\n const toggle = useCallback(() => {\n setIsOpen((prev) => {\n if (prev) {\n onClose?.();\n } else {\n onOpen?.();\n }\n return !prev;\n });\n }, [onOpen, onClose]);\n\n useEffect(() => {\n if (isOpen) {\n hasOpenedRef.current = true;\n originalOverflowRef.current = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = originalOverflowRef.current;\n };\n } else if (hasOpenedRef.current) {\n document.body.style.overflow = originalOverflowRef.current;\n return () => {\n document.body.style.overflow = originalOverflowRef.current;\n };\n }\n }, [isOpen]);\n\n useEffect(() => {\n if (!isOpen) return;\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (closeOnEsc && e.key === \"Escape\") {\n close();\n return;\n }\n handleKeyDown(e);\n };\n\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [isOpen, closeOnEsc, close, handleKeyDown]);\n\n useEffect(() => {\n if (isOpen && modalRef.current) {\n activate(modalRef.current);\n } else if (!isOpen) {\n deactivate();\n }\n }, [isOpen, activate, deactivate]);\n\n const setModalRef = useCallback(\n (el: HTMLElement | null) => {\n modalRef.current = el;\n if (el && isOpen) {\n activate(el);\n }\n },\n [isOpen, activate],\n );\n\n const modalProps: ModalProps & { ref: (el: HTMLElement | null) => void } = {\n role: \"dialog\",\n \"aria-modal\": true,\n tabIndex: -1,\n ref: setModalRef,\n };\n\n const overlayProps: OverlayProps = {\n \"data-rmod-overlay\": \"true\",\n };\n\n return {\n isOpen,\n open,\n close,\n toggle,\n modalProps,\n overlayProps,\n };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface UseModalOptions {
|
|
2
|
+
defaultOpen?: boolean;
|
|
3
|
+
onOpen?: () => void;
|
|
4
|
+
onClose?: () => void;
|
|
5
|
+
closeOnEsc?: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface ModalProps {
|
|
8
|
+
role: "dialog";
|
|
9
|
+
"aria-modal": true;
|
|
10
|
+
tabIndex: -1;
|
|
11
|
+
}
|
|
12
|
+
interface OverlayProps {
|
|
13
|
+
"data-rmod-overlay": string;
|
|
14
|
+
}
|
|
15
|
+
interface UseModalResult {
|
|
16
|
+
isOpen: boolean;
|
|
17
|
+
open: () => void;
|
|
18
|
+
close: () => void;
|
|
19
|
+
toggle: () => void;
|
|
20
|
+
modalProps: ModalProps;
|
|
21
|
+
overlayProps: OverlayProps;
|
|
22
|
+
}
|
|
23
|
+
declare function useModal(options?: UseModalOptions): UseModalResult;
|
|
24
|
+
|
|
25
|
+
interface UseFocusTrapResult {
|
|
26
|
+
activate: (container: HTMLElement) => void;
|
|
27
|
+
deactivate: () => void;
|
|
28
|
+
handleKeyDown: (e: KeyboardEvent) => void;
|
|
29
|
+
}
|
|
30
|
+
declare function useFocusTrap(): UseFocusTrapResult;
|
|
31
|
+
|
|
32
|
+
export { type ModalProps, type OverlayProps, type UseFocusTrapResult, type UseModalOptions, type UseModalResult, useFocusTrap, useModal };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface UseModalOptions {
|
|
2
|
+
defaultOpen?: boolean;
|
|
3
|
+
onOpen?: () => void;
|
|
4
|
+
onClose?: () => void;
|
|
5
|
+
closeOnEsc?: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface ModalProps {
|
|
8
|
+
role: "dialog";
|
|
9
|
+
"aria-modal": true;
|
|
10
|
+
tabIndex: -1;
|
|
11
|
+
}
|
|
12
|
+
interface OverlayProps {
|
|
13
|
+
"data-rmod-overlay": string;
|
|
14
|
+
}
|
|
15
|
+
interface UseModalResult {
|
|
16
|
+
isOpen: boolean;
|
|
17
|
+
open: () => void;
|
|
18
|
+
close: () => void;
|
|
19
|
+
toggle: () => void;
|
|
20
|
+
modalProps: ModalProps;
|
|
21
|
+
overlayProps: OverlayProps;
|
|
22
|
+
}
|
|
23
|
+
declare function useModal(options?: UseModalOptions): UseModalResult;
|
|
24
|
+
|
|
25
|
+
interface UseFocusTrapResult {
|
|
26
|
+
activate: (container: HTMLElement) => void;
|
|
27
|
+
deactivate: () => void;
|
|
28
|
+
handleKeyDown: (e: KeyboardEvent) => void;
|
|
29
|
+
}
|
|
30
|
+
declare function useFocusTrap(): UseFocusTrapResult;
|
|
31
|
+
|
|
32
|
+
export { type ModalProps, type OverlayProps, type UseFocusTrapResult, type UseModalOptions, type UseModalResult, useFocusTrap, useModal };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useFocusTrap } from './chunk-BFH4N5GR.js';
|
|
2
|
+
export { useFocusTrap } from './chunk-BFH4N5GR.js';
|
|
3
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
function useModal(options = {}) {
|
|
6
|
+
const { defaultOpen = false, onOpen, onClose, closeOnEsc = true } = options;
|
|
7
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
8
|
+
const { activate, deactivate, handleKeyDown } = useFocusTrap();
|
|
9
|
+
const modalRef = useRef(null);
|
|
10
|
+
const originalOverflowRef = useRef("");
|
|
11
|
+
const hasOpenedRef = useRef(defaultOpen);
|
|
12
|
+
const open = useCallback(() => {
|
|
13
|
+
setIsOpen(true);
|
|
14
|
+
onOpen?.();
|
|
15
|
+
}, [onOpen]);
|
|
16
|
+
const close = useCallback(() => {
|
|
17
|
+
setIsOpen(false);
|
|
18
|
+
onClose?.();
|
|
19
|
+
}, [onClose]);
|
|
20
|
+
const toggle = useCallback(() => {
|
|
21
|
+
setIsOpen((prev) => {
|
|
22
|
+
if (prev) {
|
|
23
|
+
onClose?.();
|
|
24
|
+
} else {
|
|
25
|
+
onOpen?.();
|
|
26
|
+
}
|
|
27
|
+
return !prev;
|
|
28
|
+
});
|
|
29
|
+
}, [onOpen, onClose]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (isOpen) {
|
|
32
|
+
hasOpenedRef.current = true;
|
|
33
|
+
originalOverflowRef.current = document.body.style.overflow;
|
|
34
|
+
document.body.style.overflow = "hidden";
|
|
35
|
+
return () => {
|
|
36
|
+
document.body.style.overflow = originalOverflowRef.current;
|
|
37
|
+
};
|
|
38
|
+
} else if (hasOpenedRef.current) {
|
|
39
|
+
document.body.style.overflow = originalOverflowRef.current;
|
|
40
|
+
return () => {
|
|
41
|
+
document.body.style.overflow = originalOverflowRef.current;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}, [isOpen]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!isOpen) return;
|
|
47
|
+
const onKeyDown = (e) => {
|
|
48
|
+
if (closeOnEsc && e.key === "Escape") {
|
|
49
|
+
close();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
handleKeyDown(e);
|
|
53
|
+
};
|
|
54
|
+
document.addEventListener("keydown", onKeyDown);
|
|
55
|
+
return () => {
|
|
56
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
57
|
+
};
|
|
58
|
+
}, [isOpen, closeOnEsc, close, handleKeyDown]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (isOpen && modalRef.current) {
|
|
61
|
+
activate(modalRef.current);
|
|
62
|
+
} else if (!isOpen) {
|
|
63
|
+
deactivate();
|
|
64
|
+
}
|
|
65
|
+
}, [isOpen, activate, deactivate]);
|
|
66
|
+
const setModalRef = useCallback(
|
|
67
|
+
(el) => {
|
|
68
|
+
modalRef.current = el;
|
|
69
|
+
if (el && isOpen) {
|
|
70
|
+
activate(el);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[isOpen, activate]
|
|
74
|
+
);
|
|
75
|
+
const modalProps = {
|
|
76
|
+
role: "dialog",
|
|
77
|
+
"aria-modal": true,
|
|
78
|
+
tabIndex: -1,
|
|
79
|
+
ref: setModalRef
|
|
80
|
+
};
|
|
81
|
+
const overlayProps = {
|
|
82
|
+
"data-rmod-overlay": "true"
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
isOpen,
|
|
86
|
+
open,
|
|
87
|
+
close,
|
|
88
|
+
toggle,
|
|
89
|
+
modalProps,
|
|
90
|
+
overlayProps
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { useModal };
|
|
95
|
+
//# sourceMappingURL=index.js.map
|
|
96
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useModal.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useFocusTrap } from \"./useFocusTrap\";\n\nexport interface UseModalOptions {\n defaultOpen?: boolean;\n onOpen?: () => void;\n onClose?: () => void;\n closeOnEsc?: boolean;\n}\n\nexport interface ModalProps {\n role: \"dialog\";\n \"aria-modal\": true;\n tabIndex: -1;\n}\n\nexport interface OverlayProps {\n \"data-rmod-overlay\": string;\n}\n\nexport interface UseModalResult {\n isOpen: boolean;\n open: () => void;\n close: () => void;\n toggle: () => void;\n modalProps: ModalProps;\n overlayProps: OverlayProps;\n}\n\nexport function useModal(options: UseModalOptions = {}): UseModalResult {\n const { defaultOpen = false, onOpen, onClose, closeOnEsc = true } = options;\n\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const { activate, deactivate, handleKeyDown } = useFocusTrap();\n const modalRef = useRef<HTMLElement | null>(null);\n const originalOverflowRef = useRef<string>(\"\");\n const hasOpenedRef = useRef(defaultOpen);\n\n const open = useCallback(() => {\n setIsOpen(true);\n onOpen?.();\n }, [onOpen]);\n\n const close = useCallback(() => {\n setIsOpen(false);\n onClose?.();\n }, [onClose]);\n\n const toggle = useCallback(() => {\n setIsOpen((prev) => {\n if (prev) {\n onClose?.();\n } else {\n onOpen?.();\n }\n return !prev;\n });\n }, [onOpen, onClose]);\n\n useEffect(() => {\n if (isOpen) {\n hasOpenedRef.current = true;\n originalOverflowRef.current = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = originalOverflowRef.current;\n };\n } else if (hasOpenedRef.current) {\n document.body.style.overflow = originalOverflowRef.current;\n return () => {\n document.body.style.overflow = originalOverflowRef.current;\n };\n }\n }, [isOpen]);\n\n useEffect(() => {\n if (!isOpen) return;\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (closeOnEsc && e.key === \"Escape\") {\n close();\n return;\n }\n handleKeyDown(e);\n };\n\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [isOpen, closeOnEsc, close, handleKeyDown]);\n\n useEffect(() => {\n if (isOpen && modalRef.current) {\n activate(modalRef.current);\n } else if (!isOpen) {\n deactivate();\n }\n }, [isOpen, activate, deactivate]);\n\n const setModalRef = useCallback(\n (el: HTMLElement | null) => {\n modalRef.current = el;\n if (el && isOpen) {\n activate(el);\n }\n },\n [isOpen, activate],\n );\n\n const modalProps: ModalProps & { ref: (el: HTMLElement | null) => void } = {\n role: \"dialog\",\n \"aria-modal\": true,\n tabIndex: -1,\n ref: setModalRef,\n };\n\n const overlayProps: OverlayProps = {\n \"data-rmod-overlay\": \"true\",\n };\n\n return {\n isOpen,\n open,\n close,\n toggle,\n modalProps,\n overlayProps,\n };\n}\n"]}
|