@douyinfe/semi-foundation 2.12.0-alpha.0 → 2.12.0-beta.2
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/lib/cjs/utils/FocusHandle.d.ts +25 -0
- package/lib/cjs/utils/FocusHandle.js +161 -0
- package/lib/es/utils/FocusHandle.d.ts +25 -0
- package/lib/es/utils/FocusHandle.js +145 -0
- package/package.json +3 -3
- package/utils/FocusHandle.ts +159 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
declare type FocusRedirectListener = (element: HTMLElement) => boolean;
|
|
2
|
+
interface HandleOptions {
|
|
3
|
+
enable?: boolean;
|
|
4
|
+
onFocusRedirectListener?: FocusRedirectListener | FocusRedirectListener[];
|
|
5
|
+
}
|
|
6
|
+
declare class FocusTrapHandle {
|
|
7
|
+
container: HTMLElement;
|
|
8
|
+
private options;
|
|
9
|
+
private focusRedirectListenerList;
|
|
10
|
+
private _enable;
|
|
11
|
+
constructor(container: HTMLElement, options?: HandleOptions);
|
|
12
|
+
addFocusRedirectListener: (listener: FocusRedirectListener) => () => void;
|
|
13
|
+
removeFocusRedirectListener: (listener: FocusRedirectListener) => void;
|
|
14
|
+
get enable(): boolean;
|
|
15
|
+
set enable(value: boolean);
|
|
16
|
+
destroy: () => void;
|
|
17
|
+
private shouldFocusRedirect;
|
|
18
|
+
private focusElement;
|
|
19
|
+
private onKeyPress;
|
|
20
|
+
private handleContainerTabKeyDown;
|
|
21
|
+
private handleContainerShiftTabKeyDown;
|
|
22
|
+
static getFocusableElements(node: HTMLElement): HTMLElement[];
|
|
23
|
+
static getActiveElement(): HTMLElement | null;
|
|
24
|
+
}
|
|
25
|
+
export default FocusTrapHandle;
|
|
@@ -0,0 +1,161 @@
|
|
|
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("@douyinfe/semi-foundation/utils/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
|
+
element === null || element === void 0 ? void 0 : element.focus();
|
|
78
|
+
event.preventDefault(); // prevent browser default tab move behavior
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this.onKeyPress = event => {
|
|
82
|
+
if (event && event.key === 'Tab') {
|
|
83
|
+
const focusableElements = FocusTrapHandle.getFocusableElements(this.container);
|
|
84
|
+
const focusableNum = focusableElements.length;
|
|
85
|
+
|
|
86
|
+
if (focusableNum) {
|
|
87
|
+
// Shift + Tab will move focus backward
|
|
88
|
+
if (event.shiftKey) {
|
|
89
|
+
this.handleContainerShiftTabKeyDown(focusableElements, event);
|
|
90
|
+
} else {
|
|
91
|
+
this.handleContainerTabKeyDown(focusableElements, event);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
this.handleContainerTabKeyDown = (focusableElements, event) => {
|
|
98
|
+
const activeElement = FocusTrapHandle.getActiveElement();
|
|
99
|
+
const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
|
|
100
|
+
const redirectForcingElement = focusableElements[0];
|
|
101
|
+
|
|
102
|
+
if (isLastCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
|
|
103
|
+
this.focusElement(redirectForcingElement, event);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
this.handleContainerShiftTabKeyDown = (focusableElements, event) => {
|
|
108
|
+
const activeElement = FocusTrapHandle.getActiveElement();
|
|
109
|
+
const isFirstCurrentFocus = focusableElements[0] === activeElement;
|
|
110
|
+
const redirectForcingElement = focusableElements[focusableElements.length - 1];
|
|
111
|
+
|
|
112
|
+
if (isFirstCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
|
|
113
|
+
this.focusElement(redirectForcingElement, event);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
(0, _freeze.default)(options); // prevent user to change options after init;
|
|
118
|
+
|
|
119
|
+
this.container = container;
|
|
120
|
+
this.options = options;
|
|
121
|
+
this.enable = (_a = options === null || options === void 0 ? void 0 : options.enable) !== null && _a !== void 0 ? _a : true;
|
|
122
|
+
|
|
123
|
+
this.focusRedirectListenerList = (() => {
|
|
124
|
+
if (options === null || options === void 0 ? void 0 : options.onFocusRedirectListener) {
|
|
125
|
+
return (0, _isArray.default)(options.onFocusRedirectListener) ? [...options.onFocusRedirectListener] : [options.onFocusRedirectListener];
|
|
126
|
+
} else {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
|
|
131
|
+
this.container.addEventListener('keydown', this.onKeyPress);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get enable() {
|
|
135
|
+
return this._enable;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
set enable(value) {
|
|
139
|
+
this._enable = value;
|
|
140
|
+
} // ---- static func ----
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
static getFocusableElements(node) {
|
|
144
|
+
if (!(0, _dom.isHTMLElement)(node)) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
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'])"];
|
|
149
|
+
const focusableSelectorsStr = focusableSelectorsList.join(','); // we are not filtered elements which are invisible
|
|
150
|
+
|
|
151
|
+
return (0, _from.default)(node.querySelectorAll(focusableSelectorsStr));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static getActiveElement() {
|
|
155
|
+
return document ? document.activeElement : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
var _default = FocusTrapHandle;
|
|
161
|
+
exports.default = _default;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
declare type FocusRedirectListener = (element: HTMLElement) => boolean;
|
|
2
|
+
interface HandleOptions {
|
|
3
|
+
enable?: boolean;
|
|
4
|
+
onFocusRedirectListener?: FocusRedirectListener | FocusRedirectListener[];
|
|
5
|
+
}
|
|
6
|
+
declare class FocusTrapHandle {
|
|
7
|
+
container: HTMLElement;
|
|
8
|
+
private options;
|
|
9
|
+
private focusRedirectListenerList;
|
|
10
|
+
private _enable;
|
|
11
|
+
constructor(container: HTMLElement, options?: HandleOptions);
|
|
12
|
+
addFocusRedirectListener: (listener: FocusRedirectListener) => () => void;
|
|
13
|
+
removeFocusRedirectListener: (listener: FocusRedirectListener) => void;
|
|
14
|
+
get enable(): boolean;
|
|
15
|
+
set enable(value: boolean);
|
|
16
|
+
destroy: () => void;
|
|
17
|
+
private shouldFocusRedirect;
|
|
18
|
+
private focusElement;
|
|
19
|
+
private onKeyPress;
|
|
20
|
+
private handleContainerTabKeyDown;
|
|
21
|
+
private handleContainerShiftTabKeyDown;
|
|
22
|
+
static getFocusableElements(node: HTMLElement): HTMLElement[];
|
|
23
|
+
static getActiveElement(): HTMLElement | null;
|
|
24
|
+
}
|
|
25
|
+
export default FocusTrapHandle;
|
|
@@ -0,0 +1,145 @@
|
|
|
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 "@douyinfe/semi-foundation/utils/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
|
+
element === null || element === void 0 ? void 0 : element.focus();
|
|
62
|
+
event.preventDefault(); // prevent browser default tab move behavior
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.onKeyPress = event => {
|
|
66
|
+
if (event && event.key === 'Tab') {
|
|
67
|
+
const focusableElements = FocusTrapHandle.getFocusableElements(this.container);
|
|
68
|
+
const focusableNum = focusableElements.length;
|
|
69
|
+
|
|
70
|
+
if (focusableNum) {
|
|
71
|
+
// Shift + Tab will move focus backward
|
|
72
|
+
if (event.shiftKey) {
|
|
73
|
+
this.handleContainerShiftTabKeyDown(focusableElements, event);
|
|
74
|
+
} else {
|
|
75
|
+
this.handleContainerTabKeyDown(focusableElements, event);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this.handleContainerTabKeyDown = (focusableElements, event) => {
|
|
82
|
+
const activeElement = FocusTrapHandle.getActiveElement();
|
|
83
|
+
const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
|
|
84
|
+
const redirectForcingElement = focusableElements[0];
|
|
85
|
+
|
|
86
|
+
if (isLastCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
|
|
87
|
+
this.focusElement(redirectForcingElement, event);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.handleContainerShiftTabKeyDown = (focusableElements, event) => {
|
|
92
|
+
const activeElement = FocusTrapHandle.getActiveElement();
|
|
93
|
+
const isFirstCurrentFocus = focusableElements[0] === activeElement;
|
|
94
|
+
const redirectForcingElement = focusableElements[focusableElements.length - 1];
|
|
95
|
+
|
|
96
|
+
if (isFirstCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
|
|
97
|
+
this.focusElement(redirectForcingElement, event);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
_Object$freeze(options); // prevent user to change options after init;
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
this.container = container;
|
|
105
|
+
this.options = options;
|
|
106
|
+
this.enable = (_a = options === null || options === void 0 ? void 0 : options.enable) !== null && _a !== void 0 ? _a : true;
|
|
107
|
+
|
|
108
|
+
this.focusRedirectListenerList = (() => {
|
|
109
|
+
if (options === null || options === void 0 ? void 0 : options.onFocusRedirectListener) {
|
|
110
|
+
return _Array$isArray(options.onFocusRedirectListener) ? [...options.onFocusRedirectListener] : [options.onFocusRedirectListener];
|
|
111
|
+
} else {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
|
|
116
|
+
this.container.addEventListener('keydown', this.onKeyPress);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get enable() {
|
|
120
|
+
return this._enable;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
set enable(value) {
|
|
124
|
+
this._enable = value;
|
|
125
|
+
} // ---- static func ----
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
static getFocusableElements(node) {
|
|
129
|
+
if (!isHTMLElement(node)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
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'])"];
|
|
134
|
+
const focusableSelectorsStr = focusableSelectorsList.join(','); // we are not filtered elements which are invisible
|
|
135
|
+
|
|
136
|
+
return _Array$from(node.querySelectorAll(focusableSelectorsStr));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static getActiveElement() {
|
|
140
|
+
return document ? document.activeElement : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default FocusTrapHandle;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@douyinfe/semi-foundation",
|
|
3
|
-
"version": "2.12.0-
|
|
3
|
+
"version": "2.12.0-beta.2",
|
|
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": "
|
|
11
|
+
"@douyinfe/semi-animation": "2.12.0-beta.2",
|
|
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": "d594b54331194b982f24ef179147622642073c3d",
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@babel/plugin-proposal-decorators": "^7.15.8",
|
|
30
30
|
"@babel/plugin-transform-runtime": "^7.15.8",
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { isHTMLElement } from "@douyinfe/semi-foundation/utils/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
|
+
}
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Usage:
|
|
14
|
+
* // Eg1: Pass a dom as the tab tarp container.
|
|
15
|
+
* const handle = new FocusTrapHandle(container, { enable: true });
|
|
16
|
+
*
|
|
17
|
+
* // 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.
|
|
18
|
+
* handle.addFocusRedirectListener((e)=>{
|
|
19
|
+
* return true; // return false to prevent redirect on target DOM;
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Eg3: Set it to false in order to disable tab tarp at any moment;
|
|
23
|
+
* handle.enable = true;
|
|
24
|
+
*
|
|
25
|
+
* // Eg4: Destroy instance when component is unmounting for saving resource;
|
|
26
|
+
* handle.destroy();
|
|
27
|
+
*
|
|
28
|
+
* */
|
|
29
|
+
|
|
30
|
+
class FocusTrapHandle {
|
|
31
|
+
public container: HTMLElement;
|
|
32
|
+
private options: HandleOptions;
|
|
33
|
+
private focusRedirectListenerList: FocusRedirectListener[];
|
|
34
|
+
private _enable: boolean;
|
|
35
|
+
|
|
36
|
+
constructor(container: HTMLElement, options?: HandleOptions) {
|
|
37
|
+
Object.freeze(options); // prevent user to change options after init;
|
|
38
|
+
this.container = container;
|
|
39
|
+
this.options = options;
|
|
40
|
+
this.enable = options?.enable ?? true;
|
|
41
|
+
this.focusRedirectListenerList = (() => {
|
|
42
|
+
if (options?.onFocusRedirectListener) {
|
|
43
|
+
return Array.isArray(options.onFocusRedirectListener) ? [...options.onFocusRedirectListener] : [options.onFocusRedirectListener];
|
|
44
|
+
} else {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
this.container.addEventListener('keydown', this.onKeyPress);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public addFocusRedirectListener = (listener: FocusRedirectListener) => {
|
|
52
|
+
this.focusRedirectListenerList.push(listener);
|
|
53
|
+
return () => this.removeFocusRedirectListener(listener);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public removeFocusRedirectListener = (listener: FocusRedirectListener) => {
|
|
57
|
+
this.focusRedirectListenerList = without(this.focusRedirectListenerList, listener);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public get enable() {
|
|
61
|
+
return this._enable;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public set enable(value) {
|
|
65
|
+
this._enable = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public destroy = () => {
|
|
69
|
+
this.container?.removeEventListener('keydown', this.onKeyPress);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- private func ----
|
|
73
|
+
|
|
74
|
+
private shouldFocusRedirect = (element: HTMLElement) => {
|
|
75
|
+
if (!this.enable) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
for (const listener of this.focusRedirectListenerList) {
|
|
79
|
+
const should = listener(element);
|
|
80
|
+
if (!should) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private focusElement = (element: HTMLElement, event: KeyboardEvent) => {
|
|
88
|
+
element?.focus();
|
|
89
|
+
event.preventDefault(); // prevent browser default tab move behavior
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
private onKeyPress = (event: KeyboardEvent) => {
|
|
94
|
+
if (event && event.key === 'Tab') {
|
|
95
|
+
const focusableElements = FocusTrapHandle.getFocusableElements(this.container);
|
|
96
|
+
const focusableNum = focusableElements.length;
|
|
97
|
+
if (focusableNum) {
|
|
98
|
+
// Shift + Tab will move focus backward
|
|
99
|
+
if (event.shiftKey) {
|
|
100
|
+
this.handleContainerShiftTabKeyDown(focusableElements, event);
|
|
101
|
+
} else {
|
|
102
|
+
this.handleContainerTabKeyDown(focusableElements, event);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private handleContainerTabKeyDown = (focusableElements: any[], event: any) => {
|
|
109
|
+
const activeElement = FocusTrapHandle.getActiveElement();
|
|
110
|
+
const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
|
|
111
|
+
|
|
112
|
+
const redirectForcingElement = focusableElements[0];
|
|
113
|
+
if (isLastCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
|
|
114
|
+
this.focusElement(redirectForcingElement, event);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
private handleContainerShiftTabKeyDown = (focusableElements: any[], event: KeyboardEvent) => {
|
|
120
|
+
const activeElement = FocusTrapHandle.getActiveElement();
|
|
121
|
+
const isFirstCurrentFocus = focusableElements[0] === activeElement;
|
|
122
|
+
const redirectForcingElement = focusableElements[focusableElements.length - 1];
|
|
123
|
+
if (isFirstCurrentFocus && this.shouldFocusRedirect(redirectForcingElement)) {
|
|
124
|
+
this.focusElement(redirectForcingElement, event);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
// ---- static func ----
|
|
130
|
+
|
|
131
|
+
static getFocusableElements(node: HTMLElement) {
|
|
132
|
+
if (!isHTMLElement(node)) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const focusableSelectorsList = [
|
|
136
|
+
"input:not([disabled]):not([tabindex='-1'])",
|
|
137
|
+
"textarea:not([disabled]):not([tabindex='-1'])",
|
|
138
|
+
"button:not([disabled]):not([tabindex='-1'])",
|
|
139
|
+
"a[href]:not([tabindex='-1'])",
|
|
140
|
+
"select:not([disabled]):not([tabindex='-1'])",
|
|
141
|
+
"area[href]:not([tabindex='-1'])",
|
|
142
|
+
"iframe:not([tabindex='-1'])",
|
|
143
|
+
"object:not([tabindex='-1'])",
|
|
144
|
+
"*[tabindex]:not([tabindex='-1'])",
|
|
145
|
+
"*[contenteditable]:not([tabindex='-1'])",
|
|
146
|
+
];
|
|
147
|
+
const focusableSelectorsStr = focusableSelectorsList.join(',');
|
|
148
|
+
// we are not filtered elements which are invisible
|
|
149
|
+
return Array.from(node.querySelectorAll<HTMLElement>(focusableSelectorsStr));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static getActiveElement(): HTMLElement | null {
|
|
153
|
+
return document ? document.activeElement as HTMLElement : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default FocusTrapHandle;
|