@data-slot/tooltip 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 +216 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +1 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# @data-slot/tooltip
|
|
2
|
+
|
|
3
|
+
Headless tooltip component for vanilla JavaScript. Accessible, unstyled, tiny.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @data-slot/tooltip
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<div data-slot="tooltip">
|
|
15
|
+
<button data-slot="tooltip-trigger">Hover me</button>
|
|
16
|
+
<div data-slot="tooltip-content" role="tooltip" hidden>
|
|
17
|
+
Helpful tooltip text
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<script type="module">
|
|
22
|
+
import { create } from "@data-slot/tooltip";
|
|
23
|
+
|
|
24
|
+
const controllers = create();
|
|
25
|
+
</script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## API
|
|
29
|
+
|
|
30
|
+
### `create(scope?)`
|
|
31
|
+
|
|
32
|
+
Auto-discover and bind all tooltip instances in a scope (defaults to `document`).
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { create } from "@data-slot/tooltip";
|
|
36
|
+
|
|
37
|
+
const controllers = create(); // Returns TooltipController[]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### `createTooltip(root, options?)`
|
|
41
|
+
|
|
42
|
+
Create a controller for a specific element.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { createTooltip } from "@data-slot/tooltip";
|
|
46
|
+
|
|
47
|
+
const tooltip = createTooltip(element, {
|
|
48
|
+
delay: 300,
|
|
49
|
+
skipDelayDuration: 300,
|
|
50
|
+
position: "top",
|
|
51
|
+
onOpenChange: (open) => console.log(open),
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Options
|
|
56
|
+
|
|
57
|
+
| Option | Type | Default | Description |
|
|
58
|
+
|--------|------|---------|-------------|
|
|
59
|
+
| `delay` | `number` | `300` | Delay before showing tooltip (ms) |
|
|
60
|
+
| `skipDelayDuration` | `number` | `300` | Duration to skip delay after closing (ms). Set to `0` to disable. |
|
|
61
|
+
| `position` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Position relative to trigger |
|
|
62
|
+
| `onOpenChange` | `(open: boolean) => void` | `undefined` | Callback when visibility changes |
|
|
63
|
+
|
|
64
|
+
### Controller
|
|
65
|
+
|
|
66
|
+
| Method/Property | Description |
|
|
67
|
+
|-----------------|-------------|
|
|
68
|
+
| `show()` | Show the tooltip immediately |
|
|
69
|
+
| `hide()` | Hide the tooltip |
|
|
70
|
+
| `isOpen` | Current visibility state (readonly `boolean`) |
|
|
71
|
+
| `destroy()` | Cleanup all event listeners |
|
|
72
|
+
|
|
73
|
+
## Markup Structure
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<div data-slot="tooltip">
|
|
77
|
+
<button data-slot="tooltip-trigger">Trigger</button>
|
|
78
|
+
<div data-slot="tooltip-content" role="tooltip">Content</div>
|
|
79
|
+
</div>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Both `tooltip-trigger` and `tooltip-content` are required.
|
|
83
|
+
|
|
84
|
+
### Data Attributes
|
|
85
|
+
|
|
86
|
+
Set position via HTML:
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<div data-slot="tooltip-content" data-position="bottom" role="tooltip">
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Styling
|
|
93
|
+
|
|
94
|
+
### Basic Styling
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
/* Hidden state */
|
|
98
|
+
[data-slot="tooltip-content"][hidden] {
|
|
99
|
+
display: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Positioning */
|
|
103
|
+
[data-slot="tooltip"] {
|
|
104
|
+
position: relative;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
[data-slot="tooltip-content"] {
|
|
108
|
+
position: absolute;
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
[data-slot="tooltip-content"][data-position="top"] {
|
|
113
|
+
bottom: 100%;
|
|
114
|
+
left: 50%;
|
|
115
|
+
transform: translateX(-50%);
|
|
116
|
+
margin-bottom: 8px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
[data-slot="tooltip-content"][data-position="bottom"] {
|
|
120
|
+
top: 100%;
|
|
121
|
+
left: 50%;
|
|
122
|
+
transform: translateX(-50%);
|
|
123
|
+
margin-top: 8px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
[data-slot="tooltip-content"][data-position="left"] {
|
|
127
|
+
right: 100%;
|
|
128
|
+
top: 50%;
|
|
129
|
+
transform: translateY(-50%);
|
|
130
|
+
margin-right: 8px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[data-slot="tooltip-content"][data-position="right"] {
|
|
134
|
+
left: 100%;
|
|
135
|
+
top: 50%;
|
|
136
|
+
transform: translateY(-50%);
|
|
137
|
+
margin-left: 8px;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Animations
|
|
142
|
+
|
|
143
|
+
```css
|
|
144
|
+
[data-slot="tooltip-content"] {
|
|
145
|
+
opacity: 0;
|
|
146
|
+
transition: opacity 0.15s;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
[data-slot="tooltip"][data-state="open"] [data-slot="tooltip-content"] {
|
|
150
|
+
opacity: 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Skip transition during warm-up (instant show) */
|
|
154
|
+
[data-slot="tooltip-content"][data-instant] {
|
|
155
|
+
transition: none;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Tailwind Example
|
|
160
|
+
|
|
161
|
+
```html
|
|
162
|
+
<div data-slot="tooltip" class="relative inline-block group">
|
|
163
|
+
<button data-slot="tooltip-trigger">
|
|
164
|
+
Hover me
|
|
165
|
+
</button>
|
|
166
|
+
<div
|
|
167
|
+
data-slot="tooltip-content"
|
|
168
|
+
data-position="top"
|
|
169
|
+
role="tooltip"
|
|
170
|
+
class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 text-white text-sm rounded opacity-0 group-data-[state=open]:opacity-100 transition-opacity pointer-events-none"
|
|
171
|
+
>
|
|
172
|
+
Tooltip text
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Warm-up Behavior
|
|
178
|
+
|
|
179
|
+
When a user closes one tooltip and quickly hovers another, the second tooltip shows instantly (no delay). This creates a fluid browsing experience similar to native OS tooltips.
|
|
180
|
+
|
|
181
|
+
- Controlled by `skipDelayDuration` option
|
|
182
|
+
- Set to `0` to disable this behavior
|
|
183
|
+
- The `data-instant` attribute is set when showing instantly (use to skip CSS transitions)
|
|
184
|
+
|
|
185
|
+
## Accessibility
|
|
186
|
+
|
|
187
|
+
The component automatically handles:
|
|
188
|
+
|
|
189
|
+
- `role="tooltip"` on content
|
|
190
|
+
- `aria-describedby` linking trigger to content
|
|
191
|
+
- Unique ID generation for content
|
|
192
|
+
|
|
193
|
+
## Keyboard & Interaction
|
|
194
|
+
|
|
195
|
+
| Action | Behavior |
|
|
196
|
+
|--------|----------|
|
|
197
|
+
| Mouse enter | Show after delay |
|
|
198
|
+
| Mouse leave | Hide immediately |
|
|
199
|
+
| Focus | Show after delay |
|
|
200
|
+
| Blur | Hide immediately |
|
|
201
|
+
| `Escape` | Hide immediately |
|
|
202
|
+
|
|
203
|
+
## Events
|
|
204
|
+
|
|
205
|
+
Listen for changes via custom events:
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
element.addEventListener("tooltip:change", (e) => {
|
|
209
|
+
console.log("Tooltip visible:", e.detail.open);
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
216
|
+
|
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`));let l=0;const u=300;function d(e,t={}){let{delay:n=300,skipDelayDuration:r=u,onOpenChange:i}=t,a=(0,c.getPart)(e,`tooltip-trigger`),o=(0,c.getPart)(e,`tooltip-content`);if(!a||!o)throw Error(`Tooltip requires trigger and content slots`);let s=t.position??o.dataset.position??`top`,d=!1,f=null,p=[],m=(0,c.ensureId)(o,`tooltip-content`);a.setAttribute(`aria-describedby`,m),o.setAttribute(`role`,`tooltip`),o.setAttribute(`data-position`,s);let h=(t,n=!1)=>{d!==t&&(d=t,o.hidden=!d,e.setAttribute(`data-state`,d?`open`:`closed`),n&&d?o.setAttribute(`data-instant`,``):o.removeAttribute(`data-instant`),(0,c.emit)(e,`tooltip:change`,{open:d}),i?.(d))},g=()=>{if(f)return;let e=Date.now();if(e<l){h(!0,!0);return}f=setTimeout(()=>{h(!0,!1),f=null},n)},_=()=>{f&&(clearTimeout(f),f=null),d&&r>0&&(l=Date.now()+r),h(!1)};o.hidden=!0,e.setAttribute(`data-state`,`closed`),p.push((0,c.on)(a,`mouseenter`,g),(0,c.on)(a,`mouseleave`,_),(0,c.on)(a,`focus`,g),(0,c.on)(a,`blur`,_)),p.push((0,c.on)(document,`keydown`,e=>{d&&e.key===`Escape`&&_()}));let v={show:()=>{f&&(clearTimeout(f),f=null),h(!0)},hide:_,get isOpen(){return d},destroy:()=>{f&&clearTimeout(f),p.forEach(e=>e()),p.length=0}};return v}const f=new WeakSet;function p(e=document){let t=[];for(let n of(0,c.getRoots)(e,`tooltip`)){if(f.has(n))continue;f.add(n),t.push(d(n))}return t}exports.create=p,exports.createTooltip=d;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
3
|
+
interface TooltipOptions {
|
|
4
|
+
/** Delay before showing tooltip (ms) */
|
|
5
|
+
delay?: number;
|
|
6
|
+
/** Duration to skip delay after closing (ms). Set to 0 to disable warm-up behavior. */
|
|
7
|
+
skipDelayDuration?: number;
|
|
8
|
+
/** Position of tooltip relative to trigger */
|
|
9
|
+
position?: TooltipPosition;
|
|
10
|
+
/** Callback when visibility changes */
|
|
11
|
+
onOpenChange?: (open: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
interface TooltipController {
|
|
14
|
+
/** Show the tooltip */
|
|
15
|
+
show(): void;
|
|
16
|
+
/** Hide the tooltip */
|
|
17
|
+
hide(): void;
|
|
18
|
+
/** Current visibility state */
|
|
19
|
+
readonly isOpen: boolean;
|
|
20
|
+
/** Cleanup all event listeners */
|
|
21
|
+
destroy(): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a tooltip controller for a root element
|
|
25
|
+
*
|
|
26
|
+
* Expected markup:
|
|
27
|
+
* ```html
|
|
28
|
+
* <div data-slot="tooltip">
|
|
29
|
+
* <button data-slot="tooltip-trigger">Hover me</button>
|
|
30
|
+
* <div data-slot="tooltip-content" role="tooltip">Tooltip text</div>
|
|
31
|
+
* </div>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare function createTooltip(root: Element, options?: TooltipOptions): TooltipController;
|
|
35
|
+
/**
|
|
36
|
+
* Find and bind all tooltip components in a scope
|
|
37
|
+
* Returns array of controllers for programmatic access
|
|
38
|
+
*/
|
|
39
|
+
declare function create(scope?: ParentNode): TooltipController[];
|
|
40
|
+
//#endregion
|
|
41
|
+
export { TooltipController, TooltipOptions, TooltipPosition, create, createTooltip };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
3
|
+
interface TooltipOptions {
|
|
4
|
+
/** Delay before showing tooltip (ms) */
|
|
5
|
+
delay?: number;
|
|
6
|
+
/** Duration to skip delay after closing (ms). Set to 0 to disable warm-up behavior. */
|
|
7
|
+
skipDelayDuration?: number;
|
|
8
|
+
/** Position of tooltip relative to trigger */
|
|
9
|
+
position?: TooltipPosition;
|
|
10
|
+
/** Callback when visibility changes */
|
|
11
|
+
onOpenChange?: (open: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
interface TooltipController {
|
|
14
|
+
/** Show the tooltip */
|
|
15
|
+
show(): void;
|
|
16
|
+
/** Hide the tooltip */
|
|
17
|
+
hide(): void;
|
|
18
|
+
/** Current visibility state */
|
|
19
|
+
readonly isOpen: boolean;
|
|
20
|
+
/** Cleanup all event listeners */
|
|
21
|
+
destroy(): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a tooltip controller for a root element
|
|
25
|
+
*
|
|
26
|
+
* Expected markup:
|
|
27
|
+
* ```html
|
|
28
|
+
* <div data-slot="tooltip">
|
|
29
|
+
* <button data-slot="tooltip-trigger">Hover me</button>
|
|
30
|
+
* <div data-slot="tooltip-content" role="tooltip">Tooltip text</div>
|
|
31
|
+
* </div>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare function createTooltip(root: Element, options?: TooltipOptions): TooltipController;
|
|
35
|
+
/**
|
|
36
|
+
* Find and bind all tooltip components in a scope
|
|
37
|
+
* Returns array of controllers for programmatic access
|
|
38
|
+
*/
|
|
39
|
+
declare function create(scope?: ParentNode): TooltipController[];
|
|
40
|
+
//#endregion
|
|
41
|
+
export { TooltipController, TooltipOptions, TooltipPosition, create, createTooltip };
|
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}from"@data-slot/core";let a=0;const o=300;function s(r,s={}){let{delay:c=300,skipDelayDuration:l=o,onOpenChange:u}=s,d=n(r,`tooltip-trigger`),f=n(r,`tooltip-content`);if(!d||!f)throw Error(`Tooltip requires trigger and content slots`);let p=s.position??f.dataset.position??`top`,m=!1,h=null,g=[],_=t(f,`tooltip-content`);d.setAttribute(`aria-describedby`,_),f.setAttribute(`role`,`tooltip`),f.setAttribute(`data-position`,p);let v=(t,n=!1)=>{m!==t&&(m=t,f.hidden=!m,r.setAttribute(`data-state`,m?`open`:`closed`),n&&m?f.setAttribute(`data-instant`,``):f.removeAttribute(`data-instant`),e(r,`tooltip:change`,{open:m}),u?.(m))},y=()=>{if(h)return;let e=Date.now();if(e<a){v(!0,!0);return}h=setTimeout(()=>{v(!0,!1),h=null},c)},b=()=>{h&&(clearTimeout(h),h=null),m&&l>0&&(a=Date.now()+l),v(!1)};f.hidden=!0,r.setAttribute(`data-state`,`closed`),g.push(i(d,`mouseenter`,y),i(d,`mouseleave`,b),i(d,`focus`,y),i(d,`blur`,b)),g.push(i(document,`keydown`,e=>{m&&e.key===`Escape`&&b()}));let x={show:()=>{h&&(clearTimeout(h),h=null),v(!0)},hide:b,get isOpen(){return m},destroy:()=>{h&&clearTimeout(h),g.forEach(e=>e()),g.length=0}};return x}const c=new WeakSet;function l(e=document){let t=[];for(let n of r(e,`tooltip`)){if(c.has(n))continue;c.add(n),t.push(s(n))}return t}export{l as create,s as createTooltip};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@data-slot/tooltip",
|
|
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", "tooltip", "vanilla", "data-slot"],
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|