@usels/core 0.0.1-beta.3
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 +21 -0
- package/dist/browser/useEventListener/index.d.mts +56 -0
- package/dist/browser/useEventListener/index.d.ts +56 -0
- package/dist/browser/useEventListener/index.js +112 -0
- package/dist/browser/useEventListener/index.js.map +1 -0
- package/dist/browser/useEventListener/index.mjs +88 -0
- package/dist/browser/useEventListener/index.mjs.map +1 -0
- package/dist/browser/useMediaQuery/demo.d.mts +5 -0
- package/dist/browser/useMediaQuery/demo.d.ts +5 -0
- package/dist/browser/useMediaQuery/demo.js +82 -0
- package/dist/browser/useMediaQuery/demo.js.map +1 -0
- package/dist/browser/useMediaQuery/demo.mjs +62 -0
- package/dist/browser/useMediaQuery/demo.mjs.map +1 -0
- package/dist/browser/useMediaQuery/index.d.mts +11 -0
- package/dist/browser/useMediaQuery/index.d.ts +11 -0
- package/dist/browser/useMediaQuery/index.js +89 -0
- package/dist/browser/useMediaQuery/index.js.map +1 -0
- package/dist/browser/useMediaQuery/index.mjs +64 -0
- package/dist/browser/useMediaQuery/index.mjs.map +1 -0
- package/dist/components/Auto/index.d.mts +33 -0
- package/dist/components/Auto/index.d.ts +33 -0
- package/dist/components/Auto/index.js +66 -0
- package/dist/components/Auto/index.js.map +1 -0
- package/dist/components/Auto/index.mjs +34 -0
- package/dist/components/Auto/index.mjs.map +1 -0
- package/dist/elements/useDocumentVisibility/demo.d.mts +5 -0
- package/dist/elements/useDocumentVisibility/demo.d.ts +5 -0
- package/dist/elements/useDocumentVisibility/demo.js +130 -0
- package/dist/elements/useDocumentVisibility/demo.js.map +1 -0
- package/dist/elements/useDocumentVisibility/demo.mjs +114 -0
- package/dist/elements/useDocumentVisibility/demo.mjs.map +1 -0
- package/dist/elements/useDocumentVisibility/index.d.mts +5 -0
- package/dist/elements/useDocumentVisibility/index.d.ts +5 -0
- package/dist/elements/useDocumentVisibility/index.js +45 -0
- package/dist/elements/useDocumentVisibility/index.js.map +1 -0
- package/dist/elements/useDocumentVisibility/index.mjs +21 -0
- package/dist/elements/useDocumentVisibility/index.mjs.map +1 -0
- package/dist/elements/useElementBounding/demo.d.mts +5 -0
- package/dist/elements/useElementBounding/demo.d.ts +5 -0
- package/dist/elements/useElementBounding/demo.js +86 -0
- package/dist/elements/useElementBounding/demo.js.map +1 -0
- package/dist/elements/useElementBounding/demo.mjs +66 -0
- package/dist/elements/useElementBounding/demo.mjs.map +1 -0
- package/dist/elements/useElementBounding/index.d.mts +46 -0
- package/dist/elements/useElementBounding/index.d.ts +46 -0
- package/dist/elements/useElementBounding/index.js +122 -0
- package/dist/elements/useElementBounding/index.js.map +1 -0
- package/dist/elements/useElementBounding/index.mjs +98 -0
- package/dist/elements/useElementBounding/index.mjs.map +1 -0
- package/dist/elements/useElementSize/demo.d.mts +5 -0
- package/dist/elements/useElementSize/demo.d.ts +5 -0
- package/dist/elements/useElementSize/demo.js +82 -0
- package/dist/elements/useElementSize/demo.js.map +1 -0
- package/dist/elements/useElementSize/demo.mjs +62 -0
- package/dist/elements/useElementSize/demo.mjs.map +1 -0
- package/dist/elements/useElementSize/index.d.mts +34 -0
- package/dist/elements/useElementSize/index.d.ts +34 -0
- package/dist/elements/useElementSize/index.js +85 -0
- package/dist/elements/useElementSize/index.js.map +1 -0
- package/dist/elements/useElementSize/index.mjs +61 -0
- package/dist/elements/useElementSize/index.mjs.map +1 -0
- package/dist/elements/useElementVisibility/demo.d.mts +5 -0
- package/dist/elements/useElementVisibility/demo.d.ts +5 -0
- package/dist/elements/useElementVisibility/demo.js +110 -0
- package/dist/elements/useElementVisibility/demo.js.map +1 -0
- package/dist/elements/useElementVisibility/demo.mjs +90 -0
- package/dist/elements/useElementVisibility/demo.mjs.map +1 -0
- package/dist/elements/useElementVisibility/index.d.mts +43 -0
- package/dist/elements/useElementVisibility/index.d.ts +43 -0
- package/dist/elements/useElementVisibility/index.js +58 -0
- package/dist/elements/useElementVisibility/index.js.map +1 -0
- package/dist/elements/useElementVisibility/index.mjs +34 -0
- package/dist/elements/useElementVisibility/index.mjs.map +1 -0
- package/dist/elements/useIntersectionObserver/demo.d.mts +5 -0
- package/dist/elements/useIntersectionObserver/demo.d.ts +5 -0
- package/dist/elements/useIntersectionObserver/demo.js +173 -0
- package/dist/elements/useIntersectionObserver/demo.js.map +1 -0
- package/dist/elements/useIntersectionObserver/demo.mjs +153 -0
- package/dist/elements/useIntersectionObserver/demo.mjs.map +1 -0
- package/dist/elements/useIntersectionObserver/index.d.mts +47 -0
- package/dist/elements/useIntersectionObserver/index.d.ts +47 -0
- package/dist/elements/useIntersectionObserver/index.js +111 -0
- package/dist/elements/useIntersectionObserver/index.js.map +1 -0
- package/dist/elements/useIntersectionObserver/index.mjs +87 -0
- package/dist/elements/useIntersectionObserver/index.mjs.map +1 -0
- package/dist/elements/useMouseInElement/demo.d.mts +5 -0
- package/dist/elements/useMouseInElement/demo.d.ts +5 -0
- package/dist/elements/useMouseInElement/demo.js +103 -0
- package/dist/elements/useMouseInElement/demo.js.map +1 -0
- package/dist/elements/useMouseInElement/demo.mjs +83 -0
- package/dist/elements/useMouseInElement/demo.mjs.map +1 -0
- package/dist/elements/useMouseInElement/index.d.mts +56 -0
- package/dist/elements/useMouseInElement/index.d.ts +56 -0
- package/dist/elements/useMouseInElement/index.js +148 -0
- package/dist/elements/useMouseInElement/index.js.map +1 -0
- package/dist/elements/useMouseInElement/index.mjs +124 -0
- package/dist/elements/useMouseInElement/index.mjs.map +1 -0
- package/dist/elements/useMutationObserver/demo.d.mts +5 -0
- package/dist/elements/useMutationObserver/demo.d.ts +5 -0
- package/dist/elements/useMutationObserver/demo.js +240 -0
- package/dist/elements/useMutationObserver/demo.js.map +1 -0
- package/dist/elements/useMutationObserver/demo.mjs +220 -0
- package/dist/elements/useMutationObserver/demo.mjs.map +1 -0
- package/dist/elements/useMutationObserver/index.d.mts +15 -0
- package/dist/elements/useMutationObserver/index.d.ts +15 -0
- package/dist/elements/useMutationObserver/index.js +69 -0
- package/dist/elements/useMutationObserver/index.js.map +1 -0
- package/dist/elements/useMutationObserver/index.mjs +45 -0
- package/dist/elements/useMutationObserver/index.mjs.map +1 -0
- package/dist/elements/useParentElement/demo.d.mts +5 -0
- package/dist/elements/useParentElement/demo.d.ts +5 -0
- package/dist/elements/useParentElement/demo.js +132 -0
- package/dist/elements/useParentElement/demo.js.map +1 -0
- package/dist/elements/useParentElement/demo.mjs +112 -0
- package/dist/elements/useParentElement/demo.mjs.map +1 -0
- package/dist/elements/useParentElement/index.d.mts +7 -0
- package/dist/elements/useParentElement/index.d.ts +7 -0
- package/dist/elements/useParentElement/index.js +47 -0
- package/dist/elements/useParentElement/index.js.map +1 -0
- package/dist/elements/useParentElement/index.mjs +23 -0
- package/dist/elements/useParentElement/index.mjs.map +1 -0
- package/dist/elements/useRef$/index.js +89 -0
- package/dist/elements/useRef$/index.js.map +1 -0
- package/dist/elements/useRef$/index.mjs +62 -0
- package/dist/elements/useRef$/index.mjs.map +1 -0
- package/dist/elements/useRef_/index.d.mts +60 -0
- package/dist/elements/useRef_/index.d.ts +60 -0
- package/dist/elements/useResizeObserver/demo.d.mts +5 -0
- package/dist/elements/useResizeObserver/demo.d.ts +5 -0
- package/dist/elements/useResizeObserver/demo.js +90 -0
- package/dist/elements/useResizeObserver/demo.js.map +1 -0
- package/dist/elements/useResizeObserver/demo.mjs +70 -0
- package/dist/elements/useResizeObserver/demo.mjs.map +1 -0
- package/dist/elements/useResizeObserver/index.d.mts +36 -0
- package/dist/elements/useResizeObserver/index.d.ts +36 -0
- package/dist/elements/useResizeObserver/index.js +74 -0
- package/dist/elements/useResizeObserver/index.js.map +1 -0
- package/dist/elements/useResizeObserver/index.mjs +49 -0
- package/dist/elements/useResizeObserver/index.mjs.map +1 -0
- package/dist/elements/useWindowFocus/demo.d.mts +5 -0
- package/dist/elements/useWindowFocus/demo.d.ts +5 -0
- package/dist/elements/useWindowFocus/demo.js +100 -0
- package/dist/elements/useWindowFocus/demo.js.map +1 -0
- package/dist/elements/useWindowFocus/demo.mjs +80 -0
- package/dist/elements/useWindowFocus/demo.mjs.map +1 -0
- package/dist/elements/useWindowFocus/index.d.mts +5 -0
- package/dist/elements/useWindowFocus/index.d.ts +5 -0
- package/dist/elements/useWindowFocus/index.js +42 -0
- package/dist/elements/useWindowFocus/index.js.map +1 -0
- package/dist/elements/useWindowFocus/index.mjs +18 -0
- package/dist/elements/useWindowFocus/index.mjs.map +1 -0
- package/dist/elements/useWindowSize/demo.d.mts +5 -0
- package/dist/elements/useWindowSize/demo.d.ts +5 -0
- package/dist/elements/useWindowSize/demo.js +78 -0
- package/dist/elements/useWindowSize/demo.js.map +1 -0
- package/dist/elements/useWindowSize/demo.mjs +58 -0
- package/dist/elements/useWindowSize/demo.mjs.map +1 -0
- package/dist/elements/useWindowSize/index.d.mts +17 -0
- package/dist/elements/useWindowSize/index.d.ts +17 -0
- package/dist/elements/useWindowSize/index.js +96 -0
- package/dist/elements/useWindowSize/index.js.map +1 -0
- package/dist/elements/useWindowSize/index.mjs +76 -0
- package/dist/elements/useWindowSize/index.mjs.map +1 -0
- package/dist/function/get/index.d.mts +45 -0
- package/dist/function/get/index.d.ts +45 -0
- package/dist/function/get/index.js +39 -0
- package/dist/function/get/index.js.map +1 -0
- package/dist/function/get/index.mjs +15 -0
- package/dist/function/get/index.mjs.map +1 -0
- package/dist/function/peek/index.d.mts +46 -0
- package/dist/function/peek/index.d.ts +46 -0
- package/dist/function/peek/index.js +39 -0
- package/dist/function/peek/index.js.map +1 -0
- package/dist/function/peek/index.mjs +15 -0
- package/dist/function/peek/index.mjs.map +1 -0
- package/dist/function/useMayObservableOptions/index.d.mts +59 -0
- package/dist/function/useMayObservableOptions/index.d.ts +59 -0
- package/dist/function/useMayObservableOptions/index.js +109 -0
- package/dist/function/useMayObservableOptions/index.js.map +1 -0
- package/dist/function/useMayObservableOptions/index.mjs +88 -0
- package/dist/function/useMayObservableOptions/index.mjs.map +1 -0
- package/dist/function/useSupported/index.d.mts +6 -0
- package/dist/function/useSupported/index.d.ts +6 -0
- package/dist/function/useSupported/index.js +37 -0
- package/dist/function/useSupported/index.js.map +1 -0
- package/dist/function/useSupported/index.mjs +13 -0
- package/dist/function/useSupported/index.mjs.map +1 -0
- package/dist/function/useWhenMounted/index.d.mts +6 -0
- package/dist/function/useWhenMounted/index.d.ts +6 -0
- package/dist/function/useWhenMounted/index.js +37 -0
- package/dist/function/useWhenMounted/index.js.map +1 -0
- package/dist/function/useWhenMounted/index.mjs +13 -0
- package/dist/function/useWhenMounted/index.mjs.map +1 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +22 -0
- package/dist/index.mjs.map +1 -0
- package/dist/sensors/useScroll/demo.d.mts +5 -0
- package/dist/sensors/useScroll/demo.d.ts +5 -0
- package/dist/sensors/useScroll/demo.js +121 -0
- package/dist/sensors/useScroll/demo.js.map +1 -0
- package/dist/sensors/useScroll/demo.mjs +101 -0
- package/dist/sensors/useScroll/demo.mjs.map +1 -0
- package/dist/sensors/useScroll/index.d.mts +42 -0
- package/dist/sensors/useScroll/index.d.ts +42 -0
- package/dist/sensors/useScroll/index.js +149 -0
- package/dist/sensors/useScroll/index.js.map +1 -0
- package/dist/sensors/useScroll/index.mjs +125 -0
- package/dist/sensors/useScroll/index.mjs.map +1 -0
- package/dist/sensors/useWindowScroll/demo.d.mts +5 -0
- package/dist/sensors/useWindowScroll/demo.d.ts +5 -0
- package/dist/sensors/useWindowScroll/demo.js +84 -0
- package/dist/sensors/useWindowScroll/demo.js.map +1 -0
- package/dist/sensors/useWindowScroll/demo.mjs +64 -0
- package/dist/sensors/useWindowScroll/demo.mjs.map +1 -0
- package/dist/sensors/useWindowScroll/index.d.mts +9 -0
- package/dist/sensors/useWindowScroll/index.d.ts +9 -0
- package/dist/sensors/useWindowScroll/index.js +36 -0
- package/dist/sensors/useWindowScroll/index.js.map +1 -0
- package/dist/sensors/useWindowScroll/index.mjs +12 -0
- package/dist/sensors/useWindowScroll/index.mjs.map +1 -0
- package/dist/shared/configurable.d.mts +21 -0
- package/dist/shared/configurable.d.ts +21 -0
- package/dist/shared/configurable.js +39 -0
- package/dist/shared/configurable.js.map +1 -0
- package/dist/shared/configurable.mjs +12 -0
- package/dist/shared/configurable.mjs.map +1 -0
- package/dist/shared/index.d.mts +4 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.js +31 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/index.mjs +7 -0
- package/dist/shared/index.mjs.map +1 -0
- package/dist/shared/normalizeTargets/index.d.mts +21 -0
- package/dist/shared/normalizeTargets/index.d.ts +21 -0
- package/dist/shared/normalizeTargets/index.js +36 -0
- package/dist/shared/normalizeTargets/index.js.map +1 -0
- package/dist/shared/normalizeTargets/index.mjs +12 -0
- package/dist/shared/normalizeTargets/index.mjs.map +1 -0
- package/dist/shared/utils.d.mts +15 -0
- package/dist/shared/utils.d.ts +15 -0
- package/dist/shared/utils.js +87 -0
- package/dist/shared/utils.js.map +1 -0
- package/dist/shared/utils.mjs +52 -0
- package/dist/shared/utils.mjs.map +1 -0
- package/dist/types.d.mts +52 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/types.mjs +1 -0
- package/dist/types.mjs.map +1 -0
- package/package.json +54 -0
- package/src/browser/useEventListener/index.md +109 -0
- package/src/browser/useEventListener/index.spec.ts +611 -0
- package/src/browser/useEventListener/index.ts +242 -0
- package/src/browser/useMediaQuery/demo.tsx +58 -0
- package/src/browser/useMediaQuery/index.md +40 -0
- package/src/browser/useMediaQuery/index.spec.ts +267 -0
- package/src/browser/useMediaQuery/index.ts +96 -0
- package/src/components/Auto/index.tsx +65 -0
- package/src/elements/useDocumentVisibility/demo.tsx +111 -0
- package/src/elements/useDocumentVisibility/index.md +51 -0
- package/src/elements/useDocumentVisibility/index.spec.ts +114 -0
- package/src/elements/useDocumentVisibility/index.ts +26 -0
- package/src/elements/useElementBounding/demo.tsx +63 -0
- package/src/elements/useElementBounding/index.md +59 -0
- package/src/elements/useElementBounding/index.ts +159 -0
- package/src/elements/useElementSize/demo.tsx +48 -0
- package/src/elements/useElementSize/index.md +60 -0
- package/src/elements/useElementSize/index.spec.ts +295 -0
- package/src/elements/useElementSize/index.ts +100 -0
- package/src/elements/useElementVisibility/deep-observable-pattern.spec.ts +453 -0
- package/src/elements/useElementVisibility/demo.tsx +97 -0
- package/src/elements/useElementVisibility/index.md +98 -0
- package/src/elements/useElementVisibility/index.spec.ts +227 -0
- package/src/elements/useElementVisibility/index.ts +78 -0
- package/src/elements/useIntersectionObserver/demo.tsx +180 -0
- package/src/elements/useIntersectionObserver/index.md +99 -0
- package/src/elements/useIntersectionObserver/index.spec.ts +482 -0
- package/src/elements/useIntersectionObserver/index.ts +149 -0
- package/src/elements/useMouseInElement/demo.tsx +79 -0
- package/src/elements/useMouseInElement/index.md +71 -0
- package/src/elements/useMouseInElement/index.spec.ts +398 -0
- package/src/elements/useMouseInElement/index.ts +209 -0
- package/src/elements/useMutationObserver/demo.tsx +270 -0
- package/src/elements/useMutationObserver/index.md +99 -0
- package/src/elements/useMutationObserver/index.spec.ts +421 -0
- package/src/elements/useMutationObserver/index.ts +66 -0
- package/src/elements/useParentElement/demo.tsx +120 -0
- package/src/elements/useParentElement/index.md +67 -0
- package/src/elements/useParentElement/index.spec.ts +208 -0
- package/src/elements/useParentElement/index.ts +35 -0
- package/src/elements/useRef$/index.md +62 -0
- package/src/elements/useRef$/index.spec.ts +205 -0
- package/src/elements/useRef$/index.ts +137 -0
- package/src/elements/useRef$/useImperativeHandle.spec.ts +339 -0
- package/src/elements/useResizeObserver/demo.tsx +56 -0
- package/src/elements/useResizeObserver/index.md +51 -0
- package/src/elements/useResizeObserver/index.spec.ts +312 -0
- package/src/elements/useResizeObserver/index.ts +106 -0
- package/src/elements/useWindowFocus/demo.tsx +71 -0
- package/src/elements/useWindowFocus/index.md +35 -0
- package/src/elements/useWindowFocus/index.spec.ts +103 -0
- package/src/elements/useWindowFocus/index.ts +21 -0
- package/src/elements/useWindowSize/demo.tsx +46 -0
- package/src/elements/useWindowSize/index.md +50 -0
- package/src/elements/useWindowSize/index.spec.ts +310 -0
- package/src/elements/useWindowSize/index.ts +107 -0
- package/src/function/get/index.md +25 -0
- package/src/function/get/index.spec.ts +87 -0
- package/src/function/get/index.ts +70 -0
- package/src/function/peek/index.spec.ts +97 -0
- package/src/function/peek/index.ts +69 -0
- package/src/function/useMayObservableOptions/index.spec.ts +521 -0
- package/src/function/useMayObservableOptions/index.ts +173 -0
- package/src/function/useSupported/index.md +38 -0
- package/src/function/useSupported/index.spec.ts +116 -0
- package/src/function/useSupported/index.ts +14 -0
- package/src/function/useWhenMounted/index.md +25 -0
- package/src/function/useWhenMounted/index.spec.ts +120 -0
- package/src/function/useWhenMounted/index.ts +16 -0
- package/src/index.ts +25 -0
- package/src/sensors/useScroll/demo.tsx +98 -0
- package/src/sensors/useScroll/index.md +112 -0
- package/src/sensors/useScroll/index.spec.ts +678 -0
- package/src/sensors/useScroll/index.ts +201 -0
- package/src/sensors/useWindowScroll/demo.tsx +69 -0
- package/src/sensors/useWindowScroll/index.md +88 -0
- package/src/sensors/useWindowScroll/index.spec.ts +69 -0
- package/src/sensors/useWindowScroll/index.ts +11 -0
- package/src/shared/configurable.ts +35 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/normalizeTargets/index.spec.ts +76 -0
- package/src/shared/normalizeTargets/index.ts +27 -0
- package/src/shared/utils.ts +67 -0
- package/src/types.ts +56 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, renderHook } from "@testing-library/react";
|
|
3
|
+
import { observable, ObservableHint } from "@legendapp/state";
|
|
4
|
+
import type { OpaqueObject } from "@legendapp/state";
|
|
5
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
6
|
+
import { useRef$ } from "../useRef$";
|
|
7
|
+
import { useParentElement } from ".";
|
|
8
|
+
|
|
9
|
+
const wrapEl = (el: Element) => observable<OpaqueObject<Element> | null>(ObservableHint.opaque(el));
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// DOM helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const attached: Element[] = [];
|
|
16
|
+
|
|
17
|
+
/** Attach a child inside a parent div to the document and track for cleanup. */
|
|
18
|
+
function attachToBody<T extends Element>(child: T): { child: T; parent: HTMLDivElement } {
|
|
19
|
+
const parent = document.createElement("div");
|
|
20
|
+
parent.appendChild(child);
|
|
21
|
+
document.body.appendChild(parent);
|
|
22
|
+
attached.push(parent);
|
|
23
|
+
return { child, parent };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
attached.forEach((el) => el.parentNode?.removeChild(el));
|
|
28
|
+
attached.length = 0;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// useParentElement()
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
describe("useParentElement()", () => {
|
|
36
|
+
it("returns parentElement of a plain element attached to DOM", () => {
|
|
37
|
+
const child = document.createElement("span");
|
|
38
|
+
const { parent } = attachToBody(child);
|
|
39
|
+
|
|
40
|
+
const { result } = renderHook(() => useParentElement(wrapEl(child)));
|
|
41
|
+
|
|
42
|
+
expect(result.current.get()).toBe(parent);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns null when element is not attached to DOM", () => {
|
|
46
|
+
const el = document.createElement("div");
|
|
47
|
+
// not appended anywhere
|
|
48
|
+
|
|
49
|
+
const { result } = renderHook(() => useParentElement(wrapEl(el)));
|
|
50
|
+
|
|
51
|
+
expect(result.current.get()).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns null when element param is undefined", () => {
|
|
55
|
+
const { result } = renderHook(() => useParentElement());
|
|
56
|
+
|
|
57
|
+
expect(result.current.get()).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("reacts to Ref$ — returns parent after element is assigned", () => {
|
|
61
|
+
const child = document.createElement("span");
|
|
62
|
+
const { parent } = attachToBody(child);
|
|
63
|
+
|
|
64
|
+
const { result } = renderHook(() => {
|
|
65
|
+
const el$ = useRef$<HTMLSpanElement>();
|
|
66
|
+
return { el$, parent$: useParentElement(el$) };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Before el$ is assigned — no element to resolve
|
|
70
|
+
expect(result.current.parent$.get()).toBeNull();
|
|
71
|
+
|
|
72
|
+
act(() => result.current.el$(child));
|
|
73
|
+
|
|
74
|
+
expect(result.current.parent$.get()).toBe(parent);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("reacts to Observable<Element|null> — updates when value is set", () => {
|
|
78
|
+
const child = document.createElement("p");
|
|
79
|
+
const { parent } = attachToBody(child);
|
|
80
|
+
|
|
81
|
+
const target$ = observable<HTMLElement | null>(null);
|
|
82
|
+
|
|
83
|
+
const { result } = renderHook(() => useParentElement(target$ as any));
|
|
84
|
+
|
|
85
|
+
expect(result.current.get()).toBeNull();
|
|
86
|
+
|
|
87
|
+
act(() => {
|
|
88
|
+
target$.set(child);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.current.get()).toBe(parent);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("updates when Ref$ is reassigned to a different element", () => {
|
|
95
|
+
const childA = document.createElement("span");
|
|
96
|
+
const { parent: parentA } = attachToBody(childA);
|
|
97
|
+
|
|
98
|
+
const childB = document.createElement("em");
|
|
99
|
+
const { parent: parentB } = attachToBody(childB);
|
|
100
|
+
|
|
101
|
+
const { result } = renderHook(() => {
|
|
102
|
+
const el$ = useRef$<HTMLElement>();
|
|
103
|
+
return { el$, parent$: useParentElement(el$) };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
act(() => result.current.el$(childA));
|
|
107
|
+
expect(result.current.parent$.get()).toBe(parentA);
|
|
108
|
+
|
|
109
|
+
act(() => result.current.el$(childB));
|
|
110
|
+
expect(result.current.parent$.get()).toBe(parentB);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns null when Observable is set to null", () => {
|
|
114
|
+
const child = document.createElement("span");
|
|
115
|
+
attachToBody(child);
|
|
116
|
+
|
|
117
|
+
const target$ = observable<HTMLElement | null>(child);
|
|
118
|
+
|
|
119
|
+
const { result } = renderHook(() => useParentElement(target$ as any));
|
|
120
|
+
|
|
121
|
+
act(() => {
|
|
122
|
+
target$.set(null);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result.current.get()).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("observable value persists after unmount", () => {
|
|
129
|
+
const child = document.createElement("span");
|
|
130
|
+
attachToBody(child);
|
|
131
|
+
|
|
132
|
+
const { result, unmount } = renderHook(() => useParentElement(wrapEl(child)));
|
|
133
|
+
|
|
134
|
+
expect(result.current.get()).not.toBeNull();
|
|
135
|
+
unmount();
|
|
136
|
+
// Observable value remains readable after unmount
|
|
137
|
+
expect(result.current.get()).not.toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("returns null when Document is passed", () => {
|
|
141
|
+
const { result } = renderHook(() => useParentElement(document as any));
|
|
142
|
+
expect(result.current.get()).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("handles null → value → null cycle for Observable", () => {
|
|
146
|
+
const child = document.createElement("span");
|
|
147
|
+
const { parent } = attachToBody(child);
|
|
148
|
+
|
|
149
|
+
const target$ = observable<HTMLElement | null>(null);
|
|
150
|
+
const { result } = renderHook(() => useParentElement(target$ as any));
|
|
151
|
+
|
|
152
|
+
expect(result.current.get()).toBeNull();
|
|
153
|
+
|
|
154
|
+
act(() => { target$.set(child); });
|
|
155
|
+
expect(result.current.get()).toBe(parent);
|
|
156
|
+
|
|
157
|
+
act(() => { target$.set(null); });
|
|
158
|
+
expect(result.current.get()).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("reads parent of Ref$ that already holds an element at mount time", () => {
|
|
162
|
+
const child = document.createElement("span");
|
|
163
|
+
const { parent } = attachToBody(child);
|
|
164
|
+
|
|
165
|
+
// Phase 1: create el$ and assign the element
|
|
166
|
+
let sharedRef$: ReturnType<typeof useRef$<HTMLSpanElement>>;
|
|
167
|
+
const { result: elResult } = renderHook(() => {
|
|
168
|
+
const el$ = useRef$<HTMLSpanElement>();
|
|
169
|
+
sharedRef$ = el$;
|
|
170
|
+
return el$;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
act(() => { elResult.current(child); });
|
|
174
|
+
|
|
175
|
+
// Phase 2: pass the pre-assigned el$ to useParentElement
|
|
176
|
+
// useMount should pick up the existing element value immediately
|
|
177
|
+
const { result } = renderHook(() => useParentElement(sharedRef$!));
|
|
178
|
+
|
|
179
|
+
expect(result.current.get()).toBe(parent);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("does NOT update when element's Observable value is unchanged (only DOM moved)", () => {
|
|
183
|
+
const child = document.createElement("span");
|
|
184
|
+
const { parent: parentA } = attachToBody(child);
|
|
185
|
+
|
|
186
|
+
const { result } = renderHook(() => useParentElement(wrapEl(child)));
|
|
187
|
+
expect(result.current.get()).toBe(parentA);
|
|
188
|
+
|
|
189
|
+
const parentB = document.createElement("div");
|
|
190
|
+
document.body.appendChild(parentB);
|
|
191
|
+
attached.push(parentB);
|
|
192
|
+
parentB.appendChild(child);
|
|
193
|
+
|
|
194
|
+
// plain element는 Observable이 아니므로 갱신되지 않음
|
|
195
|
+
expect(result.current.get()).toBe(parentA);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("returns parent of an SVGElement", () => {
|
|
199
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
200
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
201
|
+
svg.appendChild(circle);
|
|
202
|
+
document.body.appendChild(svg);
|
|
203
|
+
attached.push(svg);
|
|
204
|
+
|
|
205
|
+
const { result } = renderHook(() => useParentElement(circle as any));
|
|
206
|
+
expect(result.current.get()).toBe(svg);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Observable, OpaqueObject } from "@legendapp/state";
|
|
2
|
+
import { ObservableHint } from "@legendapp/state";
|
|
3
|
+
import { useMount, useObservable, useObserve } from "@legendapp/state/react";
|
|
4
|
+
import { getElement } from "../useRef$";
|
|
5
|
+
import type { MaybeElement } from "../useRef$";
|
|
6
|
+
|
|
7
|
+
export function useParentElement(
|
|
8
|
+
element?: MaybeElement,
|
|
9
|
+
): Observable<OpaqueObject<HTMLElement | SVGElement> | null> {
|
|
10
|
+
const parent$ = useObservable<OpaqueObject<HTMLElement | SVGElement> | null>(
|
|
11
|
+
null,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* NOTE: plain element (non-Observable, non-Ref$)을 전달한 경우,
|
|
16
|
+
* 해당 요소가 DOM 내에서 다른 부모로 이동하더라도 자동으로 갱신되지 않습니다.
|
|
17
|
+
* 동적 감지가 필요하면 Ref$ 또는 Observable<Element>를 사용하세요.
|
|
18
|
+
*/
|
|
19
|
+
const update = () => {
|
|
20
|
+
if (!element) return;
|
|
21
|
+
const el = getElement(element as MaybeElement);
|
|
22
|
+
// Document / Window 는 parentElement 프로퍼티가 없으므로 null → SSR-safe
|
|
23
|
+
const parent = (el as HTMLElement | null)?.parentElement ?? null;
|
|
24
|
+
parent$.set(
|
|
25
|
+
parent
|
|
26
|
+
? ObservableHint.opaque(parent as unknown as HTMLElement | SVGElement)
|
|
27
|
+
: null,
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
useMount(update);
|
|
32
|
+
useObserve(update, { immediate: false });
|
|
33
|
+
|
|
34
|
+
return parent$;
|
|
35
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useRef$
|
|
3
|
+
category: elements
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
An observable element ref hook that serves as a drop-in replacement for `useRef`. Works with callback ref composition and `forwardRef` patterns. The element is wrapped with `opaqueObject` to prevent legendapp/state from deeply observing DOM properties.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
### Standalone (useRef replacement)
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { useRef$ } from '@usels/core'
|
|
14
|
+
|
|
15
|
+
function Component() {
|
|
16
|
+
const el$ = useRef$<HTMLDivElement>()
|
|
17
|
+
|
|
18
|
+
return <div ref={el$} />
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Reactive access with useObserve
|
|
23
|
+
|
|
24
|
+
Calling `el$.get()` inside `useObserve` automatically re-runs the observer when the element is mounted or unmounted.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useObserve } from '@legendapp/state/react'
|
|
28
|
+
import { useRef$ } from '@usels/core'
|
|
29
|
+
|
|
30
|
+
function Component() {
|
|
31
|
+
const el$ = useRef$<HTMLDivElement>()
|
|
32
|
+
|
|
33
|
+
useObserve(() => {
|
|
34
|
+
const el = el$.get()
|
|
35
|
+
if (el) {
|
|
36
|
+
el.focus()
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return <div ref={el$} />
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### forwardRef pattern
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { forwardRef } from 'react'
|
|
48
|
+
import { useRef$ } from '@usels/core'
|
|
49
|
+
|
|
50
|
+
const Component = forwardRef<HTMLDivElement>((props, ref) => {
|
|
51
|
+
const el$ = useRef$(ref)
|
|
52
|
+
|
|
53
|
+
useObserve(() => {
|
|
54
|
+
const el = el$.get()
|
|
55
|
+
if (el) {
|
|
56
|
+
console.log('element mounted:', el)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return <div ref={el$} />
|
|
61
|
+
})
|
|
62
|
+
```
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { render, renderHook, act } from "@testing-library/react";
|
|
3
|
+
import { useObserve } from "@legendapp/state/react";
|
|
4
|
+
import { createElement, createRef, forwardRef, useRef } from "react";
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { useRef$ } from ".";
|
|
7
|
+
|
|
8
|
+
const noop = () => {};
|
|
9
|
+
|
|
10
|
+
describe("useRef$()", () => {
|
|
11
|
+
it("initial value is null", () => {
|
|
12
|
+
const { result } = renderHook(() => useRef$<HTMLDivElement>(noop));
|
|
13
|
+
expect(result.current.get()).toBe(null);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("registers element in observable when called with an element", () => {
|
|
17
|
+
const { result } = renderHook(() => useRef$<HTMLDivElement>(noop));
|
|
18
|
+
const div = document.createElement("div");
|
|
19
|
+
|
|
20
|
+
act(() => {
|
|
21
|
+
result.current(div);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(result.current.get()).toBe(div);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("resets observable to null when called with null", () => {
|
|
28
|
+
const { result } = renderHook(() => useRef$<HTMLDivElement>(noop));
|
|
29
|
+
const div = document.createElement("div");
|
|
30
|
+
|
|
31
|
+
act(() => result.current(div));
|
|
32
|
+
act(() => result.current(null));
|
|
33
|
+
|
|
34
|
+
expect(result.current.get()).toBe(null);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("el$ maintains stable reference across re-renders", () => {
|
|
38
|
+
const { result, rerender } = renderHook(() => useRef$<HTMLDivElement>(noop));
|
|
39
|
+
const el$1 = result.current;
|
|
40
|
+
|
|
41
|
+
rerender();
|
|
42
|
+
|
|
43
|
+
expect(result.current).toBe(el$1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("el$ is callable and exposes get/peek as functions", () => {
|
|
47
|
+
const { result } = renderHook(() => useRef$(noop));
|
|
48
|
+
expect(typeof result.current).toBe("function");
|
|
49
|
+
expect(typeof result.current.get).toBe("function");
|
|
50
|
+
expect(typeof result.current.peek).toBe("function");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("calls externalRef first then updates observable", () => {
|
|
54
|
+
const callOrder: string[] = [];
|
|
55
|
+
const externalRef = vi.fn((_node: HTMLDivElement | null) => {
|
|
56
|
+
callOrder.push("externalRef");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const { result } = renderHook(() => useRef$<HTMLDivElement>(externalRef));
|
|
60
|
+
const div = document.createElement("div");
|
|
61
|
+
|
|
62
|
+
act(() => {
|
|
63
|
+
result.current(div);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(externalRef).toHaveBeenCalledWith(div);
|
|
67
|
+
expect(result.current.get()).toBe(div);
|
|
68
|
+
expect(callOrder[0]).toBe("externalRef"); // external runs first
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("uses latest externalRef after re-render", () => {
|
|
72
|
+
let currentRef = vi.fn();
|
|
73
|
+
|
|
74
|
+
const { result, rerender } = renderHook(
|
|
75
|
+
({ ref }) => useRef$<HTMLDivElement>(ref),
|
|
76
|
+
{ initialProps: { ref: currentRef } }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const newRef = vi.fn();
|
|
80
|
+
rerender({ ref: newRef });
|
|
81
|
+
|
|
82
|
+
const div = document.createElement("div");
|
|
83
|
+
act(() => {
|
|
84
|
+
result.current(div);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(currentRef).not.toHaveBeenCalled();
|
|
88
|
+
expect(newRef).toHaveBeenCalledWith(div);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("works without any argument (standalone useRef replacement)", () => {
|
|
92
|
+
const { result } = renderHook(() => useRef$<HTMLDivElement>());
|
|
93
|
+
const div = document.createElement("div");
|
|
94
|
+
|
|
95
|
+
act(() => result.current(div));
|
|
96
|
+
|
|
97
|
+
expect(result.current.get()).toBe(div);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("syncs RefObject.current when RefObject is provided", () => {
|
|
101
|
+
const refObject = createRef<HTMLDivElement>();
|
|
102
|
+
|
|
103
|
+
const { result } = renderHook(() => useRef$<HTMLDivElement>(refObject));
|
|
104
|
+
const div = document.createElement("div");
|
|
105
|
+
|
|
106
|
+
act(() => result.current(div));
|
|
107
|
+
|
|
108
|
+
expect(refObject.current).toBe(div);
|
|
109
|
+
expect(result.current.get()).toBe(div);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("clears RefObject.current to null on unmount", () => {
|
|
113
|
+
const refObject = createRef<HTMLDivElement>();
|
|
114
|
+
|
|
115
|
+
const { result } = renderHook(() => useRef$<HTMLDivElement>(refObject));
|
|
116
|
+
const div = document.createElement("div");
|
|
117
|
+
|
|
118
|
+
act(() => result.current(div));
|
|
119
|
+
act(() => result.current(null));
|
|
120
|
+
|
|
121
|
+
expect(refObject.current).toBe(null);
|
|
122
|
+
expect(result.current.get()).toBe(null);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("handles null externalRef gracefully (forwardRef passing null)", () => {
|
|
126
|
+
const { result } = renderHook(() => useRef$<HTMLDivElement>(null));
|
|
127
|
+
const div = document.createElement("div");
|
|
128
|
+
|
|
129
|
+
act(() => result.current(div));
|
|
130
|
+
|
|
131
|
+
expect(result.current.get()).toBe(div);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("updates latest RefObject when externalRef changes between renders", () => {
|
|
135
|
+
const { result, rerender } = renderHook(
|
|
136
|
+
({ ref }) => useRef$<HTMLDivElement>(ref),
|
|
137
|
+
{ initialProps: { ref: createRef<HTMLDivElement>() } }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const newRef = createRef<HTMLDivElement>();
|
|
141
|
+
rerender({ ref: newRef });
|
|
142
|
+
|
|
143
|
+
const div = document.createElement("div");
|
|
144
|
+
act(() => result.current(div));
|
|
145
|
+
|
|
146
|
+
expect(newRef.current).toBe(div);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("can be used with useRef inside forwardRef pattern", () => {
|
|
150
|
+
const { result } = renderHook(() => {
|
|
151
|
+
const localRef = useRef<HTMLDivElement>(null);
|
|
152
|
+
return { el$: useRef$<HTMLDivElement>(localRef), localRef };
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const div = document.createElement("div");
|
|
156
|
+
act(() => result.current.el$(div));
|
|
157
|
+
|
|
158
|
+
expect(result.current.localRef.current).toBe(div);
|
|
159
|
+
expect(result.current.el$.get()).toBe(div);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("reactivity works inside forwardRef component", () => {
|
|
163
|
+
const observeSpy = vi.fn();
|
|
164
|
+
|
|
165
|
+
const Component = forwardRef<HTMLDivElement, object>((_, ref) => {
|
|
166
|
+
const el$ = useRef$(ref);
|
|
167
|
+
useObserve(() => {
|
|
168
|
+
el$.get();
|
|
169
|
+
observeSpy();
|
|
170
|
+
});
|
|
171
|
+
return createElement("div", { ref: el$ });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const parentRef = createRef<HTMLDivElement>();
|
|
175
|
+
render(createElement(Component, { ref: parentRef }));
|
|
176
|
+
|
|
177
|
+
// 1st: initial useObserve (el = null), 2nd: element assigned
|
|
178
|
+
expect(observeSpy).toHaveBeenCalledTimes(2);
|
|
179
|
+
expect(parentRef.current).not.toBe(null);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("triggers useObserve when element is assigned", () => {
|
|
183
|
+
const observeSpy = vi.fn();
|
|
184
|
+
|
|
185
|
+
const { result } = renderHook(() => {
|
|
186
|
+
const el$ = useRef$<HTMLDivElement>(noop);
|
|
187
|
+
useObserve(() => {
|
|
188
|
+
el$.get(); // register as selector
|
|
189
|
+
observeSpy();
|
|
190
|
+
});
|
|
191
|
+
return el$;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// called once on mount
|
|
195
|
+
expect(observeSpy).toHaveBeenCalledTimes(1);
|
|
196
|
+
|
|
197
|
+
const div = document.createElement("div");
|
|
198
|
+
act(() => {
|
|
199
|
+
result.current(div);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// called again when element changes
|
|
203
|
+
expect(observeSpy).toHaveBeenCalledTimes(2);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { isObservable, ObservableHint } from "@legendapp/state";
|
|
2
|
+
import type { Observable, OpaqueObject } from "@legendapp/state";
|
|
3
|
+
import { useObservable } from "@legendapp/state/react";
|
|
4
|
+
import { type Ref, type RefObject, useMemo, useRef } from "react";
|
|
5
|
+
import { isWindow } from "../../shared";
|
|
6
|
+
|
|
7
|
+
export type Ref$<T> = ((node: T | null) => void) & {
|
|
8
|
+
/** returns element, registers tracking when called inside useObserve */
|
|
9
|
+
get(): OpaqueObject<T> | null;
|
|
10
|
+
/** returns element without registering tracking */
|
|
11
|
+
peek(): OpaqueObject<T> | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A value that resolves to an Element, Document, Window, or null.
|
|
16
|
+
*
|
|
17
|
+
* - `Ref$<T>` — React-managed element ref (created via `useRef$()`). Primary choice.
|
|
18
|
+
* - `Document` / `Window` — stable global singletons, always safe to pass raw.
|
|
19
|
+
* - `Observable<OpaqueObject<Element> | null>` — for imperatively obtained elements.
|
|
20
|
+
* Use `ObservableHint.opaque(el)` when storing: `observable(ObservableHint.opaque(el))`.
|
|
21
|
+
*
|
|
22
|
+
* Raw `HTMLElement` is intentionally excluded: in React's render model, elements
|
|
23
|
+
* don't exist at hook call time, making raw element references inherently stale.
|
|
24
|
+
*/
|
|
25
|
+
export type MaybeElement =
|
|
26
|
+
| Ref$<any>
|
|
27
|
+
| Document
|
|
28
|
+
| Window
|
|
29
|
+
| null
|
|
30
|
+
| Observable<OpaqueObject<Element> | null>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates an observable element ref. Can be used as a drop-in replacement for
|
|
34
|
+
* `useRef`, composed with callback refs, or used with `forwardRef`.
|
|
35
|
+
*
|
|
36
|
+
* The element is wrapped with `opaqueObject` to prevent legendapp/state
|
|
37
|
+
* from making DOM properties reactive (deep observation).
|
|
38
|
+
*
|
|
39
|
+
* @param externalRef - Optional. Accepts callback ref, RefObject, or null (forwardRef compatible).
|
|
40
|
+
* @returns A callable ref that is also observable via `get`/`peek`
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* // standalone — useRef replacement
|
|
45
|
+
* const el$ = useRef$<HTMLDivElement>();
|
|
46
|
+
* return <div ref={el$} />;
|
|
47
|
+
*
|
|
48
|
+
* // forwardRef compatible
|
|
49
|
+
* const Component = forwardRef<HTMLDivElement>((props, ref) => {
|
|
50
|
+
* const el$ = useRef$(ref);
|
|
51
|
+
* return <div ref={el$} />;
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // callback ref composition
|
|
55
|
+
* const myRef = useCallback((node: HTMLDivElement | null) => {
|
|
56
|
+
* node?.focus();
|
|
57
|
+
* }, []);
|
|
58
|
+
* const el$ = useRef$(myRef);
|
|
59
|
+
* return <div ref={el$} />;
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function useRef$<T extends Element = Element>(
|
|
63
|
+
externalRef?: Ref<T> | null,
|
|
64
|
+
): Ref$<T> {
|
|
65
|
+
const el$ = useObservable<OpaqueObject<T> | null>(null);
|
|
66
|
+
|
|
67
|
+
// store externalRef — simple assignment each render, no new closure
|
|
68
|
+
const extRef = useRef(externalRef);
|
|
69
|
+
extRef.current = externalRef;
|
|
70
|
+
|
|
71
|
+
return useMemo(
|
|
72
|
+
() =>
|
|
73
|
+
Object.assign(
|
|
74
|
+
(node: T | null) => {
|
|
75
|
+
const ext = extRef.current;
|
|
76
|
+
if (typeof ext === "function") {
|
|
77
|
+
ext(node);
|
|
78
|
+
} else if (ext != null && "current" in ext) {
|
|
79
|
+
(ext as RefObject<T | null>).current = node;
|
|
80
|
+
}
|
|
81
|
+
(el$ as any).set(node ? ObservableHint.opaque(node) : null);
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
get: () => el$.get(),
|
|
85
|
+
peek: () => el$.peek(),
|
|
86
|
+
},
|
|
87
|
+
) as Ref$<T>,
|
|
88
|
+
[], // eslint-disable-line react-hooks/exhaustive-deps
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Type guard for Ref$ — distinguishes it from Observable and raw values */
|
|
93
|
+
export function isRef$(v: unknown): v is Ref$<Element> {
|
|
94
|
+
return (
|
|
95
|
+
typeof v === "function" && !isObservable(v) && "get" in v && "peek" in v
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Unwraps MaybeElement with tracking (use inside useObserve) */
|
|
100
|
+
export function getElement(
|
|
101
|
+
v: MaybeElement,
|
|
102
|
+
): HTMLElement | Document | Window | null {
|
|
103
|
+
if (isRef$(v)) {
|
|
104
|
+
const raw = v.get();
|
|
105
|
+
return raw
|
|
106
|
+
? ((raw as OpaqueObject<Element>).valueOf() as HTMLElement)
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
if (isWindow(v)) return v;
|
|
110
|
+
if (isObservable(v)) {
|
|
111
|
+
const val = (v as Observable<OpaqueObject<Element> | null>).get();
|
|
112
|
+
return val
|
|
113
|
+
? ((val as OpaqueObject<Element>).valueOf() as HTMLElement)
|
|
114
|
+
: null;
|
|
115
|
+
}
|
|
116
|
+
return v as Document | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Unwraps MaybeElement without tracking (use inside setup/peek) */
|
|
120
|
+
export function peekElement(
|
|
121
|
+
v: MaybeElement,
|
|
122
|
+
): HTMLElement | Document | Window | null {
|
|
123
|
+
if (isRef$(v)) {
|
|
124
|
+
const raw = v.peek();
|
|
125
|
+
return raw
|
|
126
|
+
? ((raw as OpaqueObject<Element>).valueOf() as HTMLElement)
|
|
127
|
+
: null;
|
|
128
|
+
}
|
|
129
|
+
if (isWindow(v)) return v;
|
|
130
|
+
if (isObservable(v)) {
|
|
131
|
+
const val = (v as Observable<OpaqueObject<Element> | null>).peek();
|
|
132
|
+
return val
|
|
133
|
+
? ((val as OpaqueObject<Element>).valueOf() as HTMLElement)
|
|
134
|
+
: null;
|
|
135
|
+
}
|
|
136
|
+
return v as Document | null;
|
|
137
|
+
}
|