focus-trap 2.4.6 → 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 CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## 2.4.6
4
9
 
5
10
  - Add slight delay before moving focus to the first element in the trap.
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 or dropdown menus**.
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
- 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!
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
@@ -121,7 +121,7 @@ Returns the `focusTrap`.
121
121
 
122
122
  ## Examples
123
123
 
124
- Read code in `demo/` (it's very simple), and [see how it works](http://davidtheclark.github.io/focus-trap/demo/).
124
+ Read code in `demo/` and [see how it works](http://davidtheclark.github.io/focus-trap/demo/).
125
125
 
126
126
  Here's what happens in `demo-one.js`:
127
127
 
@@ -147,7 +147,23 @@ document.getElementById('deactivate-one').addEventListener('click', function ()
147
147
 
148
148
  ## Other details
149
149
 
150
- - *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.
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.
151
167
 
152
168
  ## Development
153
169
 
@@ -1,53 +1,55 @@
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 e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
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
2
  var tabbable = require('tabbable');
3
+ var xtend = require('xtend');
3
4
 
4
5
  var listeningFocusTrap = null;
5
6
 
6
7
  function focusTrap(element, userOptions) {
7
- var tabbableNodes = [];
8
- var firstTabbableNode = null;
9
- var lastTabbableNode = null;
10
- var nodeFocusedBeforeActivation = null;
11
- var active = false;
12
- var paused = false;
13
- var tabEvent = null;
14
-
15
- var container = (typeof element === 'string')
16
- ? document.querySelector(element)
17
- : element;
18
-
19
- var config = userOptions || {};
20
- config.returnFocusOnDeactivate = (userOptions && userOptions.returnFocusOnDeactivate !== undefined)
21
- ? userOptions.returnFocusOnDeactivate
22
- : true;
23
- config.escapeDeactivates = (userOptions && userOptions.escapeDeactivates !== undefined)
24
- ? userOptions.escapeDeactivates
25
- : true;
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
+ };
26
28
 
27
29
  var trap = {
28
30
  activate: activate,
29
31
  deactivate: deactivate,
30
32
  pause: pause,
31
- unpause: unpause,
33
+ unpause: unpause
32
34
  };
33
35
 
34
36
  return trap;
35
37
 
36
38
  function activate(activateOptions) {
37
- if (active) return;
39
+ if (state.active) return;
38
40
 
39
- var defaultedActivateOptions = {
40
- onActivate: (activateOptions && activateOptions.onActivate !== undefined)
41
- ? activateOptions.onActivate
42
- : config.onActivate,
43
- };
41
+ updateTabbableNodes();
44
42
 
45
- active = true;
46
- paused = false;
47
- nodeFocusedBeforeActivation = document.activeElement;
43
+ state.active = true;
44
+ state.paused = false;
45
+ state.nodeFocusedBeforeActivation = doc.activeElement;
48
46
 
49
- if (defaultedActivateOptions.onActivate) {
50
- defaultedActivateOptions.onActivate();
47
+ var onActivate =
48
+ activateOptions && activateOptions.onActivate
49
+ ? activateOptions.onActivate
50
+ : config.onActivate;
51
+ if (onActivate) {
52
+ onActivate();
51
53
  }
52
54
 
53
55
  addListeners();
@@ -55,48 +57,47 @@ function focusTrap(element, userOptions) {
55
57
  }
56
58
 
57
59
  function deactivate(deactivateOptions) {
58
- if (!active) return;
59
-
60
- var defaultedDeactivateOptions = {
61
- returnFocus: (deactivateOptions && deactivateOptions.returnFocus !== undefined)
62
- ? deactivateOptions.returnFocus
63
- : config.returnFocusOnDeactivate,
64
- onDeactivate: (deactivateOptions && deactivateOptions.onDeactivate !== undefined)
65
- ? deactivateOptions.onDeactivate
66
- : config.onDeactivate,
67
- };
60
+ if (!state.active) return;
68
61
 
69
62
  removeListeners();
63
+ state.active = false;
64
+ state.paused = false;
70
65
 
71
- if (defaultedDeactivateOptions.onDeactivate) {
72
- defaultedDeactivateOptions.onDeactivate();
66
+ var onDeactivate =
67
+ deactivateOptions && deactivateOptions.onDeactivate !== undefined
68
+ ? deactivateOptions.onDeactivate
69
+ : config.onDeactivate;
70
+ if (onDeactivate) {
71
+ onDeactivate();
73
72
  }
74
73
 
75
- if (defaultedDeactivateOptions.returnFocus) {
76
- setTimeout(function () {
77
- tryFocus(nodeFocusedBeforeActivation);
78
- }, 0);
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
+ });
79
82
  }
80
83
 
81
- active = false;
82
- paused = false;
83
- return this;
84
+ return trap;
84
85
  }
85
86
 
86
87
  function pause() {
87
- if (paused || !active) return;
88
- paused = true;
88
+ if (state.paused || !state.active) return;
89
+ state.paused = true;
89
90
  removeListeners();
90
91
  }
91
92
 
92
93
  function unpause() {
93
- if (!paused || !active) return;
94
- paused = false;
94
+ if (!state.paused || !state.active) return;
95
+ state.paused = false;
95
96
  addListeners();
96
97
  }
97
98
 
98
99
  function addListeners() {
99
- if (!active) return;
100
+ if (!state.active) return;
100
101
 
101
102
  // There can be only one listening focus trap at a time
102
103
  if (listeningFocusTrap) {
@@ -105,27 +106,29 @@ function focusTrap(element, userOptions) {
105
106
  listeningFocusTrap = trap;
106
107
 
107
108
  updateTabbableNodes();
108
- // Ensure that the focused element doesn't capture the event that caused the focus trap activation
109
- setTimeout(function () {
110
- tryFocus(firstFocusNode());
111
- }, 0);
112
- document.addEventListener('focus', checkFocus, true);
113
- document.addEventListener('click', checkClick, true);
114
- document.addEventListener('mousedown', checkPointerDown, true);
115
- document.addEventListener('touchstart', checkPointerDown, true);
116
- document.addEventListener('keydown', checkKey, true);
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);
117
120
 
118
121
  return trap;
119
122
  }
120
123
 
121
124
  function removeListeners() {
122
- if (!active || listeningFocusTrap !== trap) return;
125
+ if (!state.active || listeningFocusTrap !== trap) return;
123
126
 
124
- document.removeEventListener('focus', checkFocus, true);
125
- document.removeEventListener('click', checkClick, true);
126
- document.removeEventListener('mousedown', checkPointerDown, true);
127
- document.removeEventListener('touchstart', checkPointerDown, true);
128
- document.removeEventListener('keydown', checkKey, true);
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);
129
132
 
130
133
  listeningFocusTrap = null;
131
134
 
@@ -139,7 +142,7 @@ function focusTrap(element, userOptions) {
139
142
  return null;
140
143
  }
141
144
  if (typeof optionValue === 'string') {
142
- node = document.querySelector(optionValue);
145
+ node = doc.querySelector(optionValue);
143
146
  if (!node) {
144
147
  throw new Error('`' + optionName + '` refers to no known node');
145
148
  }
@@ -153,211 +156,349 @@ function focusTrap(element, userOptions) {
153
156
  return node;
154
157
  }
155
158
 
156
- function firstFocusNode() {
159
+ function getInitialFocusNode() {
157
160
  var node;
158
161
  if (getNodeForOption('initialFocus') !== null) {
159
162
  node = getNodeForOption('initialFocus');
160
- } else if (container.contains(document.activeElement)) {
161
- node = document.activeElement;
163
+ } else if (container.contains(doc.activeElement)) {
164
+ node = doc.activeElement;
162
165
  } else {
163
- node = tabbableNodes[0] || getNodeForOption('fallbackFocus');
166
+ node = state.firstTabbableNode || getNodeForOption('fallbackFocus');
164
167
  }
165
168
 
166
169
  if (!node) {
167
- throw new Error('You can\'t have a focus-trap without at least one focusable element');
170
+ throw new Error(
171
+ "You can't have a focus-trap without at least one focusable element"
172
+ );
168
173
  }
169
174
 
170
175
  return node;
171
176
  }
172
177
 
173
178
  // This needs to be done on mousedown and touchstart instead of click
174
- // so that it precedes the focus event
179
+ // so that it precedes the focus event.
175
180
  function checkPointerDown(e) {
176
- if (config.clickOutsideDeactivates && !container.contains(e.target)) {
177
- deactivate({ returnFocus: false });
178
- }
179
- }
180
-
181
- function checkClick(e) {
182
- if (config.clickOutsideDeactivates) return;
183
181
  if (container.contains(e.target)) return;
184
- e.preventDefault();
185
- e.stopImmediatePropagation();
182
+ if (config.clickOutsideDeactivates) {
183
+ deactivate({
184
+ returnFocus: !tabbable.isFocusable(e.target)
185
+ });
186
+ } else {
187
+ e.preventDefault();
188
+ }
186
189
  }
187
190
 
188
- function checkFocus(e) {
189
- if (container.contains(e.target)) return;
190
- e.preventDefault();
191
- e.stopImmediatePropagation();
192
- // Checking for a blur method here resolves a Firefox issue (#15)
193
- if (typeof e.target.blur === 'function') e.target.blur();
194
-
195
- if (tabEvent) {
196
- readjustFocus(tabEvent);
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;
197
196
  }
197
+ e.stopImmediatePropagation();
198
+ tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
198
199
  }
199
200
 
200
201
  function checkKey(e) {
201
- if (e.key === 'Tab' || e.keyCode === 9) {
202
- handleTab(e);
203
- }
204
-
205
202
  if (config.escapeDeactivates !== false && isEscapeEvent(e)) {
203
+ e.preventDefault();
206
204
  deactivate();
205
+ return;
206
+ }
207
+ if (isTabEvent(e)) {
208
+ checkTab(e);
209
+ return;
207
210
  }
208
211
  }
209
212
 
210
- function handleTab(e) {
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) {
211
218
  updateTabbableNodes();
212
-
213
- if (e.target.hasAttribute('tabindex') && Number(e.target.getAttribute('tabindex')) < 0) {
214
- return tabEvent = e;
219
+ if (e.shiftKey && e.target === state.firstTabbableNode) {
220
+ e.preventDefault();
221
+ tryFocus(state.lastTabbableNode);
222
+ return;
215
223
  }
216
-
217
- e.preventDefault();
218
- var currentFocusIndex = tabbableNodes.indexOf(e.target);
219
-
220
- if (e.shiftKey) {
221
- if (e.target === firstTabbableNode || tabbableNodes.indexOf(e.target) === -1) {
222
- return tryFocus(lastTabbableNode);
223
- }
224
- return tryFocus(tabbableNodes[currentFocusIndex - 1]);
224
+ if (!e.shiftKey && e.target === state.lastTabbableNode) {
225
+ e.preventDefault();
226
+ tryFocus(state.firstTabbableNode);
227
+ return;
225
228
  }
229
+ }
226
230
 
227
- if (e.target === lastTabbableNode) return tryFocus(firstTabbableNode);
228
-
229
- tryFocus(tabbableNodes[currentFocusIndex + 1]);
231
+ function checkClick(e) {
232
+ if (config.clickOutsideDeactivates) return;
233
+ if (container.contains(e.target)) return;
234
+ e.preventDefault();
235
+ e.stopImmediatePropagation();
230
236
  }
231
237
 
232
238
  function updateTabbableNodes() {
233
- tabbableNodes = tabbable(container);
234
- firstTabbableNode = tabbableNodes[0];
235
- lastTabbableNode = tabbableNodes[tabbableNodes.length - 1];
239
+ var tabbableNodes = tabbable(container);
240
+ state.firstTabbableNode = tabbableNodes[0] || getInitialFocusNode();
241
+ state.lastTabbableNode =
242
+ tabbableNodes[tabbableNodes.length - 1] || getInitialFocusNode();
236
243
  }
237
244
 
238
- function readjustFocus(e) {
239
- if (e.shiftKey) return tryFocus(lastTabbableNode);
245
+ function tryFocus(node) {
246
+ if (node === doc.activeElement) return;
247
+ if (!node || !node.focus) {
248
+ tryFocus(getInitialFocusNode());
249
+ return;
250
+ }
240
251
 
241
- tryFocus(firstTabbableNode);
252
+ node.focus();
253
+ state.mostRecentlyFocusedNode = node;
254
+ if (isSelectableInput(node)) {
255
+ node.select();
256
+ }
242
257
  }
243
258
  }
244
259
 
260
+ function isSelectableInput(node) {
261
+ return (
262
+ node.tagName &&
263
+ node.tagName.toLowerCase() === 'input' &&
264
+ typeof node.select === 'function'
265
+ );
266
+ }
267
+
245
268
  function isEscapeEvent(e) {
246
269
  return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
247
270
  }
248
271
 
249
- function tryFocus(node) {
250
- if (!node || !node.focus) return;
251
- if (node === document.activeElement) return;
272
+ function isTabEvent(e) {
273
+ return e.key === 'Tab' || e.keyCode === 9;
274
+ }
252
275
 
253
- node.focus();
254
- if (node.tagName.toLowerCase() === 'input') {
255
- node.select();
256
- }
276
+ function delay(fn) {
277
+ return setTimeout(fn, 0);
257
278
  }
258
279
 
259
280
  module.exports = focusTrap;
260
281
 
261
- },{"tabbable":2}],2:[function(require,module,exports){
262
- module.exports = function(el) {
263
- var basicTabbables = [];
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 = [];
264
303
  var orderedTabbables = [];
265
304
 
266
- // A node is "available" if
267
- // - it's computed style
268
- var isUnavailable = createIsUnavailable();
269
-
270
- var candidateSelectors = [
271
- 'input',
272
- 'select',
273
- 'a[href]',
274
- 'textarea',
275
- 'button',
276
- '[tabindex]',
277
- ];
305
+ var untouchabilityChecker = new UntouchabilityChecker(elementDocument);
306
+ var candidates = el.querySelectorAll(candidateSelector);
278
307
 
279
- var candidates = el.querySelectorAll(candidateSelectors);
308
+ if (options.includeContainer) {
309
+ if (matches.call(el, candidateSelector)) {
310
+ candidates = Array.prototype.slice.apply(candidates);
311
+ candidates.unshift(el);
312
+ }
313
+ }
280
314
 
281
- var candidate, candidateIndex;
282
- for (var i = 0, l = candidates.length; i < l; i++) {
315
+ var i, candidate, candidateTabindex;
316
+ for (i = 0; i < candidates.length; i++) {
283
317
  candidate = candidates[i];
284
- candidateIndex = candidate.tabIndex;
285
-
286
- if (
287
- candidateIndex < 0
288
- || (candidate.tagName === 'INPUT' && candidate.type === 'hidden')
289
- || candidate.disabled
290
- || isUnavailable(candidate)
291
- ) {
292
- continue;
293
- }
294
318
 
295
- if (candidateIndex === 0) {
296
- basicTabbables.push(candidate);
319
+ if (!isNodeMatchingSelectorTabbable(candidate, untouchabilityChecker)) continue;
320
+
321
+ candidateTabindex = getTabindex(candidate);
322
+ if (candidateTabindex === 0) {
323
+ regularTabbables.push(candidate);
297
324
  } else {
298
325
  orderedTabbables.push({
299
- tabIndex: candidateIndex,
326
+ documentOrder: i,
327
+ tabIndex: candidateTabindex,
300
328
  node: candidate,
301
329
  });
302
330
  }
303
331
  }
304
332
 
305
333
  var tabbableNodes = orderedTabbables
306
- .sort(function(a, b) {
307
- return a.tabIndex - b.tabIndex;
308
- })
309
- .map(function(a) {
310
- return a.node
311
- });
312
-
313
- Array.prototype.push.apply(tabbableNodes, basicTabbables);
334
+ .sort(sortOrderedTabbables)
335
+ .map(function(a) { return a.node })
336
+ .concat(regularTabbables);
314
337
 
315
338
  return tabbableNodes;
316
339
  }
317
340
 
318
- function createIsUnavailable() {
319
- // Node cache must be refreshed on every check, in case
320
- // the content of the element has changed
321
- var isOffCache = [];
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
+ }
322
403
 
323
- // "off" means `display: none;`, as opposed to "hidden",
324
- // which means `visibility: hidden;`. getComputedStyle
325
- // accurately reflects visiblity in context but not
326
- // "off" state, so we need to recursively check parents.
404
+ function isInput(node) {
405
+ return node.tagName === 'INPUT';
406
+ }
407
+
408
+ function isHiddenInput(node) {
409
+ return isInput(node) && node.type === 'hidden';
410
+ }
327
411
 
328
- function isOff(node, nodeComputedStyle) {
329
- if (node === document.documentElement) return false;
412
+ function isRadio(node) {
413
+ return isInput(node) && node.type === 'radio';
414
+ }
330
415
 
331
- // Find the cached node (Array.prototype.find not available in IE9)
332
- for (var i = 0, length = isOffCache.length; i < length; i++) {
333
- if (isOffCache[i][0] === node) return isOffCache[i][1];
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];
334
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
+ }
335
446
 
336
- nodeComputedStyle = nodeComputedStyle || window.getComputedStyle(node);
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);
337
459
 
338
460
  var result = false;
339
461
 
340
462
  if (nodeComputedStyle.display === 'none') {
341
463
  result = true;
342
464
  } else if (node.parentNode) {
343
- result = isOff(node.parentNode);
465
+ result = this.hasDisplayNone(node.parentNode);
344
466
  }
345
467
 
346
- isOffCache.push([node, result]);
468
+ this.cache.push([node, result]);
347
469
 
348
470
  return result;
349
- }
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
+ }
350
479
 
351
- return function isUnavailable(node) {
352
- if (node === document.documentElement) return false;
480
+ module.exports = tabbable;
353
481
 
354
- var computedStyle = window.getComputedStyle(node);
482
+ },{}],3:[function(require,module,exports){
483
+ module.exports = extend
355
484
 
356
- if (isOff(node, computedStyle)) return true;
485
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
357
486
 
358
- return computedStyle.visibility === 'hidden';
359
- }
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
360
501
  }
361
502
 
362
503
  },{}]},{},[1])(1)
363
- });
504
+ });
@@ -1 +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 e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}({1:[function(require,module,exports){var tabbable=require("tabbable");var listeningFocusTrap=null;function focusTrap(element,userOptions){var tabbableNodes=[];var firstTabbableNode=null;var lastTabbableNode=null;var nodeFocusedBeforeActivation=null;var active=false;var paused=false;var tabEvent=null;var container=typeof element==="string"?document.querySelector(element):element;var config=userOptions||{};config.returnFocusOnDeactivate=userOptions&&userOptions.returnFocusOnDeactivate!==undefined?userOptions.returnFocusOnDeactivate:true;config.escapeDeactivates=userOptions&&userOptions.escapeDeactivates!==undefined?userOptions.escapeDeactivates:true;var trap={activate:activate,deactivate:deactivate,pause:pause,unpause:unpause};return trap;function activate(activateOptions){if(active)return;var defaultedActivateOptions={onActivate:activateOptions&&activateOptions.onActivate!==undefined?activateOptions.onActivate:config.onActivate};active=true;paused=false;nodeFocusedBeforeActivation=document.activeElement;if(defaultedActivateOptions.onActivate){defaultedActivateOptions.onActivate()}addListeners();return trap}function deactivate(deactivateOptions){if(!active)return;var defaultedDeactivateOptions={returnFocus:deactivateOptions&&deactivateOptions.returnFocus!==undefined?deactivateOptions.returnFocus:config.returnFocusOnDeactivate,onDeactivate:deactivateOptions&&deactivateOptions.onDeactivate!==undefined?deactivateOptions.onDeactivate:config.onDeactivate};removeListeners();if(defaultedDeactivateOptions.onDeactivate){defaultedDeactivateOptions.onDeactivate()}if(defaultedDeactivateOptions.returnFocus){setTimeout(function(){tryFocus(nodeFocusedBeforeActivation)},0)}active=false;paused=false;return this}function pause(){if(paused||!active)return;paused=true;removeListeners()}function unpause(){if(!paused||!active)return;paused=false;addListeners()}function addListeners(){if(!active)return;if(listeningFocusTrap){listeningFocusTrap.pause()}listeningFocusTrap=trap;updateTabbableNodes();setTimeout(function(){tryFocus(firstFocusNode())},0);document.addEventListener("focus",checkFocus,true);document.addEventListener("click",checkClick,true);document.addEventListener("mousedown",checkPointerDown,true);document.addEventListener("touchstart",checkPointerDown,true);document.addEventListener("keydown",checkKey,true);return trap}function removeListeners(){if(!active||listeningFocusTrap!==trap)return;document.removeEventListener("focus",checkFocus,true);document.removeEventListener("click",checkClick,true);document.removeEventListener("mousedown",checkPointerDown,true);document.removeEventListener("touchstart",checkPointerDown,true);document.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=document.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 firstFocusNode(){var node;if(getNodeForOption("initialFocus")!==null){node=getNodeForOption("initialFocus")}else if(container.contains(document.activeElement)){node=document.activeElement}else{node=tabbableNodes[0]||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(config.clickOutsideDeactivates&&!container.contains(e.target)){deactivate({returnFocus:false})}}function checkClick(e){if(config.clickOutsideDeactivates)return;if(container.contains(e.target))return;e.preventDefault();e.stopImmediatePropagation()}function checkFocus(e){if(container.contains(e.target))return;e.preventDefault();e.stopImmediatePropagation();if(typeof e.target.blur==="function")e.target.blur();if(tabEvent){readjustFocus(tabEvent)}}function checkKey(e){if(e.key==="Tab"||e.keyCode===9){handleTab(e)}if(config.escapeDeactivates!==false&&isEscapeEvent(e)){deactivate()}}function handleTab(e){updateTabbableNodes();if(e.target.hasAttribute("tabindex")&&Number(e.target.getAttribute("tabindex"))<0){return tabEvent=e}e.preventDefault();var currentFocusIndex=tabbableNodes.indexOf(e.target);if(e.shiftKey){if(e.target===firstTabbableNode||tabbableNodes.indexOf(e.target)===-1){return tryFocus(lastTabbableNode)}return tryFocus(tabbableNodes[currentFocusIndex-1])}if(e.target===lastTabbableNode)return tryFocus(firstTabbableNode);tryFocus(tabbableNodes[currentFocusIndex+1])}function updateTabbableNodes(){tabbableNodes=tabbable(container);firstTabbableNode=tabbableNodes[0];lastTabbableNode=tabbableNodes[tabbableNodes.length-1]}function readjustFocus(e){if(e.shiftKey)return tryFocus(lastTabbableNode);tryFocus(firstTabbableNode)}}function isEscapeEvent(e){return e.key==="Escape"||e.key==="Esc"||e.keyCode===27}function tryFocus(node){if(!node||!node.focus)return;if(node===document.activeElement)return;node.focus();if(node.tagName.toLowerCase()==="input"){node.select()}}module.exports=focusTrap},{tabbable:2}],2:[function(require,module,exports){module.exports=function(el){var basicTabbables=[];var orderedTabbables=[];var isUnavailable=createIsUnavailable();var candidateSelectors=["input","select","a[href]","textarea","button","[tabindex]"];var candidates=el.querySelectorAll(candidateSelectors);var candidate,candidateIndex;for(var i=0,l=candidates.length;i<l;i++){candidate=candidates[i];candidateIndex=candidate.tabIndex;if(candidateIndex<0||candidate.tagName==="INPUT"&&candidate.type==="hidden"||candidate.disabled||isUnavailable(candidate)){continue}if(candidateIndex===0){basicTabbables.push(candidate)}else{orderedTabbables.push({tabIndex:candidateIndex,node:candidate})}}var tabbableNodes=orderedTabbables.sort(function(a,b){return a.tabIndex-b.tabIndex}).map(function(a){return a.node});Array.prototype.push.apply(tabbableNodes,basicTabbables);return tabbableNodes};function createIsUnavailable(){var isOffCache=[];function isOff(node,nodeComputedStyle){if(node===document.documentElement)return false;for(var i=0,length=isOffCache.length;i<length;i++){if(isOffCache[i][0]===node)return isOffCache[i][1]}nodeComputedStyle=nodeComputedStyle||window.getComputedStyle(node);var result=false;if(nodeComputedStyle.display==="none"){result=true}else if(node.parentNode){result=isOff(node.parentNode)}isOffCache.push([node,result]);return result}return function isUnavailable(node){if(node===document.documentElement)return false;var computedStyle=window.getComputedStyle(node);if(isOff(node,computedStyle))return true;return computedStyle.visibility==="hidden"}}},{}]},{},[1])(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 tabbableNodes = [];
7
- var firstTabbableNode = null;
8
- var lastTabbableNode = null;
9
- var nodeFocusedBeforeActivation = null;
10
- var active = false;
11
- var paused = false;
12
- var tabEvent = null;
13
-
14
- var container = (typeof element === 'string')
15
- ? document.querySelector(element)
16
- : element;
17
-
18
- var config = userOptions || {};
19
- config.returnFocusOnDeactivate = (userOptions && userOptions.returnFocusOnDeactivate !== undefined)
20
- ? userOptions.returnFocusOnDeactivate
21
- : true;
22
- config.escapeDeactivates = (userOptions && userOptions.escapeDeactivates !== undefined)
23
- ? userOptions.escapeDeactivates
24
- : true;
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
- var defaultedActivateOptions = {
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 = document.activeElement;
42
+ state.active = true;
43
+ state.paused = false;
44
+ state.nodeFocusedBeforeActivation = doc.activeElement;
47
45
 
48
- if (defaultedActivateOptions.onActivate) {
49
- defaultedActivateOptions.onActivate();
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
- if (defaultedDeactivateOptions.onDeactivate) {
71
- defaultedDeactivateOptions.onDeactivate();
65
+ var onDeactivate =
66
+ deactivateOptions && deactivateOptions.onDeactivate !== undefined
67
+ ? deactivateOptions.onDeactivate
68
+ : config.onDeactivate;
69
+ if (onDeactivate) {
70
+ onDeactivate();
72
71
  }
73
72
 
74
- if (defaultedDeactivateOptions.returnFocus) {
75
- setTimeout(function () {
76
- tryFocus(nodeFocusedBeforeActivation);
77
- }, 0);
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
- active = false;
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,27 +105,29 @@ function focusTrap(element, userOptions) {
104
105
  listeningFocusTrap = trap;
105
106
 
106
107
  updateTabbableNodes();
107
- // Ensure that the focused element doesn't capture the event that caused the focus trap activation
108
- setTimeout(function () {
109
- tryFocus(firstFocusNode());
110
- }, 0);
111
- document.addEventListener('focus', checkFocus, true);
112
- document.addEventListener('click', checkClick, true);
113
- document.addEventListener('mousedown', checkPointerDown, true);
114
- document.addEventListener('touchstart', checkPointerDown, true);
115
- document.addEventListener('keydown', checkKey, true);
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);
116
119
 
117
120
  return trap;
118
121
  }
119
122
 
120
123
  function removeListeners() {
121
- if (!active || listeningFocusTrap !== trap) return;
124
+ if (!state.active || listeningFocusTrap !== trap) return;
122
125
 
123
- document.removeEventListener('focus', checkFocus, true);
124
- document.removeEventListener('click', checkClick, true);
125
- document.removeEventListener('mousedown', checkPointerDown, true);
126
- document.removeEventListener('touchstart', checkPointerDown, true);
127
- document.removeEventListener('keydown', checkKey, true);
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);
128
131
 
129
132
  listeningFocusTrap = null;
130
133
 
@@ -138,7 +141,7 @@ function focusTrap(element, userOptions) {
138
141
  return null;
139
142
  }
140
143
  if (typeof optionValue === 'string') {
141
- node = document.querySelector(optionValue);
144
+ node = doc.querySelector(optionValue);
142
145
  if (!node) {
143
146
  throw new Error('`' + optionName + '` refers to no known node');
144
147
  }
@@ -152,107 +155,125 @@ function focusTrap(element, userOptions) {
152
155
  return node;
153
156
  }
154
157
 
155
- function firstFocusNode() {
158
+ function getInitialFocusNode() {
156
159
  var node;
157
160
  if (getNodeForOption('initialFocus') !== null) {
158
161
  node = getNodeForOption('initialFocus');
159
- } else if (container.contains(document.activeElement)) {
160
- node = document.activeElement;
162
+ } else if (container.contains(doc.activeElement)) {
163
+ node = doc.activeElement;
161
164
  } else {
162
- node = tabbableNodes[0] || getNodeForOption('fallbackFocus');
165
+ node = state.firstTabbableNode || getNodeForOption('fallbackFocus');
163
166
  }
164
167
 
165
168
  if (!node) {
166
- throw new Error('You can\'t have a focus-trap without at least one focusable element');
169
+ throw new Error(
170
+ "You can't have a focus-trap without at least one focusable element"
171
+ );
167
172
  }
168
173
 
169
174
  return node;
170
175
  }
171
176
 
172
177
  // This needs to be done on mousedown and touchstart instead of click
173
- // so that it precedes the focus event
178
+ // so that it precedes the focus event.
174
179
  function checkPointerDown(e) {
175
- if (config.clickOutsideDeactivates && !container.contains(e.target)) {
176
- deactivate({ returnFocus: false });
177
- }
178
- }
179
-
180
- function checkClick(e) {
181
- if (config.clickOutsideDeactivates) return;
182
180
  if (container.contains(e.target)) return;
183
- e.preventDefault();
184
- e.stopImmediatePropagation();
181
+ if (config.clickOutsideDeactivates) {
182
+ deactivate({
183
+ returnFocus: !tabbable.isFocusable(e.target)
184
+ });
185
+ } else {
186
+ e.preventDefault();
187
+ }
185
188
  }
186
189
 
187
- function checkFocus(e) {
188
- if (container.contains(e.target)) return;
189
- e.preventDefault();
190
- e.stopImmediatePropagation();
191
- // Checking for a blur method here resolves a Firefox issue (#15)
192
- if (typeof e.target.blur === 'function') e.target.blur();
193
-
194
- if (tabEvent) {
195
- 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;
196
195
  }
196
+ e.stopImmediatePropagation();
197
+ tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
197
198
  }
198
199
 
199
200
  function checkKey(e) {
200
- if (e.key === 'Tab' || e.keyCode === 9) {
201
- handleTab(e);
202
- }
203
-
204
201
  if (config.escapeDeactivates !== false && isEscapeEvent(e)) {
202
+ e.preventDefault();
205
203
  deactivate();
204
+ return;
205
+ }
206
+ if (isTabEvent(e)) {
207
+ checkTab(e);
208
+ return;
206
209
  }
207
210
  }
208
211
 
209
- function handleTab(e) {
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) {
210
217
  updateTabbableNodes();
211
-
212
- if (e.target.hasAttribute('tabindex') && Number(e.target.getAttribute('tabindex')) < 0) {
213
- return tabEvent = e;
218
+ if (e.shiftKey && e.target === state.firstTabbableNode) {
219
+ e.preventDefault();
220
+ tryFocus(state.lastTabbableNode);
221
+ return;
214
222
  }
215
-
216
- e.preventDefault();
217
- var currentFocusIndex = tabbableNodes.indexOf(e.target);
218
-
219
- if (e.shiftKey) {
220
- if (e.target === firstTabbableNode || tabbableNodes.indexOf(e.target) === -1) {
221
- return tryFocus(lastTabbableNode);
222
- }
223
- return tryFocus(tabbableNodes[currentFocusIndex - 1]);
223
+ if (!e.shiftKey && e.target === state.lastTabbableNode) {
224
+ e.preventDefault();
225
+ tryFocus(state.firstTabbableNode);
226
+ return;
224
227
  }
228
+ }
225
229
 
226
- if (e.target === lastTabbableNode) return tryFocus(firstTabbableNode);
227
-
228
- tryFocus(tabbableNodes[currentFocusIndex + 1]);
230
+ function checkClick(e) {
231
+ if (config.clickOutsideDeactivates) return;
232
+ if (container.contains(e.target)) return;
233
+ e.preventDefault();
234
+ e.stopImmediatePropagation();
229
235
  }
230
236
 
231
237
  function updateTabbableNodes() {
232
- tabbableNodes = tabbable(container);
233
- firstTabbableNode = tabbableNodes[0];
234
- lastTabbableNode = tabbableNodes[tabbableNodes.length - 1];
238
+ var tabbableNodes = tabbable(container);
239
+ state.firstTabbableNode = tabbableNodes[0] || getInitialFocusNode();
240
+ state.lastTabbableNode =
241
+ tabbableNodes[tabbableNodes.length - 1] || getInitialFocusNode();
235
242
  }
236
243
 
237
- function readjustFocus(e) {
238
- if (e.shiftKey) return tryFocus(lastTabbableNode);
244
+ function tryFocus(node) {
245
+ if (node === doc.activeElement) return;
246
+ if (!node || !node.focus) {
247
+ tryFocus(getInitialFocusNode());
248
+ return;
249
+ }
239
250
 
240
- tryFocus(firstTabbableNode);
251
+ node.focus();
252
+ state.mostRecentlyFocusedNode = node;
253
+ if (isSelectableInput(node)) {
254
+ node.select();
255
+ }
241
256
  }
242
257
  }
243
258
 
259
+ function isSelectableInput(node) {
260
+ return (
261
+ node.tagName &&
262
+ node.tagName.toLowerCase() === 'input' &&
263
+ typeof node.select === 'function'
264
+ );
265
+ }
266
+
244
267
  function isEscapeEvent(e) {
245
268
  return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
246
269
  }
247
270
 
248
- function tryFocus(node) {
249
- if (!node || !node.focus) return;
250
- if (node === document.activeElement) return;
271
+ function isTabEvent(e) {
272
+ return e.key === 'Tab' || e.keyCode === 9;
273
+ }
251
274
 
252
- node.focus();
253
- if (node.tagName.toLowerCase() === 'input') {
254
- node.select();
255
- }
275
+ function delay(fn) {
276
+ return setTimeout(fn, 0);
256
277
  }
257
278
 
258
279
  module.exports = focusTrap;
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "focus-trap",
3
- "version": "2.4.6",
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",
10
12
  "clean": "del-cli dist && make-dir dist",
@@ -34,14 +36,18 @@
34
36
  },
35
37
  "homepage": "https://github.com/davidtheclark/focus-trap#readme",
36
38
  "dependencies": {
37
- "tabbable": "^1.0.3"
39
+ "tabbable": "^3.1.0",
40
+ "xtend": "^4.0.1"
38
41
  },
39
42
  "devDependencies": {
40
43
  "browserify": "^13.3.0",
41
44
  "budo": "^9.4.1",
42
45
  "del-cli": "^1.1.0",
43
46
  "eslint": "^3.13.1",
47
+ "husky": "^0.14.3",
48
+ "lint-staged": "^7.2.0",
44
49
  "make-dir-cli": "^1.0.0",
50
+ "prettier": "^1.14.0",
45
51
  "uglify-js": "^3.3.22"
46
52
  },
47
53
  "files": [