@douyinfe/semi-foundation 2.102.0 → 2.104.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';
@@ -174,6 +174,11 @@
174
174
  height: 100%;
175
175
  font-size: 14px;
176
176
  }
177
+ .semi-radio-inner-display .semi-icon svg {
178
+ display: block;
179
+ width: 100%;
180
+ height: 100%;
181
+ }
177
182
  .semi-radio-content {
178
183
  display: flex;
179
184
  flex-direction: column;
@@ -236,6 +236,15 @@ $inner-width: $width-icon-medium;
236
236
  width: 100%;
237
237
  height: 100%;
238
238
  font-size: 14px;
239
+
240
+ // #3350: 显式设置 svg 尺寸并 block 化,替代 svg 自带的 width/height="1em"。
241
+ // Safari 对以 em 指定尺寸的内联 SVG 存在渲染偏差,会导致 checked 状态
242
+ // 的白点视觉偏移;100% 让 svg 与图标容器严格同尺寸,无偏移空间。
243
+ svg {
244
+ display: block;
245
+ width: 100%;
246
+ height: 100%;
247
+ }
239
248
  }
240
249
  }
241
250
  }
@@ -41,7 +41,16 @@ export declare class ResizeGroupFoundation<P = Record<string, any>, S = Record<s
41
41
  totalMinus: number;
42
42
  itemPercentMap: Map<number, number>;
43
43
  type?: ResizeEventType;
44
+ sizeInitialized: boolean;
44
45
  init(): void;
46
+ /**
47
+ * When the group is initially mounted inside a display:none container, its own size
48
+ * and the handler sizes are all 0, so item sizes are computed with a `- 0px` handler
49
+ * offset and never corrected once the container becomes visible. Recalculate the layout
50
+ * the first time the group becomes measurable. Once it has a valid size, later resizes
51
+ * must NOT re-run initSpace, otherwise the user's manual drag results would be reset. #3336
52
+ */
53
+ handleGroupResize: () => void;
45
54
  get window(): Window | null;
46
55
  registerEvents: () => void;
47
56
  unregisterEvents: () => void;
