@ably/ui 17.11.0-dev.4c4b6b55 → 17.11.0-dev.672950e8

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/core/Expander.js CHANGED
@@ -1,2 +1,2 @@
1
- import React,{useEffect,useRef,useState}from"react";import*as RadixCollapsible from"@radix-ui/react-collapsible";import{throttle}from"es-toolkit/compat";import cn from"./utils/cn";const Expander=({heightThreshold=200,className,fadeClassName,controlsClassName,controlsOpenedLabel,controlsClosedLabel,children})=>{const innerRef=useRef(null);const[showControls,setShowControls]=useState(false);const[contentHeight,setContentHeight]=useState(heightThreshold);const[expanded,setExpanded]=useState(false);useEffect(()=>{if(innerRef.current){setContentHeight(innerRef.current.clientHeight)}setShowControls(contentHeight>=heightThreshold)},[contentHeight,heightThreshold]);useEffect(()=>{const onResize=throttle(()=>{if(innerRef.current){setContentHeight(innerRef.current.clientHeight)}},250);window.addEventListener("resize",onResize);return()=>{window.removeEventListener("resize",onResize)}},[]);const height=contentHeight<heightThreshold?"auto":expanded?contentHeight:heightThreshold;return React.createElement(RadixCollapsible.Root,{open:expanded,onOpenChange:setExpanded},React.createElement("div",{style:{height},"data-testid":"expander-container",className:cn("overflow-hidden transition-all relative",className)},showControls&&!expanded&&React.createElement("div",{className:cn("h-16 w-full bg-gradient-to-t from-white to-transparent absolute bottom-0 left-0 right-0",fadeClassName)}),React.createElement("div",{ref:innerRef},children)),showControls&&React.createElement(RadixCollapsible.Trigger,{asChild:true},React.createElement("button",{"data-testid":"expander-controls",className:cn(heightThreshold===0&&!expanded?"":"mt-4","cursor-pointer font-bold text-gui-blue-default-light hover:text-gui-blue-hover-light",controlsClassName)},expanded?controlsOpenedLabel??"View less -":controlsClosedLabel??"View all +")))};export default Expander;
1
+ import React,{useMemo,useRef,useState}from"react";import*as RadixCollapsible from"@radix-ui/react-collapsible";import cn from"./utils/cn";import{useContentHeight}from"./hooks/use-content-height";const Expander=({heightThreshold=200,className,fadeClassName,controlsClassName,controlsOpenedLabel,controlsClosedLabel,children})=>{const innerRef=useRef(null);const[expanded,setExpanded]=useState(false);const contentHeight=useContentHeight(innerRef,heightThreshold);const showControls=useMemo(()=>contentHeight>=heightThreshold,[contentHeight,heightThreshold]);const height=useMemo(()=>contentHeight<heightThreshold?"auto":expanded?contentHeight:heightThreshold,[contentHeight,heightThreshold,expanded]);return React.createElement(RadixCollapsible.Root,{open:expanded,onOpenChange:setExpanded},React.createElement("div",{style:{height},"data-testid":"expander-container",className:cn("overflow-hidden transition-all relative",className)},showControls&&!expanded&&React.createElement("div",{className:cn("h-16 w-full bg-gradient-to-t from-white to-transparent absolute bottom-0 left-0 right-0",fadeClassName)}),React.createElement("div",{ref:innerRef},children)),showControls&&React.createElement(RadixCollapsible.Trigger,{asChild:true},React.createElement("button",{"data-testid":"expander-controls",className:cn(heightThreshold===0&&!expanded?"":"mt-4","cursor-pointer font-bold text-gui-blue-default-light hover:text-gui-blue-hover-light",controlsClassName)},expanded?controlsOpenedLabel??"View less -":controlsClosedLabel??"View all +")))};export default Expander;
2
2
  //# sourceMappingURL=Expander.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/Expander.tsx"],"sourcesContent":["import React, {\n PropsWithChildren,\n ReactNode,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport * as RadixCollapsible from \"@radix-ui/react-collapsible\";\nimport { throttle } from \"es-toolkit/compat\";\nimport cn from \"./utils/cn\";\n\ntype ExpanderProps = {\n heightThreshold?: number;\n className?: string;\n fadeClassName?: string;\n controlsClassName?: string;\n controlsOpenedLabel?: string | ReactNode;\n controlsClosedLabel?: string | ReactNode;\n};\n\nconst Expander = ({\n heightThreshold = 200,\n className,\n fadeClassName,\n controlsClassName,\n controlsOpenedLabel,\n controlsClosedLabel,\n children,\n}: PropsWithChildren<ExpanderProps>) => {\n const innerRef = useRef<HTMLDivElement>(null);\n const [showControls, setShowControls] = useState(false);\n const [contentHeight, setContentHeight] = useState<number>(heightThreshold);\n const [expanded, setExpanded] = useState(false);\n\n useEffect(() => {\n if (innerRef.current) {\n setContentHeight(innerRef.current.clientHeight);\n }\n\n setShowControls(contentHeight >= heightThreshold);\n }, [contentHeight, heightThreshold]);\n\n useEffect(() => {\n const onResize = throttle(() => {\n if (innerRef.current) {\n setContentHeight(innerRef.current.clientHeight);\n }\n }, 250);\n\n window.addEventListener(\"resize\", onResize);\n return () => {\n window.removeEventListener(\"resize\", onResize);\n };\n }, []);\n\n const height =\n contentHeight < heightThreshold\n ? \"auto\"\n : expanded\n ? contentHeight\n : heightThreshold;\n\n return (\n <RadixCollapsible.Root open={expanded} onOpenChange={setExpanded}>\n <div\n style={{ height }}\n data-testid=\"expander-container\"\n className={cn(\"overflow-hidden transition-all relative\", className)}\n >\n {showControls && !expanded && (\n <div\n className={cn(\n \"h-16 w-full bg-gradient-to-t from-white to-transparent absolute bottom-0 left-0 right-0\",\n fadeClassName,\n )}\n ></div>\n )}\n <div ref={innerRef}>{children}</div>\n </div>\n {showControls && (\n <RadixCollapsible.Trigger asChild>\n <button\n data-testid=\"expander-controls\"\n className={cn(\n heightThreshold === 0 && !expanded ? \"\" : \"mt-4\",\n \"cursor-pointer font-bold text-gui-blue-default-light hover:text-gui-blue-hover-light\",\n controlsClassName,\n )}\n >\n {expanded\n ? (controlsOpenedLabel ?? \"View less -\")\n : (controlsClosedLabel ?? \"View all +\")}\n </button>\n </RadixCollapsible.Trigger>\n )}\n </RadixCollapsible.Root>\n );\n};\n\nexport default Expander;\n"],"names":["React","useEffect","useRef","useState","RadixCollapsible","throttle","cn","Expander","heightThreshold","className","fadeClassName","controlsClassName","controlsOpenedLabel","controlsClosedLabel","children","innerRef","showControls","setShowControls","contentHeight","setContentHeight","expanded","setExpanded","current","clientHeight","onResize","window","addEventListener","removeEventListener","height","Root","open","onOpenChange","div","style","data-testid","ref","Trigger","asChild","button"],"mappings":"AAAA,OAAOA,OAGLC,SAAS,CACTC,MAAM,CACNC,QAAQ,KACH,OAAQ,AACf,WAAYC,qBAAsB,6BAA8B,AAChE,QAASC,QAAQ,KAAQ,mBAAoB,AAC7C,QAAOC,OAAQ,YAAa,CAW5B,MAAMC,SAAW,CAAC,CAChBC,gBAAkB,GAAG,CACrBC,SAAS,CACTC,aAAa,CACbC,iBAAiB,CACjBC,mBAAmB,CACnBC,mBAAmB,CACnBC,QAAQ,CACyB,IACjC,MAAMC,SAAWb,OAAuB,MACxC,KAAM,CAACc,aAAcC,gBAAgB,CAAGd,SAAS,OACjD,KAAM,CAACe,cAAeC,iBAAiB,CAAGhB,SAAiBK,iBAC3D,KAAM,CAACY,SAAUC,YAAY,CAAGlB,SAAS,OAEzCF,UAAU,KACR,GAAIc,SAASO,OAAO,CAAE,CACpBH,iBAAiBJ,SAASO,OAAO,CAACC,YAAY,CAChD,CAEAN,gBAAgBC,eAAiBV,gBACnC,EAAG,CAACU,cAAeV,gBAAgB,EAEnCP,UAAU,KACR,MAAMuB,SAAWnB,SAAS,KACxB,GAAIU,SAASO,OAAO,CAAE,CACpBH,iBAAiBJ,SAASO,OAAO,CAACC,YAAY,CAChD,CACF,EAAG,KAEHE,OAAOC,gBAAgB,CAAC,SAAUF,UAClC,MAAO,KACLC,OAAOE,mBAAmB,CAAC,SAAUH,SACvC,CACF,EAAG,EAAE,EAEL,MAAMI,OACJV,cAAgBV,gBACZ,OACAY,SACEF,cACAV,gBAER,OACE,oBAACJ,iBAAiByB,IAAI,EAACC,KAAMV,SAAUW,aAAcV,aACnD,oBAACW,OACCC,MAAO,CAAEL,MAAO,EAChBM,cAAY,qBACZzB,UAAWH,GAAG,0CAA2CG,YAExDO,cAAgB,CAACI,UAChB,oBAACY,OACCvB,UAAWH,GACT,0FACAI,iBAIN,oBAACsB,OAAIG,IAAKpB,UAAWD,WAEtBE,cACC,oBAACZ,iBAAiBgC,OAAO,EAACC,QAAAA,MACxB,oBAACC,UACCJ,cAAY,oBACZzB,UAAWH,GACTE,kBAAoB,GAAK,CAACY,SAAW,GAAK,OAC1C,uFACAT,oBAGDS,SACIR,qBAAuB,cACvBC,qBAAuB,eAMxC,CAEA,gBAAeN,QAAS"}
