@douyinfe/semi-foundation 2.96.0 → 2.97.0

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 (81) hide show
  1. package/cascader/foundation.ts +74 -19
  2. package/datePicker/datePicker.scss +100 -5
  3. package/form/foundation.ts +7 -3
  4. package/form/utils.ts +7 -2
  5. package/image/previewImageFoundation.ts +34 -3
  6. package/image/previewInnerFoundation.ts +15 -4
  7. package/input/textarea.scss +35 -0
  8. package/lib/cjs/cascader/foundation.d.ts +12 -0
  9. package/lib/cjs/cascader/foundation.js +68 -23
  10. package/lib/cjs/datePicker/datePicker.css +67 -5
  11. package/lib/cjs/datePicker/datePicker.scss +100 -5
  12. package/lib/cjs/form/foundation.d.ts +1 -1
  13. package/lib/cjs/form/foundation.js +6 -6
  14. package/lib/cjs/form/utils.js +5 -2
  15. package/lib/cjs/image/previewImageFoundation.d.ts +4 -0
  16. package/lib/cjs/image/previewImageFoundation.js +33 -2
  17. package/lib/cjs/image/previewInnerFoundation.d.ts +1 -0
  18. package/lib/cjs/image/previewInnerFoundation.js +17 -4
  19. package/lib/cjs/input/textarea.css +17 -0
  20. package/lib/cjs/input/textarea.scss +35 -0
  21. package/lib/cjs/navigation/navigation.css +2 -1
  22. package/lib/cjs/navigation/navigation.scss +1 -0
  23. package/lib/cjs/navigation/variables.scss +1 -1
  24. package/lib/cjs/overflowList/foundation.d.ts +1 -0
  25. package/lib/cjs/overflowList/foundation.js +51 -1
  26. package/lib/cjs/select/foundation.d.ts +1 -1
  27. package/lib/cjs/select/foundation.js +28 -2
  28. package/lib/cjs/switch/switch.css +1 -0
  29. package/lib/cjs/switch/switch.scss +1 -0
  30. package/lib/cjs/switch/variables.scss +2 -1
  31. package/lib/cjs/table/foundation.js +2 -1
  32. package/lib/cjs/tag/tag.css +26 -0
  33. package/lib/cjs/tag/tag.scss +33 -0
  34. package/lib/cjs/tagInput/tagInput.css +17 -0
  35. package/lib/cjs/tagInput/tagInput.scss +18 -0
  36. package/lib/cjs/timePicker/constants.d.ts +1 -0
  37. package/lib/cjs/timePicker/foundation.d.ts +7 -1
  38. package/lib/cjs/timePicker/foundation.js +62 -11
  39. package/lib/es/cascader/foundation.d.ts +12 -0
  40. package/lib/es/cascader/foundation.js +68 -23
  41. package/lib/es/datePicker/datePicker.css +67 -5
  42. package/lib/es/datePicker/datePicker.scss +100 -5
  43. package/lib/es/form/foundation.d.ts +1 -1
  44. package/lib/es/form/foundation.js +6 -6
  45. package/lib/es/form/utils.js +5 -2
  46. package/lib/es/image/previewImageFoundation.d.ts +4 -0
  47. package/lib/es/image/previewImageFoundation.js +33 -2
  48. package/lib/es/image/previewInnerFoundation.d.ts +1 -0
  49. package/lib/es/image/previewInnerFoundation.js +17 -4
  50. package/lib/es/input/textarea.css +17 -0
  51. package/lib/es/input/textarea.scss +35 -0
  52. package/lib/es/navigation/navigation.css +2 -1
  53. package/lib/es/navigation/navigation.scss +1 -0
  54. package/lib/es/navigation/variables.scss +1 -1
  55. package/lib/es/overflowList/foundation.d.ts +1 -0
  56. package/lib/es/overflowList/foundation.js +51 -1
  57. package/lib/es/select/foundation.d.ts +1 -1
  58. package/lib/es/select/foundation.js +28 -2
  59. package/lib/es/switch/switch.css +1 -0
  60. package/lib/es/switch/switch.scss +1 -0
  61. package/lib/es/switch/variables.scss +2 -1
  62. package/lib/es/table/foundation.js +2 -1
  63. package/lib/es/tag/tag.css +26 -0
  64. package/lib/es/tag/tag.scss +33 -0
  65. package/lib/es/tagInput/tagInput.css +17 -0
  66. package/lib/es/tagInput/tagInput.scss +18 -0
  67. package/lib/es/timePicker/constants.d.ts +1 -0
  68. package/lib/es/timePicker/foundation.d.ts +7 -1
  69. package/lib/es/timePicker/foundation.js +62 -11
  70. package/navigation/navigation.scss +1 -0
  71. package/navigation/variables.scss +1 -1
  72. package/overflowList/foundation.ts +48 -2
  73. package/package.json +4 -4
  74. package/select/foundation.ts +27 -2
  75. package/switch/switch.scss +1 -0
  76. package/switch/variables.scss +2 -1
  77. package/table/foundation.ts +2 -1
  78. package/tag/tag.scss +33 -0
  79. package/tagInput/tagInput.scss +18 -0
  80. package/timePicker/constants.ts +2 -0
  81. package/timePicker/foundation.ts +62 -10
