@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.
Files changed (90) hide show
  1. package/es/components/ChainGraph/_hooks/useChainGraphData.js +29 -8
  2. package/es/components/ChainGraph/_hooks/useChainGraphLayout.js +14 -3
  3. package/es/components/Chart/Bar/index.js +49 -19
  4. package/es/components/Chart/Bubble/index.js +21 -6
  5. package/es/components/Chart/Common/index.js +7 -2
  6. package/es/components/Chart/Heatmap/index.js +21 -6
  7. package/es/components/Chart/Line/index.js +49 -19
  8. package/es/components/Chart/Pie/Pie3D/index.js +22 -6
  9. package/es/components/Chart/Pie/index.js +21 -6
  10. package/es/components/Chart/Sankey/index.js +11 -2
  11. package/es/components/Chart/Tree/index.js +11 -2
  12. package/es/components/Chat/ChatInput/index.js +7 -2
  13. package/es/components/Icon/index.js +8 -2
  14. package/es/components/Input/Number/index.js +28 -21
  15. package/es/components/Input/Password/index.js +44 -36
  16. package/es/components/Input/Search/index.js +44 -36
  17. package/es/components/Input/TextArea/index.js +64 -42
  18. package/es/components/Input/index.js +40 -34
  19. package/es/components/Select/_hooks/useSelectComposition.js +6 -2
  20. package/es/components/Select/_hooks/useSelectInputOptions.js +8 -2
  21. package/es/components/Slider/index.js +24 -15
  22. package/es/components/Table/_hooks/useAutoScrolling.js +16 -7
  23. package/es/components/Upload/_hooks/useUploadFileList.js +8 -2
  24. package/es/components/Upload/_hooks/useUploadOperations.js +8 -2
  25. package/es/hooks/useLatestRef.d.ts +24 -0
  26. package/es/hooks/useLatestRef.js +29 -0
  27. package/es/layout/Menu/_hooks/useOnlyLvOneMenu.js +9 -2
  28. package/lib/components/ChainGraph/_hooks/useChainGraphData.js +24 -7
  29. package/lib/components/ChainGraph/_hooks/useChainGraphLayout.js +12 -2
  30. package/lib/components/Chart/Bar/index.js +45 -18
  31. package/lib/components/Chart/Bubble/index.js +19 -6
  32. package/lib/components/Chart/Common/index.js +6 -2
  33. package/lib/components/Chart/Heatmap/index.js +19 -6
  34. package/lib/components/Chart/Line/index.js +45 -18
  35. package/lib/components/Chart/Pie/Pie3D/index.js +20 -6
  36. package/lib/components/Chart/Pie/index.js +19 -6
  37. package/lib/components/Chart/Sankey/index.js +10 -2
  38. package/lib/components/Chart/Tree/index.js +10 -2
  39. package/lib/components/Chat/ChatInput/index.js +6 -2
  40. package/lib/components/Icon/index.js +7 -2
  41. package/lib/components/Input/Number/index.js +27 -18
  42. package/lib/components/Input/Password/index.js +40 -26
  43. package/lib/components/Input/Search/index.js +42 -26
  44. package/lib/components/Input/TextArea/index.js +54 -26
  45. package/lib/components/Input/index.js +37 -24
  46. package/lib/components/Select/_hooks/useSelectComposition.js +6 -2
  47. package/lib/components/Select/_hooks/useSelectInputOptions.js +7 -2
  48. package/lib/components/Slider/index.js +23 -11
  49. package/lib/components/Table/_hooks/useAutoScrolling.js +16 -7
  50. package/lib/components/Upload/_hooks/useUploadFileList.js +7 -2
  51. package/lib/components/Upload/_hooks/useUploadOperations.js +7 -2
  52. package/lib/hooks/useLatestRef.d.ts +24 -0
  53. package/lib/hooks/useLatestRef.js +35 -0
  54. package/lib/layout/Menu/_hooks/useOnlyLvOneMenu.js +8 -2
  55. package/package.json +9 -3
  56. package/src/__tests__/callbackDepsGuard.test.ts +119 -0
  57. package/src/__tests__/setup.ts +26 -0
  58. package/src/components/ChainGraph/_hooks/callbacks.test.tsx +155 -0
  59. package/src/components/ChainGraph/_hooks/useChainGraphData.ts +38 -10
  60. package/src/components/ChainGraph/_hooks/useChainGraphLayout.ts +16 -3
  61. package/src/components/Chart/Bar/index.tsx +59 -21
  62. package/src/components/Chart/Bubble/index.tsx +22 -7
  63. package/src/components/Chart/Common/index.tsx +7 -2
  64. package/src/components/Chart/Heatmap/index.tsx +25 -6
  65. package/src/components/Chart/Line/index.tsx +59 -21
  66. package/src/components/Chart/Pie/Pie3D/index.tsx +22 -7
  67. package/src/components/Chart/Pie/index.tsx +22 -7
  68. package/src/components/Chart/Sankey/index.tsx +10 -4
  69. package/src/components/Chart/Tree/index.tsx +10 -4
  70. package/src/components/Chart/callbacks.test.tsx +315 -0
  71. package/src/components/Chat/ChatInput/index.tsx +7 -2
  72. package/src/components/Icon/index.tsx +7 -2
  73. package/src/components/Input/Number/index.tsx +25 -23
  74. package/src/components/Input/Password/index.tsx +37 -26
  75. package/src/components/Input/Search/index.tsx +39 -26
  76. package/src/components/Input/TextArea/index.test.tsx +208 -0
  77. package/src/components/Input/TextArea/index.tsx +56 -30
  78. package/src/components/Input/index.test.tsx +189 -0
  79. package/src/components/Input/index.tsx +36 -29
  80. package/src/components/Select/_hooks/useSelectComposition.test.ts +47 -0
  81. package/src/components/Select/_hooks/useSelectComposition.ts +6 -2
  82. package/src/components/Select/_hooks/useSelectInputOptions.ts +6 -9
  83. package/src/components/Slider/index.tsx +22 -12
  84. package/src/components/Table/_hooks/useAutoScrolling.test.tsx +95 -0
  85. package/src/components/Table/_hooks/useAutoScrolling.ts +16 -7
  86. package/src/components/Upload/_hooks/useUploadFileList.ts +6 -2
  87. package/src/components/Upload/_hooks/useUploadOperations.ts +7 -2
  88. package/src/hooks/useLatestRef.ts +31 -0
  89. package/src/layout/Menu/_hooks/useOnlyLvOneMenu.test.tsx +56 -0
  90. package/src/layout/Menu/_hooks/useOnlyLvOneMenu.ts +8 -2
