@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.
@@ -7,6 +7,14 @@ exports.default = void 0;
7
7
  var _foundation = _interopRequireDefault(require("../base/foundation"));
8
8
  var _keyCode = _interopRequireDefault(require("../utils/keyCode"));
9
9
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
+ /**
11
+ * Stack of `handleKeyDown` handlers for modals that currently listen on
12
+ * `document`. Every ModalContent registers its own document-level keydown
13
+ * listener, so `stopPropagation()` cannot stop sibling listeners on the same
14
+ * node — one ESC used to close every open dialog. Only the most recently
15
+ * mounted (top-most) listener should respond.
16
+ */
17
+ const escListenerStack = [];
10
18
  class ModalContentFoundation extends _foundation.default {
11
19
  constructor(adapter) {
12
20
  super(Object.assign(Object.assign({}, ModalContentFoundation.defaultAdapter), adapter));
@@ -15,6 +23,12 @@ class ModalContentFoundation extends _foundation.default {
15
23
  closeOnEsc
16
24
  } = this.getProps();
17
25
  if (closeOnEsc && e.keyCode === _keyCode.default.ESC) {
26
+ // Multiple open modals each registered a document-level keydown
27
+ // listener; only the top-most (most recently mounted) one may
28
+ // close, so a single ESC closes dialogs one at a time.
29
+ if (escListenerStack[escListenerStack.length - 1] !== this.handleKeyDown) {
30
+ return;
31
+ }
18
32
  e.stopPropagation();
19
33
  this.close(e);
20
34
  return;
@@ -34,9 +48,16 @@ class ModalContentFoundation extends _foundation.default {
34
48
  }
35
49
  handleKeyDownEventListenerMount() {
36
50
  this._adapter.addKeyDownEventListener();
51
+ if (this.getProps().closeOnEsc) {
52
+ escListenerStack.push(this.handleKeyDown);
53
+ }
37
54
  }
38
55
  handleKeyDownEventListenerUnmount() {
39
56
  this._adapter.removeKeyDownEventListener();
57
+ const index = escListenerStack.indexOf(this.handleKeyDown);
58
+ if (index !== -1) {
59
+ escListenerStack.splice(index, 1);
60
+ }
40
61
  }
41
62
  getMouseState() {
42
63
  this._adapter.getMouseState();
@@ -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;
@@ -31,6 +31,21 @@ exports.ResizeItemFoundation = ResizeItemFoundation;
31
31
  class ResizeGroupFoundation extends _foundation.default {
32
32
  constructor(adapter) {
33
33
  super(Object.assign({}, adapter));
34
+ // 首次 initSpace 时 group 是否可测量(非 display:none)。
35
+ // Whether the group was measurable (not inside display:none) on the first initSpace run. #3336
36
+ this.sizeInitialized = false;
37
+ /**
38
+ * When the group is initially mounted inside a display:none container, its own size
39
+ * and the handler sizes are all 0, so item sizes are computed with a `- 0px` handler
40
+ * offset and never corrected once the container becomes visible. Recalculate the layout
41
+ * the first time the group becomes measurable. Once it has a valid size, later resizes
42
+ * must NOT re-run initSpace, otherwise the user's manual drag results would be reset. #3336
43
+ */
44
+ this.handleGroupResize = () => {
45
+ if (!this.sizeInitialized && this.groupSize > 0) {
46
+ this.initSpace();
47
+ }
48
+ };
34
49
  this.registerEvents = () => {
35
50
  this._adapter.registerEvents(this.type);
36
51
  };
@@ -194,6 +209,9 @@ class ResizeGroupFoundation extends _foundation.default {
194
209
  // calculate accurate space for group item
195
210
  let handlerSizes = new Array(this._adapter.getHandlerCount()).fill(0);
196
211
  let parentSize = this.groupSize;
212
+ // Mark whether the group is measurable now; if not (e.g. inside display:none),
213
+ // handleGroupResize will re-run initSpace once it becomes visible. #3336
214
+ this.sizeInitialized = parentSize > 0;
197
215
  this.totalMinus = 0;
198
216
  for (let i = 0; i < this._adapter.getHandlerCount(); i++) {
199
217
  let handlerSize = direction === 'horizontal' ? this._adapter.getHandler(i).offsetWidth : this._adapter.getHandler(i).offsetHeight;
@@ -125,7 +125,8 @@ const isTimeFormatLike = (time, formatToken) => {
125
125
  const hmsReg = /[H|m|s]{1,2}/;
126
126
  const formatSplitted = formatToken.split(formatNotSupportChReg); // => ['HH', 'mm'];
127
127
  const timeSeparator = formatToken.replace(formatSupportChReg, ''); // => :
128
- const timeReg = new RegExp(`[${timeSeparator}]`, 'g'); // => /[:]/g
128
+ const escapedTimeSeparator = timeSeparator.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); // escape regExp special characters to avoid invalid regExp, e.g. format="HH\\mm"
129
+ const timeReg = new RegExp(`[${escapedTimeSeparator}]`, 'g'); // => /[:]/g
129
130
  const timeSplitted = time.split(timeReg); // => ['12', '0]
130
131
  if (formatSplitted.length !== timeSplitted.length) {
131
132
  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
  /**
@@ -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: []
@@ -1,5 +1,13 @@
1
1
  import BaseFoundation from '../base/foundation';
2
2
  import KeyCode from '../utils/keyCode';
3
+ /**
4
+ * Stack of `handleKeyDown` handlers for modals that currently listen on
5
+ * `document`. Every ModalContent registers its own document-level keydown
6
+ * listener, so `stopPropagation()` cannot stop sibling listeners on the same
7
+ * node — one ESC used to close every open dialog. Only the most recently
8
+ * mounted (top-most) listener should respond.
9
+ */
10
+ const escListenerStack = [];
3
11
  export default class ModalContentFoundation extends BaseFoundation {
4
12
  constructor(adapter) {
5
13
  super(Object.assign(Object.assign({}, ModalContentFoundation.defaultAdapter), adapter));
@@ -8,6 +16,12 @@ export default class ModalContentFoundation extends BaseFoundation {
8
16
  closeOnEsc
9
17
  } = this.getProps();
10
18
  if (closeOnEsc && e.keyCode === KeyCode.ESC) {
19
+ // Multiple open modals each registered a document-level keydown
20
+ // listener; only the top-most (most recently mounted) one may
21
+ // close, so a single ESC closes dialogs one at a time.
22
+ if (escListenerStack[escListenerStack.length - 1] !== this.handleKeyDown) {
23
+ return;
24
+ }
11
25
  e.stopPropagation();
12
26
  this.close(e);
13
27
  return;
@@ -27,9 +41,16 @@ export default class ModalContentFoundation extends BaseFoundation {
27
41
  }
28
42
  handleKeyDownEventListenerMount() {
29
43
  this._adapter.addKeyDownEventListener();
44
+ if (this.getProps().closeOnEsc) {
45
+ escListenerStack.push(this.handleKeyDown);
46
+ }
30
47
  }
31
48
  handleKeyDownEventListenerUnmount() {
32
49
  this._adapter.removeKeyDownEventListener();
50
+ const index = escListenerStack.indexOf(this.handleKeyDown);
51
+ if (index !== -1) {
52
+ escListenerStack.splice(index, 1);
53
+ }
33
54
  }
34
55
  getMouseState() {
35
56
  this._adapter.getMouseState();