@@ -170,6 +170,7 @@ export interface BasicCascaderProps {
170
170
  preventScroll?: boolean;
171
171
  virtualizeInSearch?: Virtualize;
172
172
  checkRelation?: string;
173
+ remote?: boolean;
173
174
  onClear?: () => void;
174
175
  triggerRender?: (props: BasicTriggerRenderProps) => any;
175
176
  onListScroll?: (e: any, panel: BasicScrollPanelProps) => void;
@@ -423,6 +424,76 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
423
424
  } else {
424
425
  this._adapter.updateStates({ keyEntities });
425
426
  }
427
+
428
+ // If options(treeData) updates during searching (e.g. remote search async update),
429
+ // we need to sync filteredKeys with the latest keyEntities; otherwise the UI may
430
+ // render empty list ("暂无数据") because filteredKeys are based on stale entities.
431
+ // NOTE: updateSelectedKey/updateStates are async in React, so we pass keyEntities
432
+ // explicitly to avoid reading stale state.
433
+ this.recalculateFilteredKeys(undefined, keyEntities);
434
+ }
435
+
436
+ /**
437
+ * Calculate filtered keys based on current props.
438
+ * - In remote mode: do not do local match, treat current treeData nodes as results
439
+ * - In local mode: perform matching by filterTreeNode
440
+ */
441
+ _calcFilteredKeys(sugInput: string, keyEntities?: BasicEntities): string[] {
442
+ if (!sugInput) {
443
+ return [];
444
+ }
445
+ const { treeNodeFilterProp, filterTreeNode, filterLeafOnly, remote } = this.getProps();
446
+ const entities = Object.values(keyEntities ?? this.getState('keyEntities')) as BasicEntity[];
447
+
448
+ if (remote) {
449
+ return entities
450
+ .filter(item => !item._notExist)
451
+ .filter(item => (filterTreeNode && !filterLeafOnly) || this._isLeaf(item.data))
452
+ .map(item => item.key);
453
+ }
454
+
455
+ return entities
456
+ .filter(item => {
457
+ const { key, _notExist, data } = item;
458
+ if (_notExist) {
459
+ return false;
460
+ }
461
+ const filteredPath = this.getItemPropPath(key, treeNodeFilterProp, keyEntities);
462
+ return filter(sugInput, data, filterTreeNode, filteredPath);
463
+ })
464
+ .filter(item => (filterTreeNode && !filterLeafOnly) || this._isLeaf(item.data))
465
+ .map(item => item.key);
466
+ }
467
+
468
+ /**
469
+ * Sync filteredKeys with latest options/keyEntities WITHOUT triggering onSearch.
470
+ * Used when treeData changes asynchronously in searching state.
471
+ */
472
+ recalculateFilteredKeys(input?: string, nextKeyEntities?: BasicEntities) {
473
+ const isFilterable = this._isFilterable();
474
+ if (!isFilterable) {
475
+ return;
476
+ }
477
+
478
+ // When input is not explicitly provided, only recalculate in searching state.
479
+ // Otherwise, treeData updates may incorrectly force component into searching mode
480
+ // because inputValue can be the selected label text in normal (non-searching) state.
481
+ const currentIsSearching = this.getState('isSearching');
482
+ if (isUndefined(input) && !currentIsSearching) {
483
+ return;
484
+ }
485
+
486
+ const sugInput = isUndefined(input) ? this.getState('inputValue') : input;
487
+ const filteredKeys = this._calcFilteredKeys(sugInput, nextKeyEntities);
488
+ const updateStates: Partial<BasicCascaderInnerData> = {
489
+ isSearching: Boolean(sugInput),
490
+ filteredKeys: new Set(filteredKeys),
491
+ };
492
+ if (nextKeyEntities) {
493
+ updateStates.keyEntities = nextKeyEntities;
494
+ }
495
+ this._adapter.updateStates(updateStates);
496
+ this._adapter.rePositionDropdown();
426
497
  }
