@hsu-react/ui 2.5.2 → 2.5.4

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 (64) hide show
  1. package/es/components/Chart/Bubble/index.js +7 -2
  2. package/es/components/Chart/Common/index.js +7 -2
  3. package/es/components/Chart/Heatmap/index.js +7 -2
  4. package/es/components/Chart/Pie/Pie3D/index.js +8 -2
  5. package/es/components/Chart/Pie/index.js +7 -2
  6. package/es/components/Chat/ChatInput/index.js +7 -2
  7. package/es/components/Icon/index.js +8 -2
  8. package/es/components/Input/Number/index.js +28 -21
  9. package/es/components/Input/Password/index.js +44 -36
  10. package/es/components/Input/Search/index.js +44 -36
  11. package/es/components/Input/TextArea/index.js +64 -42
  12. package/es/components/Input/index.js +40 -34
  13. package/es/components/Select/_hooks/useSelectInputOptions.js +8 -2
  14. package/es/components/Slider/index.js +24 -15
  15. package/es/components/Upload/_hooks/useUploadFileList.js +8 -2
  16. package/es/components/Upload/_hooks/useUploadOperations.js +8 -2
  17. package/es/hooks/useLatestRef.d.ts +24 -0
  18. package/es/hooks/useLatestRef.js +29 -0
  19. package/es/layout/Menu/index.js +24 -6
  20. package/es/layout/Menu/index.module.scss +53 -10
  21. package/lib/components/Chart/Bubble/index.js +6 -2
  22. package/lib/components/Chart/Common/index.js +6 -2
  23. package/lib/components/Chart/Heatmap/index.js +6 -2
  24. package/lib/components/Chart/Pie/Pie3D/index.js +7 -2
  25. package/lib/components/Chart/Pie/index.js +6 -2
  26. package/lib/components/Chat/ChatInput/index.js +6 -2
  27. package/lib/components/Icon/index.js +7 -2
  28. package/lib/components/Input/Number/index.js +27 -18
  29. package/lib/components/Input/Password/index.js +40 -26
  30. package/lib/components/Input/Search/index.js +42 -26
  31. package/lib/components/Input/TextArea/index.js +54 -26
  32. package/lib/components/Input/index.js +37 -24
  33. package/lib/components/Select/_hooks/useSelectInputOptions.js +7 -2
  34. package/lib/components/Slider/index.js +23 -11
  35. package/lib/components/Upload/_hooks/useUploadFileList.js +7 -2
  36. package/lib/components/Upload/_hooks/useUploadOperations.js +7 -2
  37. package/lib/hooks/useLatestRef.d.ts +24 -0
  38. package/lib/hooks/useLatestRef.js +35 -0
  39. package/lib/layout/Menu/index.js +24 -6
  40. package/lib/layout/Menu/index.module.scss +53 -10
  41. package/package.json +9 -3
  42. package/src/__tests__/setup.ts +26 -0
  43. package/src/components/Chart/Bubble/index.tsx +7 -2
  44. package/src/components/Chart/Common/index.tsx +7 -2
  45. package/src/components/Chart/Heatmap/index.tsx +7 -2
  46. package/src/components/Chart/Pie/Pie3D/index.tsx +7 -2
  47. package/src/components/Chart/Pie/index.tsx +7 -2
  48. package/src/components/Chat/ChatInput/index.tsx +7 -2
  49. package/src/components/Icon/index.tsx +7 -2
  50. package/src/components/Input/Number/index.tsx +25 -23
  51. package/src/components/Input/Password/index.tsx +37 -26
  52. package/src/components/Input/Search/index.tsx +39 -26
  53. package/src/components/Input/TextArea/index.test.tsx +208 -0
  54. package/src/components/Input/TextArea/index.tsx +56 -30
  55. package/src/components/Input/index.test.tsx +189 -0
  56. package/src/components/Input/index.tsx +36 -29
  57. package/src/components/Select/_hooks/useSelectInputOptions.ts +6 -9
  58. package/src/components/Slider/index.tsx +22 -12
  59. package/src/components/Upload/_hooks/useUploadFileList.ts +6 -2
  60. package/src/components/Upload/_hooks/useUploadOperations.ts +7 -2
  61. package/src/hooks/useLatestRef.ts +31 -0
  62. package/src/layout/Menu/index.md +11 -0
  63. package/src/layout/Menu/index.module.scss +53 -10
  64. package/src/layout/Menu/index.tsx +24 -7
@@ -17,6 +17,7 @@ import AntdTextArea from "antd/es/input/TextArea";
17
17
  import React, { useEffect, useRef, useState } from "react";
18
18
  import classNames from "classnames";
19
19
  import styles from "./index.module.scss";
20
+ import { useLatestRef } from "../../../hooks/useLatestRef";
20
21
  import { jsx as _jsx } from "react/jsx-runtime";
21
22
  import { jsxs as _jsxs } from "react/jsx-runtime";
