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

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.
@@ -67,16 +67,14 @@ function connect(service, normalize) {
67
67
  return {
68
68
  open,
69
69
  setOpen(nextOpen) {
70
- const open2 = state.matches("open", "closing");
71
- if (open2 === nextOpen) return;
72
- send({ type: nextOpen ? "open" : "close" });
70
+ send({ type: nextOpen ? "OPEN" : "CLOSE", replaces: "open" });
73
71
  },
74
72
  triggerValue,
75
73
  setTriggerValue(value) {
76
- send({ type: "triggerValue.set", value: value ?? void 0 });
74
+ send({ type: "TRIGGER_VALUE.SET", value: value ?? void 0 });
77
75
  },
78
76
  reposition(options = {}) {
79
- send({ type: "positioning.set", options });
77
+ send({ type: "POSITIONING.SET", options });
80
78
  },
81
79
  getTriggerState,
82
80
  getTriggerProps(props = {}) {
@@ -97,14 +95,14 @@ function connect(service, normalize) {
97
95
  if (disabled) return;
98
96
  if (!prop("closeOnClick")) return;
99
97
  const shouldSwitch = open && value != null && !current;
100
- send({ type: shouldSwitch ? "triggerValue.set" : "close", src: "trigger.click", value, triggerId });
98
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "CLOSE", src: "trigger.click", value, triggerId });
101
99
  },
102
100
  onFocus(event) {
103
101
  if (event.defaultPrevented) return;
104
102
  if (disabled) return;
105
103
  if (!(0, import_focus_visible.isFocusVisible)()) return;
106
104
  const shouldSwitch = open && value != null && !current;
107
- send({ type: shouldSwitch ? "triggerValue.set" : "open", src: "trigger.focus", value, triggerId });
105
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "OPEN", src: "trigger.focus", value, triggerId });
108
106
  },
109
107
  onBlur(event) {
110
108
  if (event.defaultPrevented) return;
@@ -113,7 +111,7 @@ function connect(service, normalize) {
113
111
  const activeEl = event.relatedTarget ?? scope.getDoc().activeElement;
114
112
  const focusedAnotherTrigger = activeEl?.closest(`[${import_tooltip.parts.trigger.attr}="${scope.id}"]`) != null;
115
113
  if (!focusedAnotherTrigger) {
116
- send({ type: "close", src: "trigger.blur", value, triggerId });
114
+ send({ type: "CLOSE", src: "trigger.blur", value, triggerId });
117
115
  }
118
116
  },
119
117
  onPointerDown(event) {
@@ -122,7 +120,7 @@ function connect(service, normalize) {
122
120
  if (!(0, import_dom_query.isLeftClick)(event)) return;
123
121
  if (!prop("closeOnPointerDown")) return;
124
122
  if (id === import_tooltip2.store.get("id")) {
125
- send({ type: "close", src: "trigger.pointerdown", value, triggerId });
123
+ send({ type: "CLOSE", src: "trigger.pointerdown", value, triggerId });
126
124
  }
127
125
  },
128
126
  onPointerMove(event) {
@@ -130,21 +128,21 @@ function connect(service, normalize) {
130
128
  if (disabled) return;
131
129
  if (event.pointerType === "touch") return;
132
130
  const shouldSwitch = open && value != null && !current;
133
- send({ type: shouldSwitch ? "triggerValue.set" : "pointer.move", value, triggerId });
131
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "POINTER_MOVE", value, triggerId });
134
132
  },
135
133
  onPointerOver(event) {
136
134
  if (event.defaultPrevented) return;
137
135
  if (disabled) return;
138
136
  if (event.pointerType === "touch") return;
139
- send({ type: "pointer.move", value, triggerId });
137
+ send({ type: "POINTER_MOVE", value, triggerId });
140
138
  },
141
139
  onPointerLeave() {
142
140
  if (disabled) return;
143
- send({ type: "pointer.leave" });
141
+ send({ type: "POINTER_LEAVE" });
144
142
  },
145
143
  onPointerCancel() {
146
144
  if (disabled) return;
147
- send({ type: "pointer.leave" });
145
+ send({ type: "POINTER_LEAVE" });
148
146
  }
149
147
  });
150
148
  },
