@douyinfe/semi-foundation 2.10.1 → 2.10.5

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.
@@ -145,6 +145,7 @@ export interface BasicCascaderProps {
145
145
  disableStrictly?: boolean;
146
146
  leafOnly?: boolean;
147
147
  enableLeafClick?: boolean;
148
+ preventScroll?: boolean;
148
149
  onClear?: () => void;
149
150
  triggerRender?: (props: BasicTriggerRenderProps) => any;
150
151
  onListScroll?: (e: any, panel: BasicScrollPanelProps) => void;
@@ -130,6 +130,7 @@ export interface BaseCheckboxProps {
130
130
  extra?: any;
131
131
  addonId?: string;
132
132
  extraId?: string;
133
+ preventScroll?: boolean;
133
134
  }
134
135
 
135
136
  export default CheckboxFoundation;
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  /* eslint-disable max-len, max-depth, */
3
3
  import { format, isValid, isSameSecond, isEqual as isDateEqual, isDate } from 'date-fns';
4
- import { get, isObject, isString, isEqual } from 'lodash';
4
+ import { get, isObject, isString, isEqual, isFunction } from 'lodash';
5
5
 
6
6
  import BaseFoundation, { DefaultAdapter } from '../base/foundation';
7
7
  import { isValidDate, isTimestamp } from './_utils/index';
@@ -162,6 +162,7 @@ export interface DatePickerFoundationProps extends ElementProps, RenderProps, Ev
162
162
  localeCode?: string;
163
163
  rangeSeparator?: string;
164
164
  insetInput?: boolean;
165
+ preventScroll?: boolean;
165
166
  }
166
167
 
167
168
  export interface DatePickerFoundationState {
@@ -244,12 +245,25 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
244
245
  this._adapter.updatePrevTimezone(prevTimeZone);
245
246
  this._adapter.updateInputValue(null);
246
247
  this._adapter.updateValue(result);
248
+ this.initRangeInputFocus(result);
247
249
 
248
250
  if (this._adapter.needConfirm()) {
249
251
  this._adapter.updateCachedSelectedValue(result);
250
252
  }
251
253
  }
252
254
 