@@ -8,6 +8,7 @@ var _react = _interopRequireWildcard(require("react"));
8
8
  var _useContainerReady = _interopRequireDefault(require("../_hooks/useContainerReady"));
9
9
  var _indexModule = _interopRequireDefault(require("../index.module.scss"));
10
10
  var echarts = _interopRequireWildcard(require("echarts"));
11
+ var _useLatestRef = _interopRequireDefault(require("../../../hooks/useLatestRef"));
11
12
  var _jsxRuntime = require("react/jsx-runtime");
12
13
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
14
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -104,6 +105,13 @@ const Sankey = props => {
104
105
  return option;
105
106
  }, [seriesData, seriesLinks, series]);
106
107
 
108
+ // getImage 只是「出图后把 dataURL 交出去」的出口,既不决定图怎么画,
109
+ // 也不该决定要不要重画。它进依赖数组会让 effect 跟着重跑 → setOption(notMerge)
110
+ // → 图重绘 → 再触发一次 finished → 再回调;消费方把图存进 state 就是死循环。
111
+ // 而且 finished 监听只在首次 init 时注册,闭包里锁死的是首帧的 getImage,
112
+ // 后续换了引用永远调不到 —— 依赖数组写了它也没用,是条死依赖。走 ref 两个问题一起解决。
113
+ const getImageRef = (0, _useLatestRef.default)(getImage);
114
+
107
115
  // Callback handling chart resize
