@hsu-react/ui 2.4.1 → 2.4.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 (42) hide show
  1. package/es/components/Chart/Bar/index.js +9 -2
  2. package/es/components/Chart/Common/index.js +9 -2
  3. package/es/components/Chart/Gauge/index.js +9 -2
  4. package/es/components/Chart/Heatmap/index.js +9 -2
  5. package/es/components/Chart/Line/index.js +9 -2
  6. package/es/components/Chart/Pie/Pie3D/index.js +9 -2
  7. package/es/components/Chart/Pie/index.js +9 -2
  8. package/es/components/Chart/Polar/index.js +9 -2
  9. package/es/components/Chart/Radar/index.js +9 -2
  10. package/es/components/Chart/Sankey/index.js +9 -2
  11. package/es/components/Chart/Tree/index.js +9 -2
  12. package/es/components/Chart/_hooks/useContainerReady.d.ts +18 -0
  13. package/es/components/Chart/_hooks/useContainerReady.js +56 -0
  14. package/es/layout/NavTabBar/_hooks/useTabPath.js +45 -8
  15. package/lib/components/Chart/Bar/index.js +9 -2
  16. package/lib/components/Chart/Common/index.js +9 -2
  17. package/lib/components/Chart/Gauge/index.js +9 -2
  18. package/lib/components/Chart/Heatmap/index.js +9 -2
  19. package/lib/components/Chart/Line/index.js +9 -2
  20. package/lib/components/Chart/Pie/Pie3D/index.js +9 -2
  21. package/lib/components/Chart/Pie/index.js +9 -2
  22. package/lib/components/Chart/Polar/index.js +9 -2
  23. package/lib/components/Chart/Radar/index.js +9 -2
  24. package/lib/components/Chart/Sankey/index.js +9 -2
  25. package/lib/components/Chart/Tree/index.js +9 -2
  26. package/lib/components/Chart/_hooks/useContainerReady.d.ts +18 -0
  27. package/lib/components/Chart/_hooks/useContainerReady.js +51 -0
  28. package/lib/layout/NavTabBar/_hooks/useTabPath.js +42 -7
  29. package/package.json +1 -1
  30. package/src/components/Chart/Bar/index.tsx +10 -1
  31. package/src/components/Chart/Common/index.tsx +12 -2
  32. package/src/components/Chart/Gauge/index.tsx +12 -2
  33. package/src/components/Chart/Heatmap/index.tsx +12 -2
  34. package/src/components/Chart/Line/index.tsx +10 -1
  35. package/src/components/Chart/Pie/Pie3D/index.tsx +10 -1
  36. package/src/components/Chart/Pie/index.tsx +10 -1
  37. package/src/components/Chart/Polar/index.tsx +12 -2
  38. package/src/components/Chart/Radar/index.tsx +10 -1
  39. package/src/components/Chart/Sankey/index.tsx +12 -2
  40. package/src/components/Chart/Tree/index.tsx +12 -2
  41. package/src/components/Chart/_hooks/useContainerReady.ts +50 -0
  42. package/src/layout/NavTabBar/_hooks/useTabPath.ts +43 -10
@@ -0,0 +1,50 @@
1
+ import { RefObject, useEffect, useState } from "react";
2
+
3
+ /**
4
+ * Whether the referenced element has a non-zero box yet.
5
+ *
6
+ * `echarts.init` on a 0×0 element logs `Can't get DOM width or height` and builds a canvas with no
7
+ * size, recovering only on the next `resize()`. A chart hits that path whenever it mounts before
8
+ * layout has given it space — inside a keep-alive tab that is not the active one, a collapsed
9
+ * panel, a modal that has not opened yet — so consuming apps see the warning for charts that end
10
+ * up rendering perfectly well.
11
+ *
12
+ * Gating `init` on this flag defers it to the first frame the element actually occupies space.
13
+ *
14
+ * It **latches**: once measured, the element going back to 0×0 (a keep-alive tab losing focus)
15
+ * must not read as "not ready" again, or the init effect would tear the chart down and re-create
16
+ * it on every tab switch.
17
+ */
18
+ const useContainerReady = (ref: RefObject<HTMLElement | null>): boolean => {
19
+ const [ready, setReady] = useState(false);
20
+
21
+ useEffect(() => {
22
+ if (ready) return;
23
+
24
+ const el = ref.current;
25
+ if (!el) return;
26
+
27
+ const measure = (): boolean => {
28
+ const { width, height } = el.getBoundingClientRect();
29
+ if (width > 0 && height > 0) {
30
+ setReady(true);
31
+ return true;
32
+ }
33
+ return false;
34
+ };
35
+
36
+ // Already laid out on mount — the common case, and it must not cost an extra frame
37
+ if (measure()) return;
38
+
39
+ const observer = new ResizeObserver(() => {
40
+ if (measure()) observer.disconnect();
41
+ });
42
+ observer.observe(el);
43
+
44
+ return () => observer.disconnect();
45
+ }, [ready, ref]);
46
+
47
+ return ready;
48
+ };
49
+
50
+ export default useContainerReady;
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { useLocation } from "react-router";
3
3
  import { TabType } from "..";
