Skip to content

Commit f55c324

Browse files
committed
extract hook and add tests
1 parent c219a02 commit f55c324

File tree

3 files changed

+134
-19
lines changed

3 files changed

+134
-19
lines changed

chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { TopLevelSpec } from "vega-lite";
44
import type { ComponentProps, ComponentState } from "@/index";
55
import { useSignalListeners } from "./hooks/useSignalListeners";
66
import { useVegaTheme, type VegaTheme } from "./hooks/useVegaTheme";
7-
import { useCallback, useRef, useState } from "react";
7+
import { useResizeObserver } from "./hooks/useResizeObserver";
88

99
interface VegaChartState extends ComponentState {
1010
theme?: VegaTheme | "default" | "system";
@@ -25,24 +25,7 @@ export function VegaChart({
2525
}: VegaChartProps) {
2626
const signalListeners = useSignalListeners(chart, type, id, onChange);
2727
const vegaTheme = useVegaTheme(theme);
28-
const containerRef = useRef<ResizeObserver | null>(null);
29-
const [containerSizeKey, setContainerSizeKey] = useState(0);
30-
const containerCallbackRef = useCallback((node: Element | null) => {
31-
if (containerRef.current) {
32-
containerRef.current.disconnect();
33-
containerRef.current = null;
34-
}
35-
if (node !== null) {
36-
const resizeObserver = new ResizeObserver((_entries) => {
37-
// We only need to know that a resize happened because it triggers a
38-
// re-render allowing vega to get the latest layout.
39-
setContainerSizeKey(Date.now());
40-
});
41-
42-
resizeObserver.observe(node);
43-
containerRef.current = resizeObserver;
44-
}
45-
}, []);
28+
const { containerSizeKey, containerCallbackRef } = useResizeObserver();
4629
if (chart) {
4730
return (
4831
<div id="chart-container" ref={containerCallbackRef} style={style}>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { useResizeObserver } from "./useResizeObserver";
3+
import { act, render } from "@testing-library/react";
4+
import { useEffect } from "react";
5+
6+
global.ResizeObserver = class {
7+
observe() {}
8+
unobserve() {}
9+
disconnect() {}
10+
};
11+
12+
describe("useResizeObserver", () => {
13+
it("should observe a node", () => {
14+
let callbackRef: (node: Element | null) => void;
15+
16+
const TestComponent = () => {
17+
const { containerCallbackRef } = useResizeObserver();
18+
callbackRef = containerCallbackRef;
19+
return <div ref={containerCallbackRef} />;
20+
};
21+
22+
render(<TestComponent />);
23+
24+
const observeSpy = vi.spyOn(global.ResizeObserver.prototype, "observe");
25+
26+
const node = document.createElement("div");
27+
act(() => {
28+
if (callbackRef) {
29+
callbackRef(node);
30+
}
31+
});
32+
33+
expect(observeSpy).toHaveBeenCalledWith(node);
34+
});
35+
36+
it("should disconnect previous observer when node changes", () => {
37+
let callbackRef: (node: Element | null) => void;
38+
39+
const TestComponent = () => {
40+
const { containerCallbackRef } = useResizeObserver();
41+
callbackRef = containerCallbackRef;
42+
return null;
43+
};
44+
45+
render(<TestComponent />);
46+
47+
const disconnectSpy = vi.spyOn(
48+
global.ResizeObserver.prototype,
49+
"disconnect",
50+
);
51+
52+
const observeSpy = vi.spyOn(global.ResizeObserver.prototype, "observe");
53+
54+
const node1 = document.createElement("div");
55+
const node2 = document.createElement("div");
56+
57+
act(() => {
58+
if (callbackRef) {
59+
callbackRef(node1);
60+
callbackRef(node2);
61+
}
62+
});
63+
64+
expect(disconnectSpy).toHaveBeenCalled();
65+
expect(observeSpy).toHaveBeenCalledWith(node2);
66+
});
67+
68+
it("should update containerSizeKey on resize", () => {
69+
let observerCallback: ResizeObserverCallback | null = null;
70+
71+
global.ResizeObserver = class {
72+
constructor(cb: ResizeObserverCallback) {
73+
observerCallback = cb;
74+
}
75+
observe() {}
76+
unobserve() {}
77+
disconnect() {}
78+
};
79+
80+
let lastKey = 0;
81+
82+
const TestComponent = () => {
83+
const { containerCallbackRef, containerSizeKey } = useResizeObserver();
84+
85+
useEffect(() => {
86+
lastKey = containerSizeKey;
87+
}, [containerSizeKey]);
88+
89+
return <div ref={containerCallbackRef} />;
90+
};
91+
92+
render(<TestComponent />);
93+
const initialKey = lastKey;
94+
95+
act(() => {
96+
if (observerCallback) {
97+
observerCallback([], {} as ResizeObserver);
98+
}
99+
});
100+
101+
expect(lastKey).not.toBe(initialKey);
102+
});
103+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useCallback, useRef, useState } from "react";
2+
3+
export interface ResizeObserverResult {
4+
containerSizeKey: number;
5+
containerCallbackRef: (node: Element | null) => void;
6+
}
7+
8+
export function useResizeObserver(): ResizeObserverResult {
9+
const containerRef = useRef<ResizeObserver | null>(null);
10+
const [containerSizeKey, setContainerSizeKey] = useState(0);
11+
const containerCallbackRef = useCallback((node: Element | null) => {
12+
if (containerRef.current) {
13+
containerRef.current.disconnect();
14+
containerRef.current = null;
15+
}
16+
if (node !== null) {
17+
const resizeObserver = new ResizeObserver((_entries) => {
18+
// We only need to know that a resize happened because it triggers a
19+
// re-render allowing vega to get the latest layout.
20+
setContainerSizeKey(Date.now());
21+
});
22+
23+
resizeObserver.observe(node);
24+
containerRef.current = resizeObserver;
25+
}
26+
}, []);
27+
28+
return { containerSizeKey, containerCallbackRef };
29+
}

0 commit comments

Comments
 (0)