index.js 25 KB

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