@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,159 @@
|
|
|
1
|
+
import type { Observable } from "@legendapp/state";
|
|
2
|
+
import { useObservable } from "@legendapp/state/react";
|
|
3
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
4
|
+
import { type MaybeElement, peekElement } from "../useRef$";
|
|
5
|
+
import { useResizeObserver } from "../useResizeObserver";
|
|
6
|
+
import { useMutationObserver } from "../useMutationObserver";
|
|
7
|
+
import { useEventListener } from "../../browser/useEventListener";
|
|
8
|
+
import { isWindow } from "../../shared";
|
|
9
|
+
import { useMayObservableOptions } from "../../function/useMayObservableOptions";
|
|
10
|
+
import type { DeepMaybeObservable } from "../../types";
|
|
11
|
+
|
|
12
|
+
export interface UseElementBoundingOptions {
|
|
13
|
+
/** Reset all values to 0 when element unmounts. Default: true */
|
|
14
|
+
reset?: boolean;
|
|
15
|
+
/** Re-calculate on window resize. Default: true */
|
|
16
|
+
windowResize?: boolean;
|
|
17
|
+
/** Re-calculate on window scroll. Default: true */
|
|
18
|
+
windowScroll?: boolean;
|
|
19
|
+
/** Calculate immediately on mount. Default: true */
|
|
20
|
+
immediate?: boolean;
|
|
21
|
+
/** Use requestAnimationFrame to read rect after CSS transforms settle. Default: true */
|
|
22
|
+
useCssTransforms?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseElementBoundingReturn {
|
|
26
|
+
x: Observable<number>;
|
|
27
|
+
y: Observable<number>;
|
|
28
|
+
top: Observable<number>;
|
|
29
|
+
right: Observable<number>;
|
|
30
|
+
bottom: Observable<number>;
|
|
31
|
+
left: Observable<number>;
|
|
32
|
+
width: Observable<number>;
|
|
33
|
+
height: Observable<number>;
|
|
34
|
+
update: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ZERO = {
|
|
38
|
+
x: 0,
|
|
39
|
+
y: 0,
|
|
40
|
+
top: 0,
|
|
41
|
+
right: 0,
|
|
42
|
+
bottom: 0,
|
|
43
|
+
left: 0,
|
|
44
|
+
width: 0,
|
|
45
|
+
height: 0,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// isWindow(window) returns false in SSR (typeof window === "undefined"), true in browser.
|
|
49
|
+
const win = typeof window !== "undefined" ? window : null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Tracks the bounding rect of a DOM element (x, y, top, right, bottom, left, width, height).
|
|
53
|
+
* Observes ResizeObserver, MutationObserver (style/class changes), window scroll, and resize.
|
|
54
|
+
*
|
|
55
|
+
* @param target - Element to observe: Ref$, Observable<OpaqueObject<Element>|null>, Document, Window, or null
|
|
56
|
+
* @param options - Configuration options
|
|
57
|
+
* @returns Reactive bounding rect values plus a manual `update()` function
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```tsx
|
|
61
|
+
* const el$ = useRef$<HTMLDivElement>();
|
|
62
|
+
* const { top, left, width, height } = useElementBounding(el$);
|
|
63
|
+
* return <div ref={el$} />;
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function useElementBounding(
|
|
67
|
+
target: MaybeElement,
|
|
68
|
+
options?: DeepMaybeObservable<UseElementBoundingOptions>,
|
|
69
|
+
): UseElementBoundingReturn {
|
|
70
|
+
const opts$ = useMayObservableOptions<UseElementBoundingOptions>(options, {
|
|
71
|
+
immediate: "peek",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const bounding$ = useObservable({ ...ZERO });
|
|
75
|
+
|
|
76
|
+
// Guards rAF callbacks from updating state after unmount.
|
|
77
|
+
const unmountedRef = useRef(false);
|
|
78
|
+
const rafRef = useRef<number | null>(null);
|
|
79
|
+
|
|
80
|
+
const recalculate = useCallback(() => {
|
|
81
|
+
const el = peekElement(target) as Element | null;
|
|
82
|
+
if (!el || !(el instanceof Element)) {
|
|
83
|
+
if (opts$.reset.peek() !== false) bounding$.assign({ ...ZERO });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const rect = el.getBoundingClientRect();
|
|
87
|
+
bounding$.assign({
|
|
88
|
+
x: rect.x,
|
|
89
|
+
y: rect.y,
|
|
90
|
+
top: rect.top,
|
|
91
|
+
right: rect.right,
|
|
92
|
+
bottom: rect.bottom,
|
|
93
|
+
left: rect.left,
|
|
94
|
+
width: rect.width,
|
|
95
|
+
height: rect.height,
|
|
96
|
+
});
|
|
97
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
98
|
+
|
|
99
|
+
const update = useCallback(() => {
|
|
100
|
+
if (opts$.useCssTransforms.peek() !== false) {
|
|
101
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
102
|
+
if (!unmountedRef.current) recalculate();
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
recalculate();
|
|
106
|
+
}
|
|
107
|
+
}, [recalculate]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
108
|
+
|
|
109
|
+
// Observe size changes
|
|
110
|
+
useResizeObserver(target, update);
|
|
111
|
+
|
|
112
|
+
// Observe style/class attribute changes (e.g. CSS transitions, class toggles)
|
|
113
|
+
useMutationObserver(target, update, {
|
|
114
|
+
attributes: true,
|
|
115
|
+
attributeFilter: ["style", "class"],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Observe window scroll / resize (always call hooks unconditionally — Rules of Hooks)
|
|
119
|
+
// isWindow(win) is false in SSR, so target becomes null outside the browser.
|
|
120
|
+
// peek() — evaluated once at render time, no reactive subscription needed.
|
|
121
|
+
useEventListener(
|
|
122
|
+
isWindow(win) && opts$.windowScroll.peek() !== false ? win : null,
|
|
123
|
+
"scroll",
|
|
124
|
+
update,
|
|
125
|
+
{ passive: true },
|
|
126
|
+
);
|
|
127
|
+
useEventListener(
|
|
128
|
+
isWindow(win) && opts$.windowResize.peek() !== false ? win : null,
|
|
129
|
+
"resize",
|
|
130
|
+
update,
|
|
131
|
+
{ passive: true },
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
unmountedRef.current = false;
|
|
137
|
+
if (opts$.immediate.peek() !== false) update();
|
|
138
|
+
return () => {
|
|
139
|
+
unmountedRef.current = true;
|
|
140
|
+
if (rafRef.current !== null) {
|
|
141
|
+
cancelAnimationFrame(rafRef.current);
|
|
142
|
+
rafRef.current = null;
|
|
143
|
+
}
|
|
144
|
+
if (opts$.reset.peek() !== false) bounding$.assign({ ...ZERO });
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
x: bounding$.x,
|
|
150
|
+
y: bounding$.y,
|
|
151
|
+
top: bounding$.top,
|
|
152
|
+
right: bounding$.right,
|
|
153
|
+
bottom: bounding$.bottom,
|
|
154
|
+
left: bounding$.left,
|
|
155
|
+
width: bounding$.width,
|
|
156
|
+
height: bounding$.height,
|
|
157
|
+
update,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Computed } from "@legendapp/state/react";
|
|
2
|
+
import { useRef$ } from "../useRef$";
|
|
3
|
+
import { useElementSize } from ".";
|
|
4
|
+
|
|
5
|
+
export default function UseElementSizeDemo() {
|
|
6
|
+
const el$ = useRef$<HTMLTextAreaElement>();
|
|
7
|
+
const { width, height } = useElementSize(el$);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
|
11
|
+
<div
|
|
12
|
+
style={{
|
|
13
|
+
display: "flex",
|
|
14
|
+
gap: "24px",
|
|
15
|
+
fontFamily: "monospace",
|
|
16
|
+
fontSize: "14px",
|
|
17
|
+
padding: "8px 12px",
|
|
18
|
+
background: "var(--sl-color-gray-6, #f1f5f9)",
|
|
19
|
+
borderRadius: "6px",
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<Computed>
|
|
23
|
+
{() => (
|
|
24
|
+
<>
|
|
25
|
+
<span>
|
|
26
|
+
width: <strong>{Math.round(width.get())}px</strong>
|
|
27
|
+
</span>
|
|
28
|
+
<span>
|
|
29
|
+
height: <strong>{Math.round(height.get())}px</strong>
|
|
30
|
+
</span>
|
|
31
|
+
</>
|
|
32
|
+
)}
|
|
33
|
+
</Computed>
|
|
34
|
+
</div>
|
|
35
|
+
<textarea
|
|
36
|
+
ref={el$}
|
|
37
|
+
defaultValue="Resize this textarea to see width & height update"
|
|
38
|
+
style={{
|
|
39
|
+
resize: "both",
|
|
40
|
+
overflow: "auto",
|
|
41
|
+
width: "300px",
|
|
42
|
+
height: "120px",
|
|
43
|
+
padding: "10px",
|
|
44
|
+
border: "1px solid var(--sl-color-gray-5, #cbd5e1)",
|
|
45
|
+
borderRadius: "6px",
|
|
46
|
+
fontFamily: "inherit",
|
|
47
|
+
fontSize: "14px",
|
|
48
|
+
lineHeight: "1.5",
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useElementSize
|
|
3
|
+
category: elements
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Tracks the width and height of a DOM element using the [ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
|
|
7
|
+
Returns reactive `Observable<number>` values that update whenever the element resizes.
|
|
8
|
+
SVG elements use `getBoundingClientRect()` as a fallback. Supports all three box models.
|
|
9
|
+
|
|
10
|
+
## Demo
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```tsx twoslash
|
|
15
|
+
// @noErrors
|
|
16
|
+
import { useRef$, useElementSize } from '@usels/core'
|
|
17
|
+
import { Computed } from '@legendapp/state/react'
|
|
18
|
+
|
|
19
|
+
function Component() {
|
|
20
|
+
const el$ = useRef$<HTMLDivElement>()
|
|
21
|
+
const { width, height } = useElementSize(el$)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Computed>
|
|
25
|
+
{() => (
|
|
26
|
+
<div ref={el$}>
|
|
27
|
+
{width.get()} × {height.get()}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
</Computed>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Custom initial size
|
|
36
|
+
|
|
37
|
+
```tsx twoslash
|
|
38
|
+
// @noErrors
|
|
39
|
+
import { useRef$, Ref$, useElementSize } from '@usels/core'
|
|
40
|
+
declare const el$: Ref$<HTMLDivElement>
|
|
41
|
+
// ---cut---
|
|
42
|
+
const { width, height } = useElementSize(el$, { width: 320, height: 240 })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### With `border-box`
|
|
46
|
+
|
|
47
|
+
```tsx twoslash
|
|
48
|
+
// @noErrors
|
|
49
|
+
import { useRef$, Ref$, useElementSize } from '@usels/core'
|
|
50
|
+
declare const el$: Ref$<HTMLDivElement>
|
|
51
|
+
// ---cut---
|
|
52
|
+
const { width, height } = useElementSize(el$, undefined, { box: 'border-box' })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Stopping observation manually
|
|
56
|
+
|
|
57
|
+
```tsx twoslash
|
|
58
|
+
// @noErrors
|
|
59
|
+
import { useRef$, Ref$, useElementSize } from '@usels/core'
|
|
60
|
+
declare const el$: Ref$<HTMLDivElement>
|
|
61
|
+
// ---cut---
|
|
62
|
+
const { width, height, stop } = useElementSize(el$)
|
|
63
|
+
|
|
64
|
+
stop()
|
|
65
|
+
```
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
import { observable, ObservableHint } from "@legendapp/state";
|
|
4
|
+
import type { OpaqueObject } from "@legendapp/state";
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
6
|
+
import { useRef$ } from "../useRef$";
|
|
7
|
+
import { useElementSize } from ".";
|
|
8
|
+
|
|
9
|
+
const wrapEl = (el: Element) =>
|
|
10
|
+
observable<OpaqueObject<Element> | null>(ObservableHint.opaque(el));
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// ResizeObserver mock (extended with box size support)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
type BoxSize = { inlineSize: number; blockSize: number };
|
|
17
|
+
|
|
18
|
+
class ResizeObserverMock {
|
|
19
|
+
static instances: ResizeObserverMock[] = [];
|
|
20
|
+
|
|
21
|
+
callback: ResizeObserverCallback;
|
|
22
|
+
observed: Element[] = [];
|
|
23
|
+
disconnected = false;
|
|
24
|
+
|
|
25
|
+
constructor(cb: ResizeObserverCallback) {
|
|
26
|
+
this.callback = cb;
|
|
27
|
+
ResizeObserverMock.instances.push(this);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
observe(el: Element) {
|
|
31
|
+
this.observed.push(el);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
unobserve(el: Element) {
|
|
35
|
+
this.observed = this.observed.filter((e) => e !== el);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
disconnect() {
|
|
39
|
+
this.disconnected = true;
|
|
40
|
+
this.observed = [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Trigger the callback with a configurable fake entry.
|
|
45
|
+
* All box size arrays default to empty (falling back to contentRect).
|
|
46
|
+
*/
|
|
47
|
+
trigger(
|
|
48
|
+
el: Element,
|
|
49
|
+
options: {
|
|
50
|
+
contentRect?: Partial<DOMRectReadOnly>;
|
|
51
|
+
contentBoxSize?: BoxSize[];
|
|
52
|
+
borderBoxSize?: BoxSize[];
|
|
53
|
+
devicePixelContentBoxSize?: BoxSize[];
|
|
54
|
+
} = {},
|
|
55
|
+
) {
|
|
56
|
+
if (this.disconnected) return;
|
|
57
|
+
const entry = {
|
|
58
|
+
target: el,
|
|
59
|
+
contentRect: {
|
|
60
|
+
width: 100,
|
|
61
|
+
height: 200,
|
|
62
|
+
...options.contentRect,
|
|
63
|
+
} as DOMRectReadOnly,
|
|
64
|
+
contentBoxSize: options.contentBoxSize ?? [],
|
|
65
|
+
borderBoxSize: options.borderBoxSize ?? [],
|
|
66
|
+
devicePixelContentBoxSize: options.devicePixelContentBoxSize ?? [],
|
|
67
|
+
} as unknown as ResizeObserverEntry;
|
|
68
|
+
this.callback([entry], this);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
ResizeObserverMock.instances = [];
|
|
74
|
+
vi.stubGlobal("ResizeObserver", ResizeObserverMock);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
vi.unstubAllGlobals();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// useElementSize
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
describe("useElementSize()", () => {
|
|
86
|
+
it("updates width and height on resize event (contentRect fallback)", () => {
|
|
87
|
+
const div = document.createElement("div");
|
|
88
|
+
|
|
89
|
+
const { result } = renderHook(() => useElementSize(wrapEl(div) as any));
|
|
90
|
+
|
|
91
|
+
const instance = ResizeObserverMock.instances.at(-1)!;
|
|
92
|
+
act(() =>
|
|
93
|
+
instance.trigger(div, { contentRect: { width: 320, height: 240 } }),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(result.current.width.get()).toBe(320);
|
|
97
|
+
expect(result.current.height.get()).toBe(240);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("uses getBoundingClientRect() for SVG elements", () => {
|
|
101
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
102
|
+
vi.spyOn(svg, "getBoundingClientRect").mockReturnValue({
|
|
103
|
+
width: 150,
|
|
104
|
+
height: 75,
|
|
105
|
+
top: 0,
|
|
106
|
+
left: 0,
|
|
107
|
+
bottom: 75,
|
|
108
|
+
right: 150,
|
|
109
|
+
x: 0,
|
|
110
|
+
y: 0,
|
|
111
|
+
toJSON: () => ({}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const { result } = renderHook(() => useElementSize(wrapEl(svg) as any));
|
|
115
|
+
|
|
116
|
+
const instance = ResizeObserverMock.instances.at(-1)!;
|
|
117
|
+
act(() => instance.trigger(svg));
|
|
118
|
+
|
|
119
|
+
expect(svg.getBoundingClientRect).toHaveBeenCalled();
|
|
120
|
+
expect(result.current.width.get()).toBe(150);
|
|
121
|
+
expect(result.current.height.get()).toBe(75);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("reads borderBoxSize when box option is 'border-box'", () => {
|
|
125
|
+
const div = document.createElement("div");
|
|
126
|
+
|
|
127
|
+
const { result } = renderHook(() =>
|
|
128
|
+
useElementSize(wrapEl(div) as any, undefined, { box: "border-box" }),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const instance = ResizeObserverMock.instances.at(-1)!;
|
|
132
|
+
act(() =>
|
|
133
|
+
instance.trigger(div, {
|
|
134
|
+
borderBoxSize: [{ inlineSize: 400, blockSize: 300 }],
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(result.current.width.get()).toBe(400);
|
|
139
|
+
expect(result.current.height.get()).toBe(300);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("reads devicePixelContentBoxSize when box option is 'device-pixel-content-box'", () => {
|
|
143
|
+
const div = document.createElement("div");
|
|
144
|
+
|
|
145
|
+
const { result } = renderHook(() =>
|
|
146
|
+
useElementSize(wrapEl(div) as any, undefined, {
|
|
147
|
+
box: "device-pixel-content-box",
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const instance = ResizeObserverMock.instances.at(-1)!;
|
|
152
|
+
act(() =>
|
|
153
|
+
instance.trigger(div, {
|
|
154
|
+
devicePixelContentBoxSize: [{ inlineSize: 800, blockSize: 600 }],
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result.current.width.get()).toBe(800);
|
|
159
|
+
expect(result.current.height.get()).toBe(600);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("falls back to contentBoxSize when present and no specific box matched", () => {
|
|
163
|
+
const div = document.createElement("div");
|
|
164
|
+
|
|
165
|
+
// default box is "content-box" — reads contentBoxSize
|
|
166
|
+
const { result } = renderHook(() => useElementSize(wrapEl(div) as any));
|
|
167
|
+
|
|
168
|
+
const instance = ResizeObserverMock.instances.at(-1)!;
|
|
169
|
+
act(() =>
|
|
170
|
+
instance.trigger(div, {
|
|
171
|
+
contentBoxSize: [{ inlineSize: 500, blockSize: 250 }],
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(result.current.width.get()).toBe(500);
|
|
176
|
+
expect(result.current.height.get()).toBe(250);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("falls back to contentRect when no boxSize arrays are present", () => {
|
|
180
|
+
const div = document.createElement("div");
|
|
181
|
+
|
|
182
|
+
const { result } = renderHook(() => useElementSize(wrapEl(div) as any));
|
|
183
|
+
|
|
184
|
+
const instance = ResizeObserverMock.instances.at(-1)!;
|
|
185
|
+
act(() =>
|
|
186
|
+
instance.trigger(div, { contentRect: { width: 123, height: 456 } }),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(result.current.width.get()).toBe(123);
|
|
190
|
+
expect(result.current.height.get()).toBe(456);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("returns initialSize when target is null", () => {
|
|
194
|
+
const { result } = renderHook(() =>
|
|
195
|
+
useElementSize(null as any, { width: 10, height: 20 }),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(result.current.width.get()).toBe(10);
|
|
199
|
+
expect(result.current.height.get()).toBe(20);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("returns default initialSize { width: 0, height: 0 } when no initialSize provided and target is null", () => {
|
|
203
|
+
const { result } = renderHook(() => useElementSize(null as any));
|
|
204
|
+
|
|
205
|
+
expect(result.current.width.get()).toBe(0);
|
|
206
|
+
expect(result.current.height.get()).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("stop() suppresses further size updates", () => {
|
|
210
|
+
const div = document.createElement("div");
|
|
211
|
+
|
|
212
|
+
const { result } = renderHook(() => useElementSize(wrapEl(div) as any));
|
|
213
|
+
|
|
214
|
+
const instance = ResizeObserverMock.instances.at(-1)!;
|
|
215
|
+
|
|
216
|
+
// Trigger first resize — should update
|
|
217
|
+
act(() =>
|
|
218
|
+
instance.trigger(div, { contentRect: { width: 100, height: 100 } }),
|
|
219
|
+
);
|
|
220
|
+
expect(result.current.width.get()).toBe(100);
|
|
221
|
+
|
|
222
|
+
// Stop observing
|
|
223
|
+
act(() => result.current.stop());
|
|
224
|
+
|
|
225
|
+
// Trigger again — instance is disconnected, trigger() is a no-op
|
|
226
|
+
act(() =>
|
|
227
|
+
instance.trigger(div, { contentRect: { width: 999, height: 999 } }),
|
|
228
|
+
);
|
|
229
|
+
expect(result.current.width.get()).toBe(100);
|
|
230
|
+
expect(result.current.height.get()).toBe(100);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("Ref$ target: sets offsetWidth/offsetHeight as initial size after element is assigned", () => {
|
|
234
|
+
const { result } = renderHook(() => {
|
|
235
|
+
const el$ = useRef$<HTMLDivElement>();
|
|
236
|
+
const size = useElementSize(el$);
|
|
237
|
+
return { el$, size };
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Before assignment — initial values
|
|
241
|
+
expect(result.current.size.width.get()).toBe(0);
|
|
242
|
+
expect(result.current.size.height.get()).toBe(0);
|
|
243
|
+
|
|
244
|
+
const div = document.createElement("div");
|
|
245
|
+
// jsdom returns 0 for offsetWidth/Height by default, but we can override
|
|
246
|
+
Object.defineProperty(div, "offsetWidth", { value: 640, configurable: true });
|
|
247
|
+
Object.defineProperty(div, "offsetHeight", { value: 480, configurable: true });
|
|
248
|
+
|
|
249
|
+
act(() => result.current.el$(div));
|
|
250
|
+
|
|
251
|
+
expect(result.current.size.width.get()).toBe(640);
|
|
252
|
+
expect(result.current.size.height.get()).toBe(480);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("resets to initialSize when target Ref$ becomes null", () => {
|
|
256
|
+
const div = document.createElement("div");
|
|
257
|
+
|
|
258
|
+
const { result } = renderHook(() => {
|
|
259
|
+
const el$ = useRef$<HTMLDivElement>();
|
|
260
|
+
const size = useElementSize(el$, { width: 5, height: 10 });
|
|
261
|
+
return { el$, size };
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Assign element first
|
|
265
|
+
act(() => result.current.el$(div));
|
|
266
|
+
|
|
267
|
+
// Then set element to null (passing null simulates unmounting)
|
|
268
|
+
act(() => result.current.el$(null));
|
|
269
|
+
|
|
270
|
+
expect(result.current.size.width.get()).toBe(5);
|
|
271
|
+
expect(result.current.size.height.get()).toBe(10);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("accumulates multiple borderBoxSize entries", () => {
|
|
275
|
+
const div = document.createElement("div");
|
|
276
|
+
|
|
277
|
+
const { result } = renderHook(() =>
|
|
278
|
+
useElementSize(wrapEl(div) as any, undefined, { box: "border-box" }),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const instance = ResizeObserverMock.instances.at(-1)!;
|
|
282
|
+
act(() =>
|
|
283
|
+
instance.trigger(div, {
|
|
284
|
+
borderBoxSize: [
|
|
285
|
+
{ inlineSize: 100, blockSize: 50 },
|
|
286
|
+
{ inlineSize: 200, blockSize: 100 },
|
|
287
|
+
],
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Accumulated: 100+200=300, 50+100=150
|
|
292
|
+
expect(result.current.width.get()).toBe(300);
|
|
293
|
+
expect(result.current.height.get()).toBe(150);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Observable } from "@legendapp/state";
|
|
2
|
+
import { useObservable, useObserveEffect } from "@legendapp/state/react";
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { getElement, MaybeElement } from "../useRef$";
|
|
5
|
+
import { useResizeObserver } from "../useResizeObserver";
|
|
6
|
+
|
|
7
|
+
export interface UseElementSizeOptions {
|
|
8
|
+
box?: "content-box" | "border-box" | "device-pixel-content-box";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UseElementSizeReturn {
|
|
12
|
+
width: Observable<number>;
|
|
13
|
+
height: Observable<number>;
|
|
14
|
+
stop: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tracks the width and height of a DOM element using ResizeObserver.
|
|
19
|
+
* SVG elements use getBoundingClientRect() as fallback.
|
|
20
|
+
*
|
|
21
|
+
* @param target - Element to observe (Ref$, Observable<Element|null>, Document, Window, or null)
|
|
22
|
+
* @param initialSize - Initial size values (default: { width: 0, height: 0 })
|
|
23
|
+
* @param options - Optional box model option
|
|
24
|
+
* @returns `{ width, height, stop }` — reactive size observables and manual stop function
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const el$ = useRef$<HTMLDivElement>();
|
|
29
|
+
* const { width, height } = useElementSize(el$);
|
|
30
|
+
* return <div ref={el$}>{width.get()} x {height.get()}</div>;
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useElementSize(
|
|
34
|
+
target: MaybeElement,
|
|
35
|
+
initialSize?: { width: number; height: number },
|
|
36
|
+
options?: UseElementSizeOptions,
|
|
37
|
+
): UseElementSizeReturn {
|
|
38
|
+
const initial = initialSize ?? { width: 0, height: 0 };
|
|
39
|
+
const size$ = useObservable({ width: initial.width, height: initial.height });
|
|
40
|
+
|
|
41
|
+
const onResize = useCallback<ResizeObserverCallback>((entries) => {
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const el = entry.target;
|
|
44
|
+
const isSvg = (el as Element).namespaceURI?.includes("svg");
|
|
45
|
+
|
|
46
|
+
if (isSvg) {
|
|
47
|
+
const rect = el.getBoundingClientRect();
|
|
48
|
+
size$.assign({ width: rect.width, height: rect.height });
|
|
49
|
+
} else {
|
|
50
|
+
const box = options?.box ?? "content-box";
|
|
51
|
+
let w = 0,
|
|
52
|
+
h = 0;
|
|
53
|
+
if (box === "border-box" && entry.borderBoxSize?.length) {
|
|
54
|
+
for (const b of entry.borderBoxSize) {
|
|
55
|
+
w += b.inlineSize;
|
|
56
|
+
h += b.blockSize;
|
|
57
|
+
}
|
|
58
|
+
} else if (
|
|
59
|
+
box === "device-pixel-content-box" &&
|
|
60
|
+
entry.devicePixelContentBoxSize?.length
|
|
61
|
+
) {
|
|
62
|
+
for (const b of entry.devicePixelContentBoxSize) {
|
|
63
|
+
w += b.inlineSize;
|
|
64
|
+
h += b.blockSize;
|
|
65
|
+
}
|
|
66
|
+
} else if (entry.contentBoxSize?.length) {
|
|
67
|
+
for (const b of entry.contentBoxSize) {
|
|
68
|
+
w += b.inlineSize;
|
|
69
|
+
h += b.blockSize;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
w = entry.contentRect.width;
|
|
73
|
+
h = entry.contentRect.height;
|
|
74
|
+
}
|
|
75
|
+
size$.assign({ width: w, height: h });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
79
|
+
|
|
80
|
+
const { stop } = useResizeObserver(target, onResize, { box: options?.box });
|
|
81
|
+
|
|
82
|
+
// Set initial size from offsetWidth/Height after element mounts.
|
|
83
|
+
// Uses getElement (tracked) so this re-runs when target Ref$ changes.
|
|
84
|
+
useObserveEffect(() => {
|
|
85
|
+
const el = getElement(target) as HTMLElement | null;
|
|
86
|
+
if (!el) {
|
|
87
|
+
size$.assign({ width: initial.width, height: initial.height });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const isSvg = (el as Element).namespaceURI?.includes("svg");
|
|
91
|
+
if (!isSvg) {
|
|
92
|
+
size$.assign({
|
|
93
|
+
width: (el as HTMLElement).offsetWidth ?? initial.width,
|
|
94
|
+
height: (el as HTMLElement).offsetHeight ?? initial.height,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { width: size$.width, height: size$.height, stop };
|
|
100
|
+
}
|