@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 +38 -0
- package/index.d.mts +25 -0
- package/index.d.ts +25 -0
- package/index.js +61 -0
- package/index.js.map +1 -0
- package/index.mjs +34 -0
- package/index.mjs.map +1 -0
- package/package.json +30 -0
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
|
+
}
|