@douyinfe/semi-foundation 2.10.4 → 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;
@@ -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 {
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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.4",
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.4",
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": "5c0370819f1ab319ceded2fbf0c9c7a12e4f1b77",
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;
@@ -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;