@douyinfe/semi-foundation 2.103.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();
@@ -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;
@@ -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();
@@ -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;
@@ -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.103.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.103.0",
14229
- "@douyinfe/semi-json-viewer-core": "2.103.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": "c1e3632b90b242defdfe4bfb267bcfaffbb23295",
14250
+ "gitHead": "c69b9e013ab7b45cc617ed32f1ed548e96058bcd",
14251
14251
  "devDependencies": {
14252
14252
  "@babel/plugin-transform-runtime": "^7.15.8",
14253
14253
  "@babel/preset-env": "^7.15.8",
package/radio/radio.scss CHANGED
@@ -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
  }
@@ -73,6 +73,9 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
73
73
  totalMinus: number;
74
74
  itemPercentMap: Map<number, number>; // 内部维护一个百分比数组,消除浮点计算误差
75
75
  type?: ResizeEventType;
76
+ // 首次 initSpace 时 group 是否可测量(非 display:none)。
77
+ // Whether the group was measurable (not inside display:none) on the first initSpace run. #3336
78
+ sizeInitialized: boolean = false;
76
79
 
77
80
 
78
81
  init(): void {
@@ -81,6 +84,19 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
81
84
  this.itemPercentMap = new Map();
82
85
  this.initSpace();
83
86
  }
87
+
88
+ /**
89
+ * When the group is initially mounted inside a display:none container, its own size
90
+ * and the handler sizes are all 0, so item sizes are computed with a `- 0px` handler
91
+ * offset and never corrected once the container becomes visible. Recalculate the layout
92
+ * the first time the group becomes measurable. Once it has a valid size, later resizes
93
+ * must NOT re-run initSpace, otherwise the user's manual drag results would be reset. #3336
94
+ */
95
+ handleGroupResize = () => {
96
+ if (!this.sizeInitialized && this.groupSize > 0) {
97
+ this.initSpace();
98
+ }
99
+ }
84
100
  get window(): Window | null {
85
101
  return this.groupRef.ownerDocument.defaultView as Window ?? null;
86
102
  }
@@ -231,6 +247,9 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
231
247
  // calculate accurate space for group item
232
248
  let handlerSizes = new Array(this._adapter.getHandlerCount()).fill(0);
233
249
  let parentSize = this.groupSize;
250
+ // Mark whether the group is measurable now; if not (e.g. inside display:none),
251
+ // handleGroupResize will re-run initSpace once it becomes visible. #3336
252
+ this.sizeInitialized = parentSize > 0;
234
253
  this.totalMinus = 0;
235
254
  for (let i = 0; i < this._adapter.getHandlerCount(); i++) {
236
255
  let handlerSize = direction === 'horizontal' ? this._adapter.getHandler(i).offsetWidth : this._adapter.getHandler(i).offsetHeight;
@@ -131,7 +131,8 @@ export const isTimeFormatLike = (time: string, formatToken: string) => {
131
131
  const hmsReg = /[H|m|s]{1,2}/;
132
132
  const formatSplitted = formatToken.split(formatNotSupportChReg); // => ['HH', 'mm'];
133
133
  const timeSeparator = formatToken.replace(formatSupportChReg, ''); // => :
134
- const timeReg = new RegExp(`[${timeSeparator}]`, 'g'); // => /[:]/g
134
+ const escapedTimeSeparator = timeSeparator.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); // escape regExp special characters to avoid invalid regExp, e.g. format="HH\\mm"
135
+ const timeReg = new RegExp(`[${escapedTimeSeparator}]`, 'g'); // => /[:]/g
135
136
  const timeSplitted = time.split(timeReg); // => ['12', '0]
136
137
 
137
138
  if (formatSplitted.length !== timeSplitted.length) {