4
4
  import { checkTabPathMatch } from "../_utils/pathMatch";
@@ -14,6 +14,17 @@ interface UseTabPathOptions {
14
14
  */
15
15
  export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
16
16
  const location = useLocation();
17
+
18
+ /**
19
+ * Keyed on the *contents* of `affixRouter`, not on the array itself.
20
+ *
21
+ * Callers pass an inline literal (`affixRouter={[HOME]}`), and omitting the prop hands the
22
+ * component a fresh `[]` default on every render — either way the array identity changes each
23
+ * time. Depending on it directly re-runs the effect below on every render, and that effect
24
+ * calls setState, so the whole thing becomes a render loop.
25
+ */
26
+ const affixKey = affixRouter.join("\u0001");
27
+ const affixSet = useMemo(() => new Set(affixKey ? affixKey.split("\u0001") : []), [affixKey]);
17
28
  const [tabKey, setTabKey] = useState<string>("");
18
29
  const [openKeys, setOpenkeys] = useState<TabType[]>([]);
19
30
 
@@ -27,7 +38,7 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
27
38
  _checkAffix(item.children);
28
39
  } else {
29
40
  // Check whether it is an affixed route
30
- const isAffix = affixRouter.includes(item.key) || item.affix;
41
+ const isAffix = affixSet.has(item.key) || item.affix;
31
42
  if (isAffix) {
32
43
  setOpenkeys((prev) => {
33
44
  const find = prev.find((i) => i.key === item.key);
@@ -40,7 +51,7 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
40
51
  }
41
52
  });
42
53
  },
43
- [affixRouter]
54
+ [affixSet]
44
55
  );
45
56
 
46
57
  const _checkPath = useCallback(
@@ -72,32 +83,42 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
72
83
  setTabKey(`${pathname}${search}`);
73
84
 
74
85
  setOpenkeys((prev) => {
86
+ const nextKey = `${pathname}${search}`;
75
87
  const find = prev.find((i) => i.key.split("?")[0] === pathname);
76
88
  if (find) {
89
+ // Already on this key: return `prev` untouched. `map` builds a new array
90
+ // every time, and React treats a new reference as a state change — which is
91
+ // the other half of the render loop above.
92
+ if (find.key === nextKey) {
93
+ return prev;
94
+ }
77
95
  return prev.map((i) =>
78
- i.key.split("?")[0] === pathname
79
- ? { ...i, key: `${pathname}${search}` }
80
- : i
96
+ i.key.split("?")[0] === pathname ? { ...i, key: nextKey } : i
81
97
  );
82
98
  }
83
99
 
84
- return [...prev, { ...item, key: `${pathname}${search}` }];
100
+ return [...prev, { ...item, key: nextKey }];
85
101
  });
86
102
  } else {
87
103
  // Handle plain routes
88
104
  setTabKey(`${item.key}${search}`);
89
105
 
90
106
  setOpenkeys((prev) => {
107
+ const nextKey = `${item.key}${search}`;
91
108
  const find = prev.find((i) => i.key.split("?")[0] === item.key);
92
109
  if (find) {
110
+ // Same as above: an unchanged tab must not produce a new reference
111
+ if (find.key === nextKey) {
112
+ return prev;
113
+ }
93
114
  return prev.map((i) =>
94
115
  i.key.split("?")[0] === item.key
95
- ? { ...i, key: `${item.key}${search}` }
116
+ ? { ...i, key: nextKey }
96
117
  : i
97
118
  );
98
119
  }
99
120
 
100
- return [...prev, { ...item, key: `${item.key}${search}` }];
121
+ return [...prev, { ...item, key: nextKey }];
101
122
  });
102
123
  }
103
124
  }
@@ -107,12 +128,24 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
107
128
  [location.pathname, location.search]
108
129
  );
109
130
 
131
+ /**
132
+ * Closing the last tab navigates to `basePath` — but when that tab *was* `basePath`, the
133
+ * location never changes, so path matching would not re-run and the bar would be left empty
134
+ * while its page is still on screen. Going empty has to re-trigger the match on its own.
135
+ *
136
+ * This settles rather than looping: re-matching adds the tab back, which flips `isEmpty` and
137
+ * runs the effect once more, and that pass finds the tab already present and returns the same
138
+ * state object — so React stops there. (Only true because the setters below bail out when
139
+ * nothing changed; without that this would spin.)
140
+ */
141
+ const isEmpty = openKeys.length === 0;
142
+
110
143
  useEffect(() => {
111
144
  // Handle affixed routes first to populate openKeys
112
145
  _checkAffix(items);
113
146
  // Then run path matching
114
147
  _checkPath(items);
115
- }, [_checkAffix, _checkPath, items]);
148
+ }, [_checkAffix, _checkPath, items, isEmpty]);
116
149
 
117
150
  return { tabKey, setTabKey, openKeys, setOpenkeys };
118
151
  };