@usels/core 0.0.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/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 +83 -0
- package/dist/browser/useMediaQuery/demo.js.map +1 -0
- package/dist/browser/useMediaQuery/demo.mjs +63 -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 +87 -0
- package/dist/elements/useElementBounding/demo.js.map +1 -0
- package/dist/elements/useElementBounding/demo.mjs +67 -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 +83 -0
- package/dist/elements/useElementSize/demo.js.map +1 -0
- package/dist/elements/useElementSize/demo.mjs +63 -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 +104 -0
- package/dist/elements/useMouseInElement/demo.js.map +1 -0
- package/dist/elements/useMouseInElement/demo.mjs +84 -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 +104 -0
- package/dist/elements/useWindowFocus/demo.js.map +1 -0
- package/dist/elements/useWindowFocus/demo.mjs +84 -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 +79 -0
- package/dist/elements/useWindowSize/demo.js.map +1 -0
- package/dist/elements/useWindowSize/demo.mjs +59 -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 +122 -0
- package/dist/sensors/useScroll/demo.js.map +1 -0
- package/dist/sensors/useScroll/demo.mjs +102 -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 +85 -0
- package/dist/sensors/useWindowScroll/demo.js.map +1 -0
- package/dist/sensors/useWindowScroll/demo.mjs +65 -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 +63 -0
- package/src/browser/useMediaQuery/index.md +43 -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 +54 -0
- package/src/elements/useDocumentVisibility/index.spec.ts +114 -0
- package/src/elements/useDocumentVisibility/index.ts +26 -0
- package/src/elements/useElementBounding/demo.tsx +68 -0
- package/src/elements/useElementBounding/index.md +64 -0
- package/src/elements/useElementBounding/index.ts +159 -0
- package/src/elements/useElementSize/demo.tsx +53 -0
- package/src/elements/useElementSize/index.md +65 -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 +88 -0
- package/src/elements/useMouseInElement/index.md +76 -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 +62 -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 +79 -0
- package/src/elements/useWindowFocus/index.md +38 -0
- package/src/elements/useWindowFocus/index.spec.ts +103 -0
- package/src/elements/useWindowFocus/index.ts +21 -0
- package/src/elements/useWindowSize/demo.tsx +51 -0
- package/src/elements/useWindowSize/index.md +55 -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 +43 -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 +103 -0
- package/src/sensors/useScroll/index.md +117 -0
- package/src/sensors/useScroll/index.spec.ts +678 -0
- package/src/sensors/useScroll/index.ts +201 -0
- package/src/sensors/useWindowScroll/demo.tsx +78 -0
- package/src/sensors/useWindowScroll/index.md +98 -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,453 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Experiment: useObservable(() => get(options)) pattern for DeepMaybeObservable
|
|
4
|
+
*
|
|
5
|
+
* Verified behaviors:
|
|
6
|
+
*
|
|
7
|
+
* Case 1 — outer Observable<T>
|
|
8
|
+
* useObservable(() => get(options$)) reactively tracks options$ changes ✓
|
|
9
|
+
* Dep registered via options$.get() inside reactive context.
|
|
10
|
+
*
|
|
11
|
+
* Case 2 — per-field { field: Observable<T[K]> }
|
|
12
|
+
* Legend-State auto-dereferences inner Observables — no double-nesting ✓
|
|
13
|
+
* computed$.field.get() returns the plain value (e.g. "0px"), NOT Observable<T>
|
|
14
|
+
* Inner Observable changes ARE reflected via field-level dep tracking (not callback re-eval) ✓
|
|
15
|
+
* The outer useObservable callback is NOT re-evaluated on inner field changes.
|
|
16
|
+
*
|
|
17
|
+
* Case 3 — useElementVisibility with Element (scrollTarget) options
|
|
18
|
+
* Per-field scrollTarget as Ref$, Observable<HTMLElement | null>(null), plain HTMLElement
|
|
19
|
+
* Note: Observable<HTMLElement> starting from non-null is NOT reliably tracked
|
|
20
|
+
* (Legend-State deeply proxies HTMLElements; use Ref$ or start from null).
|
|
21
|
+
*/
|
|
22
|
+
import { act, renderHook } from "@testing-library/react";
|
|
23
|
+
import { isObservable, observable, ObservableHint } from "@legendapp/state";
|
|
24
|
+
import type { OpaqueObject } from "@legendapp/state";
|
|
25
|
+
import { useObservable } from "@legendapp/state/react";
|
|
26
|
+
|
|
27
|
+
const wrapEl = (el: Element) => observable<OpaqueObject<Element> | null>(ObservableHint.opaque(el));
|
|
28
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
29
|
+
import { get } from "../../function/get";
|
|
30
|
+
import { useRef$ } from "../useRef$";
|
|
31
|
+
import { useElementVisibility } from ".";
|
|
32
|
+
import { useIntersectionObserver } from "../useIntersectionObserver";
|
|
33
|
+
|
|
34
|
+
// --- IntersectionObserver mock ---
|
|
35
|
+
|
|
36
|
+
const mockObserve = vi.fn();
|
|
37
|
+
const mockDisconnect = vi.fn();
|
|
38
|
+
let capturedInit: IntersectionObserverInit | undefined;
|
|
39
|
+
|
|
40
|
+
const MockIntersectionObserver = vi.fn(
|
|
41
|
+
(_cb: IntersectionObserverCallback, init?: IntersectionObserverInit) => {
|
|
42
|
+
capturedInit = init;
|
|
43
|
+
return { observe: mockObserve, disconnect: mockDisconnect };
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
|
|
49
|
+
mockObserve.mockClear();
|
|
50
|
+
mockDisconnect.mockClear();
|
|
51
|
+
MockIntersectionObserver.mockClear();
|
|
52
|
+
capturedInit = undefined;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.unstubAllGlobals();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// Case 1: outer Observable<T>
|
|
61
|
+
// =============================================================================
|
|
62
|
+
|
|
63
|
+
describe("Case 1 — outer Observable<T>: useObservable(() => get(options$))", () => {
|
|
64
|
+
it("computed$ reflects updated rootMargin when outer Observable changes", () => {
|
|
65
|
+
const options$ = observable({ rootMargin: "0px" });
|
|
66
|
+
|
|
67
|
+
const { result } = renderHook(() => {
|
|
68
|
+
// useObservable(fn) — Legend-State tracks .get() calls inside fn
|
|
69
|
+
const computed$ = useObservable(() => get(options$));
|
|
70
|
+
return { computed$ };
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.current.computed$.rootMargin.get()).toBe("0px");
|
|
74
|
+
|
|
75
|
+
act(() => {
|
|
76
|
+
options$.rootMargin.set("20px");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// options$.get() was called in reactive context → dep registered
|
|
80
|
+
// → computed$ recomputes when options$ changes
|
|
81
|
+
expect(result.current.computed$.rootMargin.get()).toBe("20px");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("computed$ reflects all field changes in outer Observable", () => {
|
|
85
|
+
const options$ = observable({ rootMargin: "0px", threshold: 0 });
|
|
86
|
+
|
|
87
|
+
const { result } = renderHook(() => {
|
|
88
|
+
const computed$ = useObservable(() => get(options$));
|
|
89
|
+
return { computed$ };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
act(() => {
|
|
93
|
+
options$.set({ rootMargin: "10px", threshold: 0.5 });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result.current.computed$.rootMargin.get()).toBe("10px");
|
|
97
|
+
expect(result.current.computed$.threshold.get()).toBe(0.5);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Case 2: per-field { field: Observable<T[K]> } — auto-dereference behavior
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
describe("Case 2 — per-field { field: obs$ }: Legend-State auto-dereferences inner Observables", () => {
|
|
106
|
+
it("computed$.rootMargin.get() returns plain string — no double-nesting", () => {
|
|
107
|
+
const rootMargin$ = observable("0px");
|
|
108
|
+
|
|
109
|
+
const { result } = renderHook(() => {
|
|
110
|
+
// get({ rootMargin: rootMargin$ }) returns the plain object as-is (not Observable)
|
|
111
|
+
// useObservable wraps it, but Legend-State auto-dereferences inner Observables
|
|
112
|
+
// → computed$.rootMargin.get() returns "0px" (string), NOT Observable<string>
|
|
113
|
+
const computed$ = useObservable(() =>
|
|
114
|
+
get<{ rootMargin: typeof rootMargin$ }>({ rootMargin: rootMargin$ }),
|
|
115
|
+
);
|
|
116
|
+
return { computed$ };
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const fieldValue = result.current.computed$.rootMargin.get();
|
|
120
|
+
// Legend-State auto-dereferences: returns plain string, not Observable
|
|
121
|
+
expect(isObservable(fieldValue)).toBe(false);
|
|
122
|
+
expect(fieldValue).toBe("0px");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("changing inner Observable does NOT re-evaluate computed$ (no dep registered)", () => {
|
|
126
|
+
const rootMargin$ = observable("0px");
|
|
127
|
+
let evalCount = 0;
|
|
128
|
+
|
|
129
|
+
renderHook(() => {
|
|
130
|
+
useObservable(() => {
|
|
131
|
+
evalCount++;
|
|
132
|
+
// get() on plain object: isObservable = false → returns as-is, no .get() called
|
|
133
|
+
// → rootMargin$ is never called .get() → dep NOT registered
|
|
134
|
+
return get<{ rootMargin: typeof rootMargin$ }>({ rootMargin: rootMargin$ });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const countBeforeChange = evalCount;
|
|
139
|
+
|
|
140
|
+
act(() => {
|
|
141
|
+
rootMargin$.set("20px");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// rootMargin$ not tracked → evalCount unchanged
|
|
145
|
+
expect(evalCount).toBe(countBeforeChange);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("inner Observable change IS reflected — Legend-State tracks inner fields directly (not via callback re-eval)", () => {
|
|
149
|
+
const rootMargin$ = observable("0px");
|
|
150
|
+
let evalCount = 0;
|
|
151
|
+
|
|
152
|
+
const { result } = renderHook(() => {
|
|
153
|
+
const computed$ = useObservable(() => {
|
|
154
|
+
evalCount++;
|
|
155
|
+
return get<{ rootMargin: typeof rootMargin$ }>({ rootMargin: rootMargin$ });
|
|
156
|
+
});
|
|
157
|
+
return { computed$ };
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(result.current.computed$.rootMargin.get()).toBe("0px");
|
|
161
|
+
const countBeforeChange = evalCount;
|
|
162
|
+
|
|
163
|
+
act(() => {
|
|
164
|
+
rootMargin$.set("20px");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Legend-State tracks inner Observable fields via field-level dep (NOT callback re-evaluation)
|
|
168
|
+
// → computed$.rootMargin updates to "20px" without re-running the outer callback
|
|
169
|
+
expect(result.current.computed$.rootMargin.get()).toBe("20px");
|
|
170
|
+
expect(evalCount).toBe(countBeforeChange); // outer callback NOT re-run
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// =============================================================================
|
|
175
|
+
// useElementVisibility — per-field vs outer Observable reactivity comparison
|
|
176
|
+
// =============================================================================
|
|
177
|
+
|
|
178
|
+
describe("useElementVisibility — reactivity comparison", () => {
|
|
179
|
+
it("per-field rootMargin$ change → IntersectionObserver recreated ✓ (current implementation works)", () => {
|
|
180
|
+
const el = document.createElement("div");
|
|
181
|
+
const rootMargin$ = observable("0px");
|
|
182
|
+
|
|
183
|
+
renderHook(() =>
|
|
184
|
+
useElementVisibility(wrapEl(el), { rootMargin: rootMargin$ }),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
|
|
188
|
+
expect(capturedInit?.rootMargin).toBe("0px");
|
|
189
|
+
|
|
190
|
+
MockIntersectionObserver.mockClear();
|
|
191
|
+
mockDisconnect.mockClear();
|
|
192
|
+
|
|
193
|
+
act(() => {
|
|
194
|
+
rootMargin$.set("20px");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// opts?.rootMargin = Observable<string> passed through to useIntersectionObserver
|
|
198
|
+
// useIntersectionObserver's useObserve calls get(rootMargin$) → dep registered
|
|
199
|
+
// → rootMargin$ change triggers setup() → IntersectionObserver recreated ✓
|
|
200
|
+
expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
|
|
201
|
+
expect(capturedInit?.rootMargin).toBe("20px");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("outer Observable options$.rootMargin child-field set → known Legend-State limitation (0×, not reactive)", () => {
|
|
205
|
+
const el = document.createElement("div");
|
|
206
|
+
const options$ = observable({ rootMargin: "0px" });
|
|
207
|
+
|
|
208
|
+
renderHook(() => useElementVisibility(wrapEl(el), options$));
|
|
209
|
+
|
|
210
|
+
expect(capturedInit?.rootMargin).toBe("0px");
|
|
211
|
+
MockIntersectionObserver.mockClear();
|
|
212
|
+
mockDisconnect.mockClear();
|
|
213
|
+
|
|
214
|
+
act(() => {
|
|
215
|
+
options$.rootMargin.set("20px");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// KNOWN LIMITATION: options$.rootMargin.set("20px") is a child-field mutation.
|
|
219
|
+
// options$.get() inside useObservable's compute fn does reference-equality tracking —
|
|
220
|
+
// the parent options$ object reference is unchanged, so the dep does NOT fire.
|
|
221
|
+
// opts_EV$ does NOT recompute → opts_EV$.rootMargin stays "0px" → 0× IO recreation.
|
|
222
|
+
//
|
|
223
|
+
// Workaround: use options$.set({ rootMargin: "20px" }) (whole-object replace)
|
|
224
|
+
// OR pass rootMargin as a per-field Observable: { rootMargin: observable("0px") }.
|
|
225
|
+
expect(MockIntersectionObserver).toHaveBeenCalledTimes(0);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// =============================================================================
|
|
230
|
+
// Case 3: useElementVisibility — scrollTarget (Element) options
|
|
231
|
+
// =============================================================================
|
|
232
|
+
|
|
233
|
+
describe("Case 3 — scrollTarget (Element) in useElementVisibility", () => {
|
|
234
|
+
it("plain HTMLElement as scrollTarget", () => {
|
|
235
|
+
const el = document.createElement("div");
|
|
236
|
+
const scrollContainer = document.createElement("div");
|
|
237
|
+
|
|
238
|
+
renderHook(() =>
|
|
239
|
+
useElementVisibility(wrapEl(el), { scrollTarget: wrapEl(scrollContainer) }),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(MockIntersectionObserver).toHaveBeenCalledWith(
|
|
243
|
+
expect.any(Function),
|
|
244
|
+
expect.objectContaining({ root: scrollContainer }),
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("outer Observable<Options> with scrollTarget — resolved at mount (snapshot)", () => {
|
|
249
|
+
const el = document.createElement("div");
|
|
250
|
+
const scrollContainer = document.createElement("div");
|
|
251
|
+
const options$ = observable({
|
|
252
|
+
scrollTarget: scrollContainer as any,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
renderHook(() => useElementVisibility(wrapEl(el), options$));
|
|
256
|
+
|
|
257
|
+
expect(MockIntersectionObserver).toHaveBeenCalledWith(
|
|
258
|
+
expect.any(Function),
|
|
259
|
+
expect.objectContaining({ root: scrollContainer }),
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("Ref$ as scrollTarget — delays IntersectionObserver until Ref$ is mounted", () => {
|
|
264
|
+
const el = document.createElement("div");
|
|
265
|
+
const scrollContainer = document.createElement("div");
|
|
266
|
+
|
|
267
|
+
const { result } = renderHook(() => {
|
|
268
|
+
const scrollTarget$ = useRef$<HTMLElement>();
|
|
269
|
+
return {
|
|
270
|
+
scrollTarget$,
|
|
271
|
+
visibility: useElementVisibility(wrapEl(el), { scrollTarget: scrollTarget$ }),
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Ref$ is null → useIntersectionObserver's null guard skips setup
|
|
276
|
+
expect(MockIntersectionObserver).not.toHaveBeenCalled();
|
|
277
|
+
|
|
278
|
+
act(() => {
|
|
279
|
+
result.current.scrollTarget$(scrollContainer);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Ref$ mounted → useIntersectionObserver's useObserve re-runs → setup() with root
|
|
283
|
+
expect(MockIntersectionObserver).toHaveBeenCalledWith(
|
|
284
|
+
expect.any(Function),
|
|
285
|
+
expect.objectContaining({ root: scrollContainer }),
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("Ref$ scrollTarget change → IntersectionObserver recreated with new root", () => {
|
|
290
|
+
const el = document.createElement("div");
|
|
291
|
+
const containerA = document.createElement("div");
|
|
292
|
+
const containerB = document.createElement("div");
|
|
293
|
+
|
|
294
|
+
const { result } = renderHook(() => {
|
|
295
|
+
const scrollTarget$ = useRef$<HTMLElement>();
|
|
296
|
+
return {
|
|
297
|
+
scrollTarget$,
|
|
298
|
+
visibility: useElementVisibility(wrapEl(el), { scrollTarget: scrollTarget$ }),
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
act(() => {
|
|
303
|
+
result.current.scrollTarget$(containerA);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(MockIntersectionObserver).toHaveBeenCalledWith(
|
|
307
|
+
expect.any(Function),
|
|
308
|
+
expect.objectContaining({ root: containerA }),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
mockDisconnect.mockClear();
|
|
312
|
+
MockIntersectionObserver.mockClear();
|
|
313
|
+
|
|
314
|
+
act(() => {
|
|
315
|
+
result.current.scrollTarget$(containerB);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(mockDisconnect).toHaveBeenCalledTimes(1);
|
|
319
|
+
expect(MockIntersectionObserver).toHaveBeenCalledWith(
|
|
320
|
+
expect.any(Function),
|
|
321
|
+
expect.objectContaining({ root: containerB }),
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("per-field scrollTarget as Observable<HTMLElement | null> — null→element tracked (reliable pattern)", () => {
|
|
326
|
+
const el = document.createElement("div");
|
|
327
|
+
const container = document.createElement("div");
|
|
328
|
+
// Start with null — @legendapp/state reliably tracks null→element transitions.
|
|
329
|
+
// Use OpaqueObject to prevent Legend-State from deeply proxying the element.
|
|
330
|
+
const scrollTarget$ = observable<OpaqueObject<Element> | null>(null);
|
|
331
|
+
|
|
332
|
+
renderHook(() =>
|
|
333
|
+
useElementVisibility(wrapEl(el), { scrollTarget: scrollTarget$ }),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
MockIntersectionObserver.mockClear();
|
|
337
|
+
mockDisconnect.mockClear();
|
|
338
|
+
|
|
339
|
+
act(() => {
|
|
340
|
+
scrollTarget$.set(ObservableHint.opaque(container));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// null→element: null root delays IO creation, no old IO to disconnect
|
|
344
|
+
// → new IO created with container as root
|
|
345
|
+
expect(mockDisconnect).not.toHaveBeenCalled();
|
|
346
|
+
expect(MockIntersectionObserver).toHaveBeenCalledWith(
|
|
347
|
+
expect.any(Function),
|
|
348
|
+
expect.objectContaining({ root: container }),
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// =============================================================================
|
|
354
|
+
// Standard Pattern — useObservable(() => get(options), [options])
|
|
355
|
+
// Anti-pattern(snapshot)의 한계를 극복하는 올바른 패턴 검증
|
|
356
|
+
// =============================================================================
|
|
357
|
+
|
|
358
|
+
describe("Standard Pattern — useObservable(() => get(options), [options])", () => {
|
|
359
|
+
it("outer Observable options$ child-field change — opts$.rootMargin dep triggers IO recreated ✓", () => {
|
|
360
|
+
const el = document.createElement("div");
|
|
361
|
+
const options$ = observable({ rootMargin: "0px" });
|
|
362
|
+
|
|
363
|
+
renderHook(() => {
|
|
364
|
+
// Standard Pattern: outer Observable<Options>를 computed opts$로 정규화
|
|
365
|
+
// get(options$) → options$.get() in reactive context → dep registered
|
|
366
|
+
// opts$.rootMargin은 Observable<string> — useIntersectionObserver의 useObserve에서
|
|
367
|
+
// get(options.rootMargin) 호출 → dep registered → rootMargin 변경 시 setup() 재실행
|
|
368
|
+
const opts$ = useObservable(() => get(options$), [options$]);
|
|
369
|
+
|
|
370
|
+
useIntersectionObserver(wrapEl(el), () => {}, {
|
|
371
|
+
rootMargin: opts$.rootMargin,
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
|
|
376
|
+
expect(capturedInit?.rootMargin).toBe("0px");
|
|
377
|
+
|
|
378
|
+
MockIntersectionObserver.mockClear();
|
|
379
|
+
mockDisconnect.mockClear();
|
|
380
|
+
|
|
381
|
+
act(() => {
|
|
382
|
+
// 전체 객체 교체 — opts$ 재계산 → opts$.rootMargin 업데이트 → IO 재생성 ✓
|
|
383
|
+
// (child-field set은 useObservable 재평가를 트리거하지 않음 — Legend-State 동작)
|
|
384
|
+
options$.set({ rootMargin: "20px" });
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// opts$.rootMargin이 Observable<string>으로 전달됨
|
|
388
|
+
// → options$ 교체 → opts$ 재계산 → opts$.rootMargin 업데이트
|
|
389
|
+
// → useObserve 재실행 → IntersectionObserver 재생성 ✓
|
|
390
|
+
expect(mockDisconnect).toHaveBeenCalledTimes(1);
|
|
391
|
+
expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
|
|
392
|
+
expect(capturedInit?.rootMargin).toBe("20px");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("outer Observable options$ 전체 교체 → IntersectionObserver recreated ✓", () => {
|
|
396
|
+
const el = document.createElement("div");
|
|
397
|
+
const options$ = observable<{ rootMargin: string; threshold?: number }>({
|
|
398
|
+
rootMargin: "0px",
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
renderHook(() => {
|
|
402
|
+
const opts$ = useObservable(() => get(options$), [options$]);
|
|
403
|
+
useIntersectionObserver(wrapEl(el), () => {}, {
|
|
404
|
+
rootMargin: opts$.rootMargin,
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(capturedInit?.rootMargin).toBe("0px");
|
|
409
|
+
MockIntersectionObserver.mockClear();
|
|
410
|
+
mockDisconnect.mockClear();
|
|
411
|
+
|
|
412
|
+
act(() => {
|
|
413
|
+
// 전체 options 객체 교체
|
|
414
|
+
options$.set({ rootMargin: "50px", threshold: 0.5 });
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(mockDisconnect).toHaveBeenCalledTimes(1);
|
|
418
|
+
expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
|
|
419
|
+
expect(capturedInit?.rootMargin).toBe("50px");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("Standard Pattern with per-field Observable — 기존 per-field 동작도 유지됨 ✓", () => {
|
|
423
|
+
const el = document.createElement("div");
|
|
424
|
+
const rootMargin$ = observable("0px");
|
|
425
|
+
|
|
426
|
+
renderHook(() => {
|
|
427
|
+
// per-field Observable을 포함한 object — Standard Pattern 적용
|
|
428
|
+
// Legend-State auto-dereferences inner Observables → opts$.rootMargin은 Observable<string>
|
|
429
|
+
const opts$ = useObservable(
|
|
430
|
+
() => get<{ rootMargin: typeof rootMargin$ }>({ rootMargin: rootMargin$ }),
|
|
431
|
+
[rootMargin$],
|
|
432
|
+
);
|
|
433
|
+
useIntersectionObserver(wrapEl(el), () => {}, {
|
|
434
|
+
rootMargin: opts$.rootMargin,
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
expect(capturedInit?.rootMargin).toBe("0px");
|
|
439
|
+
MockIntersectionObserver.mockClear();
|
|
440
|
+
mockDisconnect.mockClear();
|
|
441
|
+
|
|
442
|
+
act(() => {
|
|
443
|
+
rootMargin$.set("30px");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// rootMargin$ 변경 시 두 dep 경로가 모두 fire될 수 있음:
|
|
447
|
+
// 1) opts$.rootMargin (auto-dereferenced inner dep) → useObserve 재실행
|
|
448
|
+
// 2) useObservable deps array [rootMargin$] → 재계산
|
|
449
|
+
// 결과적으로 IO가 재생성되고 최종 rootMargin은 "30px" ✓
|
|
450
|
+
expect(MockIntersectionObserver).toHaveBeenCalled();
|
|
451
|
+
expect(capturedInit?.rootMargin).toBe("30px");
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Computed } from "@legendapp/state/react";
|
|
2
|
+
import { useRef$ } from "../useRef$";
|
|
3
|
+
import { useElementVisibility } from ".";
|
|
4
|
+
|
|
5
|
+
export default function UseElementVisibilityDemo() {
|
|
6
|
+
const el$ = useRef$<HTMLDivElement>();
|
|
7
|
+
const isVisible$ = useElementVisibility(el$, { threshold: 0.5 });
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
|
11
|
+
{/* Status bar */}
|
|
12
|
+
<div
|
|
13
|
+
style={{
|
|
14
|
+
display: "flex",
|
|
15
|
+
alignItems: "center",
|
|
16
|
+
gap: "16px",
|
|
17
|
+
fontFamily: "monospace",
|
|
18
|
+
fontSize: "14px",
|
|
19
|
+
padding: "8px 12px",
|
|
20
|
+
background: "var(--sl-color-gray-6, #f1f5f9)",
|
|
21
|
+
borderRadius: "6px",
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<Computed>
|
|
25
|
+
{() => (
|
|
26
|
+
<span>
|
|
27
|
+
isVisible:{" "}
|
|
28
|
+
<strong
|
|
29
|
+
style={{
|
|
30
|
+
color: isVisible$.get()
|
|
31
|
+
? "var(--sl-color-green, #22c55e)"
|
|
32
|
+
: "inherit",
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{String(isVisible$.get())}
|
|
36
|
+
</strong>
|
|
37
|
+
</span>
|
|
38
|
+
)}
|
|
39
|
+
</Computed>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* Scrollable container */}
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
height: "200px",
|
|
46
|
+
overflowY: "auto",
|
|
47
|
+
border: "1px solid var(--sl-color-gray-5, #cbd5e1)",
|
|
48
|
+
borderRadius: "6px",
|
|
49
|
+
position: "relative",
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
display: "flex",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
justifyContent: "center",
|
|
57
|
+
height: "200px",
|
|
58
|
+
color: "var(--sl-color-gray-3, #94a3b8)",
|
|
59
|
+
fontSize: "13px",
|
|
60
|
+
fontFamily: "monospace",
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
↓ scroll down
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<Computed>
|
|
67
|
+
{() => (
|
|
68
|
+
<div
|
|
69
|
+
ref={el$}
|
|
70
|
+
style={{
|
|
71
|
+
margin: "0 16px",
|
|
72
|
+
padding: "20px",
|
|
73
|
+
borderRadius: "6px",
|
|
74
|
+
textAlign: "center",
|
|
75
|
+
fontFamily: "monospace",
|
|
76
|
+
fontSize: "13px",
|
|
77
|
+
transition: "background 0.2s, border-color 0.2s",
|
|
78
|
+
border: `2px solid ${
|
|
79
|
+
isVisible$.get()
|
|
80
|
+
? "var(--sl-color-green, #22c55e)"
|
|
81
|
+
: "var(--sl-color-gray-4, #94a3b8)"
|
|
82
|
+
}`,
|
|
83
|
+
background: isVisible$.get()
|
|
84
|
+
? "var(--sl-color-green-low, #dcfce7)"
|
|
85
|
+
: "transparent",
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
target element
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</Computed>
|
|
92
|
+
|
|
93
|
+
<div style={{ height: "140px" }} />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useElementVisibility
|
|
3
|
+
category: elements
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Tracks whether a DOM element is visible within the viewport (or a specified scroll container).
|
|
7
|
+
Returns a reactive `Observable<boolean>` that updates automatically via the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
|
|
8
|
+
|
|
9
|
+
All option values accept either a plain value or an `Observable<T>`.
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx twoslash
|
|
16
|
+
// @noErrors
|
|
17
|
+
import { useRef$, Ref$, useElementVisibility } from '@usels/core'
|
|
18
|
+
|
|
19
|
+
function Component() {
|
|
20
|
+
const el$ = useRef$<HTMLDivElement>()
|
|
21
|
+
const isVisible$ = useElementVisibility(el$)
|
|
22
|
+
|
|
23
|
+
return <div ref={el$} />
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### With initial value
|
|
28
|
+
|
|
29
|
+
```tsx twoslash
|
|
30
|
+
// @noErrors
|
|
31
|
+
import { useRef$, Ref$, useElementVisibility } from '@usels/core'
|
|
32
|
+
declare const el$: Ref$<HTMLDivElement>
|
|
33
|
+
// ---cut---
|
|
34
|
+
const isVisible$ = useElementVisibility(el$, { initialValue: true })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Stop after first visible
|
|
38
|
+
|
|
39
|
+
Use `once: true` to automatically stop observing after the element becomes visible for the first time:
|
|
40
|
+
|
|
41
|
+
```tsx twoslash
|
|
42
|
+
// @noErrors
|
|
43
|
+
import { useRef$, Ref$, useElementVisibility } from '@usels/core'
|
|
44
|
+
declare const el$: Ref$<HTMLDivElement>
|
|
45
|
+
// ---cut---
|
|
46
|
+
const isVisible$ = useElementVisibility(el$, { once: true })
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Custom scroll container
|
|
50
|
+
|
|
51
|
+
Pass a `scrollTarget` to observe intersection within a scrollable container instead of the viewport:
|
|
52
|
+
|
|
53
|
+
```tsx twoslash
|
|
54
|
+
// @noErrors
|
|
55
|
+
import { useRef$, Ref$, useElementVisibility } from '@usels/core'
|
|
56
|
+
declare const el$: Ref$<HTMLDivElement>
|
|
57
|
+
// ---cut---
|
|
58
|
+
const container$ = useRef$<HTMLDivElement>()
|
|
59
|
+
const isVisible$ = useElementVisibility(el$, { scrollTarget: container$ })
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Threshold and rootMargin
|
|
63
|
+
|
|
64
|
+
```tsx twoslash
|
|
65
|
+
// @noErrors
|
|
66
|
+
import { useRef$, Ref$, useElementVisibility } from '@usels/core'
|
|
67
|
+
declare const el$: Ref$<HTMLDivElement>
|
|
68
|
+
// ---cut---
|
|
69
|
+
const isVisible$ = useElementVisibility(el$, {
|
|
70
|
+
threshold: 0.5,
|
|
71
|
+
rootMargin: '0px 0px -100px 0px',
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Reactive options
|
|
76
|
+
|
|
77
|
+
All options accept `Observable<T>` for reactive control:
|
|
78
|
+
|
|
79
|
+
```tsx twoslash
|
|
80
|
+
// @noErrors
|
|
81
|
+
import { observable } from '@legendapp/state'
|
|
82
|
+
import { useRef$, Ref$, useElementVisibility } from '@usels/core'
|
|
83
|
+
declare const el$: Ref$<HTMLDivElement>
|
|
84
|
+
// ---cut---
|
|
85
|
+
const threshold$ = observable<number | number[]>(0.5)
|
|
86
|
+
const rootMargin$ = observable('0px')
|
|
87
|
+
const once$ = observable(false)
|
|
88
|
+
|
|
89
|
+
const isVisible$ = useElementVisibility(el$, {
|
|
90
|
+
threshold: threshold$,
|
|
91
|
+
rootMargin: rootMargin$,
|
|
92
|
+
once: once$,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// later — update reactively
|
|
96
|
+
threshold$.set(0.75)
|
|
97
|
+
rootMargin$.set('-50px 0px')
|
|
98
|
+
```
|