focus-trap 4.0.2 → 5.1.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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.0
4
+
5
+ - Add `setReturnFocus` option that allows you to set which element receives focus when the trap closes.
6
+
7
+ ## 5.0.2
8
+
9
+ - Add `allowOutsideClick` option that allows you to pass a click event through, even when `clickOutsideDeactivates` is `false`.
10
+
11
+ ## 5.0.0
12
+
13
+ - Update Tabbable to improve performance (see [Tabbable's changelog](https://github.com/davidtheclark/tabbable/blob/master/CHANGELOG.md)).
14
+ - **Breaking (kind of):** if the `onActivate` callback changes the list of tabbable nodes and the `initialFocus` option is not used, the initial focus will still go to the first element present before the callback.
15
+ - Improve performance of activating a trap.
16
+ - Register document-level event listeners as active (`passive: false`).
17
+
3
18
  ## 4.0.2
4
19
 
5
20
  - Fix reference to root element that caused errors within Shadow DOM.
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # focus-trap
2
2
 
3
+ ---
4
+
5
+ **SEEKING CO-MAINTAINERS!** Continued development of this project is going to require the work of one or more dedicated co-maintainers (or forkers). If you're interested, please comment in [this issue](https://github.com/davidtheclark/focus-trap/issues/85).
6
+
7
+ ---
8
+
3
9
  Trap focus within a DOM node.
4
10
 
5
11
  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.
@@ -63,6 +69,8 @@ Returns a new focus trap on `element`.
63
69
  - **escapeDeactivates** {boolean}: Default: `true`. If `false`, the `Escape` key will not trigger deactivation of the focus trap. This can be useful if you want to force the user to make a decision instead of allowing an easy way out.
64
70
  - **clickOutsideDeactivates** {boolean}: Default: `false`. If `true`, a click outside the focus trap will deactivate the focus trap and allow the click event to do its thing.
65
71
  - **returnFocusOnDeactivate** {boolean}: Default: `true`. If `false`, when the trap is deactivated, focus will *not* return to the element that had focus before activation.
72
+ - **setReturnFocus** {element|string|function}: By default, focus trap on deactivation will return to the element that was focused before activation. With this option you can specify another element to programmatically receive focus after deactivation. Can be a DOM node, or a selector string (which will be passed to `document.querySelector()` to find the DOM node), or a function that returns a DOM node.
73
+ - **allowOutsideClick** {function}: If set and returns `true`, a click outside the focus trap will not be prevented, even when `clickOutsideDeactivates` is `false`.
66
74
 
67
75
  ### focusTrap.activate([activateOptions])
68
76
 
@@ -2,6 +2,8 @@
2
2
  var tabbable = require('tabbable');
3
3
  var xtend = require('xtend');
4
4
 
5
+ var activeFocusDelay;
6
+
5
7
  var activeFocusTraps = (function() {
6
8
  var trapQueue = [];
7
9
  return {
@@ -91,6 +93,8 @@ function focusTrap(element, userOptions) {
91
93
  function deactivate(deactivateOptions) {
92
94
  if (!state.active) return;
93
95
 
96
+ clearTimeout(activeFocusDelay);
97
+
94
98
  removeListeners();
95
99
  state.active = false;
96
100
  state.paused = false;
@@ -111,7 +115,7 @@ function focusTrap(element, userOptions) {
111
115
  : config.returnFocusOnDeactivate;
112
116
  if (returnFocus) {
113
117
  delay(function() {
114
- tryFocus(state.nodeFocusedBeforeActivation);
118
+ tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
115
119
  });
116
120
  }
117
121
 
@@ -127,6 +131,7 @@ function focusTrap(element, userOptions) {
127
131
  function unpause() {
128
132
  if (!state.paused || !state.active) return;
129
133
  state.paused = false;
134
+ updateTabbableNodes();
130
135
  addListeners();
131
136
  }
132
137
 
@@ -136,18 +141,29 @@ function focusTrap(element, userOptions) {
136
141
  // There can be only one listening focus trap at a time
137
142
  activeFocusTraps.activateTrap(trap);
138
143
 
139
- updateTabbableNodes();
140
-
141
144
  // Delay ensures that the focused element doesn't capture the event
142
145
  // that caused the focus trap activation.
143
- delay(function() {
146
+ activeFocusDelay = delay(function() {
144
147
  tryFocus(getInitialFocusNode());
145
148
  });
149
+
146
150
  doc.addEventListener('focusin', checkFocusIn, true);
147
- doc.addEventListener('mousedown', checkPointerDown, true);
148
- doc.addEventListener('touchstart', checkPointerDown, true);
149
- doc.addEventListener('click', checkClick, true);
150
- doc.addEventListener('keydown', checkKey, true);
151
+ doc.addEventListener('mousedown', checkPointerDown, {
152
+ capture: true,
153
+ passive: false
154
+ });
155
+ doc.addEventListener('touchstart', checkPointerDown, {
156
+ capture: true,
157
+ passive: false
158
+ });
159
+ doc.addEventListener('click', checkClick, {
160
+ capture: true,
161
+ passive: false
162
+ });
163
+ doc.addEventListener('keydown', checkKey, {
164
+ capture: true,
165
+ passive: false
166
+ });
151
167
 
152
168
  return trap;
153
169
  }
@@ -197,13 +213,18 @@ function focusTrap(element, userOptions) {
197
213
 
198
214
  if (!node) {
199
215
  throw new Error(
200
- "You can't have a focus-trap without at least one focusable element"
216
+ 'Your focus-trap needs to have at least one focusable element'
201
217
  );
202
218
  }
203
219
 
204
220
  return node;
205
221
  }
206
222
 
223
+ function getReturnFocusNode(previousActiveElement) {
224
+ var node = getNodeForOption('setReturnFocus');
225
+ return node ? node : previousActiveElement;
226
+ }
227
+
207
228
  // This needs to be done on mousedown and touchstart instead of click
208
229
  // so that it precedes the focus event.
209
230
  function checkPointerDown(e) {
@@ -212,9 +233,15 @@ function focusTrap(element, userOptions) {
212
233
  deactivate({
213
234
  returnFocus: !tabbable.isFocusable(e.target)
214
235
  });
215
- } else {
216
- e.preventDefault();
236
+ return;
237
+ }
238
+ // This is needed for mobile devices.
239
+ // (If we'll only let `click` events through,
240
+ // then on mobile they will be blocked anyways if `touchstart` is blocked.)
241
+ if (config.allowOutsideClick && config.allowOutsideClick(e)) {
242
+ return;
217
243
  }
244
+ e.preventDefault();
218
245
  }
219
246
 
220
247
  // In case focus escapes the trap for some strange reason, pull it back in.
@@ -260,6 +287,9 @@ function focusTrap(element, userOptions) {
260
287
  function checkClick(e) {
261
288
  if (config.clickOutsideDeactivates) return;
262
289
  if (container.contains(e.target)) return;
290
+ if (config.allowOutsideClick && config.allowOutsideClick(e)) {
291
+ return;
292
+ }
263
293
  e.preventDefault();
264
294
  e.stopImmediatePropagation();
265
295
  }
@@ -277,7 +307,6 @@ function focusTrap(element, userOptions) {
277
307
  tryFocus(getInitialFocusNode());
278
308
  return;
279
309
  }
280
-
281
310
  node.focus();
282
311
  state.mostRecentlyFocusedNode = node;
283
312
  if (isSelectableInput(node)) {
@@ -329,11 +358,9 @@ var matches = typeof Element === 'undefined'
329
358
  function tabbable(el, options) {
330
359
  options = options || {};
331
360
 
332
- var elementDocument = el.ownerDocument || el;
333
361
  var regularTabbables = [];
334
362
  var orderedTabbables = [];
335
363
 
336
- var untouchabilityChecker = new UntouchabilityChecker(elementDocument);
337
364
  var candidates = el.querySelectorAll(candidateSelector);
338
365
 
339
366
  if (options.includeContainer) {
@@ -347,7 +374,7 @@ function tabbable(el, options) {
347
374
  for (i = 0; i < candidates.length; i++) {
348
375
  candidate = candidates[i];
349
376
 
350
- if (!isNodeMatchingSelectorTabbable(candidate, untouchabilityChecker)) continue;
377
+ if (!isNodeMatchingSelectorTabbable(candidate)) continue;
351
378
 
352
379
  candidateTabindex = getTabindex(candidate);
353
380
  if (candidateTabindex === 0) {
@@ -372,9 +399,9 @@ function tabbable(el, options) {
372
399
  tabbable.isTabbable = isTabbable;
373
400
  tabbable.isFocusable = isFocusable;
374
401
 
375
- function isNodeMatchingSelectorTabbable(node, untouchabilityChecker) {
402
+ function isNodeMatchingSelectorTabbable(node) {
376
403
  if (
377
- !isNodeMatchingSelectorFocusable(node, untouchabilityChecker)
404
+ !isNodeMatchingSelectorFocusable(node)
378
405
  || isNonTabbableRadio(node)
379
406
  || getTabindex(node) < 0
380
407
  ) {
@@ -383,18 +410,17 @@ function isNodeMatchingSelectorTabbable(node, untouchabilityChecker) {
383
410
  return true;
384
411
  }
385
412
 
386
- function isTabbable(node, untouchabilityChecker) {
413
+ function isTabbable(node) {
387
414
  if (!node) throw new Error('No node provided');
388
415
  if (matches.call(node, candidateSelector) === false) return false;
389
- return isNodeMatchingSelectorTabbable(node, untouchabilityChecker);
416
+ return isNodeMatchingSelectorTabbable(node);
390
417
  }
391
418
 
392
- function isNodeMatchingSelectorFocusable(node, untouchabilityChecker) {
393
- untouchabilityChecker = untouchabilityChecker || new UntouchabilityChecker(node.ownerDocument || node);
419
+ function isNodeMatchingSelectorFocusable(node) {
394
420
  if (
395
421
  node.disabled
396
422
  || isHiddenInput(node)
397
- || untouchabilityChecker.isUntouchable(node)
423
+ || isHidden(node)
398
424
  ) {
399
425
  return false;
400
426
  }
@@ -402,10 +428,10 @@ function isNodeMatchingSelectorFocusable(node, untouchabilityChecker) {
402
428
  }
403
429
 
404
430
  var focusableCandidateSelector = candidateSelectors.concat('iframe').join(',');
405
- function isFocusable(node, untouchabilityChecker) {
431
+ function isFocusable(node) {
406
432
  if (!node) throw new Error('No node provided');
407
433
  if (matches.call(node, focusableCandidateSelector) === false) return false;
408
- return isNodeMatchingSelectorFocusable(node, untouchabilityChecker);
434
+ return isNodeMatchingSelectorFocusable(node);
409
435
  }
410
436
 
411
437
  function getTabindex(node) {
@@ -421,13 +447,6 @@ function sortOrderedTabbables(a, b) {
421
447
  return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
422
448
  }
423
449
 
424
- // Array.prototype.find not available in IE.
425
- function find(list, predicate) {
426
- for (var i = 0, length = list.length; i < length; i++) {
427
- if (predicate(list[i])) return list[i];
428
- }
429
- }
430
-
431
450
  function isContentEditable(node) {
432
451
  return node.contentEditable === 'true';
433
452
  }
@@ -465,47 +484,10 @@ function isTabbableRadio(node) {
465
484
  return !checked || checked === node;
466
485
  }
467
486
 
468
- // An element is "untouchable" if *it or one of its ancestors* has
469
- // `visibility: hidden` or `display: none`.
470
- function UntouchabilityChecker(elementDocument) {
471
- this.doc = elementDocument;
472
- // Node cache must be refreshed on every check, in case
473
- // the content of the element has changed. The cache contains tuples
474
- // mapping nodes to their boolean result.
475
- this.cache = [];
476
- }
477
-
478
- // getComputedStyle accurately reflects `visibility: hidden` of ancestors
479
- // but not `display: none`, so we need to recursively check parents.
480
- UntouchabilityChecker.prototype.hasDisplayNone = function hasDisplayNone(node, nodeComputedStyle) {
481
- if (node.nodeType !== Node.ELEMENT_NODE) return false;
482
-
483
- // Search for a cached result.
484
- var cached = find(this.cache, function(item) {
485
- return item === node;
486
- });
487
- if (cached) return cached[1];
488
-
489
- nodeComputedStyle = nodeComputedStyle || this.doc.defaultView.getComputedStyle(node);
490
-
491
- var result = false;
492
-
493
- if (nodeComputedStyle.display === 'none') {
494
- result = true;
495
- } else if (node.parentNode) {
496
- result = this.hasDisplayNone(node.parentNode);
497
- }
498
-
499
- this.cache.push([node, result]);
500
-
501
- return result;
502
- }
503
-
504
- UntouchabilityChecker.prototype.isUntouchable = function isUntouchable(node) {
505
- if (node === this.doc.documentElement) return false;
506
- var computedStyle = this.doc.defaultView.getComputedStyle(node);
507
- if (this.hasDisplayNone(node, computedStyle)) return true;
508
- return computedStyle.visibility === 'hidden';
487
+ function isHidden(node) {
488
+ // offsetParent being null will allow detecting cases where an element is invisible or inside an invisible element,
489
+ // as long as the element does not use position: fixed. For them, their visibility has to be checked directly as well.
490
+ return node.offsetParent === null || getComputedStyle(node).visibility === 'hidden';
509
491
  }
510
492
 
511
493
  module.exports = tabbable;
@@ -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 activeFocusTraps=function(){var trapQueue=[];return{activateTrap:function(trap){if(trapQueue.length>0){var activeTrap=trapQueue[trapQueue.length-1];if(activeTrap!==trap){activeTrap.pause()}}var trapIndex=trapQueue.indexOf(trap);if(trapIndex===-1){trapQueue.push(trap)}else{trapQueue.splice(trapIndex,1);trapQueue.push(trap)}},deactivateTrap:function(trap){var trapIndex=trapQueue.indexOf(trap);if(trapIndex!==-1){trapQueue.splice(trapIndex,1)}if(trapQueue.length>0){trapQueue[trapQueue.length-1].unpause()}}}}();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;activeFocusTraps.deactivateTrap(trap);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;activeFocusTraps.activateTrap(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)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);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=typeof Element==="undefined"?function(){}: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.nodeType!==Node.ELEMENT_NODE)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)});
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 activeFocusDelay;var activeFocusTraps=function(){var trapQueue=[];return{activateTrap:function(trap){if(trapQueue.length>0){var activeTrap=trapQueue[trapQueue.length-1];if(activeTrap!==trap){activeTrap.pause()}}var trapIndex=trapQueue.indexOf(trap);if(trapIndex===-1){trapQueue.push(trap)}else{trapQueue.splice(trapIndex,1);trapQueue.push(trap)}},deactivateTrap:function(trap){var trapIndex=trapQueue.indexOf(trap);if(trapIndex!==-1){trapQueue.splice(trapIndex,1)}if(trapQueue.length>0){trapQueue[trapQueue.length-1].unpause()}}}}();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;clearTimeout(activeFocusDelay);removeListeners();state.active=false;state.paused=false;activeFocusTraps.deactivateTrap(trap);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(getReturnFocusNode(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;updateTabbableNodes();addListeners()}function addListeners(){if(!state.active)return;activeFocusTraps.activateTrap(trap);activeFocusDelay=delay(function(){tryFocus(getInitialFocusNode())});doc.addEventListener("focusin",checkFocusIn,true);doc.addEventListener("mousedown",checkPointerDown,{capture:true,passive:false});doc.addEventListener("touchstart",checkPointerDown,{capture:true,passive:false});doc.addEventListener("click",checkClick,{capture:true,passive:false});doc.addEventListener("keydown",checkKey,{capture:true,passive:false});return trap}function removeListeners(){if(!state.active)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);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("Your focus-trap needs to have at least one focusable element")}return node}function getReturnFocusNode(previousActiveElement){var node=getNodeForOption("setReturnFocus");return node?node:previousActiveElement}function checkPointerDown(e){if(container.contains(e.target))return;if(config.clickOutsideDeactivates){deactivate({returnFocus:!tabbable.isFocusable(e.target)});return}if(config.allowOutsideClick&&config.allowOutsideClick(e)){return}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;if(config.allowOutsideClick&&config.allowOutsideClick(e)){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=typeof Element==="undefined"?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector;function tabbable(el,options){options=options||{};var regularTabbables=[];var orderedTabbables=[];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))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){if(!isNodeMatchingSelectorFocusable(node)||isNonTabbableRadio(node)||getTabindex(node)<0){return false}return true}function isTabbable(node){if(!node)throw new Error("No node provided");if(matches.call(node,candidateSelector)===false)return false;return isNodeMatchingSelectorTabbable(node)}function isNodeMatchingSelectorFocusable(node){if(node.disabled||isHiddenInput(node)||isHidden(node)){return false}return true}var focusableCandidateSelector=candidateSelectors.concat("iframe").join(",");function isFocusable(node){if(!node)throw new Error("No node provided");if(matches.call(node,focusableCandidateSelector)===false)return false;return isNodeMatchingSelectorFocusable(node)}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 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 isHidden(node){return node.offsetParent===null||getComputedStyle(node).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.d.ts CHANGED
@@ -41,6 +41,12 @@ declare module "focus-trap" {
41
41
  */
42
42
  returnFocusOnDeactivate?: boolean;
43
43
 
44
+ /**
45
+ * By default, focus trap on deactivation will return to the element
46
+ * that was focused before activation.
47
+ */
48
+ setReturnFocus?: FocusTarget;
49
+
44
50
  /**
45
51
  * Default: `true`. If `false`, the `Escape` key will not trigger
46
52
  * deactivation of the focus trap. This can be useful if you want
@@ -54,6 +60,8 @@ declare module "focus-trap" {
54
60
  * deactivate the focus trap and allow the click event to do its thing.
55
61
  */
56
62
  clickOutsideDeactivates?: boolean;
63
+
64
+ allowOutsideClick?: (event: MouseEvent) => boolean;
57
65
  }
58
66
 
59
67
  type ActivateOptions = Pick<Options, "onActivate">;
package/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  var tabbable = require('tabbable');
2
2
  var xtend = require('xtend');
3
3
 
4
+ var activeFocusDelay;
5
+
4
6
  var activeFocusTraps = (function() {
5
7
  var trapQueue = [];
6
8
  return {
@@ -90,6 +92,8 @@ function focusTrap(element, userOptions) {
90
92
  function deactivate(deactivateOptions) {
91
93
  if (!state.active) return;
92
94
 
95
+ clearTimeout(activeFocusDelay);
96
+
93
97
  removeListeners();
94
98
  state.active = false;
95
99
  state.paused = false;
@@ -110,7 +114,7 @@ function focusTrap(element, userOptions) {
110
114
  : config.returnFocusOnDeactivate;
111
115
  if (returnFocus) {
112
116
  delay(function() {
113
- tryFocus(state.nodeFocusedBeforeActivation);
117
+ tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
114
118
  });
115
119
  }
116
120
 
@@ -126,6 +130,7 @@ function focusTrap(element, userOptions) {
126
130
  function unpause() {
127
131
  if (!state.paused || !state.active) return;
128
132
  state.paused = false;
133
+ updateTabbableNodes();
129
134
  addListeners();
130
135
  }
131
136
 
@@ -135,18 +140,29 @@ function focusTrap(element, userOptions) {
135
140
  // There can be only one listening focus trap at a time
136
141
  activeFocusTraps.activateTrap(trap);
137
142
 
138
- updateTabbableNodes();
139
-
140
143
  // Delay ensures that the focused element doesn't capture the event
141
144
  // that caused the focus trap activation.
142
- delay(function() {
145
+ activeFocusDelay = delay(function() {
143
146
  tryFocus(getInitialFocusNode());
144
147
  });
148
+
145
149
  doc.addEventListener('focusin', checkFocusIn, true);
146
- doc.addEventListener('mousedown', checkPointerDown, true);
147
- doc.addEventListener('touchstart', checkPointerDown, true);
148
- doc.addEventListener('click', checkClick, true);
149
- doc.addEventListener('keydown', checkKey, true);
150
+ doc.addEventListener('mousedown', checkPointerDown, {
151
+ capture: true,
152
+ passive: false
153
+ });
154
+ doc.addEventListener('touchstart', checkPointerDown, {
155
+ capture: true,
156
+ passive: false
157
+ });
158
+ doc.addEventListener('click', checkClick, {
159
+ capture: true,
160
+ passive: false
161
+ });
162
+ doc.addEventListener('keydown', checkKey, {
163
+ capture: true,
164
+ passive: false
165
+ });
150
166
 
151
167
  return trap;
152
168
  }
@@ -196,13 +212,18 @@ function focusTrap(element, userOptions) {
196
212
 
197
213
  if (!node) {
198
214
  throw new Error(
199
- "You can't have a focus-trap without at least one focusable element"
215
+ 'Your focus-trap needs to have at least one focusable element'
200
216
  );
201
217
  }
202
218
 
203
219
  return node;
204
220
  }
205
221
 
222
+ function getReturnFocusNode(previousActiveElement) {
223
+ var node = getNodeForOption('setReturnFocus');
224
+ return node ? node : previousActiveElement;
225
+ }
226
+
206
227
  // This needs to be done on mousedown and touchstart instead of click
207
228
  // so that it precedes the focus event.
208
229
  function checkPointerDown(e) {
@@ -211,9 +232,15 @@ function focusTrap(element, userOptions) {
211
232
  deactivate({
212
233
  returnFocus: !tabbable.isFocusable(e.target)
213
234
  });
214
- } else {
215
- e.preventDefault();
235
+ return;
236
+ }
237
+ // This is needed for mobile devices.
238
+ // (If we'll only let `click` events through,
239
+ // then on mobile they will be blocked anyways if `touchstart` is blocked.)
240
+ if (config.allowOutsideClick && config.allowOutsideClick(e)) {
241
+ return;
216
242
  }
243
+ e.preventDefault();
217
244
  }
218
245
 
219
246
  // In case focus escapes the trap for some strange reason, pull it back in.
@@ -259,6 +286,9 @@ function focusTrap(element, userOptions) {
259
286
  function checkClick(e) {
260
287
  if (config.clickOutsideDeactivates) return;
261
288
  if (container.contains(e.target)) return;
289
+ if (config.allowOutsideClick && config.allowOutsideClick(e)) {
290
+ return;
291
+ }
262
292
  e.preventDefault();
263
293
  e.stopImmediatePropagation();
264
294
  }
@@ -276,7 +306,6 @@ function focusTrap(element, userOptions) {
276
306
  tryFocus(getInitialFocusNode());
277
307
  return;
278
308
  }
279
-
280
309
  node.focus();
281
310
  state.mostRecentlyFocusedNode = node;
282
311
  if (isSelectableInput(node)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "focus-trap",
3
- "version": "4.0.2",
3
+ "version": "5.1.0",
4
4
  "description": "Trap focus within a DOM node.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "homepage": "https://github.com/davidtheclark/focus-trap#readme",
38
38
  "dependencies": {
39
- "tabbable": "^3.1.2",
39
+ "tabbable": "^4.0.0",
40
40
  "xtend": "^4.0.1"
41
41
  },
42
42
  "devDependencies": {