@zag-js/interact-outside 0.1.0 → 0.1.1

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.d.ts CHANGED
@@ -1,6 +1,17 @@
1
- export declare type InteractOutsideOptions = {
1
+ export declare type InteractOutsideHandlers = {
2
+ onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;
3
+ onFocusOutside?: (event: FocusOutsideEvent) => void;
4
+ };
5
+ export declare type InteractOutsideOptions = InteractOutsideHandlers & {
2
6
  exclude?: (target: HTMLElement) => boolean;
3
- onPointerDownOutside?: (event: Event) => void;
4
- onFocusOutside?: (event: Event) => void;
5
7
  };
8
+ declare type EventDetails<T> = {
9
+ originalEvent: T;
10
+ contextmenu: boolean;
11
+ focusable: boolean;
12
+ };
13
+ export declare type PointerDownOutsideEvent = CustomEvent<EventDetails<PointerEvent>>;
14
+ export declare type FocusOutsideEvent = CustomEvent<EventDetails<FocusEvent>>;
15
+ export declare type InteractOutsideEvent = PointerDownOutsideEvent | FocusOutsideEvent;
6
16
  export declare function trackInteractOutside(node: HTMLElement | null, options: InteractOutsideOptions): () => void;
17
+ export {};
package/dist/index.js CHANGED
@@ -25,29 +25,30 @@ __export(src_exports, {
25
25
  module.exports = __toCommonJS(src_exports);
26
26
 
27
27
  // ../dom/dist/index.mjs
28
- var hasProp = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
29
28
  var runIfFn = (v, ...a) => {
30
29
  const res = typeof v === "function" ? v(...a) : v;
31
30
  return res != null ? res : void 0;
32
31
  };
33
- var isRef = (v) => hasProp(v, "current");
34
- function addDomEvent(target, eventName, handler, options) {
35
- const node = isRef(target) ? target.current : runIfFn(target);
36
- node == null ? void 0 : node.addEventListener(eventName, handler, options);
37
- return () => {
38
- node == null ? void 0 : node.removeEventListener(eventName, handler, options);
39
- };
32
+ var hasProp = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
33
+ var isDom = () => typeof window !== "undefined";
34
+ function getPlatform() {
35
+ var _a;
36
+ const agent = navigator.userAgentData;
37
+ return (_a = agent == null ? void 0 : agent.platform) != null ? _a : navigator.platform;
40
38
  }
39
+ var pt = (v) => isDom() && v.test(getPlatform());
40
+ var isTouchDevice = () => isDom() && !!navigator.maxTouchPoints;
41
+ var isMac = () => pt(/^Mac/) && !isTouchDevice;
41
42
  function isWindow(value) {
42
43
  return (value == null ? void 0 : value.toString()) === "[object Window]";
43
44
  }
44
- function getOwnerDocument(el) {
45
+ function getDocument(el) {
45
46
  var _a;
46
47
  if (isWindow(el))
47
48
  return el.document;
48
49
  return (_a = el == null ? void 0 : el.ownerDocument) != null ? _a : document;
49
50
  }
50
- function getOwnerWindow(el) {
51
+ function getWindow(el) {
51
52
  var _a;
52
53
  return (_a = el == null ? void 0 : el.ownerDocument.defaultView) != null ? _a : window;
53
54
  }
@@ -63,21 +64,52 @@ function contains(parent, child) {
63
64
  function isHTMLElement(v) {
64
65
  return typeof v === "object" && (v == null ? void 0 : v.nodeType) === Node.ELEMENT_NODE && typeof (v == null ? void 0 : v.nodeName) === "string";
65
66
  }
67
+ function isVisible(el) {
68
+ if (!isHTMLElement(el))
69
+ return false;
70
+ return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
71
+ }
72
+ var isContextMenuEvent = (e) => {
73
+ return e.button === 2 || isCtrlKey(e) && e.button === 0;
74
+ };
75
+ var isCtrlKey = (v) => isMac() ? v.metaKey && !v.ctrlKey : v.ctrlKey && !v.metaKey;
76
+ function fireCustomEvent(el, type, init) {
77
+ if (!el)
78
+ return;
79
+ const win = getWindow(el);
80
+ const event = new win.CustomEvent(type, init);
81
+ return el.dispatchEvent(event);
82
+ }
83
+ var focusableSelector = "input:not([type='hidden']):not([disabled]), select:not([disabled]), textarea:not([disabled]), a[href], button:not([disabled]), [tabindex], iframe, object, embed, area[href], audio[controls], video[controls], [contenteditable]:not([contenteditable='false']), details > summary:first-of-type";
84
+ function isFocusable(element) {
85
+ if (!element)
86
+ return false;
87
+ return element.matches(focusableSelector) && isVisible(element);
88
+ }
89
+ var isRef = (v) => hasProp(v, "current");
90
+ function addDomEvent(target, eventName, handler, options) {
91
+ const node = isRef(target) ? target.current : runIfFn(target);
92
+ node == null ? void 0 : node.addEventListener(eventName, handler, options);
93
+ return () => {
94
+ node == null ? void 0 : node.removeEventListener(eventName, handler, options);
95
+ };
96
+ }
66
97
 
67
98
  // src/index.ts
99
+ var POINTER_OUTSIDE_EVENT = "pointerdown.outside";
100
+ var FOCUS_OUTSIDE_EVENT = "focus.outside";
68
101
  function trackInteractOutside(node, options) {
69
- const { exclude, onPointerDownOutside, onFocusOutside } = options;
102
+ const { exclude, onFocusOutside, onPointerDownOutside } = options;
70
103
  if (!node)
71
104
  return;
72
- const doc = getOwnerDocument(node);
73
- const win = getOwnerWindow(node);
105
+ const doc = getDocument(node);
106
+ const win = getWindow(node);
74
107
  function isEventOutside(event) {
75
108
  const target = getEventTarget(event);
76
109
  if (!(target instanceof win.HTMLElement)) {
77
110
  return false;
78
111
  }
79
- const doc2 = target.ownerDocument;
80
- if (!contains(doc2.documentElement, target)) {
112
+ if (!contains(doc.documentElement, target)) {
81
113
  return false;
82
114
  }
83
115
  if (contains(node, target)) {
@@ -85,23 +117,57 @@ function trackInteractOutside(node, options) {
85
117
  }
86
118
  return !(exclude == null ? void 0 : exclude(target));
87
119
  }
120
+ let clickHandler;
88
121
  function onPointerDown(event) {
89
- if (isEventOutside(event)) {
90
- onPointerDownOutside == null ? void 0 : onPointerDownOutside(event);
122
+ function handler() {
123
+ if (!node || !isEventOutside(event))
124
+ return;
125
+ if (onPointerDownOutside) {
126
+ node.addEventListener(POINTER_OUTSIDE_EVENT, onPointerDownOutside, { once: true });
127
+ }
128
+ fireCustomEvent(node, POINTER_OUTSIDE_EVENT, {
129
+ bubbles: false,
130
+ cancelable: true,
131
+ detail: {
132
+ originalEvent: event,
133
+ contextmenu: isContextMenuEvent(event),
134
+ focusable: isFocusable(getEventTarget(event))
135
+ }
136
+ });
137
+ }
138
+ if (event.pointerType === "touch") {
139
+ doc.removeEventListener("click", handler);
140
+ clickHandler = handler;
141
+ doc.addEventListener("click", handler, { once: true });
142
+ } else {
143
+ handler();
91
144
  }
92
145
  }
93
146
  const cleanups = /* @__PURE__ */ new Set();
94
147
  const timer = setTimeout(() => {
95
148
  cleanups.add(addDomEvent(doc, "pointerdown", onPointerDown, true));
96
- });
149
+ }, 0);
97
150
  function onFocusin(event) {
98
- if (isEventOutside(event)) {
99
- onFocusOutside == null ? void 0 : onFocusOutside(event);
151
+ if (!node || !isEventOutside(event))
152
+ return;
153
+ if (onFocusOutside) {
154
+ node.addEventListener(FOCUS_OUTSIDE_EVENT, onFocusOutside, { once: true });
100
155
  }
156
+ fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, {
157
+ bubbles: false,
158
+ cancelable: true,
159
+ detail: {
160
+ originalEvent: event,
161
+ contextmenu: false,
162
+ focusable: isFocusable(getEventTarget(event))
163
+ }
164
+ });
101
165
  }
102
166
  cleanups.add(addDomEvent(doc, "focusin", onFocusin, true));
103
167
  return () => {
104
168
  clearTimeout(timer);
169
+ if (clickHandler)
170
+ doc.removeEventListener("click", clickHandler);
105
171
  cleanups.forEach((fn) => fn());
106
172
  };
107
173
  }
package/dist/index.mjs CHANGED
@@ -1,29 +1,28 @@
1
- "use strict";
2
-
3
1
  // ../dom/dist/index.mjs
4
- var hasProp = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
5
2
  var runIfFn = (v, ...a) => {
6
3
  const res = typeof v === "function" ? v(...a) : v;
7
4
  return res != null ? res : void 0;
8
5
  };
9
- var isRef = (v) => hasProp(v, "current");
10
- function addDomEvent(target, eventName, handler, options) {
11
- const node = isRef(target) ? target.current : runIfFn(target);
12
- node == null ? void 0 : node.addEventListener(eventName, handler, options);
13
- return () => {
14
- node == null ? void 0 : node.removeEventListener(eventName, handler, options);
15
- };
6
+ var hasProp = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
7
+ var isDom = () => typeof window !== "undefined";
8
+ function getPlatform() {
9
+ var _a;
10
+ const agent = navigator.userAgentData;
11
+ return (_a = agent == null ? void 0 : agent.platform) != null ? _a : navigator.platform;
16
12
  }
13
+ var pt = (v) => isDom() && v.test(getPlatform());
14
+ var isTouchDevice = () => isDom() && !!navigator.maxTouchPoints;
15
+ var isMac = () => pt(/^Mac/) && !isTouchDevice;
17
16
  function isWindow(value) {
18
17
  return (value == null ? void 0 : value.toString()) === "[object Window]";
19
18
  }
20
- function getOwnerDocument(el) {
19
+ function getDocument(el) {
21
20
  var _a;
22
21
  if (isWindow(el))
23
22
  return el.document;
24
23
  return (_a = el == null ? void 0 : el.ownerDocument) != null ? _a : document;
25
24
  }
26
- function getOwnerWindow(el) {
25
+ function getWindow(el) {
27
26
  var _a;
28
27
  return (_a = el == null ? void 0 : el.ownerDocument.defaultView) != null ? _a : window;
29
28
  }
@@ -39,21 +38,52 @@ function contains(parent, child) {
39
38
  function isHTMLElement(v) {
40
39
  return typeof v === "object" && (v == null ? void 0 : v.nodeType) === Node.ELEMENT_NODE && typeof (v == null ? void 0 : v.nodeName) === "string";
41
40
  }
41
+ function isVisible(el) {
42
+ if (!isHTMLElement(el))
43
+ return false;
44
+ return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
45
+ }
46
+ var isContextMenuEvent = (e) => {
47
+ return e.button === 2 || isCtrlKey(e) && e.button === 0;
48
+ };
49
+ var isCtrlKey = (v) => isMac() ? v.metaKey && !v.ctrlKey : v.ctrlKey && !v.metaKey;
50
+ function fireCustomEvent(el, type, init) {
51
+ if (!el)
52
+ return;
53
+ const win = getWindow(el);
54
+ const event = new win.CustomEvent(type, init);
55
+ return el.dispatchEvent(event);
56
+ }
57
+ var focusableSelector = "input:not([type='hidden']):not([disabled]), select:not([disabled]), textarea:not([disabled]), a[href], button:not([disabled]), [tabindex], iframe, object, embed, area[href], audio[controls], video[controls], [contenteditable]:not([contenteditable='false']), details > summary:first-of-type";
58
+ function isFocusable(element) {
59
+ if (!element)
60
+ return false;
61
+ return element.matches(focusableSelector) && isVisible(element);
62
+ }
63
+ var isRef = (v) => hasProp(v, "current");
64
+ function addDomEvent(target, eventName, handler, options) {
65
+ const node = isRef(target) ? target.current : runIfFn(target);
66
+ node == null ? void 0 : node.addEventListener(eventName, handler, options);
67
+ return () => {
68
+ node == null ? void 0 : node.removeEventListener(eventName, handler, options);
69
+ };
70
+ }
42
71
 
43
72
  // src/index.ts
73
+ var POINTER_OUTSIDE_EVENT = "pointerdown.outside";
74
+ var FOCUS_OUTSIDE_EVENT = "focus.outside";
44
75
  function trackInteractOutside(node, options) {
45
- const { exclude, onPointerDownOutside, onFocusOutside } = options;
76
+ const { exclude, onFocusOutside, onPointerDownOutside } = options;
46
77
  if (!node)
47
78
  return;
48
- const doc = getOwnerDocument(node);
49
- const win = getOwnerWindow(node);
79
+ const doc = getDocument(node);
80
+ const win = getWindow(node);
50
81
  function isEventOutside(event) {
51
82
  const target = getEventTarget(event);
52
83
  if (!(target instanceof win.HTMLElement)) {
53
84
  return false;
54
85
  }
55
- const doc2 = target.ownerDocument;
56
- if (!contains(doc2.documentElement, target)) {
86
+ if (!contains(doc.documentElement, target)) {
57
87
  return false;
58
88
  }
59
89
  if (contains(node, target)) {
@@ -61,23 +91,57 @@ function trackInteractOutside(node, options) {
61
91
  }
62
92
  return !(exclude == null ? void 0 : exclude(target));
63
93
  }
94
+ let clickHandler;
64
95
  function onPointerDown(event) {
65
- if (isEventOutside(event)) {
66
- onPointerDownOutside == null ? void 0 : onPointerDownOutside(event);
96
+ function handler() {
97
+ if (!node || !isEventOutside(event))
98
+ return;
99
+ if (onPointerDownOutside) {
100
+ node.addEventListener(POINTER_OUTSIDE_EVENT, onPointerDownOutside, { once: true });
101
+ }
102
+ fireCustomEvent(node, POINTER_OUTSIDE_EVENT, {
103
+ bubbles: false,
104
+ cancelable: true,
105
+ detail: {
106
+ originalEvent: event,
107
+ contextmenu: isContextMenuEvent(event),
108
+ focusable: isFocusable(getEventTarget(event))
109
+ }
110
+ });
111
+ }
112
+ if (event.pointerType === "touch") {
113
+ doc.removeEventListener("click", handler);
114
+ clickHandler = handler;
115
+ doc.addEventListener("click", handler, { once: true });
116
+ } else {
117
+ handler();
67
118
  }
68
119
  }
69
120
  const cleanups = /* @__PURE__ */ new Set();
70
121
  const timer = setTimeout(() => {
71
122
  cleanups.add(addDomEvent(doc, "pointerdown", onPointerDown, true));
72
- });
123
+ }, 0);
73
124
  function onFocusin(event) {
74
- if (isEventOutside(event)) {
75
- onFocusOutside == null ? void 0 : onFocusOutside(event);
125
+ if (!node || !isEventOutside(event))
126
+ return;
127
+ if (onFocusOutside) {
128
+ node.addEventListener(FOCUS_OUTSIDE_EVENT, onFocusOutside, { once: true });
76
129
  }
130
+ fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, {
131
+ bubbles: false,
132
+ cancelable: true,
133
+ detail: {
134
+ originalEvent: event,
135
+ contextmenu: false,
136
+ focusable: isFocusable(getEventTarget(event))
137
+ }
138
+ });
77
139
  }
78
140
  cleanups.add(addDomEvent(doc, "focusin", onFocusin, true));
79
141
  return () => {
80
142
  clearTimeout(timer);
143
+ if (clickHandler)
144
+ doc.removeEventListener("click", clickHandler);
81
145
  cleanups.forEach((fn) => fn());
82
146
  };
83
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/interact-outside",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Track interations or focus outside an element",
5
5
  "keywords": [
6
6
  "js",
@@ -28,7 +28,7 @@
28
28
  "test:watch": "yarn test --watchAll"
29
29
  },
30
30
  "dependencies": {
31
- "@zag-js/dom-utils": "0.1.5"
31
+ "@zag-js/dom-utils": "0.1.6"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"