@@ -23,6 +23,21 @@ export class ResizeItemFoundation extends BaseFoundation {
23
23
  export class ResizeGroupFoundation extends BaseFoundation {
24
24
  constructor(adapter) {
25
25
  super(Object.assign({}, adapter));
26
+ // 首次 initSpace 时 group 是否可测量(非 display:none)。
27
+ // Whether the group was measurable (not inside display:none) on the first initSpace run. #3336
28
+ this.sizeInitialized = false;
29
+ /**
30
+ * When the group is initially mounted inside a display:none container, its own size
31
+ * and the handler sizes are all 0, so item sizes are computed with a `- 0px` handler
32
+ * offset and never corrected once the container becomes visible. Recalculate the layout
33
+ * the first time the group becomes measurable. Once it has a valid size, later resizes
34
+ * must NOT re-run initSpace, otherwise the user's manual drag results would be reset. #3336
35
+ */
36
+ this.handleGroupResize = () => {
37
+ if (!this.sizeInitialized && this.groupSize > 0) {
38
+ this.initSpace();
39
+ }
40
+ };
26
41
  this.registerEvents = () => {
27
42
  this._adapter.registerEvents(this.type);
28
43
  };
@@ -186,6 +201,9 @@ export class ResizeGroupFoundation extends BaseFoundation {
186
201
  // calculate accurate space for group item
187
202
  let handlerSizes = new Array(this._adapter.getHandlerCount()).fill(0);
188
203
  let parentSize = this.groupSize;
204
+ // Mark whether the group is measurable now; if not (e.g. inside display:none),
205
+ // handleGroupResize will re-run initSpace once it becomes visible. #3336
206
+ this.sizeInitialized = parentSize > 0;
189
207
  this.totalMinus = 0;
190
208
  for (let i = 0; i < this._adapter.getHandlerCount(); i++) {
191
209
  let handlerSize = direction === 'horizontal' ? this._adapter.getHandler(i).offsetWidth : this._adapter.getHandler(i).offsetHeight;
@@ -111,7 +111,8 @@ export const isTimeFormatLike = (time, formatToken) => {
111
111
  const hmsReg = /[H|m|s]{1,2}/;
112
112
  const formatSplitted = formatToken.split(formatNotSupportChReg); // => ['HH', 'mm'];
113
113
  const timeSeparator = formatToken.replace(formatSupportChReg, ''); // => :
114
- const timeReg = new RegExp(`[${timeSeparator}]`, 'g'); // => /[:]/g
114
+ const escapedTimeSeparator = timeSeparator.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); // escape regExp special characters to avoid invalid regExp, e.g. format="HH\\mm"
115
+ const timeReg = new RegExp(`[${escapedTimeSeparator}]`, 'g'); // => /[:]/g
115
116
  const timeSplitted = time.split(timeReg); // => ['12', '0]
116
117
  if (formatSplitted.length !== timeSplitted.length) {
117
118
  isLike = false;
@@ -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: []
@@ -30,6 +30,15 @@ export interface ModalContentAdapter extends DefaultAdapter<ModalContentProps, M
30
30
  prevFocusElementReFocus: () => void
31
31
  }
32
32
 
33
+ /**
34
+ * Stack of `handleKeyDown` handlers for modals that currently listen on
35
+ * `document`. Every ModalContent registers its own document-level keydown
36
+ * listener, so `stopPropagation()` cannot stop sibling listeners on the same
37
+ * node — one ESC used to close every open dialog. Only the most recently
38
+ * mounted (top-most) listener should respond.
39
+ */
40
+ const escListenerStack: Array<(e: any) => void> = [];
41
+
33
42
  export default class ModalContentFoundation extends BaseFoundation<ModalContentAdapter> {
34
43
 
35
44
  constructor(adapter: ModalContentAdapter) {
@@ -53,6 +62,12 @@ export default class ModalContentFoundation extends BaseFoundation<ModalContentA
53
62
  handleKeyDown = (e: any) => {
54
63
  const { closeOnEsc } = this.getProps();
55
64
  if (closeOnEsc && e.keyCode === KeyCode.ESC) {
65
+ // Multiple open modals each registered a document-level keydown
66
+ // listener; only the top-most (most recently mounted) one may
67
+ // close, so a single ESC closes dialogs one at a time.
68
+ if (escListenerStack[escListenerStack.length - 1] !== this.handleKeyDown) {
69
+ return;
70
+ }
56
71
  e.stopPropagation();
57
72
  this.close(e);
58
73
  return;
@@ -61,10 +76,17 @@ export default class ModalContentFoundation extends BaseFoundation<ModalContentA
61
76
 
62
77
  handleKeyDownEventListenerMount() {
63
78
  this._adapter.addKeyDownEventListener();
79
+ if (this.getProps().closeOnEsc) {
80
+ escListenerStack.push(this.handleKeyDown);
81
+ }
64
82
  }
65
83
 
66
84
  handleKeyDownEventListenerUnmount() {
67
85
  this._adapter.removeKeyDownEventListener();
86
+ const index = escListenerStack.indexOf(this.handleKeyDown);
87
+ if (index !== -1) {
88
+ escListenerStack.splice(index, 1);
89
+ }
68
90
  }
69
91
 
70
92
  getMouseState() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@douyinfe/semi-foundation",
3
- "version": "2.102.0",
3
+ "version": "2.104.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.104.0",
14229
+ "@douyinfe/semi-json-viewer-core": "2.104.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": "c69b9e013ab7b45cc617ed32f1ed548e96058bcd",
14251
14251
  "devDependencies": {
14252
14252
  "@babel/plugin-transform-runtime": "^7.15.8",
14253
14253
  "@babel/preset-env": "^7.15.8",