22
23
  var TextArea = function TextArea(props) {
@@ -40,56 +41,74 @@ var TextArea = function TextArea(props) {
40
41
  autoSize = props.autoSize,
41
42
  style = props.style,
42
43
  inputConfig = _objectWithoutProperties(props, _excluded);
44
+ var ref = useRef(null);
43
45
  var _useState = useState(false),
44
46
  _useState2 = _slicedToArray(_useState, 2),
45
- isComposing = _useState2[0],
46
- setComposing = _useState2[1];
47
- var ref = useRef(null);
48
- var _useState3 = useState(false),
49
- _useState4 = _slicedToArray(_useState3, 2),
50
- focused = _useState4[0],
51
- setFocused = _useState4[1];
47
+ focused = _useState2[0],
48
+ setFocused = _useState2[1];
52
49
 
53
50
  // On initialization, prefer value, then fall back to defaultValue
54
51
  var initialValue = value !== undefined ? (_value$toString = value === null || value === void 0 ? void 0 : value.toString()) !== null && _value$toString !== void 0 ? _value$toString : "" : defaultValue !== undefined ? (_defaultValue$toStrin = defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue.toString()) !== null && _defaultValue$toStrin !== void 0 ? _defaultValue$toStrin : "" : "";
55
- var _useState5 = useState(initialValue),
52
+ var _useState3 = useState(initialValue),
53
+ _useState4 = _slicedToArray(_useState3, 2),
54
+ _value = _useState4[0],
55
+ setValue = _useState4[1];
56
+ var _useState5 = useState(false),
56
57
  _useState6 = _slicedToArray(_useState5, 2),
57
- _value = _useState6[0],
58
- setValue = _useState6[1];
59
- var _useState7 = useState(initialValue),
60
- _useState8 = _slicedToArray(_useState7, 2),
61
- lastValue = _useState8[0],
62
- setLastValue = _useState8[1];
63
- var _useState9 = useState(false),
64
- _useState10 = _slicedToArray(_useState9, 2),
65
- hasError = _useState10[0],
66
- setHasError = _useState10[1];
58
+ hasError = _useState6[0],
59
+ setHasError = _useState6[1];
67
60
  var prevValueRef = useRef(undefined);
68
- useEffect(function () {
69
- if (!isComposing && _value !== lastValue) {
70
- var trimmedValue = _value.trim();
71
- var finalValue = trimmedValue === "" ? "" : _value;
72
- setLastValue(finalValue);
73
- onChange === null || onChange === void 0 || onChange(finalValue);
74
- }
75
- }, [isComposing, onChange, _value, lastValue]);
61
+ var getRefRef = useLatestRef(getRef);
62
+
63
+ // 输入法组字期间不往外通知。放 ref 不放 state:它只影响「要不要发通知」,
64
+ // 不参与渲染,而且必须在同一次事件里立刻生效
65
+ var composingRef = useRef(false);
66
+ // 最近一次通知出去的原文。只用来去掉 compositionend input 两个事件的重复通知
67
+ var notifiedRef = useRef(initialValue);
68
+
69
+ /**
70
+ * `onChange` 是「用户改了输入」这个**事件**的通知,不是从 state 推导出来的结果,
71
+ * 所以它在事件处理里发,不在 effect 里发。
72
+ *
73
+ * 早先是一个 effect 在发:依赖数组里放着 `onChange`,effect 体内又调它,
74
+ * 还用一个 `lastValue` state 记「这个值通知过没有」。两处都是错的——
75
+ * 1. 消费方传内联箭头(`onChange={(v) => setX(v)}`,React 里最常见的写法)时
76
+ * 每次渲染都是新引用,effect 每次渲染都重跑;
77
+ * 2. 「通知过没有」的记账放在 React state 里就**不可靠**:父级用 MobX /
78
+ * `useSyncExternalStore` 这类外部 store 回写 `value` 时,React 会以更高的
79
+ * 同步优先级重渲一次,effect 里排的那次 `setLastValue` 会被这一趟**跳过**,
80
+ * 于是 effect 读到过期的记账、判定「还没通知」,再发一次 → 父级再回写 →
81
+ * 死循环,React 抛 `Maximum update depth exceeded`,输入框被上层
82
+ * ErrorBoundary 整块摘掉。
83
+ *
84
+ * 现在 `onChange` 的引用稳不稳定完全不影响正确性:永远调到最新的那个回调,
85
+ * 且只在真的有新文本时调一次。
86
+ */
87
+ var notify = function notify(next) {
88
+ if (next === notifiedRef.current) return;
89
+ notifiedRef.current = next;
90
+ // 全是空白等同于空:外部拿到的是 "",但输入框里用户敲的空格照旧留着
91
+ onChange === null || onChange === void 0 || onChange(next.trim() === "" ? "" : next);
92
+ };
76
93
  useEffect(function () {
77
94
  // Update internal state only when the external value prop actually changes
78
95
  if (prevValueRef.current !== value) {
96
+ var _value$toString2;
79
97
  prevValueRef.current = value;
80
- if (value !== undefined) {
81
- setValue(value === null || value === void 0 ? void 0 : value.toString());
82
- setLastValue(value === null || value === void 0 ? void 0 : value.toString());
83
- } else {
84
- // Clear only on initialization or when explicitly set to undefined externally
85
- setValue("");
86
- setLastValue("");
87
- }
98
+
99
+ // 外部把值改了,之前通知过什么就不作数了,重新以外部值为准
100
+ var next = value !== undefined ? (_value$toString2 = value === null || value === void 0 ? void 0 : value.toString()) !== null && _value$toString2 !== void 0 ? _value$toString2 : "" : "";
101
+ notifiedRef.current = next;
102
+ setValue(next);
88
103
  }
89
104
  }, [value]);
105
+
106
+ // 只在挂载时把 ref 交出去。`getRef` 进依赖数组同样会被内联箭头带着每次渲染重跑,
107
+ // 消费方在里面 setState 就是一个死循环
90
108
  useEffect(function () {
91
- getRef === null || getRef === void 0 || getRef(ref.current);
92
- }, [getRef]);
109
+ var _getRefRef$current;
110
+ (_getRefRef$current = getRefRef.current) === null || _getRefRef$current === void 0 || _getRefRef$current.call(getRefRef, ref.current);
111
+ }, [getRefRef]);
93
112
  useEffect(function () {
94
113
  var _ref$current2;
95
114
  // Use a MutationObserver to watch for error state changes
@@ -124,19 +143,22 @@ var TextArea = function TextArea(props) {
124
143
  children: [prefix, /*#__PURE__*/_jsx(AntdTextArea, _objectSpread({
125
144
  ref: ref,
126
145
  onCompositionStart: function onCompositionStart() {
127
- return setComposing(true);
146
+ composingRef.current = true;
128
147
  },
129
- onCompositionEnd: function onCompositionEnd() {
130
- setTimeout(function () {
131
- setComposing(false);
132
- }, 1);
148
+ onCompositionEnd: function onCompositionEnd(e) {
149
+ composingRef.current = false;
150
+ // 组完字的那一下自己发通知。浏览器之间 compositionend 与 input 的先后
151
+ // 不一致,`notify` 里按原文去重,两种顺序都只会发出一次
152
+ notify(e.currentTarget.value);
133
153
  },
134
154
  value: _value,
135
155
  autoSize: disabled && text ? autoSize || {
136
156
  minRows: 1
137
157
  } : autoSize,
138
158
  onChange: function onChange(e) {
139
- setValue(e.target.value);
159
+ var next = e.target.value;
160
+ setValue(next);
161
+ if (!composingRef.current) notify(next);
140
162
  },
141
163
  onFocus: function onFocus(e) {
142
164
  setFocused(true);
@@ -22,7 +22,7 @@ import TextArea from "./TextArea";
22
22
  import classNames from "classnames";
23
23
  import styles from "./index.module.scss";
24
24
  import RangeInput from "./Range";
25
- import { useDebounceEffect } from "ahooks";
25
+ import { useLatestRef } from "../../hooks/useLatestRef";
26
26
  import { jsx as _jsx } from "react/jsx-runtime";
27
27
  var Input = function Input(props) {
28
28
  var onChange = props.onChange,
@@ -39,11 +39,11 @@ var Input = function Input(props) {
39
39
  tooltip = props.tooltip,
40
40
  escapeCharacters = props.escapeCharacters,
41
41
  inputConfig = _objectWithoutProperties(props, _excluded);
42
- var _useState = useState(false),
43
- _useState2 = _slicedToArray(_useState, 2),
44
- isComposing = _useState2[0],
45
- setComposing = _useState2[1];
46
42
  var ref = useRef(null);
43
+ var getRefRef = useLatestRef(getRef);
44
+ // 输入法组字期间不往外通知。放 ref 不放 state:它只决定「要不要发通知」,
45
+ // 不参与渲染,而且必须在同一次事件里立刻生效
46
+ var composingRef = useRef(false);
47
47
 
48
48
  // Handle escaping: if the value contains characters listed in escapeCharacters, prefix them with an escape
49
49
  var escapeValue = useCallback(function (val) {
@@ -112,27 +112,27 @@ var Input = function Input(props) {
112
112
  return unescapeValue(rawValue);
113
113
  };
114
114
  var initialValue = getInitialValue();
115
- var _useState3 = useState(initialValue),
116
- _useState4 = _slicedToArray(_useState3, 2),
117
- _value = _useState4[0],
118
- setValue = _useState4[1];
119
- var _useState5 = useState(initialValue),
120
- _useState6 = _slicedToArray(_useState5, 2),
121
- lastValue = _useState6[0],
122
- setLastValue = _useState6[1];
115
+ var _useState = useState(initialValue),
116
+ _useState2 = _slicedToArray(_useState, 2),
117
+ _value = _useState2[0],
118
+ setValue = _useState2[1];
123
119
  var prevValueRef = useRef(undefined);
124
- useDebounceEffect(function () {
125
- if (!isComposing && _value !== lastValue) {
126
- var trimmedValue = _value.trim();
127
- var finalValue = trimmedValue === "" ? "" : _value;
128
- setLastValue(finalValue);
129
- // If escapeCharacters is set and the value matches, return the escaped value
130
- var escapedValue = escapeValue(finalValue);
131
- onChange === null || onChange === void 0 || onChange(escapedValue);
132
- }
133
- }, [_value, isComposing, lastValue, onChange, escapeValue], {
134
- wait: 10
135
- });
120
+ // 最近一次通知出去的原文(未转义)。只用来去掉 compositionend 与 input
121
+ // 两个事件的重复通知
122
+ var notifiedRef = useRef(initialValue);
123
+
124
+ /**
125
+ * `onChange` 是「用户改了输入」这个**事件**的通知,不是从 state 推导出来的结果,
126
+ * 所以在事件处理里发,不在 effect 里发。详见 `TextArea/index.tsx` 里那段说明——
127
+ * 旧写法(依赖数组里放 `onChange`、体内又调它、用 state 记「通知过没有」)
128
+ * 会让传内联箭头的消费方死循环。
129
+ */
130
+ var notify = function notify(next) {
131
+ if (next === notifiedRef.current) return;
132
+ notifiedRef.current = next;
133
+ // 全是空白等同于空;escapeCharacters 命中时对外给转义后的值
134
+ onChange === null || onChange === void 0 || onChange(escapeValue(next.trim() === "" ? "" : next));
135
+ };
136
136
  useEffect(function () {
137
137
  // Update internal state only when the external value prop actually changes
138
138
  if (prevValueRef.current !== value) {
@@ -141,18 +141,22 @@ var Input = function Input(props) {
141
141
  var rawValue = typeof value === "number" ? "".concat(value) : value === null || value === void 0 ? void 0 : value.toString();
142
142
  // Unescape before displaying
143
143
  var newValue = unescapeValue(rawValue);
144
+ // 外部把值改了,之前通知过什么就不作数了,重新以外部值为准
145
+ notifiedRef.current = newValue;
144
146
  setValue(newValue);
145
- setLastValue(newValue);
146
147
  } else {
147
148
  // Clear only on initialization or when explicitly set to undefined externally
149
+ notifiedRef.current = "";
148
150
  setValue("");
149
- setLastValue("");
150
151
  }
151
152
  }
152
153
  }, [value, unescapeValue]);
154
+
155
+ // 只在挂载时把 ref 交出去。`getRef` 进依赖数组同样会被内联箭头带着每次渲染重跑
153
156
  useEffect(function () {
154
- getRef === null || getRef === void 0 || getRef(ref.current);
155
- }, [getRef]);
157
+ var _getRefRef$current;
158
+ (_getRefRef$current = getRefRef.current) === null || _getRefRef$current === void 0 || _getRefRef$current.call(getRefRef, ref.current);
159
+ }, [getRefRef]);
156
160
  return /*#__PURE__*/_jsx(Tooltip, _objectSpread(_objectSpread({
157
161
  placement: "topLeft"
158
162
  }, tooltip), {}, {
@@ -161,12 +165,13 @@ var Input = function Input(props) {
161
165
  disabled: disabled,
162
166
  placeholder: disabled ? "" : placeholder,
163
167
  onCompositionStart: function onCompositionStart() {
164
- return setComposing(true);
168
+ composingRef.current = true;
165
169
  },
166
- onCompositionEnd: function onCompositionEnd() {
167
- setTimeout(function () {
168
- setComposing(false);
169
- }, 1);
170
+ onCompositionEnd: function onCompositionEnd(e) {
171
+ composingRef.current = false;
172
+ // 组完字的那一下自己发通知。浏览器之间 compositionend 与 input 的先后
173
+ // 不一致,`notify` 里按原文去重,两种顺序都只会发出一次
174
+ notify(unescapeValue(e.currentTarget.value));
170
175
  },
171
176
  value: _value,
172
177
  onChange: function onChange(e) {
@@ -174,6 +179,7 @@ var Input = function Input(props) {
174
179
  var inputValue = e.target.value;
175
180
  var unescapedInput = unescapeValue(inputValue);
176
181
  setValue(unescapedInput);
182
+ if (!composingRef.current) notify(unescapedInput);
177
183
  },
178
184
  className: classNames(styles.antdInput, className),
179
185
  type: type,
@@ -5,6 +5,7 @@ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len
5
5
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
6
  function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
7
7
  import { useEffect, useState } from "react";
8
+ import { useLatestRef } from "../../../hooks/useLatestRef";
8
9
  /**
9
10
  * Manage input options (when there are no matching options)
10
11
  */
@@ -18,11 +19,16 @@ export function useSelectInputOptions(_ref) {
18
19
  _useState2 = _slicedToArray(_useState, 2),
19
20
  inputOptions = _useState2[0],
20
21
  setInputOptions = _useState2[1];
22
+ var onSearchRef = useLatestRef(onSearch);
23
+
24
+ // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
25
+ // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
21
26
  useEffect(function () {
22
27
  if (!isComposing) {
23
- onSearch === null || onSearch === void 0 || onSearch(searchValue);
28
+ var _onSearchRef$current;
29
+ (_onSearchRef$current = onSearchRef.current) === null || _onSearchRef$current === void 0 || _onSearchRef$current.call(onSearchRef, searchValue);
24
30
  }
25
- }, [inputOptions, isComposing, onChange, onSearch, optionsLength, searchValue]);
31
+ }, [inputOptions, isComposing, onSearchRef, optionsLength, searchValue]);
26
32
  return {
27
33
  inputOptions: inputOptions,
28
34
  setInputOptions: setInputOptions
@@ -14,7 +14,7 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
14
14
  function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
15
15
  function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
16
16
  import { Slider as AntdSlider } from "antd";
17
- import React, { useEffect, useState } from "react";
17
+ import React, { useEffect, useRef, useState } from "react";
18
18
  import classNames from "classnames";
19
19
  import styles from "./index.module.scss";
20
20
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -29,28 +29,37 @@ var Slider = function Slider(props) {
29
29
  _useState2 = _slicedToArray(_useState, 2),
30
30
  _value = _useState2[0],
31
31
  setValue = _useState2[1];
32
- var _useState3 = useState(value),
33
- _useState4 = _slicedToArray(_useState3, 2),
34
- lastValue = _useState4[0],
35
- setLastValue = _useState4[1];
32
+ // 最近一次通知出去的值,代替原来那个用 state 记账的写法
33
+ var notifiedRef = useRef(value);
34
+
35
+ /**
36
+ * `onChange` 是「用户拖了滑块」这个**事件**的通知,不是从 state 推导出来的结果,
37
+ * 所以在事件处理里发,不在 effect 里发。详见 `Input/TextArea/index.tsx` 里那段
38
+ * 说明——旧写法(依赖数组里放 `onChange`、体内又调它、用 state 记「通知过没有」)
39
+ * 会让传内联箭头的消费方死循环。
40
+ */
41
+ var handleChange = function handleChange(next) {
42
+ setValue(next);
43
+ if (next === notifiedRef.current) return;
44
+ notifiedRef.current = next;
45
+ onChange === null || onChange === void 0 || onChange(next);
46
+ };
47
+
48
+ // 外部值(`value` / `topValue`)变了才回写,不跟着通知走
36
49
  useEffect(function () {
37
- if (_value !== lastValue) {
38
- setLastValue(_value);
39
- onChange === null || onChange === void 0 || onChange(_value);
50
+ var next = topValue || value;
51
+ if (next !== notifiedRef.current) {
52
+ notifiedRef.current = next;
53
+ setValue(next);
40
54
  }
41
- }, [lastValue, onChange, _value]);
42
- useEffect(function () {
43
- if ((topValue || value) !== lastValue) {
44
- setValue(topValue || value);
45
- }
46
- }, [lastValue, topValue, value]);
55
+ }, [topValue, value]);
47
56
  return /*#__PURE__*/_jsx(AntdSlider, _objectSpread({
48
57
  className: classNames([styles.slider, className]),
49
58
  tooltip: {
50
59
  open: false
51
60
  },
52
61
  value: _value,
53
- onChange: setValue
62
+ onChange: handleChange
54
63
  }, sliderConfig));
55
64
  };
56
65
  export default Slider;
@@ -7,6 +7,7 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
7
7
  import { useEffect, useState } from "react";
8
8
  import { deepCopy, Equal } from "hsu-utils";
9
9
  import { extractFileUrl } from "../_utils";
10
+ import { useLatestRef } from "../../../hooks/useLatestRef";
10
11
  /**
11
12
  * Manage the upload file list state
12
13
  */
@@ -22,6 +23,10 @@ export function useUploadFileList(_ref) {
22
23
  _useState4 = _slicedToArray(_useState3, 2),
23
24
  lastFileList = _useState4[0],
24
25
  setLastFileList = _useState4[1];
26
+ var onChangeRef = useLatestRef(onChange);
27
+
28
+ // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
29
+ // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
25
30
  useEffect(function () {
26
31
  if (rmFile) {
27
32
  var file = _fileList.find(function (item) {
@@ -33,14 +38,15 @@ export function useUploadFileList(_ref) {
33
38
  if (file) {
34
39
  setFilelist(filteredList);
35
40
  setTimeout(function () {
36
- onChange === null || onChange === void 0 || onChange({
41
+ var _onChangeRef$current;
42
+ (_onChangeRef$current = onChangeRef.current) === null || _onChangeRef$current === void 0 || _onChangeRef$current.call(onChangeRef, {
37
43
  file: file,
38
44
  fileList: filteredList
39
45
  });
40
46
  }, 100);
41
47
  }
42
48
  }
43
- }, [_fileList, onChange, rmFile]);
49
+ }, [_fileList, onChangeRef, rmFile]);
44
50
  useEffect(function () {
45
51
  if (!Equal.ObjEqual(fileList !== null && fileList !== void 0 ? fileList : [], lastFileList)) {
46
52
  if (fileList !== null && fileList !== void 0 && fileList.length) {
@@ -6,6 +6,7 @@ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" !=
6
6
  function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
7
7
  import { useEffect, useRef, useState } from "react";
8
8
  import { deepCopy, Equal } from "hsu-utils";
9
+ import { useLatestRef } from "../../../hooks/useLatestRef";
9
10
  /**
10
11
  * Manage upload and download operations
11
12
  */
@@ -27,6 +28,7 @@ export function useUploadOperations(_ref) {
27
28
  downloading: {},
28
29
  uploadingList: {}
29
30
  });
31
+ var onUploadingListRef = useLatestRef(onUploadingList);
30
32
 
31
33
  // Sync to the ref on update
32
34
  useEffect(function () {
@@ -35,12 +37,16 @@ export function useUploadOperations(_ref) {
35
37
  useEffect(function () {
36
38
  operationsRef.current.uploadingList = uploadingList;
37
39
  }, [uploadingList]);
40
+
41
+ // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
42
+ // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
38
43
  useEffect(function () {
39
44
  if (!Equal.ObjEqual(uploadingList, lastUploadList)) {
40
- onUploadingList === null || onUploadingList === void 0 || onUploadingList(deepCopy(uploadingList));
45
+ var _onUploadingListRef$c;
46
+ (_onUploadingListRef$c = onUploadingListRef.current) === null || _onUploadingListRef$c === void 0 || _onUploadingListRef$c.call(onUploadingListRef, deepCopy(uploadingList));
41
47
  setLastUploadList(deepCopy(uploadingList));
42
48
  }
43
- }, [uploadingList, lastUploadList, onUploadingList]);
49
+ }, [uploadingList, lastUploadList, onUploadingListRef]);
44
50
 
45
51
  // Cleanup function
46
52
  useEffect(function () {
@@ -0,0 +1,24 @@
1
+ /// <reference types="react" />
2
+ /**
3
+ * 把一个每次渲染都可能换引用的值(通常是回调 prop)装进一个**引用恒定**的 ref。
4
+ *
5
+ * 用途只有一个:让 effect 不必把回调放进依赖数组。
6
+ * 回调放进依赖数组、effect 体内又调它,是一条会让消费方死循环的写法——
7
+ * 消费方传内联箭头(`onChange={(v) => setX(v)}`,React 里最常见的写法)时
8
+ * 每次渲染都是新引用 → effect 重跑 → 调回调 → 父级 setState → 再渲染 →
9
+ * 又是新引用 …… 直到 React 抛 `Maximum update depth exceeded`。
10
+ *
11
+ * ```ts
12
+ * const onDoneRef = useLatestRef(onDone);
13
+ * useEffect(() => {
14
+ * const t = setTimeout(() => onDoneRef.current?.(), 1000);
15
+ * return () => clearTimeout(t);
16
+ * }, [onDoneRef]); // 只在挂载时跑一次,且永远调到最新的 onDone
17
+ * ```
18
+ *
19
+ * 注意:渲染期间就地赋值。React 官方对 ref 的约束是「不要在渲染中**读**可变值」,
20
+ * 写入最新的 props 是 useEffectEvent 落地前的通行做法(ahooks 的 `useLatest`、
21
+ * React 文档里的 `useEvent` polyfill 都是这么写的)。
22
+ */
23
+ export declare function useLatestRef<T>(value: T): import("react").MutableRefObject<T>;
24
+ export default useLatestRef;
@@ -0,0 +1,29 @@
1
+ import { useRef } from "react";
2
+
3
+ /**
4
+ * 把一个每次渲染都可能换引用的值(通常是回调 prop)装进一个**引用恒定**的 ref。
5
+ *
6
+ * 用途只有一个:让 effect 不必把回调放进依赖数组。
7
+ * 回调放进依赖数组、effect 体内又调它,是一条会让消费方死循环的写法——
8
+ * 消费方传内联箭头(`onChange={(v) => setX(v)}`,React 里最常见的写法)时
9
+ * 每次渲染都是新引用 → effect 重跑 → 调回调 → 父级 setState → 再渲染 →
10
+ * 又是新引用 …… 直到 React 抛 `Maximum update depth exceeded`。
11
+ *
12
+ * ```ts
13
+ * const onDoneRef = useLatestRef(onDone);
14
+ * useEffect(() => {
15
+ * const t = setTimeout(() => onDoneRef.current?.(), 1000);
16
+ * return () => clearTimeout(t);
17
+ * }, [onDoneRef]); // 只在挂载时跑一次,且永远调到最新的 onDone
18
+ * ```
19
+ *
20
+ * 注意:渲染期间就地赋值。React 官方对 ref 的约束是「不要在渲染中**读**可变值」,
21
+ * 写入最新的 props 是 useEffectEvent 落地前的通行做法(ahooks 的 `useLatest`、
22
+ * React 文档里的 `useEvent` polyfill 都是这么写的)。
23
+ */
24
+ export function useLatestRef(value) {
25
+ var ref = useRef(value);
26
+ ref.current = value;
27
+ return ref;
28
+ }
29
+ export default useLatestRef;
@@ -184,12 +184,30 @@ var Menu = function Menu(props) {
184
184
  }));
185
185
 
186
186
  // 次级菜单默认带一段头部(至少给出返回入口),调用方可用 secondaryHeader 整块换掉
187
- return secondary ? /*#__PURE__*/_jsxs("div", {
188
- className: styles.secondaryWrap,
189
- children: [secondaryHeader !== null && secondaryHeader !== void 0 ? secondaryHeader : /*#__PURE__*/_jsx(SecondaryHeader, {
190
- collapsed: collapsed,
191
- theme: theme
192
- }), menu]
187
+ if (secondary) {
188
+ return /*#__PURE__*/_jsxs("div", {
189
+ className: classNames(styles.scrollHost, styles.secondaryWrap),
190
+ children: [secondaryHeader !== null && secondaryHeader !== void 0 ? secondaryHeader : /*#__PURE__*/_jsx(SecondaryHeader, {
191
+ collapsed: collapsed,
192
+ theme: theme
193
+ }), menu]
194
+ });
195
+ }
196
+
197
+ /**
198
+ * 侧栏(inline)菜单自带滚动宿主:宿主不滚、菜单根滚。
199
+ *
200
+ * 不这么做的话「谁滚」就落在消费方的 Sider 上 —— 而 antd 的
201
+ * `.ant-layout-sider` / `.ant-layout-sider-children` 都没有 overflow,
202
+ * 展开子菜单的动画期间高度先超出再收回,滚动条会闪一下。几何契约必须由组件
203
+ * 自己闭合,不能指望每个消费方都补一遍 overflow 规则。
204
+ *
205
+ * 横向菜单不需要(rc-overflow 自己收成 “…”),也不能加:那层 div 会把
206
+ * `.menu` 从 Header 的 flex 行里挪走。
207
+ */
208
+ return mode === "inline" ? /*#__PURE__*/_jsx("div", {
209
+ className: styles.scrollHost,
210
+ children: menu
193
211
  }) : menu;
194
212
  };
195
213
  export default Menu;
@@ -1,13 +1,6 @@
1
1
  // 注意:CSS Module 里的类名都会被哈希,antd 的类名必须写在 :global() 中,否则
2
2
  // `.ant-menu-horizontal` 会变成 `.ant-menu-horizontal___xxxxx`,静悄悄地永不生效。
3
3
  .menu {
4
- &::-webkit-scrollbar {
5
- width: 0px;
6
- height: 0px;
7
- }
8
-
9
- scrollbar-width: none;
10
-
11
4
  // 顶部横向菜单:占满剩余宽度,背景交给外层的导航渐变
12
5
  &:global(.ant-menu-horizontal) {
13
6
  flex: 1;
@@ -15,6 +8,50 @@
15
8
 
16
9
  background-color: transparent;
17
10
  }
11
+
12
+ /* ===== inline(侧栏):菜单根**就是**那个滚动容器 =====
13
+ antd 的 `.ant-layout-sider` / `.ant-layout-sider-children` 都没有设 overflow,
14
+ 所以在这条路径上,「谁滚」原本是没人规定的 —— 展开子菜单那 200ms 里
15
+ rc-motion 会逐帧改高度,容器先超出再收回,滚动条由**当时**恰好溢出的那个
16
+ 元素临时长出来,于是闪一下。
17
+
18
+ 这里把它定死:菜单根 `flex: 1 / min-height: 0 / overflow-y: auto`,配上外面
19
+ 那层不滚的 `.scrollHost`,中间态怎么抖都轮不到别人显示滚动条;菜单长过一屏
20
+ 时也仍然滚得到底部。 */
21
+ &:global(.ant-menu-inline),
22
+ &:global(.ant-menu-inline-collapsed) {
23
+ flex: 1;
24
+ min-height: 0px;
25
+
26
+ // 横向一律不滚:菜单文字过长时该省略,不该把侧栏推出一条横向滚动条
27
+ overflow-x: hidden;
28
+ overflow-y: auto;
29
+ }
30
+
31
+ /* 子菜单在展开 / 收起动画期间被 rc-motion 逐帧改高度。antd 的
32
+ `.ant-motion-collapse` 自带 overflow hidden,这里补的是**动画结束、类名摘掉
33
+ 之后**那一帧:此时 ul 的高度刚回到 auto,若父级恰好差几像素,浏览器会先画出
34
+ 滚动条再重排。子菜单本来也永远不需要自己滚。 */
35
+ :global(.ant-menu-sub.ant-menu-inline) {
36
+ overflow: hidden;
37
+ }
38
+
39
+ /* ===== 滚动条外观:本组件不管,交给消费方 =====
40
+ 这里曾经写着 `scrollbar-width: none` + `::-webkit-scrollbar { width: 0 }`,
41
+ 用「把滚动条永久藏掉」来遮上面那个闪动。现在闪动由 overflow 归属治本了,
42
+ 这份「藏」不但多余,还有两处害处,所以整段删掉:
43
+
44
+ 1. **Chrome 121+ 一旦读到 `scrollbar-width` / `scrollbar-color` 的非默认值,
45
+ 就会整套忽略该元素上的 `::-webkit-scrollbar` 规则。** 库里无差别写下一句
46
+ `scrollbar-width: none`,等于在每个消费方的侧栏上悄悄废掉他们全站那套
47
+ 滚动条皮肤,只剩系统细条 —— 而且不报错、很难查。
48
+ (所以本库任何地方要写这两个属性,都必须锁在
49
+ `@supports (-moz-appearance: none)` 里,让 Chrome 读不到。)
50
+ 2. 滚动条的「显 / 隐 / 配色」是应用级的设计语言(有的应用要常驻,有的要
51
+ 悬浮才显现),不是菜单组件的行为。库只该规定**谁滚**,不该用高权重规则
52
+ 把可见性堵死、逼消费方回过头来盖。
53
+
54
+ 消费方在全局写 `::-webkit-scrollbar` 那一套即可,会自然落到菜单根上。 */
18
55
  }
19
56
 
20
57
  // 弹出的子菜单挂在 body 上,够不到 .menu,所以由组件把这个类挂到 popup 上
@@ -37,14 +74,20 @@
37
74
  }
38
75
  }
39
76
 
40
- // 次级菜单:头部固定在上,菜单撑满剩余高度
41
- .secondaryWrap {
77
+ /* inline 菜单的滚动宿主:撑满侧栏高度、自己**不滚**,把高度整份让给菜单根。
78
+ 有它在,菜单组件才是自带滚动容器语义的一整块 —— 消费方把 Menu 塞进任何一个
79
+ 有高度的容器(antd 的 Sider、或自己的 div)都不必再补 overflow 规则。 */
80
+ .scrollHost {
42
81
  height: 100%;
43
82
  display: flex;
44
83
  flex-direction: column;
45
84
 
85
+ overflow: hidden;
86
+ }
87
+
88
+ // 次级菜单:头部固定在上,菜单撑满剩余高度(滚动归属见 .scrollHost)
89
+ .secondaryWrap {
46
90
  :global(.ant-menu) {
47
- flex: 1;
48
91
  border-inline-end: none !important;
49
92
  }
50
93
  }
@@ -8,6 +8,7 @@ var _react = _interopRequireWildcard(require("react"));
8
8
  var _indexModule = _interopRequireDefault(require("../index.module.scss"));
9
9
  var echarts = _interopRequireWildcard(require("echarts"));
10
10
  var _bubble = require("../_utils/bubble");
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); }
@@ -254,7 +255,10 @@ const Bubble = props => {
254
255
  chartInstanceRef.current.setOption(chartOption, true);
255
256
  }
256
257
  }, [chartOption, data]);
258
+ const onChartRef = (0, _useLatestRef.useLatestRef)(onChart);
257
259
 
260
+ // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
261
+ // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
258
262
  // Initialize the chart
259
263
  (0, _react.useEffect)(() => {
260
264
  if (!chartRef.current) return;
@@ -267,7 +271,7 @@ const Bubble = props => {
267
271
  chart = echarts.init(chartRef.current);
268
272
  chartInstanceRef.current = chart;
269
273
  // Call the onChart callback on first initialization
270
- onChart?.(chart);
274
+ onChartRef.current?.(chart);
271
275
  }
272
276
 
273
277
  // Resize first so echarts picks up the correct container size, then apply the option
@@ -289,7 +293,7 @@ const Bubble = props => {
289
293
  chartInstanceRef.current?.off("click", onClick);
290
294
  }
291
295
  };
292
- }, [chartOption, containerSize.width, containerSize.height, handleResize, onChart, onClick]);
296
+ }, [chartOption, containerSize.width, containerSize.height, handleResize, onChartRef, onClick]);
293
297
 
294
298
  // Dispose the chart instance when the component unmounts
295
299
  (0, _react.useEffect)(() => {