@zag-js/tooltip 2.0.0-next.1 → 2.0.0-next.2

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.
@@ -69,14 +69,14 @@ function connect(service, normalize) {
69
69
  setOpen(nextOpen) {
70
70
  const open2 = state.matches("open", "closing");
71
71
  if (open2 === nextOpen) return;
72
- send({ type: nextOpen ? "open" : "close" });
72
+ send({ type: nextOpen ? "OPEN" : "CLOSE" });
73
73
  },
74
74
  triggerValue,
75
75
  setTriggerValue(value) {
76
- send({ type: "triggerValue.set", value: value ?? void 0 });
76
+ send({ type: "TRIGGER_VALUE.SET", value: value ?? void 0 });
77
77
  },
78
78
  reposition(options = {}) {
79
- send({ type: "positioning.set", options });
79
+ send({ type: "POSITIONING.SET", options });
80
80
  },
81
81
  getTriggerState,
82
82
  getTriggerProps(props = {}) {
@@ -97,14 +97,14 @@ function connect(service, normalize) {
97
97
  if (disabled) return;
98
98
  if (!prop("closeOnClick")) return;
99
99
  const shouldSwitch = open && value != null && !current;
100
- send({ type: shouldSwitch ? "triggerValue.set" : "close", src: "trigger.click", value, triggerId });
100
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "CLOSE", src: "trigger.click", value, triggerId });
101
101
  },
102
102
  onFocus(event) {
103
103
  if (event.defaultPrevented) return;
104
104
  if (disabled) return;
105
105
  if (!(0, import_focus_visible.isFocusVisible)()) return;
106
106
  const shouldSwitch = open && value != null && !current;
107
- send({ type: shouldSwitch ? "triggerValue.set" : "open", src: "trigger.focus", value, triggerId });
107
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "OPEN", src: "trigger.focus", value, triggerId });
108
108
  },
109
109
  onBlur(event) {
110
110
  if (event.defaultPrevented) return;
@@ -113,7 +113,7 @@ function connect(service, normalize) {
113
113
  const activeEl = event.relatedTarget ?? scope.getDoc().activeElement;
114
114
  const focusedAnotherTrigger = activeEl?.closest(`[${import_tooltip.parts.trigger.attr}="${scope.id}"]`) != null;
115
115
  if (!focusedAnotherTrigger) {
116
- send({ type: "close", src: "trigger.blur", value, triggerId });
116
+ send({ type: "CLOSE", src: "trigger.blur", value, triggerId });
117
117
  }
118
118
  },
119
119
  onPointerDown(event) {
@@ -122,7 +122,7 @@ function connect(service, normalize) {
122
122
  if (!(0, import_dom_query.isLeftClick)(event)) return;
123
123
  if (!prop("closeOnPointerDown")) return;
124
124
  if (id === import_tooltip2.store.get("id")) {
125
- send({ type: "close", src: "trigger.pointerdown", value, triggerId });
125
+ send({ type: "CLOSE", src: "trigger.pointerdown", value, triggerId });
126
126
  }
127
127
  },
128
128
  onPointerMove(event) {
@@ -130,21 +130,21 @@ function connect(service, normalize) {
130
130
  if (disabled) return;
131
131
  if (event.pointerType === "touch") return;
132
132
  const shouldSwitch = open && value != null && !current;
133
- send({ type: shouldSwitch ? "triggerValue.set" : "pointer.move", value, triggerId });
133
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "POINTER_MOVE", value, triggerId });
134
134
  },
135
135
  onPointerOver(event) {
136
136
  if (event.defaultPrevented) return;
137
137
  if (disabled) return;
138
138
  if (event.pointerType === "touch") return;
139
- send({ type: "pointer.move", value, triggerId });
139
+ send({ type: "POINTER_MOVE", value, triggerId });
140
140
  },
141
141
  onPointerLeave() {
142
142
  if (disabled) return;
143
- send({ type: "pointer.leave" });
143
+ send({ type: "POINTER_LEAVE" });
144
144
  },
145
145
  onPointerCancel() {
146
146
  if (disabled) return;
147
- send({ type: "pointer.leave" });
147
+ send({ type: "POINTER_LEAVE" });
148
148
  }
149
149
  });
150
150
  },
@@ -183,10 +183,7 @@ function connect(service, normalize) {
183
183
  "data-placement": contentState.placement,
184
184
  "data-side": contentState.side,
185
185
  onPointerEnter() {
186
- send({ type: "content.pointer.move" });
187
- },
188
- onPointerLeave() {
189
- send({ type: "content.pointer.leave" });
186
+ send({ type: "CONTENT_POINTER_MOVE" });
190
187
  },
191
188
  style: {
192
189
  pointerEvents: prop("interactive") ? "auto" : "none"
@@ -35,14 +35,14 @@ function connect(service, normalize) {
35
35
  setOpen(nextOpen) {
36
36
  const open2 = state.matches("open", "closing");
37
37
  if (open2 === nextOpen) return;
38
- send({ type: nextOpen ? "open" : "close" });
38
+ send({ type: nextOpen ? "OPEN" : "CLOSE" });
39
39
  },
40
40
  triggerValue,
41
41
  setTriggerValue(value) {
42
- send({ type: "triggerValue.set", value: value ?? void 0 });
42
+ send({ type: "TRIGGER_VALUE.SET", value: value ?? void 0 });
43
43
  },
44
44
  reposition(options = {}) {
45
- send({ type: "positioning.set", options });
45
+ send({ type: "POSITIONING.SET", options });
46
46
  },
47
47
  getTriggerState,
48
48
  getTriggerProps(props = {}) {
@@ -63,14 +63,14 @@ function connect(service, normalize) {
63
63
  if (disabled) return;
64
64
  if (!prop("closeOnClick")) return;
65
65
  const shouldSwitch = open && value != null && !current;
66
- send({ type: shouldSwitch ? "triggerValue.set" : "close", src: "trigger.click", value, triggerId });
66
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "CLOSE", src: "trigger.click", value, triggerId });
67
67
  },
68
68
  onFocus(event) {
69
69
  if (event.defaultPrevented) return;
70
70
  if (disabled) return;
71
71
  if (!isFocusVisible()) return;
72
72
  const shouldSwitch = open && value != null && !current;
73
- send({ type: shouldSwitch ? "triggerValue.set" : "open", src: "trigger.focus", value, triggerId });
73
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "OPEN", src: "trigger.focus", value, triggerId });
74
74
  },
