@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 +193 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +1 -0
- package/package.json +30 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|