@douyinfe/semi-foundation 2.102.0 → 2.103.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.
@@ -20,6 +20,7 @@
20
20
  .semi-progress-horizontal .semi-progress-track {
21
21
  height: 100%;
22
22
  width: 100%;
23
+ overflow: hidden;
23
24
  }
24
25
  .semi-progress-horizontal .semi-progress-track-inner {
25
26
  height: 100%;
@@ -28,6 +29,11 @@
28
29
  transition: width 0.3s;
29
30
  transition-timing-function: cubic-bezier(0.62, 0.05, 0.36, 0.95);
30
31
  }
32
+ .semi-progress-horizontal .semi-progress-track-inner-indeterminate {
33
+ width: 35%;
34
+ transition: none;
35
+ animation: semi-progress-indeterminate-slide 1.6s ease-in-out infinite;
36
+ }
31
37
  .semi-progress-horizontal .semi-progress-line-text {
32
38
  min-width: 45px;
33
39
  font-weight: 600;
@@ -48,6 +54,7 @@
48
54
  .semi-progress-vertical .semi-progress-track {
49
55
  height: 100%;
50
56
  width: 100%;
57
+ overflow: hidden;
51
58
  }
52
59
  .semi-progress-vertical .semi-progress-track-inner {
53
60
  background-color: var(--semi-color-success);
@@ -56,6 +63,12 @@
56
63
  transition: height 0.3s;
57
64
  transition-timing-function: cubic-bezier(0.62, 0.05, 0.36, 0.95);
58
65
  }
66
+ .semi-progress-vertical .semi-progress-track-inner-indeterminate {
67
+ height: 35%;
68
+ width: 100%;
69
+ transition: none;
70
+ animation: semi-progress-indeterminate-slide-vertical 1.6s ease-in-out infinite;
71
+ }
59
72
  .semi-progress-vertical .semi-progress-line-text {
60
73
  font-weight: 600;
61
74
  margin-top: 8px;
@@ -77,6 +90,11 @@
77
90
  transform-origin: 50% 50%;
78
91
  stroke: var(--semi-color-success);
79
92
  }
93
+ .semi-progress-circle-ring-inner-indeterminate {
94
+ transform: rotate(0deg);
95
+ transform-origin: 50% 50%;
96
+ animation: semi-progress-indeterminate-rotate 1.2s linear infinite;
97
+ }
80
98
  .semi-progress-circle-text {
81
99
  position: absolute;
82
100
  top: 50%;
@@ -88,6 +106,36 @@
88
106
  color: var(--semi-color-mode-minor-text);
89
107
  }
90
108
 
109
+ @keyframes semi-progress-indeterminate-slide {
110
+ 0% {
111
+ transform: translateX(-120%);
112
+ }
113
+ 55% {
114
+ transform: translateX(300%);
115
+ }
116
+ 100% {
117
+ transform: translateX(300%);
118
+ }
119
+ }
120
+ @keyframes semi-progress-indeterminate-slide-vertical {
121
+ 0% {
122
+ transform: translateY(-120%);
123
+ }
124
+ 55% {
125
+ transform: translateY(300%);
126
+ }
127
+ 100% {
128
+ transform: translateY(300%);
129
+ }
130
+ }
131
+ @keyframes semi-progress-indeterminate-rotate {
132
+ from {
133
+ transform: rotate(0deg);
134
+ }
135
+ to {
136
+ transform: rotate(360deg);
137
+ }
138
+ }
91
139
  .semi-rtl .semi-progress,
92
140
  .semi-portal-rtl .semi-progress {
93
141
  direction: rtl;
@@ -23,6 +23,7 @@ $module: #{$prefix}-progress;
23
23
  .#{$module}-track {
24
24
  height: 100%;
25
25
  width: 100%;
26
+ overflow: hidden;
26
27
  }
27
28
 
28
29
  .#{$module}-track-inner {
@@ -33,6 +34,12 @@ $module: #{$prefix}-progress;
33
34
  transition-timing-function: $motion-progress-transition_timing_function;
34
35
  }
35
36
 
37
+ .#{$module}-track-inner-indeterminate {
38
+ width: 35%;
39
+ transition: none;
40
+ animation: semi-progress-indeterminate-slide 1.6s ease-in-out infinite;
41
+ }
42
+
36
43
  .#{$module}-line-text {
37
44
  min-width: $width-progress_line_text;
38
45
  font-weight: $font-progress_line_text-fontWeight;
@@ -57,6 +64,7 @@ $module: #{$prefix}-progress;
57
64
  .#{$module}-track {
58
65
  height: 100%;
59
66
  width: 100%;
67
+ overflow: hidden;
60
68
  }
61
69
 
62
70
  .#{$module}-track-inner {
@@ -67,6 +75,13 @@ $module: #{$prefix}-progress;
67
75
  transition-timing-function: $motion-progress-transition_timing_function;
68
76
  }
69
77
 
78
+ .#{$module}-track-inner-indeterminate {
79
+ height: 35%;
80
+ width: 100%;
81
+ transition: none;
82
+ animation: semi-progress-indeterminate-slide-vertical 1.6s ease-in-out infinite;
83
+ }
84
+
70
85
  .#{$module}-line-text {
71
86
  font-weight: $font-progress_line_text-fontWeight;
72
87
  margin-top: $spacing-progress_vertical_line_text-marginTop;
@@ -95,6 +110,12 @@ $module: #{$prefix}-progress;
95
110
  stroke: $color-progress_track_inner-bg;
96
111
  }
97
112
 
113
+ &-ring-inner-indeterminate {
114
+ transform: rotate(0deg);
115
+ transform-origin: 50% 50%;
116
+ animation: semi-progress-indeterminate-rotate 1.2s linear infinite;
117
+ }
118
+
98
119
  &-text {
99
120
  position: absolute;
100
121
  top: 50%;
@@ -108,4 +129,42 @@ $module: #{$prefix}-progress;
108
129
  }
109
130
  }
110
131
 
111
- @import './rtl.scss';
132
+ @keyframes semi-progress-indeterminate-slide {
133
+ 0% {
134
+ transform: translateX(-120%);
135
+ }
136
+
137
+ 55% {
138
+ transform: translateX(300%);
139
+ }
140
+
141
+ 100% {
142
+ transform: translateX(300%);
143
+ }
144
+ }
145
+
146
+ @keyframes semi-progress-indeterminate-slide-vertical {
147
+ 0% {
148
+ transform: translateY(-120%);
149
+ }
150
+
151
+ 55% {
152
+ transform: translateY(300%);
153
+ }
154
+
155
+ 100% {
156
+ transform: translateY(300%);
157
+ }
158
+ }
159
+
160
+ @keyframes semi-progress-indeterminate-rotate {
161
+ from {
162
+ transform: rotate(0deg);
163
+ }
164
+
165
+ to {
166
+ transform: rotate(360deg);
167
+ }
168
+ }
169
+
170
+ @import './rtl.scss';
@@ -94,6 +94,33 @@ declare class UploadFoundation<P = Record<string, any>, S = Record<string, any>>
94
94
  * When paste event is successfully handled, we ignore the subsequent keydown event.
95
95
  */
96
96
  _pasteHandled: boolean;
