@data-slot/popover 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,193 @@
1
+ # @data-slot/popover
2
+
3
+ Headless popover component for vanilla JavaScript. Accessible, unstyled, tiny.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @data-slot/popover
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```html
14
+ <div data-slot="popover">
15
+ <button data-slot="popover-trigger">Open Popover</button>
16
+ <div data-slot="popover-content" hidden>
17
+ <p>Popover content here</p>
18
+ <button data-slot="popover-close">Close</button>
19
+ </div>
20
+ </div>
21
+
22
+ <script type="module">
23
+ import { create } from "@data-slot/popover";
24
+
25
+ const controllers = create();
26
+ </script>
27
+ ```
28
+
29
+ ## API
30
+
31
+ ### `create(scope?)`
32
+
33
+ Auto-discover and bind all popover instances in a scope (defaults to `document`).
34
+
35
+ ```typescript
36
+ import { create } from "@data-slot/popover";
37
+
38
+ const controllers = create(); // Returns PopoverController[]
39
+ ```
40
+
41
+ ### `createPopover(root, options?)`
42
+
43
+ Create a controller for a specific element.
44
+
45
+ ```typescript
46
+ import { createPopover } from "@data-slot/popover";
47
+
48
+ const popover = createPopover(element, {
49
+ defaultOpen: false,
50
+ position: "bottom",
51
+ closeOnClickOutside: true,
52
+ closeOnEscape: true,
53
+ onOpenChange: (open) => console.log(open),
54
+ });
55
+ ```
56
+
57
+ ### Options
58
+
59
+ | Option | Type | Default | Description |
60
+ |--------|------|---------|-------------|
61
+ | `defaultOpen` | `boolean` | `false` | Initial open state |
62
+ | `position` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | Position relative to trigger |
63
+ | `closeOnClickOutside` | `boolean` | `true` | Close when clicking outside |
64
+ | `closeOnEscape` | `boolean` | `true` | Close when pressing Escape |
65
+ | `onOpenChange` | `(open: boolean) => void` | `undefined` | Callback when open state changes |
66
+
67
+ ### Controller
68
+
69
+ | Method/Property | Description |
70
+ |-----------------|-------------|
71
+ | `open()` | Open the popover |
72
+ | `close()` | Close the popover |
73
+ | `toggle()` | Toggle the popover |
74
+ | `isOpen` | Current open state (readonly `boolean`) |
75
+ | `destroy()` | Cleanup all event listeners |
76
+
77
+ ## Markup Structure
78
+
79
+ ```html
80
+ <div data-slot="popover">
81
+ <button data-slot="popover-trigger">Trigger</button>
82
+ <div data-slot="popover-content">
83
+ Content
84
+ <button data-slot="popover-close">Close</button>
85
+ </div>
86
+ </div>
87
+ ```
88
+
89
+ ### Required Slots
90
+
91
+ - `popover-trigger` - Button to toggle popover
92
+ - `popover-content` - The popover panel
93
+
94
+ ### Optional Slots
95
+
96
+ - `popover-close` - Button to close the popover
97
+
98
+ ### Data Attributes
99
+
100
+ Set position via HTML:
101
+
102
+ ```html
103
+ <div data-slot="popover-content" data-position="top">
104
+ ```
105
+
106
+ ## Styling
107
+
108
+ Use `data-state` and `data-position` attributes:
109
+
110
+ ```css
111
+ /* Hidden state */
112
+ [data-slot="popover-content"][hidden] {
113
+ display: none;
114
+ }
115
+
116
+ /* Positioning */
117
+ [data-slot="popover"] {
118
+ position: relative;
119
+ }
120
+
121
+ [data-slot="popover-content"] {
122
+ position: absolute;
123
+ }
124
+
125
+ [data-slot="popover-content"][data-position="top"] {
126
+ bottom: 100%;
127
+ left: 50%;
128
+ transform: translateX(-50%);
129
+ }
130
+
131
+ [data-slot="popover-content"][data-position="bottom"] {
132
+ top: 100%;
133
+ left: 50%;
134
+ transform: translateX(-50%);
135
+ }
136
+
137
+ [data-slot="popover-content"][data-position="left"] {
138
+ right: 100%;
139
+ top: 50%;
140
+ transform: translateY(-50%);
141
+ }
142
+
143
+ [data-slot="popover-content"][data-position="right"] {
144
+ left: 100%;
145
+ top: 50%;
146
+ transform: translateY(-50%);
147
+ }
148
+ ```
149
+
150
+ With Tailwind:
151
+
152
+ ```html
153
+ <div data-slot="popover" class="relative">
154
+ <button data-slot="popover-trigger">Open</button>
155
+ <div
156
+ data-slot="popover-content"
157
+ class="absolute top-full left-1/2 -translate-x-1/2 mt-2 bg-white shadow-lg rounded-lg p-4 hidden data-[state=open]:block"
158
+ >
159
+ Content
160
+ </div>
161
+ </div>
162
+ ```
163
+
164
+ ## Accessibility
165
+
166
+ The component automatically handles:
167
+
168
+ - `aria-haspopup="dialog"` on trigger
169
+ - `aria-controls` linking trigger to content
170
+ - `aria-expanded` state on trigger
171
+ - Unique ID generation for content
172
+
173
+ ## Keyboard Navigation
174
+
175
+ | Key | Action |
176
+ |-----|--------|
177
+ | `Enter` / `Space` | Toggle popover (on trigger) |
178
+ | `Escape` | Close popover and return focus to trigger |
179
+
180
+ ## Events
181
+
182
+ Listen for changes via custom events:
183
+
184
+ ```javascript
185
+ element.addEventListener("popover:change", (e) => {
186
+ console.log("Popover open:", e.detail.open);
187
+ });
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT
193
+
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`));function l(e,t={}){let{defaultOpen:n=!1,onOpenChange:r,closeOnClickOutside:i=!0,closeOnEscape:a=!0}=t,o=(0,c.getPart)(e,`popover-trigger`),s=(0,c.getPart)(e,`popover-content`),l=(0,c.getPart)(e,`popover-close`);if(!o||!s)throw Error(`Popover requires trigger and content slots`);let u=t.position??s.dataset.position??`bottom`,d=n,f=[],p=(0,c.ensureId)(s,`popover-content`);o.setAttribute(`aria-haspopup`,`dialog`),o.setAttribute(`aria-controls`,p),s.setAttribute(`data-position`,u);let m=t=>{d!==t&&(d=t,(0,c.setAria)(o,`expanded`,d),s.hidden=!d,e.setAttribute(`data-state`,d?`open`:`closed`),(0,c.emit)(e,`popover:change`,{open:d}),r?.(d))};(0,c.setAria)(o,`expanded`,d),s.hidden=!d,e.setAttribute(`data-state`,d?`open`:`closed`),f.push((0,c.on)(o,`click`,()=>m(!d))),l&&f.push((0,c.on)(l,`click`,()=>m(!1))),i&&f.push((0,c.on)(document,`pointerdown`,t=>{if(!d)return;let n=t.target;e.contains(n)||m(!1)})),a&&f.push((0,c.on)(document,`keydown`,e=>{d&&e.key===`Escape`&&(e.preventDefault(),m(!1),o.focus())}));let h={open:()=>m(!0),close:()=>m(!1),toggle:()=>m(!d),get isOpen(){return d},destroy:()=>{f.forEach(e=>e()),f.length=0}};return h}const u=new WeakSet;function d(e=document){let t=[];for(let n of(0,c.getRoots)(e,`popover`)){if(u.has(n))continue;u.add(n),t.push(l(n))}return t}exports.create=d,exports.createPopover=l;
@@ -0,0 +1,48 @@
1
+ //#region src/index.d.ts
2
+ type PopoverPosition = "top" | "bottom" | "left" | "right";
3
+ interface PopoverOptions {
4
+ /** Initial open state */
5
+ defaultOpen?: boolean;
6
+ /** Position of popover relative to trigger */
7
+ position?: PopoverPosition;
8
+ /** Callback when open state changes */
9
+ onOpenChange?: (open: boolean) => void;
10
+ /** Close when clicking outside */
11
+ closeOnClickOutside?: boolean;
12
+ /** Close when pressing Escape */
13
+ closeOnEscape?: boolean;
14
+ }
15
+ interface PopoverController {
16
+ /** Open the popover */
17
+ open(): void;
18
+ /** Close the popover */
19
+ close(): void;
20
+ /** Toggle the popover */
21
+ toggle(): void;
22
+ /** Current open state */
23
+ readonly isOpen: boolean;
24
+ /** Cleanup all event listeners */
25
+ destroy(): void;
26
+ }
27
+ /**
28
+ * Create a popover controller for a root element
29
+ *
30
+ * Expected markup:
31
+ * ```html
32
+ * <div data-slot="popover">
33
+ * <button data-slot="popover-trigger">Open</button>
34
+ * <div data-slot="popover-content">
35
+ * Popover content
36
+ * <button data-slot="popover-close">Close</button>
37
+ * </div>
38
+ * </div>
39
+ * ```
40
+ */
41
+ declare function createPopover(root: Element, options?: PopoverOptions): PopoverController;
42
+ /**
43
+ * Find and bind all popover components in a scope
44
+ * Returns array of controllers for programmatic access
45
+ */
46
+ declare function create(scope?: ParentNode): PopoverController[];
47
+ //#endregion
48
+ export { PopoverController, PopoverOptions, PopoverPosition, create, createPopover };
@@ -0,0 +1,48 @@
1
+ //#region src/index.d.ts
2
+ type PopoverPosition = "top" | "bottom" | "left" | "right";
3
+ interface PopoverOptions {
4
+ /** Initial open state */
5
+ defaultOpen?: boolean;
6
+ /** Position of popover relative to trigger */
7
+ position?: PopoverPosition;
8
+ /** Callback when open state changes */
9
+ onOpenChange?: (open: boolean) => void;
10
+ /** Close when clicking outside */
11
+ closeOnClickOutside?: boolean;
12
+ /** Close when pressing Escape */
13
+ closeOnEscape?: boolean;
14
+ }
15
+ interface PopoverController {
16
+ /** Open the popover */
17
+ open(): void;
18
+ /** Close the popover */
19
+ close(): void;
20
+ /** Toggle the popover */
21
+ toggle(): void;
22
+ /** Current open state */
23
+ readonly isOpen: boolean;
24
+ /** Cleanup all event listeners */
25
+ destroy(): void;
26
+ }
27
+ /**
28
+ * Create a popover controller for a root element
29
+ *
30
+ * Expected markup:
31
+ * ```html
32
+ * <div data-slot="popover">
33
+ * <button data-slot="popover-trigger">Open</button>
34
+ * <div data-slot="popover-content">
35
+ * Popover content
36
+ * <button data-slot="popover-close">Close</button>
37
+ * </div>
38
+ * </div>
39
+ * ```
40
+ */
41
+ declare function createPopover(root: Element, options?: PopoverOptions): PopoverController;
42
+ /**
43
+ * Find and bind all popover components in a scope
44
+ * Returns array of controllers for programmatic access
45
+ */
46
+ declare function create(scope?: ParentNode): PopoverController[];
47
+ //#endregion
48
+ export { PopoverController, PopoverOptions, PopoverPosition, create, createPopover };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{emit as e,ensureId as t,getPart as n,getRoots as r,on as i,setAria as a}from"@data-slot/core";function o(r,o={}){let{defaultOpen:s=!1,onOpenChange:c,closeOnClickOutside:l=!0,closeOnEscape:u=!0}=o,d=n(r,`popover-trigger`),f=n(r,`popover-content`),p=n(r,`popover-close`);if(!d||!f)throw Error(`Popover requires trigger and content slots`);let m=o.position??f.dataset.position??`bottom`,h=s,g=[],_=t(f,`popover-content`);d.setAttribute(`aria-haspopup`,`dialog`),d.setAttribute(`aria-controls`,_),f.setAttribute(`data-position`,m);let v=t=>{h!==t&&(h=t,a(d,`expanded`,h),f.hidden=!h,r.setAttribute(`data-state`,h?`open`:`closed`),e(r,`popover:change`,{open:h}),c?.(h))};a(d,`expanded`,h),f.hidden=!h,r.setAttribute(`data-state`,h?`open`:`closed`),g.push(i(d,`click`,()=>v(!h))),p&&g.push(i(p,`click`,()=>v(!1))),l&&g.push(i(document,`pointerdown`,e=>{if(!h)return;let t=e.target;r.contains(t)||v(!1)})),u&&g.push(i(document,`keydown`,e=>{h&&e.key===`Escape`&&(e.preventDefault(),v(!1),d.focus())}));let y={open:()=>v(!0),close:()=>v(!1),toggle:()=>v(!h),get isOpen(){return h},destroy:()=>{g.forEach(e=>e()),g.length=0}};return y}const s=new WeakSet;function c(e=document){let t=[];for(let n of r(e,`popover`)){if(s.has(n))continue;s.add(n),t.push(o(n))}return t}export{c as create,o as createPopover};
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@data-slot/popover",
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": ["dist"],
22
+ "scripts": {
23
+ "build": "tsdown"
24
+ },
25
+ "dependencies": {
26
+ "@data-slot/core": "workspace:*"
27
+ },
28
+ "keywords": ["headless", "ui", "popover", "vanilla", "data-slot"],
29
+ "license": "MIT"
30
+ }