@data-slot/tooltip 0.2.86 → 0.2.87

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 CHANGED
@@ -59,11 +59,16 @@ const tooltip = createTooltip(element, {
59
59
  |--------|------|---------|-------------|
60
60
  | `delay` | `number` | `300` | Delay before showing tooltip (ms) |
61
61
  | `skipDelayDuration` | `number` | `300` | Duration to skip delay after closing (ms). Set to `0` to disable warm-up. |
62
- | `side` | `"top" \| "right" \| "bottom" \| "left"` | `"top"` | Side of tooltip relative to trigger (bind-time only) |
63
- | `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment along the side (bind-time only) |
62
+ | `side` | `"top" \| "right" \| "bottom" \| "left"` | `"top"` | Preferred side relative to trigger |
63
+ | `align` | `"start" \| "center" \| "end"` | `"center"` | Preferred alignment |
64
+ | `sideOffset` | `number` | `4` | Distance from trigger in pixels |
65
+ | `alignOffset` | `number` | `0` | Offset from alignment edge in pixels |
66
+ | `avoidCollisions` | `boolean` | `true` | Flip/shift to stay in viewport |
67
+ | `collisionPadding` | `number` | `8` | Viewport edge padding in pixels |
68
+ | `portal` | `boolean` | `true` | Portal content to `document.body` while open |
64
69
  | `onOpenChange` | `(open: boolean) => void` | `undefined` | Callback when visibility changes |
65
70
 
66
- **Note:** `side` and `align` are resolved once at bind time. To change placement dynamically, destroy and recreate the tooltip with new options.
71
+ **Note:** `side` and `align` are preferred placement inputs resolved at bind time. With collision handling enabled, computed `data-side` can differ at runtime.
67
72
 
68
73
  ### Data Attributes
69
74
 
@@ -75,6 +80,11 @@ Options can also be set via data attributes. JS options take precedence.
75
80
  | `data-skip-delay-duration` | number | `300` | Duration to skip delay after closing (ms) |
76
81
  | `data-side` | string | `"top"` | Side relative to trigger (checked on content first, then root) |
77
82
  | `data-align` | string | `"center"` | Alignment along the side (checked on content first, then root) |
83
+ | `data-side-offset` | number | `4` | Distance from trigger (px) |
84
+ | `data-align-offset` | number | `0` | Alignment edge offset (px) |
85
+ | `data-avoid-collisions` | boolean | `true` | Collision handling |
86
+ | `data-collision-padding` | number | `8` | Viewport edge padding (px) |
87
+ | `data-portal` | boolean | `true` | Portal while open |
78
88
 
79
89
  ```html
80
90
  <!-- Tooltip with faster response -->
@@ -87,8 +97,14 @@ Options can also be set via data attributes. JS options take precedence.
87
97
  ...
88
98
  </div>
89
99
 
90
- <!-- Side/align on content element -->
91
- <div data-slot="tooltip-content" data-side="bottom" data-align="start">
100
+ <!-- Side/align and offsets on content element -->
101
+ <div
102
+ data-slot="tooltip-content"
103
+ data-side="bottom"
104
+ data-align="start"
105
+ data-side-offset="8"
106
+ data-align-offset="4"
107
+ >
92
108
  ...
93
109
  </div>
94
110
  ```
@@ -111,7 +127,28 @@ Options can also be set via data attributes. JS options take precedence.
111
127
  </div>
112
128
  ```
113
129
 
114
- Both `tooltip-trigger` and `tooltip-content` are required.
130
+ ### Required Slots
131
+
132
+ - `tooltip-trigger`
133
+ - `tooltip-content`
134
+
135
+ ### Optional Slots
136
+
137
+ - `tooltip-positioner` - Optional authored positioning wrapper
138
+ - `tooltip-portal` - Optional authored portal wrapper that can contain `tooltip-positioner`
139
+
140
+ ### Composed Portal Markup (Optional)
141
+
142
+ ```html
143
+ <div data-slot="tooltip">
144
+ <button data-slot="tooltip-trigger">Trigger</button>
145
+ <div data-slot="tooltip-portal">
146
+ <div data-slot="tooltip-positioner">
147
+ <div data-slot="tooltip-content">Content</div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ ```
115
152
 
116
153
  ### Output Attributes
117
154
 
@@ -120,7 +157,9 @@ The component sets these attributes automatically:
120
157
  | Element | Attribute | Values |
121
158
  |---------|-----------|--------|
122
159
  | Root | `data-state` | `"open"` \| `"closed"` |
160
+ | Root | `data-open` / `data-closed` | Present when matching state |
123
161
  | Content | `data-state` | `"open"` \| `"closed"` |
162
+ | Content | `data-open` / `data-closed` | Present when matching state |
124
163
  | Content | `data-side` | `"top"` \| `"right"` \| `"bottom"` \| `"left"` |
125
164
  | Content | `data-align` | `"start"` \| `"center"` \| `"end"` |
126
165
  | Content | `role` | `"tooltip"` |
@@ -129,98 +168,53 @@ The component sets these attributes automatically:
129
168
 
130
169
  ## Styling
131
170
 
132
- This tooltip uses simple CSS positioning and is not collision-aware. Visibility is controlled entirely via CSS using the `data-state` attribute.
171
+ Position is computed in JavaScript and applied to the positioner as `position: absolute` + `transform: translate3d(...)`.
172
+ By default, content is portaled to `document.body` while open.
173
+ Use `data-open` / `data-closed`, `data-side`, and `data-align` for styling and animations.
133
174
 
134
175
  ### Recommended CSS
135
176
 
136
177
  The visibility transition trick keeps the tooltip visible during fade-out, then becomes non-focusable after the transition completes—no JS timers needed.
137
178
 
138
179
  ```css
139
- /* Positioning */
140
- [data-slot="tooltip"] {
141
- position: relative;
142
- }
143
-
144
180
  [data-slot="tooltip-content"] {
145
- position: absolute;
146
181
  white-space: nowrap;
147
- /* Hidden by default */
148
182
  opacity: 0;
149
183
  visibility: hidden;
150
184
  pointer-events: none;
151
- /* Visibility delays hiding until after opacity fades */
152
- transition: opacity 0.15s, visibility 0s linear 0.15s;
185
+ transform-origin: center;
186
+ transition: opacity 0.15s ease, visibility 0s linear 0.15s;
153
187
  }
154
188
 
155
- /* Open state */
156
- [data-slot="tooltip"][data-state="open"] [data-slot="tooltip-content"] {
189
+ [data-slot="tooltip-content"][data-open] {
157
190
  opacity: 1;
158
191
  visibility: visible;
159
192
  pointer-events: auto;
160
193
  transition-delay: 0s;
161
194
  }
162
195
 
163
- /* Side positioning */
164
- [data-slot="tooltip-content"][data-side="top"] {
165
- bottom: 100%;
166
- left: 50%;
167
- transform: translateX(-50%);
168
- margin-bottom: 8px;
169
- }
170
-
171
- [data-slot="tooltip-content"][data-side="bottom"] {
172
- top: 100%;
173
- left: 50%;
174
- transform: translateX(-50%);
175
- margin-top: 8px;
176
- }
177
-
178
- [data-slot="tooltip-content"][data-side="left"] {
179
- right: 100%;
180
- top: 50%;
181
- transform: translateY(-50%);
182
- margin-right: 8px;
183
- }
184
-
185
- [data-slot="tooltip-content"][data-side="right"] {
186
- left: 100%;
187
- top: 50%;
188
- transform: translateY(-50%);
189
- margin-left: 8px;
190
- }
191
-
192
- /* Alignment (example for top/bottom sides) */
193
- [data-slot="tooltip-content"][data-side="top"][data-align="start"],
194
- [data-slot="tooltip-content"][data-side="bottom"][data-align="start"] {
195
- left: 0;
196
- transform: none;
197
- }
198
-
199
- [data-slot="tooltip-content"][data-side="top"][data-align="end"],
200
- [data-slot="tooltip-content"][data-side="bottom"][data-align="end"] {
201
- left: auto;
202
- right: 0;
203
- transform: none;
196
+ [data-slot="tooltip-content"][data-closed] {
197
+ pointer-events: none;
204
198
  }
205
199
  ```
206
200
 
207
201
  ### Tailwind Example
208
202
 
209
- Use `group` on the root and `group-data-[state=open]:` for open state styles:
203
+ Use root/content data attributes for open-state styling:
210
204
 
211
205
  ```html
212
- <div data-slot="tooltip" class="group relative inline-block">
206
+ <div data-slot="tooltip">
213
207
  <button data-slot="tooltip-trigger">
214
208
  Hover me
215
209
  </button>
216
210
  <div
217
211
  data-slot="tooltip-content"
218
212
  data-side="top"
219
- class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1
213
+ class="px-2 py-1
220
214
  bg-gray-900 text-white text-sm rounded
221
215
  opacity-0 pointer-events-none transition-opacity duration-150
222
- group-data-[state=open]:opacity-100
223
- group-data-[state=open]:pointer-events-auto"
216
+ data-[open]:opacity-100
217
+ data-[open]:pointer-events-auto"
224
218
  >
225
219
  Tooltip text
226
220
  </div>
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@data-slot/core`),t=0;const n=[`top`,`right`,`bottom`,`left`],r=[`start`,`center`,`end`];function i(i,a={}){let o=(0,e.getPart)(i,`tooltip-trigger`),s=(0,e.getPart)(i,`tooltip-content`);if(!o||!s)throw Error(`Tooltip requires trigger and content slots`);let c=a.delay??(0,e.getDataNumber)(i,`delay`)??300,l=a.skipDelayDuration??(0,e.getDataNumber)(i,`skipDelayDuration`)??300,u=a.onOpenChange,d=a.side??(0,e.getDataEnum)(s,`side`,n)??(0,e.getDataEnum)(i,`side`,n)??`top`,f=a.align??(0,e.getDataEnum)(s,`align`,r)??(0,e.getDataEnum)(i,`align`,r)??`center`,p=!1,m=!1,h=null,g=[],_=(0,e.ensureId)(s,`tooltip-content`);s.setAttribute(`role`,`tooltip`),s.setAttribute(`data-side`,d),s.setAttribute(`data-align`,f);let v=()=>o.hasAttribute(`disabled`)||o.getAttribute(`aria-disabled`)===`true`,y=(t,n)=>{if(p===t)return;p=t;let r=p?`open`:`closed`;i.setAttribute(`data-state`,r),s.setAttribute(`data-state`,r),p?(o.setAttribute(`aria-describedby`,_),s.setAttribute(`aria-hidden`,`false`)):(o.removeAttribute(`aria-describedby`),s.setAttribute(`aria-hidden`,`true`)),(0,e.emit)(i,`tooltip:change`,{open:p,trigger:o,content:s,reason:n}),u?.(p)},b=e=>{if(h&&=(clearTimeout(h),null),Date.now()<t){y(!0,e);return}h=setTimeout(()=>{y(!0,e),h=null},c)},x=e=>{h&&=(clearTimeout(h),null),p&&l>0&&(t=Date.now()+l),y(!1,e)};return s.setAttribute(`aria-hidden`,`true`),i.setAttribute(`data-state`,`closed`),s.setAttribute(`data-state`,`closed`),g.push((0,e.on)(o,`pointerenter`,e=>{e.pointerType!==`touch`&&(v()||b(`pointer`))}),(0,e.on)(o,`pointerleave`,e=>{if(e.pointerType===`touch`||m)return;let t=e.relatedTarget;t&&s.contains(t)||x(`pointer`)}),(0,e.on)(o,`focus`,()=>{m=!0,!v()&&b(`focus`)}),(0,e.on)(o,`blur`,()=>{m=!1,x(`blur`)})),g.push((0,e.on)(s,`pointerleave`,e=>{if(e.pointerType===`touch`||m)return;let t=e.relatedTarget;t&&o.contains(t)||x(`pointer`)})),g.push((0,e.on)(i,`tooltip:set`,e=>{let t=e.detail,n;if(t?.open===void 0?t?.value!==void 0&&(n=t.value):n=t.open,typeof n==`boolean`)if(n){if(v())return;h&&=(clearTimeout(h),null),y(!0,`api`)}else x(`api`)})),g.push((0,e.createDismissLayer)({root:i,isOpen:()=>p,onDismiss:()=>x(`escape`),closeOnClickOutside:!1,closeOnEscape:!0,preventEscapeDefault:!1})),{show:()=>{v()||(h&&=(clearTimeout(h),null),y(!0,`api`))},hide:()=>x(`api`),get isOpen(){return p},destroy:()=>{h&&clearTimeout(h),g.forEach(e=>e()),g.length=0}}}const a=new WeakSet;function o(t=document){let n=[];for(let r of(0,e.getRoots)(t,`tooltip`))a.has(r)||(a.add(r),n.push(i(r)));return n}exports.create=o,exports.createTooltip=i;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@data-slot/core`),t=0;const n=[`top`,`right`,`bottom`,`left`],r=[`start`,`center`,`end`];function i(i,a={}){let o=(0,e.getPart)(i,`tooltip-trigger`),s=(0,e.getPart)(i,`tooltip-content`),c=(0,e.getPart)(i,`tooltip-positioner`),l=c&&s&&c.contains(s)?c:null,u=(0,e.getPart)(i,`tooltip-portal`),d=u&&l&&u.contains(l)?u:null;if(!o||!s)throw Error(`Tooltip requires trigger and content slots`);let f=a.delay??(0,e.getDataNumber)(i,`delay`)??300,p=a.skipDelayDuration??(0,e.getDataNumber)(i,`skipDelayDuration`)??300,m=a.onOpenChange,h=a.portal??(0,e.getDataBool)(s,`portal`)??(0,e.getDataBool)(i,`portal`)??!0,g=a.side??(0,e.getDataEnum)(s,`side`,n)??(0,e.getDataEnum)(i,`side`,n)??`top`,_=a.align??(0,e.getDataEnum)(s,`align`,r)??(0,e.getDataEnum)(i,`align`,r)??`center`,v=a.sideOffset??(0,e.getDataNumber)(s,`sideOffset`)??(0,e.getDataNumber)(i,`sideOffset`)??4,y=a.alignOffset??(0,e.getDataNumber)(s,`alignOffset`)??(0,e.getDataNumber)(i,`alignOffset`)??0,b=a.avoidCollisions??(0,e.getDataBool)(s,`avoidCollisions`)??(0,e.getDataBool)(i,`avoidCollisions`)??!0,x=a.collisionPadding??(0,e.getDataNumber)(s,`collisionPadding`)??(0,e.getDataNumber)(i,`collisionPadding`)??8,S=!1,C=!1,w=!1,T=null,E=[],D=(0,e.createPortalLifecycle)({content:s,root:i,enabled:h,wrapperSlot:l?void 0:`tooltip-positioner`,container:l??void 0,mountTarget:l?d??l:void 0}),O=(0,e.ensureId)(s,`tooltip-content`);s.setAttribute(`role`,`tooltip`),s.setAttribute(`data-side`,g),s.setAttribute(`data-align`,_);let k=e=>{let t=D.container;if(i.setAttribute(`data-state`,e),s.setAttribute(`data-state`,e),t!==s&&t.setAttribute(`data-state`,e),e===`open`){i.setAttribute(`data-open`,``),s.setAttribute(`data-open`,``),t!==s&&t.setAttribute(`data-open`,``),i.removeAttribute(`data-closed`),s.removeAttribute(`data-closed`),t!==s&&t.removeAttribute(`data-closed`);return}i.setAttribute(`data-closed`,``),s.setAttribute(`data-closed`,``),t!==s&&t.setAttribute(`data-closed`,``),i.removeAttribute(`data-open`),s.removeAttribute(`data-open`),t!==s&&t.removeAttribute(`data-open`)},A=()=>{let t=D.container,n=i.ownerDocument.defaultView??window,r=(0,e.computeFloatingPosition)({anchorRect:o.getBoundingClientRect(),contentRect:s.getBoundingClientRect(),side:g,align:_,sideOffset:v,alignOffset:y,avoidCollisions:b,collisionPadding:x});t.style.position=`absolute`,t.style.top=`0px`,t.style.left=`0px`,t.style.transform=`translate3d(${r.x+n.scrollX}px, ${r.y+n.scrollY}px, 0)`,t.style.willChange=`transform`,t.style.margin=`0`,s.setAttribute(`data-side`,r.side),s.setAttribute(`data-align`,r.align),t!==s&&(t.setAttribute(`data-side`,r.side),t.setAttribute(`data-align`,r.align))},j=(0,e.createPresenceLifecycle)({element:s,onExitComplete:()=>{w||(D.restore(),s.hidden=!0)}}),M=(0,e.createPositionSync)({observedElements:[o,s],isActive:()=>S,ancestorScroll:!1,onUpdate:A}),N=()=>o.hasAttribute(`disabled`)||o.getAttribute(`aria-disabled`)===`true`,P=(n,r)=>{S!==n&&(!n&&S&&p>0&&(t=Date.now()+p),S=n,k(S?`open`:`closed`),S?(o.setAttribute(`aria-describedby`,O),s.setAttribute(`aria-hidden`,`false`),D.mount(),s.hidden=!1,j.enter(),A(),M.start(),M.update()):(o.removeAttribute(`aria-describedby`),s.setAttribute(`aria-hidden`,`true`),j.exit(),M.stop()),(0,e.emit)(i,`tooltip:change`,{open:S,trigger:o,content:s,reason:r}),m?.(S))},F=e=>{if(T&&=(clearTimeout(T),null),Date.now()<t){P(!0,e);return}T=setTimeout(()=>{P(!0,e),T=null},f)},I=e=>{T&&=(clearTimeout(T),null),P(!1,e)};return s.hidden=!0,s.setAttribute(`aria-hidden`,`true`),k(`closed`),E.push((0,e.on)(o,`pointerenter`,e=>{e.pointerType!==`touch`&&(N()||F(`pointer`))}),(0,e.on)(o,`pointerleave`,e=>{if(e.pointerType===`touch`||C)return;let t=e.relatedTarget;t&&s.contains(t)||I(`pointer`)}),(0,e.on)(o,`focus`,()=>{C=!0,!N()&&F(`focus`)}),(0,e.on)(o,`blur`,()=>{C=!1,I(`blur`)})),E.push((0,e.on)(s,`pointerleave`,e=>{if(e.pointerType===`touch`||C)return;let t=e.relatedTarget;t&&o.contains(t)||I(`pointer`)})),E.push((0,e.on)(i,`tooltip:set`,e=>{let t=e.detail,n;if(t?.open===void 0?t?.value!==void 0&&(n=t.value):n=t.open,typeof n==`boolean`)if(n){if(N())return;T&&=(clearTimeout(T),null),P(!0,`api`)}else I(`api`)})),E.push((0,e.createDismissLayer)({root:i,isOpen:()=>S,onDismiss:()=>I(`escape`),closeOnClickOutside:!1,closeOnEscape:!0,preventEscapeDefault:!1})),{show:()=>{N()||(T&&=(clearTimeout(T),null),P(!0,`api`))},hide:()=>I(`api`),get isOpen(){return S},destroy:()=>{w=!0,T&&clearTimeout(T),M.stop(),j.cleanup(),D.cleanup(),E.forEach(e=>e()),E.length=0}}}const a=new WeakSet;function o(t=document){let n=[];for(let r of(0,e.getRoots)(t,`tooltip`))a.has(r)||(a.add(r),n.push(i(r)));return n}exports.create=o,exports.createTooltip=i;
package/dist/index.d.cts CHANGED
@@ -7,10 +7,20 @@ interface TooltipOptions {
7
7
  delay?: number;
8
8
  /** Duration to skip delay after closing (ms). Set to 0 to disable warm-up. Default: 300 */
9
9
  skipDelayDuration?: number;
10
- /** Side of tooltip relative to trigger. Default: 'top'. Set at bind time only. */
10
+ /** Preferred side of tooltip relative to trigger. Default: 'top'. */
11
11
  side?: TooltipSide;
12
- /** Alignment along the side. Default: 'center'. Set at bind time only. */
12
+ /** Preferred alignment along the side. Default: 'center'. */
13
13
  align?: TooltipAlign;
14
+ /** Distance from trigger in pixels. Default: 4 */
15
+ sideOffset?: number;
16
+ /** Alignment-axis offset in pixels. Default: 0 */
17
+ alignOffset?: number;
18
+ /** Enable collision handling. Default: true */
19
+ avoidCollisions?: boolean;
20
+ /** Viewport edge padding used by collision handling. Default: 8 */
21
+ collisionPadding?: number;
22
+ /** Portal content to body while open. Default: true */
23
+ portal?: boolean;
14
24
  /** Callback when visibility changes */
15
25
  onOpenChange?: (open: boolean) => void;
16
26
  }
@@ -38,15 +48,15 @@ interface TooltipController {
38
48
  * ```
39
49
  *
40
50
  * Data attributes (checked on content first, then root):
41
- * - `data-side`: 'top' | 'right' | 'bottom' | 'left' (bind-time only)
42
- * - `data-align`: 'start' | 'center' | 'end' (bind-time only)
51
+ * - `data-side`: 'top' | 'right' | 'bottom' | 'left' (bind-time preferred side)
52
+ * - `data-align`: 'start' | 'center' | 'end' (bind-time preferred align)
53
+ * - `data-side-offset`: number (px)
54
+ * - `data-align-offset`: number (px)
55
+ * - `data-avoid-collisions`: boolean
56
+ * - `data-collision-padding`: number (px)
43
57
  * - `data-delay`: number (ms)
44
58
  * - `data-skip-delay-duration`: number (ms)
45
59
  *
46
- * Note: side and align are resolved once at bind time. To change placement,
47
- * destroy and recreate the tooltip with new options.
48
- *
49
- * This tooltip uses simple CSS positioning (not collision-aware).
50
60
  * Opens on hover (non-touch) and focus. Touch devices: focus-only.
51
61
  * Content is hoverable: moving pointer from trigger to content keeps it open.
52
62
  * Tooltip stays open while trigger has focus, even if pointer leaves.
package/dist/index.d.ts CHANGED
@@ -7,10 +7,20 @@ interface TooltipOptions {
7
7
  delay?: number;
8
8
  /** Duration to skip delay after closing (ms). Set to 0 to disable warm-up. Default: 300 */
9
9
  skipDelayDuration?: number;
10
- /** Side of tooltip relative to trigger. Default: 'top'. Set at bind time only. */
10
+ /** Preferred side of tooltip relative to trigger. Default: 'top'. */
11
11
  side?: TooltipSide;
12
- /** Alignment along the side. Default: 'center'. Set at bind time only. */
12
+ /** Preferred alignment along the side. Default: 'center'. */
13
13
  align?: TooltipAlign;
14
+ /** Distance from trigger in pixels. Default: 4 */
15
+ sideOffset?: number;
16
+ /** Alignment-axis offset in pixels. Default: 0 */
17
+ alignOffset?: number;
18
+ /** Enable collision handling. Default: true */
19
+ avoidCollisions?: boolean;
20
+ /** Viewport edge padding used by collision handling. Default: 8 */
21
+ collisionPadding?: number;
22
+ /** Portal content to body while open. Default: true */
23
+ portal?: boolean;
14
24
  /** Callback when visibility changes */
15
25
  onOpenChange?: (open: boolean) => void;
16
26
  }
@@ -38,15 +48,15 @@ interface TooltipController {
38
48
  * ```
39
49
  *
40
50
  * Data attributes (checked on content first, then root):
41
- * - `data-side`: 'top' | 'right' | 'bottom' | 'left' (bind-time only)
42
- * - `data-align`: 'start' | 'center' | 'end' (bind-time only)
51
+ * - `data-side`: 'top' | 'right' | 'bottom' | 'left' (bind-time preferred side)
52
+ * - `data-align`: 'start' | 'center' | 'end' (bind-time preferred align)
53
+ * - `data-side-offset`: number (px)
54
+ * - `data-align-offset`: number (px)
55
+ * - `data-avoid-collisions`: boolean
56
+ * - `data-collision-padding`: number (px)
43
57
  * - `data-delay`: number (ms)
44
58
  * - `data-skip-delay-duration`: number (ms)
45
59
  *
46
- * Note: side and align are resolved once at bind time. To change placement,
47
- * destroy and recreate the tooltip with new options.
48
- *
49
- * This tooltip uses simple CSS positioning (not collision-aware).
50
60
  * Opens on hover (non-touch) and focus. Touch devices: focus-only.
51
61
  * Content is hoverable: moving pointer from trigger to content keeps it open.
52
62
  * Tooltip stays open while trigger has focus, even if pointer leaves.
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{createDismissLayer as e,emit as t,ensureId as n,getDataEnum as r,getDataNumber as i,getPart as a,getRoots as o,on as s}from"@data-slot/core";let c=0;const l=[`top`,`right`,`bottom`,`left`],u=[`start`,`center`,`end`];function d(o,d={}){let f=a(o,`tooltip-trigger`),p=a(o,`tooltip-content`);if(!f||!p)throw Error(`Tooltip requires trigger and content slots`);let m=d.delay??i(o,`delay`)??300,h=d.skipDelayDuration??i(o,`skipDelayDuration`)??300,g=d.onOpenChange,_=d.side??r(p,`side`,l)??r(o,`side`,l)??`top`,v=d.align??r(p,`align`,u)??r(o,`align`,u)??`center`,y=!1,b=!1,x=null,S=[],C=n(p,`tooltip-content`);p.setAttribute(`role`,`tooltip`),p.setAttribute(`data-side`,_),p.setAttribute(`data-align`,v);let w=()=>f.hasAttribute(`disabled`)||f.getAttribute(`aria-disabled`)===`true`,T=(e,n)=>{if(y===e)return;y=e;let r=y?`open`:`closed`;o.setAttribute(`data-state`,r),p.setAttribute(`data-state`,r),y?(f.setAttribute(`aria-describedby`,C),p.setAttribute(`aria-hidden`,`false`)):(f.removeAttribute(`aria-describedby`),p.setAttribute(`aria-hidden`,`true`)),t(o,`tooltip:change`,{open:y,trigger:f,content:p,reason:n}),g?.(y)},E=e=>{if(x&&=(clearTimeout(x),null),Date.now()<c){T(!0,e);return}x=setTimeout(()=>{T(!0,e),x=null},m)},D=e=>{x&&=(clearTimeout(x),null),y&&h>0&&(c=Date.now()+h),T(!1,e)};return p.setAttribute(`aria-hidden`,`true`),o.setAttribute(`data-state`,`closed`),p.setAttribute(`data-state`,`closed`),S.push(s(f,`pointerenter`,e=>{e.pointerType!==`touch`&&(w()||E(`pointer`))}),s(f,`pointerleave`,e=>{if(e.pointerType===`touch`||b)return;let t=e.relatedTarget;t&&p.contains(t)||D(`pointer`)}),s(f,`focus`,()=>{b=!0,!w()&&E(`focus`)}),s(f,`blur`,()=>{b=!1,D(`blur`)})),S.push(s(p,`pointerleave`,e=>{if(e.pointerType===`touch`||b)return;let t=e.relatedTarget;t&&f.contains(t)||D(`pointer`)})),S.push(s(o,`tooltip:set`,e=>{let t=e.detail,n;if(t?.open===void 0?t?.value!==void 0&&(n=t.value):n=t.open,typeof n==`boolean`)if(n){if(w())return;x&&=(clearTimeout(x),null),T(!0,`api`)}else D(`api`)})),S.push(e({root:o,isOpen:()=>y,onDismiss:()=>D(`escape`),closeOnClickOutside:!1,closeOnEscape:!0,preventEscapeDefault:!1})),{show:()=>{w()||(x&&=(clearTimeout(x),null),T(!0,`api`))},hide:()=>D(`api`),get isOpen(){return y},destroy:()=>{x&&clearTimeout(x),S.forEach(e=>e()),S.length=0}}}const f=new WeakSet;function p(e=document){let t=[];for(let n of o(e,`tooltip`))f.has(n)||(f.add(n),t.push(d(n)));return t}export{p as create,d as createTooltip};
1
+ import{computeFloatingPosition as e,createDismissLayer as t,createPortalLifecycle as n,createPositionSync as r,createPresenceLifecycle as i,emit as a,ensureId as o,getDataBool as s,getDataEnum as c,getDataNumber as l,getPart as u,getRoots as d,on as f}from"@data-slot/core";let p=0;const m=[`top`,`right`,`bottom`,`left`],h=[`start`,`center`,`end`];function g(d,g={}){let _=u(d,`tooltip-trigger`),v=u(d,`tooltip-content`),y=u(d,`tooltip-positioner`),b=y&&v&&y.contains(v)?y:null,x=u(d,`tooltip-portal`),S=x&&b&&x.contains(b)?x:null;if(!_||!v)throw Error(`Tooltip requires trigger and content slots`);let C=g.delay??l(d,`delay`)??300,w=g.skipDelayDuration??l(d,`skipDelayDuration`)??300,T=g.onOpenChange,E=g.portal??s(v,`portal`)??s(d,`portal`)??!0,D=g.side??c(v,`side`,m)??c(d,`side`,m)??`top`,O=g.align??c(v,`align`,h)??c(d,`align`,h)??`center`,k=g.sideOffset??l(v,`sideOffset`)??l(d,`sideOffset`)??4,A=g.alignOffset??l(v,`alignOffset`)??l(d,`alignOffset`)??0,j=g.avoidCollisions??s(v,`avoidCollisions`)??s(d,`avoidCollisions`)??!0,M=g.collisionPadding??l(v,`collisionPadding`)??l(d,`collisionPadding`)??8,N=!1,P=!1,F=!1,I=null,L=[],R=n({content:v,root:d,enabled:E,wrapperSlot:b?void 0:`tooltip-positioner`,container:b??void 0,mountTarget:b?S??b:void 0}),z=o(v,`tooltip-content`);v.setAttribute(`role`,`tooltip`),v.setAttribute(`data-side`,D),v.setAttribute(`data-align`,O);let B=e=>{let t=R.container;if(d.setAttribute(`data-state`,e),v.setAttribute(`data-state`,e),t!==v&&t.setAttribute(`data-state`,e),e===`open`){d.setAttribute(`data-open`,``),v.setAttribute(`data-open`,``),t!==v&&t.setAttribute(`data-open`,``),d.removeAttribute(`data-closed`),v.removeAttribute(`data-closed`),t!==v&&t.removeAttribute(`data-closed`);return}d.setAttribute(`data-closed`,``),v.setAttribute(`data-closed`,``),t!==v&&t.setAttribute(`data-closed`,``),d.removeAttribute(`data-open`),v.removeAttribute(`data-open`),t!==v&&t.removeAttribute(`data-open`)},V=()=>{let t=R.container,n=d.ownerDocument.defaultView??window,r=e({anchorRect:_.getBoundingClientRect(),contentRect:v.getBoundingClientRect(),side:D,align:O,sideOffset:k,alignOffset:A,avoidCollisions:j,collisionPadding:M});t.style.position=`absolute`,t.style.top=`0px`,t.style.left=`0px`,t.style.transform=`translate3d(${r.x+n.scrollX}px, ${r.y+n.scrollY}px, 0)`,t.style.willChange=`transform`,t.style.margin=`0`,v.setAttribute(`data-side`,r.side),v.setAttribute(`data-align`,r.align),t!==v&&(t.setAttribute(`data-side`,r.side),t.setAttribute(`data-align`,r.align))},H=i({element:v,onExitComplete:()=>{F||(R.restore(),v.hidden=!0)}}),U=r({observedElements:[_,v],isActive:()=>N,ancestorScroll:!1,onUpdate:V}),W=()=>_.hasAttribute(`disabled`)||_.getAttribute(`aria-disabled`)===`true`,G=(e,t)=>{N!==e&&(!e&&N&&w>0&&(p=Date.now()+w),N=e,B(N?`open`:`closed`),N?(_.setAttribute(`aria-describedby`,z),v.setAttribute(`aria-hidden`,`false`),R.mount(),v.hidden=!1,H.enter(),V(),U.start(),U.update()):(_.removeAttribute(`aria-describedby`),v.setAttribute(`aria-hidden`,`true`),H.exit(),U.stop()),a(d,`tooltip:change`,{open:N,trigger:_,content:v,reason:t}),T?.(N))},K=e=>{if(I&&=(clearTimeout(I),null),Date.now()<p){G(!0,e);return}I=setTimeout(()=>{G(!0,e),I=null},C)},q=e=>{I&&=(clearTimeout(I),null),G(!1,e)};return v.hidden=!0,v.setAttribute(`aria-hidden`,`true`),B(`closed`),L.push(f(_,`pointerenter`,e=>{e.pointerType!==`touch`&&(W()||K(`pointer`))}),f(_,`pointerleave`,e=>{if(e.pointerType===`touch`||P)return;let t=e.relatedTarget;t&&v.contains(t)||q(`pointer`)}),f(_,`focus`,()=>{P=!0,!W()&&K(`focus`)}),f(_,`blur`,()=>{P=!1,q(`blur`)})),L.push(f(v,`pointerleave`,e=>{if(e.pointerType===`touch`||P)return;let t=e.relatedTarget;t&&_.contains(t)||q(`pointer`)})),L.push(f(d,`tooltip:set`,e=>{let t=e.detail,n;if(t?.open===void 0?t?.value!==void 0&&(n=t.value):n=t.open,typeof n==`boolean`)if(n){if(W())return;I&&=(clearTimeout(I),null),G(!0,`api`)}else q(`api`)})),L.push(t({root:d,isOpen:()=>N,onDismiss:()=>q(`escape`),closeOnClickOutside:!1,closeOnEscape:!0,preventEscapeDefault:!1})),{show:()=>{W()||(I&&=(clearTimeout(I),null),G(!0,`api`))},hide:()=>q(`api`),get isOpen(){return N},destroy:()=>{F=!0,I&&clearTimeout(I),U.stop(),H.cleanup(),R.cleanup(),L.forEach(e=>e()),L.length=0}}}const _=new WeakSet;function v(e=document){let t=[];for(let n of d(e,`tooltip`))_.has(n)||(_.add(n),t.push(g(n)));return t}export{v as create,g as createTooltip};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-slot/tooltip",
3
- "version": "0.2.86",
3
+ "version": "0.2.87",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.cjs",
@@ -38,6 +38,6 @@
38
38
  ],
39
39
  "license": "MIT",
40
40
  "dependencies": {
41
- "@data-slot/core": "0.2.86"
41
+ "@data-slot/core": "0.2.87"
42
42
  }
43
43
  }