@@ -183,10 +181,7 @@ function connect(service, normalize) {
183
181
  "data-placement": contentState.placement,
184
182
  "data-side": contentState.side,
185
183
  onPointerEnter() {
186
- send({ type: "content.pointer.move" });
187
- },
188
- onPointerLeave() {
189
- send({ type: "content.pointer.leave" });
184
+ send({ type: "CONTENT_POINTER_MOVE" });
190
185
  },
191
186
  style: {
192
187
  pointerEvents: prop("interactive") ? "auto" : "none"
@@ -33,16 +33,14 @@ function connect(service, normalize) {
33
33
  return {
34
34
  open,
35
35
  setOpen(nextOpen) {
36
- const open2 = state.matches("open", "closing");
37
- if (open2 === nextOpen) return;
38
- send({ type: nextOpen ? "open" : "close" });
36
+ send({ type: nextOpen ? "OPEN" : "CLOSE", replaces: "open" });
39
37
  },
40
38
  triggerValue,
41
39
  setTriggerValue(value) {
42
- send({ type: "triggerValue.set", value: value ?? void 0 });
40
+ send({ type: "TRIGGER_VALUE.SET", value: value ?? void 0 });
43
41
  },
44
42
  reposition(options = {}) {
45
- send({ type: "positioning.set", options });
43
+ send({ type: "POSITIONING.SET", options });
46
44
  },
47
45
  getTriggerState,
48
46
  getTriggerProps(props = {}) {
@@ -63,14 +61,14 @@ function connect(service, normalize) {
63
61
  if (disabled) return;
64
62
  if (!prop("closeOnClick")) return;
65
63
  const shouldSwitch = open && value != null && !current;
66
- send({ type: shouldSwitch ? "triggerValue.set" : "close", src: "trigger.click", value, triggerId });
64
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "CLOSE", src: "trigger.click", value, triggerId });
67
65
  },
68
66
  onFocus(event) {
69
67
  if (event.defaultPrevented) return;
70
68
  if (disabled) return;
71
69
  if (!isFocusVisible()) return;
72
70
  const shouldSwitch = open && value != null && !current;
73
- send({ type: shouldSwitch ? "triggerValue.set" : "open", src: "trigger.focus", value, triggerId });
71
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "OPEN", src: "trigger.focus", value, triggerId });
74
72
  },
75
73
  onBlur(event) {
76
74
  if (event.defaultPrevented) return;
@@ -79,7 +77,7 @@ function connect(service, normalize) {
79
77
  const activeEl = event.relatedTarget ?? scope.getDoc().activeElement;
80
78
  const focusedAnotherTrigger = activeEl?.closest(`[${parts.trigger.attr}="${scope.id}"]`) != null;
81
79
  if (!focusedAnotherTrigger) {
82
- send({ type: "close", src: "trigger.blur", value, triggerId });
80
+ send({ type: "CLOSE", src: "trigger.blur", value, triggerId });
83
81
  }
84
82
  },
85
83
  onPointerDown(event) {
@@ -88,7 +86,7 @@ function connect(service, normalize) {
88
86
  if (!isLeftClick(event)) return;
89
87
  if (!prop("closeOnPointerDown")) return;
90
88
  if (id === store.get("id")) {
91
- send({ type: "close", src: "trigger.pointerdown", value, triggerId });
89
+ send({ type: "CLOSE", src: "trigger.pointerdown", value, triggerId });
92
90
  }
93
91
  },
94
92
  onPointerMove(event) {
@@ -96,21 +94,21 @@ function connect(service, normalize) {
96
94
  if (disabled) return;
97
95
  if (event.pointerType === "touch") return;
98
96
  const shouldSwitch = open && value != null && !current;
99
- send({ type: shouldSwitch ? "triggerValue.set" : "pointer.move", value, triggerId });
97
+ send({ type: shouldSwitch ? "TRIGGER_VALUE.SET" : "POINTER_MOVE", value, triggerId });
100
98
  },
101
99
  onPointerOver(event) {
102
100
  if (event.defaultPrevented) return;
103
101
  if (disabled) return;
104
102
  if (event.pointerType === "touch") return;
105
- send({ type: "pointer.move", value, triggerId });
103
+ send({ type: "POINTER_MOVE", value, triggerId });
106
104
  },
107
105
  onPointerLeave() {
108
106
  if (disabled) return;
109
- send({ type: "pointer.leave" });
107
+ send({ type: "POINTER_LEAVE" });
110
108
  },
111
109
  onPointerCancel() {
112
110
  if (disabled) return;
113
- send({ type: "pointer.leave" });
111
+ send({ type: "POINTER_LEAVE" });
114
112
  }
115
113
  });
116
114
  },
@@ -149,10 +147,7 @@ function connect(service, normalize) {
149
147
  "data-placement": contentState.placement,
150
148
  "data-side": contentState.side,
151
149
  onPointerEnter() {
152
- send({ type: "content.pointer.move" });
153
- },
154
- onPointerLeave() {
155
- send({ type: "content.pointer.leave" });
150
+ send({ type: "CONTENT_POINTER_MOVE" });
156
151
  },
157
152
  style: {
158
153
  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,13 +322,14 @@ 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, {
332
+ getArrowElement: () => dom.getArrowEl(scope),
326
333
  ...prop("positioning"),
327
334
  ...event.options,
328
335
  listeners: false,
@@ -336,6 +343,7 @@ var machine = (0, import_core.createMachine)({
336
343
  const getPositionerEl2 = () => dom.getPositionerEl(scope);
337
344
  const getTriggerEl = () => dom.getActiveTriggerEl(scope, triggerValue);
338
345
  return (0, import_popper.getPlacement)(getTriggerEl, getPositionerEl2, {
346
+ getArrowElement: () => dom.getArrowEl(scope),
339
347
  ...prop("positioning"),
340
348
  onComplete(data) {
341
349
  context.set("currentPlacement", data.placement);
@@ -345,7 +353,7 @@ var machine = (0, import_core.createMachine)({
345
353
  toggleVisibility: ({ prop, event, send }) => {
346
354
  queueMicrotask(() => {
347
355
  send({
348
- type: prop("open") ? "controlled.open" : "controlled.close",
356
+ type: prop("open") ? "CONTROLLED.OPEN" : "CONTROLLED.CLOSE",
349
357
  previousEvent: event
350
358
  });
351
359
  });
@@ -357,13 +365,19 @@ var machine = (0, import_core.createMachine)({
357
365
  clearPointerMoveOpened: ({ context }) => {
358
366
  context.set("hasPointerMoveOpened", null);
359
367
  },
368
+ setIsPointer: ({ context }) => {
369
+ context.set("isPointer", true);
370
+ },
371
+ clearIsPointer: ({ context }) => {
372
+ context.set("isPointer", false);
373
+ },
360
374
  setTriggerValue: ({ context, event }) => {
361
375
  if (event.value === void 0) return;
362
376
  context.set("triggerValue", event.value);
363
377
  },
364
378
  immediateReopen: ({ send }) => {
365
379
  queueMicrotask(() => {
366
- send({ type: "reopen" });
380
+ send({ type: "REOPEN" });
367
381
  });
368
382
  }
369
383
  },
@@ -371,6 +385,20 @@ var machine = (0, import_core.createMachine)({
371
385
  trackFocusVisible: ({ scope }) => {
372
386
  return (0, import_focus_visible.trackFocusVisible)({ root: scope.getRootNode?.() });
373
387
  },
388
+ // Only when hoverable: with `interactive: false` the content has `pointer-events: none`,
389
+ // so there is no journey to protect and its rect is unreachable.
390
+ trackSafeArea: ({ send, prop, scope, context }) => {
391
+ if (!prop("interactive")) return;
392
+ return (0, import_safe_area.trackSafeArea)({
393
+ getTriggerEl: () => dom.getActiveTriggerEl(scope, context.get("triggerValue")),
394
+ getContentEl: () => dom.getContentEl(scope),
395
+ openedByPointer: () => context.get("isPointer"),
396
+ defer: true,
397
+ onLeave() {
398
+ send({ type: "SAFE_AREA.EXIT" });
399
+ }
400
+ });
401
+ },
374
402
  trackPositioning: ({ context, prop, scope }) => {
375
403
  if (!context.get("currentPlacement")) {
376
404
  context.set("currentPlacement", prop("positioning").placement);
@@ -378,6 +406,7 @@ var machine = (0, import_core.createMachine)({
378
406
  const getPositionerEl2 = () => dom.getPositionerEl(scope);
379
407
  const getTriggerEl = () => dom.getActiveTriggerEl(scope, context.get("triggerValue"));
380
408
  return (0, import_popper.getPlacement)(getTriggerEl, getPositionerEl2, {
409
+ getArrowElement: () => dom.getArrowEl(scope),
381
410
  ...prop("positioning"),
382
411
  defer: true,
383
412
  onComplete(data) {
@@ -387,7 +416,7 @@ var machine = (0, import_core.createMachine)({
387
416
  },
388
417
  trackPointerlockChange: ({ send, scope }) => {
389
418
  const doc = scope.getDoc();
390
- const onChange = () => send({ type: "close", src: "pointerlock:change" });
419
+ const onChange = () => send({ type: "CLOSE", src: "pointerlock:change" });
391
420
  return (0, import_dom_query.addDomEvent)(doc, "pointerlockchange", onChange, false);
392
421
  },
393
422
  trackScroll: ({ send, prop, scope, context }) => {
@@ -398,7 +427,7 @@ var machine = (0, import_core.createMachine)({
398
427
  const overflowParents = (0, import_dom_query.getOverflowAncestors)(triggerEl);
399
428
  const cleanups = overflowParents.map((overflowParent) => {
400
429
  const onScroll = () => {
401
- send({ type: "close", src: "scroll" });
430
+ send({ type: "CLOSE", src: "scroll" });
402
431
  };
403
432
  return (0, import_dom_query.addDomEvent)(overflowParent, "scroll", onScroll, {
404
433
  passive: true,
@@ -415,7 +444,7 @@ var machine = (0, import_core.createMachine)({
415
444
  cleanup = import_tooltip.store.subscribe(() => {
416
445
  const id = import_tooltip.store.get("id");
417
446
  if (id !== null && id !== prop("id")) {
418
- send({ type: "close", src: "id.change" });
447
+ send({ type: "CLOSE", src: "id.change" });
419
448
  }
420
449
  });
421
450
  });
@@ -427,19 +456,19 @@ var machine = (0, import_core.createMachine)({
427
456
  if ((0, import_dom_query.isComposingEvent)(event)) return;
428
457
  if (event.key !== "Escape") return;
429
458
  event.stopPropagation();
430
- send({ type: "close", src: "keydown.escape" });
459
+ send({ type: "CLOSE", src: "keydown.escape" });
431
460
  };
432
461
  return (0, import_dom_query.addDomEvent)(document, "keydown", onKeyDown, true);
433
462
  },
434
463
  waitForOpenDelay: ({ send, prop, event }) => {
435
464
  const id = setTimeout(() => {
436
- send({ type: "after.openDelay", previousEvent: event });
465
+ send({ type: "OPEN_DELAY", previousEvent: event });
437
466
  }, prop("openDelay"));
438
467
  return () => clearTimeout(id);
439
468
  },
440
469
  waitForCloseDelay: ({ send, prop, event }) => {
441
470
  const id = setTimeout(() => {
442
- send({ type: "after.closeDelay", previousEvent: event });
471
+ send({ type: "CLOSE_DELAY", previousEvent: event });
443
472
  }, prop("closeDelay"));
444
473
  return () => clearTimeout(id);
445
474
  }
@@ -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,13 +288,14 @@ 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, {
298
+ getArrowElement: () => dom.getArrowEl(scope),
292
299
  ...prop("positioning"),
293
300
  ...event.options,
294
301
  listeners: false,
@@ -302,6 +309,7 @@ var machine = createMachine({
302
309
  const getPositionerEl2 = () => dom.getPositionerEl(scope);
303
310
  const getTriggerEl = () => dom.getActiveTriggerEl(scope, triggerValue);
304
311
  return getPlacement(getTriggerEl, getPositionerEl2, {
312
+ getArrowElement: () => dom.getArrowEl(scope),
305
313
  ...prop("positioning"),
306
314
  onComplete(data) {
307
315
  context.set("currentPlacement", data.placement);
@@ -311,7 +319,7 @@ var machine = createMachine({
311
319
  toggleVisibility: ({ prop, event, send }) => {
312
320
  queueMicrotask(() => {
313
321
  send({
314
- type: prop("open") ? "controlled.open" : "controlled.close",
322
+ type: prop("open") ? "CONTROLLED.OPEN" : "CONTROLLED.CLOSE",
315
323
  previousEvent: event
316
324
  });
317
325
  });
@@ -323,13 +331,19 @@ var machine = createMachine({
323
331
  clearPointerMoveOpened: ({ context }) => {
324
332
  context.set("hasPointerMoveOpened", null);
325
333
  },
334
+ setIsPointer: ({ context }) => {
335
+ context.set("isPointer", true);
336
+ },
337
+ clearIsPointer: ({ context }) => {
338
+ context.set("isPointer", false);
339
+ },
326
340
  setTriggerValue: ({ context, event }) => {
327
341
  if (event.value === void 0) return;
328
342
  context.set("triggerValue", event.value);
329
343
  },
330
344
  immediateReopen: ({ send }) => {
331
345
  queueMicrotask(() => {
332
- send({ type: "reopen" });
346
+ send({ type: "REOPEN" });
333
347
  });
334
348
  }
335
349
  },
@@ -337,6 +351,20 @@ var machine = createMachine({
337
351
  trackFocusVisible: ({ scope }) => {
338
352
  return trackFocusVisible({ root: scope.getRootNode?.() });
339
353
  },
354
+ // Only when hoverable: with `interactive: false` the content has `pointer-events: none`,
355
+ // so there is no journey to protect and its rect is unreachable.
356
+ trackSafeArea: ({ send, prop, scope, context }) => {
357
+ if (!prop("interactive")) return;
358
+ return trackSafeArea({
359
+ getTriggerEl: () => dom.getActiveTriggerEl(scope, context.get("triggerValue")),
360
+ getContentEl: () => dom.getContentEl(scope),
361
+ openedByPointer: () => context.get("isPointer"),
362
+ defer: true,
363
+ onLeave() {
364
+ send({ type: "SAFE_AREA.EXIT" });
365
+ }
366
+ });
367
+ },
340
368
  trackPositioning: ({ context, prop, scope }) => {
341
369
  if (!context.get("currentPlacement")) {
342
370
  context.set("currentPlacement", prop("positioning").placement);
@@ -344,6 +372,7 @@ var machine = createMachine({
344
372
  const getPositionerEl2 = () => dom.getPositionerEl(scope);
345
373
  const getTriggerEl = () => dom.getActiveTriggerEl(scope, context.get("triggerValue"));
346
374
  return getPlacement(getTriggerEl, getPositionerEl2, {
375
+ getArrowElement: () => dom.getArrowEl(scope),
347
376
  ...prop("positioning"),
348
377
  defer: true,
349
378
  onComplete(data) {
@@ -353,7 +382,7 @@ var machine = createMachine({
353
382
  },
354
383
  trackPointerlockChange: ({ send, scope }) => {
355
384
  const doc = scope.getDoc();
356
- const onChange = () => send({ type: "close", src: "pointerlock:change" });
385
+ const onChange = () => send({ type: "CLOSE", src: "pointerlock:change" });
357
386
  return addDomEvent(doc, "pointerlockchange", onChange, false);
358
387
  },
359
388
  trackScroll: ({ send, prop, scope, context }) => {
@@ -364,7 +393,7 @@ var machine = createMachine({
364
393
  const overflowParents = getOverflowAncestors(triggerEl);
365
394
  const cleanups = overflowParents.map((overflowParent) => {
366
395
  const onScroll = () => {
367
- send({ type: "close", src: "scroll" });
396
+ send({ type: "CLOSE", src: "scroll" });
368
397
  };
369
398
  return addDomEvent(overflowParent, "scroll", onScroll, {
370
399
  passive: true,
@@ -381,7 +410,7 @@ var machine = createMachine({
381
410
  cleanup = store.subscribe(() => {
382
411
  const id = store.get("id");
383
412
  if (id !== null && id !== prop("id")) {
384
- send({ type: "close", src: "id.change" });
413
+ send({ type: "CLOSE", src: "id.change" });
385
414
  }
386
415
  });
387
416
  });
@@ -393,19 +422,19 @@ var machine = createMachine({
393
422
  if (isComposingEvent(event)) return;
394
423
  if (event.key !== "Escape") return;
395
424
  event.stopPropagation();
396
- send({ type: "close", src: "keydown.escape" });
425
+ send({ type: "CLOSE", src: "keydown.escape" });
397
426
  };
398
427
  return addDomEvent(document, "keydown", onKeyDown, true);
399
428
  },
400
429
  waitForOpenDelay: ({ send, prop, event }) => {
401
430
  const id = setTimeout(() => {
402
- send({ type: "after.openDelay", previousEvent: event });
431
+ send({ type: "OPEN_DELAY", previousEvent: event });
403
432
  }, prop("openDelay"));
404
433
  return () => clearTimeout(id);
405
434
  },
406
435
  waitForCloseDelay: ({ send, prop, event }) => {
407
436
  const id = setTimeout(() => {
408
- send({ type: "after.closeDelay", previousEvent: event });
437
+ send({ type: "CLOSE_DELAY", previousEvent: event });
409
438
  }, prop("closeDelay"));
410
439
  return () => clearTimeout(id);
411
440
  }
@@ -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.3",
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.3",
33
+ "@zag-js/core": "2.0.0-next.3",
34
+ "@zag-js/dom-query": "2.0.0-next.3",
35
+ "@zag-js/focus-visible": "2.0.0-next.3",
36
+ "@zag-js/popper": "2.0.0-next.3",
37
+ "@zag-js/safe-area": "2.0.0-next.3",
38
+ "@zag-js/types": "2.0.0-next.3",
39
+ "@zag-js/utils": "2.0.0-next.3"
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
  }