@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.
Files changed (341) hide show
  1. package/README.md +21 -0
  2. package/dist/browser/useEventListener/index.d.mts +56 -0
  3. package/dist/browser/useEventListener/index.d.ts +56 -0
  4. package/dist/browser/useEventListener/index.js +112 -0
  5. package/dist/browser/useEventListener/index.js.map +1 -0
  6. package/dist/browser/useEventListener/index.mjs +88 -0
  7. package/dist/browser/useEventListener/index.mjs.map +1 -0
  8. package/dist/browser/useMediaQuery/demo.d.mts +5 -0
  9. package/dist/browser/useMediaQuery/demo.d.ts +5 -0
  10. package/dist/browser/useMediaQuery/demo.js +83 -0
  11. package/dist/browser/useMediaQuery/demo.js.map +1 -0
  12. package/dist/browser/useMediaQuery/demo.mjs +63 -0
  13. package/dist/browser/useMediaQuery/demo.mjs.map +1 -0
  14. package/dist/browser/useMediaQuery/index.d.mts +11 -0
  15. package/dist/browser/useMediaQuery/index.d.ts +11 -0
  16. package/dist/browser/useMediaQuery/index.js +89 -0
  17. package/dist/browser/useMediaQuery/index.js.map +1 -0
  18. package/dist/browser/useMediaQuery/index.mjs +64 -0
  19. package/dist/browser/useMediaQuery/index.mjs.map +1 -0
  20. package/dist/components/Auto/index.d.mts +33 -0
  21. package/dist/components/Auto/index.d.ts +33 -0
  22. package/dist/components/Auto/index.js +66 -0
  23. package/dist/components/Auto/index.js.map +1 -0
  24. package/dist/components/Auto/index.mjs +34 -0
  25. package/dist/components/Auto/index.mjs.map +1 -0
  26. package/dist/elements/useDocumentVisibility/demo.d.mts +5 -0
  27. package/dist/elements/useDocumentVisibility/demo.d.ts +5 -0
  28. package/dist/elements/useDocumentVisibility/demo.js +130 -0
  29. package/dist/elements/useDocumentVisibility/demo.js.map +1 -0
  30. package/dist/elements/useDocumentVisibility/demo.mjs +114 -0
  31. package/dist/elements/useDocumentVisibility/demo.mjs.map +1 -0
  32. package/dist/elements/useDocumentVisibility/index.d.mts +5 -0
  33. package/dist/elements/useDocumentVisibility/index.d.ts +5 -0
  34. package/dist/elements/useDocumentVisibility/index.js +45 -0
  35. package/dist/elements/useDocumentVisibility/index.js.map +1 -0
  36. package/dist/elements/useDocumentVisibility/index.mjs +21 -0
  37. package/dist/elements/useDocumentVisibility/index.mjs.map +1 -0
  38. package/dist/elements/useElementBounding/demo.d.mts +5 -0
  39. package/dist/elements/useElementBounding/demo.d.ts +5 -0
  40. package/dist/elements/useElementBounding/demo.js +87 -0
  41. package/dist/elements/useElementBounding/demo.js.map +1 -0
  42. package/dist/elements/useElementBounding/demo.mjs +67 -0
  43. package/dist/elements/useElementBounding/demo.mjs.map +1 -0
  44. package/dist/elements/useElementBounding/index.d.mts +46 -0
  45. package/dist/elements/useElementBounding/index.d.ts +46 -0
  46. package/dist/elements/useElementBounding/index.js +122 -0
  47. package/dist/elements/useElementBounding/index.js.map +1 -0
  48. package/dist/elements/useElementBounding/index.mjs +98 -0
  49. package/dist/elements/useElementBounding/index.mjs.map +1 -0
  50. package/dist/elements/useElementSize/demo.d.mts +5 -0
  51. package/dist/elements/useElementSize/demo.d.ts +5 -0
  52. package/dist/elements/useElementSize/demo.js +83 -0
  53. package/dist/elements/useElementSize/demo.js.map +1 -0
  54. package/dist/elements/useElementSize/demo.mjs +63 -0
  55. package/dist/elements/useElementSize/demo.mjs.map +1 -0
  56. package/dist/elements/useElementSize/index.d.mts +34 -0
  57. package/dist/elements/useElementSize/index.d.ts +34 -0
  58. package/dist/elements/useElementSize/index.js +85 -0
  59. package/dist/elements/useElementSize/index.js.map +1 -0
  60. package/dist/elements/useElementSize/index.mjs +61 -0
  61. package/dist/elements/useElementSize/index.mjs.map +1 -0
  62. package/dist/elements/useElementVisibility/demo.d.mts +5 -0
  63. package/dist/elements/useElementVisibility/demo.d.ts +5 -0
  64. package/dist/elements/useElementVisibility/demo.js +110 -0
  65. package/dist/elements/useElementVisibility/demo.js.map +1 -0
  66. package/dist/elements/useElementVisibility/demo.mjs +90 -0
  67. package/dist/elements/useElementVisibility/demo.mjs.map +1 -0
  68. package/dist/elements/useElementVisibility/index.d.mts +43 -0
  69. package/dist/elements/useElementVisibility/index.d.ts +43 -0
  70. package/dist/elements/useElementVisibility/index.js +58 -0
  71. package/dist/elements/useElementVisibility/index.js.map +1 -0
  72. package/dist/elements/useElementVisibility/index.mjs +34 -0
  73. package/dist/elements/useElementVisibility/index.mjs.map +1 -0
  74. package/dist/elements/useIntersectionObserver/demo.d.mts +5 -0
  75. package/dist/elements/useIntersectionObserver/demo.d.ts +5 -0
  76. package/dist/elements/useIntersectionObserver/demo.js +173 -0
  77. package/dist/elements/useIntersectionObserver/demo.js.map +1 -0
  78. package/dist/elements/useIntersectionObserver/demo.mjs +153 -0
  79. package/dist/elements/useIntersectionObserver/demo.mjs.map +1 -0
  80. package/dist/elements/useIntersectionObserver/index.d.mts +47 -0
  81. package/dist/elements/useIntersectionObserver/index.d.ts +47 -0
  82. package/dist/elements/useIntersectionObserver/index.js +111 -0
  83. package/dist/elements/useIntersectionObserver/index.js.map +1 -0
  84. package/dist/elements/useIntersectionObserver/index.mjs +87 -0
  85. package/dist/elements/useIntersectionObserver/index.mjs.map +1 -0
  86. package/dist/elements/useMouseInElement/demo.d.mts +5 -0
  87. package/dist/elements/useMouseInElement/demo.d.ts +5 -0
  88. package/dist/elements/useMouseInElement/demo.js +104 -0
  89. package/dist/elements/useMouseInElement/demo.js.map +1 -0
  90. package/dist/elements/useMouseInElement/demo.mjs +84 -0
  91. package/dist/elements/useMouseInElement/demo.mjs.map +1 -0
  92. package/dist/elements/useMouseInElement/index.d.mts +56 -0
  93. package/dist/elements/useMouseInElement/index.d.ts +56 -0
  94. package/dist/elements/useMouseInElement/index.js +148 -0
  95. package/dist/elements/useMouseInElement/index.js.map +1 -0
  96. package/dist/elements/useMouseInElement/index.mjs +124 -0
  97. package/dist/elements/useMouseInElement/index.mjs.map +1 -0
  98. package/dist/elements/useMutationObserver/demo.d.mts +5 -0
  99. package/dist/elements/useMutationObserver/demo.d.ts +5 -0
  100. package/dist/elements/useMutationObserver/demo.js +240 -0
  101. package/dist/elements/useMutationObserver/demo.js.map +1 -0
  102. package/dist/elements/useMutationObserver/demo.mjs +220 -0
  103. package/dist/elements/useMutationObserver/demo.mjs.map +1 -0
  104. package/dist/elements/useMutationObserver/index.d.mts +15 -0
  105. package/dist/elements/useMutationObserver/index.d.ts +15 -0
  106. package/dist/elements/useMutationObserver/index.js +69 -0
  107. package/dist/elements/useMutationObserver/index.js.map +1 -0
  108. package/dist/elements/useMutationObserver/index.mjs +45 -0
  109. package/dist/elements/useMutationObserver/index.mjs.map +1 -0
  110. package/dist/elements/useParentElement/demo.d.mts +5 -0
  111. package/dist/elements/useParentElement/demo.d.ts +5 -0
  112. package/dist/elements/useParentElement/demo.js +132 -0
  113. package/dist/elements/useParentElement/demo.js.map +1 -0
  114. package/dist/elements/useParentElement/demo.mjs +112 -0
  115. package/dist/elements/useParentElement/demo.mjs.map +1 -0
  116. package/dist/elements/useParentElement/index.d.mts +7 -0
  117. package/dist/elements/useParentElement/index.d.ts +7 -0
  118. package/dist/elements/useParentElement/index.js +47 -0
  119. package/dist/elements/useParentElement/index.js.map +1 -0
  120. package/dist/elements/useParentElement/index.mjs +23 -0
  121. package/dist/elements/useParentElement/index.mjs.map +1 -0
  122. package/dist/elements/useRef$/index.js +89 -0
  123. package/dist/elements/useRef$/index.js.map +1 -0
  124. package/dist/elements/useRef$/index.mjs +62 -0
  125. package/dist/elements/useRef$/index.mjs.map +1 -0
  126. package/dist/elements/useRef_/index.d.mts +60 -0
  127. package/dist/elements/useRef_/index.d.ts +60 -0
  128. package/dist/elements/useResizeObserver/demo.d.mts +5 -0
  129. package/dist/elements/useResizeObserver/demo.d.ts +5 -0
  130. package/dist/elements/useResizeObserver/demo.js +90 -0
  131. package/dist/elements/useResizeObserver/demo.js.map +1 -0
  132. package/dist/elements/useResizeObserver/demo.mjs +70 -0
  133. package/dist/elements/useResizeObserver/demo.mjs.map +1 -0
  134. package/dist/elements/useResizeObserver/index.d.mts +36 -0
  135. package/dist/elements/useResizeObserver/index.d.ts +36 -0
  136. package/dist/elements/useResizeObserver/index.js +74 -0
  137. package/dist/elements/useResizeObserver/index.js.map +1 -0
  138. package/dist/elements/useResizeObserver/index.mjs +49 -0
  139. package/dist/elements/useResizeObserver/index.mjs.map +1 -0
  140. package/dist/elements/useWindowFocus/demo.d.mts +5 -0
  141. package/dist/elements/useWindowFocus/demo.d.ts +5 -0
  142. package/dist/elements/useWindowFocus/demo.js +104 -0
  143. package/dist/elements/useWindowFocus/demo.js.map +1 -0
  144. package/dist/elements/useWindowFocus/demo.mjs +84 -0
  145. package/dist/elements/useWindowFocus/demo.mjs.map +1 -0
  146. package/dist/elements/useWindowFocus/index.d.mts +5 -0
  147. package/dist/elements/useWindowFocus/index.d.ts +5 -0
  148. package/dist/elements/useWindowFocus/index.js +42 -0
  149. package/dist/elements/useWindowFocus/index.js.map +1 -0
  150. package/dist/elements/useWindowFocus/index.mjs +18 -0
  151. package/dist/elements/useWindowFocus/index.mjs.map +1 -0
  152. package/dist/elements/useWindowSize/demo.d.mts +5 -0
  153. package/dist/elements/useWindowSize/demo.d.ts +5 -0
  154. package/dist/elements/useWindowSize/demo.js +79 -0
  155. package/dist/elements/useWindowSize/demo.js.map +1 -0
  156. package/dist/elements/useWindowSize/demo.mjs +59 -0
  157. package/dist/elements/useWindowSize/demo.mjs.map +1 -0
  158. package/dist/elements/useWindowSize/index.d.mts +17 -0
  159. package/dist/elements/useWindowSize/index.d.ts +17 -0
  160. package/dist/elements/useWindowSize/index.js +96 -0
  161. package/dist/elements/useWindowSize/index.js.map +1 -0
  162. package/dist/elements/useWindowSize/index.mjs +76 -0
  163. package/dist/elements/useWindowSize/index.mjs.map +1 -0
  164. package/dist/function/get/index.d.mts +45 -0
  165. package/dist/function/get/index.d.ts +45 -0
  166. package/dist/function/get/index.js +39 -0
  167. package/dist/function/get/index.js.map +1 -0
  168. package/dist/function/get/index.mjs +15 -0
  169. package/dist/function/get/index.mjs.map +1 -0
  170. package/dist/function/peek/index.d.mts +46 -0
  171. package/dist/function/peek/index.d.ts +46 -0
  172. package/dist/function/peek/index.js +39 -0
  173. package/dist/function/peek/index.js.map +1 -0
  174. package/dist/function/peek/index.mjs +15 -0
  175. package/dist/function/peek/index.mjs.map +1 -0
  176. package/dist/function/useMayObservableOptions/index.d.mts +59 -0
  177. package/dist/function/useMayObservableOptions/index.d.ts +59 -0
  178. package/dist/function/useMayObservableOptions/index.js +109 -0
  179. package/dist/function/useMayObservableOptions/index.js.map +1 -0
  180. package/dist/function/useMayObservableOptions/index.mjs +88 -0
  181. package/dist/function/useMayObservableOptions/index.mjs.map +1 -0
  182. package/dist/function/useSupported/index.d.mts +6 -0
  183. package/dist/function/useSupported/index.d.ts +6 -0
  184. package/dist/function/useSupported/index.js +37 -0
  185. package/dist/function/useSupported/index.js.map +1 -0
  186. package/dist/function/useSupported/index.mjs +13 -0
  187. package/dist/function/useSupported/index.mjs.map +1 -0
  188. package/dist/function/useWhenMounted/index.d.mts +6 -0
  189. package/dist/function/useWhenMounted/index.d.ts +6 -0
  190. package/dist/function/useWhenMounted/index.js +37 -0
  191. package/dist/function/useWhenMounted/index.js.map +1 -0
  192. package/dist/function/useWhenMounted/index.mjs +13 -0
  193. package/dist/function/useWhenMounted/index.mjs.map +1 -0
  194. package/dist/index.d.mts +24 -0
  195. package/dist/index.d.ts +24 -0
  196. package/dist/index.js +63 -0
  197. package/dist/index.js.map +1 -0
  198. package/dist/index.mjs +22 -0
  199. package/dist/index.mjs.map +1 -0
  200. package/dist/sensors/useScroll/demo.d.mts +5 -0
  201. package/dist/sensors/useScroll/demo.d.ts +5 -0
  202. package/dist/sensors/useScroll/demo.js +122 -0
  203. package/dist/sensors/useScroll/demo.js.map +1 -0
  204. package/dist/sensors/useScroll/demo.mjs +102 -0
  205. package/dist/sensors/useScroll/demo.mjs.map +1 -0
  206. package/dist/sensors/useScroll/index.d.mts +42 -0
  207. package/dist/sensors/useScroll/index.d.ts +42 -0
  208. package/dist/sensors/useScroll/index.js +149 -0
  209. package/dist/sensors/useScroll/index.js.map +1 -0
  210. package/dist/sensors/useScroll/index.mjs +125 -0
  211. package/dist/sensors/useScroll/index.mjs.map +1 -0
  212. package/dist/sensors/useWindowScroll/demo.d.mts +5 -0
  213. package/dist/sensors/useWindowScroll/demo.d.ts +5 -0
  214. package/dist/sensors/useWindowScroll/demo.js +85 -0
  215. package/dist/sensors/useWindowScroll/demo.js.map +1 -0
  216. package/dist/sensors/useWindowScroll/demo.mjs +65 -0
  217. package/dist/sensors/useWindowScroll/demo.mjs.map +1 -0
  218. package/dist/sensors/useWindowScroll/index.d.mts +9 -0
  219. package/dist/sensors/useWindowScroll/index.d.ts +9 -0
  220. package/dist/sensors/useWindowScroll/index.js +36 -0
  221. package/dist/sensors/useWindowScroll/index.js.map +1 -0
  222. package/dist/sensors/useWindowScroll/index.mjs +12 -0
  223. package/dist/sensors/useWindowScroll/index.mjs.map +1 -0
  224. package/dist/shared/configurable.d.mts +21 -0
  225. package/dist/shared/configurable.d.ts +21 -0
  226. package/dist/shared/configurable.js +39 -0
  227. package/dist/shared/configurable.js.map +1 -0
  228. package/dist/shared/configurable.mjs +12 -0
  229. package/dist/shared/configurable.mjs.map +1 -0
  230. package/dist/shared/index.d.mts +4 -0
  231. package/dist/shared/index.d.ts +4 -0
  232. package/dist/shared/index.js +31 -0
  233. package/dist/shared/index.js.map +1 -0
  234. package/dist/shared/index.mjs +7 -0
  235. package/dist/shared/index.mjs.map +1 -0
  236. package/dist/shared/normalizeTargets/index.d.mts +21 -0
  237. package/dist/shared/normalizeTargets/index.d.ts +21 -0
  238. package/dist/shared/normalizeTargets/index.js +36 -0
  239. package/dist/shared/normalizeTargets/index.js.map +1 -0
  240. package/dist/shared/normalizeTargets/index.mjs +12 -0
  241. package/dist/shared/normalizeTargets/index.mjs.map +1 -0
  242. package/dist/shared/utils.d.mts +15 -0
  243. package/dist/shared/utils.d.ts +15 -0
  244. package/dist/shared/utils.js +87 -0
  245. package/dist/shared/utils.js.map +1 -0
  246. package/dist/shared/utils.mjs +52 -0
  247. package/dist/shared/utils.mjs.map +1 -0
  248. package/dist/types.d.mts +52 -0
  249. package/dist/types.d.ts +52 -0
  250. package/dist/types.js +17 -0
  251. package/dist/types.js.map +1 -0
  252. package/dist/types.mjs +1 -0
  253. package/dist/types.mjs.map +1 -0
  254. package/package.json +54 -0
  255. package/src/browser/useEventListener/index.md +109 -0
  256. package/src/browser/useEventListener/index.spec.ts +611 -0
  257. package/src/browser/useEventListener/index.ts +242 -0
  258. package/src/browser/useMediaQuery/demo.tsx +63 -0
  259. package/src/browser/useMediaQuery/index.md +43 -0
  260. package/src/browser/useMediaQuery/index.spec.ts +267 -0
  261. package/src/browser/useMediaQuery/index.ts +96 -0
  262. package/src/components/Auto/index.tsx +65 -0
  263. package/src/elements/useDocumentVisibility/demo.tsx +111 -0
  264. package/src/elements/useDocumentVisibility/index.md +54 -0
  265. package/src/elements/useDocumentVisibility/index.spec.ts +114 -0
  266. package/src/elements/useDocumentVisibility/index.ts +26 -0
  267. package/src/elements/useElementBounding/demo.tsx +68 -0
  268. package/src/elements/useElementBounding/index.md +64 -0
  269. package/src/elements/useElementBounding/index.ts +159 -0
  270. package/src/elements/useElementSize/demo.tsx +53 -0
  271. package/src/elements/useElementSize/index.md +65 -0
  272. package/src/elements/useElementSize/index.spec.ts +295 -0
  273. package/src/elements/useElementSize/index.ts +100 -0
  274. package/src/elements/useElementVisibility/deep-observable-pattern.spec.ts +453 -0
  275. package/src/elements/useElementVisibility/demo.tsx +97 -0
  276. package/src/elements/useElementVisibility/index.md +98 -0
  277. package/src/elements/useElementVisibility/index.spec.ts +227 -0
  278. package/src/elements/useElementVisibility/index.ts +78 -0
  279. package/src/elements/useIntersectionObserver/demo.tsx +180 -0
  280. package/src/elements/useIntersectionObserver/index.md +99 -0
  281. package/src/elements/useIntersectionObserver/index.spec.ts +482 -0
  282. package/src/elements/useIntersectionObserver/index.ts +149 -0
  283. package/src/elements/useMouseInElement/demo.tsx +88 -0
  284. package/src/elements/useMouseInElement/index.md +76 -0
  285. package/src/elements/useMouseInElement/index.spec.ts +398 -0
  286. package/src/elements/useMouseInElement/index.ts +209 -0
  287. package/src/elements/useMutationObserver/demo.tsx +270 -0
  288. package/src/elements/useMutationObserver/index.md +99 -0
  289. package/src/elements/useMutationObserver/index.spec.ts +421 -0
  290. package/src/elements/useMutationObserver/index.ts +66 -0
  291. package/src/elements/useParentElement/demo.tsx +120 -0
  292. package/src/elements/useParentElement/index.md +67 -0
  293. package/src/elements/useParentElement/index.spec.ts +208 -0
  294. package/src/elements/useParentElement/index.ts +35 -0
  295. package/src/elements/useRef$/index.md +62 -0
  296. package/src/elements/useRef$/index.spec.ts +205 -0
  297. package/src/elements/useRef$/index.ts +137 -0
  298. package/src/elements/useRef$/useImperativeHandle.spec.ts +339 -0
  299. package/src/elements/useResizeObserver/demo.tsx +62 -0
  300. package/src/elements/useResizeObserver/index.md +51 -0
  301. package/src/elements/useResizeObserver/index.spec.ts +312 -0
  302. package/src/elements/useResizeObserver/index.ts +106 -0
  303. package/src/elements/useWindowFocus/demo.tsx +79 -0
  304. package/src/elements/useWindowFocus/index.md +38 -0
  305. package/src/elements/useWindowFocus/index.spec.ts +103 -0
  306. package/src/elements/useWindowFocus/index.ts +21 -0
  307. package/src/elements/useWindowSize/demo.tsx +51 -0
  308. package/src/elements/useWindowSize/index.md +55 -0
  309. package/src/elements/useWindowSize/index.spec.ts +310 -0
  310. package/src/elements/useWindowSize/index.ts +107 -0
  311. package/src/function/get/index.md +25 -0
  312. package/src/function/get/index.spec.ts +87 -0
  313. package/src/function/get/index.ts +70 -0
  314. package/src/function/peek/index.spec.ts +97 -0
  315. package/src/function/peek/index.ts +69 -0
  316. package/src/function/useMayObservableOptions/index.spec.ts +521 -0
  317. package/src/function/useMayObservableOptions/index.ts +173 -0
  318. package/src/function/useSupported/index.md +43 -0
  319. package/src/function/useSupported/index.spec.ts +116 -0
  320. package/src/function/useSupported/index.ts +14 -0
  321. package/src/function/useWhenMounted/index.md +25 -0
  322. package/src/function/useWhenMounted/index.spec.ts +120 -0
  323. package/src/function/useWhenMounted/index.ts +16 -0
  324. package/src/index.ts +25 -0
  325. package/src/sensors/useScroll/demo.tsx +103 -0
  326. package/src/sensors/useScroll/index.md +117 -0
  327. package/src/sensors/useScroll/index.spec.ts +678 -0
  328. package/src/sensors/useScroll/index.ts +201 -0
  329. package/src/sensors/useWindowScroll/demo.tsx +78 -0
  330. package/src/sensors/useWindowScroll/index.md +98 -0
  331. package/src/sensors/useWindowScroll/index.spec.ts +69 -0
  332. package/src/sensors/useWindowScroll/index.ts +11 -0
  333. package/src/shared/configurable.ts +35 -0
  334. package/src/shared/index.ts +4 -0
  335. package/src/shared/normalizeTargets/index.spec.ts +76 -0
  336. package/src/shared/normalizeTargets/index.ts +27 -0
  337. package/src/shared/utils.ts +67 -0
  338. package/src/types.ts +56 -0
  339. package/tsconfig.json +9 -0
  340. package/tsup.config.ts +10 -0
  341. package/vitest.config.ts +22 -0