255
+ /**
256
+ * 如果用户传了一个空的 value,需要把 range input focus 设置为 rangeStart,这样用户可以清除完之后继续从开始选择
257
+ *
258
+ * If the user passes an empty value, you need to set the range input focus to rangeStart, so that the user can continue to select from the beginning after clearing
259
+ */
260
+ initRangeInputFocus(result: Date[]) {
261
+ const { triggerRender } = this.getProps();
262
+ if (this._isRangeType() && isFunction(triggerRender) && result.length === 0) {
263
+ this._adapter.setRangeInputFocus('rangeStart');
264
+ }
265
+ }
266
+
253
267
  parseWithTimezone(value: ValueType, timeZone: string | number, prevTimeZone: string | number) {
254
268
  const result: Date[] = [];
255
269
  if (Array.isArray(value) && value.length) {
@@ -1161,7 +1175,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
1161
1175
  * @returns
1162
1176
  */
1163
1177
  handleTriggerWrapperClick(e: any) {
1164
- const { disabled } = this._adapter.getProps();
1178
+ const { disabled, triggerRender } = this._adapter.getProps();
1165
1179
  const { rangeInputFocus } = this._adapter.getStates();
1166
1180
  if (disabled) {
1167
1181
  return;
@@ -1173,12 +1187,18 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
1173
1187
  * - When type is not range type, Input component will automatically focus in the same case
1174
1188
  * - isEventTarget is used to judge whether the event is a bubbling event
1175
1189
  */
1176
- if (this._isRangeType() && !rangeInputFocus && this._adapter.isEventTarget(e)) {
1177
- setTimeout(() => {
1178
- // using setTimeout get correct state value 'rangeInputFocus'
1179
- this.handleInputFocus(e, 'rangeStart');
1180
- this.openPanel();
1181
- }, 0);
1190
+ if (this._isRangeType() && !rangeInputFocus) {
1191
+ if (this._adapter.isEventTarget(e)) {
1192
+ setTimeout(() => {
1193
+ // using setTimeout get correct state value 'rangeInputFocus'
1194
+ this.handleInputFocus(e, 'rangeStart');
1195
+ }, 0);
1196
+ } else if (isFunction(triggerRender)) {
1197
+ // 如果是 triggerRender 场景,因为没有 input,因此打开面板时默认 focus 在 rangeStart
1198
+ // If it is a triggerRender scene, because there is no input, the default focus is rangeStart when the panel is opened
1199
+ this._adapter.setRangeInputFocus('rangeStart');
1200
+ }
1201
+ this.openPanel();
1182
1202
  } else {
1183
1203
  this.openPanel();
1184
1204
  }
@@ -720,9 +720,8 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
720
720
  /**
721
721
  * no need to check focus then
722
722
  * - dateRange and isDateRangeAndHasOffset
723
- * - dateRange and triggerRender
724
723
  */
725
- const needCheckFocusRecord = !(type === 'dateRange' && (isDateRangeAndHasOffset || isFunction(triggerRender)));
724
+ const needCheckFocusRecord = !(type === 'dateRange' && isDateRangeAndHasOffset);
726
725
  this._adapter.notifySelectedChange(date, { needCheckFocusRecord });
727
726
  }
728
727
  }
@@ -99,6 +99,7 @@ export interface BasicCascaderProps {
99
99
  disableStrictly?: boolean;
100
100
  leafOnly?: boolean;
101
101
  enableLeafClick?: boolean;
102
+ preventScroll?: boolean;
102
103
  onClear?: () => void;
103
104
  triggerRender?: (props: BasicTriggerRenderProps) => any;
104
105
  onListScroll?: (e: any, panel: BasicScrollPanelProps) => void;
@@ -205,6 +206,14 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
205
206
  handleSingleSelect(e: any, item: BasicEntity | BasicData): void;
206
207
  _handleMultipleSelect(item: BasicEntity | BasicData): void;
207
208
  calcNonDisabledCheckedKeys(eventKey: string, targetStatus: boolean): {
209
+ checkedKeys: Set<string>; /**
210
+ * 典型的场景是: 假设我们选中了 0-0 这个节点,此时 selectedKeys=Set('0-0'),
211
+ * 输入框会显示 0-0 的 label。当 treeData 发生更新,假设此时 0-0 在 treeData
212
+ * 中不存在,则 selectedKeys=Set('not-exist-0-0'),此时输入框显示的是 0-0,
213
+ * 也就是显示 not-exist- 后的内容。当treeData再次更新,假设此时 0-0 在 treeData
214
+ * 中存在,则 selectedKeys=Set('0-0'),此时输入框显示 0-0 的 label。 这个地
215
+ * 方做的操作就是,为了例子中第二次更新后 0-0 label 能够正常显示。
216
+ */
208
217
  /**
209
218
  * The typical scenario is: suppose we select the 0-0 node, at this time
210
219
  * selectedKeys=Set('0-0'), the input box will display a 0-0 label. When
@@ -216,7 +225,6 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
216
225
  * 0-0 at this time. The operation done here is for the 0-0 label to be
217
226
  * displayed normally after the second update in the example.
218
227
  */
219
- checkedKeys: Set<string>;
220
228
  halfCheckedKeys: Set<any>;
221
229
  };
222
230
  calcCheckedStatus(targetStatus: boolean, eventKey: string): boolean;
@@ -55,5 +55,6 @@ export interface BaseCheckboxProps {
55
55
  extra?: any;
56
56
  addonId?: string;
57
57
  extraId?: string;
58
+ preventScroll?: boolean;
58
59
  }
59
60
  export default CheckboxFoundation;
@@ -132,6 +132,7 @@ export interface DatePickerFoundationProps extends ElementProps, RenderProps, Ev
132
132
  localeCode?: string;
133
133
  rangeSeparator?: string;
134
134
  insetInput?: boolean;
135
+ preventScroll?: boolean;
135
136
  }
136
137
  export interface DatePickerFoundationState {
137
138
  panelShow: boolean;
@@ -185,6 +186,12 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
185
186
  initFromProps({ value, timeZone, prevTimeZone }: Pick<DatePickerFoundationProps, 'value' | 'timeZone'> & {
186
187
  prevTimeZone?: string | number;
187
188
  }): void;
189
+ /**
190
+ * 如果用户传了一个空的 value,需要把 range input focus 设置为 rangeStart,这样用户可以清除完之后继续从开始选择
191
+ *
192
+ * If the user passes an empty value, you need to set the range input focus to rangeStart, so that the user can continue to select from the beginning after clearing
193
+ */
194
+ initRangeInputFocus(result: Date[]): void;
188
195
  parseWithTimezone(value: ValueType, timeZone: string | number, prevTimeZone: string | number): Date[];
189
196
  _isMultiple(): boolean;
190
197
  /**
@@ -38,6 +38,8 @@ var _set = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable
38
38
 
39
39
  var _setTimeout2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set-timeout"));
40
40
 
41
+ var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));
42
+
41
43
  var _isEqual2 = _interopRequireDefault(require("lodash/isEqual"));
42
44
 
43
45
  var _isString2 = _interopRequireDefault(require("lodash/isString"));
@@ -173,10 +175,28 @@ class DatePickerFoundation extends _foundation.default {
173
175
 
174
176
  this._adapter.updateValue(result);
175
177
 
178
+ this.initRangeInputFocus(result);
179
+
176
180
  if (this._adapter.needConfirm()) {
177
181
  this._adapter.updateCachedSelectedValue(result);
178
182
  }
179
183
  }
184
+ /**
185
+ * 如果用户传了一个空的 value,需要把 range input focus 设置为 rangeStart,这样用户可以清除完之后继续从开始选择
186
+ *
187
+ * If the user passes an empty value, you need to set the range input focus to rangeStart, so that the user can continue to select from the beginning after clearing
188
+ */
189
+
190
+
191
+ initRangeInputFocus(result) {
192
+ const {
193
+ triggerRender
194
+ } = this.getProps();
195
+
196
+ if (this._isRangeType() && (0, _isFunction2.default)(triggerRender) && result.length === 0) {
197
+ this._adapter.setRangeInputFocus('rangeStart');
198
+ }
199
+ }
180
200
 
181
201
  parseWithTimezone(value, timeZone, prevTimeZone) {
182
202
  const result = [];
@@ -1270,7 +1290,8 @@ class DatePickerFoundation extends _foundation.default {
1270
1290
 
1271
1291
  handleTriggerWrapperClick(e) {
1272
1292
  const {
1273
- disabled
1293
+ disabled,
1294
+ triggerRender
1274
1295
  } = this._adapter.getProps();
1275
1296
 
1276
1297
  const {
@@ -1289,12 +1310,19 @@ class DatePickerFoundation extends _foundation.default {
1289
1310
  */
1290
1311
 
1291
1312
 
1292
- if (this._isRangeType() && !rangeInputFocus && this._adapter.isEventTarget(e)) {
1293
- (0, _setTimeout2.default)(() => {
1294
- // using setTimeout get correct state value 'rangeInputFocus'
1295
- this.handleInputFocus(e, 'rangeStart');
1296
- this.openPanel();
1297
- }, 0);
1313
+ if (this._isRangeType() && !rangeInputFocus) {
1314
+ if (this._adapter.isEventTarget(e)) {
1315
+ (0, _setTimeout2.default)(() => {
1316
+ // using setTimeout get correct state value 'rangeInputFocus'
1317
+ this.handleInputFocus(e, 'rangeStart');
1318
+ }, 0);
1319
+ } else if ((0, _isFunction2.default)(triggerRender)) {
1320
+ // 如果是 triggerRender 场景,因为没有 input,因此打开面板时默认 focus 在 rangeStart
1321
+ // If it is a triggerRender scene, because there is no input, the default focus is rangeStart when the panel is opened
1322
+ this._adapter.setRangeInputFocus('rangeStart');
1323
+ }
1324
+
1325
+ this.openPanel();
1298
1326
  } else {
1299
1327
  this.openPanel();
1300
1328
  }
@@ -28,8 +28,6 @@ var _concat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-sta
28
28
 
29
29
  var _trim = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/trim"));
30
30
 
31
- var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));
32
-
33
31
  var _isEqual2 = _interopRequireDefault(require("lodash/isEqual"));
34
32
 
35
33
  var _isSet2 = _interopRequireDefault(require("lodash/isSet"));
@@ -828,11 +826,10 @@ class MonthsGridFoundation extends _foundation.default {
828
826
  /**
829
827
  * no need to check focus then
830
828
  * - dateRange and isDateRangeAndHasOffset
831
- * - dateRange and triggerRender
832
829
  */
833
830
 
834
831
 
835
- const needCheckFocusRecord = !(type === 'dateRange' && (isDateRangeAndHasOffset || (0, _isFunction2.default)(triggerRender)));
832
+ const needCheckFocusRecord = !(type === 'dateRange' && isDateRangeAndHasOffset);
836
833
 
837
834
  this._adapter.notifySelectedChange(date, {
838
835
  needCheckFocusRecord
@@ -51,6 +51,7 @@ export interface ModalProps {
51
51
  keepDOM?: boolean;
52
52
  direction?: any;
53
53
  fullScreen?: boolean;
54
+ preventScroll?: boolean;
54
55
  }
55
56
  export interface ModalState {
56
57
  hidden: boolean;
@@ -974,14 +974,17 @@ class Tooltip extends _foundation.default {
974
974
  _focusTrigger() {
975
975
  const {
976
976
  trigger,
977
- returnFocusOnClose
977
+ returnFocusOnClose,
978
+ preventScroll
978
979
  } = this.getProps();
979
980
 
980
- if (returnFocusOnClose && trigger === 'click') {
981
+ if (returnFocusOnClose && trigger !== 'custom') {
981
982
  const triggerNode = this._adapter.getTriggerNode();
982
983
 
983
984
  if (triggerNode && 'focus' in triggerNode) {
984
- triggerNode.focus();
985
+ triggerNode.focus({
986
+ preventScroll
987
+ });
985
988
  }
986
989
  }
987
990
  }
@@ -1001,34 +1004,56 @@ class Tooltip extends _foundation.default {
1001
1004
  }
1002
1005
 
1003
1006
  _handleContainerTabKeyDown(focusableElements, event) {
1007
+ const {
1008
+ preventScroll
1009
+ } = this.getProps();
1010
+
1004
1011
  const activeElement = this._adapter.getActiveElement();
1005
1012
 
1006
1013
  const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
1007
1014
 
1008
1015
  if (isLastCurrentFocus) {
1009
- focusableElements[0].focus();
1016
+ focusableElements[0].focus({
1017
+ preventScroll
1018
+ });
1010
1019
  event.preventDefault(); // prevent browser default tab move behavior
1011
1020
  }
1012
1021
  }
1013
1022
 
1014
1023
  _handleContainerShiftTabKeyDown(focusableElements, event) {
1024
+ const {
1025
+ preventScroll
1026
+ } = this.getProps();
1027
+
1015
1028
  const activeElement = this._adapter.getActiveElement();
1016
1029
 
1017
1030
  const isFirstCurrentFocus = focusableElements[0] === activeElement;
1018
1031
 
1019
1032
  if (isFirstCurrentFocus) {
1020
- focusableElements[focusableElements.length - 1].focus();
1033
+ focusableElements[focusableElements.length - 1].focus({
1034
+ preventScroll
1035
+ });
1021
1036
  event.preventDefault(); // prevent browser default tab move behavior
1022
1037
  }
1023
1038
  }
1024
1039
 
1025
1040
  _handleTriggerArrowDownKeydown(focusableElements, event) {
1026
- focusableElements[0].focus();
1041
+ const {
1042
+ preventScroll
1043
+ } = this.getProps();
1044
+ focusableElements[0].focus({
1045
+ preventScroll
1046
+ });
1027
1047
  event.preventDefault(); // prevent browser default scroll behavior
1028
1048
  }
1029
1049
 
1030
1050
  _handleTriggerArrowUpKeydown(focusableElements, event) {
1031
- focusableElements[focusableElements.length - 1].focus();
1051
+ const {
1052
+ preventScroll
1053
+ } = this.getProps();
1054
+ focusableElements[focusableElements.length - 1].focus({
1055
+ preventScroll
1056
+ });
1032
1057
  event.preventDefault(); // prevent browser default scroll behavior
1033
1058
  }
1034
1059
 
@@ -168,6 +168,7 @@ export interface BasicTreeProps {
168
168
  onContextMenu?: (e: any, node: BasicTreeNodeData) => void;
169
169
  onSearch?: (sunInput: string) => void;
170
170
  onSelect?: (selectedKeys: string, selected: boolean, selectedNode: BasicTreeNodeData) => void;
171
+ preventScroll?: boolean;
171
172
  renderDraggingNode?: (nodeInstance: HTMLElement, node: BasicTreeNodeData) => HTMLElement;
172
173
  renderFullLabel?: (renderFullLabelProps: BasicRenderFullLabelProps) => any;
173
174
  renderLabel?: (label?: any, treeNode?: BasicTreeNodeData) => any;
@@ -1092,6 +1092,7 @@ class UploadFoundation extends _foundation.default {
1092
1092
  if (!disabled) {
1093
1093
  if (directory) {
1094
1094
  this.handleDirectoryDrop(e);
1095
+ return;
1095
1096
  }
1096
1097
 
1097
1098
  const files = (0, _from.default)(e.dataTransfer.files);
@@ -0,0 +1,26 @@
1
+ declare type FocusRedirectListener = (element: HTMLElement) => boolean;
2
+ interface HandleOptions {
3
+ enable?: boolean;
4
+ onFocusRedirectListener?: FocusRedirectListener | FocusRedirectListener[];
5
+ preventScroll?: boolean;
6
+ }
7
+ declare class FocusTrapHandle {
8
+ container: HTMLElement;
9
+ private options;
10
+ private focusRedirectListenerList;
11
+ private _enable;
12
+ constructor(container: HTMLElement, options?: HandleOptions);
13
+ addFocusRedirectListener: (listener: FocusRedirectListener) => () => void;
14
+ removeFocusRedirectListener: (listener: FocusRedirectListener) => void;
15
+ get enable(): boolean;
16
+ set enable(value: boolean);
17
+ destroy: () => void;
18
+ private shouldFocusRedirect;
19
+ private focusElement;
20
+ private onKeyPress;
21
+ private handleContainerTabKeyDown;
22
+ private handleContainerShiftTabKeyDown;
23
+ static getFocusableElements(node: HTMLElement): HTMLElement[];
24
+ static getActiveElement(): HTMLElement | null;
25
+ }
26
+ export default FocusTrapHandle;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+
3
+ var _Object$defineProperty = require("@babel/runtime-corejs3/core-js-stable/object/define-property");
4
+
5
+ var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
6
+
7
+ _Object$defineProperty(exports, "__esModule", {
8
+ value: true
9
+ });
10
+
11
+ exports.default = void 0;
12
+
13
+ var _freeze = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/object/freeze"));
14
+
15
+ var _isArray = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/is-array"));
16
+
17
+ var _from = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/from"));
18
+
19
+ var _without2 = _interopRequireDefault(require("lodash/without"));
20
+
21
+ var _dom = require("./dom");
22
+
23
+ /*
24
+ * Usage:
25
+ * // Eg1: Pass a dom as the tab tarp container.
26
+ * const handle = new FocusTrapHandle(container, { enable: true });
27
+ *
28
+ * // Eg2: The focus redirect listener will be triggered when user pressed tab whiling last focusable dom is focusing in trap dom, return false to cancel redirect and use the browser normal tab focus index.
29
+ * handle.addFocusRedirectListener((e)=>{
30
+ * return true; // return false to prevent redirect on target DOM;
31
+ * });
32
+ *
33
+ * // Eg3: Set it to false in order to disable tab tarp at any moment;
34
+ * handle.enable = true;
35
+ *
36
+ * // Eg4: Destroy instance when component is unmounting for saving resource;
37
+ * handle.destroy();
38
+ *
39
+ * */
40
+ class FocusTrapHandle {
41
+ constructor(container, options) {
42
+ var _a;
43
+
44
+ this.addFocusRedirectListener = listener => {
45
+ this.focusRedirectListenerList.push(listener);
46
+ return () => this.removeFocusRedirectListener(listener);
47
+ };
48
+
49
+ this.removeFocusRedirectListener = listener => {
50
+ this.focusRedirectListenerList = (0, _without2.default)(this.focusRedirectListenerList, listener);
51
+ };
52
+
53
+ this.destroy = () => {
54
+ var _a;
55
+
56
+ (_a = this.container) === null || _a === void 0 ? void 0 : _a.removeEventListener('keydown', this.onKeyPress);
57
+ }; // ---- private func ----
58
+
59
+
60
+ this.shouldFocusRedirect = element => {
61
+ if (!this.enable) {
62
+ return false;
63
+ }
64
+
65
+ for (const listener of this.focusRedirectListenerList) {
66
+ const should = listener(element);
67
+
68
+ if (!should) {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ return true;
74
+ };
75
+
76
+ this.focusElement = (element, event) => {
77
+ const {
78
+ preventScroll
79
+ } = this.options;
80
+ element === null || element === void 0 ? void 0 : element.focus({
81
+ preventScroll
82
+ });
83
+ event.preventDefault(); // prevent browser default tab move behavior
84
+ };
85
+
86
+ this.onKeyPress = event => {
87
+ if (event && event.key === 'Tab') {
88
+ const focusableElements = FocusTrapHandle.getFocusableElements(this.container);
89
+ const focusableNum = focusableElements.length;
90
+
91
+ if (focusableNum) {
92
+ // Shift + Tab will move focus backward
93
+ if (event.shiftKey) {
94
+ this.handleContainerShiftTabKeyDown(focusableElements, event);
95
+ } else {
96
+ this.handleContainerTabKeyDown(focusableElements, event);
97
+ }
98
+ }
99
+ }
100
+ };
101
+
102
+ this.handleContainerTabKeyDown = (focusableElements, event) => {
103
+ const activeElement = FocusTrapHandle.getActiveElement();
104
+ const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
105
+ const redirectForcingElement = focusableElements[0];
106
+
107
+ if (isLastCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
108
+ this.focusElement(redirectForcingElement, event);
109
+ }
110
+ };
111
+
112
+ this.handleContainerShiftTabKeyDown = (focusableElements, event) => {
113
+ const activeElement = FocusTrapHandle.getActiveElement();
114
+ const isFirstCurrentFocus = focusableElements[0] === activeElement;
115
+ const redirectForcingElement = focusableElements[focusableElements.length - 1];
116
+
117
+ if (isFirstCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
118
+ this.focusElement(redirectForcingElement, event);
119
+ }
120
+ };
121
+
122
+ (0, _freeze.default)(options); // prevent user to change options after init;
123
+
124
+ this.container = container;
125
+ this.options = options;
126
+ this.enable = (_a = options === null || options === void 0 ? void 0 : options.enable) !== null && _a !== void 0 ? _a : true;
127
+
128
+ this.focusRedirectListenerList = (() => {
129
+ if (options === null || options === void 0 ? void 0 : options.onFocusRedirectListener) {
130
+ return (0, _isArray.default)(options.onFocusRedirectListener) ? [...options.onFocusRedirectListener] : [options.onFocusRedirectListener];
131
+ } else {
132
+ return [];
133
+ }
134
+ })();
135
+
136
+ this.container.addEventListener('keydown', this.onKeyPress);
137
+ }
138
+
139
+ get enable() {
140
+ return this._enable;
141
+ }
142
+
143
+ set enable(value) {
144
+ this._enable = value;
145
+ } // ---- static func ----
146
+
147
+
148
+ static getFocusableElements(node) {
149
+ if (!(0, _dom.isHTMLElement)(node)) {
150
+ return [];
151
+ }
152
+
153
+ const focusableSelectorsList = ["input:not([disabled]):not([tabindex='-1'])", "textarea:not([disabled]):not([tabindex='-1'])", "button:not([disabled]):not([tabindex='-1'])", "a[href]:not([tabindex='-1'])", "select:not([disabled]):not([tabindex='-1'])", "area[href]:not([tabindex='-1'])", "iframe:not([tabindex='-1'])", "object:not([tabindex='-1'])", "*[tabindex]:not([tabindex='-1'])", "*[contenteditable]:not([tabindex='-1'])"];
154
+ const focusableSelectorsStr = focusableSelectorsList.join(','); // we are not filtered elements which are invisible
155
+
156
+ return (0, _from.default)(node.querySelectorAll(focusableSelectorsStr));
157
+ }
158
+
159
+ static getActiveElement() {
160
+ return document ? document.activeElement : null;
161
+ }
162
+
163
+ }
164
+
165
+ var _default = FocusTrapHandle;
166
+ exports.default = _default;
@@ -99,6 +99,7 @@ export interface BasicCascaderProps {
99
99
  disableStrictly?: boolean;
100
100
  leafOnly?: boolean;
101
101
  enableLeafClick?: boolean;
102
+ preventScroll?: boolean;
102
103
  onClear?: () => void;
103
104
  triggerRender?: (props: BasicTriggerRenderProps) => any;
104
105
  onListScroll?: (e: any, panel: BasicScrollPanelProps) => void;
@@ -205,6 +206,14 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
205
206
  handleSingleSelect(e: any, item: BasicEntity | BasicData): void;
206
207
  _handleMultipleSelect(item: BasicEntity | BasicData): void;
207
208
  calcNonDisabledCheckedKeys(eventKey: string, targetStatus: boolean): {
209
+ checkedKeys: Set<string>; /**
210
+ * 典型的场景是: 假设我们选中了 0-0 这个节点,此时 selectedKeys=Set('0-0'),
211
+ * 输入框会显示 0-0 的 label。当 treeData 发生更新,假设此时 0-0 在 treeData
212
+ * 中不存在,则 selectedKeys=Set('not-exist-0-0'),此时输入框显示的是 0-0,
213
+ * 也就是显示 not-exist- 后的内容。当treeData再次更新,假设此时 0-0 在 treeData
214
+ * 中存在,则 selectedKeys=Set('0-0'),此时输入框显示 0-0 的 label。 这个地
215
+ * 方做的操作就是,为了例子中第二次更新后 0-0 label 能够正常显示。
216
+ */
208
217
  /**
209
218
  * The typical scenario is: suppose we select the 0-0 node, at this time
210
219
  * selectedKeys=Set('0-0'), the input box will display a 0-0 label. When
@@ -216,7 +225,6 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
216
225
  * 0-0 at this time. The operation done here is for the 0-0 label to be
217
226
  * displayed normally after the second update in the example.
218
227
  */
219
- checkedKeys: Set<string>;
220
228
  halfCheckedKeys: Set<any>;
221
229
  };
222
230
  calcCheckedStatus(targetStatus: boolean, eventKey: string): boolean;
@@ -55,5 +55,6 @@ export interface BaseCheckboxProps {
55
55
  extra?: any;
56
56
  addonId?: string;
57
57
  extraId?: string;
58
+ preventScroll?: boolean;
58
59
  }
59
60
  export default CheckboxFoundation;
@@ -132,6 +132,7 @@ export interface DatePickerFoundationProps extends ElementProps, RenderProps, Ev
132
132
  localeCode?: string;
133
133
  rangeSeparator?: string;
134
134
  insetInput?: boolean;
135
+ preventScroll?: boolean;
135
136
  }
136
137
  export interface DatePickerFoundationState {
137
138
  panelShow: boolean;
@@ -185,6 +186,12 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
185
186
  initFromProps({ value, timeZone, prevTimeZone }: Pick<DatePickerFoundationProps, 'value' | 'timeZone'> & {
186
187
  prevTimeZone?: string | number;
187
188
  }): void;
189
+ /**
190
+ * 如果用户传了一个空的 value,需要把 range input focus 设置为 rangeStart,这样用户可以清除完之后继续从开始选择
191
+ *
192
+ * If the user passes an empty value, you need to set the range input focus to rangeStart, so that the user can continue to select from the beginning after clearing
193
+ */
194
+ initRangeInputFocus(result: Date[]): void;
188
195
  parseWithTimezone(value: ValueType, timeZone: string | number, prevTimeZone: string | number): Date[];
189
196
  _isMultiple(): boolean;
190
197
  /**
@@ -1,3 +1,4 @@
1
+ import _isFunction from "lodash/isFunction";
1
2
  import _isEqual from "lodash/isEqual";
2
3
  import _isString from "lodash/isString";
3
4
  import _isObject from "lodash/isObject";
@@ -133,10 +134,28 @@ export default class DatePickerFoundation extends BaseFoundation {
133
134
 
134
135
  this._adapter.updateValue(result);
135
136
 
137
+ this.initRangeInputFocus(result);
138
+
136
139
  if (this._adapter.needConfirm()) {
137
140
  this._adapter.updateCachedSelectedValue(result);
138
141
  }
139
142
  }
143
+ /**
144
+ * 如果用户传了一个空的 value,需要把 range input focus 设置为 rangeStart,这样用户可以清除完之后继续从开始选择
145
+ *
146
+ * If the user passes an empty value, you need to set the range input focus to rangeStart, so that the user can continue to select from the beginning after clearing
147
+ */
148
+
149
+
150
+ initRangeInputFocus(result) {
151
+ const {
152
+ triggerRender
153
+ } = this.getProps();
154
+
155
+ if (this._isRangeType() && _isFunction(triggerRender) && result.length === 0) {
156
+ this._adapter.setRangeInputFocus('rangeStart');
157
+ }
158
+ }
140
159
 
141
160
  parseWithTimezone(value, timeZone, prevTimeZone) {
142
161
  const result = [];
@@ -1238,7 +1257,8 @@ export default class DatePickerFoundation extends BaseFoundation {
1238
1257
 
1239
1258
  handleTriggerWrapperClick(e) {
1240
1259
  const {
1241
- disabled
1260
+ disabled,
1261
+ triggerRender
1242
1262
  } = this._adapter.getProps();
1243
1263
 
1244
1264
  const {
@@ -1257,12 +1277,19 @@ export default class DatePickerFoundation extends BaseFoundation {
1257
1277
  */
1258
1278
 
1259
1279
 
1260
- if (this._isRangeType() && !rangeInputFocus && this._adapter.isEventTarget(e)) {
1261
- _setTimeout(() => {
1262
- // using setTimeout get correct state value 'rangeInputFocus'
1263
- this.handleInputFocus(e, 'rangeStart');
1264
- this.openPanel();
1265
- }, 0);
1280
+ if (this._isRangeType() && !rangeInputFocus) {
1281
+ if (this._adapter.isEventTarget(e)) {
1282
+ _setTimeout(() => {
1283
+ // using setTimeout get correct state value 'rangeInputFocus'
1284
+ this.handleInputFocus(e, 'rangeStart');
1285
+ }, 0);
1286
+ } else if (_isFunction(triggerRender)) {
1287
+ // 如果是 triggerRender 场景,因为没有 input,因此打开面板时默认 focus 在 rangeStart
1288
+ // If it is a triggerRender scene, because there is no input, the default focus is rangeStart when the panel is opened
1289
+ this._adapter.setRangeInputFocus('rangeStart');
1290
+ }
1291
+
1292
+ this.openPanel();
1266
1293
  } else {
1267
1294
  this.openPanel();
1268
1295
  }
@@ -1,4 +1,3 @@
1
- import _isFunction from "lodash/isFunction";
2
1
  import _isEqual from "lodash/isEqual";
3
2
  import _isSet from "lodash/isSet";
4
3
  import _includes from "lodash/includes";
@@ -800,11 +799,10 @@ export default class MonthsGridFoundation extends BaseFoundation {
800
799
  /**
801
800
  * no need to check focus then
802
801
  * - dateRange and isDateRangeAndHasOffset
803
- * - dateRange and triggerRender
804
802
  */
805
803
 
806
804
 
807
- const needCheckFocusRecord = !(type === 'dateRange' && (isDateRangeAndHasOffset || _isFunction(triggerRender)));
805
+ const needCheckFocusRecord = !(type === 'dateRange' && isDateRangeAndHasOffset);
808
806
 
809
807
  this._adapter.notifySelectedChange(date, {
810
808
  needCheckFocusRecord
@@ -51,6 +51,7 @@ export interface ModalProps {
51
51
  keepDOM?: boolean;
52
52
  direction?: any;
53
53
  fullScreen?: boolean;
54
+ preventScroll?: boolean;
54
55
  }
55
56
  export interface ModalState {
56
57
  hidden: boolean;
@@ -958,14 +958,17 @@ export default class Tooltip extends BaseFoundation {
958
958
  _focusTrigger() {
959
959
  const {
960
960
  trigger,
961
- returnFocusOnClose
961
+ returnFocusOnClose,
962
+ preventScroll
962
963
  } = this.getProps();
963
964
 
964
- if (returnFocusOnClose && trigger === 'click') {
965
+ if (returnFocusOnClose && trigger !== 'custom') {
965
966
  const triggerNode = this._adapter.getTriggerNode();
966
967
 
967
968
  if (triggerNode && 'focus' in triggerNode) {
968
- triggerNode.focus();
969
+ triggerNode.focus({
970
+ preventScroll
971
+ });
969
972
  }
970
973
  }
971
974
  }
@@ -985,34 +988,56 @@ export default class Tooltip extends BaseFoundation {
985
988
  }
986
989
 
987
990
  _handleContainerTabKeyDown(focusableElements, event) {
991
+ const {
992
+ preventScroll
993
+ } = this.getProps();
994
+
988
995
  const activeElement = this._adapter.getActiveElement();
989
996
 
990
997
  const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
991
998
 
992
999
  if (isLastCurrentFocus) {
993
- focusableElements[0].focus();
1000
+ focusableElements[0].focus({
1001
+ preventScroll
1002
+ });
994
1003
  event.preventDefault(); // prevent browser default tab move behavior
995
1004
  }
996
1005
  }
997
1006
 
998
1007
  _handleContainerShiftTabKeyDown(focusableElements, event) {
1008
+ const {
1009
+ preventScroll
1010
+ } = this.getProps();
1011
+
999
1012
  const activeElement = this._adapter.getActiveElement();
1000
1013
 
1001
1014
  const isFirstCurrentFocus = focusableElements[0] === activeElement;
1002
1015
 
1003
1016
  if (isFirstCurrentFocus) {
1004
- focusableElements[focusableElements.length - 1].focus();
1017
+ focusableElements[focusableElements.length - 1].focus({
1018
+ preventScroll
1019
+ });
1005
1020
  event.preventDefault(); // prevent browser default tab move behavior
1006
1021
  }
1007
1022
  }
1008
1023
 
1009
1024
  _handleTriggerArrowDownKeydown(focusableElements, event) {
1010
- focusableElements[0].focus();
1025
+ const {
1026
+ preventScroll
1027
+ } = this.getProps();
1028
+ focusableElements[0].focus({
1029
+ preventScroll
1030
+ });
1011
1031
  event.preventDefault(); // prevent browser default scroll behavior
1012
1032
  }
1013
1033
 
1014
1034
  _handleTriggerArrowUpKeydown(focusableElements, event) {
1015
- focusableElements[focusableElements.length - 1].focus();
1035
+ const {
1036
+ preventScroll
1037
+ } = this.getProps();
1038
+ focusableElements[focusableElements.length - 1].focus({
1039
+ preventScroll
1040
+ });
1016
1041
  event.preventDefault(); // prevent browser default scroll behavior
1017
1042
  }
1018
1043
 
@@ -168,6 +168,7 @@ export interface BasicTreeProps {
168
168
  onContextMenu?: (e: any, node: BasicTreeNodeData) => void;
169
169
  onSearch?: (sunInput: string) => void;
170
170
  onSelect?: (selectedKeys: string, selected: boolean, selectedNode: BasicTreeNodeData) => void;
171
+ preventScroll?: boolean;
171
172
  renderDraggingNode?: (nodeInstance: HTMLElement, node: BasicTreeNodeData) => HTMLElement;
172
173
  renderFullLabel?: (renderFullLabelProps: BasicRenderFullLabelProps) => any;
173
174
  renderLabel?: (label?: any, treeNode?: BasicTreeNodeData) => any;
@@ -1086,6 +1086,7 @@ class UploadFoundation extends BaseFoundation {
1086
1086
  if (!disabled) {
1087
1087
  if (directory) {
1088
1088
  this.handleDirectoryDrop(e);
1089
+ return;
1089
1090
  }
1090
1091
 
1091
1092
  const files = _Array$from(e.dataTransfer.files);
@@ -0,0 +1,26 @@
1
+ declare type FocusRedirectListener = (element: HTMLElement) => boolean;
2
+ interface HandleOptions {
3
+ enable?: boolean;
4
+ onFocusRedirectListener?: FocusRedirectListener | FocusRedirectListener[];
5
+ preventScroll?: boolean;
6
+ }
7
+ declare class FocusTrapHandle {
8
+ container: HTMLElement;
9
+ private options;
10
+ private focusRedirectListenerList;
11
+ private _enable;
12
+ constructor(container: HTMLElement, options?: HandleOptions);
13
+ addFocusRedirectListener: (listener: FocusRedirectListener) => () => void;
14
+ removeFocusRedirectListener: (listener: FocusRedirectListener) => void;
15
+ get enable(): boolean;
16
+ set enable(value: boolean);
17
+ destroy: () => void;
18
+ private shouldFocusRedirect;
19
+ private focusElement;
20
+ private onKeyPress;
21
+ private handleContainerTabKeyDown;
22
+ private handleContainerShiftTabKeyDown;
23
+ static getFocusableElements(node: HTMLElement): HTMLElement[];
24
+ static getActiveElement(): HTMLElement | null;
25
+ }
26
+ export default FocusTrapHandle;
@@ -0,0 +1,150 @@
1
+ import _without from "lodash/without";
2
+ import _Object$freeze from "@babel/runtime-corejs3/core-js-stable/object/freeze";
3
+ import _Array$isArray from "@babel/runtime-corejs3/core-js-stable/array/is-array";
4
+ import _Array$from from "@babel/runtime-corejs3/core-js-stable/array/from";
5
+ import { isHTMLElement } from "./dom";
6
+
7
+ /*
8
+ * Usage:
9
+ * // Eg1: Pass a dom as the tab tarp container.
10
+ * const handle = new FocusTrapHandle(container, { enable: true });
11
+ *
12
+ * // Eg2: The focus redirect listener will be triggered when user pressed tab whiling last focusable dom is focusing in trap dom, return false to cancel redirect and use the browser normal tab focus index.
13
+ * handle.addFocusRedirectListener((e)=>{
14
+ * return true; // return false to prevent redirect on target DOM;
15
+ * });
16
+ *
17
+ * // Eg3: Set it to false in order to disable tab tarp at any moment;
18
+ * handle.enable = true;
19
+ *
20
+ * // Eg4: Destroy instance when component is unmounting for saving resource;
21
+ * handle.destroy();
22
+ *
23
+ * */
24
+ class FocusTrapHandle {
25
+ constructor(container, options) {
26
+ var _a;
27
+
28
+ this.addFocusRedirectListener = listener => {
29
+ this.focusRedirectListenerList.push(listener);
30
+ return () => this.removeFocusRedirectListener(listener);
31
+ };
32
+
33
+ this.removeFocusRedirectListener = listener => {
34
+ this.focusRedirectListenerList = _without(this.focusRedirectListenerList, listener);
35
+ };
36
+
37
+ this.destroy = () => {
38
+ var _a;
39
+
40
+ (_a = this.container) === null || _a === void 0 ? void 0 : _a.removeEventListener('keydown', this.onKeyPress);
41
+ }; // ---- private func ----
42
+
43
+
44
+ this.shouldFocusRedirect = element => {
45
+ if (!this.enable) {
46
+ return false;
47
+ }
48
+
49
+ for (const listener of this.focusRedirectListenerList) {
50
+ const should = listener(element);
51
+
52
+ if (!should) {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ return true;
58
+ };
59
+
60
+ this.focusElement = (element, event) => {
61
+ const {
62
+ preventScroll
63
+ } = this.options;
64
+ element === null || element === void 0 ? void 0 : element.focus({
65
+ preventScroll
66
+ });
67
+ event.preventDefault(); // prevent browser default tab move behavior
68
+ };
69
+
70
+ this.onKeyPress = event => {
71
+ if (event && event.key === 'Tab') {
72
+ const focusableElements = FocusTrapHandle.getFocusableElements(this.container);
73
+ const focusableNum = focusableElements.length;
74
+
75
+ if (focusableNum) {
76
+ // Shift + Tab will move focus backward
77
+ if (event.shiftKey) {
78
+ this.handleContainerShiftTabKeyDown(focusableElements, event);
79
+ } else {
80
+ this.handleContainerTabKeyDown(focusableElements, event);
81
+ }
82
+ }
83
+ }
84
+ };
85
+
86
+ this.handleContainerTabKeyDown = (focusableElements, event) => {
87
+ const activeElement = FocusTrapHandle.getActiveElement();
88
+ const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
89
+ const redirectForcingElement = focusableElements[0];
90
+
91
+ if (isLastCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
92
+ this.focusElement(redirectForcingElement, event);
93
+ }
94
+ };
95
+
96
+ this.handleContainerShiftTabKeyDown = (focusableElements, event) => {
97
+ const activeElement = FocusTrapHandle.getActiveElement();
98
+ const isFirstCurrentFocus = focusableElements[0] === activeElement;
99
+ const redirectForcingElement = focusableElements[focusableElements.length - 1];
100
+
101
+ if (isFirstCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
102
+ this.focusElement(redirectForcingElement, event);
103
+ }
104
+ };
105
+
106
+ _Object$freeze(options); // prevent user to change options after init;
107
+
108
+
109
+ this.container = container;
110
+ this.options = options;
111
+ this.enable = (_a = options === null || options === void 0 ? void 0 : options.enable) !== null && _a !== void 0 ? _a : true;
112
+
113
+ this.focusRedirectListenerList = (() => {
114
+ if (options === null || options === void 0 ? void 0 : options.onFocusRedirectListener) {
115
+ return _Array$isArray(options.onFocusRedirectListener) ? [...options.onFocusRedirectListener] : [options.onFocusRedirectListener];
116
+ } else {
117
+ return [];
118
+ }
119
+ })();
120
+
121
+ this.container.addEventListener('keydown', this.onKeyPress);
122
+ }
123
+
124
+ get enable() {
125
+ return this._enable;
126
+ }
127
+
128
+ set enable(value) {
129
+ this._enable = value;
130
+ } // ---- static func ----
131
+
132
+
133
+ static getFocusableElements(node) {
134
+ if (!isHTMLElement(node)) {
135
+ return [];
136
+ }
137
+
138
+ const focusableSelectorsList = ["input:not([disabled]):not([tabindex='-1'])", "textarea:not([disabled]):not([tabindex='-1'])", "button:not([disabled]):not([tabindex='-1'])", "a[href]:not([tabindex='-1'])", "select:not([disabled]):not([tabindex='-1'])", "area[href]:not([tabindex='-1'])", "iframe:not([tabindex='-1'])", "object:not([tabindex='-1'])", "*[tabindex]:not([tabindex='-1'])", "*[contenteditable]:not([tabindex='-1'])"];
139
+ const focusableSelectorsStr = focusableSelectorsList.join(','); // we are not filtered elements which are invisible
140
+
141
+ return _Array$from(node.querySelectorAll(focusableSelectorsStr));
142
+ }
143
+
144
+ static getActiveElement() {
145
+ return document ? document.activeElement : null;
146
+ }
147
+
148
+ }
149
+
150
+ export default FocusTrapHandle;
@@ -54,6 +54,7 @@ export interface ModalProps {
54
54
  keepDOM?: boolean;
55
55
  direction?: any;
56
56
  fullScreen?: boolean;
57
+ preventScroll?: boolean;
57
58
  }
58
59
 
59
60
  export interface ModalState {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@douyinfe/semi-foundation",
3
- "version": "2.10.1",
3
+ "version": "2.10.5",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "build:lib": "node ./scripts/compileLib.js",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@babel/runtime-corejs3": "^7.15.4",
11
- "@douyinfe/semi-animation": "2.10.1",
11
+ "@douyinfe/semi-animation": "2.10.5",
12
12
  "async-validator": "^3.5.0",
13
13
  "classnames": "^2.2.6",
14
14
  "date-fns": "^2.9.0",
@@ -24,7 +24,7 @@
24
24
  "*.scss",
25
25
  "*.css"
26
26
  ],
27
- "gitHead": "1c84b585ff43db35b286e8c24f8333e4170d015c",
27
+ "gitHead": "17a549be94b38a44d7cd9a4d3f77c23da7df460a",
28
28
  "devDependencies": {
29
29
  "@babel/plugin-proposal-decorators": "^7.15.8",
30
30
  "@babel/plugin-transform-runtime": "^7.15.8",
@@ -880,11 +880,11 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
880
880
  * 因此 returnFocusOnClose 只支持 click trigger
881
881
  */
882
882
  _focusTrigger() {
883
- const { trigger, returnFocusOnClose } = this.getProps();
884
- if (returnFocusOnClose && trigger === 'click') {
883
+ const { trigger, returnFocusOnClose, preventScroll } = this.getProps();
884
+ if (returnFocusOnClose && trigger !== 'custom') {
885
885
  const triggerNode = this._adapter.getTriggerNode();
886
886
  if (triggerNode && 'focus' in triggerNode) {
887
- triggerNode.focus();
887
+ triggerNode.focus({ preventScroll });
888
888
  }
889
889
  }
890
890
  }
@@ -899,30 +899,34 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
899
899
  }
900
900
 
901
901
  _handleContainerTabKeyDown(focusableElements: any[], event: any) {
902
+ const { preventScroll } = this.getProps();
902
903
  const activeElement = this._adapter.getActiveElement();
903
904
  const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
904
905
  if (isLastCurrentFocus) {
905
- focusableElements[0].focus();
906
+ focusableElements[0].focus({ preventScroll });
906
907
  event.preventDefault(); // prevent browser default tab move behavior
907
908
  }
908
909
  }
909
910
 
910
911
  _handleContainerShiftTabKeyDown(focusableElements: any[], event: any) {
912
+ const { preventScroll } = this.getProps();
911
913
  const activeElement = this._adapter.getActiveElement();
912
914
  const isFirstCurrentFocus = focusableElements[0] === activeElement;
913
915
  if (isFirstCurrentFocus) {
914
- focusableElements[focusableElements.length - 1].focus();
916
+ focusableElements[focusableElements.length - 1].focus({ preventScroll });
915
917
  event.preventDefault(); // prevent browser default tab move behavior
916
918
  }
917
919
  }
918
920
 
919
921
  _handleTriggerArrowDownKeydown(focusableElements: any[], event: any) {
920
- focusableElements[0].focus();
922
+ const { preventScroll } = this.getProps();
923
+ focusableElements[0].focus({ preventScroll });
921
924
  event.preventDefault(); // prevent browser default scroll behavior
922
925
  }
923
926
 
924
927
  _handleTriggerArrowUpKeydown(focusableElements: any[], event: any) {
925
- focusableElements[focusableElements.length - 1].focus();
928
+ const { preventScroll } = this.getProps();
929
+ focusableElements[focusableElements.length - 1].focus({ preventScroll });
926
930
  event.preventDefault(); // prevent browser default scroll behavior
927
931
  }
928
932
  }
@@ -219,6 +219,7 @@ export interface BasicTreeProps {
219
219
  onContextMenu?: (e: any, node: BasicTreeNodeData) => void;
220
220
  onSearch?: (sunInput: string) => void;
221
221
  onSelect?: (selectedKeys: string, selected: boolean, selectedNode: BasicTreeNodeData) => void;
222
+ preventScroll?: boolean;
222
223
  renderDraggingNode?: (nodeInstance: HTMLElement, node: BasicTreeNodeData) => HTMLElement;
223
224
  renderFullLabel?: (renderFullLabelProps: BasicRenderFullLabelProps) => any;
224
225
  renderLabel?: (label?: any, treeNode?: BasicTreeNodeData) => any;
@@ -773,6 +773,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
773
773
  if (!disabled) {
774
774
  if (directory) {
775
775
  this.handleDirectoryDrop(e);
776
+ return;
776
777
  }
777
778
  const files: File[] = Array.from(e.dataTransfer.files);
778
779
  this.handleChange(files);
@@ -0,0 +1,161 @@
1
+ import { isHTMLElement } from "./dom";
2
+ import { without } from "lodash";
3
+
4
+
5
+ type FocusRedirectListener = (element: HTMLElement) => boolean;
6
+
7
+ interface HandleOptions {
8
+ enable?: boolean
9
+ onFocusRedirectListener?: FocusRedirectListener | FocusRedirectListener[]
10
+ preventScroll?: boolean;
11
+ }
12
+
13
+ /*
14
+ * Usage:
15
+ * // Eg1: Pass a dom as the tab tarp container.
16
+ * const handle = new FocusTrapHandle(container, { enable: true });
17
+ *
18
+ * // Eg2: The focus redirect listener will be triggered when user pressed tab whiling last focusable dom is focusing in trap dom, return false to cancel redirect and use the browser normal tab focus index.
19
+ * handle.addFocusRedirectListener((e)=>{
20
+ * return true; // return false to prevent redirect on target DOM;
21
+ * });
22
+ *
23
+ * // Eg3: Set it to false in order to disable tab tarp at any moment;
24
+ * handle.enable = true;
25
+ *
26
+ * // Eg4: Destroy instance when component is unmounting for saving resource;
27
+ * handle.destroy();
28
+ *
29
+ * */
30
+
31
+ class FocusTrapHandle {
32
+ public container: HTMLElement;
33
+ private options: HandleOptions;
34
+ private focusRedirectListenerList: FocusRedirectListener[];
35
+ private _enable: boolean;
36
+
37
+ constructor(container: HTMLElement, options?: HandleOptions) {
38
+ Object.freeze(options); // prevent user to change options after init;
39
+ this.container = container;
40
+ this.options = options;
41
+ this.enable = options?.enable ?? true;
42
+ this.focusRedirectListenerList = (() => {
43
+ if (options?.onFocusRedirectListener) {
44
+ return Array.isArray(options.onFocusRedirectListener) ? [...options.onFocusRedirectListener] : [options.onFocusRedirectListener];
45
+ } else {
46
+ return [];
47
+ }
48
+ })();
49
+ this.container.addEventListener('keydown', this.onKeyPress);
50
+ }
51
+
52
+ public addFocusRedirectListener = (listener: FocusRedirectListener) => {
53
+ this.focusRedirectListenerList.push(listener);
54
+ return () => this.removeFocusRedirectListener(listener);
55
+ }
56
+
57
+ public removeFocusRedirectListener = (listener: FocusRedirectListener) => {
58
+ this.focusRedirectListenerList = without(this.focusRedirectListenerList, listener);
59
+ }
60
+
61
+ public get enable() {
62
+ return this._enable;
63
+ }
64
+
65
+ public set enable(value) {
66
+ this._enable = value;
67
+ }
68
+
69
+ public destroy = () => {
70
+ this.container?.removeEventListener('keydown', this.onKeyPress);
71
+ }
72
+
73
+ // ---- private func ----
74
+
75
+ private shouldFocusRedirect = (element: HTMLElement) => {
76
+ if (!this.enable) {
77
+ return false;
78
+ }
79
+ for (const listener of this.focusRedirectListenerList) {
80
+ const should = listener(element);
81
+ if (!should) {
82
+ return false;
83
+ }
84
+ }
85
+ return true;
86
+ }
87
+
88
+ private focusElement = (element: HTMLElement, event: KeyboardEvent) => {
89
+ const { preventScroll } = this.options;
90
+ element?.focus({ preventScroll });
91
+ event.preventDefault(); // prevent browser default tab move behavior
92
+ }
93
+
94
+
95
+ private onKeyPress = (event: KeyboardEvent) => {
96
+ if (event && event.key === 'Tab') {
97
+ const focusableElements = FocusTrapHandle.getFocusableElements(this.container);
98
+ const focusableNum = focusableElements.length;
99
+ if (focusableNum) {
100
+ // Shift + Tab will move focus backward
101
+ if (event.shiftKey) {
102
+ this.handleContainerShiftTabKeyDown(focusableElements, event);
103
+ } else {
104
+ this.handleContainerTabKeyDown(focusableElements, event);
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ private handleContainerTabKeyDown = (focusableElements: any[], event: any) => {
111
+ const activeElement = FocusTrapHandle.getActiveElement();
112
+ const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
113
+
114
+ const redirectForcingElement = focusableElements[0];
115
+ if (isLastCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
116
+ this.focusElement(redirectForcingElement, event);
117
+ }
118
+ };
119
+
120
+
121
+ private handleContainerShiftTabKeyDown = (focusableElements: any[], event: KeyboardEvent) => {
122
+ const activeElement = FocusTrapHandle.getActiveElement();
123
+ const isFirstCurrentFocus = focusableElements[0] === activeElement;
124
+ const redirectForcingElement = focusableElements[focusableElements.length - 1];
125
+ if (isFirstCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
126
+ this.focusElement(redirectForcingElement, event);
127
+ }
128
+ };
129
+
130
+
131
+ // ---- static func ----
132
+
133
+ static getFocusableElements(node: HTMLElement) {
134
+ if (!isHTMLElement(node)) {
135
+ return [];
136
+ }
137
+ const focusableSelectorsList = [
138
+ "input:not([disabled]):not([tabindex='-1'])",
139
+ "textarea:not([disabled]):not([tabindex='-1'])",
140
+ "button:not([disabled]):not([tabindex='-1'])",
141
+ "a[href]:not([tabindex='-1'])",
142
+ "select:not([disabled]):not([tabindex='-1'])",
143
+ "area[href]:not([tabindex='-1'])",
144
+ "iframe:not([tabindex='-1'])",
145
+ "object:not([tabindex='-1'])",
146
+ "*[tabindex]:not([tabindex='-1'])",
147
+ "*[contenteditable]:not([tabindex='-1'])",
148
+ ];
149
+ const focusableSelectorsStr = focusableSelectorsList.join(',');
150
+ // we are not filtered elements which are invisible
151
+ return Array.from(node.querySelectorAll<HTMLElement>(focusableSelectorsStr));
152
+ }
153
+
154
+ static getActiveElement(): HTMLElement | null {
155
+ return document ? document.activeElement as HTMLElement : null;
156
+ }
157
+
158
+
159
+ }
160
+
161
+ export default FocusTrapHandle;