@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.
- package/cascader/foundation.ts +74 -19
- package/datePicker/datePicker.scss +100 -5
- package/form/foundation.ts +7 -3
- package/form/utils.ts +7 -2
- package/image/previewImageFoundation.ts +34 -3
- package/image/previewInnerFoundation.ts +15 -4
- package/input/textarea.scss +35 -0
- package/lib/cjs/cascader/foundation.d.ts +12 -0
- package/lib/cjs/cascader/foundation.js +68 -23
- package/lib/cjs/datePicker/datePicker.css +67 -5
- package/lib/cjs/datePicker/datePicker.scss +100 -5
- package/lib/cjs/form/foundation.d.ts +1 -1
- package/lib/cjs/form/foundation.js +6 -6
- package/lib/cjs/form/utils.js +5 -2
- package/lib/cjs/image/previewImageFoundation.d.ts +4 -0
- package/lib/cjs/image/previewImageFoundation.js +33 -2
- package/lib/cjs/image/previewInnerFoundation.d.ts +1 -0
- package/lib/cjs/image/previewInnerFoundation.js +17 -4
- package/lib/cjs/input/textarea.css +17 -0
- package/lib/cjs/input/textarea.scss +35 -0
- package/lib/cjs/navigation/navigation.css +2 -1
- package/lib/cjs/navigation/navigation.scss +1 -0
- package/lib/cjs/navigation/variables.scss +1 -1
- package/lib/cjs/overflowList/foundation.d.ts +1 -0
- package/lib/cjs/overflowList/foundation.js +51 -1
- package/lib/cjs/select/foundation.d.ts +1 -1
- package/lib/cjs/select/foundation.js +28 -2
- package/lib/cjs/switch/switch.css +1 -0
- package/lib/cjs/switch/switch.scss +1 -0
- package/lib/cjs/switch/variables.scss +2 -1
- package/lib/cjs/table/foundation.js +2 -1
- package/lib/cjs/tag/tag.css +26 -0
- package/lib/cjs/tag/tag.scss +33 -0
- package/lib/cjs/tagInput/tagInput.css +17 -0
- package/lib/cjs/tagInput/tagInput.scss +18 -0
- package/lib/cjs/timePicker/constants.d.ts +1 -0
- package/lib/cjs/timePicker/foundation.d.ts +7 -1
- package/lib/cjs/timePicker/foundation.js +62 -11
- package/lib/es/cascader/foundation.d.ts +12 -0
- package/lib/es/cascader/foundation.js +68 -23
- package/lib/es/datePicker/datePicker.css +67 -5
- package/lib/es/datePicker/datePicker.scss +100 -5
- package/lib/es/form/foundation.d.ts +1 -1
- package/lib/es/form/foundation.js +6 -6
- package/lib/es/form/utils.js +5 -2
- package/lib/es/image/previewImageFoundation.d.ts +4 -0
- package/lib/es/image/previewImageFoundation.js +33 -2
- package/lib/es/image/previewInnerFoundation.d.ts +1 -0
- package/lib/es/image/previewInnerFoundation.js +17 -4
- package/lib/es/input/textarea.css +17 -0
- package/lib/es/input/textarea.scss +35 -0
- package/lib/es/navigation/navigation.css +2 -1
- package/lib/es/navigation/navigation.scss +1 -0
- package/lib/es/navigation/variables.scss +1 -1
- package/lib/es/overflowList/foundation.d.ts +1 -0
- package/lib/es/overflowList/foundation.js +51 -1
- package/lib/es/select/foundation.d.ts +1 -1
- package/lib/es/select/foundation.js +28 -2
- package/lib/es/switch/switch.css +1 -0
- package/lib/es/switch/switch.scss +1 -0
- package/lib/es/switch/variables.scss +2 -1
- package/lib/es/table/foundation.js +2 -1
- package/lib/es/tag/tag.css +26 -0
- package/lib/es/tag/tag.scss +33 -0
- package/lib/es/tagInput/tagInput.css +17 -0
- package/lib/es/tagInput/tagInput.scss +18 -0
- package/lib/es/timePicker/constants.d.ts +1 -0
- package/lib/es/timePicker/foundation.d.ts +7 -1
- package/lib/es/timePicker/foundation.js +62 -11
- package/navigation/navigation.scss +1 -0
- package/navigation/variables.scss +1 -1
- package/overflowList/foundation.ts +48 -2
- package/package.json +4 -4
- package/select/foundation.ts +27 -2
- package/switch/switch.scss +1 -0
- package/switch/variables.scss +2 -1
- package/table/foundation.ts +2 -1
- package/tag/tag.scss +33 -0
- package/tagInput/tagInput.scss +18 -0
- package/timePicker/constants.ts +2 -0
- package/timePicker/foundation.ts +62 -10
package/cascader/foundation.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/form/foundation.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
208
|
+
const nextZoom = this._clampZoom(newZoom);
|
|
209
|
+
if (zoom !== nextZoom) {
|
|
210
|
+
notify && this._adapter.notifyZoom(nextZoom, nextZoom > zoom);
|
|
210
211
|
|
|
211
|
-
this._adapter.changeImageZoom(
|
|
212
|
+
this._adapter.changeImageZoom(nextZoom, e);
|
|
212
213
|
this.setState({
|
|
213
|
-
zoom:
|
|
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
|
}
|
package/input/textarea.scss
CHANGED
|
@@ -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)
|