427
498
 
428
499
  // call when props.value change
@@ -970,25 +1041,7 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
970
1041
 
971
1042
  handleInputChange(sugInput: string) {
972
1043
  this._adapter.updateInputValue(sugInput);
973
- const { keyEntities } = this.getStates();
974
- const { treeNodeFilterProp, filterTreeNode, filterLeafOnly } = this.getProps();
975
- let filteredKeys: string[] = [];
976
- if (sugInput) {
977
- filteredKeys = (Object.values(keyEntities) as BasicEntity[])
978
- .filter(item => {
979
- const { key, _notExist, data } = item;
980
- if (_notExist) {
981
- return false;
982
- }
983
- const filteredPath = this.getItemPropPath(key, treeNodeFilterProp);
984
- return filter(sugInput, data, filterTreeNode, filteredPath);
985
- })
986
- .filter(
987
- item => (filterTreeNode && !filterLeafOnly) ||
988
- this._isLeaf(item as unknown as BasicCascaderData)
989
- )
990
- .map(item => item.key);
991
- }
1044
+ const filteredKeys = this._calcFilteredKeys(sugInput);
992
1045
 
993
1046
  this._adapter.updateStates({
994
1047
  isSearching: Boolean(sugInput),
@@ -1054,7 +1107,9 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
1054
1107
  getRenderData() {
1055
1108
  const { keyEntities, isSearching } = this.getStates();
1056
1109
  const isFilterable = this._isFilterable();
1110
+
1057
1111
  if (isSearching && isFilterable) {
1112
+ // Both local & remote search mode should render flattened search list
1058
1113
  return this.getFilteredData();
1059
1114
  }
1060
1115
  return (Object.values(keyEntities) as BasicEntity[])
@@ -1129,7 +1129,44 @@ $module-list: #{$prefix}-scrolllist;
1129
1129
  @include font-size-small;
1130
1130
  line-height: $lineHeight-datepicker_compact;
1131
1131
 
1132
+ // Max width constraint for compact panels
1133
+ $compact-max-width: calc(100vw - 32px);
1134
+
1135
+ // Make the compact panel shrink-to-fit its contents.
1136
+ // This ensures the popover background/container expands together with
1137
+ // long locale texts instead of letting inner buttons visually overflow.
1138
+ display: inline-block;
1139
+ max-width: $compact-max-width;
1140
+
1141
+ // Ensure the popover expands to fit this compact datepicker
1142
+ &:not(&-insetInput) {
1143
+ width: max-content;
1144
+ }
1145
+
1146
+ .#{$module}-container {
1147
+ // Keep flex layout but allow the container to size by content.
1148
+ width: max-content;
1149
+ }
1150
+
1132
1151
  .#{$module}-month-grid {
1152
+ // Let the month grid contribute its intrinsic width to the popover.
1153
+ width: max-content;
1154
+
1155
+ .#{$module}-month-grid-left,
1156
+ .#{$module}-month-grid-right {
1157
+ min-width: $width-datepicker_month_compact;
1158
+ // Keep the current compact width as the baseline,
1159
+ // but allow the panel to grow to fit long locale texts.
1160
+ // When reaching viewport limit, month title will ellipsis.
1161
+ max-width: $compact-max-width;
1162
+ }
1163
+
1164
+ // Keep calendar body centered when panel grows
1165
+ // without breaking navigation layout in range mode.
1166
+ .#{$module}-month {
1167
+ margin-left: auto;
1168
+ margin-right: auto;
1169
+ }
1133
1170
 
1134
1171
  &[x-type="dateTime"],
1135
1172
  &[x-type="dateTimeRange"] {
@@ -1139,6 +1176,18 @@ $module-list: #{$prefix}-scrolllist;
1139
1176
  }
1140
1177
  }
1141
1178
 
1179
+ // Allow yam (year-month scrolllist) panel to expand with content in compact mode
1180
+ .#{$module}-yam {
1181
+ // Use relative positioning so parent can expand to fit content width
1182
+ // Height is constrained to prevent scrolllist from expanding too much
1183
+ position: relative;
1184
+ width: 100%;
1185
+ max-width: 100%;
1186
+ max-height: $height-datepicker_yam_panel_compact;
1187
+ overflow-x: auto;
1188
+ overflow-y: hidden;
1189
+ }
1190
+
1142
1191
  &[x-type="dateRange"],