@@ -0,0 +1,421 @@
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, afterEach } from "vitest";
6
+
7
+ const wrapEl = (el: Element) => observable<OpaqueObject<Element> | null>(ObservableHint.opaque(el));
8
+ import { useRef$ } from "../useRef$";
9
+ import { useMutationObserver } from ".";
10
+
11
+ const flush = () => new Promise<void>((resolve) => queueMicrotask(resolve));
12
+
13
+ describe("useMutationObserver()", () => {
14
+ afterEach(() => {
15
+ document.body.innerHTML = "";
16
+ vi.unstubAllGlobals();
17
+ });
18
+
19
+ it("isSupported is true when MutationObserver is available", () => {
20
+ const el = document.createElement("div");
21
+ const { result } = renderHook(() =>
22
+ useMutationObserver(wrapEl(el), vi.fn(), { attributes: true }),
23
+ );
24
+ expect(result.current.isSupported.get()).toBe(true);
25
+ });
26
+
27
+ it("calls callback when attribute changes", async () => {
28
+ const callback = vi.fn();
29
+ const el = document.createElement("div");
30
+ document.body.appendChild(el);
31
+
32
+ renderHook(() =>
33
+ useMutationObserver(wrapEl(el), callback, { attributes: true }),
34
+ );
35
+
36
+ await act(async () => {
37
+ el.setAttribute("data-test", "value");
38
+ await flush();
39
+ });
40
+
41
+ expect(callback).toHaveBeenCalled();
42
+ const records: MutationRecord[] = callback.mock.calls[0][0];
43
+ expect(records[0].type).toBe("attributes");
44
+ });
45
+
46
+ it("calls callback when child is added", async () => {
47
+ const callback = vi.fn();
48
+ const parent = document.createElement("div");
49
+ document.body.appendChild(parent);
50
+
51
+ renderHook(() =>
52
+ useMutationObserver(wrapEl(parent), callback, { childList: true }),
53
+ );
54
+
55
+ await act(async () => {
56
+ parent.appendChild(document.createElement("span"));
57
+ await flush();
58
+ });
59
+
60
+ expect(callback).toHaveBeenCalled();
61
+ const records: MutationRecord[] = callback.mock.calls[0][0];
62
+ expect(records[0].type).toBe("childList");
63
+ });
64
+
65
+ it("calls callback when child is removed", async () => {
66
+ const callback = vi.fn();
67
+ const parent = document.createElement("div");
68
+ const child = document.createElement("span");
69
+ parent.appendChild(child);
70
+ document.body.appendChild(parent);
71
+
72
+ renderHook(() =>
73
+ useMutationObserver(wrapEl(parent), callback, { childList: true }),
74
+ );
75
+
76
+ await act(async () => {
77
+ parent.removeChild(child);
78
+ await flush();
79
+ });
80
+
81
+ expect(callback).toHaveBeenCalled();
82
+ });
83
+
84
+ it("calls callback when text content changes", async () => {
85
+ const callback = vi.fn();
86
+ const el = document.createElement("p");
87
+ const text = document.createTextNode("initial");
88
+ el.appendChild(text);
89
+ document.body.appendChild(el);
90
+
91
+ renderHook(() =>
92
+ useMutationObserver(wrapEl(text as unknown as Element), callback, {
93
+ characterData: true,
94
+ }),
95
+ );
96
+
97
+ await act(async () => {
98
+ text.data = "changed";
99
+ await flush();
100
+ });
101
+
102
+ expect(callback).toHaveBeenCalled();
103
+ const records: MutationRecord[] = callback.mock.calls[0][0];
104
+ expect(records[0].type).toBe("characterData");
105
+ });
106
+
107
+ it("observes multiple targets independently", async () => {
108
+ const callback = vi.fn();
109
+ const el1 = document.createElement("div");
110
+ const el2 = document.createElement("div");
111
+ document.body.append(el1, el2);
112
+
113
+ renderHook(() =>
114
+ useMutationObserver([wrapEl(el1), wrapEl(el2)], callback, { attributes: true }),
115
+ );
116
+
117
+ await act(async () => {
118
+ el1.setAttribute("data-a", "1");
119
+ await flush();
120
+ });
121
+
122
+ await act(async () => {
123
+ el2.setAttribute("data-b", "2");
124
+ await flush();
125
+ });
126
+
127
+ expect(callback).toHaveBeenCalledTimes(2);
128
+ });
129
+
130
+ it("deduplicates targets when same element is passed twice", async () => {
131
+ const callback = vi.fn();
132
+ const el = document.createElement("div");
133
+ document.body.appendChild(el);
134
+
135
+ renderHook(() =>
136
+ useMutationObserver([wrapEl(el), wrapEl(el)], callback, { attributes: true }),
137
+ );
138
+
139
+ await act(async () => {
140
+ el.setAttribute("data-test", "value");
141
+ await flush();
142
+ });
143
+
144
+ // Observed only once despite being in the array twice
145
+ expect(callback).toHaveBeenCalledTimes(1);
146
+ });
147
+
148
+ it("stop() prevents callback from being called after disconnecting", async () => {
149
+ const callback = vi.fn();
150
+ const el = document.createElement("div");
151
+ document.body.appendChild(el);
152
+
153
+ const { result } = renderHook(() =>
154
+ useMutationObserver(wrapEl(el), callback, { attributes: true }),
155
+ );
156
+
157
+ act(() => {
158
+ result.current.stop();
159
+ });
160
+
161
+ await act(async () => {
162
+ el.setAttribute("data-test", "after-stop");
163
+ await flush();
164
+ });
165
+
166
+ expect(callback).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it("stop() can be called multiple times without error", () => {
170
+ const el = document.createElement("div");
171
+ const { result } = renderHook(() =>
172
+ useMutationObserver(wrapEl(el), vi.fn(), { attributes: true }),
173
+ );
174
+
175
+ expect(() => {
176
+ result.current.stop();
177
+ result.current.stop();
178
+ }).not.toThrow();
179
+ });
180
+
181
+ it("takeRecords() returns an array", () => {
182
+ const el = document.createElement("div");
183
+ const { result } = renderHook(() =>
184
+ useMutationObserver(wrapEl(el), vi.fn(), { attributes: true }),
185
+ );
186
+ expect(Array.isArray(result.current.takeRecords())).toBe(true);
187
+ });
188
+
189
+ it("takeRecords() returns empty array when no pending records", () => {
190
+ const el = document.createElement("div");
191
+ const { result } = renderHook(() =>
192
+ useMutationObserver(wrapEl(el), vi.fn(), { attributes: true }),
193
+ );
194
+ expect(result.current.takeRecords()).toHaveLength(0);
195
+ });
196
+
197
+ it("handles null target gracefully without throwing", () => {
198
+ expect(() => {
199
+ renderHook(() =>
200
+ useMutationObserver(null, vi.fn(), { attributes: true }),
201
+ );
202
+ }).not.toThrow();
203
+ });
204
+
205
+ it("cleans up observer on unmount", async () => {
206
+ const disconnectSpy = vi.spyOn(MutationObserver.prototype, "disconnect");
207
+ const el = document.createElement("div");
208
+ document.body.appendChild(el);
209
+
210
+ const { unmount } = renderHook(() =>
211
+ useMutationObserver(wrapEl(el), vi.fn(), { attributes: true }),
212
+ );
213
+
214
+ unmount();
215
+ await flush(); // useEffectOnce defers cleanup via queueMicrotask in test env
216
+
217
+ expect(disconnectSpy).toHaveBeenCalled();
218
+ disconnectSpy.mockRestore();
219
+ });
220
+
221
+ // ── isSupported: false ──────────────────────────────────────────────────────
222
+
223
+ it("isSupported is false and no observer is created when MutationObserver is unavailable", () => {
224
+ vi.stubGlobal("MutationObserver", undefined);
225
+
226
+ const el = document.createElement("div");
227
+ document.body.appendChild(el);
228
+ const callback = vi.fn();
229
+
230
+ const { result } = renderHook(() =>
231
+ useMutationObserver(wrapEl(el), callback, { attributes: true }),
232
+ );
233
+
234
+ expect(result.current.isSupported.get()).toBe(false);
235
+ el.setAttribute("data-x", "1");
236
+ expect(callback).not.toHaveBeenCalled();
237
+ });
238
+
239
+ // ── Observable / Ref$ target reactivity ─────────────────────────────────────
240
+
241
+ it("reacts to Observable<Element|null> target — starts observing after value is set", async () => {
242
+ const callback = vi.fn();
243
+ const target$ = observable<Element | null>(null);
244
+
245
+ renderHook(() =>
246
+ useMutationObserver(target$ as any, callback, { attributes: true }),
247
+ );
248
+
249
+ const el = document.createElement("div");
250
+ document.body.appendChild(el);
251
+
252
+ act(() => {
253
+ target$.set(el);
254
+ });
255
+
256
+ await act(async () => {
257
+ el.setAttribute("data-obs", "1");
258
+ await flush();
259
+ });
260
+
261
+ expect(callback).toHaveBeenCalled();
262
+ const records: MutationRecord[] = callback.mock.calls[0][0];
263
+ expect(records[0].type).toBe("attributes");
264
+ });
265
+
266
+ it("reacts to Ref$ target — starts observing after element is assigned", async () => {
267
+ const callback = vi.fn();
268
+
269
+ const { result } = renderHook(() => {
270
+ const el$ = useRef$<HTMLDivElement>();
271
+ const mo = useMutationObserver(el$ as any, callback, { attributes: true });
272
+ return { el$, mo };
273
+ });
274
+
275
+ const div = document.createElement("div");
276
+ document.body.appendChild(div);
277
+
278
+ act(() => result.current.el$(div));
279
+
280
+ await act(async () => {
281
+ div.setAttribute("data-el", "1");
282
+ await flush();
283
+ });
284
+
285
+ expect(callback).toHaveBeenCalled();
286
+ const records: MutationRecord[] = callback.mock.calls[0][0];
287
+ expect(records[0].type).toBe("attributes");
288
+ });
289
+
290
+ // ── subtree ─────────────────────────────────────────────────────────────────
291
+
292
+ it("detects mutation in a descendant node when subtree: true", async () => {
293
+ const callback = vi.fn();
294
+ const parent = document.createElement("div");
295
+ const child = document.createElement("span");
296
+ parent.appendChild(child);
297
+ document.body.appendChild(parent);
298
+
299
+ renderHook(() =>
300
+ useMutationObserver(wrapEl(parent), callback, { attributes: true, subtree: true }),
301
+ );
302
+
303
+ await act(async () => {
304
+ child.setAttribute("data-deep", "1");
305
+ await flush();
306
+ });
307
+
308
+ expect(callback).toHaveBeenCalled();
309
+ const records: MutationRecord[] = callback.mock.calls[0][0];
310
+ expect(records[0].target).toBe(child);
311
+ expect(records[0].type).toBe("attributes");
312
+ });
313
+
314
+ // ── attributeFilter ─────────────────────────────────────────────────────────
315
+
316
+ it("fires only for attributes listed in attributeFilter", async () => {
317
+ const callback = vi.fn();
318
+ const el = document.createElement("div");
319
+ document.body.appendChild(el);
320
+
321
+ renderHook(() =>
322
+ useMutationObserver(wrapEl(el), callback, {
323
+ attributes: true,
324
+ attributeFilter: ["data-allowed"],
325
+ }),
326
+ );
327
+
328
+ await act(async () => {
329
+ el.setAttribute("data-blocked", "x");
330
+ await flush();
331
+ });
332
+ expect(callback).not.toHaveBeenCalled();
333
+
334
+ await act(async () => {
335
+ el.setAttribute("data-allowed", "y");
336
+ await flush();
337
+ });
338
+ expect(callback).toHaveBeenCalledTimes(1);
339
+ });
340
+
341
+ // ── attributeOldValue ───────────────────────────────────────────────────────
342
+
343
+ it("includes oldValue in record when attributeOldValue: true", async () => {
344
+ const callback = vi.fn();
345
+ const el = document.createElement("div");
346
+ el.setAttribute("data-val", "before");
347
+ document.body.appendChild(el);
348
+
349
+ renderHook(() =>
350
+ useMutationObserver(wrapEl(el), callback, {
351
+ attributes: true,
352
+ attributeOldValue: true,
353
+ }),
354
+ );
355
+
356
+ await act(async () => {
357
+ el.setAttribute("data-val", "after");
358
+ await flush();
359
+ });
360
+
361
+ expect(callback).toHaveBeenCalled();
362
+ const records: MutationRecord[] = callback.mock.calls[0][0];
363
+ expect(records[0].oldValue).toBe("before");
364
+ });
365
+
366
+ // ── resume ──────────────────────────────────────────────────────────────────
367
+
368
+ it("resume() restarts observation after stop()", async () => {
369
+ const callback = vi.fn();
370
+ const el = document.createElement("div");
371
+ document.body.appendChild(el);
372
+
373
+ const { result } = renderHook(() =>
374
+ useMutationObserver(wrapEl(el), callback, { attributes: true }),
375
+ );
376
+
377
+ act(() => result.current.stop());
378
+
379
+ await act(async () => {
380
+ el.setAttribute("data-after-stop", "1");
381
+ await flush();
382
+ });
383
+ expect(callback).not.toHaveBeenCalled();
384
+
385
+ act(() => result.current.resume());
386
+
387
+ await act(async () => {
388
+ el.setAttribute("data-after-resume", "1");
389
+ await flush();
390
+ });
391
+ expect(callback).toHaveBeenCalledOnce();
392
+ const records: MutationRecord[] = callback.mock.calls[0][0];
393
+ expect(records[0].attributeName).toBe("data-after-resume");
394
+ });
395
+
396
+ // ── stale callback regression ───────────────────────────────────────────────
397
+
398
+ it("uses stale callback after re-render — regression guard: no callbackRef pattern", async () => {
399
+ const el = document.createElement("div");
400
+ document.body.appendChild(el);
401
+ const cb1 = vi.fn();
402
+ const cb2 = vi.fn();
403
+
404
+ const { rerender } = renderHook(
405
+ ({ cb }) => useMutationObserver(wrapEl(el), cb, { attributes: true }),
406
+ { initialProps: { cb: cb1 } },
407
+ );
408
+
409
+ rerender({ cb: cb2 });
410
+
411
+ await act(async () => {
412
+ el.setAttribute("data-stale", "1");
413
+ await flush();
414
+ });
415
+
416
+ // Observer was created with cb1 and is not recreated on re-render alone.
417
+ // This test documents the current behavior — absence of callbackRef pattern.
418
+ expect(cb1).toHaveBeenCalledOnce();
419
+ expect(cb2).not.toHaveBeenCalled();
420
+ });
421
+ });
@@ -0,0 +1,66 @@
1
+ import type { Observable } from "@legendapp/state";
2
+ import { useObservable, useMount, useObserve } from "@legendapp/state/react";
3
+ import { useRef } from "react";
4
+ import type { MaybeElement } from "../useRef$";
5
+ import { normalizeTargets } from "../useResizeObserver";
6
+
7
+ export interface UseMutationObserverOptions extends MutationObserverInit {}
8
+
9
+ export interface UseMutationObserverReturn {
10
+ isSupported: Observable<boolean>;
11
+ stop: () => void;
12
+ resume: () => void;
13
+ takeRecords: () => MutationRecord[];
14
+ }
15
+
16
+ export function useMutationObserver(
17
+ target: MaybeElement | MaybeElement[],
18
+ callback: MutationCallback,
19
+ options?: UseMutationObserverOptions,
20
+ ): UseMutationObserverReturn {
21
+ const isSupported$ = useObservable<boolean>(
22
+ typeof MutationObserver !== "undefined",
23
+ );
24
+ const observerRef = useRef<MutationObserver | null>(null);
25
+ const isMounted = useRef(false);
26
+
27
+ const cleanup = () => {
28
+ observerRef.current?.disconnect();
29
+ observerRef.current = null;
30
+ };
31
+
32
+ const setup = () => {
33
+ if (!isSupported$.peek() || !isMounted.current) return;
34
+ cleanup();
35
+
36
+ const targets = normalizeTargets(target);
37
+ const uniqueTargets = [...new Set(targets)];
38
+
39
+ if (!uniqueTargets.length) return;
40
+
41
+ observerRef.current = new MutationObserver(callback);
42
+ uniqueTargets.forEach((el) => {
43
+ observerRef.current!.observe(el, options);
44
+ });
45
+ };
46
+
47
+ useMount(() => {
48
+ isMounted.current = true;
49
+ setup();
50
+ return () => {
51
+ isMounted.current = false;
52
+ cleanup();
53
+ };
54
+ });
55
+
56
+ useObserve(() => {
57
+ normalizeTargets(target);
58
+ setup();
59
+ });
60
+
61
+ const takeRecords = (): MutationRecord[] => {
62
+ return observerRef.current?.takeRecords() ?? [];
63
+ };
64
+
65
+ return { isSupported: isSupported$, stop: cleanup, resume: setup, takeRecords };
66
+ }
@@ -0,0 +1,120 @@
1
+ import { ObservableHint } from "@legendapp/state";
2
+ import type { OpaqueObject } from "@legendapp/state";
3
+ import { Computed, useObservable } from "@legendapp/state/react";
4
+ import { useEventListener } from "../../browser/useEventListener";
5
+ import { useParentElement } from ".";
6
+
7
+ // HARD CODE OFFSET
8
+ const OFFSET_Y = -15;
9
+
10
+ function overlayStyle(
11
+ rect: DOMRect | null,
12
+ color: string,
13
+ ): React.CSSProperties {
14
+ if (!rect || rect.width === 0) return { display: "none" };
15
+ return {
16
+ position: "fixed",
17
+ pointerEvents: "none",
18
+ zIndex: 9999,
19
+ width: `${rect.width}px`,
20
+ height: `${rect.height}px`,
21
+ left: `${rect.left}px`,
22
+ top: `${rect.top + OFFSET_Y}px`,
23
+ backgroundColor: color,
24
+ outline: `1px solid ${color.replace("28", "99")}`,
25
+ transition: "all 0.05s linear",
26
+ };
27
+ }
28
+
29
+ export default function UseParentElementDemo() {
30
+ const element$ = useObservable<OpaqueObject<Element> | null>(null);
31
+ const parent$ = useParentElement(element$ as any);
32
+
33
+ useEventListener(
34
+ "mousemove",
35
+ (e: MouseEvent) => {
36
+ const el = document.elementFromPoint(e.clientX, e.clientY);
37
+ element$.set(el ? ObservableHint.opaque(el) : null);
38
+ },
39
+ { passive: true },
40
+ );
41
+
42
+ useEventListener(
43
+ "scroll",
44
+ () => {
45
+ const el = element$.peek();
46
+ element$.set(null);
47
+ element$.set(el);
48
+ },
49
+ { passive: true, capture: true },
50
+ );
51
+
52
+ return (
53
+ <div
54
+ style={{
55
+ fontFamily: "monospace",
56
+ fontSize: "13px",
57
+ display: "flex",
58
+ flexDirection: "column",
59
+ gap: "4px",
60
+ padding: "4px 0",
61
+ }}
62
+ >
63
+ <p
64
+ style={{
65
+ margin: 0,
66
+ color: "var(--sl-color-gray-3, #94a3b8)",
67
+ fontSize: "12px",
68
+ }}
69
+ >
70
+ Move your mouse over the page to highlight elements.
71
+ </p>
72
+
73
+ <Computed>
74
+ {() => {
75
+ const el = element$.get() as HTMLElement | null;
76
+ const parent = parent$.get() as HTMLElement | null;
77
+ return (
78
+ <div style={{ display: "flex", gap: "24px" }}>
79
+ <span>
80
+ current:{" "}
81
+ <strong style={{ color: "#a5a5a5" }}>
82
+ {el ? `<${el.tagName.toLowerCase()}>` : "—"}
83
+ </strong>
84
+ </span>
85
+ <span>
86
+ parent:{" "}
87
+ <strong style={{ color: "#3eaf7c" }}>
88
+ {parent ? `<${parent.tagName.toLowerCase()}>` : "—"}
89
+ </strong>
90
+ </span>
91
+ </div>
92
+ );
93
+ }}
94
+ </Computed>
95
+
96
+ <Computed>
97
+ {() => {
98
+ const el = element$.get() as HTMLElement | null;
99
+ const parent = parent$.get() as HTMLElement | null;
100
+ return (
101
+ <>
102
+ <div
103
+ style={overlayStyle(
104
+ el?.getBoundingClientRect() ?? null,
105
+ "#a5a5a528",
106
+ )}
107
+ />
108
+ <div
109
+ style={overlayStyle(
110
+ parent?.getBoundingClientRect() ?? null,
111
+ "#3eaf7c28",
112
+ )}
113
+ />
114
+ </>
115
+ );
116
+ }}
117
+ </Computed>
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: useParentElement
3
+ category: elements
4
+ ---
5
+
6
+ Returns the `parentElement` of a target DOM node as a reactive `Observable`.
7
+ Re-evaluates whenever the target `Ref$` or `Observable` changes.
8
+ Targets can be `Ref$`, `Observable<Element|null>`, or a plain `Element`.
9
+
10
+ ## Demo
11
+
12
+ ## Usage
13
+
14
+ ```tsx twoslash
15
+ // @noErrors
16
+ import { useRef$, useParentElement } from '@usels/core'
17
+
18
+ function Component() {
19
+ const el$ = useRef$<HTMLDivElement>()
20
+ const parent$ = useParentElement(el$)
21
+
22
+ return <div ref={el$} />
23
+ }
24
+ ```
25
+
26
+ ### Reading the parent element
27
+
28
+ Use inside an `observer` component to reactively render based on the parent:
29
+
30
+ ```tsx twoslash
31
+ // @noErrors
32
+ import { useRef$, useParentElement } from '@usels/core'
33
+ import { observer } from '@legendapp/state/react'
34
+
35
+ const Component = observer(() => {
36
+ const el$ = useRef$<HTMLDivElement>()
37
+ const parent$ = useParentElement(el$)
38
+
39
+ return <div ref={el$}>{parent$.get()?.tagName}</div>
40
+ })
41
+ ```
42
+
43
+ ### With an Observable target
44
+
45
+ ```tsx twoslash
46
+ // @noErrors
47
+ import { observable } from '@legendapp/state'
48
+ import { useParentElement } from '@usels/core'
49
+
50
+ function Component() {
51
+ const target$ = observable<HTMLElement | null>(null)
52
+ const parent$ = useParentElement(target$)
53
+ // ...
54
+ }
55
+ ```
56
+
57
+ ### With a plain element
58
+
59
+ ```tsx twoslash
60
+ // @noErrors
61
+ import { useParentElement } from '@usels/core'
62
+
63
+ function Component({ el }: { el: HTMLElement }) {
64
+ const parent$ = useParentElement(el)
65
+ // ...
66
+ }
67
+ ```