@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.
- package/cascader/foundation.ts +1 -0
- package/checkbox/checkboxFoundation.ts +1 -0
- package/datePicker/foundation.ts +1 -0
- package/lib/cjs/cascader/foundation.d.ts +9 -1
- package/lib/cjs/checkbox/checkboxFoundation.d.ts +1 -0
- package/lib/cjs/datePicker/foundation.d.ts +1 -0
- package/lib/cjs/modal/modalFoundation.d.ts +1 -0
- package/lib/cjs/tooltip/foundation.js +32 -7
- package/lib/cjs/tree/foundation.d.ts +1 -0
- package/lib/cjs/utils/FocusHandle.d.ts +26 -0
- package/lib/cjs/utils/FocusHandle.js +166 -0
- package/lib/es/cascader/foundation.d.ts +9 -1
- package/lib/es/checkbox/checkboxFoundation.d.ts +1 -0
- package/lib/es/datePicker/foundation.d.ts +1 -0
- package/lib/es/modal/modalFoundation.d.ts +1 -0
- package/lib/es/tooltip/foundation.js +32 -7
- package/lib/es/tree/foundation.d.ts +1 -0
- package/lib/es/utils/FocusHandle.d.ts +26 -0
- package/lib/es/utils/FocusHandle.js +150 -0
- package/modal/modalFoundation.ts +1 -0
- package/package.json +3 -3
- package/tooltip/foundation.ts +11 -7
- package/tree/foundation.ts +1 -0
- package/utils/FocusHandle.ts +161 -0
package/cascader/foundation.ts
CHANGED
|
@@ -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;
|
package/datePicker/foundation.ts
CHANGED
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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;
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/modal/modalFoundation.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@douyinfe/semi-foundation",
|
|
3
|
-
"version": "2.10.
|
|
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.
|
|
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": "
|
|
27
|
+
"gitHead": "17a549be94b38a44d7cd9a4d3f77c23da7df460a",
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@babel/plugin-proposal-decorators": "^7.15.8",
|
|
30
30
|
"@babel/plugin-transform-runtime": "^7.15.8",
|
package/tooltip/foundation.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/tree/foundation.ts
CHANGED
|
@@ -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;
|