1143
1192
  &[x-type="dateTimeRange"] {
1144
1193
  .#{$module}-month-grid-left {
@@ -1169,6 +1218,18 @@ $module-list: #{$prefix}-scrolllist;
1169
1218
  box-sizing: border-box;
1170
1219
  height: $height-datepicker_yam_panel_header_compact;
1171
1220
  padding: $spacing-datepicker_yam_panel_header_compact-padding;
1221
+ width: 100%;
1222
+ max-width: 100%;
1223
+
1224
+ // Constrain the back button to prevent overflow
1225
+ button,
1226
+ .#{$prefix}-button {
1227
+ width: 100%;
1228
+ max-width: 100%;
1229
+ overflow: hidden;
1230
+ text-overflow: ellipsis;
1231
+ white-space: nowrap;
1232
+ }
1172
1233
  }
1173
1234
 
1174
1235
  .#{$module}-yearmonth-body {
@@ -1265,9 +1326,14 @@ $module-list: #{$prefix}-scrolllist;
1265
1326
  }
1266
1327
 
1267
1328
  .#{$module}-navigation {
1329
+ box-sizing: border-box;
1268
1330
  height: $width-datepicker_nav_compact;
1269
1331
  padding: $spacing-datepicker_nav_compact-padding;
1270
1332
  padding-bottom: 0;
1333
+ // In compact mode, keep navigation constrained within each panel
1334
+ // (especially important for dateRange two-column layout).
1335
+ width: 100%;
1336
+ max-width: 100%;
1271
1337
 
1272
1338
  &-left,
1273
1339
  &-right {
@@ -1281,10 +1347,21 @@ $module-list: #{$prefix}-scrolllist;
1281
1347
  }
1282
1348
 
1283
1349
  &-month {
1350
+ min-width: 0;
1351
+ overflow: hidden;
1352
+
1284
1353
  .#{$prefix}-button {
1285
1354
  // 覆盖样式,否则会从button继承
1286
1355
  @include font-size-small;
1287
1356
  line-height: $lineHeight-datepicker_compact;
1357
+ max-width: 100%;
1358
+
1359
+ > span {
1360
+ display: block;
1361
+ overflow: hidden;
1362
+ text-overflow: ellipsis;
1363
+ white-space: nowrap;
1364
+ }
1288
1365
  }
1289
1366
  }
1290
1367
  }
@@ -1498,7 +1575,10 @@ $module-list: #{$prefix}-scrolllist;
1498
1575
  }
1499
1576
 
