@zag-js/interact-outside 0.70.0 → 0.71.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
@@ -1,34 +1,10 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
1
+ 'use strict';
19
2
 
20
- // src/index.ts
21
- var src_exports = {};
22
- __export(src_exports, {
23
- trackInteractOutside: () => trackInteractOutside
24
- });
25
- module.exports = __toCommonJS(src_exports);
26
- var import_dom_event2 = require("@zag-js/dom-event");
27
- var import_dom_query = require("@zag-js/dom-query");
28
- var import_utils = require("@zag-js/utils");
3
+ var domEvent = require('@zag-js/dom-event');
4
+ var domQuery = require('@zag-js/dom-query');
5
+ var utils = require('@zag-js/utils');
29
6
 
30
- // src/get-window-frames.ts
31
- var import_dom_event = require("@zag-js/dom-event");
7
+ // src/index.ts
32
8
  function getWindowFrames(win) {
33
9
  const frames = {
34
10
  each(cb) {
@@ -41,7 +17,7 @@ function getWindowFrames(win) {
41
17
  const cleanup = /* @__PURE__ */ new Set();
42
18
  frames.each((frame) => {
43
19
  try {
44
- cleanup.add((0, import_dom_event.queueBeforeEvent)(frame.document, event, listener));
20
+ cleanup.add(domEvent.queueBeforeEvent(frame.document, event, listener));
45
21
  } catch {
46
22
  }
47
23
  });
@@ -83,7 +59,7 @@ var POINTER_OUTSIDE_EVENT = "pointerdown.outside";
83
59
  var FOCUS_OUTSIDE_EVENT = "focus.outside";
84
60
  function isComposedPathFocusable(composedPath) {
85
61
  for (const node of composedPath) {
86
- if ((0, import_dom_query.isHTMLElement)(node) && (0, import_dom_query.isFocusable)(node)) return true;
62
+ if (domQuery.isHTMLElement(node) && domQuery.isFocusable(node)) return true;
87
63
  }
88
64
  return false;
89
65
  }
@@ -95,7 +71,7 @@ function isEventPointWithin(node, event) {
95
71
  return rect.top <= event.clientY && event.clientY <= rect.top + rect.height && rect.left <= event.clientX && event.clientX <= rect.left + rect.width;
96
72
  }
97
73
  function isEventWithinScrollbar(event) {
98
- const target = (0, import_dom_query.getEventTarget)(event);
74
+ const target = domQuery.getEventTarget(event);
99
75
  if (!target || !isPointerEvent(event)) return false;
100
76
  const isScrollableY = target.scrollHeight > target.clientHeight;
101
77
  const onScrollbarY = isScrollableY && event.clientX > target.clientWidth;
@@ -106,13 +82,13 @@ function isEventWithinScrollbar(event) {
106
82
  function trackInteractOutsideImpl(node, options) {
107
83
  const { exclude, onFocusOutside, onPointerDownOutside, onInteractOutside, defer } = options;
108
84
  if (!node) return;
109
- const doc = (0, import_dom_query.getDocument)(node);
110
- const win = (0, import_dom_query.getWindow)(node);
85
+ const doc = domQuery.getDocument(node);
86
+ const win = domQuery.getWindow(node);
111
87
  const frames = getWindowFrames(win);
112
88
  function isEventOutside(event) {
113
- const target = (0, import_dom_query.getEventTarget)(event);
114
- if (!(0, import_dom_query.isHTMLElement)(target)) return false;
115
- if ((0, import_dom_query.contains)(node, target)) return false;
89
+ const target = domQuery.getEventTarget(event);
90
+ if (!domQuery.isHTMLElement(target)) return false;
91
+ if (domQuery.contains(node, target)) return false;
116
92
  if (isEventPointWithin(node, event)) return false;
117
93
  if (isEventWithinScrollbar(event)) return false;
118
94
  return !exclude?.(target);
@@ -120,20 +96,20 @@ function trackInteractOutsideImpl(node, options) {
120
96
  const pointerdownCleanups = /* @__PURE__ */ new Set();
121
97
  function onPointerDown(event) {
122
98
  function handler() {
123
- const func = defer ? import_dom_query.raf : (v) => v();
99
+ const func = defer ? domQuery.raf : (v) => v();
124
100
  const composedPath = event.composedPath?.() ?? [event.target];
125
101
  func(() => {
126
102
  if (!node || !isEventOutside(event)) return;
127
103
  if (onPointerDownOutside || onInteractOutside) {
128
- const handler2 = (0, import_utils.callAll)(onPointerDownOutside, onInteractOutside);
104
+ const handler2 = utils.callAll(onPointerDownOutside, onInteractOutside);
129
105
  node.addEventListener(POINTER_OUTSIDE_EVENT, handler2, { once: true });
130
106
  }
131
- (0, import_dom_event2.fireCustomEvent)(node, POINTER_OUTSIDE_EVENT, {
107
+ domEvent.fireCustomEvent(node, POINTER_OUTSIDE_EVENT, {
132
108
  bubbles: false,
133
109
  cancelable: true,
134
110
  detail: {
135
111
  originalEvent: event,
136
- contextmenu: (0, import_dom_event2.isContextMenuEvent)(event),
112
+ contextmenu: domEvent.isContextMenuEvent(event),
137
113
  focusable: isComposedPathFocusable(composedPath)
138
114
  }
139
115
  });
@@ -141,7 +117,7 @@ function trackInteractOutsideImpl(node, options) {
141
117
  }
142
118
  if (event.pointerType === "touch") {
143
119
  pointerdownCleanups.forEach((fn) => fn());
144
- pointerdownCleanups.add((0, import_dom_event2.addDomEvent)(doc, "click", handler, { once: true }));
120
+ pointerdownCleanups.add(domEvent.addDomEvent(doc, "click", handler, { once: true }));
145
121
  pointerdownCleanups.add(frames.addEventListener("click", handler, { once: true }));
146
122
  } else {
147
123
  handler();
@@ -150,28 +126,28 @@ function trackInteractOutsideImpl(node, options) {
150
126
  const cleanups = /* @__PURE__ */ new Set();
151
127
  const timer = setTimeout(() => {
152
128
  cleanups.add(frames.addEventListener("pointerdown", onPointerDown, true));
153
- cleanups.add((0, import_dom_event2.addDomEvent)(doc, "pointerdown", onPointerDown, true));
129
+ cleanups.add(domEvent.addDomEvent(doc, "pointerdown", onPointerDown, true));
154
130
  }, 0);
155
131
  function onFocusin(event) {
156
- const func = defer ? import_dom_query.raf : (v) => v();
132
+ const func = defer ? domQuery.raf : (v) => v();
157
133
  func(() => {
158
134
  if (!node || !isEventOutside(event)) return;
159
135
  if (onFocusOutside || onInteractOutside) {
160
- const handler = (0, import_utils.callAll)(onFocusOutside, onInteractOutside);
136
+ const handler = utils.callAll(onFocusOutside, onInteractOutside);
161
137
  node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true });
162
138
  }
163
- (0, import_dom_event2.fireCustomEvent)(node, FOCUS_OUTSIDE_EVENT, {
139
+ domEvent.fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, {
164
140
  bubbles: false,
165
141
  cancelable: true,
166
142
  detail: {
167
143
  originalEvent: event,
168
144
  contextmenu: false,
169
- focusable: (0, import_dom_query.isFocusable)((0, import_dom_query.getEventTarget)(event))
145
+ focusable: domQuery.isFocusable(domQuery.getEventTarget(event))
170
146
  }
171
147
  });
172
148
  });
173
149
  }
174
- cleanups.add((0, import_dom_event2.addDomEvent)(doc, "focusin", onFocusin, true));
150
+ cleanups.add(domEvent.addDomEvent(doc, "focusin", onFocusin, true));
175
151
  cleanups.add(frames.addEventListener("focusin", onFocusin, true));
176
152
  return () => {
177
153
  clearTimeout(timer);
@@ -181,7 +157,7 @@ function trackInteractOutsideImpl(node, options) {
181
157
  }
182
158
  function trackInteractOutside(nodeOrFn, options) {
183
159
  const { defer } = options;
184
- const func = defer ? import_dom_query.raf : (v) => v();
160
+ const func = defer ? domQuery.raf : (v) => v();
185
161
  const cleanups = [];
186
162
  cleanups.push(
187
163
  func(() => {
@@ -193,8 +169,5 @@ function trackInteractOutside(nodeOrFn, options) {
193
169
  cleanups.forEach((fn) => fn?.());
194
170
  };
195
171
  }
196
- // Annotate the CommonJS export names for ESM import in node:
197
- 0 && (module.exports = {
198
- trackInteractOutside
199
- });
200
- //# sourceMappingURL=index.js.map
172
+
173
+ exports.trackInteractOutside = trackInteractOutside;
package/dist/index.mjs CHANGED
@@ -1,10 +1,8 @@
1
- // src/index.ts
2
- import { addDomEvent, fireCustomEvent, isContextMenuEvent } from "@zag-js/dom-event";
3
- import { contains, getDocument, getEventTarget, getWindow, isFocusable, isHTMLElement, raf } from "@zag-js/dom-query";
4
- import { callAll } from "@zag-js/utils";
1
+ import { addDomEvent, fireCustomEvent, isContextMenuEvent, queueBeforeEvent } from '@zag-js/dom-event';
2
+ import { getDocument, getWindow, raf, isFocusable, getEventTarget, isHTMLElement, contains } from '@zag-js/dom-query';
3
+ import { callAll } from '@zag-js/utils';
5
4
 
6
- // src/get-window-frames.ts
7
- import { queueBeforeEvent } from "@zag-js/dom-event";
5
+ // src/index.ts
8
6
  function getWindowFrames(win) {
9
7
  const frames = {
10
8
  each(cb) {
@@ -169,7 +167,5 @@ function trackInteractOutside(nodeOrFn, options) {
169
167
  cleanups.forEach((fn) => fn?.());
170
168
  };
171
169
  }
172
- export {
173
- trackInteractOutside
174
- };
175
- //# sourceMappingURL=index.mjs.map
170
+
171
+ export { trackInteractOutside };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/interact-outside",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "description": "Track interations or focus outside an element",
5
5
  "keywords": [
6
6
  "js",
@@ -13,13 +13,12 @@
13
13
  "repository": "https://github.com/chakra-ui/zag/tree/main/packages/utilities/interact-outside",
14
14
  "sideEffects": false,
15
15
  "files": [
16
- "dist",
17
- "src"
16
+ "dist"
18
17
  ],
19
18
  "dependencies": {
20
- "@zag-js/dom-query": "0.70.0",
21
- "@zag-js/dom-event": "0.70.0",
22
- "@zag-js/utils": "0.70.0"
19
+ "@zag-js/dom-query": "0.71.0",
20
+ "@zag-js/dom-event": "0.71.0",
21
+ "@zag-js/utils": "0.71.0"
23
22
  },
24
23
  "devDependencies": {
25
24
  "clean-package": "2.2.0"
@@ -44,7 +43,7 @@
44
43
  },
45
44
  "scripts": {
46
45
  "build": "tsup",
47
- "test": "jest --config ../../../jest.config.js --rootDir tests",
46
+ "test": "vitest",
48
47
  "lint": "eslint src",
49
48
  "test-ci": "pnpm test --ci --runInBand -u",
50
49
  "test-watch": "pnpm test --watchAll"
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
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 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(addDomEvent(doc, \"click\", handler, { once: true }))\n pointerdownCleanups.add(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 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"]}
@@ -1 +0,0 @@
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 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(addDomEvent(doc, \"click\", handler, { once: true }))\n pointerdownCleanups.add(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 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":["handler"]}
@@ -1,51 +0,0 @@
1
- import { queueBeforeEvent } from "@zag-js/dom-event"
2
-
3
- export function getWindowFrames(win: Window) {
4
- const frames = {
5
- each(cb: (win: Window) => void) {
6
- for (let i = 0; i < win.frames?.length; i += 1) {
7
- const frame = win.frames[i]
8
- if (frame) cb(frame)
9
- }
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
-
27
- addEventListener(event: string, listener: any, options?: any) {
28
- frames.each((frame) => {
29
- try {
30
- frame.document.addEventListener(event, listener, options)
31
- } catch {}
32
- })
33
-
34
- return () => {
35
- try {
36
- frames.removeEventListener(event, listener, options)
37
- } catch {}
38
- }
39
- },
40
-
41
- removeEventListener(event: string, listener: any, options?: any) {
42
- frames.each((frame) => {
43
- try {
44
- frame.document.removeEventListener(event, listener, options)
45
- } catch {}
46
- })
47
- },
48
- }
49
-
50
- return frames
51
- }
package/src/index.ts DELETED
@@ -1,189 +0,0 @@
1
- import { addDomEvent, fireCustomEvent, isContextMenuEvent } from "@zag-js/dom-event"
2
- import { contains, getDocument, getEventTarget, getWindow, isFocusable, isHTMLElement, raf } from "@zag-js/dom-query"
3
- import { callAll } from "@zag-js/utils"
4
- import { getWindowFrames } from "./get-window-frames"
5
-
6
- export interface InteractOutsideHandlers {
7
- /**
8
- * Function called when the pointer is pressed down outside the component
9
- */
10
- onPointerDownOutside?: (event: PointerDownOutsideEvent) => void
11
- /**
12
- * Function called when the focus is moved outside the component
13
- */
14
- onFocusOutside?: (event: FocusOutsideEvent) => void
15
- /**
16
- * Function called when an interaction happens outside the component
17
- */
18
- onInteractOutside?: (event: InteractOutsideEvent) => void
19
- }
20
-
21
- export interface InteractOutsideOptions extends InteractOutsideHandlers {
22
- exclude?: (target: HTMLElement) => boolean
23
- defer?: boolean
24
- }
25
-
26
- export interface EventDetails<T> {
27
- originalEvent: T
28
- contextmenu: boolean
29
- focusable: boolean
30
- }
31
-
32
- const POINTER_OUTSIDE_EVENT = "pointerdown.outside"
33
- const FOCUS_OUTSIDE_EVENT = "focus.outside"
34
-
35
- export type PointerDownOutsideEvent = CustomEvent<EventDetails<PointerEvent>>
36
-
37
- export type FocusOutsideEvent = CustomEvent<EventDetails<FocusEvent>>
38
-
39
- export type InteractOutsideEvent = PointerDownOutsideEvent | FocusOutsideEvent
40
-
41
- export type MaybeElement = HTMLElement | null | undefined
42
- export type NodeOrFn = MaybeElement | (() => MaybeElement)
43
-
44
- function isComposedPathFocusable(composedPath: EventTarget[]) {
45
- for (const node of composedPath) {
46
- if (isHTMLElement(node) && isFocusable(node)) return true
47
- }
48
- return false
49
- }
50
-
51
- const isPointerEvent = (event: Event): event is PointerEvent => "clientY" in event
52
-
53
- function isEventPointWithin(node: MaybeElement, event: Event) {
54
- if (!isPointerEvent(event) || !node) return false
55
-
56
- const rect = node.getBoundingClientRect()
57
- if (rect.width === 0 || rect.height === 0) return false
58
-
59
- return (
60
- rect.top <= event.clientY &&
61
- event.clientY <= rect.top + rect.height &&
62
- rect.left <= event.clientX &&
63
- event.clientX <= rect.left + rect.width
64
- )
65
- }
66
-
67
- function isEventWithinScrollbar(event: Event): boolean {
68
- const target = getEventTarget<HTMLElement>(event)
69
- if (!target || !isPointerEvent(event)) return false
70
-
71
- const isScrollableY = target.scrollHeight > target.clientHeight
72
- const onScrollbarY = isScrollableY && event.clientX > target.clientWidth
73
-
74
- const isScrollableX = target.scrollWidth > target.clientWidth
75
- const onScrollbarX = isScrollableX && event.clientY > target.clientHeight
76
-
77
- return onScrollbarY || onScrollbarX
78
- }
79
-
80
- function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOptions) {
81
- const { exclude, onFocusOutside, onPointerDownOutside, onInteractOutside, defer } = options
82
-
83
- if (!node) return
84
-
85
- const doc = getDocument(node)
86
- const win = getWindow(node)
87
- const frames = getWindowFrames(win)
88
-
89
- function isEventOutside(event: Event): boolean {
90
- const target = getEventTarget(event)
91
- if (!isHTMLElement(target)) return false
92
- if (contains(node, target)) return false
93
- if (isEventPointWithin(node, event)) return false
94
- if (isEventWithinScrollbar(event)) return false
95
- return !exclude?.(target)
96
- }
97
-
98
- const pointerdownCleanups: Set<VoidFunction> = new Set()
99
-
100
- function onPointerDown(event: PointerEvent) {
101
- //
102
- function handler() {
103
- const func = defer ? raf : (v: any) => v()
104
- const composedPath = event.composedPath?.() ?? [event.target]
105
- func(() => {
106
- if (!node || !isEventOutside(event)) return
107
-
108
- if (onPointerDownOutside || onInteractOutside) {
109
- const handler = callAll(onPointerDownOutside, onInteractOutside) as EventListener
110
- node.addEventListener(POINTER_OUTSIDE_EVENT, handler, { once: true })
111
- }
112
-
113
- fireCustomEvent(node, POINTER_OUTSIDE_EVENT, {
114
- bubbles: false,
115
- cancelable: true,
116
- detail: {
117
- originalEvent: event,
118
- contextmenu: isContextMenuEvent(event),
119
- focusable: isComposedPathFocusable(composedPath),
120
- },
121
- })
122
- })
123
- }
124
-
125
- if (event.pointerType === "touch") {
126
- // flush any pending pointerup events
127
- pointerdownCleanups.forEach((fn) => fn())
128
-
129
- // add a pointerup event listener to the document and all frame documents
130
- pointerdownCleanups.add(addDomEvent(doc, "click", handler, { once: true }))
131
- pointerdownCleanups.add(frames.addEventListener("click", handler, { once: true }))
132
- } else {
133
- handler()
134
- }
135
- }
136
- const cleanups = new Set<VoidFunction>()
137
-
138
- const timer = setTimeout(() => {
139
- cleanups.add(frames.addEventListener("pointerdown", onPointerDown, true))
140
- cleanups.add(addDomEvent(doc, "pointerdown", onPointerDown, true))
141
- }, 0)
142
-
143
- function onFocusin(event: FocusEvent) {
144
- //
145
- const func = defer ? raf : (v: any) => v()
146
- func(() => {
147
- if (!node || !isEventOutside(event)) return
148
-
149
- if (onFocusOutside || onInteractOutside) {
150
- const handler = callAll(onFocusOutside, onInteractOutside) as EventListener
151
- node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true })
152
- }
153
-
154
- fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, {
155
- bubbles: false,
156
- cancelable: true,
157
- detail: {
158
- originalEvent: event,
159
- contextmenu: false,
160
- focusable: isFocusable(getEventTarget(event)),
161
- },
162
- })
163
- })
164
- }
165
-
166
- cleanups.add(addDomEvent(doc, "focusin", onFocusin, true))
167
- cleanups.add(frames.addEventListener("focusin", onFocusin, true))
168
-
169
- return () => {
170
- clearTimeout(timer)
171
- pointerdownCleanups.forEach((fn) => fn())
172
- cleanups.forEach((fn) => fn())
173
- }
174
- }
175
-
176
- export function trackInteractOutside(nodeOrFn: NodeOrFn, options: InteractOutsideOptions) {
177
- const { defer } = options
178
- const func = defer ? raf : (v: any) => v()
179
- const cleanups: (VoidFunction | undefined)[] = []
180
- cleanups.push(
181
- func(() => {
182
- const node = typeof nodeOrFn === "function" ? nodeOrFn() : nodeOrFn
183
- cleanups.push(trackInteractOutsideImpl(node, options))
184
- }),
185
- )
186
- return () => {
187
- cleanups.forEach((fn) => fn?.())
188
- }
189
- }