1
+ {"version":3,"sources":["../../src/core/Expander.tsx"],"sourcesContent":["import React, {\n PropsWithChildren,\n ReactNode,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport * as RadixCollapsible from \"@radix-ui/react-collapsible\";\nimport cn from \"./utils/cn\";\nimport { useContentHeight } from \"./hooks/use-content-height\";\n\ntype ExpanderProps = {\n heightThreshold?: number;\n className?: string;\n fadeClassName?: string;\n controlsClassName?: string;\n controlsOpenedLabel?: string | ReactNode;\n controlsClosedLabel?: string | ReactNode;\n};\n\nconst Expander = ({\n heightThreshold = 200,\n className,\n fadeClassName,\n controlsClassName,\n controlsOpenedLabel,\n controlsClosedLabel,\n children,\n}: PropsWithChildren<ExpanderProps>) => {\n const innerRef = useRef<HTMLDivElement>(null);\n const [expanded, setExpanded] = useState(false);\n\n const contentHeight = useContentHeight(innerRef, heightThreshold);\n\n const showControls = useMemo(\n () => contentHeight >= heightThreshold,\n [contentHeight, heightThreshold]\n );\n\n const height = useMemo(\n () =>\n contentHeight < heightThreshold\n ? \"auto\"\n : expanded\n ? contentHeight\n : heightThreshold,\n [contentHeight, heightThreshold, expanded]\n );\n\n return (\n <RadixCollapsible.Root open={expanded} onOpenChange={setExpanded}>\n <div\n style={{ height }}\n data-testid=\"expander-container\"\n className={cn(\"overflow-hidden transition-all relative\", className)}\n >\n {showControls && !expanded && (\n <div\n className={cn(\n \"h-16 w-full bg-gradient-to-t from-white to-transparent absolute bottom-0 left-0 right-0\",\n fadeClassName,\n )}\n ></div>\n )}\n <div ref={innerRef}>{children}</div>\n </div>\n {showControls && (\n <RadixCollapsible.Trigger asChild>\n <button\n data-testid=\"expander-controls\"\n className={cn(\n heightThreshold === 0 && !expanded ? \"\" : \"mt-4\",\n \"cursor-pointer font-bold text-gui-blue-default-light hover:text-gui-blue-hover-light\",\n controlsClassName,\n )}\n >\n {expanded\n ? (controlsOpenedLabel ?? \"View less -\")\n : (controlsClosedLabel ?? \"View all +\")}\n </button>\n </RadixCollapsible.Trigger>\n )}\n </RadixCollapsible.Root>\n );\n};\n\nexport default Expander;\n"],"names":["React","useMemo","useRef","useState","RadixCollapsible","cn","useContentHeight","Expander","heightThreshold","className","fadeClassName","controlsClassName","controlsOpenedLabel","controlsClosedLabel","children","innerRef","expanded","setExpanded","contentHeight","showControls","height","Root","open","onOpenChange","div","style","data-testid","ref","Trigger","asChild","button"],"mappings":"AAAA,OAAOA,OAGLC,OAAO,CACPC,MAAM,CACNC,QAAQ,KACH,OAAQ,AACf,WAAYC,qBAAsB,6BAA8B,AAChE,QAAOC,OAAQ,YAAa,AAC5B,QAASC,gBAAgB,KAAQ,4BAA6B,CAW9D,MAAMC,SAAW,CAAC,CAChBC,gBAAkB,GAAG,CACrBC,SAAS,CACTC,aAAa,CACbC,iBAAiB,CACjBC,mBAAmB,CACnBC,mBAAmB,CACnBC,QAAQ,CACyB,IACjC,MAAMC,SAAWb,OAAuB,MACxC,KAAM,CAACc,SAAUC,YAAY,CAAGd,SAAS,OAEzC,MAAMe,cAAgBZ,iBAAiBS,SAAUP,iBAEjD,MAAMW,aAAelB,QACnB,IAAMiB,eAAiBV,gBACvB,CAACU,cAAeV,gBAAgB,EAGlC,MAAMY,OAASnB,QACb,IACEiB,cAAgBV,gBACZ,OACAQ,SACEE,cACAV,gBACR,CAACU,cAAeV,gBAAiBQ,SAAS,EAG5C,OACE,oBAACZ,iBAAiBiB,IAAI,EAACC,KAAMN,SAAUO,aAAcN,aACnD,oBAACO,OACCC,MAAO,CAAEL,MAAO,EAChBM,cAAY,qBACZjB,UAAWJ,GAAG,0CAA2CI,YAExDU,cAAgB,CAACH,UAChB,oBAACQ,OACCf,UAAWJ,GACT,0FACAK,iBAIN,oBAACc,OAAIG,IAAKZ,UAAWD,WAEtBK,cACC,oBAACf,iBAAiBwB,OAAO,EAACC,QAAAA,MACxB,oBAACC,UACCJ,cAAY,oBACZjB,UAAWJ,GACTG,kBAAoB,GAAK,CAACQ,SAAW,GAAK,OAC1C,uFACAL,oBAGDK,SACIJ,qBAAuB,cACvBC,qBAAuB,eAMxC,CAEA,gBAAeN,QAAS"}
@@ -0,0 +1,2 @@
1
+ import{useState,useEffect,useRef}from"react";export function useContentHeight(ref,initialHeight=0){const[contentHeight,setContentHeight]=useState(initialHeight);const observerRef=useRef(null);useEffect(()=>{const element=ref.current;if(!element){return}observerRef.current=new ResizeObserver(entries=>{requestAnimationFrame(()=>{const entry=entries[0];if(entry&&entry.contentRect){const newHeight=Math.round(entry.contentRect.height);setContentHeight(newHeight)}})});observerRef.current.observe(element);return()=>{observerRef.current?.disconnect();observerRef.current=null}},[ref]);return contentHeight}
2
+ //# sourceMappingURL=use-content-height.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/hooks/use-content-height.ts"],"sourcesContent":["import { useState, useEffect, useRef, RefObject } from \"react\";\n\n/**\n * Custom hook that tracks the content height of an element using ResizeObserver.\n * This eliminates forced reflows by using the browser's native resize observation API\n * instead of synchronous clientHeight/getBoundingClientRect queries.\n *\n * @param ref - React ref to the element to observe\n * @param initialHeight - Initial height value (default: 0)\n * @returns Current content height in pixels\n */\nexport function useContentHeight(\n ref: RefObject<HTMLElement>,\n initialHeight = 0\n): number {\n const [contentHeight, setContentHeight] = useState<number>(initialHeight);\n const observerRef = useRef<ResizeObserver | null>(null);\n\n useEffect(() => {\n const element = ref.current;\n\n if (!element) {\n return;\n }\n\n observerRef.current = new ResizeObserver((entries) => {\n requestAnimationFrame(() => {\n const entry = entries[0];\n if (entry && entry.contentRect) {\n const newHeight = Math.round(entry.contentRect.height);\n setContentHeight(newHeight);\n }\n });\n });\n\n observerRef.current.observe(element);\n\n return () => {\n observerRef.current?.disconnect();\n observerRef.current = null;\n };\n }, [ref]);\n\n return contentHeight;\n}\n"],"names":["useState","useEffect","useRef","useContentHeight","ref","initialHeight","contentHeight","setContentHeight","observerRef","element","current","ResizeObserver","entries","requestAnimationFrame","entry","contentRect","newHeight","Math","round","height","observe","disconnect"],"mappings":"AAAA,OAASA,QAAQ,CAAEC,SAAS,CAAEC,MAAM,KAAmB,OAAQ,AAW/D,QAAO,SAASC,iBACdC,GAA2B,CAC3BC,cAAgB,CAAC,EAEjB,KAAM,CAACC,cAAeC,iBAAiB,CAAGP,SAAiBK,eAC3D,MAAMG,YAAcN,OAA8B,MAElDD,UAAU,KACR,MAAMQ,QAAUL,IAAIM,OAAO,CAE3B,GAAI,CAACD,QAAS,CACZ,MACF,CAEAD,YAAYE,OAAO,CAAG,IAAIC,eAAe,AAACC,UACxCC,sBAAsB,KACpB,MAAMC,MAAQF,OAAO,CAAC,EAAE,CACxB,GAAIE,OAASA,MAAMC,WAAW,CAAE,CAC9B,MAAMC,UAAYC,KAAKC,KAAK,CAACJ,MAAMC,WAAW,CAACI,MAAM,EACrDZ,iBAAiBS,UACnB,CACF,EACF,GAEAR,YAAYE,OAAO,CAACU,OAAO,CAACX,SAE5B,MAAO,KACLD,YAAYE,OAAO,EAAEW,YACrBb,CAAAA,YAAYE,OAAO,CAAG,IACxB,CACF,EAAG,CAACN,IAAI,EAER,OAAOE,aACT"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/core/hooks/use-themed-scrollpoints.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n */\n\nimport { describe, it, expect, beforeEach, afterEach, vi } from \"vitest\";\nimport { renderHook, waitFor } from \"@testing-library/react\";\nimport { useThemedScrollpoints } from \"./use-themed-scrollpoints\";\n\ndescribe(\"useThemedScrollpoints\", () => {\n let observerCallback: IntersectionObserverCallback;\n let mockObserve: ReturnType<typeof vi.fn>;\n let mockUnobserve: ReturnType<typeof vi.fn>;\n let mockDisconnect: ReturnType<typeof vi.fn>;\n\n beforeEach(() => {\n // Mock IntersectionObserver\n mockObserve = vi.fn();\n mockUnobserve = vi.fn();\n mockDisconnect = vi.fn();\n\n global.IntersectionObserver = vi.fn((callback) => {\n observerCallback = callback;\n return {\n observe: mockObserve,\n unobserve: mockUnobserve,\n disconnect: mockDisconnect,\n };\n }) as any;\n\n // Mock requestAnimationFrame\n global.requestAnimationFrame = vi.fn((cb) => {\n cb(0);\n return 0;\n });\n });\n\n afterEach(() => {\n vi.clearAllMocks();\n document.body.innerHTML = \"\";\n });\n\n it(\"returns first scrollpoint className on mount\", () => {\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(result.current).toBe(\"theme-light\");\n });\n\n it(\"returns empty string when no scrollpoints provided\", () => {\n const { result } = renderHook(() => useThemedScrollpoints([]));\n expect(result.current).toBe(\"\");\n });\n\n it(\"does not create IntersectionObserver when no scrollpoints provided\", () => {\n renderHook(() => useThemedScrollpoints([]));\n\n expect(IntersectionObserver).not.toHaveBeenCalled();\n });\n\n it(\"creates IntersectionObserver with correct config\", () => {\n const scrollpoints = [{ id: \"zone1\", className: \"theme-light\" }];\n\n // Create DOM element\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(IntersectionObserver).toHaveBeenCalledWith(\n expect.any(Function),\n expect.objectContaining({\n rootMargin: \"-64px 0px 0px 0px\",\n threshold: 0,\n })\n );\n });\n\n it(\"observes all scrollpoint elements that exist in DOM\", () => {\n // Create DOM elements\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(mockObserve).toHaveBeenCalledTimes(2);\n expect(mockObserve).toHaveBeenCalledWith(elem1);\n expect(mockObserve).toHaveBeenCalledWith(elem2);\n });\n\n it(\"logs warning for missing DOM elements\", () => {\n const consoleWarn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n const scrollpoints = [{ id: \"non-existent\", className: \"theme\" }];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(consoleWarn).toHaveBeenCalledWith(\n expect.stringContaining('Element with id \"non-existent\" not found')\n );\n\n consoleWarn.mockRestore();\n });\n\n it(\"observes existing elements and warns about missing ones\", () => {\n const consoleWarn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n // Create only one element\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"missing\", className: \"theme-dark\" },\n ];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(mockObserve).toHaveBeenCalledTimes(1);\n expect(mockObserve).toHaveBeenCalledWith(elem);\n expect(consoleWarn).toHaveBeenCalledWith(\n expect.stringContaining('Element with id \"missing\" not found')\n );\n\n consoleWarn.mockRestore();\n });\n\n it(\"updates className when intersection occurs\", async () => {\n // Create DOM elements\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n // Initial state\n expect(result.current).toBe(\"theme-light\");\n\n // Simulate intersection with zone2\n observerCallback(\n [\n {\n target: elem2,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n await waitFor(() => {\n expect(result.current).toBe(\"theme-dark\");\n });\n });\n\n it(\"does not update className if same scrollpoint intersects again\", async () => {\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [{ id: \"zone1\", className: \"theme-light\" }];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(result.current).toBe(\"theme-light\");\n\n // Simulate intersection with same element\n const renderCount = result.current;\n observerCallback(\n [\n {\n target: elem,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n // Should not trigger re-render (className unchanged)\n expect(result.current).toBe(renderCount);\n });\n\n it(\"ignores non-intersecting entries\", async () => {\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(result.current).toBe(\"theme-light\");\n\n // Simulate non-intersecting entry\n observerCallback(\n [\n {\n target: elem2,\n isIntersecting: false,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n // Should remain unchanged\n expect(result.current).toBe(\"theme-light\");\n });\n\n it(\"uses first intersecting entry when multiple entries intersect\", async () => {\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n const elem3 = document.createElement(\"div\");\n elem3.id = \"zone3\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n document.body.appendChild(elem3);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n { id: \"zone3\", className: \"theme-blue\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n // Simulate multiple intersections (zone2 and zone3 both intersecting)\n observerCallback(\n [\n {\n target: elem2,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n {\n target: elem3,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n await waitFor(() => {\n // Should use first intersecting entry (zone2)\n expect(result.current).toBe(\"theme-dark\");\n });\n });\n\n it(\"disconnects observer on unmount\", () => {\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [{ id: \"zone1\", className: \"theme-light\" }];\n\n const { unmount } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n unmount();\n\n expect(mockDisconnect).toHaveBeenCalled();\n });\n\n it(\"recreates observer when scrollpoints change\", () => {\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const { rerender } = renderHook(\n ({ scrollpoints }) => useThemedScrollpoints(scrollpoints),\n {\n initialProps: {\n scrollpoints: [{ id: \"zone1\", className: \"theme-light\" }],\n },\n }\n );\n\n expect(IntersectionObserver).toHaveBeenCalledTimes(1);\n expect(mockObserve).toHaveBeenCalledTimes(1);\n\n // Change scrollpoints\n rerender({\n scrollpoints: [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ],\n });\n\n // Should disconnect old observer and create new one\n expect(mockDisconnect).toHaveBeenCalledTimes(1);\n expect(IntersectionObserver).toHaveBeenCalledTimes(2);\n expect(mockObserve).toHaveBeenCalledTimes(3); // 1 from first render + 2 from second\n });\n\n it(\"uses requestAnimationFrame for state updates\", () => {\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n const rafSpy = vi.spyOn(global, \"requestAnimationFrame\");\n\n // Simulate intersection\n observerCallback(\n [\n {\n target: elem,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n expect(rafSpy).toHaveBeenCalled();\n\n rafSpy.mockRestore();\n });\n});\n"],"names":["describe","it","expect","beforeEach","afterEach","vi","renderHook","waitFor","useThemedScrollpoints","observerCallback","mockObserve","mockUnobserve","mockDisconnect","fn","global","IntersectionObserver","callback","observe","unobserve","disconnect","requestAnimationFrame","cb","clearAllMocks","document","body","innerHTML","scrollpoints","id","className","result","current","toBe","not","toHaveBeenCalled","elem","createElement","appendChild","toHaveBeenCalledWith","any","Function","objectContaining","rootMargin","threshold","elem1","elem2","toHaveBeenCalledTimes","consoleWarn","spyOn","console","mockImplementation","stringContaining","mockRestore","target","isIntersecting","renderCount","elem3","unmount","rerender","initialProps","rafSpy"],"mappings":"AAIA,OAASA,QAAQ,CAAEC,EAAE,CAAEC,MAAM,CAAEC,UAAU,CAAEC,SAAS,CAAEC,EAAE,KAAQ,QAAS,AACzE,QAASC,UAAU,CAAEC,OAAO,KAAQ,wBAAyB,AAC7D,QAASC,qBAAqB,KAAQ,2BAA4B,CAElER,SAAS,wBAAyB,KAChC,IAAIS,iBACJ,IAAIC,YACJ,IAAIC,cACJ,IAAIC,eAEJT,WAAW,KAETO,YAAcL,GAAGQ,EAAE,GACnBF,cAAgBN,GAAGQ,EAAE,GACrBD,eAAiBP,GAAGQ,EAAE,EAEtBC,CAAAA,OAAOC,oBAAoB,CAAGV,GAAGQ,EAAE,CAAC,AAACG,WACnCP,iBAAmBO,SACnB,MAAO,CACLC,QAASP,YACTQ,UAAWP,cACXQ,WAAYP,cACd,CACF,EAGAE,CAAAA,OAAOM,qBAAqB,CAAGf,GAAGQ,EAAE,CAAC,AAACQ,KACpCA,GAAG,GACH,OAAO,CACT,EACF,GAEAjB,UAAU,KACRC,GAAGiB,aAAa,EAChBC,CAAAA,SAASC,IAAI,CAACC,SAAS,CAAG,EAC5B,GAEAxB,GAAG,+CAAgD,KACjD,MAAMyB,aAAe,CACnB,CAAEC,GAAI,QAASC,UAAW,aAAc,EACxC,CAAED,GAAI,QAASC,UAAW,YAAa,EACxC,CAED,KAAM,CAAEC,MAAM,CAAE,CAAGvB,WAAW,IAAME,sBAAsBkB,eAE1DxB,OAAO2B,OAAOC,OAAO,EAAEC,IAAI,CAAC,cAC9B,GAEA9B,GAAG,qDAAsD,KACvD,KAAM,CAAE4B,MAAM,CAAE,CAAGvB,WAAW,IAAME,sBAAsB,EAAE,GAC5DN,OAAO2B,OAAOC,OAAO,EAAEC,IAAI,CAAC,GAC9B,GAEA9B,GAAG,qEAAsE,KACvEK,WAAW,IAAME,sBAAsB,EAAE,GAEzCN,OAAOa,sBAAsBiB,GAAG,CAACC,gBAAgB,EACnD,GAEAhC,GAAG,mDAAoD,KACrD,MAAMyB,aAAe,CAAC,CAAEC,GAAI,QAASC,UAAW,aAAc,EAAE,CAGhE,MAAMM,KAAOX,SAASY,aAAa,CAAC,MACpCD,CAAAA,KAAKP,EAAE,CAAG,QACVJ,SAASC,IAAI,CAACY,WAAW,CAACF,MAE1B5B,WAAW,IAAME,sBAAsBkB,eAEvCxB,OAAOa,sBAAsBsB,oBAAoB,CAC/CnC,OAAOoC,GAAG,CAACC,UACXrC,OAAOsC,gBAAgB,CAAC,CACtBC,WAAY,oBACZC,UAAW,CACb,GAEJ,GAEAzC,GAAG,sDAAuD,KAExD,MAAM0C,MAAQpB,SAASY,aAAa,CAAC,MACrCQ,CAAAA,MAAMhB,EAAE,CAAG,QACX,MAAMiB,MAAQrB,SAASY,aAAa,CAAC,MACrCS,CAAAA,MAAMjB,EAAE,CAAG,QACXJ,SAASC,IAAI,CAACY,WAAW,CAACO,OAC1BpB,SAASC,IAAI,CAACY,WAAW,CAACQ,OAE1B,MAAMlB,aAAe,CACnB,CAAEC,GAAI,QAASC,UAAW,aAAc,EACxC,CAAED,GAAI,QAASC,UAAW,YAAa,EACxC,CAEDtB,WAAW,IAAME,sBAAsBkB,eAEvCxB,OAAOQ,aAAamC,qBAAqB,CAAC,GAC1C3C,OAAOQ,aAAa2B,oBAAoB,CAACM,OACzCzC,OAAOQ,aAAa2B,oBAAoB,CAACO,MAC3C,GAEA3C,GAAG,wCAAyC,KAC1C,MAAM6C,YAAczC,GAAG0C,KAAK,CAACC,QAAS,QAAQC,kBAAkB,CAAC,KAAO,GAExE,MAAMvB,aAAe,CAAC,CAAEC,GAAI,eAAgBC,UAAW,OAAQ,EAAE,CAEjEtB,WAAW,IAAME,sBAAsBkB,eAEvCxB,OAAO4C,aAAaT,oBAAoB,CACtCnC,OAAOgD,gBAAgB,CAAC,6CAG1BJ,YAAYK,WAAW,EACzB,GAEAlD,GAAG,0DAA2D,KAC5D,MAAM6C,YAAczC,GAAG0C,KAAK,CAACC,QAAS,QAAQC,kBAAkB,CAAC,KAAO,GAGxE,MAAMf,KAAOX,SAASY,aAAa,CAAC,MACpCD,CAAAA,KAAKP,EAAE,CAAG,QACVJ,SAASC,IAAI,CAACY,WAAW,CAACF,MAE1B,MAAMR,aAAe,CACnB,CAAEC,GAAI,QAASC,UAAW,aAAc,EACxC,CAAED,GAAI,UAAWC,UAAW,YAAa,EAC1C,CAEDtB,WAAW,IAAME,sBAAsBkB,eAEvCxB,OAAOQ,aAAamC,qBAAqB,CAAC,GAC1C3C,OAAOQ,aAAa2B,oBAAoB,CAACH,MACzChC,OAAO4C,aAAaT,oBAAoB,CACtCnC,OAAOgD,gBAAgB,CAAC,wCAG1BJ,YAAYK,WAAW,EACzB,GAEAlD,GAAG,6CAA8C,UAE/C,MAAM0C,MAAQpB,SAASY,aAAa,CAAC,MACrCQ,CAAAA,MAAMhB,EAAE,CAAG,QACX,MAAMiB,MAAQrB,SAASY,aAAa,CAAC,MACrCS,CAAAA,MAAMjB,EAAE,CAAG,QACXJ,SAASC,IAAI,CAACY,WAAW,CAACO,OAC1BpB,SAASC,IAAI,CAACY,WAAW,CAACQ,OAE1B,MAAMlB,aAAe,CACnB,CAAEC,GAAI,QAASC,UAAW,aAAc,EACxC,CAAED,GAAI,QAASC,UAAW,YAAa,EACxC,CAED,KAAM,CAAEC,MAAM,CAAE,CAAGvB,WAAW,IAAME,sBAAsBkB,eAG1DxB,OAAO2B,OAAOC,OAAO,EAAEC,IAAI,CAAC,eAG5BtB,iBACE,CACE,CACE2C,OAAQR,MACRS,eAAgB,IAClB,EACD,CACD,CAAC,EAGH,OAAM9C,QAAQ,KACZL,OAAO2B,OAAOC,OAAO,EAAEC,IAAI,CAAC,aAC9B,EACF,GAEA9B,GAAG,iEAAkE,UACnE,MAAMiC,KAAOX,SAASY,aAAa,CAAC,MACpCD,CAAAA,KAAKP,EAAE,CAAG,QACVJ,SAASC,IAAI,CAACY,WAAW,CAACF,MAE1B,MAAMR,aAAe,CAAC,CAAEC,GAAI,QAASC,UAAW,aAAc,EAAE,CAEhE,KAAM,CAAEC,MAAM,CAAE,CAAGvB,WAAW,IAAME,sBAAsBkB,eAE1DxB,OAAO2B,OAAOC,OAAO,EAAEC,IAAI,CAAC,eAG5B,MAAMuB,YAAczB,OAAOC,OAAO,CAClCrB,iBACE,CACE,CACE2C,OAAQlB,KACRmB,eAAgB,IAClB,EACD,CACD,CAAC,GAIHnD,OAAO2B,OAAOC,OAAO,EAAEC,IAAI,CAACuB,YAC9B,GAEArD,GAAG,mCAAoC,UACrC,MAAM0C,MAAQpB,SAASY,aAAa,CAAC,MACrCQ,CAAAA,MAAMhB,EAAE,CAAG,QACX,MAAMiB,MAAQrB,SAASY,aAAa,CAAC,MACrCS,CAAAA,MAAMjB,EAAE,CAAG,QACXJ,SAASC,IAAI,CAACY,WAAW,CAACO,OAC1BpB,SAASC,IAAI,CAACY,WAAW,CAACQ,OAE1B,MAAMlB,aAAe,CACnB,CAAEC,GAAI,QAASC,UAAW,aAAc,EACxC,CAAED,GAAI,QAASC,UAAW,YAAa,EACxC,CAED,KAAM,CAAEC,MAAM,CAAE,CAAGvB,WAAW,IAAME,sBAAsBkB,eAE1DxB,OAAO2B,OAAOC,OAAO,EAAEC,IAAI,CAAC,eAG5BtB,iBACE,CACE,CACE2C,OAAQR,MACRS,eAAgB,KAClB,EACD,CACD,CAAC,GAIHnD,OAAO2B,OAAOC,OAAO,EAAEC,IAAI,CAAC,cAC9B,GAEA9B,GAAG,gEAAiE,UAClE,MAAM0C,MAAQpB,SAASY,aAAa,CAAC,MACrCQ,CAAAA,MAAMhB,EAAE,CAAG,QACX,MAAMiB,MAAQrB,SAASY,aAAa,CAAC,MACrCS,CAAAA,MAAMjB,EAAE,CAAG,QACX,MAAM4B,MAAQhC,SAASY,aAAa,CAAC,MACrCoB,CAAAA,MAAM5B,EAAE,CAAG,QACXJ,SAASC,IAAI,CAACY,WAAW,CAACO,OAC1BpB,SAASC,IAAI,CAACY,WAAW,CAACQ,OAC1BrB,SAASC,IAAI,CAACY,WAAW,CAACmB,OAE1B,MAAM7B,aAAe,CACnB,CAAEC,GAAI,QAASC,UAAW,aAAc,EACxC,CAAED,GAAI,QAASC,UAAW,YAAa,EACvC,CAAED,GAAI,QAASC,UAAW,YAAa,EACxC,CAED,KAAM,CAAEC,MAAM,CAAE,CAAGvB,WAAW,IAAME,sBAAsBkB,eAG1DjB,iBACE,CACE,CACE2C,OAAQR,MACRS,eAAgB,IAClB,EACA,CACED,OAAQG,MACRF,eAAgB,IAClB,EACD,CACD,CAAC,EAGH,OAAM9C,QAAQ,KAEZL,OAAO2B,OAAOC,OAAO,EAAEC,IAAI,CAAC,aAC9B,EACF,GAEA9B,GAAG,kCAAmC,KACpC,MAAMiC,KAAOX,SAASY,aAAa,CAAC,MACpCD,CAAAA,KAAKP,EAAE,CAAG,QACVJ,SAASC,IAAI,CAACY,WAAW,CAACF,MAE1B,MAAMR,aAAe,CAAC,CAAEC,GAAI,QAASC,UAAW,aAAc,EAAE,CAEhE,KAAM,CAAE4B,OAAO,CAAE,CAAGlD,WAAW,IAAME,sBAAsBkB,eAE3D8B,UAEAtD,OAAOU,gBAAgBqB,gBAAgB,EACzC,GAEAhC,GAAG,8CAA+C,KAChD,MAAM0C,MAAQpB,SAASY,aAAa,CAAC,MACrCQ,CAAAA,MAAMhB,EAAE,CAAG,QACX,MAAMiB,MAAQrB,SAASY,aAAa,CAAC,MACrCS,CAAAA,MAAMjB,EAAE,CAAG,QACXJ,SAASC,IAAI,CAACY,WAAW,CAACO,OAC1BpB,SAASC,IAAI,CAACY,WAAW,CAACQ,OAE1B,KAAM,CAAEa,QAAQ,CAAE,CAAGnD,WACnB,CAAC,CAAEoB,YAAY,CAAE,GAAKlB,sBAAsBkB,cAC5C,CACEgC,aAAc,CACZhC,aAAc,CAAC,CAAEC,GAAI,QAASC,UAAW,aAAc,EAAE,AAC3D,CACF,GAGF1B,OAAOa,sBAAsB8B,qBAAqB,CAAC,GACnD3C,OAAOQ,aAAamC,qBAAqB,CAAC,GAG1CY,SAAS,CACP/B,aAAc,CACZ,CAAEC,GAAI,QAASC,UAAW,aAAc,EACxC,CAAED,GAAI,QAASC,UAAW,YAAa,EACxC,AACH,GAGA1B,OAAOU,gBAAgBiC,qBAAqB,CAAC,GAC7C3C,OAAOa,sBAAsB8B,qBAAqB,CAAC,GACnD3C,OAAOQ,aAAamC,qBAAqB,CAAC,EAC5C,GAEA5C,GAAG,+CAAgD,KACjD,MAAMiC,KAAOX,SAASY,aAAa,CAAC,MACpCD,CAAAA,KAAKP,EAAE,CAAG,QACVJ,SAASC,IAAI,CAACY,WAAW,CAACF,MAE1B,MAAMR,aAAe,CACnB,CAAEC,GAAI,QAASC,UAAW,aAAc,EACxC,CAAED,GAAI,QAASC,UAAW,YAAa,EACxC,CAEDtB,WAAW,IAAME,sBAAsBkB,eAEvC,MAAMiC,OAAStD,GAAG0C,KAAK,CAACjC,OAAQ,yBAGhCL,iBACE,CACE,CACE2C,OAAQlB,KACRmB,eAAgB,IAClB,EACD,CACD,CAAC,GAGHnD,OAAOyD,QAAQ1B,gBAAgB,GAE/B0B,OAAOR,WAAW,EACpB,EACF"}
1
+ {"version":3,"sources":["../../../src/core/hooks/use-themed-scrollpoints.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n */\n\nimport { describe, it, expect, beforeEach, afterEach, vi } from \"vitest\";\nimport { renderHook, waitFor } from \"@testing-library/react\";\nimport { useThemedScrollpoints } from \"./use-themed-scrollpoints\";\n\ndescribe(\"useThemedScrollpoints\", () => {\n let observerCallback: IntersectionObserverCallback;\n let mockObserve: ReturnType<typeof vi.fn>;\n let mockUnobserve: ReturnType<typeof vi.fn>;\n let mockDisconnect: ReturnType<typeof vi.fn>;\n\n beforeEach(() => {\n // Mock IntersectionObserver\n mockObserve = vi.fn();\n mockUnobserve = vi.fn();\n mockDisconnect = vi.fn();\n\n global.IntersectionObserver = vi.fn((callback) => {\n observerCallback = callback;\n return {\n observe: mockObserve,\n unobserve: mockUnobserve,\n disconnect: mockDisconnect,\n };\n }) as unknown as typeof IntersectionObserver;\n\n // Mock requestAnimationFrame\n global.requestAnimationFrame = vi.fn((cb) => {\n cb(0);\n return 0;\n });\n });\n\n afterEach(() => {\n vi.clearAllMocks();\n document.body.innerHTML = \"\";\n });\n\n it(\"returns first scrollpoint className on mount\", () => {\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(result.current).toBe(\"theme-light\");\n });\n\n it(\"returns empty string when no scrollpoints provided\", () => {\n const { result } = renderHook(() => useThemedScrollpoints([]));\n expect(result.current).toBe(\"\");\n });\n\n it(\"does not create IntersectionObserver when no scrollpoints provided\", () => {\n renderHook(() => useThemedScrollpoints([]));\n\n expect(IntersectionObserver).not.toHaveBeenCalled();\n });\n\n it(\"creates IntersectionObserver with correct config\", () => {\n const scrollpoints = [{ id: \"zone1\", className: \"theme-light\" }];\n\n // Create DOM element\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(IntersectionObserver).toHaveBeenCalledWith(\n expect.any(Function),\n expect.objectContaining({\n rootMargin: \"-64px 0px 0px 0px\",\n threshold: 0,\n })\n );\n });\n\n it(\"observes all scrollpoint elements that exist in DOM\", () => {\n // Create DOM elements\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(mockObserve).toHaveBeenCalledTimes(2);\n expect(mockObserve).toHaveBeenCalledWith(elem1);\n expect(mockObserve).toHaveBeenCalledWith(elem2);\n });\n\n it(\"logs warning for missing DOM elements\", () => {\n const consoleWarn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n const scrollpoints = [{ id: \"non-existent\", className: \"theme\" }];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(consoleWarn).toHaveBeenCalledWith(\n expect.stringContaining('Element with id \"non-existent\" not found')\n );\n\n consoleWarn.mockRestore();\n });\n\n it(\"observes existing elements and warns about missing ones\", () => {\n const consoleWarn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n // Create only one element\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"missing\", className: \"theme-dark\" },\n ];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(mockObserve).toHaveBeenCalledTimes(1);\n expect(mockObserve).toHaveBeenCalledWith(elem);\n expect(consoleWarn).toHaveBeenCalledWith(\n expect.stringContaining('Element with id \"missing\" not found')\n );\n\n consoleWarn.mockRestore();\n });\n\n it(\"updates className when intersection occurs\", async () => {\n // Create DOM elements\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n // Initial state\n expect(result.current).toBe(\"theme-light\");\n\n // Simulate intersection with zone2\n observerCallback(\n [\n {\n target: elem2,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n await waitFor(() => {\n expect(result.current).toBe(\"theme-dark\");\n });\n });\n\n it(\"does not update className if same scrollpoint intersects again\", async () => {\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [{ id: \"zone1\", className: \"theme-light\" }];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(result.current).toBe(\"theme-light\");\n\n // Simulate intersection with same element\n const renderCount = result.current;\n observerCallback(\n [\n {\n target: elem,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n // Should not trigger re-render (className unchanged)\n expect(result.current).toBe(renderCount);\n });\n\n it(\"ignores non-intersecting entries\", async () => {\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(result.current).toBe(\"theme-light\");\n\n // Simulate non-intersecting entry\n observerCallback(\n [\n {\n target: elem2,\n isIntersecting: false,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n // Should remain unchanged\n expect(result.current).toBe(\"theme-light\");\n });\n\n it(\"uses first intersecting entry when multiple entries intersect\", async () => {\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n const elem3 = document.createElement(\"div\");\n elem3.id = \"zone3\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n document.body.appendChild(elem3);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n { id: \"zone3\", className: \"theme-blue\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n // Simulate multiple intersections (zone2 and zone3 both intersecting)\n observerCallback(\n [\n {\n target: elem2,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n {\n target: elem3,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n await waitFor(() => {\n // Should use first intersecting entry (zone2)\n expect(result.current).toBe(\"theme-dark\");\n });\n });\n\n it(\"disconnects observer on unmount\", () => {\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [{ id: \"zone1\", className: \"theme-light\" }];\n\n const { unmount } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n unmount();\n\n expect(mockDisconnect).toHaveBeenCalled();\n });\n\n it(\"recreates observer when scrollpoints change\", () => {\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const { rerender } = renderHook(\n ({ scrollpoints }) => useThemedScrollpoints(scrollpoints),\n {\n initialProps: {\n scrollpoints: [{ id: \"zone1\", className: \"theme-light\" }],\n },\n }\n );\n\n expect(IntersectionObserver).toHaveBeenCalledTimes(1);\n expect(mockObserve).toHaveBeenCalledTimes(1);\n\n // Change scrollpoints\n rerender({\n scrollpoints: [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ],\n });\n\n // Should disconnect old observer and create new one\n expect(mockDisconnect).toHaveBeenCalledTimes(1);\n expect(IntersectionObserver).toHaveBeenCalledTimes(2);\n expect(mockObserve).toHaveBeenCalledTimes(3); // 1 from first render + 2 from second\n });\n\n it(\"uses requestAnimationFrame for state updates\", () => {\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n const rafSpy = vi.spyOn(global, \"requestAnimationFrame\");\n\n // Simulate intersection\n observerCallback(\n [\n {\n target: elem,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n expect(rafSpy).toHaveBeenCalled();\n\n rafSpy.mockRestore();\n });\n});\n"],"names":["describe","it","expect","beforeEach","afterEach","vi","renderHook","waitFor","useThemedScrollpoints","observerCallback","mockObserve","mockUnobserve","mockDisconnect","fn","global","IntersectionObserver","callback","observe","unobserve","disconnect","requestAnimationFrame","cb","clearAllMocks","document","body","innerHTML","scrollpoints","id","className","result","current","toBe","not","toHaveBeenCalled","elem","createElement","appendChild","toHaveBeenCalledWith","any","Function","objectContaining","rootMargin","threshold","elem1","elem2","toHaveBeenCalledTimes","consoleWarn","spyOn","console","mockImplementation","stringContaining","mockRestore","target","isIntersecting","renderCount","elem3","unmount","rerender","initialProps","rafSpy"],"mappings":"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"}
package/index.d.ts CHANGED
@@ -5998,6 +5998,21 @@ export const queryIdAll: (val: string, root?: ParentNode) => NodeListOf<Element>
5998
5998
  //# sourceMappingURL=dom-query.d.ts.map
