@data-slot/dialog 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/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # @data-slot/dialog
2
+
3
+ Headless modal dialog component for vanilla JavaScript. Accessible, unstyled, tiny.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @data-slot/dialog
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```html
14
+ <div data-slot="dialog">
15
+ <button data-slot="dialog-trigger">Open Dialog</button>
16
+ <div data-slot="dialog-content" hidden>
17
+ <h2 data-slot="dialog-title">Dialog Title</h2>
18
+ <p data-slot="dialog-description">Dialog description text.</p>
19
+ <button data-slot="dialog-close">Close</button>
20
+ </div>
21
+ </div>
22
+
23
+ <script type="module">
24
+ import { create } from "@data-slot/dialog";
25
+
26
+ const controllers = create();
27
+ </script>
28
+ ```
29
+
30
+ ## API
31
+
32
+ ### `create(scope?)`
33
+
34
+ Auto-discover and bind all dialog instances in a scope (defaults to `document`).
35
+
36
+ ```typescript
37
+ import { create } from "@data-slot/dialog";
38
+
39
+ const controllers = create(); // Returns DialogController[]
40
+ ```
41
+
42
+ ### `createDialog(root, options?)`
43
+
44
+ Create a controller for a specific element.
45
+
46
+ ```typescript
47
+ import { createDialog } from "@data-slot/dialog";
48
+
49
+ const dialog = createDialog(element, {
50
+ defaultOpen: false,
51
+ closeOnClickOutside: true,
52
+ closeOnEscape: true,
53
+ lockScroll: true,
54
+ onOpenChange: (open) => console.log(open),
55
+ });
56
+ ```
57
+
58
+ ### Options
59
+
60
+ | Option | Type | Default | Description |
61
+ |--------|------|---------|-------------|
62
+ | `defaultOpen` | `boolean` | `false` | Initial open state |
63
+ | `closeOnClickOutside` | `boolean` | `true` | Close when clicking outside content |
64
+ | `closeOnEscape` | `boolean` | `true` | Close when pressing Escape |
65
+ | `lockScroll` | `boolean` | `true` | Lock body scroll when open |
66
+ | `onOpenChange` | `(open: boolean) => void` | `undefined` | Callback when open state changes |
67
+
68
+ ### Controller
69
+
70
+ | Method/Property | Description |
71
+ |-----------------|-------------|
72
+ | `open()` | Open the dialog |
73
+ | `close()` | Close the dialog |
74
+ | `toggle()` | Toggle the dialog |
75
+ | `isOpen` | Current open state (readonly `boolean`) |
76
+ | `destroy()` | Cleanup all event listeners |
77
+
78
+ ## Markup Structure
79
+
80
+ ```html
81
+ <div data-slot="dialog">
82
+ <button data-slot="dialog-trigger">Open</button>
83
+ <div data-slot="dialog-content" role="dialog">
84
+ <h2 data-slot="dialog-title">Title</h2>
85
+ <p data-slot="dialog-description">Description</p>
86
+ <button data-slot="dialog-close">Close</button>
87
+ </div>
88
+ </div>
89
+ ```
90
+
91
+ ### Required Slots
92
+
93
+ - `dialog-content` - The dialog panel (required)
94
+
95
+ ### Optional Slots
96
+
97
+ - `dialog-trigger` - Button to open the dialog
98
+ - `dialog-title` - Title for `aria-labelledby`
99
+ - `dialog-description` - Description for `aria-describedby`
100
+ - `dialog-close` - Button to close the dialog
101
+
102
+ ## Styling
103
+
104
+ Use `data-state` attributes for CSS styling:
105
+
106
+ ```css
107
+ /* Backdrop/overlay */
108
+ [data-slot="dialog-content"] {
109
+ position: fixed;
110
+ inset: 0;
111
+ display: grid;
112
+ place-items: center;
113
+ background: rgba(0, 0, 0, 0.5);
114
+ }
115
+
116
+ /* Closed state */
117
+ [data-slot="dialog"][data-state="closed"] [data-slot="dialog-content"] {
118
+ display: none;
119
+ }
120
+
121
+ /* Animation */
122
+ [data-slot="dialog-content"] {
123
+ opacity: 0;
124
+ transition: opacity 0.2s;
125
+ }
126
+
127
+ [data-slot="dialog"][data-state="open"] [data-slot="dialog-content"] {
128
+ opacity: 1;
129
+ }
130
+ ```
131
+
132
+ With Tailwind:
133
+
134
+ ```html
135
+ <div data-slot="dialog-content" class="fixed inset-0 grid place-items-center bg-black/50 data-[state=closed]:hidden">
136
+ <div class="bg-white rounded-lg p-6 max-w-md">
137
+ <!-- Dialog content -->
138
+ </div>
139
+ </div>
140
+ ```
141
+
142
+ ## Accessibility
143
+
144
+ The component automatically handles:
145
+
146
+ - `role="dialog"` on content
147
+ - `aria-modal="true"` on content
148
+ - `aria-labelledby` linked to title
149
+ - `aria-describedby` linked to description
150
+ - `aria-haspopup="dialog"` on trigger
151
+ - `aria-expanded` state on trigger
152
+ - Focus trap within dialog
153
+ - Focus restoration on close
154
+
155
+ ## Keyboard Navigation
156
+
157
+ | Key | Action |
158
+ |-----|--------|
159
+ | `Escape` | Close dialog |
160
+ | `Tab` | Cycle focus within dialog |
161
+ | `Shift+Tab` | Cycle focus backwards |
162
+
163
+ ## Events
164
+
165
+ Listen for changes via custom events:
166
+
167
+ ```javascript
168
+ element.addEventListener("dialog:change", (e) => {
169
+ console.log("Dialog open:", e.detail.open);
170
+ });
171
+ ```
172
+
173
+ ## License
174
+
175
+ MIT
176
+
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=s(require(`@data-slot/core`)),l=`a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])`,u=[];let d=0,f=``,p=``;function m(e,t={}){let{defaultOpen:n=!1,onOpenChange:r,closeOnClickOutside:i=!0,closeOnEscape:a=!0,lockScroll:o=!0,alertDialog:s=!1}=t,m=(0,c.getPart)(e,`dialog-trigger`),h=(0,c.getPart)(e,`dialog-overlay`),g=(0,c.getPart)(e,`dialog-content`),_=(0,c.getPart)(e,`dialog-close`),v=(0,c.getPart)(e,`dialog-title`),y=(0,c.getPart)(e,`dialog-description`);if(!g)throw Error(`Dialog requires dialog-content slot`);if(!h)throw Error(`Dialog requires dialog-overlay slot`);let b=!1,x=null,S=[];(0,c.ensureId)(g,`dialog-content`),g.setAttribute(`role`,s?`alertdialog`:`dialog`),(0,c.setAria)(g,`modal`,!0),(0,c.linkLabelledBy)(g,v,y),h.setAttribute(`role`,`presentation`),h.setAttribute(`aria-hidden`,`true`),h.tabIndex=-1,m&&(m.setAttribute(`aria-haspopup`,`dialog`),m.setAttribute(`aria-controls`,g.id),(0,c.setAria)(m,`expanded`,!1));let C=!1,w=()=>{g.hasAttribute(`tabindex`)||(g.tabIndex=-1,C=!0)},T=()=>{C&&(g.removeAttribute(`tabindex`),C=!1)},E=()=>{let e=g.querySelector(`[autofocus]`);if(e)return e.focus();let t=g.querySelector(l);if(t)return t.focus();w(),g.focus()},D=(t,n=!1)=>{if(!(b===t&&!n)){if(t){if(x=document.activeElement,u.push(O),o){if(d===0){let e=window.innerWidth-document.documentElement.clientWidth;f=document.body.style.overflow,p=document.body.style.paddingRight,document.body.style.paddingRight=`${e}px`,document.body.style.overflow=`hidden`}d++}}else{let e=u.indexOf(O);e!==-1&&u.splice(e,1),o&&(d--,d===0&&(document.body.style.overflow=f,document.body.style.paddingRight=p)),T();let t=x;x=null,requestAnimationFrame(()=>{t&&document.contains(t)&&typeof t.focus==`function`&&t.focus()})}b=t,g.hidden=!b,h.hidden=!b,m&&(0,c.setAria)(m,`expanded`,b),e.setAttribute(`data-state`,b?`open`:`closed`),(0,c.emit)(e,`dialog:change`,{open:b}),r?.(b),t&&requestAnimationFrame(E)}};n?(g.hidden=!1,h.hidden=!1,e.setAttribute(`data-state`,`open`),D(!0,!0)):(g.hidden=!0,h.hidden=!0,e.setAttribute(`data-state`,`closed`)),m&&S.push((0,c.on)(m,`click`,()=>D(!b))),_&&S.push((0,c.on)(_,`click`,()=>D(!1))),i&&S.push((0,c.on)(h,`pointerdown`,e=>{e.target===h&&b&&D(!1)})),S.push((0,c.on)(document,`keydown`,e=>{if(b){if(e.key===`Escape`&&a){let t=u[u.length-1];t===O&&(e.preventDefault(),D(!1));return}if(e.key===`Tab`){let t=g.querySelectorAll(l);if(t.length===0){e.preventDefault(),w(),g.focus();return}let n=t[0],r=t[t.length-1],i=document.activeElement;if(!g.contains(i)){e.preventDefault(),n.focus();return}if(n===r){e.preventDefault();return}e.shiftKey?i===n&&(e.preventDefault(),r.focus()):i===r&&(e.preventDefault(),n.focus())}}}));let O={open:()=>D(!0),close:()=>D(!1),toggle:()=>D(!b),get isOpen(){return b},destroy:()=>{b&&D(!1,!0),T(),S.forEach(e=>e()),S.length=0}};return O}const h=new WeakSet;function g(e=document){let t=[];for(let n of(0,c.getRoots)(e,`dialog`)){if(h.has(n))continue;h.add(n),t.push(m(n))}return t}exports.create=g,exports.createDialog=m;
@@ -0,0 +1,54 @@
1
+ //#region src/index.d.ts
2
+ interface DialogOptions {
3
+ /** Initial open state */
4
+ defaultOpen?: boolean;
5
+ /** Callback when open state changes */
6
+ onOpenChange?: (open: boolean) => void;
7
+ /** Close when clicking overlay */
8
+ closeOnClickOutside?: boolean;
9
+ /** Close when pressing Escape */
10
+ closeOnEscape?: boolean;
11
+ /** Lock body scroll when open */
12
+ lockScroll?: boolean;
13
+ /** Use alertdialog role for blocking confirmations */
14
+ alertDialog?: boolean;
15
+ }
16
+ interface DialogController {
17
+ /** Open the dialog */
18
+ open(): void;
19
+ /** Close the dialog */
20
+ close(): void;
21
+ /** Toggle the dialog */
22
+ toggle(): void;
23
+ /** Current open state */
24
+ readonly isOpen: boolean;
25
+ /** Cleanup all event listeners */
26
+ destroy(): void;
27
+ }
28
+ /**
29
+ * Create a dialog controller for a root element
30
+ *
31
+ * Expected markup:
32
+ * ```html
33
+ * <div data-slot="dialog">
34
+ * <button data-slot="dialog-trigger">Open</button>
35
+ * <div data-slot="dialog-overlay"></div>
36
+ * <div data-slot="dialog-content">
37
+ * <h2 data-slot="dialog-title">Title</h2>
38
+ * <p data-slot="dialog-description">Description</p>
39
+ * <button data-slot="dialog-close">Close</button>
40
+ * </div>
41
+ * </div>
42
+ * ```
43
+ *
44
+ * Note: Overlay is required. For modal behavior with inert backgrounds,
45
+ * mount dialogs as direct children of document.body (portal pattern).
46
+ */
47
+ declare function createDialog(root: Element, options?: DialogOptions): DialogController;
48
+ /**
49
+ * Find and bind all dialog components in a scope
50
+ * Returns array of controllers for programmatic access
51
+ */
52
+ declare function create(scope?: ParentNode): DialogController[];
53
+ //#endregion
54
+ export { DialogController, DialogOptions, create, createDialog };
@@ -0,0 +1,54 @@
1
+ //#region src/index.d.ts
2
+ interface DialogOptions {
3
+ /** Initial open state */
4
+ defaultOpen?: boolean;
5
+ /** Callback when open state changes */
6
+ onOpenChange?: (open: boolean) => void;
7
+ /** Close when clicking overlay */
8
+ closeOnClickOutside?: boolean;
9
+ /** Close when pressing Escape */
10
+ closeOnEscape?: boolean;
11
+ /** Lock body scroll when open */
12
+ lockScroll?: boolean;
13
+ /** Use alertdialog role for blocking confirmations */
14
+ alertDialog?: boolean;
15
+ }
16
+ interface DialogController {
17
+ /** Open the dialog */
18
+ open(): void;
19
+ /** Close the dialog */
20
+ close(): void;
21
+ /** Toggle the dialog */
22
+ toggle(): void;
23
+ /** Current open state */
24
+ readonly isOpen: boolean;
25
+ /** Cleanup all event listeners */
26
+ destroy(): void;
27
+ }
28
+ /**
29
+ * Create a dialog controller for a root element
30
+ *
31
+ * Expected markup:
32
+ * ```html
33
+ * <div data-slot="dialog">
34
+ * <button data-slot="dialog-trigger">Open</button>
35
+ * <div data-slot="dialog-overlay"></div>
36
+ * <div data-slot="dialog-content">
37
+ * <h2 data-slot="dialog-title">Title</h2>
38
+ * <p data-slot="dialog-description">Description</p>
39
+ * <button data-slot="dialog-close">Close</button>
40
+ * </div>
41
+ * </div>
42
+ * ```
43
+ *
44
+ * Note: Overlay is required. For modal behavior with inert backgrounds,
45
+ * mount dialogs as direct children of document.body (portal pattern).
46
+ */
47
+ declare function createDialog(root: Element, options?: DialogOptions): DialogController;
48
+ /**
49
+ * Find and bind all dialog components in a scope
50
+ * Returns array of controllers for programmatic access
51
+ */
52
+ declare function create(scope?: ParentNode): DialogController[];
53
+ //#endregion
54
+ export { DialogController, DialogOptions, create, createDialog };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{emit as e,ensureId as t,getPart as n,getRoots as r,linkLabelledBy as i,on as a,setAria as o}from"@data-slot/core";const s=`a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])`,c=[];let l=0,u=``,d=``;function f(r,f={}){let{defaultOpen:p=!1,onOpenChange:m,closeOnClickOutside:h=!0,closeOnEscape:g=!0,lockScroll:_=!0,alertDialog:v=!1}=f,y=n(r,`dialog-trigger`),b=n(r,`dialog-overlay`),x=n(r,`dialog-content`),S=n(r,`dialog-close`),C=n(r,`dialog-title`),w=n(r,`dialog-description`);if(!x)throw Error(`Dialog requires dialog-content slot`);if(!b)throw Error(`Dialog requires dialog-overlay slot`);let T=!1,E=null,D=[];t(x,`dialog-content`),x.setAttribute(`role`,v?`alertdialog`:`dialog`),o(x,`modal`,!0),i(x,C,w),b.setAttribute(`role`,`presentation`),b.setAttribute(`aria-hidden`,`true`),b.tabIndex=-1,y&&(y.setAttribute(`aria-haspopup`,`dialog`),y.setAttribute(`aria-controls`,x.id),o(y,`expanded`,!1));let O=!1,k=()=>{x.hasAttribute(`tabindex`)||(x.tabIndex=-1,O=!0)},A=()=>{O&&(x.removeAttribute(`tabindex`),O=!1)},j=()=>{let e=x.querySelector(`[autofocus]`);if(e)return e.focus();let t=x.querySelector(s);if(t)return t.focus();k(),x.focus()},M=(t,n=!1)=>{if(!(T===t&&!n)){if(t){if(E=document.activeElement,c.push(N),_){if(l===0){let e=window.innerWidth-document.documentElement.clientWidth;u=document.body.style.overflow,d=document.body.style.paddingRight,document.body.style.paddingRight=`${e}px`,document.body.style.overflow=`hidden`}l++}}else{let e=c.indexOf(N);e!==-1&&c.splice(e,1),_&&(l--,l===0&&(document.body.style.overflow=u,document.body.style.paddingRight=d)),A();let t=E;E=null,requestAnimationFrame(()=>{t&&document.contains(t)&&typeof t.focus==`function`&&t.focus()})}T=t,x.hidden=!T,b.hidden=!T,y&&o(y,`expanded`,T),r.setAttribute(`data-state`,T?`open`:`closed`),e(r,`dialog:change`,{open:T}),m?.(T),t&&requestAnimationFrame(j)}};p?(x.hidden=!1,b.hidden=!1,r.setAttribute(`data-state`,`open`),M(!0,!0)):(x.hidden=!0,b.hidden=!0,r.setAttribute(`data-state`,`closed`)),y&&D.push(a(y,`click`,()=>M(!T))),S&&D.push(a(S,`click`,()=>M(!1))),h&&D.push(a(b,`pointerdown`,e=>{e.target===b&&T&&M(!1)})),D.push(a(document,`keydown`,e=>{if(T){if(e.key===`Escape`&&g){let t=c[c.length-1];t===N&&(e.preventDefault(),M(!1));return}if(e.key===`Tab`){let t=x.querySelectorAll(s);if(t.length===0){e.preventDefault(),k(),x.focus();return}let n=t[0],r=t[t.length-1],i=document.activeElement;if(!x.contains(i)){e.preventDefault(),n.focus();return}if(n===r){e.preventDefault();return}e.shiftKey?i===n&&(e.preventDefault(),r.focus()):i===r&&(e.preventDefault(),n.focus())}}}));let N={open:()=>M(!0),close:()=>M(!1),toggle:()=>M(!T),get isOpen(){return T},destroy:()=>{T&&M(!1,!0),A(),D.forEach(e=>e()),D.length=0}};return N}const p=new WeakSet;function m(e=document){let t=[];for(let n of r(e,`dialog`)){if(p.has(n))continue;p.add(n),t.push(f(n))}return t}export{m as create,f as createDialog};
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@data-slot/dialog",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsdown"
26
+ },
27
+ "dependencies": {
28
+ "@data-slot/core": "workspace:*"
29
+ },
30
+ "keywords": [
31
+ "headless",
32
+ "ui",
33
+ "dialog",
34
+ "modal",
35
+ "vanilla",
36
+ "data-slot"
37
+ ],
38
+ "license": "MIT"
39
+ }