@hsu-react/ui 2.5.3 → 2.5.5
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/es/components/ChainGraph/_hooks/useChainGraphData.js +29 -8
- package/es/components/ChainGraph/_hooks/useChainGraphLayout.js +14 -3
- package/es/components/Chart/Bar/index.js +49 -19
- package/es/components/Chart/Bubble/index.js +21 -6
- package/es/components/Chart/Common/index.js +7 -2
- package/es/components/Chart/Heatmap/index.js +21 -6
- package/es/components/Chart/Line/index.js +49 -19
- package/es/components/Chart/Pie/Pie3D/index.js +22 -6
- package/es/components/Chart/Pie/index.js +21 -6
- package/es/components/Chart/Sankey/index.js +11 -2
- package/es/components/Chart/Tree/index.js +11 -2
- package/es/components/Chat/ChatInput/index.js +7 -2
- package/es/components/Icon/index.js +8 -2
- package/es/components/Input/Number/index.js +28 -21
- package/es/components/Input/Password/index.js +44 -36
- package/es/components/Input/Search/index.js +44 -36
- package/es/components/Input/TextArea/index.js +64 -42
- package/es/components/Input/index.js +40 -34
- package/es/components/Select/_hooks/useSelectComposition.js +6 -2
- package/es/components/Select/_hooks/useSelectInputOptions.js +8 -2
- package/es/components/Slider/index.js +24 -15
- package/es/components/Table/_hooks/useAutoScrolling.js +16 -7
- package/es/components/Upload/_hooks/useUploadFileList.js +8 -2
- package/es/components/Upload/_hooks/useUploadOperations.js +8 -2
- package/es/hooks/useLatestRef.d.ts +24 -0
- package/es/hooks/useLatestRef.js +29 -0
- package/es/layout/Menu/_hooks/useOnlyLvOneMenu.js +9 -2
- package/lib/components/ChainGraph/_hooks/useChainGraphData.js +24 -7
- package/lib/components/ChainGraph/_hooks/useChainGraphLayout.js +12 -2
- package/lib/components/Chart/Bar/index.js +45 -18
- package/lib/components/Chart/Bubble/index.js +19 -6
- package/lib/components/Chart/Common/index.js +6 -2
- package/lib/components/Chart/Heatmap/index.js +19 -6
- package/lib/components/Chart/Line/index.js +45 -18
- package/lib/components/Chart/Pie/Pie3D/index.js +20 -6
- package/lib/components/Chart/Pie/index.js +19 -6
- package/lib/components/Chart/Sankey/index.js +10 -2
- package/lib/components/Chart/Tree/index.js +10 -2
- package/lib/components/Chat/ChatInput/index.js +6 -2
- package/lib/components/Icon/index.js +7 -2
- package/lib/components/Input/Number/index.js +27 -18
- package/lib/components/Input/Password/index.js +40 -26
- package/lib/components/Input/Search/index.js +42 -26
- package/lib/components/Input/TextArea/index.js +54 -26
- package/lib/components/Input/index.js +37 -24
- package/lib/components/Select/_hooks/useSelectComposition.js +6 -2
- package/lib/components/Select/_hooks/useSelectInputOptions.js +7 -2
- package/lib/components/Slider/index.js +23 -11
- package/lib/components/Table/_hooks/useAutoScrolling.js +16 -7
- package/lib/components/Upload/_hooks/useUploadFileList.js +7 -2
- package/lib/components/Upload/_hooks/useUploadOperations.js +7 -2
- package/lib/hooks/useLatestRef.d.ts +24 -0
- package/lib/hooks/useLatestRef.js +35 -0
- package/lib/layout/Menu/_hooks/useOnlyLvOneMenu.js +8 -2
- package/package.json +9 -3
- package/src/__tests__/callbackDepsGuard.test.ts +119 -0
- package/src/__tests__/setup.ts +26 -0
- package/src/components/ChainGraph/_hooks/callbacks.test.tsx +155 -0
- package/src/components/ChainGraph/_hooks/useChainGraphData.ts +38 -10
- package/src/components/ChainGraph/_hooks/useChainGraphLayout.ts +16 -3
- package/src/components/Chart/Bar/index.tsx +59 -21
- package/src/components/Chart/Bubble/index.tsx +22 -7
- package/src/components/Chart/Common/index.tsx +7 -2
- package/src/components/Chart/Heatmap/index.tsx +25 -6
- package/src/components/Chart/Line/index.tsx +59 -21
- package/src/components/Chart/Pie/Pie3D/index.tsx +22 -7
- package/src/components/Chart/Pie/index.tsx +22 -7
- package/src/components/Chart/Sankey/index.tsx +10 -4
- package/src/components/Chart/Tree/index.tsx +10 -4
- package/src/components/Chart/callbacks.test.tsx +315 -0
- package/src/components/Chat/ChatInput/index.tsx +7 -2
- package/src/components/Icon/index.tsx +7 -2
- package/src/components/Input/Number/index.tsx +25 -23
- package/src/components/Input/Password/index.tsx +37 -26
- package/src/components/Input/Search/index.tsx +39 -26
- package/src/components/Input/TextArea/index.test.tsx +208 -0
- package/src/components/Input/TextArea/index.tsx +56 -30
- package/src/components/Input/index.test.tsx +189 -0
- package/src/components/Input/index.tsx +36 -29
- package/src/components/Select/_hooks/useSelectComposition.test.ts +47 -0
- package/src/components/Select/_hooks/useSelectComposition.ts +6 -2
- package/src/components/Select/_hooks/useSelectInputOptions.ts +6 -9
- package/src/components/Slider/index.tsx +22 -12
- package/src/components/Table/_hooks/useAutoScrolling.test.tsx +95 -0
- package/src/components/Table/_hooks/useAutoScrolling.ts +16 -7
- package/src/components/Upload/_hooks/useUploadFileList.ts +6 -2
- package/src/components/Upload/_hooks/useUploadOperations.ts +7 -2
- package/src/hooks/useLatestRef.ts +31 -0
- package/src/layout/Menu/_hooks/useOnlyLvOneMenu.test.tsx +56 -0
- package/src/layout/Menu/_hooks/useOnlyLvOneMenu.ts +8 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { act, renderHook } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
vi.mock("../ChainGraphServices", () => ({ default: class {} }));
|
|
5
|
+
|
|
6
|
+
import { useChainGraphLayout } from "./useChainGraphLayout";
|
|
7
|
+
import { useChainGraphData } from "./useChainGraphData";
|
|
8
|
+
import type ChainGraphServices from "../ChainGraphServices";
|
|
9
|
+
import type { TreeGraphData } from "..";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ChainGraph 把回调交给图实例,由图实例在后续事件里回调。
|
|
13
|
+
* 这类「出口型」回调进依赖数组,会让消费方传内联箭头时反复触发重排 / 重新 setData。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DATA = { id: "root", label: "root" } as unknown as TreeGraphData;
|
|
17
|
+
|
|
18
|
+
describe("useChainGraphLayout", () => {
|
|
19
|
+
const makeGraph = () =>
|
|
20
|
+
({ changeLayout: vi.fn() }) as unknown as ChainGraphServices & {
|
|
21
|
+
changeLayout: ReturnType<typeof vi.fn>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
it("getImage 换引用不会重新 changeLayout(整张图重排)", () => {
|
|
25
|
+
const graph = makeGraph();
|
|
26
|
+
const { rerender } = renderHook(
|
|
27
|
+
({ getImage }: { getImage?: (img: string) => void }) =>
|
|
28
|
+
useChainGraphLayout({ graph, octopus: true, rootLevel: 0, getImage }),
|
|
29
|
+
{ initialProps: { getImage: vi.fn() } },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(graph.changeLayout).toHaveBeenCalledTimes(1);
|
|
33
|
+
|
|
34
|
+
rerender({ getImage: vi.fn() });
|
|
35
|
+
rerender({ getImage: vi.fn() });
|
|
36
|
+
|
|
37
|
+
expect(graph.changeLayout).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("出图时调到的是最新的 getImage,包括从无到有传入的情况", () => {
|
|
41
|
+
const graph = makeGraph();
|
|
42
|
+
const { rerender } = renderHook(
|
|
43
|
+
({ getImage }: { getImage?: (img: string) => void }) =>
|
|
44
|
+
useChainGraphLayout({ graph, octopus: true, rootLevel: 0, getImage }),
|
|
45
|
+
{ initialProps: {} as { getImage?: (img: string) => void } },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const forwarded = graph.changeLayout.mock.calls[0][2] as (
|
|
49
|
+
img: string,
|
|
50
|
+
) => void;
|
|
51
|
+
// 没传回调时转发函数照样存在,调用不报错
|
|
52
|
+
expect(() => forwarded("img-0")).not.toThrow();
|
|
53
|
+
|
|
54
|
+
const later = vi.fn();
|
|
55
|
+
rerender({ getImage: later });
|
|
56
|
+
// 图实例里存的还是同一个转发函数,但它现在会转给新传入的回调
|
|
57
|
+
forwarded("img-1");
|
|
58
|
+
expect(later).toHaveBeenCalledWith("img-1");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("useChainGraphData", () => {
|
|
63
|
+
const makeGraph = () =>
|
|
64
|
+
({ setData: vi.fn() }) as unknown as ChainGraphServices & {
|
|
65
|
+
setData: ReturnType<typeof vi.fn>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type Props = {
|
|
69
|
+
getImage?: (img: string) => void;
|
|
70
|
+
labelRender?: (label: TreeGraphData) => string;
|
|
71
|
+
onLayoutingChange?: (v: boolean) => void;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
it("三个回调换引用都不会重新 setData", () => {
|
|
75
|
+
const graph = makeGraph();
|
|
76
|
+
const { rerender } = renderHook(
|
|
77
|
+
(props: Props) => useChainGraphData({ graph, data: DATA, ...props }),
|
|
78
|
+
{
|
|
79
|
+
initialProps: {
|
|
80
|
+
getImage: vi.fn(),
|
|
81
|
+
labelRender: vi.fn(() => "a"),
|
|
82
|
+
onLayoutingChange: vi.fn(),
|
|
83
|
+
} as Props,
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(graph.setData).toHaveBeenCalledTimes(1);
|
|
88
|
+
|
|
89
|
+
rerender({
|
|
90
|
+
getImage: vi.fn(),
|
|
91
|
+
labelRender: vi.fn(() => "b"),
|
|
92
|
+
onLayoutingChange: vi.fn(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(graph.setData).toHaveBeenCalledTimes(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("图实例回调时取到的是最新引用,不是首次 setData 时的那个", () => {
|
|
99
|
+
const graph = makeGraph();
|
|
100
|
+
const firstLabel = vi.fn(() => "first");
|
|
101
|
+
const { rerender } = renderHook(
|
|
102
|
+
(props: Props) => useChainGraphData({ graph, data: DATA, ...props }),
|
|
103
|
+
{
|
|
104
|
+
initialProps: {
|
|
105
|
+
getImage: vi.fn(),
|
|
106
|
+
labelRender: firstLabel,
|
|
107
|
+
onLayoutingChange: vi.fn(),
|
|
108
|
+
} as Props,
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const pushed = graph.setData.mock.calls[0][0] as {
|
|
113
|
+
getImage: (img: string) => void;
|
|
114
|
+
labelRender?: (label: TreeGraphData) => string;
|
|
115
|
+
isLayouting: (v: boolean) => void;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const nextImage = vi.fn();
|
|
119
|
+
const nextLabel = vi.fn(() => "next");
|
|
120
|
+
const nextLayouting = vi.fn();
|
|
121
|
+
rerender({
|
|
122
|
+
getImage: nextImage,
|
|
123
|
+
labelRender: nextLabel,
|
|
124
|
+
onLayoutingChange: nextLayouting,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
pushed.getImage("img");
|
|
128
|
+
expect(nextImage).toHaveBeenCalledWith("img");
|
|
129
|
+
|
|
130
|
+
expect(pushed.labelRender!(DATA)).toBe("next");
|
|
131
|
+
expect(firstLabel).not.toHaveBeenCalled();
|
|
132
|
+
|
|
133
|
+
act(() => pushed.isLayouting(false));
|
|
134
|
+
expect(nextLayouting).toHaveBeenCalledWith(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("labelRender 的「传了才改写 label」语义保留:没传就是 undefined,从无到有会带上", () => {
|
|
138
|
+
const graph = makeGraph();
|
|
139
|
+
const { rerender } = renderHook(
|
|
140
|
+
({ data, labelRender }: { data: TreeGraphData; labelRender?: Props["labelRender"] }) =>
|
|
141
|
+
useChainGraphData({ graph, data, labelRender }),
|
|
142
|
+
{ initialProps: { data: DATA } as { data: TreeGraphData; labelRender?: Props["labelRender"] } },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(graph.setData.mock.calls[0][0].labelRender).toBeUndefined();
|
|
146
|
+
|
|
147
|
+
const label = vi.fn(() => "x");
|
|
148
|
+
const nextData = { id: "root2", label: "root2" } as unknown as TreeGraphData;
|
|
149
|
+
rerender({ data: nextData, labelRender: label });
|
|
150
|
+
|
|
151
|
+
expect(graph.setData).toHaveBeenCalledTimes(2);
|
|
152
|
+
expect(typeof graph.setData.mock.calls[1][0].labelRender).toBe("function");
|
|
153
|
+
expect(graph.setData.mock.calls[1][0].labelRender(DATA)).toBe("x");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
import { Equal } from "hsu-utils";
|
|
3
3
|
import ChainGraphServices from "../ChainGraphServices";
|
|
4
4
|
import { TreeGraphData } from "..";
|
|
5
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
5
6
|
|
|
6
7
|
interface UseChainGraphDataProps {
|
|
7
8
|
graph: ChainGraphServices | null;
|
|
@@ -28,6 +29,35 @@ export function useChainGraphData(props: UseChainGraphDataProps) {
|
|
|
28
29
|
);
|
|
29
30
|
const [isLayouting, setIsLayouting] = useState(true);
|
|
30
31
|
|
|
32
|
+
// 这三个回调都是「交给图实例、由它在后续事件里回调」的出口,不是数据本身。
|
|
33
|
+
// 它们进依赖数组是条死依赖:effect 体被 ObjEqual(lastData, data) 挡住,
|
|
34
|
+
// 引用变了也不会重新 setData —— 图里存的还是首次 setData 时那个闭包,
|
|
35
|
+
// 换了引用永远调不到(stale closure)。改成引用恒定的转发函数:
|
|
36
|
+
// 依赖数组只留真正的输入(graph/data/level/rootLevel),调用时总是取到最新的回调。
|
|
37
|
+
const getImageRef = useLatestRef(getImage);
|
|
38
|
+
const labelRenderRef = useLatestRef(labelRender);
|
|
39
|
+
const onLayoutingChangeRef = useLatestRef(onLayoutingChange);
|
|
40
|
+
|
|
41
|
+
const emitImage = useCallback(
|
|
42
|
+
(img: string) => {
|
|
43
|
+
getImageRef.current?.(img);
|
|
44
|
+
},
|
|
45
|
+
[getImageRef]
|
|
46
|
+
);
|
|
47
|
+
// labelRender 在 ChainGraphServices.setData 里是「传了才改写 label」的真值判断,
|
|
48
|
+
// 所以这里保留「有没有传」的语义:没传就传 undefined,传了才给转发函数。
|
|
49
|
+
const renderLabel = useCallback(
|
|
50
|
+
(label: TreeGraphData) => labelRenderRef.current!(label),
|
|
51
|
+
[labelRenderRef]
|
|
52
|
+
);
|
|
53
|
+
const emitLayouting = useCallback(
|
|
54
|
+
(value: boolean) => {
|
|
55
|
+
setIsLayouting(value);
|
|
56
|
+
onLayoutingChangeRef.current?.(value);
|
|
57
|
+
},
|
|
58
|
+
[onLayoutingChangeRef]
|
|
59
|
+
);
|
|
60
|
+
|
|
31
61
|
useEffect(() => {
|
|
32
62
|
if (graph && !Equal.ObjEqual(lastData, data)) {
|
|
33
63
|
setLastData(data);
|
|
@@ -35,12 +65,9 @@ export function useChainGraphData(props: UseChainGraphDataProps) {
|
|
|
35
65
|
data,
|
|
36
66
|
level,
|
|
37
67
|
rootLevel,
|
|
38
|
-
getImage,
|
|
39
|
-
isLayouting:
|
|
40
|
-
|
|
41
|
-
onLayoutingChange?.(value);
|
|
42
|
-
},
|
|
43
|
-
labelRender,
|
|
68
|
+
getImage: emitImage,
|
|
69
|
+
isLayouting: emitLayouting,
|
|
70
|
+
labelRender: labelRenderRef.current ? renderLabel : undefined,
|
|
44
71
|
});
|
|
45
72
|
}
|
|
46
73
|
}, [
|
|
@@ -48,10 +75,11 @@ export function useChainGraphData(props: UseChainGraphDataProps) {
|
|
|
48
75
|
data,
|
|
49
76
|
level,
|
|
50
77
|
rootLevel,
|
|
51
|
-
getImage,
|
|
52
|
-
labelRender,
|
|
53
|
-
onLayoutingChange,
|
|
54
78
|
lastData,
|
|
79
|
+
emitImage,
|
|
80
|
+
emitLayouting,
|
|
81
|
+
renderLabel,
|
|
82
|
+
labelRenderRef,
|
|
55
83
|
]);
|
|
56
84
|
|
|
57
85
|
return { isLayouting };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
2
|
import ChainGraphServices from "../ChainGraphServices";
|
|
3
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
3
4
|
|
|
4
5
|
interface UseChainGraphLayoutProps {
|
|
5
6
|
graph: ChainGraphServices | null;
|
|
@@ -11,9 +12,21 @@ interface UseChainGraphLayoutProps {
|
|
|
11
12
|
export function useChainGraphLayout(props: UseChainGraphLayoutProps) {
|
|
12
13
|
const { graph, octopus, rootLevel, getImage } = props;
|
|
13
14
|
|
|
15
|
+
// getImage 只是「布局完成后把截图 dataURL 交出去」的出口,不是布局的输入。
|
|
16
|
+
// 它进依赖数组会让消费方每传一次内联箭头就重新 changeLayout 一次 —— 整张图重排;
|
|
17
|
+
// 消费方再把截图存进 state,就变成「重排 → 出图 → setState → 新引用 → 再重排」的死循环。
|
|
18
|
+
// 用引用恒定的转发函数注册,调用时再取最新的 getImage。
|
|
19
|
+
const getImageRef = useLatestRef(getImage);
|
|
20
|
+
const emitImage = useCallback(
|
|
21
|
+
(img: string) => {
|
|
22
|
+
getImageRef.current?.(img);
|
|
23
|
+
},
|
|
24
|
+
[getImageRef],
|
|
25
|
+
);
|
|
26
|
+
|
|
14
27
|
useEffect(() => {
|
|
15
28
|
if (graph && octopus !== undefined) {
|
|
16
|
-
graph.changeLayout(octopus, rootLevel,
|
|
29
|
+
graph.changeLayout(octopus, rootLevel, emitImage);
|
|
17
30
|
}
|
|
18
|
-
}, [graph, octopus, rootLevel,
|
|
31
|
+
}, [graph, octopus, rootLevel, emitImage]);
|
|
19
32
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "../_utils/cartesian";
|
|
25
25
|
import { resolveChartChrome } from "../_utils/chartTheme";
|
|
26
26
|
import useIsDark from "../../../hooks/useIsDark";
|
|
27
|
+
import useLatestRef from "../../../hooks/useLatestRef";
|
|
27
28
|
|
|
28
29
|
export interface ChartBarProps extends ChartCommonProps {
|
|
29
30
|
chartTitle?: string;
|
|
@@ -89,6 +90,36 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
89
90
|
const isDark = useIsDark();
|
|
90
91
|
const chrome = useMemo(() => resolveChartChrome(isDark), [isDark]);
|
|
91
92
|
|
|
93
|
+
// 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
|
|
94
|
+
// 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
|
|
95
|
+
// 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
|
|
96
|
+
// 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
|
|
97
|
+
// 这样「从无到有传入回调」仍会重新注册,不会漏。
|
|
98
|
+
const onClickRef = useLatestRef(onClick);
|
|
99
|
+
const onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
|
|
100
|
+
const onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
|
|
101
|
+
const hasOnClick = !!onClick;
|
|
102
|
+
const hasOnLegendSelectChanged = !!onLegendSelectChanged;
|
|
103
|
+
const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
|
|
104
|
+
// 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
|
|
105
|
+
// 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
|
|
106
|
+
const notifiedZoomWindowRef = useRef<DataZoomIndexWindow | null>(null);
|
|
107
|
+
const emitZoomWindow = useCallback(
|
|
108
|
+
(next: DataZoomIndexWindow) => {
|
|
109
|
+
const prev = notifiedZoomWindowRef.current;
|
|
110
|
+
if (
|
|
111
|
+
prev &&
|
|
112
|
+
prev.startIndex === next.startIndex &&
|
|
113
|
+
prev.endIndex === next.endIndex
|
|
114
|
+
) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
notifiedZoomWindowRef.current = next;
|
|
118
|
+
onDataZoomWindowChangedRef.current?.(next);
|
|
119
|
+
},
|
|
120
|
+
[onDataZoomWindowChangedRef],
|
|
121
|
+
);
|
|
122
|
+
|
|
92
123
|
// Cache the chart option with useMemo
|
|
93
124
|
const chartOption = useMemo(() => {
|
|
94
125
|
const def_xAxis = createDefaultCategoryXAxis(xAxisData, chrome);
|
|
@@ -328,18 +359,18 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
328
359
|
}
|
|
329
360
|
|
|
330
361
|
// Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
|
|
331
|
-
if (
|
|
362
|
+
if (hasOnDataZoomWindowChanged) {
|
|
332
363
|
const zooms = (chart.getOption() as ChartsOption | undefined)
|
|
333
364
|
?.dataZoom as Array<{ start?: number; end?: number }> | undefined;
|
|
334
365
|
const zoom = zooms?.[0];
|
|
335
366
|
const totalLen = xAxisData?.length ?? 0;
|
|
336
367
|
if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
|
|
337
|
-
|
|
368
|
+
emitZoomWindow(
|
|
338
369
|
percentWindowToIndexWindow(zoom.start, zoom.end, totalLen),
|
|
339
370
|
);
|
|
340
371
|
} else {
|
|
341
372
|
// Without dataZoom (e.g. scrolling not enabled due to insufficient data) the window is the full range; sync once to avoid a stale old window
|
|
342
|
-
|
|
373
|
+
emitZoomWindow({
|
|
343
374
|
startIndex: 0,
|
|
344
375
|
endIndex: Math.max(0, totalLen - 1),
|
|
345
376
|
});
|
|
@@ -355,18 +386,21 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
355
386
|
resizeObserverRef.current.observe(chartRef.current);
|
|
356
387
|
}
|
|
357
388
|
|
|
358
|
-
// Add click event
|
|
359
|
-
|
|
360
|
-
|
|
389
|
+
// Add click event(注册与否看布尔量,实际调用取最新引用)
|
|
390
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
391
|
+
onClickRef.current?.(event);
|
|
392
|
+
};
|
|
393
|
+
if (hasOnClick) {
|
|
394
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
361
395
|
}
|
|
362
396
|
|
|
363
397
|
// Legend selection event
|
|
364
398
|
const handleLegendSelectChanged = (params: unknown) => {
|
|
365
|
-
|
|
399
|
+
onLegendSelectChangedRef.current?.(
|
|
366
400
|
(params as { selected: Record<string, boolean> }).selected,
|
|
367
401
|
);
|
|
368
402
|
};
|
|
369
|
-
if (
|
|
403
|
+
if (hasOnLegendSelectChanged) {
|
|
370
404
|
chartInstanceRef.current?.on(
|
|
371
405
|
"legendselectchanged",
|
|
372
406
|
handleLegendSelectChanged,
|
|
@@ -392,7 +426,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
392
426
|
typeof info?.startValue === "number" &&
|
|
393
427
|
typeof info?.endValue === "number"
|
|
394
428
|
) {
|
|
395
|
-
|
|
429
|
+
emitZoomWindow({
|
|
396
430
|
startIndex: Math.max(0, Math.round(info.startValue)),
|
|
397
431
|
endIndex: Math.max(0, Math.round(info.endValue)),
|
|
398
432
|
});
|
|
@@ -400,7 +434,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
400
434
|
typeof info?.start === "number" &&
|
|
401
435
|
typeof info?.end === "number"
|
|
402
436
|
) {
|
|
403
|
-
|
|
437
|
+
emitZoomWindow(
|
|
404
438
|
percentWindowToIndexWindow(
|
|
405
439
|
info.start,
|
|
406
440
|
info.end,
|
|
@@ -409,7 +443,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
409
443
|
);
|
|
410
444
|
}
|
|
411
445
|
};
|
|
412
|
-
if (
|
|
446
|
+
if (hasOnDataZoomWindowChanged) {
|
|
413
447
|
chartInstanceRef.current?.on("datazoom", handleDataZoom);
|
|
414
448
|
}
|
|
415
449
|
|
|
@@ -445,18 +479,18 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
445
479
|
return () => {
|
|
446
480
|
window.removeEventListener("resize", handleResize);
|
|
447
481
|
|
|
448
|
-
if (
|
|
449
|
-
chartInstanceRef.current?.off("click",
|
|
482
|
+
if (hasOnClick) {
|
|
483
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
450
484
|
}
|
|
451
485
|
|
|
452
|
-
if (
|
|
486
|
+
if (hasOnLegendSelectChanged) {
|
|
453
487
|
chartInstanceRef.current?.off(
|
|
454
488
|
"legendselectchanged",
|
|
455
489
|
handleLegendSelectChanged,
|
|
456
490
|
);
|
|
457
491
|
}
|
|
458
492
|
|
|
459
|
-
if (
|
|
493
|
+
if (hasOnDataZoomWindowChanged) {
|
|
460
494
|
chartInstanceRef.current?.off("datazoom", handleDataZoom);
|
|
461
495
|
}
|
|
462
496
|
|
|
@@ -468,9 +502,12 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
468
502
|
}, [
|
|
469
503
|
chartOption,
|
|
470
504
|
handleResize,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
505
|
+
onClickRef,
|
|
506
|
+
onLegendSelectChangedRef,
|
|
507
|
+
hasOnClick,
|
|
508
|
+
hasOnLegendSelectChanged,
|
|
509
|
+
hasOnDataZoomWindowChanged,
|
|
510
|
+
emitZoomWindow,
|
|
474
511
|
xAxisData,
|
|
475
512
|
enableLegendAutoScroll,
|
|
476
513
|
legendVisibleCount,
|
|
@@ -507,9 +544,9 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
507
544
|
startIndex: normalizedScroll.startIndex,
|
|
508
545
|
autoPlay: normalizedScroll.autoScroll,
|
|
509
546
|
enableWheelScroll,
|
|
510
|
-
onWindowChange:
|
|
547
|
+
onWindowChange: hasOnDataZoomWindowChanged
|
|
511
548
|
? (startPercent, endPercent) =>
|
|
512
|
-
|
|
549
|
+
emitZoomWindow(
|
|
513
550
|
percentWindowToIndexWindow(
|
|
514
551
|
startPercent,
|
|
515
552
|
endPercent,
|
|
@@ -525,7 +562,8 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
525
562
|
normalizedScroll,
|
|
526
563
|
sliderDataZoom,
|
|
527
564
|
xAxisData,
|
|
528
|
-
|
|
565
|
+
hasOnDataZoomWindowChanged,
|
|
566
|
+
emitZoomWindow,
|
|
529
567
|
]);
|
|
530
568
|
|
|
531
569
|
// Clean up resources when the component unmounts
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
defaultBubbleColorList,
|
|
13
13
|
drawCircles,
|
|
14
14
|
} from "../_utils/bubble";
|
|
15
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
15
16
|
|
|
16
17
|
export interface BubbleDataItem {
|
|
17
18
|
name: string;
|
|
@@ -370,6 +371,16 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
370
371
|
}
|
|
371
372
|
}, [chartOption, data]);
|
|
372
373
|
|
|
374
|
+
const onChartRef = useLatestRef(onChart);
|
|
375
|
+
|
|
376
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
|
|
377
|
+
// 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
|
|
378
|
+
// 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
|
|
379
|
+
const onClickRef = useLatestRef(onClick);
|
|
380
|
+
const hasOnClick = !!onClick;
|
|
381
|
+
|
|
382
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
383
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
373
384
|
// Initialize the chart
|
|
374
385
|
useEffect(() => {
|
|
375
386
|
if (!chartRef.current) return;
|
|
@@ -382,7 +393,7 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
382
393
|
chart = echarts.init(chartRef.current);
|
|
383
394
|
chartInstanceRef.current = chart;
|
|
384
395
|
// Call the onChart callback on first initialization
|
|
385
|
-
|
|
396
|
+
onChartRef.current?.(chart);
|
|
386
397
|
}
|
|
387
398
|
|
|
388
399
|
// Resize first so echarts picks up the correct container size, then apply the option
|
|
@@ -393,16 +404,19 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
393
404
|
window.addEventListener("resize", handleResize);
|
|
394
405
|
|
|
395
406
|
// Add click event
|
|
396
|
-
|
|
397
|
-
|
|
407
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
408
|
+
onClickRef.current?.(event);
|
|
409
|
+
};
|
|
410
|
+
if (hasOnClick) {
|
|
411
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
398
412
|
}
|
|
399
413
|
|
|
400
414
|
// Cleanup function
|
|
401
415
|
return () => {
|
|
402
416
|
window.removeEventListener("resize", handleResize);
|
|
403
417
|
|
|
404
|
-
if (
|
|
405
|
-
chartInstanceRef.current?.off("click",
|
|
418
|
+
if (hasOnClick) {
|
|
419
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
406
420
|
}
|
|
407
421
|
};
|
|
408
422
|
}, [
|
|
@@ -410,8 +424,9 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
410
424
|
containerSize.width,
|
|
411
425
|
containerSize.height,
|
|
412
426
|
handleResize,
|
|
413
|
-
|
|
414
|
-
|
|
427
|
+
onChartRef,
|
|
428
|
+
onClickRef,
|
|
429
|
+
hasOnClick,
|
|
415
430
|
]);
|
|
416
431
|
|
|
417
432
|
// Dispose the chart instance when the component unmounts
|
|
@@ -3,6 +3,7 @@ import useContainerReady from "../_hooks/useContainerReady";
|
|
|
3
3
|
import styles from "../index.module.scss";
|
|
4
4
|
import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
|
|
5
5
|
import * as echarts from "echarts";
|
|
6
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
6
7
|
|
|
7
8
|
const Common: React.FC<ChartCommonProps> = (props) => {
|
|
8
9
|
const { className, style, onChart, ...coreOption } = props;
|
|
@@ -31,6 +32,10 @@ const Common: React.FC<ChartCommonProps> = (props) => {
|
|
|
31
32
|
chartInstanceRef.current?.resize();
|
|
32
33
|
}, []);
|
|
33
34
|
|
|
35
|
+
const onChartRef = useLatestRef(onChart);
|
|
36
|
+
|
|
37
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
38
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
34
39
|
// Initialize the chart
|
|
35
40
|
useEffect(() => {
|
|
36
41
|
if (!chartRef.current || !containerReady) return;
|
|
@@ -46,7 +51,7 @@ const Common: React.FC<ChartCommonProps> = (props) => {
|
|
|
46
51
|
chart.setOption(chartOption as ChartOptionType, true);
|
|
47
52
|
|
|
48
53
|
// Chart-ready callback (can be used for legend auto-scroll, etc.)
|
|
49
|
-
|
|
54
|
+
onChartRef.current?.(chart);
|
|
50
55
|
|
|
51
56
|
// Add resize listener
|
|
52
57
|
window.addEventListener("resize", handleResize);
|
|
@@ -61,7 +66,7 @@ const Common: React.FC<ChartCommonProps> = (props) => {
|
|
|
61
66
|
return () => {
|
|
62
67
|
window.removeEventListener("resize", handleResize);
|
|
63
68
|
};
|
|
64
|
-
}, [chartOption, handleResize,
|
|
69
|
+
}, [chartOption, handleResize, onChartRef,
|
|
65
70
|
containerReady,
|
|
66
71
|
]);
|
|
67
72
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../_utils/heatmap";
|
|
10
10
|
import { resolveChartChrome } from "../_utils/chartTheme";
|
|
11
11
|
import useIsDark from "../../../hooks/useIsDark";
|
|
12
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
12
13
|
|
|
13
14
|
export interface HeatmapDataItem {
|
|
14
15
|
/** X-axis index or name */
|
|
@@ -266,6 +267,16 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
266
267
|
chartInstanceRef.current?.resize();
|
|
267
268
|
}, []);
|
|
268
269
|
|
|
270
|
+
const onChartRef = useLatestRef(onChart);
|
|
271
|
+
|
|
272
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
|
|
273
|
+
// 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
|
|
274
|
+
// 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
|
|
275
|
+
const onClickRef = useLatestRef(onClick);
|
|
276
|
+
const hasOnClick = !!onClick;
|
|
277
|
+
|
|
278
|
+
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
279
|
+
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
269
280
|
// Initialize the chart
|
|
270
281
|
useEffect(() => {
|
|
271
282
|
if (!chartRef.current || !containerReady) return;
|
|
@@ -276,7 +287,7 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
276
287
|
chart = echarts.init(chartRef.current);
|
|
277
288
|
chartInstanceRef.current = chart;
|
|
278
289
|
// Call the onChart callback on first initialization
|
|
279
|
-
|
|
290
|
+
onChartRef.current?.(chart);
|
|
280
291
|
}
|
|
281
292
|
|
|
282
293
|
// Apply the option
|
|
@@ -292,19 +303,27 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
292
303
|
}
|
|
293
304
|
|
|
294
305
|
// Add click event
|
|
295
|
-
|
|
296
|
-
|
|
306
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
307
|
+
onClickRef.current?.(event);
|
|
308
|
+
};
|
|
309
|
+
if (hasOnClick) {
|
|
310
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
297
311
|
}
|
|
298
312
|
|
|
299
313
|
// Cleanup function
|
|
300
314
|
return () => {
|
|
301
315
|
window.removeEventListener("resize", handleResize);
|
|
302
316
|
|
|
303
|
-
if (
|
|
304
|
-
chartInstanceRef.current?.off("click",
|
|
317
|
+
if (hasOnClick) {
|
|
318
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
305
319
|
}
|
|
306
320
|
};
|
|
307
|
-
}, [
|
|
321
|
+
}, [
|
|
322
|
+
chartOption,
|
|
323
|
+
handleResize,
|
|
324
|
+
onChartRef,
|
|
325
|
+
onClickRef,
|
|
326
|
+
hasOnClick,
|
|
308
327
|
containerReady,
|
|
309
328
|
]);
|
|
310
329
|
|