@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
@@ -34,11 +34,21 @@ class OverflowListFoundation extends _foundation.default {
34
34
  } = this.getProps();
35
35
  const {
36
36
  visibleState,
37
- overflow
37
+ overflow,
38
+ scrollOverflow
38
39
  } = this.getStates();
39
40
  if (!this.isScrollMode()) {
40
41
  return overflow;
41
42
  }
43
+ // Scroll mode relies on IntersectionObserver to compute visibility.
44
+ // During recalculation (e.g. items changed, or before observer fires),
45
+ // keep last computed overflow to avoid UI flicker (tabs arrows/menu state).
46
+ if (!visibleState || visibleState.size === 0) {
47
+ if (Array.isArray(scrollOverflow) && scrollOverflow.length === 2) {
48
+ return scrollOverflow;
49
+ }
50
+ return [[], []];
51
+ }
42
52
  const visibleStateArr = items.map(_ref => {
43
53
  let {
44
54
  key
@@ -47,11 +57,49 @@ class OverflowListFoundation extends _foundation.default {
47
57
  });
48
58
  const visibleStart = visibleStateArr.indexOf(true);
49
59
  const visibleEnd = visibleStateArr.lastIndexOf(true);
60
+ // If no item is visible (e.g. initial layout not ready or list out of viewport),
61
+ // treat it as "unknown" and keep last computed overflow to avoid wrong enabling.
62
+ if (visibleStart < 0 || visibleEnd < 0) {
63
+ if (Array.isArray(scrollOverflow) && scrollOverflow.length === 2) {
64
+ return scrollOverflow;
65
+ }
66
+ return [[], []];
67
+ }
50
68
  const overflowList = [];
51
69
  overflowList[0] = visibleStart >= 0 ? items.slice(0, visibleStart) : [];
52
70
  overflowList[1] = visibleEnd >= 0 ? items.slice(visibleEnd + 1, items.length) : items.slice();
53
71
  return overflowList;
54
72
  }
73
+ _syncScrollOverflowCache(nextVisibleState) {
74
+ if (!this.isScrollMode()) {
75
+ return;
76
+ }
77
+ const {
78
+ items
79
+ } = this.getProps();
80
+ const visibleStateArr = items.map(_ref2 => {
81
+ let {
82
+ key
83
+ } = _ref2;
84
+ return Boolean(nextVisibleState.get(key));
85
+ });
86
+ const visibleStart = visibleStateArr.indexOf(true);
87
+ const visibleEnd = visibleStateArr.lastIndexOf(true);
88
+ // No visible items means the result is not reliable; keep cache and stay "calculating"
89
+ if (visibleStart < 0 || visibleEnd < 0) {
90
+ this._adapter.updateStates({
91
+ isScrollOverflowCalculating: true
92
+ });
93
+ return;
94
+ }
95
+ const overflowList = [];
96
+ overflowList[0] = visibleStart >= 0 ? items.slice(0, visibleStart) : [];
97
+ overflowList[1] = visibleEnd >= 0 ? items.slice(visibleEnd + 1, items.length) : items.slice();
98
+ this._adapter.updateStates({
99
+ scrollOverflow: overflowList,
100
+ isScrollOverflowCalculating: false
101
+ });
102
+ }
55
103
  handleIntersect(entries) {
56
104
  const visibleState = (0, _fastCopy.default)(this.getState('visibleState'));
57
105
  const res = {};
@@ -80,6 +128,8 @@ class OverflowListFoundation extends _foundation.default {
80
128
  }
81
129
  this.previousY = currentY;
82
130
  this._adapter.updateVisibleState(visibleState);
131
+ // Keep scroll overflow cache in sync for stable UI
132
+ this._syncScrollOverflowCache(visibleState);
83
133
  this._adapter.notifyIntersect(res);
84
134
  }
85
135
  handleCollapseOverflow() {
@@ -54,7 +54,7 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
54
54
  constructor(adapter: SelectAdapter);
55
55
  _keydownHandler: (...arg: any[]) => void | null;
56
56
  init(): void;
57
- focus(optionsForOpen?: BasicOptionProps[]): void;
57
+ focus(optionsForOpen?: BasicOptionProps[], openDropdown?: boolean): void;
58
58
  _focusTrigger(): void;
59
59
  destroy(): void;
60
60
  _setDropdownWidth(): void;
@@ -46,15 +46,40 @@ class SelectFoundation extends _foundation.default {
46
46
  this.focus(originalOptions);
47
47
  }
48
48
  }
49
- focus(optionsForOpen) {
49
+ focus(optionsForOpen, openDropdown) {
50
50
  const isFilterable = this._isFilterable();
51
51
  const isMultiple = this._isMultiple();
52
52
  const {
53
53
  isOpen
54
54
  } = this.getStates();
55
+ const {
56
+ searchPosition
57
+ } = this.getProps();
58
+ // Default to true if not specified (backward compatibility)
59
+ const shouldOpenDropdown = openDropdown !== false;
55
60
  this._adapter.updateFocusState(true);
56
61
  this._adapter.setIsFocusInContainer(false);
57
62
  if (isFilterable) {
63
+ /**
64
+ * When openDropdown is false, we only want to "refocus" the Select
65
+ * without changing dropdown visibility.
66
+ *
67
+ * NOTE: For searchPosition='dropdown', the search input is rendered in dropdown,
68
+ * so we should NOT toggle trigger input(showInput) here, otherwise it may affect
69
+ * tabIndex / focusability unexpectedly.
70
+ */
71
+ if (!shouldOpenDropdown) {
72
+ if (searchPosition === _constants.strings.SEARCH_POSITION_TRIGGER) {
73
+ if (isMultiple) {
74
+ this.focusInput();
75
+ } else {
76
+ this.toggle2SearchInput(true);
77
+ }
78
+ } else {
79
+ this._focusTrigger();
80
+ }
81
+ return;
82
+ }
58
83
  if (isMultiple) {
59
84
  // when filter and multiple, focus input and open dropdown
60
85
  this.focusInput();
@@ -1092,7 +1117,8 @@ class SelectFoundation extends _foundation.default {
1092
1117
  this.clearInput(e);
1093
1118
  }
1094
1119
  // after click showClear button, the select need to be focused
1095
- this.focus();
1120
+ // but don't open dropdown to avoid focus state confusion
1121
+ this.focus(undefined, false);
1096
1122
  this.clearSelected();
1097
1123
  // prevent this click open dropdown
1098
1124
  e.stopPropagation();
@@ -33,6 +33,7 @@
33
33
  }
34
34
  .semi-switch-checked .semi-switch-knob {
35
35
  transform: translateX(18px);
36
+ background-color: rgba(var(--semi-white), 1);
36
37
  }
37
38
  .semi-switch-checked:active .semi-switch-knob {
38
39
  transform: translateX(12px);
@@ -43,6 +43,7 @@ $module: #{$prefix}-switch;
43
43
 
44
44
  .#{$module}-knob {
45
45
  transform: translateX($spacing-switch_checked-translateX);
46
+ background-color: $color-switch_knob-bg-checked;
46
47
  }
47
48
 
48
49
  &:active {
@@ -30,7 +30,8 @@ $color-switch_disabled-border-default: var(--semi-color-border); // 禁用态开
30
30
  $color-switch_disabled-bg-hover: transparent; // 禁用态开关背景色 - 悬浮
31
31
  $color-switch_checked_disabled-bg-default: var(--semi-color-success-disabled); // 禁用开启态开关背景颜色
32
32
  $color-switch_checked_disabled-border-default: transparent; // 禁用开启态开关描边颜色
33
- $color-switch_knob-bg-default: rgba(var(--semi-white), 1); // 开关滑块背景颜色
33
+ $color-switch_knob-bg-default: rgba(var(--semi-white), 1); // 开关滑块背景颜色 - 关闭态
34
+ $color-switch_knob-bg-checked: rgba(var(--semi-white), 1); // 开关滑块背景颜色 - 开启态
34
35
  $color-switch_knob-border-default: var(--semi-color-border); // 开关滑块描边颜色
35
36
  $color-switch_checked-text-default: var(--semi-color-white); // 开启态开关文案颜色
36
37
  $color-switch_unchecked-text-default: var(--semi-color-text-2); // 关闭态开关文案颜色
@@ -888,7 +888,8 @@ class TableFoundation extends _foundation.default {
888
888
  let e = arguments.length > 1 ? arguments[1] : undefined;
889
889
  let check = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
890
890
  var _a, _b, _c, _d;
891
- this.stopPropagation(e);
891
+ /* Do not call stopPropagation here, otherwise the click event registered via onHeaderCell
892
+ will be blocked when the click hot area is the whole title (#1861). */
892
893
  /* if mouse down to the resizable handle, do not trigger the sorting,fix #2802
893
894
  The target of the click event may be different from the target of the mousedown,
894
895
  e.g: Press the mouse, move to another node and then release it,
@@ -505,6 +505,32 @@
505
505
  color: var(--semi-color-text-0);
506
506
  }
507
507
 
508
+ .semi-tag-split {
509
+ display: inline-flex;
510
+ align-items: center;
511
+ }
512
+ .semi-tag-split .semi-tag {
513
+ border-radius: 0;
514
+ margin-right: 1px;
515
+ }
516
+ .semi-tag-split .semi-tag-first {
517
+ border-top-left-radius: var(--semi-border-radius-small);
518
+ border-bottom-left-radius: var(--semi-border-radius-small);
519
+ }
520
+ .semi-tag-split .semi-tag-last {
521
+ border-top-right-radius: var(--semi-border-radius-small);
522
+ border-bottom-right-radius: var(--semi-border-radius-small);
523
+ margin-right: unset;
524
+ }
525
+ .semi-tag-split .semi-tag-circle.semi-tag-first {
526
+ border-top-left-radius: var(--semi-border-radius-full);
527
+ border-bottom-left-radius: var(--semi-border-radius-full);
528
+ }
529
+ .semi-tag-split .semi-tag-circle.semi-tag-last {
530
+ border-top-right-radius: var(--semi-border-radius-full);
531
+ border-bottom-right-radius: var(--semi-border-radius-full);
532
+ }
533
+
508
534
  .semi-rtl .semi-tag,
509
535
  .semi-portal-rtl .semi-tag {
510
536
  direction: rtl;
@@ -329,4 +329,37 @@ $types: "ghost", "solid", "light";
329
329
  color: $color-tag_avatar-text-default;
330
330
  }
331
331
 
332
+ .#{$module}-split {
333
+ display: inline-flex;
334
+ align-items: center;
335
+
336
+ .#{$module} {
337
+ border-radius: 0;
338
+ margin-right: 1px;
339
+
340
+ &-first {
341
+ border-top-left-radius: $radius-tag;
342
+ border-bottom-left-radius: $radius-tag;
343
+ }
344
+
345
+ &-last {
346
+ border-top-right-radius: $radius-tag;
347
+ border-bottom-right-radius: $radius-tag;
348
+ margin-right: unset;
349
+ }
350
+
351
+ &-circle {
352
+ &.#{$module}-first {
353
+ border-top-left-radius: $radius-tag_circle;
354
+ border-bottom-left-radius: $radius-tag_circle;
355
+ }
356
+
357
+ &.#{$module}-last {
358
+ border-top-right-radius: $radius-tag_circle;
359
+ border-bottom-right-radius: $radius-tag_circle;
360
+ }
361
+ }
362
+ }
363
+ }
364
+
332
365
  @import './rtl.scss';
@@ -112,6 +112,8 @@
112
112
  padding-left: 4px;
113
113
  padding-right: 4px;
114
114
  overflow: hidden;
115
+ position: relative;
116
+ /* hidden mirror used to measure input text width */
115
117
  }
116
118
  .semi-tagInput-wrapper-tag {
117
119
  margin-right: 4px;
@@ -154,6 +156,8 @@
154
156
  .semi-tagInput-wrapper .semi-tagInput-wrapper-input {
155
157
  flex-grow: 1;
156
158
  width: min-content;
159
+ min-width: 2px;
160
+ max-width: 100%;
157
161
  border: none;
158
162
  outline: none;
159
163
  background-color: transparent;
@@ -195,6 +199,19 @@
195
199
  height: 24px;
196
200
  line-height: 24px;
197
201
  }
202
+ .semi-tagInput-wrapper-inputMirror {
203
+ position: absolute;
204
+ top: 0;
205
+ left: 0;
206
+ visibility: hidden;
207
+ pointer-events: none;
208
+ height: 0;
209
+ overflow: hidden;
210
+ white-space: pre;
211
+ font-size: 14px;
212
+ font-weight: 400;
213
+ font-family: inherit;
214
+ }
198
215
  .semi-tagInput-clearBtn {
199
216
  display: flex;
200
217
  justify-content: center;
@@ -137,6 +137,7 @@ $module: #{$prefix}-tagInput;
137
137
  padding-left: $spacing-extra-tight;
138
138
  padding-right: $spacing-extra-tight;
139
139
  overflow: hidden;
140
+ position: relative;
140
141
 
141
142
  &-tag {
142
143
  margin-right: $spacing-extra-tight;
@@ -190,6 +191,8 @@ $module: #{$prefix}-tagInput;
190
191
  & &-input {
191
192
  flex-grow: 1;
192
193
  width: min-content;
194
+ min-width: 2px;
195
+ max-width: 100%;
193
196
  // min-width: 38px;
194
197
  border: none;
195
198
  outline: none;
@@ -240,6 +243,21 @@ $module: #{$prefix}-tagInput;
240
243
  }
241
244
  }
242
245
  }
246
+
247
+ /* hidden mirror used to measure input text width */
248
+ &-inputMirror {
249
+ position: absolute;
250
+ top: 0;
251
+ left: 0;
252
+ visibility: hidden;
253
+ pointer-events: none;
254
+ height: 0;
255
+ overflow: hidden;
256
+ white-space: pre;
257
+ font-size: $font-size-regular;
258
+ font-weight: $font-weight-regular;
259
+ font-family: inherit;
260
+ }
243
261
  }
244
262
 
245
263
  &-clearBtn {
@@ -1,3 +1,4 @@
1
+ export type PanelType = 'left' | 'right';
1
2
  declare const cssClasses: {
2
3
  PREFIX: string;
3
4
  RANGE_PICKER: string;
@@ -1,3 +1,4 @@
1
+ import type { PanelType } from './constants';
1
2
  import BaseFoundation, { DefaultAdapter } from '../base/foundation';
2
3
  export type Position = 'top' | 'topLeft' | 'topRight' | 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'leftTopOver' | 'rightTopOver';
3
4
  export interface TimePickerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
@@ -16,11 +17,16 @@ declare class TimePickerFoundation<P = Record<string, any>, S = Record<string, a
16
17
  constructor(adapter: TimePickerAdapter<P, S>);
17
18
  init(): void;
18
19
  getPosition(): Position;
20
+ getDisabledTimeFns(panelType: PanelType, dates: Date[]): {
21
+ disabledHours: any;
22
+ disabledMinutes: any;
23
+ disabledSeconds: any;
24
+ };
19
25
  isDisabledHMS({ hours, minutes, seconds }: {
20
26
  hours: number;
21
27
  minutes: number;
22
28
  seconds: number;
23
- }): boolean;
29
+ }, panelType?: PanelType, dates?: Date[]): boolean;
24
30
  isValidTimeZone(timeZone: string | number): boolean;
25
31
  getDefaultFormatIfNeed(): string;
26
32
  /**
@@ -33,17 +33,44 @@ class TimePickerFoundation extends _foundation.default {
33
33
  const rtlDirection = direction === 'rtl' ? 'bottomRight' : '';
34
34
  return position || rtlDirection || _constants.strings.DEFAULT_POSITION[type];
35
35
  }
36
+ getDisabledTimeFns(panelType, dates) {
37
+ const {
38
+ disabledHours,
39
+ disabledMinutes,
40
+ disabledSeconds,
41
+ disabledTime
42
+ } = this.getProps();
43
+ // disabledTime is range-only: only invoke it when the picker is
44
+ // actually a range picker. In single mode panelType has no meaning, so
45
+ // we fall back to the top-level disabledHours / disabledMinutes /
46
+ // disabledSeconds without invoking disabledTime.
47
+ if (typeof disabledTime === 'function' && this._adapter.isRangePicker()) {
48
+ const disabledObj = disabledTime(dates, panelType) || {};
49
+ return {
50
+ disabledHours: disabledObj.disabledHours || disabledHours,
51
+ disabledMinutes: disabledObj.disabledMinutes || disabledMinutes,
52
+ disabledSeconds: disabledObj.disabledSeconds || disabledSeconds
53
+ };
54
+ }
55
+ return {
56
+ disabledHours,
57
+ disabledMinutes,
58
+ disabledSeconds
59
+ };
60
+ }
36
61
  isDisabledHMS(_ref) {
37
62
  let {
38
63
  hours,
39
64
  minutes,
40
65
  seconds
41
66
  } = _ref;
67
+ let panelType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'left';
68
+ let dates = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
42
69
  const {
43
70
  disabledHours,
44
71
  disabledMinutes,
45
72
  disabledSeconds
46
- } = this.getProps();
73
+ } = this.getDisabledTimeFns(panelType, dates);
47
74
  const hDis = !(0, _isNullOrUndefined.default)(hours) && (0, _utils.hourIsDisabled)(disabledHours, hours);
48
75
  const mDis = !(0, _isNullOrUndefined.default)(hours) && !(0, _isNullOrUndefined.default)(minutes) && (0, _utils.minuteIsDisabled)(disabledMinutes, hours, minutes);
49
76
  const sDis = !(0, _isNullOrUndefined.default)(hours) && !(0, _isNullOrUndefined.default)(minutes) && !(0, _isNullOrUndefined.default)(seconds) && (0, _utils.secondIsDisabled)(disabledSeconds, hours, minutes, seconds);
@@ -165,11 +192,23 @@ class TimePickerFoundation extends _foundation.default {
165
192
  if (this.isValidTimeZone(timeZone)) {
166
193
  dates = dates.map(date => (0, _dateFnsExtra.utcToZonedTime)(this.isValidTimeZone(__prevTimeZone) ? (0, _dateFnsExtra.zonedTimeToUtc)(date, __prevTimeZone) : date, timeZone));
167
194
  }
168
- invalid = dates.some(d => this.isDisabledHMS({
169
- hours: d.getHours(),
170
- minutes: d.getMinutes(),
171
- seconds: d.getSeconds()
172
- }));
195
+ if (this._adapter.isRangePicker()) {
196
+ invalid = dates.some((d, idx) => {
197
+ const panelType = idx === 1 ? 'right' : 'left';
198
+ return this.isDisabledHMS({
199
+ hours: d.getHours(),
200
+ minutes: d.getMinutes(),
201
+ seconds: d.getSeconds()
202
+ }, panelType, dates);
203
+ });
204
+ } else {
205
+ const d = dates[0];
206
+ invalid = d ? this.isDisabledHMS({
207
+ hours: d.getHours(),
208
+ minutes: d.getMinutes(),
209
+ seconds: d.getSeconds()
210
+ }, 'left', dates) : false;
211
+ }
173
212
  }
174
213
  const inputValue = this.formatValue(dates);
175
214
  this.setState({
@@ -265,11 +304,23 @@ class TimePickerFoundation extends _foundation.default {
265
304
  let dates = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
266
305
  let invalid = dates.some(d => isNaN(Number(d)));
267
306
  if (!invalid) {
268
- invalid = dates.some(d => this.isDisabledHMS({
269
- hours: d.getHours(),
270
- minutes: d.getMinutes(),
271
- seconds: d.getSeconds()
272
- }));
307
+ if (this._adapter.isRangePicker()) {
308
+ invalid = dates.some((d, idx) => {
309
+ const panelType = idx === 1 ? 'right' : 'left';
310
+ return this.isDisabledHMS({
311
+ hours: d.getHours(),
312
+ minutes: d.getMinutes(),
313
+ seconds: d.getSeconds()
314
+ }, panelType, dates);
315
+ });
316
+ } else {
317
+ const d = dates[0];
318
+ invalid = d ? this.isDisabledHMS({
319
+ hours: d.getHours(),
320
+ minutes: d.getMinutes(),
321
+ seconds: d.getSeconds()
322
+ }, 'left', dates) : false;
323
+ }
273
324
  }
274
325
  return invalid;
275
326
  }
@@ -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
@@ -211,6 +211,72 @@ export default class CascaderFoundation extends BaseFoundation {
211
211
  keyEntities
212
212
  });
213
213
  }
214
+ // If options(treeData) updates during searching (e.g. remote search async update),
215
+ // we need to sync filteredKeys with the latest keyEntities; otherwise the UI may
216
+ // render empty list ("暂无数据") because filteredKeys are based on stale entities.
217
+ // NOTE: updateSelectedKey/updateStates are async in React, so we pass keyEntities
218
+ // explicitly to avoid reading stale state.
219
+ this.recalculateFilteredKeys(undefined, keyEntities);
220
+ }
221
+ /**
222
+ * Calculate filtered keys based on current props.
223
+ * - In remote mode: do not do local match, treat current treeData nodes as results
224
+ * - In local mode: perform matching by filterTreeNode
225
+ */
226
+ _calcFilteredKeys(sugInput, keyEntities) {
227
+ if (!sugInput) {
228
+ return [];
229
+ }
230
+ const {
231
+ treeNodeFilterProp,
232
+ filterTreeNode,
233
+ filterLeafOnly,
234
+ remote
235
+ } = this.getProps();
236
+ const entities = Object.values(keyEntities !== null && keyEntities !== void 0 ? keyEntities : this.getState('keyEntities'));
237
+ if (remote) {
238
+ return entities.filter(item => !item._notExist).filter(item => filterTreeNode && !filterLeafOnly || this._isLeaf(item.data)).map(item => item.key);
239
+ }
240
+ return entities.filter(item => {
241
+ const {
242
+ key,
243
+ _notExist,
244
+ data
245
+ } = item;
246
+ if (_notExist) {
247
+ return false;
248
+ }
249
+ const filteredPath = this.getItemPropPath(key, treeNodeFilterProp, keyEntities);
250
+ return filter(sugInput, data, filterTreeNode, filteredPath);
251
+ }).filter(item => filterTreeNode && !filterLeafOnly || this._isLeaf(item.data)).map(item => item.key);
252
+ }
253
+ /**
254
+ * Sync filteredKeys with latest options/keyEntities WITHOUT triggering onSearch.
255
+ * Used when treeData changes asynchronously in searching state.
256
+ */
257
+ recalculateFilteredKeys(input, nextKeyEntities) {
258
+ const isFilterable = this._isFilterable();
259
+ if (!isFilterable) {
260
+ return;
261
+ }
262
+ // When input is not explicitly provided, only recalculate in searching state.
263
+ // Otherwise, treeData updates may incorrectly force component into searching mode
264
+ // because inputValue can be the selected label text in normal (non-searching) state.
265
+ const currentIsSearching = this.getState('isSearching');
266
+ if (_isUndefined(input) && !currentIsSearching) {
267
+ return;
268
+ }
269
+ const sugInput = _isUndefined(input) ? this.getState('inputValue') : input;
270
+ const filteredKeys = this._calcFilteredKeys(sugInput, nextKeyEntities);
271
+ const updateStates = {
272
+ isSearching: Boolean(sugInput),
273
+ filteredKeys: new Set(filteredKeys)
274
+ };
275
+ if (nextKeyEntities) {
276
+ updateStates.keyEntities = nextKeyEntities;
277
+ }
278
+ this._adapter.updateStates(updateStates);
279
+ this._adapter.rePositionDropdown();
214
280
  }
215
281
  // call when props.value change
216
282
  handleValueChange(value) {
@@ -797,29 +863,7 @@ export default class CascaderFoundation extends BaseFoundation {
797
863
  }
798
864
  handleInputChange(sugInput) {
799
865
  this._adapter.updateInputValue(sugInput);
800
- const {
801
- keyEntities
802
- } = this.getStates();
803
- const {
804
- treeNodeFilterProp,
805
- filterTreeNode,
806
- filterLeafOnly
807
- } = this.getProps();
808
- let filteredKeys = [];
809
- if (sugInput) {
810
- filteredKeys = Object.values(keyEntities).filter(item => {
811
- const {
812
- key,
813
- _notExist,
814
- data
815
- } = item;
816
- if (_notExist) {
817
- return false;
818
- }
819
- const filteredPath = this.getItemPropPath(key, treeNodeFilterProp);
820
- return filter(sugInput, data, filterTreeNode, filteredPath);
821
- }).filter(item => filterTreeNode && !filterLeafOnly || this._isLeaf(item)).map(item => item.key);
822
- }
866
+ const filteredKeys = this._calcFilteredKeys(sugInput);
823
867
  this._adapter.updateStates({
824
868
  isSearching: Boolean(sugInput),
825
869
  filteredKeys: new Set(filteredKeys)
@@ -890,6 +934,7 @@ export default class CascaderFoundation extends BaseFoundation {
890
934
  } = this.getStates();
891
935
  const isFilterable = this._isFilterable();
892
936
  if (isSearching && isFilterable) {
937
+ // Both local & remote search mode should render flattened search list
893
938
  return this.getFilteredData();
894
939
  }
895
940
  return Object.values(keyEntities).filter(item => item.parentKey === null && !item._notExist)