@usels/core 0.0.1-beta.3

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