1500
1577
  &-month {
1501
- max-width: $width-datepicker_panel_compact;
1578
+ // Keep compact baseline width, but allow growing to fit long locale texts.
1579
+ min-width: $width-datepicker_panel_compact;
1580
+ width: max-content;
1581
+ max-width: $compact-max-width;
1502
1582
 
1503
1583
  &[x-insetinput=true] {
1504
1584
  .#{$module}-quick-control-right-content-wrapper,
@@ -1514,7 +1594,10 @@ $module-list: #{$prefix}-scrolllist;
1514
1594
  }
1515
1595
 
1516
1596
  &-date {
1517
- max-width: $width-datepicker_panel_compact;
1597
+ // Keep compact baseline width, but allow growing to fit long locale texts.
1598
+ min-width: $width-datepicker_panel_compact;
1599
+ width: max-content;
1600
+ max-width: $compact-max-width;
1518
1601
 
1519
1602
  &[x-insetinput=true] {
1520
1603
  .#{$module}-quick-control-right-content-wrapper,
@@ -1530,7 +1613,10 @@ $module-list: #{$prefix}-scrolllist;
1530
1613
  }
1531
1614
 
1532
1615
  &-dateTime {
1533
- max-width: $width-datepicker_panel_compact;
1616
+ // Keep compact baseline width, but allow growing to fit long locale texts.
1617
+ min-width: $width-datepicker_panel_compact;
1618
+ width: max-content;
1619
+ max-width: $compact-max-width;
1534
1620
 
1535
1621
  &[x-insetinput=true] {
1536
1622
  .#{$module}-quick-control-right-content-wrapper,
@@ -1546,7 +1632,12 @@ $module-list: #{$prefix}-scrolllist;
1546
1632
  }
1547
1633
 
1548
1634
  &-dateRange {
1549
- max-width: $width-datepicker_panel_compact * 2;
1635
+ // Keep compact baseline width, but allow growing to fit long locale texts.
1636
+ // Avoid min-width > max-width on small viewports (which would force overflow).
1637
+ // Keep the 2-panel baseline when possible, but cap it to the compact viewport limit.
1638
+ min-width: min(#{$width-datepicker_panel_compact * 2}, #{$compact-max-width});
1639
+ width: max-content;
1640
+ max-width: $compact-max-width;
1550
1641
 
1551
1642
  &[x-insetinput=true] {
1552
1643
  .#{$module}-quick-control-right-content-wrapper,
@@ -1562,7 +1653,11 @@ $module-list: #{$prefix}-scrolllist;
1562
1653
  }
1563
1654
 
1564
1655
  &-dateTimeRange {
1565
- max-width: $width-datepicker_panel_compact * 2;
1656
+ // Keep compact baseline width, but allow growing to fit long locale texts.
1657
+ // Avoid min-width > max-width on small viewports (which would force overflow).
1658
+ min-width: min(#{$width-datepicker_panel_compact * 2}, #{$compact-max-width});
1659
+ width: max-content;
1660
+ max-width: $compact-max-width;
1566
1661
 
1567
1662
  &[x-insetinput=true] {
1568
1663
  .#{$module}-quick-control-right-content-wrapper,
@@ -158,7 +158,9 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
158
158
  }
159
159
 
160
160
  validate(fieldPaths?: Array<string> | ValidateOptions): Promise<unknown> {
161
- const { validateFields } = this.getProps();
161
+ const props = this.getProps();
162
+ // `validator` is the recommended name; `validateFields` is kept as a deprecated alias.
163
+ const validateFields = props.validator || props.validateFields;
162
164
 
163
165
  // Parse options
164
166
  let fields: Array<string> | undefined;
@@ -184,7 +186,9 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
184
186
  // form level validate
185
187
  _formValidate(silent: boolean = false): Promise<unknown> {
186
188
  const { values } = this.data;
187
- const { validateFields } = this.getProps();
189
+ const props = this.getProps();
190
+ // `validator` is the recommended name; `validateFields` is kept as a deprecated alias.
191
+ const validateFields = props.validator || props.validateFields;
188
192
 
189
193
  return new Promise((resolve, reject) => {
190
194
  let maybePromisedErrors;
@@ -245,7 +249,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
245
249
  }
246
250
 
247
251
  // field level validate
248
- _fieldsValidate(fieldPaths: Array<string>, silent: boolean = false): Promise<unknown> {
252
+ _fieldsValidate(fieldPaths?: Array<string>, silent: boolean = false): Promise<unknown> {
249
253
  const { values } = this.data;
250
254
  // When there is no custom validation function at Form level, perform validation of each Field
251
255
  return new Promise((resolve, reject) => {
package/form/utils.ts CHANGED
@@ -131,6 +131,7 @@ export function mergeProps(props: any) {
131
131
  wrapperCol,
132
132
  initValue,
133
133
  validate,
134
+ validator,
134
135
  /**
135
136
  * error、warning、default、success
136
137
  */
@@ -179,6 +180,10 @@ export function mergeProps(props: any) {
179
180
  const required = isRequired(rules);
180
181
 
181
182
  emptyValue = typeof emptyValue !== 'undefined' ? emptyValue : '';
183
+
184
+ // `validator` is the recommended name; `validate` is kept as a deprecated alias.
185
+ const finalValidate = validator || validate;
186
+
182
187
  return {
183
188
  field,
184
189
  label,
@@ -191,7 +196,7 @@ export function mergeProps(props: any) {
191
196
  noErrorMessage,
192
197
  isInInputGroup,
193
198
  initValue,
194
- validate,
199
+ validate: finalValidate,
195
200
  validateStatus,
196
201
  trigger,
197
202
  allowEmptyString,
@@ -218,4 +223,4 @@ export function mergeProps(props: any) {
218
223
 
219
224
  function bothEmptyArray(val: any, otherVal: any) {
220
225
  return Array.isArray(val) && Array.isArray(otherVal) && !val.length && !otherVal.length;
221
- }
226
+ }
@@ -51,6 +51,28 @@ export default class PreviewImageFoundation<P = Record<string, any>, S = Record<
51
51
  containerWidth = 0;
52
52
  containerHeight = 0;
53
53
 
54
+ // initialZoom should only be applied once per image src (first initialization)
55
+ private _initialZoomApplied = false;
56
+ private _initialZoomAppliedSrc: any = undefined;
57
+
58
+ private _syncInitialZoomFlagWithSrc = () => {
59
+ const src = this.getProp("src");
60
+ if (src !== this._initialZoomAppliedSrc) {
61
+ this._initialZoomAppliedSrc = src;
62
+ this._initialZoomApplied = false;
63
+ }
64
+ };
65
+
66
+ private _clampZoom = (zoom: number) => {
67
+ const { maxZoom, minZoom } = this.getProps() as any;
68
+ const max = typeof maxZoom === 'number' ? maxZoom : 5;
69
+ const min = typeof minZoom === 'number' ? minZoom : 0.1;
70
+ if (typeof zoom !== 'number' || !Number.isFinite(zoom)) {
71
+ return min;
72
+ }
73
+ return Math.min(max, Math.max(min, zoom));
74
+ };
75
+
54
76
  init() {
55
77
  this._getContainerBoundingRectSize();
56
78
  }
@@ -84,14 +106,21 @@ export default class PreviewImageFoundation<P = Record<string, any>, S = Record<
84
106
  }
85
107
 
86
108
  _getInitialZoom = () => {
87
- const { ratio } = this.getProps();
109
+ const { ratio, initialZoom } = this.getProps() as any;
88
110
  let _zoom = 1;
89
111
 
112
+ // initialZoom is only used for the first initialization of each src
113
+ this._syncInitialZoomFlagWithSrc();
114
+ if (!this._initialZoomApplied && typeof initialZoom === 'number' && Number.isFinite(initialZoom) && initialZoom > 0) {
115
+ this._initialZoomApplied = true;
116
+ return this._clampZoom(initialZoom);
117
+ }
118
+
90
119
  if (ratio === 'adaptation') {
91
120
  _zoom = this._getAdaptationZoom();
92
121
  }
93
122
 
94
- return _zoom;
123
+ return this._clampZoom(_zoom);
95
124
  }
96
125
 
97
126
  setLoading = (loading: boolean) => {
@@ -108,6 +137,8 @@ export default class PreviewImageFoundation<P = Record<string, any>, S = Record<
108
137
  const { naturalWidth: w, naturalHeight: h } = e.target as any;
109
138
  this.originImageHeight = h;
110
139
  this.originImageWidth = w;
140
+ // New image is loaded; allow initialZoom to be applied once for this src
141
+ this._syncInitialZoomFlagWithSrc();
111
142
  this.setState({
112
143
  loading: false,
113
144
  } as any);
@@ -357,4 +388,4 @@ export default class PreviewImageFoundation<P = Record<string, any>, S = Record<
357
388
  y: boundOffsetY
358
389
  };
359
390
  }
360
- }
391
+ }
@@ -205,12 +205,13 @@ export default class PreviewInnerFoundation<P = Record<string, any>, S = Record<
205
205
 
206
206
  handleZoomImage = (newZoom: number, notify: boolean = true, e?: WheelEvent) => {
207
207
  const { zoom } = this.getStates();
208
- if (zoom !== newZoom) {
209
- notify && this._adapter.notifyZoom(newZoom, newZoom > zoom);
208
+ const nextZoom = this._clampZoom(newZoom);
209
+ if (zoom !== nextZoom) {
210
+ notify && this._adapter.notifyZoom(nextZoom, nextZoom > zoom);
210
211
 
211
- this._adapter.changeImageZoom(newZoom, e);
212
+ this._adapter.changeImageZoom(nextZoom, e);
212
213
  this.setState({
213
- zoom: newZoom,
214
+ zoom: nextZoom,
214
215
  } as any);
215
216
  }
216
217
  }
@@ -316,4 +317,14 @@ export default class PreviewInnerFoundation<P = Record<string, any>, S = Record<
316
317
  this.preloadSingleImage();
317
318
  }
318
319
  }
320
+
321
+ private _clampZoom = (zoom: number) => {
322
+ const { maxZoom, minZoom } = this.getProps() as any;
323
+ const max = typeof maxZoom === 'number' ? maxZoom : 5;
324
+ const min = typeof minZoom === 'number' ? minZoom : 0.1;
325
+ if (typeof zoom !== 'number' || !Number.isFinite(zoom)) {
326
+ return min;
327
+ }
328
+ return Math.min(max, Math.max(min, zoom));
329
+ };
319
330
  }
@@ -16,6 +16,21 @@ $module: #{$prefix}-input;
16
16
  transition: background-color $transition_duration-input-bg $transition_function-input-bg $transition_delay-input-bg,
17
17
  border $transition_duration-input-border $transition_function-input-border $transition_delay-input-border;
18
18
 
19
+ // When native resize changes textarea width, wrapper (border/clear/counter) should follow.
20
+ // Default wrapper is `width: 100%`, so it won't grow with textarea. Enable shrink-to-fit.
21
+ &-resizeX {
22
+ // Keep original textarea wrapper formatting (stacking counter, etc.),
23
+ // only shrink-to-fit width so border follows horizontal resize.
24
+ // Using inline-flex here may change internal layout and cause clear/counter misalignment.
25
+ display: inline-block;
26
+ width: fit-content;
27
+ max-width: 100%;
28
+ }
29
+
30
+ &-resizeY {
31
+ // Keep default width behavior; vertical resize doesn't require wrapper width change.
32
+ }
33
+
19
34
  &:hover {
20
35
  background-color: $color-input_default-bg-hover;
21
36
  }
@@ -40,6 +55,10 @@ $module: #{$prefix}-input;
40
55
  color: $color-textarea-icon-default;
41
56
  right: $spacing-textarea-icon-right;
42
57
  height: $height-textarea-default;
58
+ // Center the icon within the clearbtn area
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
43
62
 
44
63
  & > svg {
45
64
  pointer-events: none;
@@ -130,6 +149,7 @@ $module: #{$prefix}-input;
130
149
 
131
150
  .#{$module}-textarea {
132
151
  position: relative;
152
+ // resize is now controlled by resize prop, default to none for backward compatibility
133
153
  resize: none;
134
154
  // min-height: $height-input_default;
135
155
  padding: $spacing-textarea-paddingY $spacing-textarea-paddingX;
@@ -177,6 +197,8 @@ $module: #{$prefix}-input;
177
197
 
178
198
  &-autosize {
179
199
  overflow: hidden;
200
+ // When autosize is enabled, force resize to none to avoid conflicts
201
+ resize: none;
180
202
  }
181
203
 
182
204
  &-counter {
@@ -235,6 +257,19 @@ $module: #{$prefix}-input;
235
257
  padding: 0;
236
258
  align-items: flex-start;
237
259
 
260
+ &.#{$module}-textarea-wrapper-resizeX {
261
+ // Keep line number + textarea layout, but let width shrink-to-fit
262
+ display: inline-flex;
263
+ width: fit-content;
264
+ max-width: 100%;
265
+
266
+ .#{$module}-textarea-content {
267
+ flex: 0 0 auto;
268
+ }
269
+
270
+ // Do not force a minimum width here; allow the control to fit narrow containers.
271
+ }
272
+
238
273
  .#{$module}-textarea-lineNumber {
239
274
  flex-shrink: 0;
240
275
  padding: $spacing-textarea-paddingY $spacing-textarea-paddingX;
@@ -114,6 +114,7 @@ export interface BasicCascaderProps {
114
114
  preventScroll?: boolean;
115
115
  virtualizeInSearch?: Virtualize;
116
116
  checkRelation?: string;
117
+ remote?: boolean;
117
118
  onClear?: () => void;
118
119
  triggerRender?: (props: BasicTriggerRenderProps) => any;
119
120
  onListScroll?: (e: any, panel: BasicScrollPanelProps) => void;
@@ -204,6 +205,17 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
204
205
  getItemPropPath(selectedKey: string, prop: string | any[], keyEntities?: BasicEntities): any[];
205
206
  _getCacheValue(keyEntities: BasicEntities): any;
206
207
  collectOptions(init?: boolean): void;
208
+ /**
209
+ * Calculate filtered keys based on current props.
210
+ * - In remote mode: do not do local match, treat current treeData nodes as results
211
+ * - In local mode: perform matching by filterTreeNode
212
+ */
213
+ _calcFilteredKeys(sugInput: string, keyEntities?: BasicEntities): string[];
214
+ /**
215
+ * Sync filteredKeys with latest options/keyEntities WITHOUT triggering onSearch.
216
+ * Used when treeData changes asynchronously in searching state.
217
+ */
218
+ recalculateFilteredKeys(input?: string, nextKeyEntities?: BasicEntities): void;
207
219
  handleValueChange(value: BasicValue): void;
208
220
  /**
209
221
  * When single selection, the clear objects of
@@ -218,6 +218,72 @@ class CascaderFoundation extends _foundation.default {
218
218
  keyEntities
219
219
  });
220
220
  }
221
+ // If options(treeData) updates during searching (e.g. remote search async update),
222
+ // we need to sync filteredKeys with the latest keyEntities; otherwise the UI may
223
+ // render empty list ("暂无数据") because filteredKeys are based on stale entities.
224
+ // NOTE: updateSelectedKey/updateStates are async in React, so we pass keyEntities
225
+ // explicitly to avoid reading stale state.
226
+ this.recalculateFilteredKeys(undefined, keyEntities);
227
+ }
228
+ /**
229
+ * Calculate filtered keys based on current props.
230
+ * - In remote mode: do not do local match, treat current treeData nodes as results
231
+ * - In local mode: perform matching by filterTreeNode
232
+ */
233
+ _calcFilteredKeys(sugInput, keyEntities) {
234
+ if (!sugInput) {
235
+ return [];
236
+ }
237
+ const {
238
+ treeNodeFilterProp,
239
+ filterTreeNode,
240
+ filterLeafOnly,
241
+ remote
242
+ } = this.getProps();
243
+ const entities = Object.values(keyEntities !== null && keyEntities !== void 0 ? keyEntities : this.getState('keyEntities'));
244
+ if (remote) {
245
+ return entities.filter(item => !item._notExist).filter(item => filterTreeNode && !filterLeafOnly || this._isLeaf(item.data)).map(item => item.key);
246
+ }
247
+ return entities.filter(item => {
248
+ const {
249
+ key,
250
+ _notExist,
251
+ data
252
+ } = item;
253
+ if (_notExist) {
254
+ return false;
255
+ }
256
+ const filteredPath = this.getItemPropPath(key, treeNodeFilterProp, keyEntities);
257
+ return (0, _util.filter)(sugInput, data, filterTreeNode, filteredPath);
258
+ }).filter(item => filterTreeNode && !filterLeafOnly || this._isLeaf(item.data)).map(item => item.key);
259
+ }
260
+ /**
261
+ * Sync filteredKeys with latest options/keyEntities WITHOUT triggering onSearch.
262
+ * Used when treeData changes asynchronously in searching state.
263
+ */
264
+ recalculateFilteredKeys(input, nextKeyEntities) {
265
+ const isFilterable = this._isFilterable();
266
+ if (!isFilterable) {
267
+ return;
268
+ }
269
+ // When input is not explicitly provided, only recalculate in searching state.
270
+ // Otherwise, treeData updates may incorrectly force component into searching mode
271
+ // because inputValue can be the selected label text in normal (non-searching) state.
272
+ const currentIsSearching = this.getState('isSearching');
273
+ if ((0, _isUndefined2.default)(input) && !currentIsSearching) {
274
+ return;
275
+ }
276
+ const sugInput = (0, _isUndefined2.default)(input) ? this.getState('inputValue') : input;
277
+ const filteredKeys = this._calcFilteredKeys(sugInput, nextKeyEntities);
278
+ const updateStates = {
279
+ isSearching: Boolean(sugInput),
280
+ filteredKeys: new Set(filteredKeys)
281
+ };
282
+ if (nextKeyEntities) {
283
+ updateStates.keyEntities = nextKeyEntities;
284
+ }
285
+ this._adapter.updateStates(updateStates);
286
+ this._adapter.rePositionDropdown();
221
287
  }
222
288
  // call when props.value change
223
289
  handleValueChange(value) {
@@ -804,29 +870,7 @@ class CascaderFoundation extends _foundation.default {
804
870
  }
805
871
  handleInputChange(sugInput) {
806
872
  this._adapter.updateInputValue(sugInput);
807
- const {
808
- keyEntities
809
- } = this.getStates();
810
- const {
811
- treeNodeFilterProp,
812
- filterTreeNode,
813
- filterLeafOnly
814
- } = this.getProps();
815
- let filteredKeys = [];
816
- if (sugInput) {
817
- filteredKeys = Object.values(keyEntities).filter(item => {
818
- const {
819
- key,
820
- _notExist,
821
- data
822
- } = item;
823
- if (_notExist) {
824
- return false;
825
- }
826
- const filteredPath = this.getItemPropPath(key, treeNodeFilterProp);
827
- return (0, _util.filter)(sugInput, data, filterTreeNode, filteredPath);
828
- }).filter(item => filterTreeNode && !filterLeafOnly || this._isLeaf(item)).map(item => item.key);
829
- }
873
+ const filteredKeys = this._calcFilteredKeys(sugInput);
830
874
  this._adapter.updateStates({
831
875
  isSearching: Boolean(sugInput),
832
876
  filteredKeys: new Set(filteredKeys)
@@ -897,6 +941,7 @@ class CascaderFoundation extends _foundation.default {
897
941
  } = this.getStates();
898
942
  const isFilterable = this._isFilterable();
899
943
  if (isSearching && isFilterable) {
944
+ // Both local & remote search mode should render flattened search list
900
945
  return this.getFilteredData();
901
946
  }
902
947
  return Object.values(keyEntities).filter(item => item.parentKey === null && !item._notExist)