focus-trap 2.4.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +25 -7
- package/dist/focus-trap.js +504 -0
- package/dist/focus-trap.min.js +1 -0
- package/index.js +149 -125
- package/package.json +19 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
- **Breaking (kind of):** Update Tabbable to detect more elements and be more careful with radio buttons (see [Tabbable's changelog](https://github.com/davidtheclark/tabbable/blob/master/CHANGELOG.md)).
|
|
6
|
+
- **Breaking (kind of):** If `clickOutsideDeactivates` and `returnFocusOnDeactivate` are both `true`, focus will be returned to the pre-trap element only if the clicked element is not focusable.
|
|
7
|
+
|
|
8
|
+
## 2.4.6
|
|
9
|
+
|
|
10
|
+
- Add slight delay before moving focus to the first element in the trap.
|
|
11
|
+
This should prevent an occasional bug caused when the first element in the trap will close the trap if it picks up on the event that triggered the trap's opening.
|
|
12
|
+
|
|
13
|
+
## 2.4.5
|
|
14
|
+
|
|
15
|
+
- Fix `"main"` field in `package.json`.
|
|
16
|
+
|
|
17
|
+
## 2.4.4
|
|
18
|
+
|
|
19
|
+
- Publish UMD build so people can download it from `unpkg.com`.
|
|
20
|
+
|
|
3
21
|
## 2.4.3
|
|
4
22
|
|
|
5
23
|
- Fixed: TypeScript signature for `activate` function.
|
package/README.md
CHANGED
|
@@ -4,16 +4,14 @@ Trap focus within a DOM node.
|
|
|
4
4
|
|
|
5
5
|
There may come a time when you find it important to trap focus within a DOM node — so that when a user hits `Tab` or `Shift+Tab` or clicks around, she can't escape a certain cycle of focusable elements.
|
|
6
6
|
|
|
7
|
-
You will definitely face this challenge when you are try to build **accessible modals
|
|
7
|
+
You will definitely face this challenge when you are try to build **accessible modals**.
|
|
8
8
|
|
|
9
|
-
This module is a little **vanilla JS** solution to that problem.
|
|
9
|
+
This module is a little, modular **vanilla JS** solution to that problem.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Use it in your higher-level components. For example, if you are using React check out [focus-trap-react](https://github.com/davidtheclark/focus-trap-react), a light wrapper around this library. If you are not a React user, consider creating light wrappers in your framework-of-choice.
|
|
12
12
|
|
|
13
13
|
## What it does
|
|
14
14
|
|
|
15
|
-
[Check out the demos.](http://davidtheclark.github.io/focus-trap/demo/)
|
|
16
|
-
|
|
17
15
|
When a focus trap is activated, this is what should happen:
|
|
18
16
|
|
|
19
17
|
- Some element within the focus trap receives focus. By default, this will be the first element in the focus trap's tab order (as determined by [tabbable](https://github.com/davidtheclark/tabbable)). Alternately, you can specify an element that should receive this initial focus.
|
|
@@ -26,6 +24,8 @@ When the focus trap is deactivated, this is what should happen:
|
|
|
26
24
|
- Focus is passed to *whichever element had focus when the trap was activated* (e.g. the button that opened the modal or menu).
|
|
27
25
|
- Tabbing and clicking behave normally everywhere.
|
|
28
26
|
|
|
27
|
+
[Check out the demos.](http://davidtheclark.github.io/focus-trap/demo/)
|
|
28
|
+
|
|
29
29
|
For more advanced usage (e.g. focus traps within focus traps), you can also pause a focus trap's behavior without deactivating it entirely, then unpause at will.
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
@@ -34,6 +34,8 @@ For more advanced usage (e.g. focus traps within focus traps), you can also paus
|
|
|
34
34
|
npm install focus-trap
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
You can also use a UMD version published to `unpkg.com` as `dist/focus-trap.js` and `dist/focus-trap.min.js`.
|
|
38
|
+
|
|
37
39
|
## Browser Support
|
|
38
40
|
|
|
39
41
|
IE9+
|
|
@@ -119,7 +121,7 @@ Returns the `focusTrap`.
|
|
|
119
121
|
|
|
120
122
|
## Examples
|
|
121
123
|
|
|
122
|
-
Read code in `demo/`
|
|
124
|
+
Read code in `demo/` and [see how it works](http://davidtheclark.github.io/focus-trap/demo/).
|
|
123
125
|
|
|
124
126
|
Here's what happens in `demo-one.js`:
|
|
125
127
|
|
|
@@ -145,7 +147,23 @@ document.getElementById('deactivate-one').addEventListener('click', function ()
|
|
|
145
147
|
|
|
146
148
|
## Other details
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
### One at a time
|
|
151
|
+
|
|
152
|
+
*Only one focus trap can be listening at a time.* So if you want two focus traps active at a time, one of them has to be paused.
|
|
153
|
+
|
|
154
|
+
### Use predictable elements for the first and last tabbable elements in your trap
|
|
155
|
+
|
|
156
|
+
The focus trap will work best if the *first* and *last* focusable elements in your trap are simple elements that all browsers treat the same, like buttons and inputs.**
|
|
157
|
+
|
|
158
|
+
Tabbing will work as expected with trickier, less predictable elements — like iframes, shadow trees, audio and video elements, etc. — as long as they are *between* more predictable elements (that is, if they are not the first or last tabbable element in the trap).
|
|
159
|
+
|
|
160
|
+
This limitation is ultimately rooted in browser inconsistencies and inadequacies, but it comes to focus-trap through its dependency [Tababble](https://github.com/davidtheclark/tabbable). You can read about more details [in the Tabbable documentation](https://github.com/davidtheclark/tabbable#more-details).
|
|
161
|
+
|
|
162
|
+
### Your trap should include a tabbable element or a focusable container
|
|
163
|
+
|
|
164
|
+
You can't have a focus trap without focus, so an error will be thrown if you try to initialize focus-trap with an element that contains no tabbable nodes.
|
|
165
|
+
|
|
166
|
+
If you find yourself in this situation, you should give you container `tabindex="-1"` and set it as `initialFocus` or `fallbackFocus`. A couple of demos illustrate this.
|
|
149
167
|
|
|
150
168
|
## Development
|
|
151
169
|
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.focusTrap = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
|
2
|
+
var tabbable = require('tabbable');
|
|
3
|
+
var xtend = require('xtend');
|
|
4
|
+
|
|
5
|
+
var listeningFocusTrap = null;
|
|
6
|
+
|
|
7
|
+
function focusTrap(element, userOptions) {
|
|
8
|
+
var doc = document;
|
|
9
|
+
var container =
|
|
10
|
+
typeof element === 'string' ? doc.querySelector(element) : element;
|
|
11
|
+
|
|
12
|
+
var config = xtend(
|
|
13
|
+
{
|
|
14
|
+
returnFocusOnDeactivate: true,
|
|
15
|
+
escapeDeactivates: true
|
|
16
|
+
},
|
|
17
|
+
userOptions
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
var state = {
|
|
21
|
+
firstTabbableNode: null,
|
|
22
|
+
lastTabbableNode: null,
|
|
23
|
+
nodeFocusedBeforeActivation: null,
|
|
24
|
+
mostRecentlyFocusedNode: null,
|
|
25
|
+
active: false,
|
|
26
|
+
paused: false
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
var trap = {
|
|
30
|
+
activate: activate,
|
|
31
|
+
deactivate: deactivate,
|
|
32
|
+
pause: pause,
|
|
33
|
+
unpause: unpause
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return trap;
|
|
37
|
+
|
|
38
|
+
function activate(activateOptions) {
|
|
39
|
+
if (state.active) return;
|
|
40
|
+
|
|
41
|
+
updateTabbableNodes();
|
|
42
|
+
|
|
43
|
+
state.active = true;
|
|
44
|
+
state.paused = false;
|
|
45
|
+
state.nodeFocusedBeforeActivation = doc.activeElement;
|
|
46
|
+
|
|
47
|
+
var onActivate =
|
|
48
|
+
activateOptions && activateOptions.onActivate
|
|
49
|
+
? activateOptions.onActivate
|
|
50
|
+
: config.onActivate;
|
|
51
|
+
if (onActivate) {
|
|
52
|
+
onActivate();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
addListeners();
|
|
56
|
+
return trap;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function deactivate(deactivateOptions) {
|
|
60
|
+
if (!state.active) return;
|
|
61
|
+
|
|
62
|
+
removeListeners();
|
|
63
|
+
state.active = false;
|
|
64
|
+
state.paused = false;
|
|
65
|
+
|
|
66
|
+
var onDeactivate =
|
|
67
|
+
deactivateOptions && deactivateOptions.onDeactivate !== undefined
|
|
68
|
+
? deactivateOptions.onDeactivate
|
|
69
|
+
: config.onDeactivate;
|
|
70
|
+
if (onDeactivate) {
|
|
71
|
+
onDeactivate();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var returnFocus =
|
|
75
|
+
deactivateOptions && deactivateOptions.returnFocus !== undefined
|
|
76
|
+
? deactivateOptions.returnFocus
|
|
77
|
+
: config.returnFocusOnDeactivate;
|
|
78
|
+
if (returnFocus) {
|
|
79
|
+
delay(function() {
|
|
80
|
+
tryFocus(state.nodeFocusedBeforeActivation);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return trap;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pause() {
|
|
88
|
+
if (state.paused || !state.active) return;
|
|
89
|
+
state.paused = true;
|
|
90
|
+
removeListeners();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function unpause() {
|
|
94
|
+
if (!state.paused || !state.active) return;
|
|
95
|
+
state.paused = false;
|
|
96
|
+
addListeners();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function addListeners() {
|
|
100
|
+
if (!state.active) return;
|
|
101
|
+
|
|
102
|
+
// There can be only one listening focus trap at a time
|
|
103
|
+
if (listeningFocusTrap) {
|
|
104
|
+
listeningFocusTrap.pause();
|
|
105
|
+
}
|
|
106
|
+
listeningFocusTrap = trap;
|
|
107
|
+
|
|
108
|
+
updateTabbableNodes();
|
|
109
|
+
|
|
110
|
+
// Delay ensures that the focused element doesn't capture the event
|
|
111
|
+
// that caused the focus trap activation.
|
|
112
|
+
delay(function() {
|
|
113
|
+
tryFocus(getInitialFocusNode());
|
|
114
|
+
});
|
|
115
|
+
doc.addEventListener('focusin', checkFocusIn, true);
|
|
116
|
+
doc.addEventListener('mousedown', checkPointerDown, true);
|
|
117
|
+
doc.addEventListener('touchstart', checkPointerDown, true);
|
|
118
|
+
doc.addEventListener('click', checkClick, true);
|
|
119
|
+
doc.addEventListener('keydown', checkKey, true);
|
|
120
|
+
|
|
121
|
+
return trap;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function removeListeners() {
|
|
125
|
+
if (!state.active || listeningFocusTrap !== trap) return;
|
|
126
|
+
|
|
127
|
+
doc.removeEventListener('focusin', checkFocusIn, true);
|
|
128
|
+
doc.removeEventListener('mousedown', checkPointerDown, true);
|
|
129
|
+
doc.removeEventListener('touchstart', checkPointerDown, true);
|
|
130
|
+
doc.removeEventListener('click', checkClick, true);
|
|
131
|
+
doc.removeEventListener('keydown', checkKey, true);
|
|
132
|
+
|
|
133
|
+
listeningFocusTrap = null;
|
|
134
|
+
|
|
135
|
+
return trap;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getNodeForOption(optionName) {
|
|
139
|
+
var optionValue = config[optionName];
|
|
140
|
+
var node = optionValue;
|
|
141
|
+
if (!optionValue) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (typeof optionValue === 'string') {
|
|
145
|
+
node = doc.querySelector(optionValue);
|
|
146
|
+
if (!node) {
|
|
147
|
+
throw new Error('`' + optionName + '` refers to no known node');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (typeof optionValue === 'function') {
|
|
151
|
+
node = optionValue();
|
|
152
|
+
if (!node) {
|
|
153
|
+
throw new Error('`' + optionName + '` did not return a node');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return node;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getInitialFocusNode() {
|
|
160
|
+
var node;
|
|
161
|
+
if (getNodeForOption('initialFocus') !== null) {
|
|
162
|
+
node = getNodeForOption('initialFocus');
|
|
163
|
+
} else if (container.contains(doc.activeElement)) {
|
|
164
|
+
node = doc.activeElement;
|
|
165
|
+
} else {
|
|
166
|
+
node = state.firstTabbableNode || getNodeForOption('fallbackFocus');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!node) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
"You can't have a focus-trap without at least one focusable element"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return node;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// This needs to be done on mousedown and touchstart instead of click
|
|
179
|
+
// so that it precedes the focus event.
|
|
180
|
+
function checkPointerDown(e) {
|
|
181
|
+
if (container.contains(e.target)) return;
|
|
182
|
+
if (config.clickOutsideDeactivates) {
|
|
183
|
+
deactivate({
|
|
184
|
+
returnFocus: !tabbable.isFocusable(e.target)
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// In case focus escapes the trap for some strange reason, pull it back in.
|
|
192
|
+
function checkFocusIn(e) {
|
|
193
|
+
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
|
194
|
+
if (container.contains(e.target) || e.target instanceof Document) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
e.stopImmediatePropagation();
|
|
198
|
+
tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function checkKey(e) {
|
|
202
|
+
if (config.escapeDeactivates !== false && isEscapeEvent(e)) {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
deactivate();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (isTabEvent(e)) {
|
|
208
|
+
checkTab(e);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Hijack Tab events on the first and last focusable nodes of the trap,
|
|
214
|
+
// in order to prevent focus from escaping. If it escapes for even a
|
|
215
|
+
// moment it can end up scrolling the page and causing confusion so we
|
|
216
|
+
// kind of need to capture the action at the keydown phase.
|
|
217
|
+
function checkTab(e) {
|
|
218
|
+
updateTabbableNodes();
|
|
219
|
+
if (e.shiftKey && e.target === state.firstTabbableNode) {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
tryFocus(state.lastTabbableNode);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!e.shiftKey && e.target === state.lastTabbableNode) {
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
tryFocus(state.firstTabbableNode);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function checkClick(e) {
|
|
232
|
+
if (config.clickOutsideDeactivates) return;
|
|
233
|
+
if (container.contains(e.target)) return;
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
e.stopImmediatePropagation();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function updateTabbableNodes() {
|
|
239
|
+
var tabbableNodes = tabbable(container);
|
|
240
|
+
state.firstTabbableNode = tabbableNodes[0] || getInitialFocusNode();
|
|
241
|
+
state.lastTabbableNode =
|
|
242
|
+
tabbableNodes[tabbableNodes.length - 1] || getInitialFocusNode();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function tryFocus(node) {
|
|
246
|
+
if (node === doc.activeElement) return;
|
|
247
|
+
if (!node || !node.focus) {
|
|
248
|
+
tryFocus(getInitialFocusNode());
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
node.focus();
|
|
253
|
+
state.mostRecentlyFocusedNode = node;
|
|
254
|
+
if (isSelectableInput(node)) {
|
|
255
|
+
node.select();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isSelectableInput(node) {
|
|
261
|
+
return (
|
|
262
|
+
node.tagName &&
|
|
263
|
+
node.tagName.toLowerCase() === 'input' &&
|
|
264
|
+
typeof node.select === 'function'
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isEscapeEvent(e) {
|
|
269
|
+
return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isTabEvent(e) {
|
|
273
|
+
return e.key === 'Tab' || e.keyCode === 9;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function delay(fn) {
|
|
277
|
+
return setTimeout(fn, 0);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = focusTrap;
|
|
281
|
+
|
|
282
|
+
},{"tabbable":2,"xtend":3}],2:[function(require,module,exports){
|
|
283
|
+
var candidateSelectors = [
|
|
284
|
+
'input',
|
|
285
|
+
'select',
|
|
286
|
+
'textarea',
|
|
287
|
+
'a[href]',
|
|
288
|
+
'button',
|
|
289
|
+
'[tabindex]',
|
|
290
|
+
'audio[controls]',
|
|
291
|
+
'video[controls]',
|
|
292
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
293
|
+
];
|
|
294
|
+
var candidateSelector = candidateSelectors.join(',');
|
|
295
|
+
|
|
296
|
+
var matches = Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
|
297
|
+
|
|
298
|
+
function tabbable(el, options) {
|
|
299
|
+
options = options || {};
|
|
300
|
+
|
|
301
|
+
var elementDocument = el.ownerDocument || el;
|
|
302
|
+
var regularTabbables = [];
|
|
303
|
+
var orderedTabbables = [];
|
|
304
|
+
|
|
305
|
+
var untouchabilityChecker = new UntouchabilityChecker(elementDocument);
|
|
306
|
+
var candidates = el.querySelectorAll(candidateSelector);
|
|
307
|
+
|
|
308
|
+
if (options.includeContainer) {
|
|
309
|
+
if (matches.call(el, candidateSelector)) {
|
|
310
|
+
candidates = Array.prototype.slice.apply(candidates);
|
|
311
|
+
candidates.unshift(el);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
var i, candidate, candidateTabindex;
|
|
316
|
+
for (i = 0; i < candidates.length; i++) {
|
|
317
|
+
candidate = candidates[i];
|
|
318
|
+
|
|
319
|
+
if (!isNodeMatchingSelectorTabbable(candidate, untouchabilityChecker)) continue;
|
|
320
|
+
|
|
321
|
+
candidateTabindex = getTabindex(candidate);
|
|
322
|
+
if (candidateTabindex === 0) {
|
|
323
|
+
regularTabbables.push(candidate);
|
|
324
|
+
} else {
|
|
325
|
+
orderedTabbables.push({
|
|
326
|
+
documentOrder: i,
|
|
327
|
+
tabIndex: candidateTabindex,
|
|
328
|
+
node: candidate,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
var tabbableNodes = orderedTabbables
|
|
334
|
+
.sort(sortOrderedTabbables)
|
|
335
|
+
.map(function(a) { return a.node })
|
|
336
|
+
.concat(regularTabbables);
|
|
337
|
+
|
|
338
|
+
return tabbableNodes;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
tabbable.isTabbable = isTabbable;
|
|
342
|
+
tabbable.isFocusable = isFocusable;
|
|
343
|
+
|
|
344
|
+
function isNodeMatchingSelectorTabbable(node, untouchabilityChecker) {
|
|
345
|
+
if (
|
|
346
|
+
!isNodeMatchingSelectorFocusable(node, untouchabilityChecker)
|
|
347
|
+
|| isNonTabbableRadio(node)
|
|
348
|
+
|| getTabindex(node) < 0
|
|
349
|
+
) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function isTabbable(node, untouchabilityChecker) {
|
|
356
|
+
if (!node) throw new Error('No node provided');
|
|
357
|
+
if (matches.call(node, candidateSelector) === false) return false;
|
|
358
|
+
return isNodeMatchingSelectorTabbable(node, untouchabilityChecker);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isNodeMatchingSelectorFocusable(node, untouchabilityChecker) {
|
|
362
|
+
untouchabilityChecker = untouchabilityChecker || new UntouchabilityChecker(node.ownerDocument || node);
|
|
363
|
+
if (
|
|
364
|
+
node.disabled
|
|
365
|
+
|| isHiddenInput(node)
|
|
366
|
+
|| untouchabilityChecker.isUntouchable(node)
|
|
367
|
+
) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
var focusableCandidateSelector = candidateSelectors.concat('iframe').join(',');
|
|
374
|
+
function isFocusable(node, untouchabilityChecker) {
|
|
375
|
+
if (!node) throw new Error('No node provided');
|
|
376
|
+
if (matches.call(node, focusableCandidateSelector) === false) return false;
|
|
377
|
+
return isNodeMatchingSelectorFocusable(node, untouchabilityChecker);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function getTabindex(node) {
|
|
381
|
+
var tabindexAttr = parseInt(node.getAttribute('tabindex'), 10);
|
|
382
|
+
if (!isNaN(tabindexAttr)) return tabindexAttr;
|
|
383
|
+
// Browsers do not return `tabIndex` correctly for contentEditable nodes;
|
|
384
|
+
// so if they don't have a tabindex attribute specifically set, assume it's 0.
|
|
385
|
+
if (isContentEditable(node)) return 0;
|
|
386
|
+
return node.tabIndex;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function sortOrderedTabbables(a, b) {
|
|
390
|
+
return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Array.prototype.find not available in IE.
|
|
394
|
+
function find(list, predicate) {
|
|
395
|
+
for (var i = 0, length = list.length; i < length; i++) {
|
|
396
|
+
if (predicate(list[i])) return list[i];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isContentEditable(node) {
|
|
401
|
+
return node.contentEditable === 'true';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isInput(node) {
|
|
405
|
+
return node.tagName === 'INPUT';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function isHiddenInput(node) {
|
|
409
|
+
return isInput(node) && node.type === 'hidden';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isRadio(node) {
|
|
413
|
+
return isInput(node) && node.type === 'radio';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isNonTabbableRadio(node) {
|
|
417
|
+
return isRadio(node) && !isTabbableRadio(node);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function getCheckedRadio(nodes) {
|
|
421
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
422
|
+
if (nodes[i].checked) {
|
|
423
|
+
return nodes[i];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isTabbableRadio(node) {
|
|
429
|
+
if (!node.name) return true;
|
|
430
|
+
// This won't account for the edge case where you have radio groups with the same
|
|
431
|
+
// in separate forms on the same page.
|
|
432
|
+
var radioSet = node.ownerDocument.querySelectorAll('input[type="radio"][name="' + node.name + '"]');
|
|
433
|
+
var checked = getCheckedRadio(radioSet);
|
|
434
|
+
return !checked || checked === node;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// An element is "untouchable" if *it or one of its ancestors* has
|
|
438
|
+
// `visibility: hidden` or `display: none`.
|
|
439
|
+
function UntouchabilityChecker(elementDocument) {
|
|
440
|
+
this.doc = elementDocument;
|
|
441
|
+
// Node cache must be refreshed on every check, in case
|
|
442
|
+
// the content of the element has changed. The cache contains tuples
|
|
443
|
+
// mapping nodes to their boolean result.
|
|
444
|
+
this.cache = [];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// getComputedStyle accurately reflects `visibility: hidden` of ancestors
|
|
448
|
+
// but not `display: none`, so we need to recursively check parents.
|
|
449
|
+
UntouchabilityChecker.prototype.hasDisplayNone = function hasDisplayNone(node, nodeComputedStyle) {
|
|
450
|
+
if (node === this.doc.documentElement) return false;
|
|
451
|
+
|
|
452
|
+
// Search for a cached result.
|
|
453
|
+
var cached = find(this.cache, function(item) {
|
|
454
|
+
return item === node;
|
|
455
|
+
});
|
|
456
|
+
if (cached) return cached[1];
|
|
457
|
+
|
|
458
|
+
nodeComputedStyle = nodeComputedStyle || this.doc.defaultView.getComputedStyle(node);
|
|
459
|
+
|
|
460
|
+
var result = false;
|
|
461
|
+
|
|
462
|
+
if (nodeComputedStyle.display === 'none') {
|
|
463
|
+
result = true;
|
|
464
|
+
} else if (node.parentNode) {
|
|
465
|
+
result = this.hasDisplayNone(node.parentNode);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this.cache.push([node, result]);
|
|
469
|
+
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
UntouchabilityChecker.prototype.isUntouchable = function isUntouchable(node) {
|
|
474
|
+
if (node === this.doc.documentElement) return false;
|
|
475
|
+
var computedStyle = this.doc.defaultView.getComputedStyle(node);
|
|
476
|
+
if (this.hasDisplayNone(node, computedStyle)) return true;
|
|
477
|
+
return computedStyle.visibility === 'hidden';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
module.exports = tabbable;
|
|
481
|
+
|
|
482
|
+
},{}],3:[function(require,module,exports){
|
|
483
|
+
module.exports = extend
|
|
484
|
+
|
|
485
|
+
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
486
|
+
|
|
487
|
+
function extend() {
|
|
488
|
+
var target = {}
|
|
489
|
+
|
|
490
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
491
|
+
var source = arguments[i]
|
|
492
|
+
|
|
493
|
+
for (var key in source) {
|
|
494
|
+
if (hasOwnProperty.call(source, key)) {
|
|
495
|
+
target[key] = source[key]
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return target
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
},{}]},{},[1])(1)
|
|
504
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.focusTrap=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r}()({1:[function(require,module,exports){var tabbable=require("tabbable");var xtend=require("xtend");var listeningFocusTrap=null;function focusTrap(element,userOptions){var doc=document;var container=typeof element==="string"?doc.querySelector(element):element;var config=xtend({returnFocusOnDeactivate:true,escapeDeactivates:true},userOptions);var state={firstTabbableNode:null,lastTabbableNode:null,nodeFocusedBeforeActivation:null,mostRecentlyFocusedNode:null,active:false,paused:false};var trap={activate:activate,deactivate:deactivate,pause:pause,unpause:unpause};return trap;function activate(activateOptions){if(state.active)return;updateTabbableNodes();state.active=true;state.paused=false;state.nodeFocusedBeforeActivation=doc.activeElement;var onActivate=activateOptions&&activateOptions.onActivate?activateOptions.onActivate:config.onActivate;if(onActivate){onActivate()}addListeners();return trap}function deactivate(deactivateOptions){if(!state.active)return;removeListeners();state.active=false;state.paused=false;var onDeactivate=deactivateOptions&&deactivateOptions.onDeactivate!==undefined?deactivateOptions.onDeactivate:config.onDeactivate;if(onDeactivate){onDeactivate()}var returnFocus=deactivateOptions&&deactivateOptions.returnFocus!==undefined?deactivateOptions.returnFocus:config.returnFocusOnDeactivate;if(returnFocus){delay(function(){tryFocus(state.nodeFocusedBeforeActivation)})}return trap}function pause(){if(state.paused||!state.active)return;state.paused=true;removeListeners()}function unpause(){if(!state.paused||!state.active)return;state.paused=false;addListeners()}function addListeners(){if(!state.active)return;if(listeningFocusTrap){listeningFocusTrap.pause()}listeningFocusTrap=trap;updateTabbableNodes();delay(function(){tryFocus(getInitialFocusNode())});doc.addEventListener("focusin",checkFocusIn,true);doc.addEventListener("mousedown",checkPointerDown,true);doc.addEventListener("touchstart",checkPointerDown,true);doc.addEventListener("click",checkClick,true);doc.addEventListener("keydown",checkKey,true);return trap}function removeListeners(){if(!state.active||listeningFocusTrap!==trap)return;doc.removeEventListener("focusin",checkFocusIn,true);doc.removeEventListener("mousedown",checkPointerDown,true);doc.removeEventListener("touchstart",checkPointerDown,true);doc.removeEventListener("click",checkClick,true);doc.removeEventListener("keydown",checkKey,true);listeningFocusTrap=null;return trap}function getNodeForOption(optionName){var optionValue=config[optionName];var node=optionValue;if(!optionValue){return null}if(typeof optionValue==="string"){node=doc.querySelector(optionValue);if(!node){throw new Error("`"+optionName+"` refers to no known node")}}if(typeof optionValue==="function"){node=optionValue();if(!node){throw new Error("`"+optionName+"` did not return a node")}}return node}function getInitialFocusNode(){var node;if(getNodeForOption("initialFocus")!==null){node=getNodeForOption("initialFocus")}else if(container.contains(doc.activeElement)){node=doc.activeElement}else{node=state.firstTabbableNode||getNodeForOption("fallbackFocus")}if(!node){throw new Error("You can't have a focus-trap without at least one focusable element")}return node}function checkPointerDown(e){if(container.contains(e.target))return;if(config.clickOutsideDeactivates){deactivate({returnFocus:!tabbable.isFocusable(e.target)})}else{e.preventDefault()}}function checkFocusIn(e){if(container.contains(e.target)||e.target instanceof Document){return}e.stopImmediatePropagation();tryFocus(state.mostRecentlyFocusedNode||getInitialFocusNode())}function checkKey(e){if(config.escapeDeactivates!==false&&isEscapeEvent(e)){e.preventDefault();deactivate();return}if(isTabEvent(e)){checkTab(e);return}}function checkTab(e){updateTabbableNodes();if(e.shiftKey&&e.target===state.firstTabbableNode){e.preventDefault();tryFocus(state.lastTabbableNode);return}if(!e.shiftKey&&e.target===state.lastTabbableNode){e.preventDefault();tryFocus(state.firstTabbableNode);return}}function checkClick(e){if(config.clickOutsideDeactivates)return;if(container.contains(e.target))return;e.preventDefault();e.stopImmediatePropagation()}function updateTabbableNodes(){var tabbableNodes=tabbable(container);state.firstTabbableNode=tabbableNodes[0]||getInitialFocusNode();state.lastTabbableNode=tabbableNodes[tabbableNodes.length-1]||getInitialFocusNode()}function tryFocus(node){if(node===doc.activeElement)return;if(!node||!node.focus){tryFocus(getInitialFocusNode());return}node.focus();state.mostRecentlyFocusedNode=node;if(isSelectableInput(node)){node.select()}}}function isSelectableInput(node){return node.tagName&&node.tagName.toLowerCase()==="input"&&typeof node.select==="function"}function isEscapeEvent(e){return e.key==="Escape"||e.key==="Esc"||e.keyCode===27}function isTabEvent(e){return e.key==="Tab"||e.keyCode===9}function delay(fn){return setTimeout(fn,0)}module.exports=focusTrap},{tabbable:2,xtend:3}],2:[function(require,module,exports){var candidateSelectors=["input","select","textarea","a[href]","button","[tabindex]","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])'];var candidateSelector=candidateSelectors.join(",");var matches=Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector;function tabbable(el,options){options=options||{};var elementDocument=el.ownerDocument||el;var regularTabbables=[];var orderedTabbables=[];var untouchabilityChecker=new UntouchabilityChecker(elementDocument);var candidates=el.querySelectorAll(candidateSelector);if(options.includeContainer){if(matches.call(el,candidateSelector)){candidates=Array.prototype.slice.apply(candidates);candidates.unshift(el)}}var i,candidate,candidateTabindex;for(i=0;i<candidates.length;i++){candidate=candidates[i];if(!isNodeMatchingSelectorTabbable(candidate,untouchabilityChecker))continue;candidateTabindex=getTabindex(candidate);if(candidateTabindex===0){regularTabbables.push(candidate)}else{orderedTabbables.push({documentOrder:i,tabIndex:candidateTabindex,node:candidate})}}var tabbableNodes=orderedTabbables.sort(sortOrderedTabbables).map(function(a){return a.node}).concat(regularTabbables);return tabbableNodes}tabbable.isTabbable=isTabbable;tabbable.isFocusable=isFocusable;function isNodeMatchingSelectorTabbable(node,untouchabilityChecker){if(!isNodeMatchingSelectorFocusable(node,untouchabilityChecker)||isNonTabbableRadio(node)||getTabindex(node)<0){return false}return true}function isTabbable(node,untouchabilityChecker){if(!node)throw new Error("No node provided");if(matches.call(node,candidateSelector)===false)return false;return isNodeMatchingSelectorTabbable(node,untouchabilityChecker)}function isNodeMatchingSelectorFocusable(node,untouchabilityChecker){untouchabilityChecker=untouchabilityChecker||new UntouchabilityChecker(node.ownerDocument||node);if(node.disabled||isHiddenInput(node)||untouchabilityChecker.isUntouchable(node)){return false}return true}var focusableCandidateSelector=candidateSelectors.concat("iframe").join(",");function isFocusable(node,untouchabilityChecker){if(!node)throw new Error("No node provided");if(matches.call(node,focusableCandidateSelector)===false)return false;return isNodeMatchingSelectorFocusable(node,untouchabilityChecker)}function getTabindex(node){var tabindexAttr=parseInt(node.getAttribute("tabindex"),10);if(!isNaN(tabindexAttr))return tabindexAttr;if(isContentEditable(node))return 0;return node.tabIndex}function sortOrderedTabbables(a,b){return a.tabIndex===b.tabIndex?a.documentOrder-b.documentOrder:a.tabIndex-b.tabIndex}function find(list,predicate){for(var i=0,length=list.length;i<length;i++){if(predicate(list[i]))return list[i]}}function isContentEditable(node){return node.contentEditable==="true"}function isInput(node){return node.tagName==="INPUT"}function isHiddenInput(node){return isInput(node)&&node.type==="hidden"}function isRadio(node){return isInput(node)&&node.type==="radio"}function isNonTabbableRadio(node){return isRadio(node)&&!isTabbableRadio(node)}function getCheckedRadio(nodes){for(var i=0;i<nodes.length;i++){if(nodes[i].checked){return nodes[i]}}}function isTabbableRadio(node){if(!node.name)return true;var radioSet=node.ownerDocument.querySelectorAll('input[type="radio"][name="'+node.name+'"]');var checked=getCheckedRadio(radioSet);return!checked||checked===node}function UntouchabilityChecker(elementDocument){this.doc=elementDocument;this.cache=[]}UntouchabilityChecker.prototype.hasDisplayNone=function hasDisplayNone(node,nodeComputedStyle){if(node===this.doc.documentElement)return false;var cached=find(this.cache,function(item){return item===node});if(cached)return cached[1];nodeComputedStyle=nodeComputedStyle||this.doc.defaultView.getComputedStyle(node);var result=false;if(nodeComputedStyle.display==="none"){result=true}else if(node.parentNode){result=this.hasDisplayNone(node.parentNode)}this.cache.push([node,result]);return result};UntouchabilityChecker.prototype.isUntouchable=function isUntouchable(node){if(node===this.doc.documentElement)return false;var computedStyle=this.doc.defaultView.getComputedStyle(node);if(this.hasDisplayNone(node,computedStyle))return true;return computedStyle.visibility==="hidden"};module.exports=tabbable},{}],3:[function(require,module,exports){module.exports=extend;var hasOwnProperty=Object.prototype.hasOwnProperty;function extend(){var target={};for(var i=0;i<arguments.length;i++){var source=arguments[i];for(var key in source){if(hasOwnProperty.call(source,key)){target[key]=source[key]}}}return target}},{}]},{},[1])(1)});
|
package/index.js
CHANGED
|
@@ -1,52 +1,54 @@
|
|
|
1
1
|
var tabbable = require('tabbable');
|
|
2
|
+
var xtend = require('xtend');
|
|
2
3
|
|
|
3
4
|
var listeningFocusTrap = null;
|
|
4
5
|
|
|
5
6
|
function focusTrap(element, userOptions) {
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
:
|
|
7
|
+
var doc = document;
|
|
8
|
+
var container =
|
|
9
|
+
typeof element === 'string' ? doc.querySelector(element) : element;
|
|
10
|
+
|
|
11
|
+
var config = xtend(
|
|
12
|
+
{
|
|
13
|
+
returnFocusOnDeactivate: true,
|
|
14
|
+
escapeDeactivates: true
|
|
15
|
+
},
|
|
16
|
+
userOptions
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
var state = {
|
|
20
|
+
firstTabbableNode: null,
|
|
21
|
+
lastTabbableNode: null,
|
|
22
|
+
nodeFocusedBeforeActivation: null,
|
|
23
|
+
mostRecentlyFocusedNode: null,
|
|
24
|
+
active: false,
|
|
25
|
+
paused: false
|
|
26
|
+
};
|
|
25
27
|
|
|
26
28
|
var trap = {
|
|
27
29
|
activate: activate,
|
|
28
30
|
deactivate: deactivate,
|
|
29
31
|
pause: pause,
|
|
30
|
-
unpause: unpause
|
|
32
|
+
unpause: unpause
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
return trap;
|
|
34
36
|
|
|
35
37
|
function activate(activateOptions) {
|
|
36
|
-
if (active) return;
|
|
38
|
+
if (state.active) return;
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
onActivate: (activateOptions && activateOptions.onActivate !== undefined)
|
|
40
|
-
? activateOptions.onActivate
|
|
41
|
-
: config.onActivate,
|
|
42
|
-
};
|
|
40
|
+
updateTabbableNodes();
|
|
43
41
|
|
|
44
|
-
active = true;
|
|
45
|
-
paused = false;
|
|
46
|
-
nodeFocusedBeforeActivation =
|
|
42
|
+
state.active = true;
|
|
43
|
+
state.paused = false;
|
|
44
|
+
state.nodeFocusedBeforeActivation = doc.activeElement;
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
var onActivate =
|
|
47
|
+
activateOptions && activateOptions.onActivate
|
|
48
|
+
? activateOptions.onActivate
|
|
49
|
+
: config.onActivate;
|
|
50
|
+
if (onActivate) {
|
|
51
|
+
onActivate();
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
addListeners();
|
|
@@ -54,48 +56,47 @@ function focusTrap(element, userOptions) {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
function deactivate(deactivateOptions) {
|
|
57
|
-
if (!active) return;
|
|
58
|
-
|
|
59
|
-
var defaultedDeactivateOptions = {
|
|
60
|
-
returnFocus: (deactivateOptions && deactivateOptions.returnFocus !== undefined)
|
|
61
|
-
? deactivateOptions.returnFocus
|
|
62
|
-
: config.returnFocusOnDeactivate,
|
|
63
|
-
onDeactivate: (deactivateOptions && deactivateOptions.onDeactivate !== undefined)
|
|
64
|
-
? deactivateOptions.onDeactivate
|
|
65
|
-
: config.onDeactivate,
|
|
66
|
-
};
|
|
59
|
+
if (!state.active) return;
|
|
67
60
|
|
|
68
61
|
removeListeners();
|
|
62
|
+
state.active = false;
|
|
63
|
+
state.paused = false;
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
var onDeactivate =
|
|
66
|
+
deactivateOptions && deactivateOptions.onDeactivate !== undefined
|
|
67
|
+
? deactivateOptions.onDeactivate
|
|
68
|
+
: config.onDeactivate;
|
|
69
|
+
if (onDeactivate) {
|
|
70
|
+
onDeactivate();
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
var returnFocus =
|
|
74
|
+
deactivateOptions && deactivateOptions.returnFocus !== undefined
|
|
75
|
+
? deactivateOptions.returnFocus
|
|
76
|
+
: config.returnFocusOnDeactivate;
|
|
77
|
+
if (returnFocus) {
|
|
78
|
+
delay(function() {
|
|
79
|
+
tryFocus(state.nodeFocusedBeforeActivation);
|
|
80
|
+
});
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
paused = false;
|
|
82
|
-
return this;
|
|
83
|
+
return trap;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function pause() {
|
|
86
|
-
if (paused || !active) return;
|
|
87
|
-
paused = true;
|
|
87
|
+
if (state.paused || !state.active) return;
|
|
88
|
+
state.paused = true;
|
|
88
89
|
removeListeners();
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
function unpause() {
|
|
92
|
-
if (!paused || !active) return;
|
|
93
|
-
paused = false;
|
|
93
|
+
if (!state.paused || !state.active) return;
|
|
94
|
+
state.paused = false;
|
|
94
95
|
addListeners();
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function addListeners() {
|
|
98
|
-
if (!active) return;
|
|
99
|
+
if (!state.active) return;
|
|
99
100
|
|
|
100
101
|
// There can be only one listening focus trap at a time
|
|
101
102
|
if (listeningFocusTrap) {
|
|
@@ -104,24 +105,29 @@ function focusTrap(element, userOptions) {
|
|
|
104
105
|
listeningFocusTrap = trap;
|
|
105
106
|
|
|
106
107
|
updateTabbableNodes();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
|
|
109
|
+
// Delay ensures that the focused element doesn't capture the event
|
|
110
|
+
// that caused the focus trap activation.
|
|
111
|
+
delay(function() {
|
|
112
|
+
tryFocus(getInitialFocusNode());
|
|
113
|
+
});
|
|
114
|
+
doc.addEventListener('focusin', checkFocusIn, true);
|
|
115
|
+
doc.addEventListener('mousedown', checkPointerDown, true);
|
|
116
|
+
doc.addEventListener('touchstart', checkPointerDown, true);
|
|
117
|
+
doc.addEventListener('click', checkClick, true);
|
|
118
|
+
doc.addEventListener('keydown', checkKey, true);
|
|
113
119
|
|
|
114
120
|
return trap;
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
function removeListeners() {
|
|
118
|
-
if (!active || listeningFocusTrap !== trap) return;
|
|
124
|
+
if (!state.active || listeningFocusTrap !== trap) return;
|
|
119
125
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
doc.removeEventListener('focusin', checkFocusIn, true);
|
|
127
|
+
doc.removeEventListener('mousedown', checkPointerDown, true);
|
|
128
|
+
doc.removeEventListener('touchstart', checkPointerDown, true);
|
|
129
|
+
doc.removeEventListener('click', checkClick, true);
|
|
130
|
+
doc.removeEventListener('keydown', checkKey, true);
|
|
125
131
|
|
|
126
132
|
listeningFocusTrap = null;
|
|
127
133
|
|
|
@@ -135,7 +141,7 @@ function focusTrap(element, userOptions) {
|
|
|
135
141
|
return null;
|
|
136
142
|
}
|
|
137
143
|
if (typeof optionValue === 'string') {
|
|
138
|
-
node =
|
|
144
|
+
node = doc.querySelector(optionValue);
|
|
139
145
|
if (!node) {
|
|
140
146
|
throw new Error('`' + optionName + '` refers to no known node');
|
|
141
147
|
}
|
|
@@ -149,107 +155,125 @@ function focusTrap(element, userOptions) {
|
|
|
149
155
|
return node;
|
|
150
156
|
}
|
|
151
157
|
|
|
152
|
-
function
|
|
158
|
+
function getInitialFocusNode() {
|
|
153
159
|
var node;
|
|
154
160
|
if (getNodeForOption('initialFocus') !== null) {
|
|
155
161
|
node = getNodeForOption('initialFocus');
|
|
156
|
-
} else if (container.contains(
|
|
157
|
-
node =
|
|
162
|
+
} else if (container.contains(doc.activeElement)) {
|
|
163
|
+
node = doc.activeElement;
|
|
158
164
|
} else {
|
|
159
|
-
node =
|
|
165
|
+
node = state.firstTabbableNode || getNodeForOption('fallbackFocus');
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
if (!node) {
|
|
163
|
-
throw new Error(
|
|
169
|
+
throw new Error(
|
|
170
|
+
"You can't have a focus-trap without at least one focusable element"
|
|
171
|
+
);
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
return node;
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
// This needs to be done on mousedown and touchstart instead of click
|
|
170
|
-
// so that it precedes the focus event
|
|
178
|
+
// so that it precedes the focus event.
|
|
171
179
|
function checkPointerDown(e) {
|
|
172
|
-
if (config.clickOutsideDeactivates && !container.contains(e.target)) {
|
|
173
|
-
deactivate({ returnFocus: false });
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function checkClick(e) {
|
|
178
|
-
if (config.clickOutsideDeactivates) return;
|
|
179
180
|
if (container.contains(e.target)) return;
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
if (config.clickOutsideDeactivates) {
|
|
182
|
+
deactivate({
|
|
183
|
+
returnFocus: !tabbable.isFocusable(e.target)
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
}
|
|
182
188
|
}
|
|
183
189
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
e.
|
|
188
|
-
|
|
189
|
-
if (typeof e.target.blur === 'function') e.target.blur();
|
|
190
|
-
|
|
191
|
-
if (tabEvent) {
|
|
192
|
-
readjustFocus(tabEvent);
|
|
190
|
+
// In case focus escapes the trap for some strange reason, pull it back in.
|
|
191
|
+
function checkFocusIn(e) {
|
|
192
|
+
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
|
193
|
+
if (container.contains(e.target) || e.target instanceof Document) {
|
|
194
|
+
return;
|
|
193
195
|
}
|
|
196
|
+
e.stopImmediatePropagation();
|
|
197
|
+
tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
function checkKey(e) {
|
|
197
|
-
if (e.key === 'Tab' || e.keyCode === 9) {
|
|
198
|
-
handleTab(e);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
201
|
if (config.escapeDeactivates !== false && isEscapeEvent(e)) {
|
|
202
|
+
e.preventDefault();
|
|
202
203
|
deactivate();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (isTabEvent(e)) {
|
|
207
|
+
checkTab(e);
|
|
208
|
+
return;
|
|
203
209
|
}
|
|
204
210
|
}
|
|
205
211
|
|
|
206
|
-
|
|
212
|
+
// Hijack Tab events on the first and last focusable nodes of the trap,
|
|
213
|
+
// in order to prevent focus from escaping. If it escapes for even a
|
|
214
|
+
// moment it can end up scrolling the page and causing confusion so we
|
|
215
|
+
// kind of need to capture the action at the keydown phase.
|
|
216
|
+
function checkTab(e) {
|
|
207
217
|
updateTabbableNodes();
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
if (e.shiftKey && e.target === state.firstTabbableNode) {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
tryFocus(state.lastTabbableNode);
|
|
221
|
+
return;
|
|
211
222
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (e.shiftKey) {
|
|
217
|
-
if (e.target === firstTabbableNode || tabbableNodes.indexOf(e.target) === -1) {
|
|
218
|
-
return tryFocus(lastTabbableNode);
|
|
219
|
-
}
|
|
220
|
-
return tryFocus(tabbableNodes[currentFocusIndex - 1]);
|
|
223
|
+
if (!e.shiftKey && e.target === state.lastTabbableNode) {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
tryFocus(state.firstTabbableNode);
|
|
226
|
+
return;
|
|
221
227
|
}
|
|
228
|
+
}
|
|
222
229
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
function checkClick(e) {
|
|
231
|
+
if (config.clickOutsideDeactivates) return;
|
|
232
|
+
if (container.contains(e.target)) return;
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
e.stopImmediatePropagation();
|
|
226
235
|
}
|
|
227
236
|
|
|
228
237
|
function updateTabbableNodes() {
|
|
229
|
-
tabbableNodes = tabbable(container);
|
|
230
|
-
firstTabbableNode = tabbableNodes[0];
|
|
231
|
-
lastTabbableNode =
|
|
238
|
+
var tabbableNodes = tabbable(container);
|
|
239
|
+
state.firstTabbableNode = tabbableNodes[0] || getInitialFocusNode();
|
|
240
|
+
state.lastTabbableNode =
|
|
241
|
+
tabbableNodes[tabbableNodes.length - 1] || getInitialFocusNode();
|
|
232
242
|
}
|
|
233
243
|
|
|
234
|
-
function
|
|
235
|
-
if (
|
|
244
|
+
function tryFocus(node) {
|
|
245
|
+
if (node === doc.activeElement) return;
|
|
246
|
+
if (!node || !node.focus) {
|
|
247
|
+
tryFocus(getInitialFocusNode());
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
236
250
|
|
|
237
|
-
|
|
251
|
+
node.focus();
|
|
252
|
+
state.mostRecentlyFocusedNode = node;
|
|
253
|
+
if (isSelectableInput(node)) {
|
|
254
|
+
node.select();
|
|
255
|
+
}
|
|
238
256
|
}
|
|
239
257
|
}
|
|
240
258
|
|
|
259
|
+
function isSelectableInput(node) {
|
|
260
|
+
return (
|
|
261
|
+
node.tagName &&
|
|
262
|
+
node.tagName.toLowerCase() === 'input' &&
|
|
263
|
+
typeof node.select === 'function'
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
241
267
|
function isEscapeEvent(e) {
|
|
242
268
|
return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
|
|
243
269
|
}
|
|
244
270
|
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
271
|
+
function isTabEvent(e) {
|
|
272
|
+
return e.key === 'Tab' || e.keyCode === 9;
|
|
273
|
+
}
|
|
248
274
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
node.select();
|
|
252
|
-
}
|
|
275
|
+
function delay(fn) {
|
|
276
|
+
return setTimeout(fn, 0);
|
|
253
277
|
}
|
|
254
278
|
|
|
255
279
|
module.exports = focusTrap;
|
package/package.json
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "focus-trap",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Trap focus within a DOM node.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
+
"precommit": "lint-staged",
|
|
9
|
+
"format": "prettier --write '**/*.{js,json}'",
|
|
8
10
|
"lint": "eslint .",
|
|
9
11
|
"demo-bundle": "browserify demo/js/index.js -o demo/demo-bundle.js",
|
|
12
|
+
"clean": "del-cli dist && make-dir dist",
|
|
13
|
+
"build-dev": "npm run clean && browserify index.js -s focusTrap > dist/focus-trap.js",
|
|
14
|
+
"minify": "uglifyjs dist/focus-trap.js > dist/focus-trap.min.js",
|
|
15
|
+
"build": "npm run build-dev && npm run minify",
|
|
10
16
|
"start": "budo demo/js/index.js:demo-bundle.js --dir demo --live",
|
|
11
|
-
"test": "npm run lint"
|
|
17
|
+
"test": "npm run lint",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
12
19
|
},
|
|
13
20
|
"repository": {
|
|
14
21
|
"type": "git",
|
|
@@ -29,14 +36,22 @@
|
|
|
29
36
|
},
|
|
30
37
|
"homepage": "https://github.com/davidtheclark/focus-trap#readme",
|
|
31
38
|
"dependencies": {
|
|
32
|
-
"tabbable": "^1.0
|
|
39
|
+
"tabbable": "^3.1.0",
|
|
40
|
+
"xtend": "^4.0.1"
|
|
33
41
|
},
|
|
34
42
|
"devDependencies": {
|
|
35
43
|
"browserify": "^13.3.0",
|
|
36
44
|
"budo": "^9.4.1",
|
|
37
|
-
"
|
|
45
|
+
"del-cli": "^1.1.0",
|
|
46
|
+
"eslint": "^3.13.1",
|
|
47
|
+
"husky": "^0.14.3",
|
|
48
|
+
"lint-staged": "^7.2.0",
|
|
49
|
+
"make-dir-cli": "^1.0.0",
|
|
50
|
+
"prettier": "^1.14.0",
|
|
51
|
+
"uglify-js": "^3.3.22"
|
|
38
52
|
},
|
|
39
53
|
"files": [
|
|
54
|
+
"dist",
|
|
40
55
|
"index.js",
|
|
41
56
|
"index.d.ts"
|
|
42
57
|
]
|