@xsolla/xui-hooks 0.175.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/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @xsolla/xui-hooks
2
+
3
+ Shared React hooks for the XUI toolkit.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @xsolla/xui-hooks
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### `useClickOutside`
14
+
15
+ Calls a callback when a `mousedown` occurs outside the referenced element. The
16
+ listener is attached only while `enabled` is strictly `true`, so you can pass a
17
+ possibly-nullish flag without coercing it (e.g. only listen while a dropdown is
18
+ open and you're on the web).
19
+
20
+ ```tsx
21
+ import { useRef, useState } from "react";
22
+ import { useClickOutside } from "@xsolla/xui-hooks";
23
+
24
+ function Dropdown() {
25
+ const ref = useRef<HTMLDivElement>(null);
26
+ const [open, setOpen] = useState(false);
27
+
28
+ useClickOutside(ref, () => setOpen(false), open);
29
+
30
+ return <div ref={ref}>{/* ... */}</div>;
31
+ }
32
+ ```
33
+
34
+ ## Exports
35
+
36
+ - `useClickOutside(ref, callback, enabled)` — Fires `callback(event)` on a
37
+ `mousedown` outside `ref`. Active only when `enabled === true`;
38
+ `false`/`null`/`undefined` keep it inactive.
package/index.d.mts ADDED
@@ -0,0 +1,25 @@
1
+ import { RefObject } from 'react';
2
+
3
+ /**
4
+ * Calls `callback` when a `mousedown` happens outside of `ref`'s element.
5
+ *
6
+ * Shadow-DOM safe (microfrontends): the listener is attached to the element's
7
+ * `ownerDocument` and "outside" is checked via `event.composedPath()`, which
8
+ * pierces shadow boundaries. This avoids the retargeting problem where a
9
+ * document-level `event.target` collapses to the shadow host, and still catches
10
+ * clicks in the light DOM outside the shadow root.
11
+ *
12
+ * Uses the capture phase so a descendant calling `event.stopPropagation()` on
13
+ * `mousedown` (common in nested microfrontend trees) cannot suppress detection.
14
+ * The trigger that toggles open state must live inside `ref`, otherwise the
15
+ * capture listener may fire before the trigger's own handler.
16
+ *
17
+ * @param ref Ref to the element to detect outside clicks for.
18
+ * @param callback Invoked with the originating event on an outside click.
19
+ * @param enabled Listener is attached only when this is strictly `true`.
20
+ * `false`/`null`/`undefined` leave it detached, so consumers
21
+ * can pass a possibly-nullish flag without coercing it.
22
+ */
23
+ declare function useClickOutside(ref: RefObject<HTMLElement | null>, callback: (event: MouseEvent) => void, enabled: boolean | null | undefined): void;
24
+
25
+ export { useClickOutside };
package/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { RefObject } from 'react';
2
+
3
+ /**
4
+ * Calls `callback` when a `mousedown` happens outside of `ref`'s element.
5
+ *
6
+ * Shadow-DOM safe (microfrontends): the listener is attached to the element's
7
+ * `ownerDocument` and "outside" is checked via `event.composedPath()`, which
8
+ * pierces shadow boundaries. This avoids the retargeting problem where a
9
+ * document-level `event.target` collapses to the shadow host, and still catches
10
+ * clicks in the light DOM outside the shadow root.
11
+ *
12
+ * Uses the capture phase so a descendant calling `event.stopPropagation()` on
13
+ * `mousedown` (common in nested microfrontend trees) cannot suppress detection.
14
+ * The trigger that toggles open state must live inside `ref`, otherwise the
15
+ * capture listener may fire before the trigger's own handler.
16
+ *
17
+ * @param ref Ref to the element to detect outside clicks for.
18
+ * @param callback Invoked with the originating event on an outside click.
19
+ * @param enabled Listener is attached only when this is strictly `true`.
20
+ * `false`/`null`/`undefined` leave it detached, so consumers
21
+ * can pass a possibly-nullish flag without coercing it.
22
+ */
23
+ declare function useClickOutside(ref: RefObject<HTMLElement | null>, callback: (event: MouseEvent) => void, enabled: boolean | null | undefined): void;
24
+
25
+ export { useClickOutside };
package/index.js ADDED
@@ -0,0 +1,61 @@
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);
19
+
20
+ // src/index.tsx
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ useClickOutside: () => useClickOutside
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/useClickOutside.ts
28
+ var import_react = require("react");
29
+ function useClickOutside(ref, callback, enabled) {
30
+ (0, import_react.useEffect)(() => {
31
+ if (enabled !== true) {
32
+ return;
33
+ }
34
+ const { current: element } = ref;
35
+ if (!element) {
36
+ return;
37
+ }
38
+ const doc = element.ownerDocument ?? (typeof document !== "undefined" ? document : null);
39
+ if (!doc) {
40
+ return;
41
+ }
42
+ const handleClick = (event) => {
43
+ const { current: target } = ref;
44
+ if (!target || !target.isConnected) {
45
+ return;
46
+ }
47
+ const composedPath = typeof event.composedPath === "function" ? event.composedPath() : [];
48
+ const isInside = composedPath.length ? composedPath.includes(target) : target.contains(event.target);
49
+ if (!isInside) {
50
+ callback(event);
51
+ }
52
+ };
53
+ doc.addEventListener("mousedown", handleClick, true);
54
+ return () => doc.removeEventListener("mousedown", handleClick, true);
55
+ }, [ref, callback, enabled]);
56
+ }
57
+ // Annotate the CommonJS export names for ESM import in node:
58
+ 0 && (module.exports = {
59
+ useClickOutside
60
+ });
61
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx","../src/useClickOutside.ts"],"sourcesContent":["export * from \"./useClickOutside\";\n","import { useEffect, type RefObject } from \"react\";\n\n/**\n * Calls `callback` when a `mousedown` happens outside of `ref`'s element.\n *\n * Shadow-DOM safe (microfrontends): the listener is attached to the element's\n * `ownerDocument` and \"outside\" is checked via `event.composedPath()`, which\n * pierces shadow boundaries. This avoids the retargeting problem where a\n * document-level `event.target` collapses to the shadow host, and still catches\n * clicks in the light DOM outside the shadow root.\n *\n * Uses the capture phase so a descendant calling `event.stopPropagation()` on\n * `mousedown` (common in nested microfrontend trees) cannot suppress detection.\n * The trigger that toggles open state must live inside `ref`, otherwise the\n * capture listener may fire before the trigger's own handler.\n *\n * @param ref Ref to the element to detect outside clicks for.\n * @param callback Invoked with the originating event on an outside click.\n * @param enabled Listener is attached only when this is strictly `true`.\n * `false`/`null`/`undefined` leave it detached, so consumers\n * can pass a possibly-nullish flag without coercing it.\n */\nexport function useClickOutside(\n ref: RefObject<HTMLElement | null>,\n callback: (event: MouseEvent) => void,\n enabled: boolean | null | undefined\n): void {\n useEffect(() => {\n if (enabled !== true) {\n return;\n }\n\n const { current: element } = ref;\n if (!element) {\n return;\n }\n\n const doc =\n element.ownerDocument ??\n (typeof document !== \"undefined\" ? document : null);\n if (!doc) {\n return;\n }\n\n const handleClick = (event: MouseEvent) => {\n const { current: target } = ref;\n if (!target || !target.isConnected) {\n return;\n }\n\n const composedPath =\n typeof event.composedPath === \"function\" ? event.composedPath() : [];\n\n const isInside = composedPath.length\n ? composedPath.includes(target)\n : target.contains(event.target as Node);\n\n if (!isInside) {\n callback(event);\n }\n };\n\n doc.addEventListener(\"mousedown\", handleClick, true);\n return () => doc.removeEventListener(\"mousedown\", handleClick, true);\n }, [ref, callback, enabled]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA0C;AAsBnC,SAAS,gBACd,KACA,UACA,SACM;AACN,8BAAU,MAAM;AACd,QAAI,YAAY,MAAM;AACpB;AAAA,IACF;AAEA,UAAM,EAAE,SAAS,QAAQ,IAAI;AAC7B,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAEA,UAAM,MACJ,QAAQ,kBACP,OAAO,aAAa,cAAc,WAAW;AAChD,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AAEA,UAAM,cAAc,CAAC,UAAsB;AACzC,YAAM,EAAE,SAAS,OAAO,IAAI;AAC5B,UAAI,CAAC,UAAU,CAAC,OAAO,aAAa;AAClC;AAAA,MACF;AAEA,YAAM,eACJ,OAAO,MAAM,iBAAiB,aAAa,MAAM,aAAa,IAAI,CAAC;AAErE,YAAM,WAAW,aAAa,SAC1B,aAAa,SAAS,MAAM,IAC5B,OAAO,SAAS,MAAM,MAAc;AAExC,UAAI,CAAC,UAAU;AACb,iBAAS,KAAK;AAAA,MAChB;AAAA,IACF;AAEA,QAAI,iBAAiB,aAAa,aAAa,IAAI;AACnD,WAAO,MAAM,IAAI,oBAAoB,aAAa,aAAa,IAAI;AAAA,EACrE,GAAG,CAAC,KAAK,UAAU,OAAO,CAAC;AAC7B;","names":[]}
package/index.mjs ADDED
@@ -0,0 +1,34 @@
1
+ // src/useClickOutside.ts
2
+ import { useEffect } from "react";
3
+ function useClickOutside(ref, callback, enabled) {
4
+ useEffect(() => {
5
+ if (enabled !== true) {
6
+ return;
7
+ }
8
+ const { current: element } = ref;
9
+ if (!element) {
10
+ return;
11
+ }
12
+ const doc = element.ownerDocument ?? (typeof document !== "undefined" ? document : null);
13
+ if (!doc) {
14
+ return;
15
+ }
16
+ const handleClick = (event) => {
17
+ const { current: target } = ref;
18
+ if (!target || !target.isConnected) {
19
+ return;
20
+ }
21
+ const composedPath = typeof event.composedPath === "function" ? event.composedPath() : [];
22
+ const isInside = composedPath.length ? composedPath.includes(target) : target.contains(event.target);
23
+ if (!isInside) {
24
+ callback(event);
25
+ }
26
+ };
27
+ doc.addEventListener("mousedown", handleClick, true);
28
+ return () => doc.removeEventListener("mousedown", handleClick, true);
29
+ }, [ref, callback, enabled]);
30
+ }
31
+ export {
32
+ useClickOutside
33
+ };
34
+ //# sourceMappingURL=index.mjs.map
package/index.mjs.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useClickOutside.ts"],"sourcesContent":["import { useEffect, type RefObject } from \"react\";\n\n/**\n * Calls `callback` when a `mousedown` happens outside of `ref`'s element.\n *\n * Shadow-DOM safe (microfrontends): the listener is attached to the element's\n * `ownerDocument` and \"outside\" is checked via `event.composedPath()`, which\n * pierces shadow boundaries. This avoids the retargeting problem where a\n * document-level `event.target` collapses to the shadow host, and still catches\n * clicks in the light DOM outside the shadow root.\n *\n * Uses the capture phase so a descendant calling `event.stopPropagation()` on\n * `mousedown` (common in nested microfrontend trees) cannot suppress detection.\n * The trigger that toggles open state must live inside `ref`, otherwise the\n * capture listener may fire before the trigger's own handler.\n *\n * @param ref Ref to the element to detect outside clicks for.\n * @param callback Invoked with the originating event on an outside click.\n * @param enabled Listener is attached only when this is strictly `true`.\n * `false`/`null`/`undefined` leave it detached, so consumers\n * can pass a possibly-nullish flag without coercing it.\n */\nexport function useClickOutside(\n ref: RefObject<HTMLElement | null>,\n callback: (event: MouseEvent) => void,\n enabled: boolean | null | undefined\n): void {\n useEffect(() => {\n if (enabled !== true) {\n return;\n }\n\n const { current: element } = ref;\n if (!element) {\n return;\n }\n\n const doc =\n element.ownerDocument ??\n (typeof document !== \"undefined\" ? document : null);\n if (!doc) {\n return;\n }\n\n const handleClick = (event: MouseEvent) => {\n const { current: target } = ref;\n if (!target || !target.isConnected) {\n return;\n }\n\n const composedPath =\n typeof event.composedPath === \"function\" ? event.composedPath() : [];\n\n const isInside = composedPath.length\n ? composedPath.includes(target)\n : target.contains(event.target as Node);\n\n if (!isInside) {\n callback(event);\n }\n };\n\n doc.addEventListener(\"mousedown\", handleClick, true);\n return () => doc.removeEventListener(\"mousedown\", handleClick, true);\n }, [ref, callback, enabled]);\n}\n"],"mappings":";AAAA,SAAS,iBAAiC;AAsBnC,SAAS,gBACd,KACA,UACA,SACM;AACN,YAAU,MAAM;AACd,QAAI,YAAY,MAAM;AACpB;AAAA,IACF;AAEA,UAAM,EAAE,SAAS,QAAQ,IAAI;AAC7B,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAEA,UAAM,MACJ,QAAQ,kBACP,OAAO,aAAa,cAAc,WAAW;AAChD,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AAEA,UAAM,cAAc,CAAC,UAAsB;AACzC,YAAM,EAAE,SAAS,OAAO,IAAI;AAC5B,UAAI,CAAC,UAAU,CAAC,OAAO,aAAa;AAClC;AAAA,MACF;AAEA,YAAM,eACJ,OAAO,MAAM,iBAAiB,aAAa,MAAM,aAAa,IAAI,CAAC;AAErE,YAAM,WAAW,aAAa,SAC1B,aAAa,SAAS,MAAM,IAC5B,OAAO,SAAS,MAAM,MAAc;AAExC,UAAI,CAAC,UAAU;AACb,iBAAS,KAAK;AAAA,MAChB;AAAA,IACF;AAEA,QAAI,iBAAiB,aAAa,aAAa,IAAI;AACnD,WAAO,MAAM,IAAI,oBAAoB,aAAa,aAAa,IAAI;AAAA,EACrE,GAAG,CAAC,KAAK,UAAU,OAAO,CAAC;AAC7B;","names":[]}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@xsolla/xui-hooks",
3
+ "version": "0.175.0",
4
+ "main": "./index.js",
5
+ "module": "./index.mjs",
6
+ "types": "./index.d.ts",
7
+ "scripts": {
8
+ "build": "tsup",
9
+ "test": "vitest",
10
+ "test:run": "vitest run",
11
+ "test:coverage": "vitest run --coverage"
12
+ },
13
+ "peerDependencies": {
14
+ "react": ">=16.8.0"
15
+ },
16
+ "devDependencies": {
17
+ "@testing-library/jest-dom": "^6.9.1",
18
+ "@testing-library/react": "^14.1.2",
19
+ "@types/react": "^18.0.0",
20
+ "@types/react-dom": "^18.0.0",
21
+ "@vitest/coverage-v8": "^4.0.18",
22
+ "jsdom": "^24.0.0",
23
+ "react": "^18.0.0",
24
+ "react-dom": "^18.0.0",
25
+ "tsup": "^8.0.0",
26
+ "vitest": "^4.0.18"
27
+ },
28
+ "license": "MIT",
29
+ "sideEffects": false
30
+ }