75
75
  onBlur(event) {
76
76
  if (event.defaultPrevented) return;
@@ -79,7 +79,7 @@ function connect(service, normalize) {
79
79
  const activeEl = event.relatedTarget ?? scope.getDoc().activeElement;
80
80
  const focusedAnotherTrigger = activeEl?.closest(`[${parts.trigger.attr}="${scope.id}"]`) != null;
81
81
  if (!focusedAnotherTrigger) {
82
- send({ type: "close", src: "trigger.blur", value, triggerId });
82
+ send({ type: "CLOSE", src: "trigger.blur", value, triggerId });
83
83
  }
84
84
  },
85
85
  onPointerDown(event) {
@@ -88,7 +88,7 @@ function connect(service, normalize) {
88
88
  if (!isLeftClick(event)) return;
89
89
  if (!prop("closeOnPointerDown")) return;
90
90
  if (id === store.get("id")) {
91
- send({ type: "close", src: "trigger.pointerdown", value, triggerId });
91
+ send({ type: "CLOSE", src: "trigger.pointerdown", value, triggerId });
92
92
  }
93
93
  },
94
94
  onPointerMove(event) {
@@ -96,21 +96,21 @@ function connect(service, normalize) {
96
96
  if (disabled) return;
97
97
  if (event.pointerType === "touch") return;
98
98
  const shouldSwitch = open && value != null && !current;
99
- send({ type: shouldSwitch ? "triggerValue.set" : "pointer.move", value, triggerId });
99
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "POINTER_MOVE", value, triggerId });
100
100
  },
101
101
  onPointerOver(event) {
102
102
  if (event.defaultPrevented) return;
103
103
  if (disabled) return;
104
104
  if (event.pointerType === "touch") return;
105
- send({ type: "pointer.move", value, triggerId });
105
+ send({ type: "POINTER_MOVE", value, triggerId });
106
106
  },
107
107
  onPointerLeave() {
108
108
  if (disabled) return;
109
- send({ type: "pointer.leave" });
109
+ send({ type: "POINTER_LEAVE" });
110
110
  },
111
111
  onPointerCancel() {
112
112
  if (disabled) return;
113
- send({ type: "pointer.leave" });
113
+ send({ type: "POINTER_LEAVE" });
114
114
  }
115
115
  });
116
116
  },
