focus-trap.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. /*!
  2. * focus-trap 7.0.0
  3. * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
  4. */
  5. 'use strict';
  6. Object.defineProperty(exports, '__esModule', { value: true });
  7. var tabbable = require('tabbable');
  8. function ownKeys(object, enumerableOnly) {
  9. var keys = Object.keys(object);
  10. if (Object.getOwnPropertySymbols) {
  11. var symbols = Object.getOwnPropertySymbols(object);
  12. enumerableOnly && (symbols = symbols.filter(function (sym) {
  13. return Object.getOwnPropertyDescriptor(object, sym).enumerable;
  14. })), keys.push.apply(keys, symbols);
  15. }
  16. return keys;
  17. }
  18. function _objectSpread2(target) {
  19. for (var i = 1; i < arguments.length; i++) {
  20. var source = null != arguments[i] ? arguments[i] : {};
  21. i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
  22. _defineProperty(target, key, source[key]);
  23. }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
  24. Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
  25. });
  26. }
  27. return target;
  28. }
  29. function _defineProperty(obj, key, value) {
  30. if (key in obj) {
  31. Object.defineProperty(obj, key, {
  32. value: value,
  33. enumerable: true,
  34. configurable: true,
  35. writable: true
  36. });
  37. } else {
  38. obj[key] = value;
  39. }
  40. return obj;
  41. }
  42. var activeFocusTraps = function () {
  43. var trapQueue = [];
  44. return {
  45. activateTrap: function activateTrap(trap) {
  46. if (trapQueue.length > 0) {
  47. var activeTrap = trapQueue[trapQueue.length - 1];
  48. if (activeTrap !== trap) {
  49. activeTrap.pause();
  50. }
  51. }
  52. var trapIndex = trapQueue.indexOf(trap);
  53. if (trapIndex === -1) {
  54. trapQueue.push(trap);
  55. } else {
  56. // move this existing trap to the front of the queue
  57. trapQueue.splice(trapIndex, 1);
  58. trapQueue.push(trap);
  59. }
  60. },
  61. deactivateTrap: function deactivateTrap(trap) {
  62. var trapIndex = trapQueue.indexOf(trap);
  63. if (trapIndex !== -1) {
  64. trapQueue.splice(trapIndex, 1);
  65. }
  66. if (trapQueue.length > 0) {
  67. trapQueue[trapQueue.length - 1].unpause();
  68. }
  69. }
  70. };
  71. }();
  72. var isSelectableInput = function isSelectableInput(node) {
  73. return node.tagName && node.tagName.toLowerCase() === 'input' && typeof node.select === 'function';
  74. };
  75. var isEscapeEvent = function isEscapeEvent(e) {
  76. return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
  77. };
  78. var isTabEvent = function isTabEvent(e) {
  79. return e.key === 'Tab' || e.keyCode === 9;
  80. };
  81. var delay = function delay(fn) {
  82. return setTimeout(fn, 0);
  83. }; // Array.find/findIndex() are not supported on IE; this replicates enough
  84. // of Array.findIndex() for our needs
  85. var findIndex = function findIndex(arr, fn) {
  86. var idx = -1;
  87. arr.every(function (value, i) {
  88. if (fn(value)) {
  89. idx = i;
  90. return false; // break
  91. }
  92. return true; // next
  93. });
  94. return idx;
  95. };
  96. /**
  97. * Get an option's value when it could be a plain value, or a handler that provides
  98. * the value.
  99. * @param {*} value Option's value to check.
  100. * @param {...*} [params] Any parameters to pass to the handler, if `value` is a function.
  101. * @returns {*} The `value`, or the handler's returned value.
  102. */
  103. var valueOrHandler = function valueOrHandler(value) {
  104. for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
  105. params[_key - 1] = arguments[_key];
  106. }
  107. return typeof value === 'function' ? value.apply(void 0, params) : value;
  108. };
  109. var getActualTarget = function getActualTarget(event) {
  110. // NOTE: If the trap is _inside_ a shadow DOM, event.target will always be the
  111. // shadow host. However, event.target.composedPath() will be an array of
  112. // nodes "clicked" from inner-most (the actual element inside the shadow) to
  113. // outer-most (the host HTML document). If we have access to composedPath(),
  114. // then use its first element; otherwise, fall back to event.target (and
  115. // this only works for an _open_ shadow DOM; otherwise,
  116. // composedPath()[0] === event.target always).
  117. return event.target.shadowRoot && typeof event.composedPath === 'function' ? event.composedPath()[0] : event.target;
  118. };
  119. var createFocusTrap = function createFocusTrap(elements, userOptions) {
  120. // SSR: a live trap shouldn't be created in this type of environment so this
  121. // should be safe code to execute if the `document` option isn't specified
  122. var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document;
  123. var config = _objectSpread2({
  124. returnFocusOnDeactivate: true,
  125. escapeDeactivates: true,
  126. delayInitialFocus: true
  127. }, userOptions);
  128. var state = {
  129. // containers given to createFocusTrap()
  130. // @type {Array<HTMLElement>}
  131. containers: [],
  132. // list of objects identifying tabbable nodes in `containers` in the trap
  133. // NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap
  134. // is active, but the trap should never get to a state where there isn't at least one group
  135. // with at least one tabbable node in it (that would lead to an error condition that would
  136. // result in an error being thrown)
  137. // @type {Array<{
  138. // container: HTMLElement,
  139. // tabbableNodes: Array<HTMLElement>, // empty if none
  140. // focusableNodes: Array<HTMLElement>, // empty if none
  141. // firstTabbableNode: HTMLElement|null,
  142. // lastTabbableNode: HTMLElement|null,
  143. // nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined
  144. // }>}
  145. containerGroups: [],
  146. // same order/length as `containers` list
  147. // references to objects in `containerGroups`, but only those that actually have
  148. // tabbable nodes in them
  149. // NOTE: same order as `containers` and `containerGroups`, but __not necessarily__
  150. // the same length
  151. tabbableGroups: [],
  152. nodeFocusedBeforeActivation: null,
  153. mostRecentlyFocusedNode: null,
  154. active: false,
  155. paused: false,
  156. // timer ID for when delayInitialFocus is true and initial focus in this trap
  157. // has been delayed during activation
  158. delayInitialFocusTimer: undefined
  159. };
  160. var trap; // eslint-disable-line prefer-const -- some private functions reference it, and its methods reference private functions, so we must declare here and define later
  161. /**
  162. * Gets a configuration option value.
  163. * @param {Object|undefined} configOverrideOptions If true, and option is defined in this set,
  164. * value will be taken from this object. Otherwise, value will be taken from base configuration.
  165. * @param {string} optionName Name of the option whose value is sought.
  166. * @param {string|undefined} [configOptionName] Name of option to use __instead of__ `optionName`
  167. * IIF `configOverrideOptions` is not defined. Otherwise, `optionName` is used.
  168. */
  169. var getOption = function getOption(configOverrideOptions, optionName, configOptionName) {
  170. return configOverrideOptions && configOverrideOptions[optionName] !== undefined ? configOverrideOptions[optionName] : config[configOptionName || optionName];
  171. };
  172. /**
  173. * Finds the index of the container that contains the element.
  174. * @param {HTMLElement} element
  175. * @returns {number} Index of the container in either `state.containers` or
  176. * `state.containerGroups` (the order/length of these lists are the same); -1
  177. * if the element isn't found.
  178. */
  179. var findContainerIndex = function findContainerIndex(element) {
  180. // NOTE: search `containerGroups` because it's possible a group contains no tabbable
  181. // nodes, but still contains focusable nodes (e.g. if they all have `tabindex=-1`)
  182. // and we still need to find the element in there
  183. return state.containerGroups.findIndex(function (_ref) {
  184. var container = _ref.container,
  185. tabbableNodes = _ref.tabbableNodes;
  186. return container.contains(element) || // fall back to explicit tabbable search which will take into consideration any
  187. // web components if the `tabbableOptions.getShadowRoot` option was used for
  188. // the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
  189. // look inside web components even if open)
  190. tabbableNodes.find(function (node) {
  191. return node === element;
  192. });
  193. });
  194. };
  195. /**
  196. * Gets the node for the given option, which is expected to be an option that
  197. * can be either a DOM node, a string that is a selector to get a node, `false`
  198. * (if a node is explicitly NOT given), or a function that returns any of these
  199. * values.
  200. * @param {string} optionName
  201. * @returns {undefined | false | HTMLElement | SVGElement} Returns
  202. * `undefined` if the option is not specified; `false` if the option
  203. * resolved to `false` (node explicitly not given); otherwise, the resolved
  204. * DOM node.
  205. * @throws {Error} If the option is set, not `false`, and is not, or does not
  206. * resolve to a node.
  207. */
  208. var getNodeForOption = function getNodeForOption(optionName) {
  209. var optionValue = config[optionName];
  210. if (typeof optionValue === 'function') {
  211. for (var _len2 = arguments.length, params = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
  212. params[_key2 - 1] = arguments[_key2];
  213. }
  214. optionValue = optionValue.apply(void 0, params);
  215. }
  216. if (optionValue === true) {
  217. optionValue = undefined; // use default value
  218. }
  219. if (!optionValue) {
  220. if (optionValue === undefined || optionValue === false) {
  221. return optionValue;
  222. } // else, empty string (invalid), null (invalid), 0 (invalid)
  223. throw new Error("`".concat(optionName, "` was specified but was not a node, or did not return a node"));
  224. }
  225. var node = optionValue; // could be HTMLElement, SVGElement, or non-empty string at this point
  226. if (typeof optionValue === 'string') {
  227. node = doc.querySelector(optionValue); // resolve to node, or null if fails
  228. if (!node) {
  229. throw new Error("`".concat(optionName, "` as selector refers to no known node"));
  230. }
  231. }
  232. return node;
  233. };
  234. var getInitialFocusNode = function getInitialFocusNode() {
  235. var node = getNodeForOption('initialFocus'); // false explicitly indicates we want no initialFocus at all
  236. if (node === false) {
  237. return false;
  238. }
  239. if (node === undefined) {
  240. // option not specified: use fallback options
  241. if (findContainerIndex(doc.activeElement) >= 0) {
  242. node = doc.activeElement;
  243. } else {
  244. var firstTabbableGroup = state.tabbableGroups[0];
  245. var firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode; // NOTE: `fallbackFocus` option function cannot return `false` (not supported)
  246. node = firstTabbableNode || getNodeForOption('fallbackFocus');
  247. }
  248. }
  249. if (!node) {
  250. throw new Error('Your focus-trap needs to have at least one focusable element');
  251. }
  252. return node;
  253. };
  254. var updateTabbableNodes = function updateTabbableNodes() {
  255. state.containerGroups = state.containers.map(function (container) {
  256. var tabbableNodes = tabbable.tabbable(container, config.tabbableOptions); // NOTE: if we have tabbable nodes, we must have focusable nodes; focusable nodes
  257. // are a superset of tabbable nodes
  258. var focusableNodes = tabbable.focusable(container, config.tabbableOptions);
  259. return {
  260. container: container,
  261. tabbableNodes: tabbableNodes,
  262. focusableNodes: focusableNodes,
  263. firstTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[0] : null,
  264. lastTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : null,
  265. /**
  266. * Finds the __tabbable__ node that follows the given node in the specified direction,
  267. * in this container, if any.
  268. * @param {HTMLElement} node
  269. * @param {boolean} [forward] True if going in forward tab order; false if going
  270. * in reverse.
  271. * @returns {HTMLElement|undefined} The next tabbable node, if any.
  272. */
  273. nextTabbableNode: function nextTabbableNode(node) {
  274. var forward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
  275. // NOTE: If tabindex is positive (in order to manipulate the tab order separate
  276. // from the DOM order), this __will not work__ because the list of focusableNodes,
  277. // while it contains tabbable nodes, does not sort its nodes in any order other
  278. // than DOM order, because it can't: Where would you place focusable (but not
  279. // tabbable) nodes in that order? They have no order, because they aren't tabbale...
  280. // Support for positive tabindex is already broken and hard to manage (possibly
  281. // not supportable, TBD), so this isn't going to make things worse than they
  282. // already are, and at least makes things better for the majority of cases where
  283. // tabindex is either 0/unset or negative.
  284. // FYI, positive tabindex issue: https://github.com/focus-trap/focus-trap/issues/375
  285. var nodeIdx = focusableNodes.findIndex(function (n) {
  286. return n === node;
  287. });
  288. if (nodeIdx < 0) {
  289. return undefined;
  290. }
  291. if (forward) {
  292. return focusableNodes.slice(nodeIdx + 1).find(function (n) {
  293. return tabbable.isTabbable(n, config.tabbableOptions);
  294. });
  295. }
  296. return focusableNodes.slice(0, nodeIdx).reverse().find(function (n) {
  297. return tabbable.isTabbable(n, config.tabbableOptions);
  298. });
  299. }
  300. };
  301. });
  302. state.tabbableGroups = state.containerGroups.filter(function (group) {
  303. return group.tabbableNodes.length > 0;
  304. }); // throw if no groups have tabbable nodes and we don't have a fallback focus node either
  305. if (state.tabbableGroups.length <= 0 && !getNodeForOption('fallbackFocus') // returning false not supported for this option
  306. ) {
  307. throw new Error('Your focus-trap must have at least one container with at least one tabbable node in it at all times');
  308. }
  309. };
  310. var tryFocus = function tryFocus(node) {
  311. if (node === false) {
  312. return;
  313. }
  314. if (node === doc.activeElement) {
  315. return;
  316. }
  317. if (!node || !node.focus) {
  318. tryFocus(getInitialFocusNode());
  319. return;
  320. }
  321. node.focus({
  322. preventScroll: !!config.preventScroll
  323. });
  324. state.mostRecentlyFocusedNode = node;
  325. if (isSelectableInput(node)) {
  326. node.select();
  327. }
  328. };
  329. var getReturnFocusNode = function getReturnFocusNode(previousActiveElement) {
  330. var node = getNodeForOption('setReturnFocus', previousActiveElement);
  331. return node ? node : node === false ? false : previousActiveElement;
  332. }; // This needs to be done on mousedown and touchstart instead of click
  333. // so that it precedes the focus event.
  334. var checkPointerDown = function checkPointerDown(e) {
  335. var target = getActualTarget(e);
  336. if (findContainerIndex(target) >= 0) {
  337. // allow the click since it ocurred inside the trap
  338. return;
  339. }
  340. if (valueOrHandler(config.clickOutsideDeactivates, e)) {
  341. // immediately deactivate the trap
  342. trap.deactivate({
  343. // if, on deactivation, we should return focus to the node originally-focused
  344. // when the trap was activated (or the configured `setReturnFocus` node),
  345. // then assume it's also OK to return focus to the outside node that was
  346. // just clicked, causing deactivation, as long as that node is focusable;
  347. // if it isn't focusable, then return focus to the original node focused
  348. // on activation (or the configured `setReturnFocus` node)
  349. // NOTE: by setting `returnFocus: false`, deactivate() will do nothing,
  350. // which will result in the outside click setting focus to the node
  351. // that was clicked, whether it's focusable or not; by setting
  352. // `returnFocus: true`, we'll attempt to re-focus the node originally-focused
  353. // on activation (or the configured `setReturnFocus` node)
  354. returnFocus: config.returnFocusOnDeactivate && !tabbable.isFocusable(target, config.tabbableOptions)
  355. });
  356. return;
  357. } // This is needed for mobile devices.
  358. // (If we'll only let `click` events through,
  359. // then on mobile they will be blocked anyways if `touchstart` is blocked.)
  360. if (valueOrHandler(config.allowOutsideClick, e)) {
  361. // allow the click outside the trap to take place
  362. return;
  363. } // otherwise, prevent the click
  364. e.preventDefault();
  365. }; // In case focus escapes the trap for some strange reason, pull it back in.
  366. var checkFocusIn = function checkFocusIn(e) {
  367. var target = getActualTarget(e);
  368. var targetContained = findContainerIndex(target) >= 0; // In Firefox when you Tab out of an iframe the Document is briefly focused.
  369. if (targetContained || target instanceof Document) {
  370. if (targetContained) {
  371. state.mostRecentlyFocusedNode = target;
  372. }
  373. } else {
  374. // escaped! pull it back in to where it just left
  375. e.stopImmediatePropagation();
  376. tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
  377. }
  378. }; // Hijack Tab events on the first and last focusable nodes of the trap,
  379. // in order to prevent focus from escaping. If it escapes for even a
  380. // moment it can end up scrolling the page and causing confusion so we
  381. // kind of need to capture the action at the keydown phase.
  382. var checkTab = function checkTab(e) {
  383. var target = getActualTarget(e);
  384. updateTabbableNodes();
  385. var destinationNode = null;
  386. if (state.tabbableGroups.length > 0) {
  387. // make sure the target is actually contained in a group
  388. // NOTE: the target may also be the container itself if it's focusable
  389. // with tabIndex='-1' and was given initial focus
  390. var containerIndex = findContainerIndex(target);
  391. var containerGroup = containerIndex >= 0 ? state.containerGroups[containerIndex] : undefined;
  392. if (containerIndex < 0) {
  393. // target not found in any group: quite possible focus has escaped the trap,
  394. // so bring it back in to...
  395. if (e.shiftKey) {
  396. // ...the last node in the last group
  397. destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode;
  398. } else {
  399. // ...the first node in the first group
  400. destinationNode = state.tabbableGroups[0].firstTabbableNode;
  401. }
  402. } else if (e.shiftKey) {
  403. // REVERSE
  404. // is the target the first tabbable node in a group?
  405. var startOfGroupIndex = findIndex(state.tabbableGroups, function (_ref2) {
  406. var firstTabbableNode = _ref2.firstTabbableNode;
  407. return target === firstTabbableNode;
  408. });
  409. if (startOfGroupIndex < 0 && (containerGroup.container === target || tabbable.isFocusable(target, config.tabbableOptions) && !tabbable.isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target, false))) {
  410. // an exception case where the target is either the container itself, or
  411. // a non-tabbable node that was given focus (i.e. tabindex is negative
  412. // and user clicked on it or node was programmatically given focus)
  413. // and is not followed by any other tabbable node, in which
  414. // case, we should handle shift+tab as if focus were on the container's
  415. // first tabbable node, and go to the last tabbable node of the LAST group
  416. startOfGroupIndex = containerIndex;
  417. }
  418. if (startOfGroupIndex >= 0) {
  419. // YES: then shift+tab should go to the last tabbable node in the
  420. // previous group (and wrap around to the last tabbable node of
  421. // the LAST group if it's the first tabbable node of the FIRST group)
  422. var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
  423. var destinationGroup = state.tabbableGroups[destinationGroupIndex];
  424. destinationNode = destinationGroup.lastTabbableNode;
  425. }
  426. } else {
  427. // FORWARD
  428. // is the target the last tabbable node in a group?
  429. var lastOfGroupIndex = findIndex(state.tabbableGroups, function (_ref3) {
  430. var lastTabbableNode = _ref3.lastTabbableNode;
  431. return target === lastTabbableNode;
  432. });
  433. if (lastOfGroupIndex < 0 && (containerGroup.container === target || tabbable.isFocusable(target, config.tabbableOptions) && !tabbable.isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target))) {
  434. // an exception case where the target is the container itself, or
  435. // a non-tabbable node that was given focus (i.e. tabindex is negative
  436. // and user clicked on it or node was programmatically given focus)
  437. // and is not followed by any other tabbable node, in which
  438. // case, we should handle tab as if focus were on the container's
  439. // last tabbable node, and go to the first tabbable node of the FIRST group
  440. lastOfGroupIndex = containerIndex;
  441. }
  442. if (lastOfGroupIndex >= 0) {
  443. // YES: then tab should go to the first tabbable node in the next
  444. // group (and wrap around to the first tabbable node of the FIRST
  445. // group if it's the last tabbable node of the LAST group)
  446. var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
  447. var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
  448. destinationNode = _destinationGroup.firstTabbableNode;
  449. }
  450. }
  451. } else {
  452. // NOTE: the fallbackFocus option does not support returning false to opt-out
  453. destinationNode = getNodeForOption('fallbackFocus');
  454. }
  455. if (destinationNode) {
  456. e.preventDefault();
  457. tryFocus(destinationNode);
  458. } // else, let the browser take care of [shift+]tab and move the focus
  459. };
  460. var checkKey = function checkKey(e) {
  461. if (isEscapeEvent(e) && valueOrHandler(config.escapeDeactivates, e) !== false) {
  462. e.preventDefault();
  463. trap.deactivate();
  464. return;
  465. }
  466. if (isTabEvent(e)) {
  467. checkTab(e);
  468. return;
  469. }
  470. };
  471. var checkClick = function checkClick(e) {
  472. var target = getActualTarget(e);
  473. if (findContainerIndex(target) >= 0) {
  474. return;
  475. }
  476. if (valueOrHandler(config.clickOutsideDeactivates, e)) {
  477. return;
  478. }
  479. if (valueOrHandler(config.allowOutsideClick, e)) {
  480. return;
  481. }
  482. e.preventDefault();
  483. e.stopImmediatePropagation();
  484. }; //
  485. // EVENT LISTENERS
  486. //
  487. var addListeners = function addListeners() {
  488. if (!state.active) {
  489. return;
  490. } // There can be only one listening focus trap at a time
  491. activeFocusTraps.activateTrap(trap); // Delay ensures that the focused element doesn't capture the event
  492. // that caused the focus trap activation.
  493. state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () {
  494. tryFocus(getInitialFocusNode());
  495. }) : tryFocus(getInitialFocusNode());
  496. doc.addEventListener('focusin', checkFocusIn, true);
  497. doc.addEventListener('mousedown', checkPointerDown, {
  498. capture: true,
  499. passive: false
  500. });
  501. doc.addEventListener('touchstart', checkPointerDown, {
  502. capture: true,
  503. passive: false
  504. });
  505. doc.addEventListener('click', checkClick, {
  506. capture: true,
  507. passive: false
  508. });
  509. doc.addEventListener('keydown', checkKey, {
  510. capture: true,
  511. passive: false
  512. });
  513. return trap;
  514. };
  515. var removeListeners = function removeListeners() {
  516. if (!state.active) {
  517. return;
  518. }
  519. doc.removeEventListener('focusin', checkFocusIn, true);
  520. doc.removeEventListener('mousedown', checkPointerDown, true);
  521. doc.removeEventListener('touchstart', checkPointerDown, true);
  522. doc.removeEventListener('click', checkClick, true);
  523. doc.removeEventListener('keydown', checkKey, true);
  524. return trap;
  525. }; //
  526. // TRAP DEFINITION
  527. //
  528. trap = {
  529. get active() {
  530. return state.active;
  531. },
  532. get paused() {
  533. return state.paused;
  534. },
  535. activate: function activate(activateOptions) {
  536. if (state.active) {
  537. return this;
  538. }
  539. var onActivate = getOption(activateOptions, 'onActivate');
  540. var onPostActivate = getOption(activateOptions, 'onPostActivate');
  541. var checkCanFocusTrap = getOption(activateOptions, 'checkCanFocusTrap');
  542. if (!checkCanFocusTrap) {
  543. updateTabbableNodes();
  544. }
  545. state.active = true;
  546. state.paused = false;
  547. state.nodeFocusedBeforeActivation = doc.activeElement;
  548. if (onActivate) {
  549. onActivate();
  550. }
  551. var finishActivation = function finishActivation() {
  552. if (checkCanFocusTrap) {
  553. updateTabbableNodes();
  554. }
  555. addListeners();
  556. if (onPostActivate) {
  557. onPostActivate();
  558. }
  559. };
  560. if (checkCanFocusTrap) {
  561. checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation);
  562. return this;
  563. }
  564. finishActivation();
  565. return this;
  566. },
  567. deactivate: function deactivate(deactivateOptions) {
  568. if (!state.active) {
  569. return this;
  570. }
  571. var options = _objectSpread2({
  572. onDeactivate: config.onDeactivate,
  573. onPostDeactivate: config.onPostDeactivate,
  574. checkCanReturnFocus: config.checkCanReturnFocus
  575. }, deactivateOptions);
  576. clearTimeout(state.delayInitialFocusTimer); // noop if undefined
  577. state.delayInitialFocusTimer = undefined;
  578. removeListeners();
  579. state.active = false;
  580. state.paused = false;
  581. activeFocusTraps.deactivateTrap(trap);
  582. var onDeactivate = getOption(options, 'onDeactivate');
  583. var onPostDeactivate = getOption(options, 'onPostDeactivate');
  584. var checkCanReturnFocus = getOption(options, 'checkCanReturnFocus');
  585. var returnFocus = getOption(options, 'returnFocus', 'returnFocusOnDeactivate');
  586. if (onDeactivate) {
  587. onDeactivate();
  588. }
  589. var finishDeactivation = function finishDeactivation() {
  590. delay(function () {
  591. if (returnFocus) {
  592. tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
  593. }
  594. if (onPostDeactivate) {
  595. onPostDeactivate();
  596. }
  597. });
  598. };
  599. if (returnFocus && checkCanReturnFocus) {
  600. checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation);
  601. return this;
  602. }
  603. finishDeactivation();
  604. return this;
  605. },
  606. pause: function pause() {
  607. if (state.paused || !state.active) {
  608. return this;
  609. }
  610. state.paused = true;
  611. removeListeners();
  612. return this;
  613. },
  614. unpause: function unpause() {
  615. if (!state.paused || !state.active) {
  616. return this;
  617. }
  618. state.paused = false;
  619. updateTabbableNodes();
  620. addListeners();
  621. return this;
  622. },
  623. updateContainerElements: function updateContainerElements(containerElements) {
  624. var elementsAsArray = [].concat(containerElements).filter(Boolean);
  625. state.containers = elementsAsArray.map(function (element) {
  626. return typeof element === 'string' ? doc.querySelector(element) : element;
  627. });
  628. if (state.active) {
  629. updateTabbableNodes();
  630. }
  631. return this;
  632. }
  633. }; // initialize container elements
  634. trap.updateContainerElements(elements);
  635. return trap;
  636. };
  637. exports.createFocusTrap = createFocusTrap;
  638. //# sourceMappingURL=focus-trap.js.map