@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 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"]}
@@ -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 };
@@ -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"]}