97
+ /**
98
+ * Working copy of fileList used during a synchronous startUpload batch.
99
+ * Do NOT rely on React state fileList here: setState is async (batched in React 18),
100
+ * so reading getState('fileList') between iterations of a sync loop returns a stale
101
+ * snapshot and earlier removals get overwritten by later ones (see #3335).
102
+ * Seeded at the start of startUpload and cleared when the sync loop finishes (see
103
+ * try/finally in startUpload); async (promise) results fall back to _latestFileList
104
+ * then React state.
105
+ */
106
+ _batchFileList: Array<BaseFileItem> | null;
107
+ /**
108
+ * The most recent fileList produced by handleBeforeUploadResultInObject.
109
+ * Unlike React state (which is flushed asynchronously under React 18 automatic
110
+ * batching), this is updated synchronously, so a later async beforeUpload result
111
+ * composes with earlier sync removals of the same batch instead of reading a stale
112
+ * snapshot (mixed sync + async beforeUpload results, see #3335).
113
+ */
114
+ _latestFileList: Array<BaseFileItem> | null;
115
+ /**
116
+ * Unify all fileList commits: keep _latestFileList in sync synchronously so async
117
+ * beforeUpload results compose with recent mutations even before React flushes
118
+ * setState (React 18 automatic batching). (#3335)
119
+ */
120
+ updateFileListInternal(newFileList: Array<BaseFileItem>, callback?: () => void): void;
121
+ /** Keep the synchronous snapshot aligned with an externally controlled fileList. */
122
+ syncLatestFileList(fileList: Array<BaseFileItem>): void;
123
+ getLatestFileList(): Array<BaseFileItem>;
97
124
  constructor(adapter: UploadAdapter<P, S>);
98
125
  init(): void;
