@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 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;
@@ -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 };
@@ -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
+ }