@hsu-react/ui 0.0.14 → 0.0.16
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/Select/AutoCompleteSelect/index.js +70 -31
- package/es/components/TextEllipsis/index.js +21 -19
- package/lib/components/Select/AutoCompleteSelect/index.js +56 -22
- package/lib/components/TextEllipsis/index.js +21 -19
- package/package.json +1 -1
- package/src/components/Select/AutoCompleteSelect/index.tsx +71 -26
- package/src/components/TextEllipsis/index.tsx +25 -24
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
2
|
-
var _excluded = ["prefix", "suffix", "className", "selectClassName", "onFocus", "onBlur", "onSearch", "disabled", "popupClassName", "filterOption", "onChange", "options", "placement", "onDropdownVisibleChange", "popupMatchSelectWidth", "popupMatchContentWidth", "optionFontSize"],
|
|
2
|
+
var _excluded = ["prefix", "suffix", "className", "selectClassName", "onFocus", "onBlur", "onSearch", "disabled", "popupClassName", "filterOption", "onChange", "options", "placement", "onDropdownVisibleChange", "popupMatchSelectWidth", "popupMatchContentWidth", "optionFontSize", "value"],
|
|
3
3
|
_excluded2 = ["label", "value", "disabled"];
|
|
4
4
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
5
5
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
@@ -15,7 +15,7 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
|
15
15
|
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; }
|
|
16
16
|
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; }
|
|
17
17
|
import { AutoComplete } from "antd";
|
|
18
|
-
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
18
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
19
19
|
import classNames from "classnames";
|
|
20
20
|
import styles from "./index.module.scss";
|
|
21
21
|
import { generateRandomStr } from "hsu-utils";
|
|
@@ -48,6 +48,7 @@ var AutoCompleteSelect = function AutoCompleteSelect(props) {
|
|
|
48
48
|
popupMatchContentWidth = props.popupMatchContentWidth,
|
|
49
49
|
_props$optionFontSize = props.optionFontSize,
|
|
50
50
|
optionFontSize = _props$optionFontSize === void 0 ? 14 : _props$optionFontSize,
|
|
51
|
+
value = props.value,
|
|
51
52
|
antdAutoCompleteConfig = _objectWithoutProperties(props, _excluded);
|
|
52
53
|
var _useState = useState(false),
|
|
53
54
|
_useState2 = _slicedToArray(_useState, 2),
|
|
@@ -58,23 +59,33 @@ var AutoCompleteSelect = function AutoCompleteSelect(props) {
|
|
|
58
59
|
_useState4 = _slicedToArray(_useState3, 2),
|
|
59
60
|
open = _useState4[0],
|
|
60
61
|
setOpen = _useState4[1];
|
|
61
|
-
var _useState5 = useState(
|
|
62
|
+
var _useState5 = useState(""),
|
|
62
63
|
_useState6 = _slicedToArray(_useState5, 2),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
var _useState7 = useState(
|
|
64
|
+
searchText = _useState6[0],
|
|
65
|
+
setSearchText = _useState6[1];
|
|
66
|
+
var _useState7 = useState(false),
|
|
66
67
|
_useState8 = _slicedToArray(_useState7, 2),
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
legacyHasErrorStatus = _useState8[0],
|
|
69
|
+
setLegacyHasErrorStatus = _useState8[1];
|
|
70
|
+
var _useState9 = useState(true),
|
|
71
|
+
_useState10 = _slicedToArray(_useState9, 2),
|
|
72
|
+
legacyHasArrowOrClear = _useState10[0],
|
|
73
|
+
setLegacyHasArrowOrClear = _useState10[1];
|
|
69
74
|
var legacyHasSelector = isLegacyHasSelectorBrowser();
|
|
70
75
|
var cls = useMemo(function () {
|
|
71
76
|
return generateRandomStr(10);
|
|
72
77
|
}, []);
|
|
78
|
+
var handleSearch = useCallback(function (searchValue) {
|
|
79
|
+
setSearchText(searchValue);
|
|
80
|
+
onSearch === null || onSearch === void 0 || onSearch(searchValue);
|
|
81
|
+
}, [onSearch]);
|
|
82
|
+
|
|
83
|
+
// Always pass handleSearch so IME composition events stay observed
|
|
84
|
+
// (an in-progress pinyin input must not wipe out the option list)
|
|
73
85
|
var _useSelectComposition = useSelectComposition({
|
|
74
|
-
onSearch:
|
|
86
|
+
onSearch: handleSearch
|
|
75
87
|
}),
|
|
76
88
|
isComposing = _useSelectComposition.isComposing;
|
|
77
|
-
useSelectPopupPosition(autoCompleteRef, open, cls);
|
|
78
89
|
var autoCompleteWidth = autoCompleteRef.current ? autoCompleteRef.current.offsetWidth : 0;
|
|
79
90
|
var calculatedPopupWidth = popupMatchContentWidth ? calculatePopupWidth({
|
|
80
91
|
options: options === null || options === void 0 ? void 0 : options.map(function (opt) {
|
|
@@ -103,27 +114,51 @@ var AutoCompleteSelect = function AutoCompleteSelect(props) {
|
|
|
103
114
|
});
|
|
104
115
|
}, [options]);
|
|
105
116
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
// Filter rule: prefer the custom filterOption, otherwise fall back to a
|
|
118
|
+
// case-insensitive substring match
|
|
119
|
+
var matchOption = useMemo(function () {
|
|
109
120
|
if (customFilterOption) {
|
|
110
|
-
return
|
|
111
|
-
return customFilterOption(inputValue, option);
|
|
112
|
-
};
|
|
121
|
+
return customFilterOption;
|
|
113
122
|
}
|
|
114
|
-
|
|
115
|
-
// Default filtering logic: case-insensitive substring match
|
|
116
123
|
return function (inputValue, option) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
var optionValue = ((_option$value = option.value) !== null && _option$value !== void 0 ? _option$value : "").toString().toUpperCase();
|
|
121
|
-
var optionLabel = ((_ref = (_option$label = option.label) !== null && _option$label !== void 0 ? _option$label : option.value) !== null && _ref !== void 0 ? _ref : "").toString().toUpperCase();
|
|
122
|
-
return optionValue.includes(searchText) || optionLabel.includes(searchText);
|
|
124
|
+
var _option$value, _ref, _option$label;
|
|
125
|
+
if (!option) {
|
|
126
|
+
return true;
|
|
123
127
|
}
|
|
124
|
-
|
|
128
|
+
var keyword = inputValue.toUpperCase();
|
|
129
|
+
var optionValue = ((_option$value = option.value) !== null && _option$value !== void 0 ? _option$value : "").toString().toUpperCase();
|
|
130
|
+
var optionLabel = ((_ref = (_option$label = option.label) !== null && _option$label !== void 0 ? _option$label : option.value) !== null && _ref !== void 0 ? _ref : "").toString().toUpperCase();
|
|
131
|
+
return optionValue.includes(keyword) || optionLabel.includes(keyword);
|
|
125
132
|
};
|
|
126
|
-
}, [customFilterOption
|
|
133
|
+
}, [customFilterOption]);
|
|
134
|
+
|
|
135
|
+
// Text the filter runs against: the controlled value wins, otherwise use the
|
|
136
|
+
// internally tracked search keyword
|
|
137
|
+
var filterText = useMemo(function () {
|
|
138
|
+
if (value !== undefined && value !== null) {
|
|
139
|
+
return String(value);
|
|
140
|
+
}
|
|
141
|
+
return searchText;
|
|
142
|
+
}, [value, searchText]);
|
|
143
|
+
|
|
144
|
+
// Filter inside the component (antd's own filterOption is turned off) so we
|
|
145
|
+
// know whether any candidate survives, keeping controlled open in sync
|
|
146
|
+
var filteredOptions = useMemo(function () {
|
|
147
|
+
// Skip filtering while composing so an in-progress pinyin input does not
|
|
148
|
+
// empty the candidate list
|
|
149
|
+
if (isComposing || !filterText) {
|
|
150
|
+
return autoCompleteOptions;
|
|
151
|
+
}
|
|
152
|
+
return autoCompleteOptions.filter(function (option) {
|
|
153
|
+
return matchOption(filterText, option);
|
|
154
|
+
});
|
|
155
|
+
}, [autoCompleteOptions, filterText, isComposing, matchOption]);
|
|
156
|
+
|
|
157
|
+
// In combobox mode antd hides the popup by itself when there is no candidate
|
|
158
|
+
// and skips onDropdownVisibleChange, so fold "no candidate" into open to stop
|
|
159
|
+
// the controlled state from drifting and leaving an empty popup behind
|
|
160
|
+
var mergedOpen = open && filteredOptions.length > 0;
|
|
161
|
+
useSelectPopupPosition(autoCompleteRef, mergedOpen, cls);
|
|
127
162
|
useEffect(function () {
|
|
128
163
|
if (!legacyHasSelector) {
|
|
129
164
|
return;
|
|
@@ -163,7 +198,7 @@ var AutoCompleteSelect = function AutoCompleteSelect(props) {
|
|
|
163
198
|
return function () {
|
|
164
199
|
observer.disconnect();
|
|
165
200
|
};
|
|
166
|
-
}, [legacyHasSelector,
|
|
201
|
+
}, [legacyHasSelector, mergedOpen]);
|
|
167
202
|
return /*#__PURE__*/_jsxs("div", {
|
|
168
203
|
className: classNames(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty({}, styles.autoCompleteSelect, true), className !== null && className !== void 0 ? className : "", true), styles.focused, focused), styles.disabled, disabled), styles.popupMatchContentWidth, popupMatchContentWidth), styles.legacyHasErrorStatus, legacyHasSelector && legacyHasErrorStatus)),
|
|
169
204
|
ref: autoCompleteRef,
|
|
@@ -172,19 +207,23 @@ var AutoCompleteSelect = function AutoCompleteSelect(props) {
|
|
|
172
207
|
}), /*#__PURE__*/_jsx(AutoComplete, _objectSpread({}, _objectSpread(_objectSpread({
|
|
173
208
|
allowClear: true
|
|
174
209
|
}, antdAutoCompleteConfig), {}, {
|
|
210
|
+
value: value,
|
|
175
211
|
onFocus: function onFocus(e) {
|
|
176
212
|
setFocused(true);
|
|
177
213
|
_onFocus === null || _onFocus === void 0 || _onFocus(e);
|
|
178
214
|
},
|
|
179
215
|
onBlur: function onBlur(e) {
|
|
180
216
|
setFocused(false);
|
|
217
|
+
// antd skips onDropdownVisibleChange when there is no candidate,
|
|
218
|
+
// so collapse the controlled state manually on blur
|
|
219
|
+
setOpen(false);
|
|
181
220
|
_onBlur === null || _onBlur === void 0 || _onBlur(e);
|
|
182
221
|
},
|
|
183
|
-
options:
|
|
184
|
-
filterOption:
|
|
222
|
+
options: filteredOptions,
|
|
223
|
+
filterOption: false,
|
|
185
224
|
onChange: onChange,
|
|
186
|
-
onSearch:
|
|
187
|
-
open:
|
|
225
|
+
onSearch: handleSearch,
|
|
226
|
+
open: mergedOpen,
|
|
188
227
|
onDropdownVisibleChange: function onDropdownVisibleChange(visible) {
|
|
189
228
|
setOpen(visible);
|
|
190
229
|
_onDropdownVisibleChange === null || _onDropdownVisibleChange === void 0 || _onDropdownVisibleChange(visible);
|
|
@@ -107,6 +107,8 @@ var TextEllipsis = function TextEllipsis(_ref) {
|
|
|
107
107
|
};
|
|
108
108
|
var textContent = getTextContent(children);
|
|
109
109
|
var update = function update() {
|
|
110
|
+
// Detached nodes measure 0/0 — never let them overwrite a live measurement
|
|
111
|
+
if (!element.isConnected) return;
|
|
110
112
|
var isTextOverflow = element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;
|
|
111
113
|
setIsOverflow(isTextOverflow);
|
|
112
114
|
setMeasuredWidth(element.clientWidth);
|
|
@@ -179,24 +181,24 @@ var TextEllipsis = function TextEllipsis(_ref) {
|
|
|
179
181
|
})]
|
|
180
182
|
});
|
|
181
183
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
|
|
184
|
+
// Always keep the Tooltip wrapper mounted and toggle via title:
|
|
185
|
+
// conditionally wrapping remounts the text DOM when isOverflow flips, which detaches
|
|
186
|
+
// the node the measurement effect's ResizeObserver holds — its final callback then
|
|
187
|
+
// reads 0/0 from the detached node and resets isOverflow, so the Tooltip never shows.
|
|
188
|
+
return /*#__PURE__*/_jsx(Tooltip, _objectSpread(_objectSpread({
|
|
189
|
+
arrow: false,
|
|
190
|
+
placement: "bottomLeft"
|
|
191
|
+
}, restTooltipConfig), {}, {
|
|
192
|
+
title: isOverflow && !disabled ? tooltipTitle : undefined,
|
|
193
|
+
styles: _objectSpread({
|
|
194
|
+
body: {
|
|
195
|
+
width: tooltipWidth,
|
|
196
|
+
overflow: "auto",
|
|
197
|
+
maxHeight: "300px",
|
|
198
|
+
whiteSpace: "pre-wrap"
|
|
199
|
+
}
|
|
200
|
+
}, styles),
|
|
201
|
+
children: content
|
|
202
|
+
}));
|
|
201
203
|
};
|
|
202
204
|
export default TextEllipsis;
|
|
@@ -38,21 +38,29 @@ const AutoCompleteSelect = props => {
|
|
|
38
38
|
popupMatchSelectWidth,
|
|
39
39
|
popupMatchContentWidth,
|
|
40
40
|
optionFontSize = 14,
|
|
41
|
+
value,
|
|
41
42
|
...antdAutoCompleteConfig
|
|
42
43
|
} = props;
|
|
43
44
|
const [focused, setFocused] = (0, _react.useState)(false);
|
|
44
45
|
const autoCompleteRef = (0, _react.useRef)(null);
|
|
45
46
|
const [open, setOpen] = (0, _react.useState)(false);
|
|
47
|
+
const [searchText, setSearchText] = (0, _react.useState)("");
|
|
46
48
|
const [legacyHasErrorStatus, setLegacyHasErrorStatus] = (0, _react.useState)(false);
|
|
47
49
|
const [legacyHasArrowOrClear, setLegacyHasArrowOrClear] = (0, _react.useState)(true);
|
|
48
50
|
const legacyHasSelector = (0, _cssSupports.isLegacyHasSelectorBrowser)();
|
|
49
51
|
const cls = (0, _react.useMemo)(() => (0, _hsuUtils.generateRandomStr)(10), []);
|
|
52
|
+
const handleSearch = (0, _react.useCallback)(searchValue => {
|
|
53
|
+
setSearchText(searchValue);
|
|
54
|
+
onSearch?.(searchValue);
|
|
55
|
+
}, [onSearch]);
|
|
56
|
+
|
|
57
|
+
// Always pass handleSearch so IME composition events stay observed
|
|
58
|
+
// (an in-progress pinyin input must not wipe out the option list)
|
|
50
59
|
const {
|
|
51
60
|
isComposing
|
|
52
61
|
} = (0, _hooks.useSelectComposition)({
|
|
53
|
-
onSearch
|
|
62
|
+
onSearch: handleSearch
|
|
54
63
|
});
|
|
55
|
-
(0, _hooks.useSelectPopupPosition)(autoCompleteRef, open, cls);
|
|
56
64
|
const autoCompleteWidth = autoCompleteRef.current ? autoCompleteRef.current.offsetWidth : 0;
|
|
57
65
|
const calculatedPopupWidth = popupMatchContentWidth ? (0, _utils.calculatePopupWidth)({
|
|
58
66
|
options: options?.map(opt => ({
|
|
@@ -81,26 +89,48 @@ const AutoCompleteSelect = props => {
|
|
|
81
89
|
});
|
|
82
90
|
}, [options]);
|
|
83
91
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
// Filter rule: prefer the custom filterOption, otherwise fall back to a
|
|
93
|
+
// case-insensitive substring match
|
|
94
|
+
const matchOption = (0, _react.useMemo)(() => {
|
|
87
95
|
if (customFilterOption) {
|
|
88
|
-
return
|
|
89
|
-
return customFilterOption(inputValue, option);
|
|
90
|
-
};
|
|
96
|
+
return customFilterOption;
|
|
91
97
|
}
|
|
92
|
-
|
|
93
|
-
// Default filtering logic: case-insensitive substring match
|
|
94
98
|
return (inputValue, option) => {
|
|
95
|
-
if (!
|
|
96
|
-
|
|
97
|
-
const optionValue = (option.value ?? "").toString().toUpperCase();
|
|
98
|
-
const optionLabel = (option.label ?? option.value ?? "").toString().toUpperCase();
|
|
99
|
-
return optionValue.includes(searchText) || optionLabel.includes(searchText);
|
|
99
|
+
if (!option) {
|
|
100
|
+
return true;
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
+
const keyword = inputValue.toUpperCase();
|
|
103
|
+
const optionValue = (option.value ?? "").toString().toUpperCase();
|
|
104
|
+
const optionLabel = (option.label ?? option.value ?? "").toString().toUpperCase();
|
|
105
|
+
return optionValue.includes(keyword) || optionLabel.includes(keyword);
|
|
102
106
|
};
|
|
103
|
-
}, [customFilterOption
|
|
107
|
+
}, [customFilterOption]);
|
|
108
|
+
|
|
109
|
+
// Text the filter runs against: the controlled value wins, otherwise use the
|
|
110
|
+
// internally tracked search keyword
|
|
111
|
+
const filterText = (0, _react.useMemo)(() => {
|
|
112
|
+
if (value !== undefined && value !== null) {
|
|
113
|
+
return String(value);
|
|
114
|
+
}
|
|
115
|
+
return searchText;
|
|
116
|
+
}, [value, searchText]);
|
|
117
|
+
|
|
118
|
+
// Filter inside the component (antd's own filterOption is turned off) so we
|
|
119
|
+
// know whether any candidate survives, keeping controlled open in sync
|
|
120
|
+
const filteredOptions = (0, _react.useMemo)(() => {
|
|
121
|
+
// Skip filtering while composing so an in-progress pinyin input does not
|
|
122
|
+
// empty the candidate list
|
|
123
|
+
if (isComposing || !filterText) {
|
|
124
|
+
return autoCompleteOptions;
|
|
125
|
+
}
|
|
126
|
+
return autoCompleteOptions.filter(option => matchOption(filterText, option));
|
|
127
|
+
}, [autoCompleteOptions, filterText, isComposing, matchOption]);
|
|
128
|
+
|
|
129
|
+
// In combobox mode antd hides the popup by itself when there is no candidate
|
|
130
|
+
// and skips onDropdownVisibleChange, so fold "no candidate" into open to stop
|
|
131
|
+
// the controlled state from drifting and leaving an empty popup behind
|
|
132
|
+
const mergedOpen = open && filteredOptions.length > 0;
|
|
133
|
+
(0, _hooks.useSelectPopupPosition)(autoCompleteRef, mergedOpen, cls);
|
|
104
134
|
(0, _react.useEffect)(() => {
|
|
105
135
|
if (!legacyHasSelector) {
|
|
106
136
|
return;
|
|
@@ -140,7 +170,7 @@ const AutoCompleteSelect = props => {
|
|
|
140
170
|
return () => {
|
|
141
171
|
observer.disconnect();
|
|
142
172
|
};
|
|
143
|
-
}, [legacyHasSelector,
|
|
173
|
+
}, [legacyHasSelector, mergedOpen]);
|
|
144
174
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
|
|
145
175
|
className: (0, _classnames.default)({
|
|
146
176
|
[_indexModule.default.autoCompleteSelect]: true,
|
|
@@ -156,19 +186,23 @@ const AutoCompleteSelect = props => {
|
|
|
156
186
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_antd.AutoComplete, {
|
|
157
187
|
allowClear: true,
|
|
158
188
|
...antdAutoCompleteConfig,
|
|
189
|
+
value,
|
|
159
190
|
onFocus: e => {
|
|
160
191
|
setFocused(true);
|
|
161
192
|
onFocus?.(e);
|
|
162
193
|
},
|
|
163
194
|
onBlur: e => {
|
|
164
195
|
setFocused(false);
|
|
196
|
+
// antd skips onDropdownVisibleChange when there is no candidate,
|
|
197
|
+
// so collapse the controlled state manually on blur
|
|
198
|
+
setOpen(false);
|
|
165
199
|
onBlur?.(e);
|
|
166
200
|
},
|
|
167
|
-
options:
|
|
168
|
-
filterOption:
|
|
201
|
+
options: filteredOptions,
|
|
202
|
+
filterOption: false,
|
|
169
203
|
onChange,
|
|
170
|
-
onSearch,
|
|
171
|
-
open,
|
|
204
|
+
onSearch: handleSearch,
|
|
205
|
+
open: mergedOpen,
|
|
172
206
|
onDropdownVisibleChange: visible => {
|
|
173
207
|
setOpen(visible);
|
|
174
208
|
onDropdownVisibleChange?.(visible);
|
|
@@ -87,6 +87,8 @@ const TextEllipsis = ({
|
|
|
87
87
|
};
|
|
88
88
|
const textContent = getTextContent(children);
|
|
89
89
|
const update = () => {
|
|
90
|
+
// Detached nodes measure 0/0 — never let them overwrite a live measurement
|
|
91
|
+
if (!element.isConnected) return;
|
|
90
92
|
const isTextOverflow = element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;
|
|
91
93
|
setIsOverflow(isTextOverflow);
|
|
92
94
|
setMeasuredWidth(element.clientWidth);
|
|
@@ -157,25 +159,25 @@ const TextEllipsis = ({
|
|
|
157
159
|
})]
|
|
158
160
|
});
|
|
159
161
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
162
|
+
// Always keep the Tooltip wrapper mounted and toggle via title:
|
|
163
|
+
// conditionally wrapping remounts the text DOM when isOverflow flips, which detaches
|
|
164
|
+
// the node the measurement effect's ResizeObserver holds — its final callback then
|
|
165
|
+
// reads 0/0 from the detached node and resets isOverflow, so the Tooltip never shows.
|
|
166
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_antd.Tooltip, {
|
|
167
|
+
arrow: false,
|
|
168
|
+
placement: "bottomLeft",
|
|
169
|
+
...restTooltipConfig,
|
|
170
|
+
title: isOverflow && !disabled ? tooltipTitle : undefined,
|
|
171
|
+
styles: {
|
|
172
|
+
body: {
|
|
173
|
+
width: tooltipWidth,
|
|
174
|
+
overflow: "auto",
|
|
175
|
+
maxHeight: "300px",
|
|
176
|
+
whiteSpace: "pre-wrap"
|
|
175
177
|
},
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
...styles
|
|
179
|
+
},
|
|
180
|
+
children: content
|
|
181
|
+
});
|
|
180
182
|
};
|
|
181
183
|
var _default = exports.default = TextEllipsis;
|
package/package.json
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { AutoComplete, AutoCompleteProps } from "antd";
|
|
2
|
-
import React, {
|
|
2
|
+
import React, {
|
|
3
|
+
ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
3
10
|
import classNames from "classnames";
|
|
4
11
|
import styles from "./index.module.scss";
|
|
5
12
|
import { generateRandomStr } from "hsu-utils";
|
|
@@ -52,11 +59,13 @@ const AutoCompleteSelect: React.FC<AutoCompleteSelectProps> = (props) => {
|
|
|
52
59
|
popupMatchSelectWidth,
|
|
53
60
|
popupMatchContentWidth,
|
|
54
61
|
optionFontSize = 14,
|
|
62
|
+
value,
|
|
55
63
|
...antdAutoCompleteConfig
|
|
56
64
|
} = props;
|
|
57
65
|
const [focused, setFocused] = useState<boolean>(false);
|
|
58
66
|
const autoCompleteRef = useRef<HTMLDivElement>(null);
|
|
59
67
|
const [open, setOpen] = useState<boolean>(false);
|
|
68
|
+
const [searchText, setSearchText] = useState<string>("");
|
|
60
69
|
const [legacyHasErrorStatus, setLegacyHasErrorStatus] =
|
|
61
70
|
useState<boolean>(false);
|
|
62
71
|
const [legacyHasArrowOrClear, setLegacyHasArrowOrClear] =
|
|
@@ -65,9 +74,17 @@ const AutoCompleteSelect: React.FC<AutoCompleteSelectProps> = (props) => {
|
|
|
65
74
|
|
|
66
75
|
const cls = useMemo(() => generateRandomStr(10), []);
|
|
67
76
|
|
|
68
|
-
const
|
|
77
|
+
const handleSearch = useCallback(
|
|
78
|
+
(searchValue: string) => {
|
|
79
|
+
setSearchText(searchValue);
|
|
80
|
+
onSearch?.(searchValue);
|
|
81
|
+
},
|
|
82
|
+
[onSearch],
|
|
83
|
+
);
|
|
69
84
|
|
|
70
|
-
|
|
85
|
+
// Always pass handleSearch so IME composition events stay observed
|
|
86
|
+
// (an in-progress pinyin input must not wipe out the option list)
|
|
87
|
+
const { isComposing } = useSelectComposition({ onSearch: handleSearch });
|
|
71
88
|
|
|
72
89
|
const autoCompleteWidth = autoCompleteRef.current
|
|
73
90
|
? autoCompleteRef.current.offsetWidth
|
|
@@ -97,30 +114,54 @@ const AutoCompleteSelect: React.FC<AutoCompleteSelectProps> = (props) => {
|
|
|
97
114
|
});
|
|
98
115
|
}, [options]);
|
|
99
116
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
// Filter rule: prefer the custom filterOption, otherwise fall back to a
|
|
118
|
+
// case-insensitive substring match
|
|
119
|
+
const matchOption = useMemo(() => {
|
|
103
120
|
if (customFilterOption) {
|
|
104
|
-
return
|
|
105
|
-
return customFilterOption(inputValue, option);
|
|
106
|
-
};
|
|
121
|
+
return customFilterOption;
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
// Default filtering logic: case-insensitive substring match
|
|
110
124
|
return (inputValue: string, option?: AutoCompleteOption): boolean => {
|
|
111
|
-
if (!
|
|
112
|
-
|
|
113
|
-
const optionValue = (option.value ?? "").toString().toUpperCase();
|
|
114
|
-
const optionLabel = (option.label ?? option.value ?? "")
|
|
115
|
-
.toString()
|
|
116
|
-
.toUpperCase();
|
|
117
|
-
return (
|
|
118
|
-
optionValue.includes(searchText) || optionLabel.includes(searchText)
|
|
119
|
-
);
|
|
125
|
+
if (!option) {
|
|
126
|
+
return true;
|
|
120
127
|
}
|
|
121
|
-
|
|
128
|
+
const keyword = inputValue.toUpperCase();
|
|
129
|
+
const optionValue = (option.value ?? "").toString().toUpperCase();
|
|
130
|
+
const optionLabel = (option.label ?? option.value ?? "")
|
|
131
|
+
.toString()
|
|
132
|
+
.toUpperCase();
|
|
133
|
+
return optionValue.includes(keyword) || optionLabel.includes(keyword);
|
|
122
134
|
};
|
|
123
|
-
}, [customFilterOption
|
|
135
|
+
}, [customFilterOption]);
|
|
136
|
+
|
|
137
|
+
// Text the filter runs against: the controlled value wins, otherwise use the
|
|
138
|
+
// internally tracked search keyword
|
|
139
|
+
const filterText = useMemo(() => {
|
|
140
|
+
if (value !== undefined && value !== null) {
|
|
141
|
+
return String(value);
|
|
142
|
+
}
|
|
143
|
+
return searchText;
|
|
144
|
+
}, [value, searchText]);
|
|
145
|
+
|
|
146
|
+
// Filter inside the component (antd's own filterOption is turned off) so we
|
|
147
|
+
// know whether any candidate survives, keeping controlled open in sync
|
|
148
|
+
const filteredOptions = useMemo(() => {
|
|
149
|
+
// Skip filtering while composing so an in-progress pinyin input does not
|
|
150
|
+
// empty the candidate list
|
|
151
|
+
if (isComposing || !filterText) {
|
|
152
|
+
return autoCompleteOptions;
|
|
153
|
+
}
|
|
154
|
+
return autoCompleteOptions.filter((option) =>
|
|
155
|
+
matchOption(filterText, option),
|
|
156
|
+
);
|
|
157
|
+
}, [autoCompleteOptions, filterText, isComposing, matchOption]);
|
|
158
|
+
|
|
159
|
+
// In combobox mode antd hides the popup by itself when there is no candidate
|
|
160
|
+
// and skips onDropdownVisibleChange, so fold "no candidate" into open to stop
|
|
161
|
+
// the controlled state from drifting and leaving an empty popup behind
|
|
162
|
+
const mergedOpen = open && filteredOptions.length > 0;
|
|
163
|
+
|
|
164
|
+
useSelectPopupPosition(autoCompleteRef, mergedOpen, cls);
|
|
124
165
|
|
|
125
166
|
useEffect(() => {
|
|
126
167
|
if (!legacyHasSelector) {
|
|
@@ -177,7 +218,7 @@ const AutoCompleteSelect: React.FC<AutoCompleteSelectProps> = (props) => {
|
|
|
177
218
|
return () => {
|
|
178
219
|
observer.disconnect();
|
|
179
220
|
};
|
|
180
|
-
}, [legacyHasSelector,
|
|
221
|
+
}, [legacyHasSelector, mergedOpen]);
|
|
181
222
|
|
|
182
223
|
return (
|
|
183
224
|
<div
|
|
@@ -197,19 +238,23 @@ const AutoCompleteSelect: React.FC<AutoCompleteSelectProps> = (props) => {
|
|
|
197
238
|
{...{
|
|
198
239
|
allowClear: true,
|
|
199
240
|
...antdAutoCompleteConfig,
|
|
241
|
+
value,
|
|
200
242
|
onFocus: (e) => {
|
|
201
243
|
setFocused(true);
|
|
202
244
|
onFocus?.(e);
|
|
203
245
|
},
|
|
204
246
|
onBlur: (e) => {
|
|
205
247
|
setFocused(false);
|
|
248
|
+
// antd skips onDropdownVisibleChange when there is no candidate,
|
|
249
|
+
// so collapse the controlled state manually on blur
|
|
250
|
+
setOpen(false);
|
|
206
251
|
onBlur?.(e);
|
|
207
252
|
},
|
|
208
|
-
options:
|
|
209
|
-
filterOption:
|
|
253
|
+
options: filteredOptions,
|
|
254
|
+
filterOption: false,
|
|
210
255
|
onChange,
|
|
211
|
-
onSearch,
|
|
212
|
-
open,
|
|
256
|
+
onSearch: handleSearch,
|
|
257
|
+
open: mergedOpen,
|
|
213
258
|
onDropdownVisibleChange: (visible) => {
|
|
214
259
|
setOpen(visible);
|
|
215
260
|
onDropdownVisibleChange?.(visible);
|
|
@@ -109,6 +109,8 @@ const TextEllipsis: React.FC<TextEllipsisProps> = ({
|
|
|
109
109
|
const textContent = getTextContent(children);
|
|
110
110
|
|
|
111
111
|
const update = () => {
|
|
112
|
+
// Detached nodes measure 0/0 — never let them overwrite a live measurement
|
|
113
|
+
if (!element.isConnected) return;
|
|
112
114
|
const isTextOverflow =
|
|
113
115
|
element.scrollWidth > element.clientWidth ||
|
|
114
116
|
element.scrollHeight > element.clientHeight;
|
|
@@ -189,30 +191,29 @@ const TextEllipsis: React.FC<TextEllipsisProps> = ({
|
|
|
189
191
|
</span>
|
|
190
192
|
);
|
|
191
193
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return content;
|
|
194
|
+
// Always keep the Tooltip wrapper mounted and toggle via title:
|
|
195
|
+
// conditionally wrapping remounts the text DOM when isOverflow flips, which detaches
|
|
196
|
+
// the node the measurement effect's ResizeObserver holds — its final callback then
|
|
197
|
+
// reads 0/0 from the detached node and resets isOverflow, so the Tooltip never shows.
|
|
198
|
+
return (
|
|
199
|
+
<Tooltip
|
|
200
|
+
arrow={false}
|
|
201
|
+
placement="bottomLeft"
|
|
202
|
+
{...restTooltipConfig}
|
|
203
|
+
title={isOverflow && !disabled ? tooltipTitle : undefined}
|
|
204
|
+
styles={{
|
|
205
|
+
body: {
|
|
206
|
+
width: tooltipWidth,
|
|
207
|
+
overflow: "auto",
|
|
208
|
+
maxHeight: "300px",
|
|
209
|
+
whiteSpace: "pre-wrap",
|
|
210
|
+
},
|
|
211
|
+
...styles,
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{content}
|
|
215
|
+
</Tooltip>
|
|
216
|
+
);
|
|
216
217
|
};
|
|
217
218
|
|
|
218
219
|
export default TextEllipsis;
|