108
116
  const handleResize = (0, _react.useCallback)(() => {
109
117
  // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
@@ -125,7 +133,7 @@ const Sankey = props => {
125
133
 
126
134
  // Attach the finished event listener on first initialization
127
135
  chart.on("finished", () => {
128
- getImage?.(chart.getDataURL({
136
+ getImageRef.current?.(chart.getDataURL({
129
137
  type: "png",
130
138
  pixelRatio: 1,
131
139
  backgroundColor: "#fff"
@@ -149,7 +157,7 @@ const Sankey = props => {
149
157
  return () => {
150
158
  window.removeEventListener("resize", handleResize);
151
159
  };
152
- }, [chartOption, handleResize, getImage, containerReady]);
160
+ }, [chartOption, handleResize, getImageRef, containerReady]);
153
161
 
154
162
  // Clean up resources on unmount
155
163
  (0, _react.useEffect)(() => {
@@ -8,6 +8,7 @@ var _react = _interopRequireWildcard(require("react"));
8
8
  var _useContainerReady = _interopRequireDefault(require("../_hooks/useContainerReady"));
9
9
  var _indexModule = _interopRequireDefault(require("../index.module.scss"));
10
10
  var echarts = _interopRequireWildcard(require("echarts"));
11
+ var _useLatestRef = _interopRequireDefault(require("../../../hooks/useLatestRef"));
11
12
  var _hsuUtils = require("hsu-utils");
12
13
  var _tree = require("../_utils/tree");
13
14
  var _jsxRuntime = require("react/jsx-runtime");
@@ -44,6 +45,13 @@ const Tree = props => {
44
45
  return option;
45
46
  }, [seriesData, series]);
46
47
 
48
+ // getImage 只是「出图后把 dataURL 交出去」的出口,既不决定图怎么画,
49
+ // 也不该决定要不要重画。它进依赖数组会让 effect 跟着重跑 → setOption(notMerge)
50
+ // → 图重绘 → 再触发一次 finished → 再回调;消费方把图存进 state 就是死循环。
51
+ // 而且 finished 监听只在首次 init 时注册,闭包里锁死的是首帧的 getImage,
52
+ // 后续换了引用永远调不到 —— 依赖数组写了它也没用,是条死依赖。走 ref 两个问题一起解决。
53
+ const getImageRef = (0, _useLatestRef.default)(getImage);
54
+
47
55
  // Callback handling chart resize
48
56
  const handleResize = (0, _react.useCallback)(() => {
49
57
  // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
@@ -65,7 +73,7 @@ const Tree = props => {
65
73
 
66
74
  // Attach the finished event listener on first initialization
67
75
  chart.on("finished", () => {
68
- getImage?.(chart.getDataURL({
76
+ getImageRef.current?.(chart.getDataURL({
69
77
  type: "png",
70
78
  pixelRatio: 1,
71
79
  backgroundColor: "#fff"
@@ -89,7 +97,7 @@ const Tree = props => {
89
97
  return () => {
90
98
  window.removeEventListener("resize", handleResize);
91
99
  };
92
- }, [chartOption, handleResize, getImage, containerReady]);
100
+ }, [chartOption, handleResize, getImageRef, containerReady]);
93
101
 
94
102
  // Clean up resources on unmount
95
103
  (0, _react.useEffect)(() => {
@@ -12,6 +12,7 @@ var _hooks = require("./_hooks");
12
12
  var _components = require("./_components");
13
13
  var _classnames = _interopRequireDefault(require("classnames"));
14
14
  var _Button = _interopRequireDefault(require("../../Button"));
15
+ var _useLatestRef = require("../../../hooks/useLatestRef");
15
16
  var _jsxRuntime = require("react/jsx-runtime");
16
17
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
18
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -61,7 +62,10 @@ const ChatInput = props => {
61
62
  const isControlled = externalAgentsState !== undefined;
62
63
  // Use external state or internal state
63
64
  const agentsState = isControlled ? externalAgentsState : internalAgentsState;
65
+ const onAgentsChangeRef = (0, _useLatestRef.useLatestRef)(onAgentsChange);
64
66
 
67
+ // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
68
+ // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
65
69
  // When the model switches, sync the agents state (only in uncontrolled mode)
66
70
  (0, _react.useEffect)(() => {
67
71
  if (isControlled) return; // In controlled mode, state is managed externally
@@ -69,9 +73,9 @@ const ChatInput = props => {
69
73
  const currentModel = modelConfig?.modelList.find(m => m.value === modelConfig?.modelType);
70
74
  if (currentModel?.agents && currentModel.agents.length > 0) {
71
75
  setInternalAgentsState(currentModel.agents);
72
- onAgentsChange?.(currentModel.agents);
76
+ onAgentsChangeRef.current?.(currentModel.agents);
73
77
  }
74
- }, [modelConfig?.modelType, modelConfig?.modelList, onAgentsChange, isControlled]);
78
+ }, [modelConfig?.modelType, modelConfig?.modelList, onAgentsChangeRef, isControlled]);
75
79
 
76
80
  // Based on the currently selected model, filter the feature configs supported by that model
77
81
  const currentModel = modelConfig?.modelList.find(m => m.value === modelConfig?.modelType);
@@ -9,6 +9,7 @@ var AntdIcons = _interopRequireWildcard(require("@ant-design/icons"));
9
9
  var _react2 = _interopRequireWildcard(require("react"));
10
10
  var _classnames = _interopRequireDefault(require("classnames"));
11
11
  var _indexModule = _interopRequireDefault(require("./index.module.scss"));
12
+ var _useLatestRef = require("../../hooks/useLatestRef");
12
13
  var _jsxRuntime = require("react/jsx-runtime");
13
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
15
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -48,9 +49,13 @@ const Icon = /*#__PURE__*/_react2.default.forwardRef((props, forwardedRef) => {
48
49
  forwardedRef.current = node;
49
50
  }
50
51
  }, [forwardedRef]);
52
+ const onRefRef = (0, _useLatestRef.useLatestRef)(onRef);
53
+
54
+ // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
55
+ // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
51
56
  (0, _react2.useEffect)(() => {
52
- onRef?.(ref);
53
- }, [onRef, ref]);
57
+ onRefRef.current?.(ref);
58
+ }, [onRefRef, ref]);
54
59
  const mergedStyle = {
55
60
  color,
56
61
  fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize,
@@ -9,7 +9,7 @@ var _react = _interopRequireWildcard(require("react"));
9
9
  var _icons = require("@ant-design/icons");
10
10
  var _classnames = _interopRequireDefault(require("classnames"));
11
11
  var _indexModule = _interopRequireDefault(require("./index.module.scss"));
12
- var _ahooks = require("ahooks");
12
+ var _useLatestRef = require("../../../hooks/useLatestRef");
13
13
  var _jsxRuntime = require("react/jsx-runtime");
14
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
15
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -32,20 +32,26 @@ const InputNumber = props => {
32
32
  // `getRef` keeps handing consumers an `HTMLInputElement` exactly as before.
33
33
  const ref = (0, _react.useRef)(null);
34
34
  const wrapperRef = (0, _react.useRef)(null);
35
+ const getRefRef = (0, _useLatestRef.useLatestRef)(getRef);
35
36
 
36
37
  // On initialization, prefer value, then fall back to defaultValue
37
38
  const initialValue = value !== undefined ? typeof value === "number" ? `${value}` : value?.toString() ?? "" : defaultValue !== undefined ? typeof defaultValue === "number" ? `${defaultValue}` : defaultValue?.toString() ?? "" : "";
38
39
  const [_value, setValue] = (0, _react.useState)(initialValue);
39
- const [lastValue, setLastValue] = (0, _react.useState)(initialValue);
40
40
  const prevValueRef = (0, _react.useRef)(undefined);
41
- (0, _ahooks.useDebounceEffect)(() => {
42
- if (_value !== lastValue) {
43
- setLastValue(_value);
44
- onChange?.(_value);
45
- }
46
- }, [_value, lastValue, onChange], {
47
- wait: 10
48
- });
41
+ // 最近一次通知出去的文本,代替原来那个用 state 记账的写法
42
+ const notifiedRef = (0, _react.useRef)(initialValue);
43
+
44
+ /**
45
+ * `onChange` 是「用户改了输入」这个**事件**的通知,不是从 state 推导出来的结果,
46
+ * 所以在事件处理里发,不在 effect 里发。详见 `TextArea/index.tsx` 里那段说明——
47
+ * 旧写法(依赖数组里放 `onChange`、体内又调它、用 state 记「通知过没有」)
48
+ * 会让传内联箭头的消费方死循环。
49
+ */
50
+ const notify = next => {
51
+ if (next === notifiedRef.current) return;
52
+ notifiedRef.current = next;
53
+ onChange?.(next);
54
+ };
49
55
  (0, _react.useEffect)(() => {
50
56
  // Update internal state only when the external value prop actually changes
51
57
  if (prevValueRef.current !== value) {
@@ -62,30 +68,32 @@ const InputNumber = props => {
62
68
  // 表示的是同一个数,此时以用户正在敲的那份文本为准。
63
69
  const sameNumber = value !== undefined && value !== null && _value !== "" && Number(value) === Number(_value);
64
70
  if (sameNumber) {
65
- // 只同步「已提交值」,不动正在编辑的文本
66
- setLastValue(_value);
71
+ // 只同步「已通知过的值」,不动正在编辑的文本
72
+ notifiedRef.current = _value;
67
73
  } else if (value !== undefined && value !== null) {
68
74
  const newValue = typeof value === "number" ? `${value}` : value?.toString();
75
+ // 外部把值改了,之前通知过什么就不作数了,重新以外部值为准
76
+ notifiedRef.current = newValue;
69
77
  setValue(newValue);
70
- setLastValue(newValue);
71
78
  } else {
72
79
  // Clear only on initialization or when explicitly set to undefined/null externally
80
+ notifiedRef.current = "";
73
81
  setValue("");
74
- setLastValue("");
75
82
  }
76
83
  }
77
84
  // `_value` 是刻意不进依赖的:这个 effect 只该在**外部** value 变化时跑,
78
85
  // 把它加进来会让每次键入都重跑一遍,等于绕过上面那个 prevValueRef 判断
79
86
  // eslint-disable-next-line react-hooks/exhaustive-deps
80
87
  }, [value]);
88
+
89
+ // 只在挂载时把 ref 交出去。`getRef` 进依赖数组同样会被内联箭头带着每次渲染重跑
81
90
  (0, _react.useEffect)(() => {
82
- getRef?.(ref.current);
83
- }, [getRef]);
91
+ getRefRef.current?.(ref.current);
92
+ }, [getRefRef]);
84
93
  const handleClear = e => {
85
94
  e.stopPropagation();
86
95
  setValue("");
87
- setLastValue("");
88
- onChange?.("");
96
+ notify("");
89
97
  };
90
98
  const showClear = allowClear && _value !== "" && _value !== undefined && !disabled;
91
99
  const input = /*#__PURE__*/(0, _jsxRuntime.jsx)(_antd.InputNumber, {
@@ -94,6 +102,7 @@ const InputNumber = props => {
94
102
  onChange: e => {
95
103
  const newValue = e === null || e === undefined ? "" : (typeof e === "number" ? e : e || "").toString();
96
104
  setValue(newValue);
105
+ notify(newValue);
97
106
  },
98
107
  className: (0, _classnames.default)(_indexModule.default.antdInput, className),
99
108
  controls: false,
@@ -8,6 +8,7 @@ var _Password = _interopRequireDefault(require("antd/es/input/Password"));
8
8
  var _react = _interopRequireWildcard(require("react"));
9
9
  var _classnames = _interopRequireDefault(require("classnames"));
10
10
  var _indexModule = _interopRequireDefault(require("./index.module.scss"));
11
+ var _useLatestRef = require("../../../hooks/useLatestRef");
11
12
  var _jsxRuntime = require("react/jsx-runtime");
12
13
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
13
14
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
@@ -21,51 +22,64 @@ const Password = props => {
21
22
  className,
22
23
  ...inputConfig
23
24
  } = props;
24
- const [isComposing, setComposing] = (0, _react.useState)(false);
25
25
  const ref = (0, _react.useRef)(null);
26
+ const getRefRef = (0, _useLatestRef.useLatestRef)(getRef);
27
+ // 输入法组字期间不往外通知。放 ref 不放 state:它只决定「要不要发通知」,
28
+ // 不参与渲染,而且必须在同一次事件里立刻生效
29
+ const composingRef = (0, _react.useRef)(false);
26
30
 
27
31
  // On initialization, prefer value, then fall back to defaultValue
28
32
  const initialValue = value !== undefined ? value?.toString() ?? "" : defaultValue !== undefined ? defaultValue?.toString() ?? "" : "";
29
33
  const [_value, setValue] = (0, _react.useState)(initialValue);
30
- const [lastValue, setLastValue] = (0, _react.useState)(initialValue);
31
34
  const prevValueRef = (0, _react.useRef)(undefined);
32
- (0, _react.useEffect)(() => {
33
- if (!isComposing && _value !== lastValue) {
34
- const trimmedValue = _value.trim();
35
- const finalValue = trimmedValue === "" ? "" : _value;
36
- setLastValue(finalValue);
37
- onChange?.(finalValue);
38
- }
39
- }, [_value, isComposing, lastValue, onChange]);
35
+ // 最近一次通知出去的原文。只用来去掉 compositionend 与 input 两个事件的重复通知
36
+ const notifiedRef = (0, _react.useRef)(initialValue);
37
+
38
+ /**
39
+ * `onChange` 是「用户改了输入」这个**事件**的通知,不是从 state 推导出来的结果,
40
+ * 所以在事件处理里发,不在 effect 里发。详见 `TextArea/index.tsx` 里那段说明——
41
+ * 旧写法(依赖数组里放 `onChange`、体内又调它、用 state 记「通知过没有」)
42
+ * 会让传内联箭头的消费方死循环。
43
+ */
44
+ const notify = next => {
45
+ if (next === notifiedRef.current) return;
46
+ notifiedRef.current = next;
47
+ // 全是空白等同于空
48
+ onChange?.(next.trim() === "" ? "" : next);
49
+ };
40
50
  (0, _react.useEffect)(() => {
41
51
  // Update internal state only when the external value prop actually changes
42
52
  if (prevValueRef.current !== value) {
43
53
  prevValueRef.current = value;
44
- if (value !== undefined) {
45
- setValue(value?.toString());
46
- setLastValue(value?.toString());
47
- } else {
48
- // Clear only on initialization or when explicitly set to undefined externally
49
- setValue("");
50
- setLastValue("");
51
- }
54
+
55
+ // 外部把值改了,之前通知过什么就不作数了,重新以外部值为准
56
+ const next = value !== undefined ? value?.toString() ?? "" : "";
57
+ notifiedRef.current = next;
58
+ setValue(next);
52
59
  }
53
60
  }, [value]);
61
+
62
+ // 只在挂载时把 ref 交出去。`getRef` 进依赖数组同样会被内联箭头带着每次渲染重跑
54
63
  (0, _react.useEffect)(() => {
55
- getRef?.(ref.current);
56
- }, [getRef]);
64
+ getRefRef.current?.(ref.current);
65
+ }, [getRefRef]);
57
66
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Password.default, {
58
67
  className: (0, _classnames.default)(_indexModule.default.antdInput, className),
59
68
  ref: ref,
60
- onCompositionStart: () => setComposing(true),
61
- onCompositionEnd: () => {
62
- setTimeout(() => {
63
- setComposing(false);
64
- }, 1);
69
+ onCompositionStart: () => {
70
+ composingRef.current = true;
71
+ },
72
+ onCompositionEnd: e => {
73
+ composingRef.current = false;
74
+ // 组完字的那一下自己发通知。浏览器之间 compositionend 与 input 的先后
75
+ // 不一致,`notify` 里按原文去重,两种顺序都只会发出一次
76
+ notify(e.currentTarget.value);
65
77
  },
66
78
  value: _value,
67
79
  onChange: e => {
68
- setValue(e.target.value);
80
+ const next = e.target.value;
81
+ setValue(next);
82
+ if (!composingRef.current) notify(next);
69
83
  },
70
84
  ...inputConfig
71
85
  });
@@ -8,6 +8,7 @@ var _react = _interopRequireWildcard(require("react"));
8
8
  var _Search = _interopRequireDefault(require("antd/es/input/Search"));
9
9
  var _classnames = _interopRequireDefault(require("classnames"));
10
10
  var _indexModule = _interopRequireDefault(require("./index.module.scss"));
11
+ var _useLatestRef = require("../../../hooks/useLatestRef");
11
12
  var _jsxRuntime = require("react/jsx-runtime");
12
13
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
14
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -21,50 +22,65 @@ const Search = props => {
21
22
  className,
22
23
  ...inputConfig
23
24
  } = props;
24
- const [isComposing, setComposing] = (0, _react.useState)(false);
25
25
  const ref = (0, _react.useRef)(null);
26
+ const getRefRef = (0, _useLatestRef.useLatestRef)(getRef);
27
+ // 输入法组字期间不往外通知。放 ref 不放 state:它只决定「要不要发通知」,
28
+ // 不参与渲染,而且必须在同一次事件里立刻生效
29
+ const composingRef = (0, _react.useRef)(false);
26
30
 
27
31
  // On initialization, prefer value, then fall back to defaultValue
28
32
  const initialValue = value !== undefined ? value?.toString() ?? "" : defaultValue !== undefined ? defaultValue?.toString() ?? "" : "";
29
33
  const [_value, setValue] = (0, _react.useState)(initialValue);
30
- const [lastValue, setLastValue] = (0, _react.useState)(initialValue);
31
34
  const prevValueRef = (0, _react.useRef)(undefined);
32
- (0, _react.useEffect)(() => {
33
- if (!isComposing && _value !== lastValue) {
34
- const trimmedValue = _value.trim();
35
- const finalValue = trimmedValue === "" ? "" : _value;
36
- setLastValue(finalValue);
37
- onChange?.(finalValue);
38
- }
39
- }, [_value, isComposing, lastValue, onChange]);
35
+ // 最近一次通知出去的原文。只用来去掉 compositionend 与 input 两个事件的重复通知
36
+ const notifiedRef = (0, _react.useRef)(initialValue);
37
+
38
+ /**
39
+ * `onChange` 是「用户改了输入」这个**事件**的通知,不是从 state 推导出来的结果,
40
+ * 所以在事件处理里发,不在 effect 里发。详见 `TextArea/index.tsx` 里那段说明——
41
+ * 旧写法(依赖数组里放 `onChange`、体内又调它、用 state 记「通知过没有」)
42
+ * 会让传内联箭头的消费方死循环。
43
+ */
44
+ const notify = next => {
45
+ if (next === notifiedRef.current) return;
46
+ notifiedRef.current = next;
47
+ // 全是空白等同于空
48
+ onChange?.(next.trim() === "" ? "" : next);
49
+ };
40
50
  (0, _react.useEffect)(() => {
41
51
  // Update internal state only when the external value prop actually changes
42
52
  if (prevValueRef.current !== value) {
43
53
  prevValueRef.current = value;
44
- if (value !== undefined) {
45
- setValue(value?.toString());
46
- setLastValue(value?.toString());
47
- } else {
48
- // Clear only on initialization or when explicitly set to undefined externally
49
- setValue("");
50
- setLastValue("");
51
- }
54
+
55
+ // 外部把值改了,之前通知过什么就不作数了,重新以外部值为准
56
+ const next = value !== undefined ? value?.toString() ?? "" : "";
57
+ notifiedRef.current = next;
58
+ setValue(next);
52
59
  }
53
60
  }, [value]);
61
+
62
+ // 只在挂载时把 ref 交出去。`getRef` 进依赖数组同样会被内联箭头带着每次渲染重跑
54
63
  (0, _react.useEffect)(() => {
55
- getRef?.(ref.current);
56
- }, [getRef]);
64
+ getRefRef.current?.(ref.current);
65
+ }, [getRefRef]);
57
66
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Search.default, {
58
67
  ref: ref,
59
68
  className: (0, _classnames.default)(_indexModule.default.antdInput, className),
60
- onCompositionStart: () => setComposing(true),
61
- onCompositionEnd: () => {
62
- setTimeout(() => {
63
- setComposing(false);
64
- }, 1);
69
+ onCompositionStart: () => {
70
+ composingRef.current = true;
71
+ },
72
+ onCompositionEnd: e => {
73
+ composingRef.current = false;
74
+ // 组完字的那一下自己发通知。浏览器之间 compositionend 与 input 的先后
75
+ // 不一致,`notify` 里按原文去重,两种顺序都只会发出一次
76
+ notify(e.currentTarget.value);
65
77
  },
66
78
  value: _value,
67
- onChange: e => setValue(e.target.value),
79
+ onChange: e => {
80
+ const next = e.target.value;
81
+ setValue(next);
82
+ if (!composingRef.current) notify(next);
83
+ },
68
84
  ...inputConfig
69
85
  });
70
86
  };
@@ -8,6 +8,7 @@ var _TextArea = _interopRequireDefault(require("antd/es/input/TextArea"));
8
8
  var _react = _interopRequireWildcard(require("react"));
9
9
  var _classnames = _interopRequireDefault(require("classnames"));
10
10
  var _indexModule = _interopRequireDefault(require("./index.module.scss"));
11
+ var _useLatestRef = require("../../../hooks/useLatestRef");
11
12
  var _jsxRuntime = require("react/jsx-runtime");
12
13
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
13
14
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
@@ -33,41 +34,63 @@ const TextArea = props => {
33
34
  style,
34
35
  ...inputConfig
35
36
  } = props;
36
- const [isComposing, setComposing] = (0, _react.useState)(false);
37
37
  const ref = (0, _react.useRef)(null);
38
38
  const [focused, setFocused] = (0, _react.useState)(false);
39
39
 
40
40
  // On initialization, prefer value, then fall back to defaultValue
41
41
  const initialValue = value !== undefined ? value?.toString() ?? "" : defaultValue !== undefined ? defaultValue?.toString() ?? "" : "";
42
42
  const [_value, setValue] = (0, _react.useState)(initialValue);
43
- const [lastValue, setLastValue] = (0, _react.useState)(initialValue);
44
43
  const [hasError, setHasError] = (0, _react.useState)(false);
45
44
  const prevValueRef = (0, _react.useRef)(undefined);
46
- (0, _react.useEffect)(() => {
47
- if (!isComposing && _value !== lastValue) {
48
- const trimmedValue = _value.trim();
49
- const finalValue = trimmedValue === "" ? "" : _value;
50
- setLastValue(finalValue);
51
- onChange?.(finalValue);
52
- }
53
- }, [isComposing, onChange, _value, lastValue]);
45
+ const getRefRef = (0, _useLatestRef.useLatestRef)(getRef);
46
+
47
+ // 输入法组字期间不往外通知。放 ref 不放 state:它只影响「要不要发通知」,
48
+ // 不参与渲染,而且必须在同一次事件里立刻生效
49
+ const composingRef = (0, _react.useRef)(false);
50
+ // 最近一次通知出去的原文。只用来去掉 compositionend 与 input 两个事件的重复通知
51
+ const notifiedRef = (0, _react.useRef)(initialValue);
52
+
53
+ /**
54
+ * `onChange` 是「用户改了输入」这个**事件**的通知,不是从 state 推导出来的结果,
55
+ * 所以它在事件处理里发,不在 effect 里发。
56
+ *
57
+ * 早先是一个 effect 在发:依赖数组里放着 `onChange`,effect 体内又调它,
58
+ * 还用一个 `lastValue` state 记「这个值通知过没有」。两处都是错的——
59
+ * 1. 消费方传内联箭头(`onChange={(v) => setX(v)}`,React 里最常见的写法)时
60
+ * 每次渲染都是新引用,effect 每次渲染都重跑;
61
+ * 2. 「通知过没有」的记账放在 React state 里就**不可靠**:父级用 MobX /
62
+ * `useSyncExternalStore` 这类外部 store 回写 `value` 时,React 会以更高的
63
+ * 同步优先级重渲一次,effect 里排的那次 `setLastValue` 会被这一趟**跳过**,
64
+ * 于是 effect 读到过期的记账、判定「还没通知」,再发一次 → 父级再回写 →
65
+ * 死循环,React 抛 `Maximum update depth exceeded`,输入框被上层
66
+ * ErrorBoundary 整块摘掉。
67
+ *
68
+ * 现在 `onChange` 的引用稳不稳定完全不影响正确性:永远调到最新的那个回调,
69
+ * 且只在真的有新文本时调一次。
70
+ */
71
+ const notify = next => {
72
+ if (next === notifiedRef.current) return;
73
+ notifiedRef.current = next;
74
+ // 全是空白等同于空:外部拿到的是 "",但输入框里用户敲的空格照旧留着
75
+ onChange?.(next.trim() === "" ? "" : next);
76
+ };
54
77
  (0, _react.useEffect)(() => {
55
78
  // Update internal state only when the external value prop actually changes
56
79
  if (prevValueRef.current !== value) {
57
80
  prevValueRef.current = value;
58
- if (value !== undefined) {
59
- setValue(value?.toString());
60
- setLastValue(value?.toString());
61
- } else {
62
- // Clear only on initialization or when explicitly set to undefined externally
63
- setValue("");
64
- setLastValue("");
65
- }
81
+
82
+ // 外部把值改了,之前通知过什么就不作数了,重新以外部值为准
83
+ const next = value !== undefined ? value?.toString() ?? "" : "";
84
+ notifiedRef.current = next;
85
+ setValue(next);
66
86
  }
67
87
  }, [value]);
88
+
89
+ // 只在挂载时把 ref 交出去。`getRef` 进依赖数组同样会被内联箭头带着每次渲染重跑,
90
+ // 消费方在里面 setState 就是一个死循环
68
91
  (0, _react.useEffect)(() => {
69
- getRef?.(ref.current);
70
- }, [getRef]);
92
+ getRefRef.current?.(ref.current);
93
+ }, [getRefRef]);
71
94
  (0, _react.useEffect)(() => {
72
95
  // Use a MutationObserver to watch for error state changes
73
96
  const checkError = () => {
@@ -106,18 +129,23 @@ const TextArea = props => {
106
129
  style: style,
107
130
  children: [prefix, /*#__PURE__*/(0, _jsxRuntime.jsx)(_TextArea.default, {
108
131
  ref: ref,
109
- onCompositionStart: () => setComposing(true),
110
- onCompositionEnd: () => {
111
- setTimeout(() => {
112
- setComposing(false);
113
- }, 1);
132
+ onCompositionStart: () => {
133
+ composingRef.current = true;
134
+ },
135
+ onCompositionEnd: e => {
136
+ composingRef.current = false;
137
+ // 组完字的那一下自己发通知。浏览器之间 compositionend 与 input 的先后
138
+ // 不一致,`notify` 里按原文去重,两种顺序都只会发出一次
139
+ notify(e.currentTarget.value);
114
140
  },
115
141
  value: _value,
116
142
  autoSize: disabled && text ? autoSize || {
117
143
  minRows: 1
118
144
  } : autoSize,
119
145
  onChange: e => {
120
- setValue(e.target.value);
146
+ const next = e.target.value;
147
+ setValue(next);
148
+ if (!composingRef.current) notify(next);
121
149
  },
122
150
  onFocus: e => {
123
151
  setFocused(true);