@zag-js/interact-outside 0.56.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,20 +23,35 @@ __export(src_exports, {
23
23
  trackInteractOutside: () => trackInteractOutside
24
24
  });
25
25
  module.exports = __toCommonJS(src_exports);
26
- var import_dom_event = require("@zag-js/dom-event");
26
+ var import_dom_event2 = require("@zag-js/dom-event");
27
27
  var import_dom_query = require("@zag-js/dom-query");
28
28
  var import_utils = require("@zag-js/utils");
29
29
 
30
30
  // src/get-window-frames.ts
31
+ var import_dom_event = require("@zag-js/dom-event");
31
32
  function getWindowFrames(win) {
32
33
  const frames = {
33
34
  each(cb) {
34
35
  for (let i = 0; i < win.frames?.length; i += 1) {
35
36
  const frame = win.frames[i];
36
- if (frame)
37
- cb(frame);
37
+ if (frame) cb(frame);
38
38
  }
39
39
  },
40
+ queueBeforeEvent(event, listener) {
41
+ const cleanup = /* @__PURE__ */ new Set();
42
+ frames.each((frame) => {
43
+ try {
44
+ cleanup.add((0, import_dom_event.queueBeforeEvent)(frame.document, event, listener));
45
+ } catch {
46
+ }
47
+ });
48
+ return () => {
49
+ try {
50
+ cleanup.forEach((fn) => fn());
51
+ } catch {
52
+ }
53
+ };
54
+ },
40
55
  addEventListener(event, listener, options) {
41
56
  frames.each((frame) => {
42
57
  try {
@@ -68,24 +83,20 @@ var POINTER_OUTSIDE_EVENT = "pointerdown.outside";
68
83
  var FOCUS_OUTSIDE_EVENT = "focus.outside";
69
84
  function isComposedPathFocusable(composedPath) {
70
85
  for (const node of composedPath) {
71
- if ((0, import_dom_query.isHTMLElement)(node) && (0, import_dom_query.isFocusable)(node))
72
- return true;
86
+ if ((0, import_dom_query.isHTMLElement)(node) && (0, import_dom_query.isFocusable)(node)) return true;
73
87
  }
74
88
  return false;
75
89
  }
76
90
  var isPointerEvent = (event) => "clientY" in event;
77
91
  function isEventPointWithin(node, event) {
78
- if (!isPointerEvent(event) || !node)
79
- return false;
92
+ if (!isPointerEvent(event) || !node) return false;
80
93
  const rect = node.getBoundingClientRect();
81
- if (rect.width === 0 || rect.height === 0)
82
- return false;
94
+ if (rect.width === 0 || rect.height === 0) return false;
83
95
  return rect.top <= event.clientY && event.clientY <= rect.top + rect.height && rect.left <= event.clientX && event.clientX <= rect.left + rect.width;
84
96
  }
85
97
  function isEventWithinScrollbar(event) {
86
98
  const target = (0, import_dom_query.getEventTarget)(event);
87
- if (!target || !isPointerEvent(event))
88
- return false;
99
+ if (!target || !isPointerEvent(event)) return false;
89
100
  const isScrollableY = target.scrollHeight > target.clientHeight;
90
101
  const onScrollbarY = isScrollableY && event.clientX > target.clientWidth;
91
102
  const isScrollableX = target.scrollWidth > target.clientWidth;
@@ -94,52 +105,44 @@ function isEventWithinScrollbar(event) {
94
105
  }
95
106
  function trackInteractOutsideImpl(node, options) {
96
107
  const { exclude, onFocusOutside, onPointerDownOutside, onInteractOutside, defer } = options;
97
- if (!node)
98
- return;
108
+ if (!node) return;
99
109
  const doc = (0, import_dom_query.getDocument)(node);
100
110
  const win = (0, import_dom_query.getWindow)(node);
101
111
  const frames = getWindowFrames(win);
102
112
  function isEventOutside(event) {
103
113
  const target = (0, import_dom_query.getEventTarget)(event);
104
- if (!(0, import_dom_query.isHTMLElement)(target))
105
- return false;
106
- if ((0, import_dom_query.contains)(node, target))
107
- return false;
108
- if (isEventPointWithin(node, event))
109
- return false;
110
- if (isEventWithinScrollbar(event))
111
- return false;
114
+ if (!(0, import_dom_query.isHTMLElement)(target)) return false;
115
+ if ((0, import_dom_query.contains)(node, target)) return false;
116
+ if (isEventPointWithin(node, event)) return false;
117
+ if (isEventWithinScrollbar(event)) return false;
112
118
  return !exclude?.(target);
113
119
  }
114
- let clickHandler;
120
+ const pointerdownCleanups = /* @__PURE__ */ new Set();
115
121
  function onPointerDown(event) {
116
122
  function handler() {
117
123
  const func = defer ? import_dom_query.raf : (v) => v();
118
124
  const composedPath = event.composedPath?.() ?? [event.target];
119
125
  func(() => {
120
- if (!node || !isEventOutside(event))
121
- return;
126
+ if (!node || !isEventOutside(event)) return;
122
127
  if (onPointerDownOutside || onInteractOutside) {
123
128
  const handler2 = (0, import_utils.callAll)(onPointerDownOutside, onInteractOutside);
124
129
  node.addEventListener(POINTER_OUTSIDE_EVENT, handler2, { once: true });
125
130
  }
126
- (0, import_dom_event.fireCustomEvent)(node, POINTER_OUTSIDE_EVENT, {
131
+ (0, import_dom_event2.fireCustomEvent)(node, POINTER_OUTSIDE_EVENT, {
127
132
  bubbles: false,
128
133
  cancelable: true,
129
134
  detail: {
130
135
  originalEvent: event,
131
- contextmenu: (0, import_dom_event.isContextMenuEvent)(event),
136
+ contextmenu: (0, import_dom_event2.isContextMenuEvent)(event),
132
137
  focusable: isComposedPathFocusable(composedPath)
133
138
  }
134
139
  });
135
140
  });
136
141
  }
137
142
  if (event.pointerType === "touch") {
138
- frames.removeEventListener("click", handler);
139
- doc.removeEventListener("click", handler);
140
- clickHandler = handler;
141
- doc.addEventListener("click", handler, { once: true });
142
- frames.addEventListener("click", handler, { once: true });
143
+ pointerdownCleanups.forEach((fn) => fn());
144
+ pointerdownCleanups.add((0, import_dom_event2.queueBeforeEvent)(doc, "pointerup", handler));
145
+ pointerdownCleanups.add(frames.queueBeforeEvent("pointerup", handler));
143
146
  } else {
144
147
  handler();
145
148
  }
@@ -147,18 +150,17 @@ function trackInteractOutsideImpl(node, options) {
147
150
  const cleanups = /* @__PURE__ */ new Set();
148
151
  const timer = setTimeout(() => {
149
152
  cleanups.add(frames.addEventListener("pointerdown", onPointerDown, true));
150
- cleanups.add((0, import_dom_event.addDomEvent)(doc, "pointerdown", onPointerDown, true));
153
+ cleanups.add((0, import_dom_event2.addDomEvent)(doc, "pointerdown", onPointerDown, true));
151
154
  }, 0);
152
155
  function onFocusin(event) {
153
156
  const func = defer ? import_dom_query.raf : (v) => v();
154
157
  func(() => {
155
- if (!node || !isEventOutside(event))
156
- return;
158
+ if (!node || !isEventOutside(event)) return;
157
159
  if (onFocusOutside || onInteractOutside) {
158
160
  const handler = (0, import_utils.callAll)(onFocusOutside, onInteractOutside);
159
161
  node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true });
160
162
  }
161
- (0, import_dom_event.fireCustomEvent)(node, FOCUS_OUTSIDE_EVENT, {
163
+ (0, import_dom_event2.fireCustomEvent)(node, FOCUS_OUTSIDE_EVENT, {
162
164
  bubbles: false,
163
165
  cancelable: true,
164
166
  detail: {
@@ -169,14 +171,11 @@ function trackInteractOutsideImpl(node, options) {
169
171
  });
170
172
  });
171
173
  }
172
- cleanups.add((0, import_dom_event.addDomEvent)(doc, "focusin", onFocusin, true));
174
+ cleanups.add((0, import_dom_event2.addDomEvent)(doc, "focusin", onFocusin, true));
173
175
  cleanups.add(frames.addEventListener("focusin", onFocusin, true));
174
176
  return () => {
175
177
  clearTimeout(timer);
176
- if (clickHandler) {
177
- frames.removeEventListener("click", clickHandler);
178
- doc.removeEventListener("click", clickHandler);
179
- }
178
+ pointerdownCleanups.forEach((fn) => fn());
180
179
  cleanups.forEach((fn) => fn());
181
180
  };
182
181
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/get-window-frames.ts"],"sourcesContent":["import { addDomEvent, fireCustomEvent, isContextMenuEvent } from \"@zag-js/dom-event\"\nimport { contains, getDocument, getEventTarget, getWindow, isFocusable, isHTMLElement, raf } from \"@zag-js/dom-query\"\nimport { callAll } from \"@zag-js/utils\"\nimport { getWindowFrames } from \"./get-window-frames\"\n\nexport interface InteractOutsideHandlers {\n /**\n * Function called when the pointer is pressed down outside the component\n */\n onPointerDownOutside?: (event: PointerDownOutsideEvent) => void\n /**\n * Function called when the focus is moved outside the component\n */\n onFocusOutside?: (event: FocusOutsideEvent) => void\n /**\n * Function called when an interaction happens outside the component\n */\n onInteractOutside?: (event: InteractOutsideEvent) => void\n}\n\nexport interface InteractOutsideOptions extends InteractOutsideHandlers {\n exclude?: (target: HTMLElement) => boolean\n defer?: boolean\n}\n\nexport interface EventDetails<T> {\n originalEvent: T\n contextmenu: boolean\n focusable: boolean\n}\n\nconst POINTER_OUTSIDE_EVENT = \"pointerdown.outside\"\nconst FOCUS_OUTSIDE_EVENT = \"focus.outside\"\n\nexport type PointerDownOutsideEvent = CustomEvent<EventDetails<PointerEvent>>\n\nexport type FocusOutsideEvent = CustomEvent<EventDetails<FocusEvent>>\n\nexport type InteractOutsideEvent = PointerDownOutsideEvent | FocusOutsideEvent\n\nexport type MaybeElement = HTMLElement | null | undefined\nexport type NodeOrFn = MaybeElement | (() => MaybeElement)\n\nfunction isComposedPathFocusable(composedPath: EventTarget[]) {\n for (const node of composedPath) {\n if (isHTMLElement(node) && isFocusable(node)) return true\n }\n return false\n}\n\nconst isPointerEvent = (event: Event): event is PointerEvent => \"clientY\" in event\n\nfunction isEventPointWithin(node: MaybeElement, event: Event) {\n if (!isPointerEvent(event) || !node) return false\n\n const rect = node.getBoundingClientRect()\n if (rect.width === 0 || rect.height === 0) return false\n\n return (\n rect.top <= event.clientY &&\n event.clientY <= rect.top + rect.height &&\n rect.left <= event.clientX &&\n event.clientX <= rect.left + rect.width\n )\n}\n\nfunction isEventWithinScrollbar(event: Event): boolean {\n const target = getEventTarget<HTMLElement>(event)\n if (!target || !isPointerEvent(event)) return false\n\n const isScrollableY = target.scrollHeight > target.clientHeight\n const onScrollbarY = isScrollableY && event.clientX > target.clientWidth\n\n const isScrollableX = target.scrollWidth > target.clientWidth\n const onScrollbarX = isScrollableX && event.clientY > target.clientHeight\n\n return onScrollbarY || onScrollbarX\n}\n\nfunction trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOptions) {\n const { exclude, onFocusOutside, onPointerDownOutside, onInteractOutside, defer } = options\n\n if (!node) return\n\n const doc = getDocument(node)\n const win = getWindow(node)\n const frames = getWindowFrames(win)\n\n function isEventOutside(event: Event): boolean {\n const target = getEventTarget(event)\n if (!isHTMLElement(target)) return false\n if (contains(node, target)) return false\n if (isEventPointWithin(node, event)) return false\n if (isEventWithinScrollbar(event)) return false\n return !exclude?.(target)\n }\n\n let clickHandler: VoidFunction\n\n function onPointerDown(event: PointerEvent) {\n //\n function handler() {\n const func = defer ? raf : (v: any) => v()\n const composedPath = event.composedPath?.() ?? [event.target]\n func(() => {\n if (!node || !isEventOutside(event)) return\n\n if (onPointerDownOutside || onInteractOutside) {\n const handler = callAll(onPointerDownOutside, onInteractOutside) as EventListener\n node.addEventListener(POINTER_OUTSIDE_EVENT, handler, { once: true })\n }\n\n fireCustomEvent(node, POINTER_OUTSIDE_EVENT, {\n bubbles: false,\n cancelable: true,\n detail: {\n originalEvent: event,\n contextmenu: isContextMenuEvent(event),\n focusable: isComposedPathFocusable(composedPath),\n },\n })\n })\n }\n\n if (event.pointerType === \"touch\") {\n frames.removeEventListener(\"click\", handler)\n doc.removeEventListener(\"click\", handler)\n\n clickHandler = handler\n\n doc.addEventListener(\"click\", handler, { once: true })\n frames.addEventListener(\"click\", handler, { once: true })\n } else {\n handler()\n }\n }\n const cleanups = new Set<VoidFunction>()\n\n const timer = setTimeout(() => {\n cleanups.add(frames.addEventListener(\"pointerdown\", onPointerDown, true))\n cleanups.add(addDomEvent(doc, \"pointerdown\", onPointerDown, true))\n }, 0)\n\n function onFocusin(event: FocusEvent) {\n //\n const func = defer ? raf : (v: any) => v()\n func(() => {\n if (!node || !isEventOutside(event)) return\n\n if (onFocusOutside || onInteractOutside) {\n const handler = callAll(onFocusOutside, onInteractOutside) as EventListener\n node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true })\n }\n\n fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, {\n bubbles: false,\n cancelable: true,\n detail: {\n originalEvent: event,\n contextmenu: false,\n focusable: isFocusable(getEventTarget(event)),\n },\n })\n })\n }\n\n cleanups.add(addDomEvent(doc, \"focusin\", onFocusin, true))\n cleanups.add(frames.addEventListener(\"focusin\", onFocusin, true))\n\n return () => {\n clearTimeout(timer)\n if (clickHandler) {\n frames.removeEventListener(\"click\", clickHandler)\n doc.removeEventListener(\"click\", clickHandler)\n }\n cleanups.forEach((fn) => fn())\n }\n}\n\nexport function trackInteractOutside(nodeOrFn: NodeOrFn, options: InteractOutsideOptions) {\n const { defer } = options\n const func = defer ? raf : (v: any) => v()\n const cleanups: (VoidFunction | undefined)[] = []\n cleanups.push(\n func(() => {\n const node = typeof nodeOrFn === \"function\" ? nodeOrFn() : nodeOrFn\n cleanups.push(trackInteractOutsideImpl(node, options))\n }),\n )\n return () => {\n cleanups.forEach((fn) => fn?.())\n }\n}\n","export function getWindowFrames(win: Window) {\n const frames = {\n each(cb: (win: Window) => void) {\n for (let i = 0; i < win.frames?.length; i += 1) {\n const frame = win.frames[i]\n if (frame) cb(frame)\n }\n },\n addEventListener(event: string, listener: any, options?: any) {\n frames.each((frame) => {\n try {\n frame.document.addEventListener(event, listener, options)\n } catch {}\n })\n return () => {\n try {\n frames.removeEventListener(event, listener, options)\n } catch {}\n }\n },\n removeEventListener(event: string, listener: any, options?: any) {\n frames.each((frame) => {\n try {\n frame.document.removeEventListener(event, listener, options)\n } catch {}\n })\n },\n }\n return frames\n}\n"],"mappings":"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","names":["handler"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/get-window-frames.ts"],"sourcesContent":["import { addDomEvent, fireCustomEvent, isContextMenuEvent, queueBeforeEvent } from \"@zag-js/dom-event\"\nimport { contains, getDocument, getEventTarget, getWindow, isFocusable, isHTMLElement, raf } from \"@zag-js/dom-query\"\nimport { callAll } from \"@zag-js/utils\"\nimport { getWindowFrames } from \"./get-window-frames\"\n\nexport interface InteractOutsideHandlers {\n /**\n * Function called when the pointer is pressed down outside the component\n */\n onPointerDownOutside?: (event: PointerDownOutsideEvent) => void\n /**\n * Function called when the focus is moved outside the component\n */\n onFocusOutside?: (event: FocusOutsideEvent) => void\n /**\n * Function called when an interaction happens outside the component\n */\n onInteractOutside?: (event: InteractOutsideEvent) => void\n}\n\nexport interface InteractOutsideOptions extends InteractOutsideHandlers {\n exclude?: (target: HTMLElement) => boolean\n defer?: boolean\n}\n\nexport interface EventDetails<T> {\n originalEvent: T\n contextmenu: boolean\n focusable: boolean\n}\n\nconst POINTER_OUTSIDE_EVENT = \"pointerdown.outside\"\nconst FOCUS_OUTSIDE_EVENT = \"focus.outside\"\n\nexport type PointerDownOutsideEvent = CustomEvent<EventDetails<PointerEvent>>\n\nexport type FocusOutsideEvent = CustomEvent<EventDetails<FocusEvent>>\n\nexport type InteractOutsideEvent = PointerDownOutsideEvent | FocusOutsideEvent\n\nexport type MaybeElement = HTMLElement | null | undefined\nexport type NodeOrFn = MaybeElement | (() => MaybeElement)\n\nfunction isComposedPathFocusable(composedPath: EventTarget[]) {\n for (const node of composedPath) {\n if (isHTMLElement(node) && isFocusable(node)) return true\n }\n return false\n}\n\nconst isPointerEvent = (event: Event): event is PointerEvent => \"clientY\" in event\n\nfunction isEventPointWithin(node: MaybeElement, event: Event) {\n if (!isPointerEvent(event) || !node) return false\n\n const rect = node.getBoundingClientRect()\n if (rect.width === 0 || rect.height === 0) return false\n\n return (\n rect.top <= event.clientY &&\n event.clientY <= rect.top + rect.height &&\n rect.left <= event.clientX &&\n event.clientX <= rect.left + rect.width\n )\n}\n\nfunction isEventWithinScrollbar(event: Event): boolean {\n const target = getEventTarget<HTMLElement>(event)\n if (!target || !isPointerEvent(event)) return false\n\n const isScrollableY = target.scrollHeight > target.clientHeight\n const onScrollbarY = isScrollableY && event.clientX > target.clientWidth\n\n const isScrollableX = target.scrollWidth > target.clientWidth\n const onScrollbarX = isScrollableX && event.clientY > target.clientHeight\n\n return onScrollbarY || onScrollbarX\n}\n\nfunction trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOptions) {\n const { exclude, onFocusOutside, onPointerDownOutside, onInteractOutside, defer } = options\n\n if (!node) return\n\n const doc = getDocument(node)\n const win = getWindow(node)\n const frames = getWindowFrames(win)\n\n function isEventOutside(event: Event): boolean {\n const target = getEventTarget(event)\n if (!isHTMLElement(target)) return false\n if (contains(node, target)) return false\n if (isEventPointWithin(node, event)) return false\n if (isEventWithinScrollbar(event)) return false\n return !exclude?.(target)\n }\n\n const pointerdownCleanups: Set<VoidFunction> = new Set()\n\n function onPointerDown(event: PointerEvent) {\n //\n function handler() {\n const func = defer ? raf : (v: any) => v()\n const composedPath = event.composedPath?.() ?? [event.target]\n func(() => {\n if (!node || !isEventOutside(event)) return\n\n if (onPointerDownOutside || onInteractOutside) {\n const handler = callAll(onPointerDownOutside, onInteractOutside) as EventListener\n node.addEventListener(POINTER_OUTSIDE_EVENT, handler, { once: true })\n }\n\n fireCustomEvent(node, POINTER_OUTSIDE_EVENT, {\n bubbles: false,\n cancelable: true,\n detail: {\n originalEvent: event,\n contextmenu: isContextMenuEvent(event),\n focusable: isComposedPathFocusable(composedPath),\n },\n })\n })\n }\n\n if (event.pointerType === \"touch\") {\n // flush any pending pointerup events\n pointerdownCleanups.forEach((fn) => fn())\n\n // add a pointerup event listener to the document and all frame documents\n pointerdownCleanups.add(queueBeforeEvent(doc, \"pointerup\", handler))\n pointerdownCleanups.add(frames.queueBeforeEvent(\"pointerup\", handler))\n } else {\n handler()\n }\n }\n const cleanups = new Set<VoidFunction>()\n\n const timer = setTimeout(() => {\n cleanups.add(frames.addEventListener(\"pointerdown\", onPointerDown, true))\n cleanups.add(addDomEvent(doc, \"pointerdown\", onPointerDown, true))\n }, 0)\n\n function onFocusin(event: FocusEvent) {\n //\n const func = defer ? raf : (v: any) => v()\n func(() => {\n if (!node || !isEventOutside(event)) return\n\n if (onFocusOutside || onInteractOutside) {\n const handler = callAll(onFocusOutside, onInteractOutside) as EventListener\n node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true })\n }\n\n fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, {\n bubbles: false,\n cancelable: true,\n detail: {\n originalEvent: event,\n contextmenu: false,\n focusable: isFocusable(getEventTarget(event)),\n },\n })\n })\n }\n\n cleanups.add(addDomEvent(doc, \"focusin\", onFocusin, true))\n cleanups.add(frames.addEventListener(\"focusin\", onFocusin, true))\n\n return () => {\n clearTimeout(timer)\n pointerdownCleanups.forEach((fn) => fn())\n cleanups.forEach((fn) => fn())\n }\n}\n\nexport function trackInteractOutside(nodeOrFn: NodeOrFn, options: InteractOutsideOptions) {\n const { defer } = options\n const func = defer ? raf : (v: any) => v()\n const cleanups: (VoidFunction | undefined)[] = []\n cleanups.push(\n func(() => {\n const node = typeof nodeOrFn === \"function\" ? nodeOrFn() : nodeOrFn\n cleanups.push(trackInteractOutsideImpl(node, options))\n }),\n )\n return () => {\n cleanups.forEach((fn) => fn?.())\n }\n}\n","import { queueBeforeEvent } from \"@zag-js/dom-event\"\n\nexport function getWindowFrames(win: Window) {\n const frames = {\n each(cb: (win: Window) => void) {\n for (let i = 0; i < win.frames?.length; i += 1) {\n const frame = win.frames[i]\n if (frame) cb(frame)\n }\n },\n\n queueBeforeEvent(event: string, listener: any) {\n const cleanup = new Set<VoidFunction>()\n frames.each((frame) => {\n try {\n cleanup.add(queueBeforeEvent(frame.document, event, listener))\n } catch {}\n })\n\n return () => {\n try {\n cleanup.forEach((fn) => fn())\n } catch {}\n }\n },\n\n addEventListener(event: string, listener: any, options?: any) {\n frames.each((frame) => {\n try {\n frame.document.addEventListener(event, listener, options)\n } catch {}\n })\n\n return () => {\n try {\n frames.removeEventListener(event, listener, options)\n } catch {}\n }\n },\n\n removeEventListener(event: string, listener: any, options?: any) {\n frames.each((frame) => {\n try {\n frame.document.removeEventListener(event, listener, options)\n } catch {}\n })\n },\n }\n\n return frames\n}\n"],"mappings":"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","names":["import_dom_event","handler"]}
package/dist/index.mjs CHANGED
@@ -1,18 +1,33 @@
1
1
  // src/index.ts
2
- import { addDomEvent, fireCustomEvent, isContextMenuEvent } from "@zag-js/dom-event";
2
+ import { addDomEvent, fireCustomEvent, isContextMenuEvent, queueBeforeEvent as queueBeforeEvent2 } from "@zag-js/dom-event";
3
3
  import { contains, getDocument, getEventTarget, getWindow, isFocusable, isHTMLElement, raf } from "@zag-js/dom-query";
4
4
  import { callAll } from "@zag-js/utils";
5
5
 
6
6
  // src/get-window-frames.ts
7
+ import { queueBeforeEvent } from "@zag-js/dom-event";
7
8
  function getWindowFrames(win) {
8
9
  const frames = {
9
10
  each(cb) {
10
11
  for (let i = 0; i < win.frames?.length; i += 1) {
11
12
  const frame = win.frames[i];
12
- if (frame)
13
- cb(frame);
13
+ if (frame) cb(frame);
14
14
  }
15
15
  },
16
+ queueBeforeEvent(event, listener) {
17
+ const cleanup = /* @__PURE__ */ new Set();
18
+ frames.each((frame) => {
19
+ try {
20
+ cleanup.add(queueBeforeEvent(frame.document, event, listener));
21
+ } catch {
22
+ }
23
+ });
24
+ return () => {
25
+ try {
26
+ cleanup.forEach((fn) => fn());
27
+ } catch {
28
+ }
29
+ };
30
+ },
16
31
  addEventListener(event, listener, options) {
17
32
  frames.each((frame) => {
18
33
  try {
@@ -44,24 +59,20 @@ var POINTER_OUTSIDE_EVENT = "pointerdown.outside";
44
59
  var FOCUS_OUTSIDE_EVENT = "focus.outside";
45
60
  function isComposedPathFocusable(composedPath) {
46
61
  for (const node of composedPath) {
47
- if (isHTMLElement(node) && isFocusable(node))
48
- return true;
62
+ if (isHTMLElement(node) && isFocusable(node)) return true;
49
63
  }
50
64
  return false;
51
65
  }
52
66
  var isPointerEvent = (event) => "clientY" in event;
53
67
  function isEventPointWithin(node, event) {
54
- if (!isPointerEvent(event) || !node)
55
- return false;
68
+ if (!isPointerEvent(event) || !node) return false;
56
69
  const rect = node.getBoundingClientRect();
57
- if (rect.width === 0 || rect.height === 0)
58
- return false;
70
+ if (rect.width === 0 || rect.height === 0) return false;
59
71
  return rect.top <= event.clientY && event.clientY <= rect.top + rect.height && rect.left <= event.clientX && event.clientX <= rect.left + rect.width;
60
72
  }
61
73
  function isEventWithinScrollbar(event) {
62
74
  const target = getEventTarget(event);
63
- if (!target || !isPointerEvent(event))
64
- return false;
75
+ if (!target || !isPointerEvent(event)) return false;
65
76
  const isScrollableY = target.scrollHeight > target.clientHeight;
66
77
  const onScrollbarY = isScrollableY && event.clientX > target.clientWidth;
67
78
  const isScrollableX = target.scrollWidth > target.clientWidth;
@@ -70,31 +81,25 @@ function isEventWithinScrollbar(event) {
70
81
  }
71
82
  function trackInteractOutsideImpl(node, options) {
72
83
  const { exclude, onFocusOutside, onPointerDownOutside, onInteractOutside, defer } = options;
73
- if (!node)
74
- return;
84
+ if (!node) return;
75
85
  const doc = getDocument(node);
76
86
  const win = getWindow(node);
77
87
  const frames = getWindowFrames(win);
78
88
  function isEventOutside(event) {
79
89
  const target = getEventTarget(event);
80
- if (!isHTMLElement(target))
81
- return false;
82
- if (contains(node, target))
83
- return false;
84
- if (isEventPointWithin(node, event))
85
- return false;
86
- if (isEventWithinScrollbar(event))
87
- return false;
90
+ if (!isHTMLElement(target)) return false;
91
+ if (contains(node, target)) return false;
92
+ if (isEventPointWithin(node, event)) return false;
93
+ if (isEventWithinScrollbar(event)) return false;
88
94
  return !exclude?.(target);
89
95
  }
90
- let clickHandler;
96
+ const pointerdownCleanups = /* @__PURE__ */ new Set();
91
97
  function onPointerDown(event) {
92
98
  function handler() {
93
99
  const func = defer ? raf : (v) => v();
94
100
  const composedPath = event.composedPath?.() ?? [event.target];
95
101
  func(() => {
96
- if (!node || !isEventOutside(event))
97
- return;
102
+ if (!node || !isEventOutside(event)) return;
98
103
  if (onPointerDownOutside || onInteractOutside) {
99
104
  const handler2 = callAll(onPointerDownOutside, onInteractOutside);
100
105
  node.addEventListener(POINTER_OUTSIDE_EVENT, handler2, { once: true });
@@ -111,11 +116,9 @@ function trackInteractOutsideImpl(node, options) {
111
116
  });
112
117
  }
113
118
  if (event.pointerType === "touch") {
114
- frames.removeEventListener("click", handler);
115
- doc.removeEventListener("click", handler);
116
- clickHandler = handler;
117
- doc.addEventListener("click", handler, { once: true });
118
- frames.addEventListener("click", handler, { once: true });
119
+ pointerdownCleanups.forEach((fn) => fn());
120
+ pointerdownCleanups.add(queueBeforeEvent2(doc, "pointerup", handler));
121
+ pointerdownCleanups.add(frames.queueBeforeEvent("pointerup", handler));
119
122
  } else {
120
123
  handler();
121
124
  }
@@ -128,8 +131,7 @@ function trackInteractOutsideImpl(node, options) {
128
131
  function onFocusin(event) {
129
132
  const func = defer ? raf : (v) => v();
130
133
  func(() => {
131
- if (!node || !isEventOutside(event))
132
- return;
134
+ if (!node || !isEventOutside(event)) return;
133
135
  if (onFocusOutside || onInteractOutside) {
134
136
  const handler = callAll(onFocusOutside, onInteractOutside);
135
137
  node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true });
@@ -149,10 +151,7 @@ function trackInteractOutsideImpl(node, options) {
149
151
  cleanups.add(frames.addEventListener("focusin", onFocusin, true));
150
152
  return () => {
151
153
  clearTimeout(timer);
152
- if (clickHandler) {
153
- frames.removeEventListener("click", clickHandler);
154
- doc.removeEventListener("click", clickHandler);
155
- }
154
+ pointerdownCleanups.forEach((fn) => fn());
156
155
  cleanups.forEach((fn) => fn());
157
156
  };
158
157
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/get-window-frames.ts"],"sourcesContent":["import { addDomEvent, fireCustomEvent, isContextMenuEvent } from \"@zag-js/dom-event\"\nimport { contains, getDocument, getEventTarget, getWindow, isFocusable, isHTMLElement, raf } from \"@zag-js/dom-query\"\nimport { callAll } from \"@zag-js/utils\"\nimport { getWindowFrames } from \"./get-window-frames\"\n\nexport interface InteractOutsideHandlers {\n /**\n * Function called when the pointer is pressed down outside the component\n */\n onPointerDownOutside?: (event: PointerDownOutsideEvent) => void\n /**\n * Function called when the focus is moved outside the component\n */\n onFocusOutside?: (event: FocusOutsideEvent) => void\n /**\n * Function called when an interaction happens outside the component\n */\n onInteractOutside?: (event: InteractOutsideEvent) => void\n}\n\nexport interface InteractOutsideOptions extends InteractOutsideHandlers {\n exclude?: (target: HTMLElement) => boolean\n defer?: boolean\n}\n\nexport interface EventDetails<T> {\n originalEvent: T\n contextmenu: boolean\n focusable: boolean\n}\n\nconst POINTER_OUTSIDE_EVENT = \"pointerdown.outside\"\nconst FOCUS_OUTSIDE_EVENT = \"focus.outside\"\n\nexport type PointerDownOutsideEvent = CustomEvent<EventDetails<PointerEvent>>\n\nexport type FocusOutsideEvent = CustomEvent<EventDetails<FocusEvent>>\n\nexport type InteractOutsideEvent = PointerDownOutsideEvent | FocusOutsideEvent\n\nexport type MaybeElement = HTMLElement | null | undefined\nexport type NodeOrFn = MaybeElement | (() => MaybeElement)\n\nfunction isComposedPathFocusable(composedPath: EventTarget[]) {\n for (const node of composedPath) {\n if (isHTMLElement(node) && isFocusable(node)) return true\n }\n return false\n}\n\nconst isPointerEvent = (event: Event): event is PointerEvent => \"clientY\" in event\n\nfunction isEventPointWithin(node: MaybeElement, event: Event) {\n if (!isPointerEvent(event) || !node) return false\n\n const rect = node.getBoundingClientRect()\n if (rect.width === 0 || rect.height === 0) return false\n\n return (\n rect.top <= event.clientY &&\n event.clientY <= rect.top + rect.height &&\n rect.left <= event.clientX &&\n event.clientX <= rect.left + rect.width\n )\n}\n\nfunction isEventWithinScrollbar(event: Event): boolean {\n const target = getEventTarget<HTMLElement>(event)\n if (!target || !isPointerEvent(event)) return false\n\n const isScrollableY = target.scrollHeight > target.clientHeight\n const onScrollbarY = isScrollableY && event.clientX > target.clientWidth\n\n const isScrollableX = target.scrollWidth > target.clientWidth\n const onScrollbarX = isScrollableX && event.clientY > target.clientHeight\n\n return onScrollbarY || onScrollbarX\n}\n\nfunction trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOptions) {\n const { exclude, onFocusOutside, onPointerDownOutside, onInteractOutside, defer } = options\n\n if (!node) return\n\n const doc = getDocument(node)\n const win = getWindow(node)\n const frames = getWindowFrames(win)\n\n function isEventOutside(event: Event): boolean {\n const target = getEventTarget(event)\n if (!isHTMLElement(target)) return false\n if (contains(node, target)) return false\n if (isEventPointWithin(node, event)) return false\n if (isEventWithinScrollbar(event)) return false\n return !exclude?.(target)\n }\n\n let clickHandler: VoidFunction\n\n function onPointerDown(event: PointerEvent) {\n //\n function handler() {\n const func = defer ? raf : (v: any) => v()\n const composedPath = event.composedPath?.() ?? [event.target]\n func(() => {\n if (!node || !isEventOutside(event)) return\n\n if (onPointerDownOutside || onInteractOutside) {\n const handler = callAll(onPointerDownOutside, onInteractOutside) as EventListener\n node.addEventListener(POINTER_OUTSIDE_EVENT, handler, { once: true })\n }\n\n fireCustomEvent(node, POINTER_OUTSIDE_EVENT, {\n bubbles: false,\n cancelable: true,\n detail: {\n originalEvent: event,\n contextmenu: isContextMenuEvent(event),\n focusable: isComposedPathFocusable(composedPath),\n },\n })\n })\n }\n\n if (event.pointerType === \"touch\") {\n frames.removeEventListener(\"click\", handler)\n doc.removeEventListener(\"click\", handler)\n\n clickHandler = handler\n\n doc.addEventListener(\"click\", handler, { once: true })\n frames.addEventListener(\"click\", handler, { once: true })\n } else {\n handler()\n }\n }\n const cleanups = new Set<VoidFunction>()\n\n const timer = setTimeout(() => {\n cleanups.add(frames.addEventListener(\"pointerdown\", onPointerDown, true))\n cleanups.add(addDomEvent(doc, \"pointerdown\", onPointerDown, true))\n }, 0)\n\n function onFocusin(event: FocusEvent) {\n //\n const func = defer ? raf : (v: any) => v()\n func(() => {\n if (!node || !isEventOutside(event)) return\n\n if (onFocusOutside || onInteractOutside) {\n const handler = callAll(onFocusOutside, onInteractOutside) as EventListener\n node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true })\n }\n\n fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, {\n bubbles: false,\n cancelable: true,\n detail: {\n originalEvent: event,\n contextmenu: false,\n focusable: isFocusable(getEventTarget(event)),\n },\n })\n })\n }\n\n cleanups.add(addDomEvent(doc, \"focusin\", onFocusin, true))\n cleanups.add(frames.addEventListener(\"focusin\", onFocusin, true))\n\n return () => {\n clearTimeout(timer)\n if (clickHandler) {\n frames.removeEventListener(\"click\", clickHandler)\n doc.removeEventListener(\"click\", clickHandler)\n }\n cleanups.forEach((fn) => fn())\n }\n}\n\nexport function trackInteractOutside(nodeOrFn: NodeOrFn, options: InteractOutsideOptions) {\n const { defer } = options\n const func = defer ? raf : (v: any) => v()\n const cleanups: (VoidFunction | undefined)[] = []\n cleanups.push(\n func(() => {\n const node = typeof nodeOrFn === \"function\" ? nodeOrFn() : nodeOrFn\n cleanups.push(trackInteractOutsideImpl(node, options))\n }),\n )\n return () => {\n cleanups.forEach((fn) => fn?.())\n }\n}\n","export function getWindowFrames(win: Window) {\n const frames = {\n each(cb: (win: Window) => void) {\n for (let i = 0; i < win.frames?.length; i += 1) {\n const frame = win.frames[i]\n if (frame) cb(frame)\n }\n },\n addEventListener(event: string, listener: any, options?: any) {\n frames.each((frame) => {\n try {\n frame.document.addEventListener(event, listener, options)\n } catch {}\n })\n return () => {\n try {\n frames.removeEventListener(event, listener, options)\n } catch {}\n }\n },\n removeEventListener(event: string, listener: any, options?: any) {\n frames.each((frame) => {\n try {\n frame.document.removeEventListener(event, listener, options)\n } catch {}\n })\n },\n }\n return frames\n}\n"],"mappings":"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","names":["handler"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/get-window-frames.ts"],"sourcesContent":["import { addDomEvent, fireCustomEvent, isContextMenuEvent, queueBeforeEvent } from \"@zag-js/dom-event\"\nimport { contains, getDocument, getEventTarget, getWindow, isFocusable, isHTMLElement, raf } from \"@zag-js/dom-query\"\nimport { callAll } from \"@zag-js/utils\"\nimport { getWindowFrames } from \"./get-window-frames\"\n\nexport interface InteractOutsideHandlers {\n /**\n * Function called when the pointer is pressed down outside the component\n */\n onPointerDownOutside?: (event: PointerDownOutsideEvent) => void\n /**\n * Function called when the focus is moved outside the component\n */\n onFocusOutside?: (event: FocusOutsideEvent) => void\n /**\n * Function called when an interaction happens outside the component\n */\n onInteractOutside?: (event: InteractOutsideEvent) => void\n}\n\nexport interface InteractOutsideOptions extends InteractOutsideHandlers {\n exclude?: (target: HTMLElement) => boolean\n defer?: boolean\n}\n\nexport interface EventDetails<T> {\n originalEvent: T\n contextmenu: boolean\n focusable: boolean\n}\n\nconst POINTER_OUTSIDE_EVENT = \"pointerdown.outside\"\nconst FOCUS_OUTSIDE_EVENT = \"focus.outside\"\n\nexport type PointerDownOutsideEvent = CustomEvent<EventDetails<PointerEvent>>\n\nexport type FocusOutsideEvent = CustomEvent<EventDetails<FocusEvent>>\n\nexport type InteractOutsideEvent = PointerDownOutsideEvent | FocusOutsideEvent\n\nexport type MaybeElement = HTMLElement | null | undefined\nexport type NodeOrFn = MaybeElement | (() => MaybeElement)\n\nfunction isComposedPathFocusable(composedPath: EventTarget[]) {\n for (const node of composedPath) {\n if (isHTMLElement(node) && isFocusable(node)) return true\n }\n return false\n}\n\nconst isPointerEvent = (event: Event): event is PointerEvent => \"clientY\" in event\n\nfunction isEventPointWithin(node: MaybeElement, event: Event) {\n if (!isPointerEvent(event) || !node) return false\n\n const rect = node.getBoundingClientRect()\n if (rect.width === 0 || rect.height === 0) return false\n\n return (\n rect.top <= event.clientY &&\n event.clientY <= rect.top + rect.height &&\n rect.left <= event.clientX &&\n event.clientX <= rect.left + rect.width\n )\n}\n\nfunction isEventWithinScrollbar(event: Event): boolean {\n const target = getEventTarget<HTMLElement>(event)\n if (!target || !isPointerEvent(event)) return false\n\n const isScrollableY = target.scrollHeight > target.clientHeight\n const onScrollbarY = isScrollableY && event.clientX > target.clientWidth\n\n const isScrollableX = target.scrollWidth > target.clientWidth\n const onScrollbarX = isScrollableX && event.clientY > target.clientHeight\n\n return onScrollbarY || onScrollbarX\n}\n\nfunction trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOptions) {\n const { exclude, onFocusOutside, onPointerDownOutside, onInteractOutside, defer } = options\n\n if (!node) return\n\n const doc = getDocument(node)\n const win = getWindow(node)\n const frames = getWindowFrames(win)\n\n function isEventOutside(event: Event): boolean {\n const target = getEventTarget(event)\n if (!isHTMLElement(target)) return false\n if (contains(node, target)) return false\n if (isEventPointWithin(node, event)) return false\n if (isEventWithinScrollbar(event)) return false\n return !exclude?.(target)\n }\n\n const pointerdownCleanups: Set<VoidFunction> = new Set()\n\n function onPointerDown(event: PointerEvent) {\n //\n function handler() {\n const func = defer ? raf : (v: any) => v()\n const composedPath = event.composedPath?.() ?? [event.target]\n func(() => {\n if (!node || !isEventOutside(event)) return\n\n if (onPointerDownOutside || onInteractOutside) {\n const handler = callAll(onPointerDownOutside, onInteractOutside) as EventListener\n node.addEventListener(POINTER_OUTSIDE_EVENT, handler, { once: true })\n }\n\n fireCustomEvent(node, POINTER_OUTSIDE_EVENT, {\n bubbles: false,\n cancelable: true,\n detail: {\n originalEvent: event,\n contextmenu: isContextMenuEvent(event),\n focusable: isComposedPathFocusable(composedPath),\n },\n })\n })\n }\n\n if (event.pointerType === \"touch\") {\n // flush any pending pointerup events\n pointerdownCleanups.forEach((fn) => fn())\n\n // add a pointerup event listener to the document and all frame documents\n pointerdownCleanups.add(queueBeforeEvent(doc, \"pointerup\", handler))\n pointerdownCleanups.add(frames.queueBeforeEvent(\"pointerup\", handler))\n } else {\n handler()\n }\n }\n const cleanups = new Set<VoidFunction>()\n\n const timer = setTimeout(() => {\n cleanups.add(frames.addEventListener(\"pointerdown\", onPointerDown, true))\n cleanups.add(addDomEvent(doc, \"pointerdown\", onPointerDown, true))\n }, 0)\n\n function onFocusin(event: FocusEvent) {\n //\n const func = defer ? raf : (v: any) => v()\n func(() => {\n if (!node || !isEventOutside(event)) return\n\n if (onFocusOutside || onInteractOutside) {\n const handler = callAll(onFocusOutside, onInteractOutside) as EventListener\n node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true })\n }\n\n fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, {\n bubbles: false,\n cancelable: true,\n detail: {\n originalEvent: event,\n contextmenu: false,\n focusable: isFocusable(getEventTarget(event)),\n },\n })\n })\n }\n\n cleanups.add(addDomEvent(doc, \"focusin\", onFocusin, true))\n cleanups.add(frames.addEventListener(\"focusin\", onFocusin, true))\n\n return () => {\n clearTimeout(timer)\n pointerdownCleanups.forEach((fn) => fn())\n cleanups.forEach((fn) => fn())\n }\n}\n\nexport function trackInteractOutside(nodeOrFn: NodeOrFn, options: InteractOutsideOptions) {\n const { defer } = options\n const func = defer ? raf : (v: any) => v()\n const cleanups: (VoidFunction | undefined)[] = []\n cleanups.push(\n func(() => {\n const node = typeof nodeOrFn === \"function\" ? nodeOrFn() : nodeOrFn\n cleanups.push(trackInteractOutsideImpl(node, options))\n }),\n )\n return () => {\n cleanups.forEach((fn) => fn?.())\n }\n}\n","import { queueBeforeEvent } from \"@zag-js/dom-event\"\n\nexport function getWindowFrames(win: Window) {\n const frames = {\n each(cb: (win: Window) => void) {\n for (let i = 0; i < win.frames?.length; i += 1) {\n const frame = win.frames[i]\n if (frame) cb(frame)\n }\n },\n\n queueBeforeEvent(event: string, listener: any) {\n const cleanup = new Set<VoidFunction>()\n frames.each((frame) => {\n try {\n cleanup.add(queueBeforeEvent(frame.document, event, listener))\n } catch {}\n })\n\n return () => {\n try {\n cleanup.forEach((fn) => fn())\n } catch {}\n }\n },\n\n addEventListener(event: string, listener: any, options?: any) {\n frames.each((frame) => {\n try {\n frame.document.addEventListener(event, listener, options)\n } catch {}\n })\n\n return () => {\n try {\n frames.removeEventListener(event, listener, options)\n } catch {}\n }\n },\n\n removeEventListener(event: string, listener: any, options?: any) {\n frames.each((frame) => {\n try {\n frame.document.removeEventListener(event, listener, options)\n } catch {}\n })\n },\n }\n\n return frames\n}\n"],"mappings":"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","names":["queueBeforeEvent","handler","queueBeforeEvent"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/interact-outside",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "description": "Track interations or focus outside an element",
5
5
  "keywords": [
6
6
  "js",
@@ -17,9 +17,9 @@
17
17
  "src"
18
18
  ],
19
19
  "dependencies": {
20
- "@zag-js/dom-query": "0.56.0",
21
- "@zag-js/dom-event": "0.56.0",
22
- "@zag-js/utils": "0.56.0"
20
+ "@zag-js/dom-query": "0.57.0",
21
+ "@zag-js/dom-event": "0.57.0",
22
+ "@zag-js/utils": "0.57.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "clean-package": "2.2.0"
@@ -1,3 +1,5 @@
1
+ import { queueBeforeEvent } from "@zag-js/dom-event"
2
+
1
3
  export function getWindowFrames(win: Window) {
2
4
  const frames = {
3
5
  each(cb: (win: Window) => void) {
@@ -6,18 +8,36 @@ export function getWindowFrames(win: Window) {
6
8
  if (frame) cb(frame)
7
9
  }
8
10
  },
11
+
12
+ queueBeforeEvent(event: string, listener: any) {
13
+ const cleanup = new Set<VoidFunction>()
14
+ frames.each((frame) => {
15
+ try {
16
+ cleanup.add(queueBeforeEvent(frame.document, event, listener))
17
+ } catch {}
18
+ })
19
+
20
+ return () => {
21
+ try {
22
+ cleanup.forEach((fn) => fn())
23
+ } catch {}
24
+ }
25
+ },
26
+
9
27
  addEventListener(event: string, listener: any, options?: any) {
10
28
  frames.each((frame) => {
11
29
  try {
12
30
  frame.document.addEventListener(event, listener, options)
13
31
  } catch {}
14
32
  })
33
+
15
34
  return () => {
16
35
  try {
17
36
  frames.removeEventListener(event, listener, options)
18
37
  } catch {}
19
38
  }
20
39
  },
40
+
21
41
  removeEventListener(event: string, listener: any, options?: any) {
22
42
  frames.each((frame) => {
23
43
  try {
@@ -26,5 +46,6 @@ export function getWindowFrames(win: Window) {
26
46
  })
27
47
  },
28
48
  }
49
+
29
50
  return frames
30
51
  }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { addDomEvent, fireCustomEvent, isContextMenuEvent } from "@zag-js/dom-event"
1
+ import { addDomEvent, fireCustomEvent, isContextMenuEvent, queueBeforeEvent } from "@zag-js/dom-event"
2
2
  import { contains, getDocument, getEventTarget, getWindow, isFocusable, isHTMLElement, raf } from "@zag-js/dom-query"
3
3
  import { callAll } from "@zag-js/utils"
4
4
  import { getWindowFrames } from "./get-window-frames"
@@ -95,7 +95,7 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp
95
95
  return !exclude?.(target)
96
96
  }
97
97
 
98
- let clickHandler: VoidFunction
98
+ const pointerdownCleanups: Set<VoidFunction> = new Set()
99
99
 
100
100
  function onPointerDown(event: PointerEvent) {
101
101
  //
@@ -123,13 +123,12 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp
123
123
  }
124
124
 
125
125
  if (event.pointerType === "touch") {
126
- frames.removeEventListener("click", handler)
127
- doc.removeEventListener("click", handler)
126
+ // flush any pending pointerup events
127
+ pointerdownCleanups.forEach((fn) => fn())
128
128
 
129
- clickHandler = handler
130
-
131
- doc.addEventListener("click", handler, { once: true })
132
- frames.addEventListener("click", handler, { once: true })
129
+ // add a pointerup event listener to the document and all frame documents
130
+ pointerdownCleanups.add(queueBeforeEvent(doc, "pointerup", handler))
131
+ pointerdownCleanups.add(frames.queueBeforeEvent("pointerup", handler))
133
132
  } else {
134
133
  handler()
135
134
  }
@@ -169,10 +168,7 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp
169
168
 
170
169
  return () => {
171
170
  clearTimeout(timer)
172
- if (clickHandler) {
173
- frames.removeEventListener("click", clickHandler)
174
- doc.removeEventListener("click", clickHandler)
175
- }
171
+ pointerdownCleanups.forEach((fn) => fn())
176
172
  cleanups.forEach((fn) => fn())
177
173
  }
178
174
  }