99
126
  /**
@@ -48,6 +48,25 @@ const {
48
48
  TRIGGER_AUTO
49
49
  } = _constants.strings;
50
50
  class UploadFoundation extends _foundation.default {
51
+ /**
52
+ * Unify all fileList commits: keep _latestFileList in sync synchronously so async
53
+ * beforeUpload results compose with recent mutations even before React flushes
54
+ * setState (React 18 automatic batching). (#3335)
55
+ */
56
+ updateFileListInternal(newFileList, callback) {
57
+ if (this._batchFileList) {
58
+ this._batchFileList = newFileList;
59
+ }
60
+ this._latestFileList = newFileList;
61
+ this._adapter.updateFileList(newFileList, callback);
62
+ }
63
+ /** Keep the synchronous snapshot aligned with an externally controlled fileList. */
64
+ syncLatestFileList(fileList) {
65
+ this._latestFileList = fileList.slice();
66
+ }
67
+ getLatestFileList() {
68
+ return this._batchFileList || this._latestFileList || this.getState('fileList');
69
+ }
51
70
  constructor(adapter) {
52
71
  super(Object.assign({}, adapter));
53
72
  this.destroyState = false;
@@ -62,10 +81,29 @@ class UploadFoundation extends _foundation.default {
62
81
  * When paste event is successfully handled, we ignore the subsequent keydown event.
63
82
  */
64
83
  this._pasteHandled = false;
84
+ /**
85
+ * Working copy of fileList used during a synchronous startUpload batch.
86
+ * Do NOT rely on React state fileList here: setState is async (batched in React 18),
87
+ * so reading getState('fileList') between iterations of a sync loop returns a stale
88
+ * snapshot and earlier removals get overwritten by later ones (see #3335).
89
+ * Seeded at the start of startUpload and cleared when the sync loop finishes (see
90
+ * try/finally in startUpload); async (promise) results fall back to _latestFileList
91
+ * then React state.
92
+ */
93
+ this._batchFileList = null;
94
+ /**
95
+ * The most recent fileList produced by handleBeforeUploadResultInObject.
96
+ * Unlike React state (which is flushed asynchronously under React 18 automatic
97
+ * batching), this is updated synchronously, so a later async beforeUpload result
98
+ * composes with earlier sync removals of the same batch instead of reading a stale
99
+ * snapshot (mixed sync + async beforeUpload results, see #3335).
100
+ */
101
+ this._latestFileList = null;
65
102
  }
66
103
  init() {
67
104
  // make sure state reset, otherwise may cause upload abort in React StrictMode, like https://github.com/DouyinFE/semi-design/pull/843
68
105
  this.destroyState = false;
106
+ this.syncLatestFileList(this.getState('fileList'));
69
107
  // In controlled mode, parent may keep and pass back fileList with blob url.
70
108
  // Sync them into internal map so later remove/clear can revoke correctly.
71
109
  this._syncLocalUrlsFromFileList();
@@ -115,6 +153,8 @@ class UploadFoundation extends _foundation.default {
115
153
  if (!disabled) {
116
154
  this.unbindPastingHandler();
117
155
  }
156
+ this._batchFileList = null;
157
+ this._latestFileList = null;
118
158
  this.destroyState = true;
119
159
  }
120
160
  /**
@@ -331,7 +371,7 @@ class UploadFoundation extends _foundation.default {
331
371
  currentFile: newFileItem,
332
372
  fileList: newFileList
333
373
  });
334
- this._adapter.updateFileList(newFileList, () => {
374
+ this.updateFileListInternal(newFileList, () => {
335
375
  this._adapter.resetReplaceInput();
336
376
  if (!newFileItem._sizeInvalid) {
337
377
  this.upload(newFileItem);
@@ -375,7 +415,7 @@ class UploadFoundation extends _foundation.default {
375
415
  fileList: currentFiles,
376
416
  currentFile: currentFiles[0]
377
417
  });
378
- this._adapter.updateFileList(currentFiles, () => {
418
+ this.updateFileListInternal(currentFiles, () => {
379
419
  if (uploadTrigger === TRIGGER_AUTO) {
380
420
  this.startUpload(currentFiles);
381
421
  }
@@ -399,7 +439,7 @@ class UploadFoundation extends _foundation.default {
399
439
  });
400
440
  }
401
441
  });
402
- this._adapter.updateFileList(fileList, () => {
442
+ this.updateFileListInternal(fileList, () => {
403
443
  if (uploadTrigger === TRIGGER_AUTO) {
404
444
  this.startUpload(currentFiles);
405
445
  }
@@ -483,7 +523,7 @@ class UploadFoundation extends _foundation.default {
483
523
  fileList: newFileList,
484
524
  currentFile: null
485
525
  });
486
- this._adapter.updateFileList(newFileList, () => {
526
+ this.updateFileListInternal(newFileList, () => {
487
527
  if (uploadTrigger === TRIGGER_AUTO) {
488
528
  this.startUpload(fileItemList);
489
529
  }
@@ -492,15 +532,24 @@ class UploadFoundation extends _foundation.default {
492
532
  /* istanbul ignore next */
493
533
  manualUpload() {
494
534
  // find the list of files that have not been uploaded
495
- const waitToUploadFileList = this.getState('fileList').filter(item => item.status === FILE_STATUS_WAIT_UPLOAD);
535
+ const waitToUploadFileList = this.getLatestFileList().filter(item => item.status === FILE_STATUS_WAIT_UPLOAD);
496
536
  this.startUpload(waitToUploadFileList);
497
537
  }
498
538
  startUpload(fileList) {
499
- fileList.forEach(file => {
500
- if (!file._sizeInvalid) {
501
- this.upload(file);
502
- }
503
- });
539
+ // Seed a working copy so multiple synchronous beforeUpload results
540
+ // (e.g. several files returning autoRemove) compose correctly instead of each
541
+ // reading a stale React state snapshot and overwriting earlier removals. (#3335)
542
+ // try/finally guarantees the working copy is cleared even if beforeUpload throws.
543
+ this._batchFileList = this.getLatestFileList().slice();
544
+ try {
545
+ fileList.forEach(file => {
546
+ if (!file._sizeInvalid) {
547
+ this.upload(file);
548
+ }
549
+ });
550
+ } finally {
551
+ this._batchFileList = null;
552
+ }
504
553
  }
505
554
  upload(file) {
506
555
  const {
@@ -511,9 +560,7 @@ class UploadFoundation extends _foundation.default {
511
560
  return;
512
561
  }
513
562
  if (typeof beforeUpload === 'function') {
514
- const {
515
- fileList
516
- } = this.getStates();
563
+ const fileList = this.getLatestFileList();
517
564
  const buResult = this._adapter.notifyBeforeUpload({
518
565
  file,
519
566
  fileList
@@ -578,7 +625,11 @@ class UploadFoundation extends _foundation.default {
578
625
  validateMessage,
579
626
  fileInstance
580
627
  } = buResult;
581
- let newFileList = this.getState('fileList').slice();
628
+ // During a synchronous startUpload batch, read from the working copy; for async
629
+ // (promise) results read from the most recent synchronous snapshot, falling back
630
+ // to React state. This keeps mixed sync + async beforeUpload results composing
631
+ // correctly under React 18 automatic batching. (#3335)
632
+ let newFileList = this.getLatestFileList().slice();
582
633
  if (autoRemove) {
583
634
  this._releaseFileUrl(file.uid);
584
635
  newFileList = newFileList.filter(item => item.uid !== file.uid);
@@ -600,7 +651,7 @@ class UploadFoundation extends _foundation.default {
600
651
  }
601
652
  newFileList[index].shouldUpload = shouldUpload;
602
653
  }
603
- this._adapter.updateFileList(newFileList);
654
+ this.updateFileListInternal(newFileList);
604
655
  this._adapter.notifyChange({
605
656
  fileList: newFileList,
606
657
  currentFile: file
@@ -714,10 +765,7 @@ class UploadFoundation extends _foundation.default {
714
765
  e,
715
766
  fileInstance
716
767
  } = _ref2;
717
- const {
718
- fileList
719
- } = this.getStates();
720
- const newFileList = fileList.slice();
768
+ const newFileList = this.getLatestFileList().slice();
721
769
  let percent = 0;
722
770
  if (e.total > 0) {
723
771
  percent = Number((e.loaded / e.total * 100 * _constants.numbers.PROGRESS_COEFFICIENT).toFixed(0)) || 0;
@@ -729,7 +777,7 @@ class UploadFoundation extends _foundation.default {
729
777
  newFileList[index].percent = percent;
730
778
  newFileList[index].status = FILE_STATUS_UPLOADING;
731
779
  this._adapter.notifyProgress(percent, fileInstance, newFileList);
732
- this._adapter.updateFileList(newFileList);
780
+ this.updateFileListInternal(newFileList);
733
781
  this._adapter.notifyChange({
734
782
  fileList: newFileList,
735
783
  currentFile: newFileList[index]
@@ -741,9 +789,7 @@ class UploadFoundation extends _foundation.default {
741
789
  xhr,
742
790
  fileInstance
743
791
  } = _ref3;
744
- const {
745
- fileList
746
- } = this.getStates();
792
+ const fileList = this.getLatestFileList();
747
793
  const index = this._getFileIndex(fileInstance, fileList);
748
794
  if (index < 0) {
749
795
  return;
@@ -771,9 +817,7 @@ class UploadFoundation extends _foundation.default {
771
817
  xhr,
772
818
  response
773
819
  } = _ref4;
774
- const {
775
- fileList
776
- } = this.getStates();
820
+ const fileList = this.getLatestFileList();
777
821
  let body = null;
778
822
  const index = this._getFileIndex(fileInstance, fileList);
779
823
  if (index < 0) {
@@ -824,7 +868,7 @@ class UploadFoundation extends _foundation.default {
824
868
  fileList: newFileList,
825
869
  currentFile: newFileList[index]
826
870
  });
827
- this._adapter.updateFileList(newFileList);
871
+ this.updateFileListInternal(newFileList);
828
872
  }
829
873
  _getFileIndex(file, fileList) {
830
874
  return fileList.findIndex(item => item.uid === file.uid);
@@ -852,7 +896,7 @@ class UploadFoundation extends _foundation.default {
852
896
  newFileList.splice(index, 1);
853
897
  this._releaseFileUrl(file.uid);
854
898
  this._adapter.notifyRemove(file.fileInstance, newFileList, file);
855
- this._adapter.updateFileList(newFileList);
899
+ this.updateFileListInternal(newFileList);
856
900
  this._adapter.notifyChange({
857
901
  fileList: newFileList,
858
902
  currentFile: file
@@ -865,9 +909,7 @@ class UploadFoundation extends _foundation.default {
865
909
  xhr,
866
910
  fileInstance
867
911
  } = _ref5;
868
- const {
869
- fileList
870
- } = this.getStates();
912
+ const fileList = this.getLatestFileList();
871
913
  const index = this._getFileIndex(fileInstance, fileList);
872
914
  if (index < 0) {
873
915
  return;
@@ -885,7 +927,7 @@ class UploadFoundation extends _foundation.default {
885
927
  newFileList[index].response = error;
886
928
  newFileList[index].event = e;
887
929
  this._adapter.notifyError(error, fileInstance, newFileList, xhr);
888
- this._adapter.updateFileList(newFileList);
930
+ this.updateFileListInternal(newFileList);
889
931
  this._adapter.notifyChange({
890
932
  currentFile: newFileList[index],
891
933
  fileList: newFileList
@@ -906,7 +948,7 @@ class UploadFoundation extends _foundation.default {
906
948
  return;
907
949
  }
908
950
  this._releaseAllFileUrls();
909
- this._adapter.updateFileList([]);
951
+ this.updateFileListInternal([]);
910
952
  this._adapter.notifyClear();
911
953
  this._adapter.notifyChange({
912
954
  fileList: []
@@ -20,6 +20,7 @@
20
20
  .semi-progress-horizontal .semi-progress-track {
21
21
  height: 100%;
22
22
  width: 100%;
23
+ overflow: hidden;
23
24
  }
24
25
  .semi-progress-horizontal .semi-progress-track-inner {
25
26
  height: 100%;
@@ -28,6 +29,11 @@
28
29
  transition: width 0.3s;
29
30
  transition-timing-function: cubic-bezier(0.62, 0.05, 0.36, 0.95);
30
31
  }
32
+ .semi-progress-horizontal .semi-progress-track-inner-indeterminate {
33
+ width: 35%;
34
+ transition: none;
35
+ animation: semi-progress-indeterminate-slide 1.6s ease-in-out infinite;
36
+ }
31
37
  .semi-progress-horizontal .semi-progress-line-text {
32
38
  min-width: 45px;
33
39
  font-weight: 600;
@@ -48,6 +54,7 @@
48
54
  .semi-progress-vertical .semi-progress-track {
49
55
  height: 100%;
50
56
  width: 100%;
57
+ overflow: hidden;
51
58
  }
52
59
  .semi-progress-vertical .semi-progress-track-inner {
53
60
  background-color: var(--semi-color-success);
@@ -56,6 +63,12 @@
56
63
  transition: height 0.3s;
57
64
  transition-timing-function: cubic-bezier(0.62, 0.05, 0.36, 0.95);
58
65
  }
66
+ .semi-progress-vertical .semi-progress-track-inner-indeterminate {
67
+ height: 35%;
68
+ width: 100%;
69
+ transition: none;
70
+ animation: semi-progress-indeterminate-slide-vertical 1.6s ease-in-out infinite;
71
+ }
59
72
  .semi-progress-vertical .semi-progress-line-text {
60
73
  font-weight: 600;
61
74
  margin-top: 8px;
@@ -77,6 +90,11 @@
77
90
  transform-origin: 50% 50%;
78
91
  stroke: var(--semi-color-success);
79
92
  }
93
+ .semi-progress-circle-ring-inner-indeterminate {
94
+ transform: rotate(0deg);
95
+ transform-origin: 50% 50%;
96
+ animation: semi-progress-indeterminate-rotate 1.2s linear infinite;
97
+ }
80
98
  .semi-progress-circle-text {
81
99
  position: absolute;
82
100
  top: 50%;
@@ -88,6 +106,36 @@
88
106
  color: var(--semi-color-mode-minor-text);
89
107
  }
90
108
 
109
+ @keyframes semi-progress-indeterminate-slide {
110
+ 0% {
111
+ transform: translateX(-120%);
112
+ }
113
+ 55% {
114
+ transform: translateX(300%);
115
+ }
116
+ 100% {
117
+ transform: translateX(300%);
118
+ }
119
+ }
120
+ @keyframes semi-progress-indeterminate-slide-vertical {
121
+ 0% {
122
+ transform: translateY(-120%);
123
+ }
124
+ 55% {
125
+ transform: translateY(300%);
126
+ }
127
+ 100% {
128
+ transform: translateY(300%);
129
+ }
130
+ }
131
+ @keyframes semi-progress-indeterminate-rotate {
132
+ from {
133
+ transform: rotate(0deg);
134
+ }
135
+ to {
136
+ transform: rotate(360deg);
137
+ }
138
+ }
91
139
  .semi-rtl .semi-progress,
92
140
  .semi-portal-rtl .semi-progress {
93
141
  direction: rtl;
@@ -23,6 +23,7 @@ $module: #{$prefix}-progress;
23
23
  .#{$module}-track {
24
24
  height: 100%;
25
25
  width: 100%;
26
+ overflow: hidden;
26
27
  }
27
28
 
28
29
  .#{$module}-track-inner {
@@ -33,6 +34,12 @@ $module: #{$prefix}-progress;
33
34
  transition-timing-function: $motion-progress-transition_timing_function;
34
35
  }
35
36
 
37
+ .#{$module}-track-inner-indeterminate {
38
+ width: 35%;
39
+ transition: none;
40
+ animation: semi-progress-indeterminate-slide 1.6s ease-in-out infinite;
41
+ }
42
+
36
43
  .#{$module}-line-text {
37
44
  min-width: $width-progress_line_text;
38
45
  font-weight: $font-progress_line_text-fontWeight;
@@ -57,6 +64,7 @@ $module: #{$prefix}-progress;
57
64
  .#{$module}-track {
58
65
  height: 100%;
59
66
  width: 100%;
67
+ overflow: hidden;
60
68
  }
61
69
 
62
70
  .#{$module}-track-inner {
@@ -67,6 +75,13 @@ $module: #{$prefix}-progress;
67
75
  transition-timing-function: $motion-progress-transition_timing_function;
68
76
  }
69
77
 
78
+ .#{$module}-track-inner-indeterminate {
79
+ height: 35%;
80
+ width: 100%;
81
+ transition: none;
82
+ animation: semi-progress-indeterminate-slide-vertical 1.6s ease-in-out infinite;
83
+ }
84
+
70
85
  .#{$module}-line-text {
71
86
  font-weight: $font-progress_line_text-fontWeight;
72
87
  margin-top: $spacing-progress_vertical_line_text-marginTop;
@@ -95,6 +110,12 @@ $module: #{$prefix}-progress;
95
110
  stroke: $color-progress_track_inner-bg;
96
111
  }
97
112
 
113
+ &-ring-inner-indeterminate {
114
+ transform: rotate(0deg);
115
+ transform-origin: 50% 50%;
116
+ animation: semi-progress-indeterminate-rotate 1.2s linear infinite;
117
+ }
118
+
98
119
  &-text {
99
120
  position: absolute;
100
121
  top: 50%;
@@ -108,4 +129,42 @@ $module: #{$prefix}-progress;
108
129
  }
109
130
  }
110
131
 
111
- @import './rtl.scss';
132
+ @keyframes semi-progress-indeterminate-slide {
133
+ 0% {
134
+ transform: translateX(-120%);
135
+ }
136
+
137
+ 55% {
138
+ transform: translateX(300%);
139
+ }
140
+
141
+ 100% {
142
+ transform: translateX(300%);
143
+ }
144
+ }
145
+
146
+ @keyframes semi-progress-indeterminate-slide-vertical {
147
+ 0% {
148
+ transform: translateY(-120%);
149
+ }
150
+
151
+ 55% {
152
+ transform: translateY(300%);
153
+ }
154
+
155
+ 100% {
156
+ transform: translateY(300%);
157
+ }
158
+ }
159
+
160
+ @keyframes semi-progress-indeterminate-rotate {
161
+ from {
162
+ transform: rotate(0deg);
163
+ }
164
+
165
+ to {
166
+ transform: rotate(360deg);
167
+ }
168
+ }
169
+
170
+ @import './rtl.scss';
@@ -94,6 +94,33 @@ declare class UploadFoundation<P = Record<string, any>, S = Record<string, any>>
94
94
  * When paste event is successfully handled, we ignore the subsequent keydown event.
95
95
  */
96
96
  _pasteHandled: boolean;
97
+ /**
98
+ * Working copy of fileList used during a synchronous startUpload batch.
99
+ * Do NOT rely on React state fileList here: setState is async (batched in React 18),
100
+ * so reading getState('fileList') between iterations of a sync loop returns a stale
101
+ * snapshot and earlier removals get overwritten by later ones (see #3335).
102
+ * Seeded at the start of startUpload and cleared when the sync loop finishes (see
103
+ * try/finally in startUpload); async (promise) results fall back to _latestFileList
104
+ * then React state.
105
+ */
106
+ _batchFileList: Array<BaseFileItem> | null;
107
+ /**
108
+ * The most recent fileList produced by handleBeforeUploadResultInObject.
109
+ * Unlike React state (which is flushed asynchronously under React 18 automatic
110
+ * batching), this is updated synchronously, so a later async beforeUpload result
111
+ * composes with earlier sync removals of the same batch instead of reading a stale
112
+ * snapshot (mixed sync + async beforeUpload results, see #3335).
113
+ */
114
+ _latestFileList: Array<BaseFileItem> | null;
115
+ /**
116
+ * Unify all fileList commits: keep _latestFileList in sync synchronously so async
117
+ * beforeUpload results compose with recent mutations even before React flushes
118
+ * setState (React 18 automatic batching). (#3335)
119
+ */
120
+ updateFileListInternal(newFileList: Array<BaseFileItem>, callback?: () => void): void;
121
+ /** Keep the synchronous snapshot aligned with an externally controlled fileList. */
122
+ syncLatestFileList(fileList: Array<BaseFileItem>): void;
123
+ getLatestFileList(): Array<BaseFileItem>;
97
124
  constructor(adapter: UploadAdapter<P, S>);
98
125
  init(): void;
99
126
  /**
@@ -41,6 +41,25 @@ const {
41
41
  TRIGGER_AUTO
42
42
  } = strings;
43
43
  class UploadFoundation extends BaseFoundation {
44
+ /**
45
+ * Unify all fileList commits: keep _latestFileList in sync synchronously so async
46
+ * beforeUpload results compose with recent mutations even before React flushes
47
+ * setState (React 18 automatic batching). (#3335)
48
+ */
49
+ updateFileListInternal(newFileList, callback) {
50
+ if (this._batchFileList) {
51
+ this._batchFileList = newFileList;
52
+ }
53
+ this._latestFileList = newFileList;
54
+ this._adapter.updateFileList(newFileList, callback);
55
+ }
56
+ /** Keep the synchronous snapshot aligned with an externally controlled fileList. */
57
+ syncLatestFileList(fileList) {
58
+ this._latestFileList = fileList.slice();
59
+ }
60
+ getLatestFileList() {
61
+ return this._batchFileList || this._latestFileList || this.getState('fileList');
62
+ }
44
63
  constructor(adapter) {
45
64
  super(Object.assign({}, adapter));
46
65
  this.destroyState = false;
@@ -55,10 +74,29 @@ class UploadFoundation extends BaseFoundation {
55
74
  * When paste event is successfully handled, we ignore the subsequent keydown event.
56
75
  */
57
76
  this._pasteHandled = false;
77
+ /**
78
+ * Working copy of fileList used during a synchronous startUpload batch.
79
+ * Do NOT rely on React state fileList here: setState is async (batched in React 18),
80
+ * so reading getState('fileList') between iterations of a sync loop returns a stale
81
+ * snapshot and earlier removals get overwritten by later ones (see #3335).
82
+ * Seeded at the start of startUpload and cleared when the sync loop finishes (see
83
+ * try/finally in startUpload); async (promise) results fall back to _latestFileList
84
+ * then React state.
85
+ */
86
+ this._batchFileList = null;
87
+ /**
88
+ * The most recent fileList produced by handleBeforeUploadResultInObject.
89
+ * Unlike React state (which is flushed asynchronously under React 18 automatic
90
+ * batching), this is updated synchronously, so a later async beforeUpload result
91
+ * composes with earlier sync removals of the same batch instead of reading a stale
92
+ * snapshot (mixed sync + async beforeUpload results, see #3335).
93
+ */
94
+ this._latestFileList = null;
58
95
  }
59
96
  init() {
60
97
  // make sure state reset, otherwise may cause upload abort in React StrictMode, like https://github.com/DouyinFE/semi-design/pull/843
61
98
  this.destroyState = false;
99
+ this.syncLatestFileList(this.getState('fileList'));
62
100
  // In controlled mode, parent may keep and pass back fileList with blob url.
63
101
  // Sync them into internal map so later remove/clear can revoke correctly.
64
102
  this._syncLocalUrlsFromFileList();
@@ -108,6 +146,8 @@ class UploadFoundation extends BaseFoundation {
108
146
  if (!disabled) {
109
147
  this.unbindPastingHandler();
110
148
  }
149
+ this._batchFileList = null;
150
+ this._latestFileList = null;
111
151
  this.destroyState = true;
112
152
  }
113
153
  /**
@@ -324,7 +364,7 @@ class UploadFoundation extends BaseFoundation {
324
364
  currentFile: newFileItem,
325
365
  fileList: newFileList
326
366
  });
327
- this._adapter.updateFileList(newFileList, () => {
367
+ this.updateFileListInternal(newFileList, () => {
328
368
  this._adapter.resetReplaceInput();
329
369
  if (!newFileItem._sizeInvalid) {
330
370
  this.upload(newFileItem);
@@ -368,7 +408,7 @@ class UploadFoundation extends BaseFoundation {
368
408
  fileList: currentFiles,
369
409
  currentFile: currentFiles[0]
370
410
  });
371
- this._adapter.updateFileList(currentFiles, () => {
411
+ this.updateFileListInternal(currentFiles, () => {
372
412
  if (uploadTrigger === TRIGGER_AUTO) {
373
413
  this.startUpload(currentFiles);
374
414
  }
@@ -392,7 +432,7 @@ class UploadFoundation extends BaseFoundation {
392
432
  });
393
433
  }
394
434
  });
395
- this._adapter.updateFileList(fileList, () => {
435
+ this.updateFileListInternal(fileList, () => {
396
436
  if (uploadTrigger === TRIGGER_AUTO) {
397
437
  this.startUpload(currentFiles);
398
438
  }
@@ -476,7 +516,7 @@ class UploadFoundation extends BaseFoundation {
476
516
  fileList: newFileList,
477
517
  currentFile: null
478
518
  });
479
- this._adapter.updateFileList(newFileList, () => {
519
+ this.updateFileListInternal(newFileList, () => {
480
520
  if (uploadTrigger === TRIGGER_AUTO) {
481
521
  this.startUpload(fileItemList);
482
522
  }
@@ -485,15 +525,24 @@ class UploadFoundation extends BaseFoundation {
485
525
  /* istanbul ignore next */
486
526
  manualUpload() {
487
527
  // find the list of files that have not been uploaded
488
- const waitToUploadFileList = this.getState('fileList').filter(item => item.status === FILE_STATUS_WAIT_UPLOAD);
528
+ const waitToUploadFileList = this.getLatestFileList().filter(item => item.status === FILE_STATUS_WAIT_UPLOAD);
489
529
  this.startUpload(waitToUploadFileList);
490
530
  }
491
531
  startUpload(fileList) {
492
- fileList.forEach(file => {
493
- if (!file._sizeInvalid) {
494
- this.upload(file);
495
- }
496
- });
532
+ // Seed a working copy so multiple synchronous beforeUpload results
533
+ // (e.g. several files returning autoRemove) compose correctly instead of each
534
+ // reading a stale React state snapshot and overwriting earlier removals. (#3335)
535
+ // try/finally guarantees the working copy is cleared even if beforeUpload throws.
536
+ this._batchFileList = this.getLatestFileList().slice();
537
+ try {
538
+ fileList.forEach(file => {
539
+ if (!file._sizeInvalid) {
540
+ this.upload(file);
541
+ }
542
+ });
543
+ } finally {
544
+ this._batchFileList = null;
545
+ }
497
546
  }
498
547
  upload(file) {
499
548
  const {
@@ -504,9 +553,7 @@ class UploadFoundation extends BaseFoundation {
504
553
  return;
505
554
  }
506
555
  if (typeof beforeUpload === 'function') {
507
- const {
508
- fileList
509
- } = this.getStates();
556
+ const fileList = this.getLatestFileList();
510
557
  const buResult = this._adapter.notifyBeforeUpload({
511
558
  file,
512
559
  fileList
@@ -571,7 +618,11 @@ class UploadFoundation extends BaseFoundation {
571
618
  validateMessage,
572
619
  fileInstance
573
620
  } = buResult;
574
- let newFileList = this.getState('fileList').slice();
621
+ // During a synchronous startUpload batch, read from the working copy; for async
622
+ // (promise) results read from the most recent synchronous snapshot, falling back
623
+ // to React state. This keeps mixed sync + async beforeUpload results composing
624
+ // correctly under React 18 automatic batching. (#3335)
625
+ let newFileList = this.getLatestFileList().slice();
575
626
  if (autoRemove) {
576
627
  this._releaseFileUrl(file.uid);
577
628
  newFileList = newFileList.filter(item => item.uid !== file.uid);
@@ -593,7 +644,7 @@ class UploadFoundation extends BaseFoundation {
593
644
  }
594
645
  newFileList[index].shouldUpload = shouldUpload;
595
646
  }
596
- this._adapter.updateFileList(newFileList);
647
+ this.updateFileListInternal(newFileList);
597
648
  this._adapter.notifyChange({
598
649
  fileList: newFileList,
599
650
  currentFile: file
@@ -707,10 +758,7 @@ class UploadFoundation extends BaseFoundation {
707
758
  e,
708
759
  fileInstance
709
760
  } = _ref2;
710
- const {
711
- fileList
712
- } = this.getStates();
713
- const newFileList = fileList.slice();
761
+ const newFileList = this.getLatestFileList().slice();
714
762
  let percent = 0;
715
763
  if (e.total > 0) {
716
764
  percent = Number((e.loaded / e.total * 100 * numbers.PROGRESS_COEFFICIENT).toFixed(0)) || 0;
@@ -722,7 +770,7 @@ class UploadFoundation extends BaseFoundation {
722
770
  newFileList[index].percent = percent;
723
771
  newFileList[index].status = FILE_STATUS_UPLOADING;
724
772
  this._adapter.notifyProgress(percent, fileInstance, newFileList);
725
- this._adapter.updateFileList(newFileList);
773
+ this.updateFileListInternal(newFileList);
726
774
  this._adapter.notifyChange({
727
775
  fileList: newFileList,
728
776
  currentFile: newFileList[index]
@@ -734,9 +782,7 @@ class UploadFoundation extends BaseFoundation {
734
782
  xhr,
735
783
  fileInstance
736
784
  } = _ref3;
737
- const {
738
- fileList
739
- } = this.getStates();
785
+ const fileList = this.getLatestFileList();
740
786
  const index = this._getFileIndex(fileInstance, fileList);
741
787
  if (index < 0) {
742
788
  return;
@@ -764,9 +810,7 @@ class UploadFoundation extends BaseFoundation {
764
810
  xhr,
765
811
  response
766
812
  } = _ref4;
767
- const {
768
- fileList
769
- } = this.getStates();
813
+ const fileList = this.getLatestFileList();
770
814
  let body = null;
771
815
  const index = this._getFileIndex(fileInstance, fileList);
772
816
  if (index < 0) {
@@ -817,7 +861,7 @@ class UploadFoundation extends BaseFoundation {
817
861
  fileList: newFileList,
818
862
  currentFile: newFileList[index]
819
863
  });
820
- this._adapter.updateFileList(newFileList);
864
+ this.updateFileListInternal(newFileList);
821
865
  }
822
866
  _getFileIndex(file, fileList) {
823
867
  return fileList.findIndex(item => item.uid === file.uid);
@@ -845,7 +889,7 @@ class UploadFoundation extends BaseFoundation {
845
889
  newFileList.splice(index, 1);
846
890
  this._releaseFileUrl(file.uid);
847
891
  this._adapter.notifyRemove(file.fileInstance, newFileList, file);
848
- this._adapter.updateFileList(newFileList);
892
+ this.updateFileListInternal(newFileList);
849
893
  this._adapter.notifyChange({
850
894
  fileList: newFileList,
851
895
  currentFile: file
@@ -858,9 +902,7 @@ class UploadFoundation extends BaseFoundation {
858
902
  xhr,
859
903
  fileInstance
860
904
  } = _ref5;
861
- const {
862
- fileList
863
- } = this.getStates();
905
+ const fileList = this.getLatestFileList();
864
906
  const index = this._getFileIndex(fileInstance, fileList);
865
907
  if (index < 0) {
866
908
  return;
@@ -878,7 +920,7 @@ class UploadFoundation extends BaseFoundation {
878
920
  newFileList[index].response = error;
879
921
  newFileList[index].event = e;
880
922
  this._adapter.notifyError(error, fileInstance, newFileList, xhr);
881
- this._adapter.updateFileList(newFileList);
923
+ this.updateFileListInternal(newFileList);
882
924
  this._adapter.notifyChange({
883
925
  currentFile: newFileList[index],
884
926
  fileList: newFileList
@@ -899,7 +941,7 @@ class UploadFoundation extends BaseFoundation {
899
941
  return;
900
942
  }
901
943
  this._releaseAllFileUrls();
902
- this._adapter.updateFileList([]);
944
+ this.updateFileListInternal([]);
903
945
  this._adapter.notifyClear();
904
946
  this._adapter.notifyChange({
905
947
  fileList: []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@douyinfe/semi-foundation",
3
- "version": "2.102.0",
3
+ "version": "2.103.0",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "clean": "rimraf lib",
@@ -14225,8 +14225,8 @@
14225
14225
  }
14226
14226
  },
14227
14227
  "dependencies": {
14228
- "@douyinfe/semi-animation": "2.102.0",
14229
- "@douyinfe/semi-json-viewer-core": "2.102.0",
14228
+ "@douyinfe/semi-animation": "2.103.0",
14229
+ "@douyinfe/semi-json-viewer-core": "2.103.0",
14230
14230
  "@mdx-js/mdx": "^3.0.1",
14231
14231
  "async-validator": "^3.5.0",
14232
14232
  "classnames": "^2.2.6",
@@ -14247,7 +14247,7 @@
14247
14247
  "*.scss",
14248
14248
  "*.css"
14249
14249
  ],
14250
- "gitHead": "88d7738ddb55821f38d3ab3c5b7c877ebda08edf",
14250
+ "gitHead": "c1e3632b90b242defdfe4bfb267bcfaffbb23295",
14251
14251
  "devDependencies": {
14252
14252
  "@babel/plugin-transform-runtime": "^7.15.8",
14253
14253
  "@babel/preset-env": "^7.15.8",
@@ -23,6 +23,7 @@ $module: #{$prefix}-progress;
23
23
  .#{$module}-track {
24
24
  height: 100%;
25
25
  width: 100%;
26
+ overflow: hidden;
26
27
  }
27
28
 
28
29
  .#{$module}-track-inner {
@@ -33,6 +34,12 @@ $module: #{$prefix}-progress;
33
34
  transition-timing-function: $motion-progress-transition_timing_function;
34
35
  }
35
36
 
37
+ .#{$module}-track-inner-indeterminate {
38
+ width: 35%;
39
+ transition: none;
40
+ animation: semi-progress-indeterminate-slide 1.6s ease-in-out infinite;
41
+ }
42
+
36
43
  .#{$module}-line-text {
37
44
  min-width: $width-progress_line_text;
38
45
  font-weight: $font-progress_line_text-fontWeight;
@@ -57,6 +64,7 @@ $module: #{$prefix}-progress;
57
64
  .#{$module}-track {
58
65
  height: 100%;
59
66
  width: 100%;
67
+ overflow: hidden;
60
68
  }
61
69
 
62
70
  .#{$module}-track-inner {
@@ -67,6 +75,13 @@ $module: #{$prefix}-progress;
67
75
  transition-timing-function: $motion-progress-transition_timing_function;
68
76
  }
69
77
 
78
+ .#{$module}-track-inner-indeterminate {
79
+ height: 35%;
80
+ width: 100%;
81
+ transition: none;
82
+ animation: semi-progress-indeterminate-slide-vertical 1.6s ease-in-out infinite;
83
+ }
84
+
70
85
  .#{$module}-line-text {
71
86
  font-weight: $font-progress_line_text-fontWeight;
72
87
  margin-top: $spacing-progress_vertical_line_text-marginTop;
@@ -95,6 +110,12 @@ $module: #{$prefix}-progress;
95
110
  stroke: $color-progress_track_inner-bg;
96
111
  }
97
112
 
113
+ &-ring-inner-indeterminate {
114
+ transform: rotate(0deg);
115
+ transform-origin: 50% 50%;
116
+ animation: semi-progress-indeterminate-rotate 1.2s linear infinite;
117
+ }
118
+
98
119
  &-text {
99
120
  position: absolute;
100
121
  top: 50%;
@@ -108,4 +129,42 @@ $module: #{$prefix}-progress;
108
129
  }
109
130
  }
110
131
 
111
- @import './rtl.scss';
132
+ @keyframes semi-progress-indeterminate-slide {
133
+ 0% {
134
+ transform: translateX(-120%);
135
+ }
136
+
137
+ 55% {
138
+ transform: translateX(300%);
139
+ }
140
+
141
+ 100% {
142
+ transform: translateX(300%);
143
+ }
144
+ }
145
+
146
+ @keyframes semi-progress-indeterminate-slide-vertical {
147
+ 0% {
148
+ transform: translateY(-120%);
149
+ }
150
+
151
+ 55% {
152
+ transform: translateY(300%);
153
+ }
154
+
155
+ 100% {
156
+ transform: translateY(300%);
157
+ }
158
+ }
159
+
160
+ @keyframes semi-progress-indeterminate-rotate {
161
+ from {
162
+ transform: rotate(0deg);
163
+ }
164
+
165
+ to {
166
+ transform: rotate(360deg);
167
+ }
168
+ }
169
+
170
+ @import './rtl.scss';
@@ -110,6 +110,45 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
110
110
  * When paste event is successfully handled, we ignore the subsequent keydown event.
111
111
  */
112
112
  _pasteHandled: boolean = false;
113
+ /**
114
+ * Working copy of fileList used during a synchronous startUpload batch.
115
+ * Do NOT rely on React state fileList here: setState is async (batched in React 18),
116
+ * so reading getState('fileList') between iterations of a sync loop returns a stale
117
+ * snapshot and earlier removals get overwritten by later ones (see #3335).
118
+ * Seeded at the start of startUpload and cleared when the sync loop finishes (see
119
+ * try/finally in startUpload); async (promise) results fall back to _latestFileList
120
+ * then React state.
121
+ */
122
+ _batchFileList: Array<BaseFileItem> | null = null;
123
+ /**
124
+ * The most recent fileList produced by handleBeforeUploadResultInObject.
125
+ * Unlike React state (which is flushed asynchronously under React 18 automatic
126
+ * batching), this is updated synchronously, so a later async beforeUpload result
127
+ * composes with earlier sync removals of the same batch instead of reading a stale
128
+ * snapshot (mixed sync + async beforeUpload results, see #3335).
129
+ */
130
+ _latestFileList: Array<BaseFileItem> | null = null;
131
+ /**
132
+ * Unify all fileList commits: keep _latestFileList in sync synchronously so async
133
+ * beforeUpload results compose with recent mutations even before React flushes
134
+ * setState (React 18 automatic batching). (#3335)
135
+ */
136
+ updateFileListInternal(newFileList: Array<BaseFileItem>, callback?: () => void): void {
137
+ if (this._batchFileList) {
138
+ this._batchFileList = newFileList;
139
+ }
140
+ this._latestFileList = newFileList;
141
+ this._adapter.updateFileList(newFileList, callback);
142
+ }
143
+
144
+ /** Keep the synchronous snapshot aligned with an externally controlled fileList. */
145
+ syncLatestFileList(fileList: Array<BaseFileItem>): void {
146
+ this._latestFileList = fileList.slice();
147
+ }
148
+
149
+ getLatestFileList(): Array<BaseFileItem> {
150
+ return this._batchFileList || this._latestFileList || this.getState('fileList');
151
+ }
113
152
  constructor(adapter: UploadAdapter<P, S>) {
114
153
  super({ ...adapter });
115
154
  }
@@ -117,6 +156,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
117
156
  init(): void {
118
157
  // make sure state reset, otherwise may cause upload abort in React StrictMode, like https://github.com/DouyinFE/semi-design/pull/843
119
158
  this.destroyState = false;
159
+ this.syncLatestFileList(this.getState('fileList'));
120
160
  // In controlled mode, parent may keep and pass back fileList with blob url.
121
161
  // Sync them into internal map so later remove/clear can revoke correctly.
122
162
  this._syncLocalUrlsFromFileList();
@@ -160,6 +200,8 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
160
200
  if (!disabled) {
161
201
  this.unbindPastingHandler();
162
202
  }
203
+ this._batchFileList = null;
204
+ this._latestFileList = null;
163
205
  this.destroyState = true;
164
206
  }
165
207
 
@@ -363,7 +405,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
363
405
  }
364
406
  newFileList.splice(replaceIdx, 1, newFileItem);
365
407
  this._adapter.notifyChange({ currentFile: newFileItem, fileList: newFileList });
366
- this._adapter.updateFileList(newFileList, () => {
408
+ this.updateFileListInternal(newFileList, () => {
367
409
  this._adapter.resetReplaceInput();
368
410
  if (!newFileItem._sizeInvalid) {
369
411
  this.upload(newFileItem);
@@ -405,7 +447,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
405
447
  const { uploadTrigger } = this.getProps();
406
448
  const currentFiles = files.map(item => this.buildFileItem(item, uploadTrigger));
407
449
  this._adapter.notifyChange({ fileList: currentFiles, currentFile: currentFiles[0] });
408
- this._adapter.updateFileList(currentFiles, () => {
450
+ this.updateFileListInternal(currentFiles, () => {
409
451
  if (uploadTrigger === TRIGGER_AUTO) {
410
452
  this.startUpload(currentFiles);
411
453
  }
@@ -425,7 +467,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
425
467
  this._adapter.notifyChange({ fileList, currentFile: file });
426
468
  }
427
469
  });
428
- this._adapter.updateFileList(fileList, () => {
470
+ this.updateFileListInternal(fileList, () => {
429
471
  if (uploadTrigger === TRIGGER_AUTO) {
430
472
  this.startUpload(currentFiles);
431
473
  }
@@ -506,7 +548,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
506
548
 
507
549
  this._adapter.notifyFileSelect(currentFileList);
508
550
  this._adapter.notifyChange({ fileList: newFileList, currentFile: null });
509
- this._adapter.updateFileList(newFileList, () => {
551
+ this.updateFileListInternal(newFileList, () => {
510
552
  if (uploadTrigger === TRIGGER_AUTO) {
511
553
  this.startUpload(fileItemList);
512
554
  }
@@ -516,16 +558,25 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
516
558
  /* istanbul ignore next */
517
559
  manualUpload(): void {
518
560
  // find the list of files that have not been uploaded
519
- const waitToUploadFileList = this.getState('fileList').filter((item: BaseFileItem) => item.status === FILE_STATUS_WAIT_UPLOAD);
561
+ const waitToUploadFileList = this.getLatestFileList().filter((item: BaseFileItem) => item.status === FILE_STATUS_WAIT_UPLOAD);
520
562
  this.startUpload(waitToUploadFileList);
521
563
  }
522
564
 
523
565
  startUpload(fileList: Array<BaseFileItem>): void {
524
- fileList.forEach(file => {
525
- if (!file._sizeInvalid) {
526
- this.upload(file);
527
- }
528
- });
566
+ // Seed a working copy so multiple synchronous beforeUpload results
567
+ // (e.g. several files returning autoRemove) compose correctly instead of each
568
+ // reading a stale React state snapshot and overwriting earlier removals. (#3335)
569
+ // try/finally guarantees the working copy is cleared even if beforeUpload throws.
570
+ this._batchFileList = this.getLatestFileList().slice();
571
+ try {
572
+ fileList.forEach(file => {
573
+ if (!file._sizeInvalid) {
574
+ this.upload(file);
575
+ }
576
+ });
577
+ } finally {
578
+ this._batchFileList = null;
579
+ }
529
580
  }
530
581
 
531
582
  upload(file: BaseFileItem): void {
@@ -535,7 +586,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
535
586
  return;
536
587
  }
537
588
  if (typeof beforeUpload === 'function') {
538
- const { fileList } = this.getStates();
589
+ const fileList = this.getLatestFileList();
539
590
  const buResult = this._adapter.notifyBeforeUpload({ file, fileList });
540
591
  switch (true) {
541
592
  // sync validate - boolean
@@ -584,7 +635,11 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
584
635
  // handle beforeUpload result when it's an object
585
636
  handleBeforeUploadResultInObject(buResult: Partial<BeforeUploadObjectResult>, file: BaseFileItem): void {
586
637
  const { shouldUpload, status, autoRemove, validateMessage, fileInstance } = buResult;
587
- let newFileList: Array<BaseFileItem> = this.getState('fileList').slice();
638
+ // During a synchronous startUpload batch, read from the working copy; for async
639
+ // (promise) results read from the most recent synchronous snapshot, falling back
640
+ // to React state. This keeps mixed sync + async beforeUpload results composing
641
+ // correctly under React 18 automatic batching. (#3335)
642
+ let newFileList: Array<BaseFileItem> = this.getLatestFileList().slice();
588
643
  if (autoRemove) {
589
644
  this._releaseFileUrl(file.uid);
590
645
  newFileList = newFileList.filter(item => item.uid !== file.uid);
@@ -607,7 +662,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
607
662
  newFileList[index].shouldUpload = shouldUpload;
608
663
  }
609
664
 
610
- this._adapter.updateFileList(newFileList);
665
+ this.updateFileListInternal(newFileList);
611
666
  this._adapter.notifyChange({ fileList: newFileList, currentFile: file });
612
667
 
613
668
  if (shouldUpload) {
@@ -703,8 +758,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
703
758
  }
704
759
 
705
760
  handleProgress({ e, fileInstance }: { e?: ProgressEvent; fileInstance: File }): void {
706
- const { fileList } = this.getStates();
707
- const newFileList = fileList.slice();
761
+ const newFileList = this.getLatestFileList().slice();
708
762
  let percent = 0;
709
763
  if (e.total > 0) {
710
764
  percent = Number(((e.loaded / e.total) * 100 * numbers.PROGRESS_COEFFICIENT).toFixed(0)) || 0;
@@ -714,15 +768,15 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
714
768
  return;
715
769
  }
716
770
  newFileList[index].percent = percent;
717
- newFileList[index].status = FILE_STATUS_UPLOADING;
771
+ newFileList[index].status = FILE_STATUS_UPLOADING as FileItemStatus;
718
772
 
719
773
  this._adapter.notifyProgress(percent, fileInstance, newFileList);
720
- this._adapter.updateFileList(newFileList);
774
+ this.updateFileListInternal(newFileList);
721
775
  this._adapter.notifyChange({ fileList: newFileList, currentFile: newFileList[index] });
722
776
  }
723
777
 
724
778
  handleOnLoad({ e, xhr, fileInstance }: { e?: ProgressEvent; xhr: XMLHttpRequest; fileInstance: File }): void {
725
- const { fileList } = this.getStates();
779
+ const fileList = this.getLatestFileList();
726
780
  const index = this._getFileIndex(fileInstance, fileList);
727
781
  if (index < 0) {
728
782
  return;
@@ -735,7 +789,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
735
789
  }
736
790
 
737
791
  handleSuccess({ e, fileInstance, isCustomRequest = false, xhr, response }: { e?: ProgressEvent; fileInstance: CustomFile; isCustomRequest?: boolean; xhr?: XMLHttpRequest; response?: any }): void {
738
- const { fileList } = this.getStates();
792
+ const fileList = this.getLatestFileList();
739
793
  let body: any = null;
740
794
  const index = this._getFileIndex(fileInstance, fileList);
741
795
  if (index < 0) {
@@ -750,7 +804,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
750
804
  const newFileList = fileList.slice();
751
805
  const { afterUpload } = this.getProps();
752
806
 
753
- newFileList[index].status = FILE_STATUS_SUCCESS;
807
+ newFileList[index].status = FILE_STATUS_SUCCESS as FileItemStatus;
754
808
  newFileList[index].percent = 100;
755
809
  this._adapter.notifyProgress(100, fileInstance, newFileList);
756
810
  newFileList[index].response = body;
@@ -763,7 +817,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
763
817
  file: newFileList[index],
764
818
  fileList: newFileList,
765
819
  }) || {};
766
- status ? (newFileList[index].status = status) : null;
820
+ status ? (newFileList[index].status = status as FileItemStatus) : null;
767
821
  validateMessage ? (newFileList[index].validateMessage = validateMessage) : null;
768
822
  name ? (newFileList[index].name = name) : null;
769
823
  if (url) {
@@ -778,7 +832,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
778
832
  }
779
833
  this._adapter.notifySuccess(body, fileInstance, newFileList);
780
834
  this._adapter.notifyChange({ fileList: newFileList, currentFile: newFileList[index] });
781
- this._adapter.updateFileList(newFileList);
835
+ this.updateFileListInternal(newFileList);
782
836
  }
783
837
 
784
838
  _getFileIndex(file: CustomFile | BaseFileItem, fileList: Array<BaseFileItem>): number {
@@ -808,13 +862,13 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
808
862
  this._releaseFileUrl(file.uid);
809
863
 
810
864
  this._adapter.notifyRemove(file.fileInstance, newFileList, file);
811
- this._adapter.updateFileList(newFileList);
865
+ this.updateFileListInternal(newFileList);
812
866
  this._adapter.notifyChange({ fileList: newFileList, currentFile: file });
813
867
  });
814
868
  }
815
869
 
816
870
  handleError({ e, xhr, fileInstance }: { e?: ProgressEvent;xhr: XMLHttpRequest;fileInstance: CustomFile }): void {
817
- const { fileList } = this.getStates();
871
+ const fileList = this.getLatestFileList();
818
872
  const index = this._getFileIndex(fileInstance, fileList);
819
873
  if (index < 0) {
820
874
  return;
@@ -823,12 +877,12 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
823
877
  const newFileList = fileList.slice();
824
878
  const error = this.getError({ action, xhr, fileName: fileInstance.name });
825
879
 
826
- newFileList[index].status = FILE_STATUS_UPLOAD_FAIL;
880
+ newFileList[index].status = FILE_STATUS_UPLOAD_FAIL as FileItemStatus;
827
881
  newFileList[index].response = error;
828
882
  newFileList[index].event = e;
829
883
 
830
884
  this._adapter.notifyError(error, fileInstance, newFileList, xhr);
831
- this._adapter.updateFileList(newFileList);
885
+ this.updateFileListInternal(newFileList);
832
886
  this._adapter.notifyChange({ currentFile: newFileList[index], fileList: newFileList });
833
887
  }
834
888
 
@@ -844,7 +898,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
844
898
  return;
845
899
  }
846
900
  this._releaseAllFileUrls();
847
- this._adapter.updateFileList([]);
901
+ this.updateFileListInternal([]);
848
902
  this._adapter.notifyClear();
849
903
  this._adapter.notifyChange({ fileList: [] } as any);
850
904
  }).catch(error => {