@@ -149,10 +149,7 @@ function connect(service, normalize) {
149
149
  "data-placement": contentState.placement,
150
150
  "data-side": contentState.side,
151
151
  onPointerEnter() {
152
- send({ type: "content.pointer.move" });
153
- },
154
- onPointerLeave() {
155
- send({ type: "content.pointer.leave" });
152
+ send({ type: "CONTENT_POINTER_MOVE" });
156
153
  },
157
154
  style: {
158
155
  pointerEvents: prop("interactive") ? "auto" : "none"
@@ -37,6 +37,7 @@ var import_core = require("@zag-js/core");
37
37
  var import_dom_query = require("@zag-js/dom-query");
38
38
  var import_focus_visible = require("@zag-js/focus-visible");
39
39
  var import_popper = require("@zag-js/popper");
40
+ var import_safe_area = require("@zag-js/safe-area");
40
41
  var import_utils = require("@zag-js/utils");
41
42
  var dom = __toESM(require("./tooltip.dom.js"));
42
43
  var import_tooltip = require("./tooltip.store.js");
@@ -70,6 +71,7 @@ var machine = (0, import_core.createMachine)({
70
71
  context: ({ bindable, prop, scope }) => ({
71
72
  currentPlacement: bindable(() => ({ defaultValue: void 0 })),
72
73
  hasPointerMoveOpened: bindable(() => ({ defaultValue: null })),
74
+ isPointer: bindable(() => ({ defaultValue: false })),
73
75
  triggerValue: bindable(() => ({
74
76
  defaultValue: prop("defaultTriggerValue") ?? null,
75
77
  value: prop("triggerValue"),
@@ -93,18 +95,18 @@ var machine = (0, import_core.createMachine)({
93
95
  });
94
96
  },
95
97
  on: {
96
- "triggerValue.set": {
98
+ "TRIGGER_VALUE.SET": {
97
99
  actions: ["setTriggerValue", "repositionImmediate"]
98
100
  }
99
101
  },
100
102
  states: {
101
103
  closed: {
102
- entry: ["clearGlobalId"],
104
+ entry: ["clearGlobalId", "clearIsPointer"],
103
105
  on: {
104
- "controlled.open": {
106
+ "CONTROLLED.OPEN": {
105
107
  target: "open"
106
108
  },
107
- open: [
109
+ OPEN: [
108
110
  {
109
111
  guard: "isOpenControlled",
110
112
  actions: ["setTriggerValue", "invokeOnOpen"]
@@ -114,10 +116,10 @@ var machine = (0, import_core.createMachine)({
114
116
  actions: ["setTriggerValue", "invokeOnOpen"]
115
117
  }
116
118
  ],
117
- "pointer.leave": {
119
+ POINTER_LEAVE: {
118
120
  actions: ["clearPointerMoveOpened"]
119
121
  },
120
- "pointer.move": [
122
+ POINTER_MOVE: [
121
123
  {
122
124
  guard: and("noVisibleTooltip", not("hasPointerMoveOpened")),
123
125
  target: "opening",
@@ -126,7 +128,7 @@ var machine = (0, import_core.createMachine)({
126
128
  {
127
129
  guard: not("hasPointerMoveOpened"),
128
130
  target: "open",
129
- actions: ["setPointerMoveOpened", "invokeOnOpen", "setTriggerValue"]
131
+ actions: ["setPointerMoveOpened", "setIsPointer", "invokeOnOpen", "setTriggerValue"]
130
132
  }
131
133
  ]
132
134
  }
@@ -134,23 +136,23 @@ var machine = (0, import_core.createMachine)({
134
136
  opening: {
135
137
  effects: ["trackScroll", "trackPointerlockChange", "waitForOpenDelay"],
136
138
  on: {
137
- "after.openDelay": [
139
+ OPEN_DELAY: [
138
140
  {
139
141
  guard: "isOpenControlled",
140
- actions: ["setPointerMoveOpened", "invokeOnOpen"]
142
+ actions: ["setPointerMoveOpened", "setIsPointer", "invokeOnOpen"]
141
143
  },
142
144
  {
143
145
  target: "open",
144
- actions: ["setPointerMoveOpened", "invokeOnOpen"]
146
+ actions: ["setPointerMoveOpened", "setIsPointer", "invokeOnOpen"]
145
147
  }
146
148
  ],
147
- "controlled.open": {
149
+ "CONTROLLED.OPEN": {
148
150
  target: "open"
149
151
  },
150
- "controlled.close": {
152
+ "CONTROLLED.CLOSE": {
151
153
  target: "closed"
152
154
  },
153
- open: [
155
+ OPEN: [
154
156
  {
155
157
  guard: "isOpenControlled",
156
158
  actions: ["setTriggerValue", "invokeOnOpen"]
@@ -160,7 +162,7 @@ var machine = (0, import_core.createMachine)({
160
162
  actions: ["setTriggerValue", "invokeOnOpen"]
161
163
  }
162
164
  ],
163
- "pointer.leave": [
165
+ POINTER_LEAVE: [
164
166
  {
165
167
  guard: "isOpenControlled",
166
168
  // We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point)
@@ -171,7 +173,7 @@ var machine = (0, import_core.createMachine)({
171
173
  actions: ["clearPointerMoveOpened", "invokeOnClose"]
172
174
  }
173
175
  ],
174
- close: [
176
+ CLOSE: [
175
177
  {
176
178
  guard: "isOpenControlled",
177
179
  // We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point)
@@ -185,13 +187,13 @@ var machine = (0, import_core.createMachine)({
185
187
  }
186
188
  },
187
189
  open: {
188
- effects: ["trackEscapeKey", "trackScroll", "trackPointerlockChange", "trackPositioning"],
190
+ effects: ["trackEscapeKey", "trackScroll", "trackPointerlockChange", "trackPositioning", "trackSafeArea"],
189
191
  entry: ["setGlobalId"],
190
192
  on: {
191
- "controlled.close": {
193
+ "CONTROLLED.CLOSE": {
192
194
  target: "closed"
193
195
  },
194
- close: [
196
+ CLOSE: [
195
197
  {
196
198
  guard: "isOpenControlled",
197
199
  actions: ["invokeOnClose"]
@@ -201,30 +203,34 @@ var machine = (0, import_core.createMachine)({
201
203
  actions: ["invokeOnClose"]
202
204
  }
203
205
  ],
204
- "pointer.leave": [
206
+ // Hoverable content means the pointer may be travelling to it (WCAG 1.4.13), so the
207
+ // safe area decides when it has really left.
208
+ POINTER_LEAVE: [
205
209
  {
206
- guard: "isVisible",
210
+ guard: and(not("isInteractive"), "isVisible"),
207
211
  target: "closing",
208
212
  actions: ["clearPointerMoveOpened"]
209
213
  },
210
214
  // == group ==
211
215
  {
212
- guard: "isOpenControlled",
216
+ guard: and(not("isInteractive"), "isOpenControlled"),
213
217
  actions: ["clearPointerMoveOpened", "invokeOnClose"]
214
218
  },
215
219
  {
220
+ guard: not("isInteractive"),
216
221
  target: "closed",
217
222
  actions: ["clearPointerMoveOpened", "invokeOnClose"]
218
223
  }
219
224
  ],
220
- "content.pointer.leave": {
221
- guard: "isInteractive",
222
- target: "closing"
225
+ "SAFE_AREA.EXIT": {
226
+ target: "closing",
227
+ // Same bookkeeping the `POINTER_LEAVE` path does, or `closed` refuses the next hover.
228
+ actions: ["clearPointerMoveOpened"]
223
229
  },
224
- "positioning.set": {
230
+ "POSITIONING.SET": {
225
231
  actions: ["reposition"]
226
232
  },
227
- "triggerValue.set": {
233
+ "TRIGGER_VALUE.SET": {
228
234
  // Transition to closing (which cleans up trackPositioning) then immediately back to open
229
235
  // This re-creates the positioning effect with the new trigger
230
236
  target: "closing",
@@ -235,7 +241,7 @@ var machine = (0, import_core.createMachine)({
235
241
  closing: {
236
242
  effects: ["trackPositioning", "waitForCloseDelay"],
237
243
  on: {
238
- "after.closeDelay": [
244
+ CLOSE_DELAY: [
239
245
  {
240
246
  guard: "isOpenControlled",
241
247
  actions: ["invokeOnClose"]
@@ -245,13 +251,13 @@ var machine = (0, import_core.createMachine)({
245
251
  actions: ["invokeOnClose"]
246
252
  }
247
253
  ],
248
- "controlled.close": {
254
+ "CONTROLLED.CLOSE": {
249
255
  target: "closed"
250
256
  },
251
- "controlled.open": {
257
+ "CONTROLLED.OPEN": {
252
258
  target: "open"
253
259
  },
254
- close: [
260
+ CLOSE: [
255
261
  {
256
262
  guard: "isOpenControlled",
257
263
  actions: ["invokeOnClose"]
@@ -261,29 +267,29 @@ var machine = (0, import_core.createMachine)({
261
267
  actions: ["invokeOnClose"]
262
268
  }
263
269
  ],
264
- "pointer.move": [
270
+ POINTER_MOVE: [
265
271
  {
266
272
  guard: "isOpenControlled",
267
273
  // We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point)
268
- actions: ["setPointerMoveOpened", "setTriggerValue", "invokeOnOpen", "toggleVisibility"]
274
+ actions: ["setPointerMoveOpened", "setIsPointer", "setTriggerValue", "invokeOnOpen", "toggleVisibility"]
269
275
  },
270
276
  {
271
277
  target: "open",
272
- actions: ["setPointerMoveOpened", "setTriggerValue", "invokeOnOpen"]
278
+ actions: ["setPointerMoveOpened", "setIsPointer", "setTriggerValue", "invokeOnOpen"]
273
279
  }
274
280
  ],
275
- "triggerValue.set": {
281
+ "TRIGGER_VALUE.SET": {
276
282
  target: "open",
277
283
  actions: ["setTriggerValue", "repositionImmediate"]
278
284
  },
279
- reopen: {
285
+ REOPEN: {
280
286
  target: "open"
281
287
  },
282
- "content.pointer.move": {
288
+ CONTENT_POINTER_MOVE: {
283
289
  guard: "isInteractive",
284
290
  target: "open"
285
291
  },
286
- "positioning.set": {
292
+ "POSITIONING.SET": {
287
293
  actions: ["reposition"]
288
294
  }
289
295
  }
@@ -316,10 +322,10 @@ var machine = (0, import_core.createMachine)({
316
322
  },
317
323
  closeIfDisabled: ({ prop, send }) => {
318
324
  if (!prop("disabled")) return;
319
- send({ type: "close", src: "disabled.change" });
325
+ send({ type: "CLOSE", src: "disabled.change" });
320
326
  },
321
327
  reposition: ({ context, event, prop, scope }) => {
322
- if (event.type !== "positioning.set") return;
328
+ if (event.type !== "POSITIONING.SET") return;
323
329
  const getPositionerEl2 = () => dom.getPositionerEl(scope);
324
330
  const getTriggerEl = () => dom.getActiveTriggerEl(scope, context.get("triggerValue"));
325
331
  (0, import_popper.getPlacement)(getTriggerEl, getPositionerEl2, {
@@ -345,7 +351,7 @@ var machine = (0, import_core.createMachine)({
345
351
  toggleVisibility: ({ prop, event, send }) => {
346
352
  queueMicrotask(() => {
347
353
  send({
348
- type: prop("open") ? "controlled.open" : "controlled.close",
354
+ type: prop("open") ? "CONTROLLED.OPEN" : "CONTROLLED.CLOSE",
349
355
  previousEvent: event
350
356
  });
351
357
  });
@@ -357,13 +363,19 @@ var machine = (0, import_core.createMachine)({
357
363
  clearPointerMoveOpened: ({ context }) => {
358
364
  context.set("hasPointerMoveOpened", null);
359
365
  },
366
+ setIsPointer: ({ context }) => {
367
+ context.set("isPointer", true);
368
+ },
369
+ clearIsPointer: ({ context }) => {
370
+ context.set("isPointer", false);
371
+ },
360
372
  setTriggerValue: ({ context, event }) => {
361
373
  if (event.value === void 0) return;
362
374
  context.set("triggerValue", event.value);
363
375
  },
364
376
  immediateReopen: ({ send }) => {
365
377
  queueMicrotask(() => {
366
- send({ type: "reopen" });
378
+ send({ type: "REOPEN" });
367
379
  });
368
380
  }
369
381
  },
@@ -371,6 +383,20 @@ var machine = (0, import_core.createMachine)({
371
383
  trackFocusVisible: ({ scope }) => {
372
384
  return (0, import_focus_visible.trackFocusVisible)({ root: scope.getRootNode?.() });
373
385
  },
386
+ // Only when hoverable: with `interactive: false` the content has `pointer-events: none`,
387
+ // so there is no journey to protect and its rect is unreachable.
388
+ trackSafeArea: ({ send, prop, scope, context }) => {
389
+ if (!prop("interactive")) return;
390
+ return (0, import_safe_area.trackSafeArea)({
391
+ getTriggerEl: () => dom.getActiveTriggerEl(scope, context.get("triggerValue")),
392
+ getContentEl: () => dom.getContentEl(scope),
393
+ openedByPointer: () => context.get("isPointer"),
394
+ defer: true,
395
+ onLeave() {
396
+ send({ type: "SAFE_AREA.EXIT" });
397
+ }
398
+ });
399
+ },
374
400
  trackPositioning: ({ context, prop, scope }) => {
375
401
  if (!context.get("currentPlacement")) {
376
402
  context.set("currentPlacement", prop("positioning").placement);
@@ -387,7 +413,7 @@ var machine = (0, import_core.createMachine)({
387
413
  },
388
414
  trackPointerlockChange: ({ send, scope }) => {
389
415
  const doc = scope.getDoc();
390
- const onChange = () => send({ type: "close", src: "pointerlock:change" });
416
+ const onChange = () => send({ type: "CLOSE", src: "pointerlock:change" });
391
417
  return (0, import_dom_query.addDomEvent)(doc, "pointerlockchange", onChange, false);
392
418
  },
393
419
  trackScroll: ({ send, prop, scope, context }) => {
@@ -398,7 +424,7 @@ var machine = (0, import_core.createMachine)({
398
424
  const overflowParents = (0, import_dom_query.getOverflowAncestors)(triggerEl);
399
425
  const cleanups = overflowParents.map((overflowParent) => {
400
426
  const onScroll = () => {
401
- send({ type: "close", src: "scroll" });
427
+ send({ type: "CLOSE", src: "scroll" });
402
428
  };
403
429
  return (0, import_dom_query.addDomEvent)(overflowParent, "scroll", onScroll, {
404
430
  passive: true,
@@ -415,7 +441,7 @@ var machine = (0, import_core.createMachine)({
415
441
  cleanup = import_tooltip.store.subscribe(() => {
416
442
  const id = import_tooltip.store.get("id");
417
443
  if (id !== null && id !== prop("id")) {
418
- send({ type: "close", src: "id.change" });
444
+ send({ type: "CLOSE", src: "id.change" });
419
445
  }
420
446
  });
421
447
  });
@@ -427,19 +453,19 @@ var machine = (0, import_core.createMachine)({
427
453
  if ((0, import_dom_query.isComposingEvent)(event)) return;
428
454
  if (event.key !== "Escape") return;
429
455
  event.stopPropagation();
430
- send({ type: "close", src: "keydown.escape" });
456
+ send({ type: "CLOSE", src: "keydown.escape" });
431
457
  };
432
458
  return (0, import_dom_query.addDomEvent)(document, "keydown", onKeyDown, true);
433
459
  },
434
460
  waitForOpenDelay: ({ send, prop, event }) => {
435
461
  const id = setTimeout(() => {
436
- send({ type: "after.openDelay", previousEvent: event });
462
+ send({ type: "OPEN_DELAY", previousEvent: event });
437
463
  }, prop("openDelay"));
438
464
  return () => clearTimeout(id);
439
465
  },
440
466
  waitForCloseDelay: ({ send, prop, event }) => {
441
467
  const id = setTimeout(() => {
442
- send({ type: "after.closeDelay", previousEvent: event });
468
+ send({ type: "CLOSE_DELAY", previousEvent: event });
443
469
  }, prop("closeDelay"));
444
470
  return () => clearTimeout(id);
445
471
  }
@@ -3,6 +3,7 @@ import { createGuards, createMachine } from "@zag-js/core";
3
3
  import { addDomEvent, getOverflowAncestors, isComposingEvent } from "@zag-js/dom-query";
4
4
  import { trackFocusVisible } from "@zag-js/focus-visible";
5
5
  import { getPlacement } from "@zag-js/popper";
6
+ import { trackSafeArea } from "@zag-js/safe-area";
6
7
  import { ensureProps } from "@zag-js/utils";
7
8
  import * as dom from "./tooltip.dom.mjs";
8
9
  import { store } from "./tooltip.store.mjs";
@@ -36,6 +37,7 @@ var machine = createMachine({
36
37
  context: ({ bindable, prop, scope }) => ({
37
38
  currentPlacement: bindable(() => ({ defaultValue: void 0 })),
38
39
  hasPointerMoveOpened: bindable(() => ({ defaultValue: null })),
40
+ isPointer: bindable(() => ({ defaultValue: false })),
39
41
  triggerValue: bindable(() => ({
40
42
  defaultValue: prop("defaultTriggerValue") ?? null,
41
43
  value: prop("triggerValue"),
@@ -59,18 +61,18 @@ var machine = createMachine({
59
61
  });
60
62
  },
61
63
  on: {
62
- "triggerValue.set": {
64
+ "TRIGGER_VALUE.SET": {
63
65
  actions: ["setTriggerValue", "repositionImmediate"]
64
66
  }
65
67
  },
66
68
  states: {
67
69
  closed: {
68
- entry: ["clearGlobalId"],
70
+ entry: ["clearGlobalId", "clearIsPointer"],
69
71
  on: {
70
- "controlled.open": {
72
+ "CONTROLLED.OPEN": {
71
73
  target: "open"
72
74
  },
73
- open: [
75
+ OPEN: [
74
76
  {
75
77
  guard: "isOpenControlled",
76
78
  actions: ["setTriggerValue", "invokeOnOpen"]
@@ -80,10 +82,10 @@ var machine = createMachine({
80
82
  actions: ["setTriggerValue", "invokeOnOpen"]
81
83
  }
82
84
  ],
83
- "pointer.leave": {
85
+ POINTER_LEAVE: {
84
86
  actions: ["clearPointerMoveOpened"]
85
87
  },
86
- "pointer.move": [
88
+ POINTER_MOVE: [
87
89
  {
88
90
  guard: and("noVisibleTooltip", not("hasPointerMoveOpened")),
89
91
  target: "opening",
@@ -92,7 +94,7 @@ var machine = createMachine({
92
94
  {
93
95
  guard: not("hasPointerMoveOpened"),
94
96
  target: "open",
95
- actions: ["setPointerMoveOpened", "invokeOnOpen", "setTriggerValue"]
97
+ actions: ["setPointerMoveOpened", "setIsPointer", "invokeOnOpen", "setTriggerValue"]
96
98
  }
97
99
  ]
98
100
  }
@@ -100,23 +102,23 @@ var machine = createMachine({
100
102
  opening: {
101
103
  effects: ["trackScroll", "trackPointerlockChange", "waitForOpenDelay"],
102
104
  on: {
103
- "after.openDelay": [
105
+ OPEN_DELAY: [
104
106
  {
105
107
  guard: "isOpenControlled",
106
- actions: ["setPointerMoveOpened", "invokeOnOpen"]
108
+ actions: ["setPointerMoveOpened", "setIsPointer", "invokeOnOpen"]
107
109
  },
108
110
  {
109
111
  target: "open",
110
- actions: ["setPointerMoveOpened", "invokeOnOpen"]
112
+ actions: ["setPointerMoveOpened", "setIsPointer", "invokeOnOpen"]
111
113
  }
112
114
  ],
113
- "controlled.open": {
115
+ "CONTROLLED.OPEN": {
114
116
  target: "open"
115
117
  },
116
- "controlled.close": {
118
+ "CONTROLLED.CLOSE": {
117
119
  target: "closed"
118
120
  },
119
- open: [
121
+ OPEN: [
120
122
  {
121
123
  guard: "isOpenControlled",
122
124
  actions: ["setTriggerValue", "invokeOnOpen"]
@@ -126,7 +128,7 @@ var machine = createMachine({
126
128
  actions: ["setTriggerValue", "invokeOnOpen"]
127
129
  }
128
130
  ],
129
- "pointer.leave": [
131
+ POINTER_LEAVE: [
130
132
  {
131
133
  guard: "isOpenControlled",
132
134
  // We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point)
@@ -137,7 +139,7 @@ var machine = createMachine({
137
139
  actions: ["clearPointerMoveOpened", "invokeOnClose"]
138
140
  }
139
141
  ],
140
- close: [
142
+ CLOSE: [
141
143
  {
142
144
  guard: "isOpenControlled",
143
145
  // We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point)
@@ -151,13 +153,13 @@ var machine = createMachine({
151
153
  }
152
154
  },
153
155
  open: {
154
- effects: ["trackEscapeKey", "trackScroll", "trackPointerlockChange", "trackPositioning"],
156
+ effects: ["trackEscapeKey", "trackScroll", "trackPointerlockChange", "trackPositioning", "trackSafeArea"],
155
157
  entry: ["setGlobalId"],
156
158
  on: {
157
- "controlled.close": {
159
+ "CONTROLLED.CLOSE": {
158
160
  target: "closed"
159
161
  },
160
- close: [
162
+ CLOSE: [
161
163
  {
162
164
  guard: "isOpenControlled",
163
165
  actions: ["invokeOnClose"]
@@ -167,30 +169,34 @@ var machine = createMachine({
167
169
  actions: ["invokeOnClose"]
168
170
  }
169
171
  ],
170
- "pointer.leave": [
172
+ // Hoverable content means the pointer may be travelling to it (WCAG 1.4.13), so the
173
+ // safe area decides when it has really left.
174
+ POINTER_LEAVE: [
171
175
  {
172
- guard: "isVisible",
176
+ guard: and(not("isInteractive"), "isVisible"),
173
177
  target: "closing",
174
178
  actions: ["clearPointerMoveOpened"]
175
179
  },
176
180
  // == group ==
177
181
  {
178
- guard: "isOpenControlled",
182
+ guard: and(not("isInteractive"), "isOpenControlled"),
179
183
  actions: ["clearPointerMoveOpened", "invokeOnClose"]
180
184
  },
181
185
  {
186
+ guard: not("isInteractive"),
182
187
  target: "closed",
183
188
  actions: ["clearPointerMoveOpened", "invokeOnClose"]
184
189
  }
185
190
  ],
186
- "content.pointer.leave": {
187
- guard: "isInteractive",
188
- target: "closing"
191
+ "SAFE_AREA.EXIT": {
192
+ target: "closing",
193
+ // Same bookkeeping the `POINTER_LEAVE` path does, or `closed` refuses the next hover.
194
+ actions: ["clearPointerMoveOpened"]
189
195
  },
190
- "positioning.set": {
196
+ "POSITIONING.SET": {
191
197
  actions: ["reposition"]
192
198
  },
193
- "triggerValue.set": {
199
+ "TRIGGER_VALUE.SET": {
194
200
  // Transition to closing (which cleans up trackPositioning) then immediately back to open
195
201
  // This re-creates the positioning effect with the new trigger
196
202
  target: "closing",
@@ -201,7 +207,7 @@ var machine = createMachine({
201
207
  closing: {
202
208
  effects: ["trackPositioning", "waitForCloseDelay"],
203
209
  on: {
204
- "after.closeDelay": [
210
+ CLOSE_DELAY: [
205
211
  {
206
212
  guard: "isOpenControlled",
207
213
  actions: ["invokeOnClose"]
@@ -211,13 +217,13 @@ var machine = createMachine({
211
217
  actions: ["invokeOnClose"]
212
218
  }
213
219
  ],
214
- "controlled.close": {
220
+ "CONTROLLED.CLOSE": {
215
221
  target: "closed"
216
222
  },
217
- "controlled.open": {
223
+ "CONTROLLED.OPEN": {
218
224
  target: "open"
219
225
  },
220
- close: [
226
+ CLOSE: [
221
227
  {
222
228
  guard: "isOpenControlled",
223
229
  actions: ["invokeOnClose"]
@@ -227,29 +233,29 @@ var machine = createMachine({
227
233
  actions: ["invokeOnClose"]
228
234
  }
229
235
  ],
230
- "pointer.move": [
236
+ POINTER_MOVE: [
231
237
  {
232
238
  guard: "isOpenControlled",
233
239
  // We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point)
234
- actions: ["setPointerMoveOpened", "setTriggerValue", "invokeOnOpen", "toggleVisibility"]
240
+ actions: ["setPointerMoveOpened", "setIsPointer", "setTriggerValue", "invokeOnOpen", "toggleVisibility"]
235
241
  },
236
242
  {
237
243
  target: "open",
238
- actions: ["setPointerMoveOpened", "setTriggerValue", "invokeOnOpen"]
244
+ actions: ["setPointerMoveOpened", "setIsPointer", "setTriggerValue", "invokeOnOpen"]
239
245
  }
240
246
  ],
241
- "triggerValue.set": {
247
+ "TRIGGER_VALUE.SET": {
242
248
  target: "open",
243
249
  actions: ["setTriggerValue", "repositionImmediate"]
244
250
  },
245
- reopen: {
251
+ REOPEN: {
246
252
  target: "open"
247
253
  },
248
- "content.pointer.move": {
254
+ CONTENT_POINTER_MOVE: {
249
255
  guard: "isInteractive",
250
256
  target: "open"
251
257
  },
252
- "positioning.set": {
258
+ "POSITIONING.SET": {
253
259
  actions: ["reposition"]
254
260
  }
255
261
  }
@@ -282,10 +288,10 @@ var machine = createMachine({
282
288
  },
283
289
  closeIfDisabled: ({ prop, send }) => {
284
290
  if (!prop("disabled")) return;
285
- send({ type: "close", src: "disabled.change" });
291
+ send({ type: "CLOSE", src: "disabled.change" });
286
292
  },
287
293
  reposition: ({ context, event, prop, scope }) => {
288
- if (event.type !== "positioning.set") return;
294
+ if (event.type !== "POSITIONING.SET") return;
289
295
  const getPositionerEl2 = () => dom.getPositionerEl(scope);
290
296
  const getTriggerEl = () => dom.getActiveTriggerEl(scope, context.get("triggerValue"));
291
297
  getPlacement(getTriggerEl, getPositionerEl2, {
@@ -311,7 +317,7 @@ var machine = createMachine({
311
317
  toggleVisibility: ({ prop, event, send }) => {
312
318
  queueMicrotask(() => {
313
319
  send({
314
- type: prop("open") ? "controlled.open" : "controlled.close",
320
+ type: prop("open") ? "CONTROLLED.OPEN" : "CONTROLLED.CLOSE",
315
321
  previousEvent: event
316
322
  });
317
323
  });
@@ -323,13 +329,19 @@ var machine = createMachine({
323
329
  clearPointerMoveOpened: ({ context }) => {
324
330
  context.set("hasPointerMoveOpened", null);
325
331
  },
332
+ setIsPointer: ({ context }) => {
333
+ context.set("isPointer", true);
334
+ },
335
+ clearIsPointer: ({ context }) => {
336
+ context.set("isPointer", false);
337
+ },
326
338
  setTriggerValue: ({ context, event }) => {
327
339
  if (event.value === void 0) return;
328
340
  context.set("triggerValue", event.value);
329
341
  },
330
342
  immediateReopen: ({ send }) => {
331
343
  queueMicrotask(() => {
332
- send({ type: "reopen" });
344
+ send({ type: "REOPEN" });
333
345
  });
334
346
  }
335
347
  },
@@ -337,6 +349,20 @@ var machine = createMachine({
337
349
  trackFocusVisible: ({ scope }) => {
338
350
  return trackFocusVisible({ root: scope.getRootNode?.() });
339
351
  },
352
+ // Only when hoverable: with `interactive: false` the content has `pointer-events: none`,
353
+ // so there is no journey to protect and its rect is unreachable.
354
+ trackSafeArea: ({ send, prop, scope, context }) => {
355
+ if (!prop("interactive")) return;
356
+ return trackSafeArea({
357
+ getTriggerEl: () => dom.getActiveTriggerEl(scope, context.get("triggerValue")),
358
+ getContentEl: () => dom.getContentEl(scope),
359
+ openedByPointer: () => context.get("isPointer"),
360
+ defer: true,
361
+ onLeave() {
362
+ send({ type: "SAFE_AREA.EXIT" });
363
+ }
364
+ });
365
+ },
340
366
  trackPositioning: ({ context, prop, scope }) => {
341
367
  if (!context.get("currentPlacement")) {
342
368
  context.set("currentPlacement", prop("positioning").placement);
@@ -353,7 +379,7 @@ var machine = createMachine({
353
379
  },
354
380
  trackPointerlockChange: ({ send, scope }) => {
355
381
  const doc = scope.getDoc();
356
- const onChange = () => send({ type: "close", src: "pointerlock:change" });
382
+ const onChange = () => send({ type: "CLOSE", src: "pointerlock:change" });
357
383
  return addDomEvent(doc, "pointerlockchange", onChange, false);
358
384
  },
359
385
  trackScroll: ({ send, prop, scope, context }) => {
@@ -364,7 +390,7 @@ var machine = createMachine({
364
390
  const overflowParents = getOverflowAncestors(triggerEl);
365
391
  const cleanups = overflowParents.map((overflowParent) => {
366
392
  const onScroll = () => {
367
- send({ type: "close", src: "scroll" });
393
+ send({ type: "CLOSE", src: "scroll" });
368
394
  };
369
395
  return addDomEvent(overflowParent, "scroll", onScroll, {
370
396
  passive: true,
@@ -381,7 +407,7 @@ var machine = createMachine({
381
407
  cleanup = store.subscribe(() => {
382
408
  const id = store.get("id");
383
409
  if (id !== null && id !== prop("id")) {
384
- send({ type: "close", src: "id.change" });
410
+ send({ type: "CLOSE", src: "id.change" });
385
411
  }
386
412
  });
387
413
  });
@@ -393,19 +419,19 @@ var machine = createMachine({
393
419
  if (isComposingEvent(event)) return;
394
420
  if (event.key !== "Escape") return;
395
421
  event.stopPropagation();
396
- send({ type: "close", src: "keydown.escape" });
422
+ send({ type: "CLOSE", src: "keydown.escape" });
397
423
  };
398
424
  return addDomEvent(document, "keydown", onKeyDown, true);
399
425
  },
400
426
  waitForOpenDelay: ({ send, prop, event }) => {
401
427
  const id = setTimeout(() => {
402
- send({ type: "after.openDelay", previousEvent: event });
428
+ send({ type: "OPEN_DELAY", previousEvent: event });
403
429
  }, prop("openDelay"));
404
430
  return () => clearTimeout(id);
405
431
  },
406
432
  waitForCloseDelay: ({ send, prop, event }) => {
407
433
  const id = setTimeout(() => {
408
- send({ type: "after.closeDelay", previousEvent: event });
434
+ send({ type: "CLOSE_DELAY", previousEvent: event });
409
435
  }, prop("closeDelay"));
410
436
  return () => clearTimeout(id);
411
437
  }
@@ -1,7 +1,7 @@
1
1
  import { Machine, EventObject, Service } from '@zag-js/core';
2
2
  import { PositioningOptions, Placement, PlacementSide } from '@zag-js/popper';
3
3
  export { Placement, PositioningOptions } from '@zag-js/popper';
4
- import { PropTypes, RequiredBy, DirectionProperty, CommonProperties } from '@zag-js/types';
4
+ import { PropTypes, DirectionProperty, CommonProperties } from '@zag-js/types';
5
5
 
6
6
  interface OpenChangeDetails {
7
7
  open: boolean;
@@ -16,12 +16,12 @@ interface TriggerValueChangeDetails {
16
16
  */
17
17
  triggerElement: HTMLElement | null;
18
18
  }
19
- type ElementIds = Partial<{
20
- trigger: string | ((value?: string) => string);
21
- content: string;
22
- arrow: string;
23
- positioner: string;
24
- }>;
19
+ interface ElementIds {
20
+ trigger?: (string | ((value?: string) => string)) | undefined;
21
+ content?: string | undefined;
22
+ arrow?: string | undefined;
23
+ positioner?: string | undefined;
24
+ }
25
25
  interface TooltipProps extends DirectionProperty, CommonProperties {
26
26
  /**
27
27
  * The ids of the elements in the tooltip. Useful for composition.
@@ -107,10 +107,12 @@ interface TooltipProps extends DirectionProperty, CommonProperties {
107
107
  type PropsWithDefault = "openDelay" | "closeDelay" | "closeOnPointerDown" | "closeOnEscape" | "closeOnScroll" | "closeOnClick" | "interactive" | "id" | "positioning";
108
108
  interface TooltipSchema {
109
109
  state: "open" | "closed" | "opening" | "closing";
110
- props: RequiredBy<TooltipProps, PropsWithDefault>;
110
+ props: TooltipProps;
111
+ defaultPropKey: PropsWithDefault;
111
112
  context: {
112
113
  currentPlacement: Placement | undefined;
113
114
  hasPointerMoveOpened: string | null;
115
+ isPointer: boolean;
114
116
  triggerValue: string | null;
115
117
  };
116
118
  event: EventObject;
@@ -1,7 +1,7 @@
1
1
  import { Machine, EventObject, Service } from '@zag-js/core';
2
2
  import { PositioningOptions, Placement, PlacementSide } from '@zag-js/popper';
3
3
  export { Placement, PositioningOptions } from '@zag-js/popper';
4
- import { PropTypes, RequiredBy, DirectionProperty, CommonProperties } from '@zag-js/types';
4
+ import { PropTypes, DirectionProperty, CommonProperties } from '@zag-js/types';
5
5
 
6
6
  interface OpenChangeDetails {
7
7
  open: boolean;
@@ -16,12 +16,12 @@ interface TriggerValueChangeDetails {
16
16
  */
17
17
  triggerElement: HTMLElement | null;
18
18
  }
19
- type ElementIds = Partial<{
20
- trigger: string | ((value?: string) => string);
21
- content: string;
22
- arrow: string;
23
- positioner: string;
24
- }>;
19
+ interface ElementIds {
20
+ trigger?: (string | ((value?: string) => string)) | undefined;
21
+ content?: string | undefined;
22
+ arrow?: string | undefined;
23
+ positioner?: string | undefined;
24
+ }
25
25
  interface TooltipProps extends DirectionProperty, CommonProperties {
26
26
  /**
27
27
  * The ids of the elements in the tooltip. Useful for composition.
@@ -107,10 +107,12 @@ interface TooltipProps extends DirectionProperty, CommonProperties {
107
107
  type PropsWithDefault = "openDelay" | "closeDelay" | "closeOnPointerDown" | "closeOnEscape" | "closeOnScroll" | "closeOnClick" | "interactive" | "id" | "positioning";
108
108
  interface TooltipSchema {
109
109
  state: "open" | "closed" | "opening" | "closing";
110
- props: RequiredBy<TooltipProps, PropsWithDefault>;
110
+ props: TooltipProps;
111
+ defaultPropKey: PropsWithDefault;
111
112
  context: {
112
113
  currentPlacement: Placement | undefined;
113
114
  hasPointerMoveOpened: string | null;
115
+ isPointer: boolean;
114
116
  triggerValue: string | null;
115
117
  };
116
118
  event: EventObject;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/tooltip",
3
- "version": "2.0.0-next.1",
3
+ "version": "2.0.0-next.2",
4
4
  "description": "Core logic for the tooltip widget implemented as a state machine",
5
5
  "keywords": [
6
6
  "js",
@@ -29,13 +29,14 @@
29
29
  "url": "https://github.com/chakra-ui/zag/issues"
30
30
  },
31
31
  "dependencies": {
32
- "@zag-js/anatomy": "2.0.0-next.1",
33
- "@zag-js/core": "2.0.0-next.1",
34
- "@zag-js/popper": "2.0.0-next.1",
35
- "@zag-js/focus-visible": "2.0.0-next.1",
36
- "@zag-js/dom-query": "2.0.0-next.1",
37
- "@zag-js/utils": "2.0.0-next.1",
38
- "@zag-js/types": "2.0.0-next.1"
32
+ "@zag-js/anatomy": "2.0.0-next.2",
33
+ "@zag-js/core": "2.0.0-next.2",
34
+ "@zag-js/dom-query": "2.0.0-next.2",
35
+ "@zag-js/focus-visible": "2.0.0-next.2",
36
+ "@zag-js/popper": "2.0.0-next.2",
37
+ "@zag-js/safe-area": "2.0.0-next.2",
38
+ "@zag-js/types": "2.0.0-next.2",
39
+ "@zag-js/utils": "2.0.0-next.2"
39
40
  },
40
41
  "devDependencies": {
41
42
  "clean-package": "2.2.0"
@@ -69,7 +70,7 @@
69
70
  },
70
71
  "scripts": {
71
72
  "build": "tsup",
72
- "lint": "eslint src",
73
+ "lint": "oxlint src",
73
74
  "typecheck": "tsc --noEmit"
74
75
  }
75
76
  }