5999
5999
  }
6000
6000
 
6001
+ declare module '@ably/ui/core/hooks/use-content-height' {
6002
+ import { RefObject } from "react";
6003
+ /**
6004
+ * Custom hook that tracks the content height of an element using ResizeObserver.
6005
+ * This eliminates forced reflows by using the browser's native resize observation API
6006
+ * instead of synchronous clientHeight/getBoundingClientRect queries.
6007
+ *
6008
+ * @param ref - React ref to the element to observe
6009
+ * @param initialHeight - Initial height value (default: 0)
6010
+ * @returns Current content height in pixels
6011
+ */
6012
+ export function useContentHeight(ref: RefObject<HTMLElement>, initialHeight?: number): number;
6013
+ //# sourceMappingURL=use-content-height.d.ts.map
6014
+ }
6015
+
6001
6016
  declare module '@ably/ui/core/hooks/use-rails-ujs-hooks' {
6002
6017
  import { RefObject } from "react";
6003
6018
  const useRailsUjsLinks: (containerRef: RefObject<HTMLElement>) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ably/ui",
3
- "version": "17.11.0-dev.4c4b6b55",
3
+ "version": "17.11.0-dev.672950e8",
4
4
  "description": "Home of the Ably design system library ([design.ably.com](https://design.ably.com)). It provides a showcase, development/test environment and a publishing pipeline for different distributables.",
5
5
  "repository": {
6
6
  "type": "git",