focus-trap 6.3.0 → 6.6.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,62 @@
1
1
  # Changelog
2
2
 
3
+ ## 6.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 281e66c: Add option to allow no initial focus when trap activates via `initialFocus: false`
8
+
9
+ There may be cases where we don't want to focus the first tabbable element when a focus trap activates.
10
+
11
+ Examples use-cases:
12
+
13
+ - Modals/dialogs
14
+ - On mobile devices where "tabbing" doesn't make sense without a connected Bluetooth keyboard
15
+
16
+ In addition, this change ensures that any element inside the trap manually focused outside of `focus-trap` code will be brought back in focus if focus is somehow found outside of the trap.
17
+
18
+ Example usage:
19
+
20
+ When the trap activates, there will be no initially focused element inside the new trap.
21
+
22
+ ```js
23
+ const focusTrap = createFocusTrap('#some-container', {
24
+ initialFocus: false,
25
+ });
26
+ ```
27
+
28
+ - 75be463: `escapeDeactivates` can now be either a boolean (as before) or a function that takes an event and returns a boolean.
29
+
30
+ ### Patch Changes
31
+
32
+ - e2294f0: Fix race condition when activating a second trap where initial focus in the second trap may be thwarted because pausing of first trap clears the `delayInitialFocus` timer created for the second trap before during its activation sequence.
33
+
34
+ ## 6.5.1
35
+
36
+ ### Patch Changes
37
+
38
+ - c38bf3f: onPostDeactivate should always be called even if returnFocus/OnDeactivate is disabled.
39
+
40
+ ## 6.5.0
41
+
42
+ ### Minor Changes
43
+
44
+ - 278e77e: Adding 4 new configuration event options to improve support for animated dialogs and animated focus trap triggers: `checkCanFocusTrap()`, `onPostActivate()`, `checkCanReturnFocus()`, and `onPostDeactivate()`.
45
+
46
+ ### Patch Changes
47
+
48
+ - 8d11e15: Improve docs and types for most options, adding `SVGElement` as a supported type of "DOM node" since it supports the `focus()` method, same as `HTMLElement`.
49
+
50
+ ## 6.4.0
51
+
52
+ ### Minor Changes
53
+
54
+ - 21c82ce: Bump tabbable from 5.1.6 to 5.2.0. There should be no changes in behavior as a result of this upgrade as `focus-trap` does not currently leverage the new `displayCheck` option.
55
+
56
+ ### Patch Changes
57
+
58
+ - 1baf62e: Fix focus trapped on initial focus container with tabindex=-1 when pressing shift+tab (#363)
59
+
3
60
  ## 6.3.0
4
61
 
5
62
  ### Minor Changes
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # focus-trap [![CI](https://github.com/focus-trap/focus-trap/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/focus-trap/focus-trap/actions?query=workflow:CI+branch:master) [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE)
2
2
 
3
3
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
4
- [![All Contributors](https://img.shields.io/badge/all_contributors-14-orange.svg?style=flat-square)](#contributors)
4
+ [![All Contributors](https://img.shields.io/badge/all_contributors-17-orange.svg?style=flat-square)](#contributors)
5
5
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
6
6
 
7
7
  Trap focus within a DOM node.
@@ -28,7 +28,7 @@ When the focus trap is deactivated, this is what should happen:
28
28
  - Focus is passed to *whichever element had focus when the trap was activated* (e.g. the button that opened the modal or menu).
29
29
  - Tabbing and clicking behave normally everywhere.
30
30
 
31
- [Check out the demos.](http://focus-trap.github.io/focus-trap/demo/)
31
+ [Check out the demos.](http://focus-trap.github.io/focus-trap/)
32
32
 
33
33
  For more advanced usage (e.g. focus traps within focus traps), you can also pause a focus trap's behavior without deactivating it entirely, then unpause at will.
34
34
 
@@ -80,20 +80,25 @@ Returns a new focus trap on `element` (one or more "containers" of tabbable node
80
80
 
81
81
  > A focus trap must have at least one container with at least one tabbable/focusable node in it to be considered valid. While nodes can be added/removed at runtime, with the trap adjusting to added/removed tabbable nodes, __an error will be thrown__ if the trap ever gets into a state where it determines none of its containers have any tabbable nodes in them _and_ the `fallbackFocus` option does not resolve to an alternate node where focus can go.
82
82
 
83
- `createOptions`:
84
-
85
- - **onActivate** {function}: A function that will be called when the focus trap activates.
86
- - **onDeactivate** {function}: A function that will be called when the focus trap deactivates,
87
- - **initialFocus** {element|string|function}: By default, when a focus trap is activated the first element in the focus trap's tab order will receive focus. With this option you can specify a different element to receive that initial focus. 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.
88
- - **fallbackFocus** {element|string|function}: By default, an error will be thrown if the focus trap contains no elements in its tab order. With this option you can specify a fallback element to programmatically receive focus if no other tabbable elements are found. For example, you may want a popover's `<div>` to receive focus if the popover's content includes no tabbable elements. *Make sure the fallback element has a negative `tabindex` so it can be programmatically focused.* The option value can be a DOM node, a selector string (which will be passed to `document.querySelector()` to find the DOM node), or a function that returns a DOM node.
89
- - **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.
90
- - **clickOutsideDeactivates** {boolean|(e: MouseEvent) => boolean}: If `true` or returns `true`, a click outside the focus trap will deactivate the focus trap and allow the click event to do its thing (i.e. to pass-through to the element that was clicked). This option **takes precedence** over `allowOutsideClick` when it's set to `true`. Default: `false`.
83
+ #### createOptions
84
+
85
+ - **onActivate** `{() => void}`: A function that will be called **before** sending focus to the target element upon activation.
86
+ - **onPostActivate** `{() => void}`: A function that will be called **after** sending focus to the target element upon activation.
87
+ - **checkCanFocusTrap** `{(containers: Array<HTMLElement | SVGElement>) => Promise<void>}`: Animated dialogs have a small delay between when `onActivate` is called and when the focus trap is focusable. `checkCanFocusTrap` expects a promise to be returned. When that promise settles (resolves or rejects), focus will be sent to the first tabbable node (in tab order) in the focus trap (or the node configured in the `initialFocus` option). Due to the lack of Promise support, `checkCanFocusTrap` is not supported in IE unless you provide a Promise polyfill.
88
+ - **onDeactivate** `{() => void}`: A function that will be called **before** returning focus to the node that had focus prior to activation (or configured with the `setReturnFocus` option) upon deactivation.
89
+ - **onPostDeactivate** `{() => void}`: A function that will be called after the trap is deactivated, after `onDeactivate`. If the `returnFocus` deactivation option was set, it will be called **after** returning focus to the node that had focus prior to activation (or configured with the `setReturnFocus` option) upon deactivation; otherwise, it will be called after deactivation completes.
90
+ - **checkCanReturnFocus** `{(trigger: HTMLElement | SVGElement) => Promise<void>}`: An animated trigger button will have a small delay between when `onDeactivate` is called and when the focus is able to be sent back to the trigger. `checkCanReturnFocus` expects a promise to be returned. When that promise settles (resolves or rejects), focus will be sent to to the node that had focus prior to the activation of the trap (or the node configured in the `setReturnFocus` option). Due to the lack of Promise support, `checkCanReturnFocus` is not supported in IE unless you provide a Promise polyfill.
91
+ - **initialFocus** `{HTMLElement | SVGElement | string | () => HTMLElement | SVGElement | false}`: By default, when a focus trap is activated the first element in the focus trap's tab order will receive focus. With this option you can specify a different element to receive that initial focus. 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. You can also set this option to `false` to prevent any initial focus at all when the trap activates.
92
+ - **fallbackFocus** `{HTMLElement | SVGElement | string | () => HTMLElement | SVGElement}`: By default, an error will be thrown if the focus trap contains no elements in its tab order. With this option you can specify a fallback element to programmatically receive focus if no other tabbable elements are found. For example, you may want a popover's `<div>` to receive focus if the popover's content includes no tabbable elements. *Make sure the fallback element has a negative `tabindex` so it can be programmatically focused.* The option value can be a DOM node, a selector string (which will be passed to `document.querySelector()` to find the DOM node), or a function that returns a DOM node.
93
+ - **escapeDeactivates** `{boolean} | (e: KeyboardEvent) => boolean)`: Default: `true`. If `false` or returns `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. Note that if a function is given, it's only called if the ESC key was pressed.
94
+ - **clickOutsideDeactivates** `{boolean | (e: MouseEvent | TouchEvent) => boolean}`: If `true` or returns `true`, a click outside the focus trap will deactivate the focus trap and allow the click event to do its thing (i.e. to pass-through to the element that was clicked). This option **takes precedence** over `allowOutsideClick` when it's set to `true`. Default: `false`.
91
95
  - ⚠️ If you're using a password manager such as 1Password, where the app adds a clickable icon to all fillable fields, you should avoid using this option, and instead use the `allowOutsideClick` option to better control exactly when the focus trap can be deactivated. The clickable icons are usually positioned absolutely, floating on top of the fields, and therefore _not_ part of the container the trap is managing. When using the `clickOutsideDeactivates` option, clicking on a field's 1Password icon will likely cause the trap to be unintentionally deactivated.
92
- - **allowOutsideClick** {boolean|(e: MouseEvent) => boolean}: If set and is or returns `true`, a click outside the focus trap will not be prevented, even when `clickOutsideDeactivates` is `false`. When `clickOutsideDeactivates` is `true`, this option is **ignored** (i.e. if it's a function, it will not be called). Use this option to control if (and even which) clicks are allowed outside the trap in conjunction with `clickOutsideDeactivates: false`. Default: `false`.
93
- - **returnFocusOnDeactivate** {boolean}: Default: `true`. If `false`, when the trap is deactivated, focus will *not* return to the element that had focus before activation.
94
- - **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.
95
- - **preventScroll** {boolean}: By default, focus() will scroll to the element if not in viewport. It can produce unintended effects like scrolling back to the top of a modal. If set to `true`, no scroll will happen.
96
- - **delayInitialFocus** {boolean}: Default: `true`. Delays the autofocus when the focus trap is activated. This prevents elements within the focusable element from capturing the event that triggered the focus trap activation.
96
+ - **allowOutsideClick** `{boolean | (e: MouseEvent | TouchEvent) => boolean}`: If set and is or returns `true`, a click outside the focus trap will not be prevented, even when `clickOutsideDeactivates` is `false`. When `clickOutsideDeactivates` is `true`, this option is **ignored** (i.e. if it's a function, it will not be called). Use this option to control if (and even which) clicks are allowed outside the trap in conjunction with `clickOutsideDeactivates: false`. Default: `false`.
97
+ - ⚠️ If this is a function, it will be called **twice** on every click: First on `mousedown` (or `touchstart` on mobile), and then on the actual `click` if the function returned `true` on the first event. Be sure to check the event type if the double call is an issue in your code.
98
+ - **returnFocusOnDeactivate** `{boolean}`: Default: `true`. If `false`, when the trap is deactivated, focus will *not* return to the element that had focus before activation.
99
+ - **setReturnFocus** `{HTMLElement | SVGElement | string | () => HTMLElement | SVGElement}`: 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.
100
+ - **preventScroll** `{boolean}`: By default, focus() will scroll to the element if not in viewport. It can produce unintended effects like scrolling back to the top of a modal. If set to `true`, no scroll will happen.
101
+ - **delayInitialFocus** `{boolean}`: Default: `true`. Delays the autofocus to the next execution frame when the focus trap is activated. This prevents elements within the focusable element from capturing the event that triggered the focus trap activation.
97
102
 
98
103
  ### trap.activate([activateOptions])
99
104
 
@@ -113,7 +118,9 @@ Returns the `trap`.
113
118
 
114
119
  These options are used to override the focus trap's default behavior for this particular activation.
115
120
 
116
- - **onActivate** {function | null | false}: Default: whatever you chose for `createOptions.onActivate`. `null` or `false` are the equivalent of a `noop`.
121
+ - **onActivate** `{() => void}`: Default: whatever you chose for `createOptions.onActivate`. `null` or `false` are the equivalent of a `noop`.
122
+ - **onPostActivate** `{() => void}`: Default: whatever you chose for `createOptions.onPostActivate`. `null` or `false` are the equivalent of a `noop`.
123
+ - **checkCanFocusTrap** `{(containers: Array<HTMLElement | SVGElement>) => Promise<void>}`: Default: whatever you chose for `createOptions.checkCanFocusTrap`.
117
124
 
118
125
  ### trap.deactivate([deactivateOptions])
119
126
 
@@ -125,8 +132,10 @@ Returns the `trap`.
125
132
 
126
133
  These options are used to override the focus trap's default behavior for this particular deactivation.
127
134
 
128
- - **returnFocus** {boolean}: Default: whatever you chose for `createOptions.returnFocusOnDeactivate`.
129
- - **onDeactivate** {function | null | false}: Default: whatever you chose for `createOptions.onDeactivate`. `null` or `false` are the equivalent of a `noop`.
135
+ - **returnFocus** `{boolean}`: Default: whatever you chose for `createOptions.returnFocusOnDeactivate`.
136
+ - **onDeactivate** `{() => void}`: Default: whatever you chose for `createOptions.onDeactivate`. `null` or `false` are the equivalent of a `noop`.
137
+ - **onPostDeactivate** `{() => void}`: Default: whatever you chose for `createOptions.onPostDeactivate`. `null` or `false` are the equivalent of a `noop`.
138
+ - **checkCanReturnFocus** `{(trigger: HTMLElement | SVGElement) => Promise<void>}`: Default: whatever you chose for `createOptions.checkCanReturnFocus`. Not called if the `returnFocus` option is falsy. `trigger` is either the originally focused node prior to activation, or the result of the `setReturnFocus` configuration option.
130
139
 
131
140
  ### trap.pause()
132
141
 
@@ -162,7 +171,7 @@ Returns the `trap`.
162
171
 
163
172
  ## Examples
164
173
 
165
- Read code in `demo/` and [see how it works](http://focus-trap.github.io/focus-trap/demo/).
174
+ Read code in `docs/` and [see how it works](http://focus-trap.github.io/focus-trap/).
166
175
 
167
176
  Here's what happens in `default.js` (the "default behavior" demo):
168
177
 
@@ -172,25 +181,16 @@ const { createFocusTrap } = require('../../dist/focus-trap');
172
181
  const container = document.getElementById('default');
173
182
 
174
183
  const focusTrap = createFocusTrap('#default', {
175
- onActivate: function () {
176
- container.className = 'trap is-active';
177
- },
178
- onDeactivate: function () {
179
- container.className = 'trap';
180
- },
184
+ onActivate: () => container.classList.add('is-active'),
185
+ onDeactivate: () => container.classList.remove('is-active'),
181
186
  });
182
187
 
183
188
  document
184
189
  .getElementById('activate-default')
185
- .addEventListener('click', function () {
186
- focusTrap.activate();
187
- });
188
-
190
+ .addEventListener('click', focusTrap.activate);
189
191
  document
190
192
  .getElementById('deactivate-default')
191
- .addEventListener('click', function () {
192
- focusTrap.deactivate();
193
- });
193
+ .addEventListener('click', focusTrap.deactivate);
194
194
  ```
195
195
 
196
196
  ## Other details
@@ -232,25 +232,31 @@ In alphabetical order:
232
232
  <!-- markdownlint-disable -->
233
233
  <table>
234
234
  <tr>
235
- <td align="center"><a href="http://davidtheclark.com/"><img src="https://avatars2.githubusercontent.com/u/628431?v=4" width="100px;" alt=""/><br /><sub><b>David Clark</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=davidtheclark" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Adavidtheclark" title="Bug reports">🐛</a> <a href="#infra-davidtheclark" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=davidtheclark" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=davidtheclark" title="Documentation">📖</a> <a href="#maintenance-davidtheclark" title="Maintenance">🚧</a></td>
236
- <td align="center"><a href="https://stefancameron.com/"><img src="https://avatars3.githubusercontent.com/u/2855350?v=4" width="100px;" alt=""/><br /><sub><b>Stefan Cameron</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=stefcameron" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Astefcameron" title="Bug reports">🐛</a> <a href="#infra-stefcameron" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=stefcameron" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=stefcameron" title="Documentation">📖</a> <a href="#maintenance-stefcameron" title="Maintenance">🚧</a></td>
237
- <td align="center"><a href="https://github.com/liunate"><img src="https://avatars2.githubusercontent.com/u/38996291?v=4" width="100px;" alt=""/><br /><sub><b>Nate Liu</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=liunate" title="Tests">⚠️</a></td>
238
- <td align="center"><a href="https://github.com/sadick254"><img src="https://avatars2.githubusercontent.com/u/5238135?v=4" width="100px;" alt=""/><br /><sub><b>Sadick</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=sadick254" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=sadick254" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=sadick254" title="Documentation">📖</a></td>
239
- <td align="center"><a href="https://github.com/michael-ar"><img src="https://avatars3.githubusercontent.com/u/18557997?v=4" width="100px;" alt=""/><br /><sub><b>Michael Reynolds</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Amichael-ar" title="Bug reports">🐛</a></td>
240
- <td align="center"><a href="https://seanmcp.com/"><img src="https://avatars1.githubusercontent.com/u/6360367?v=4" width="100px;" alt=""/><br /><sub><b>Sean McPherson</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=SeanMcP" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=SeanMcP" title="Documentation">📖</a></td>
241
- <td align="center"><a href="https://github.com/features/security"><img src="https://avatars1.githubusercontent.com/u/27347476?v=4" width="100px;" alt=""/><br /><sub><b>Dependabot</b></sub></a><br /><a href="#maintenance-dependabot" title="Maintenance">🚧</a></td>
235
+ <td align="center"><a href="https://github.com/bparish628"><img src="https://avatars1.githubusercontent.com/u/8492971?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Benjamin Parish</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Abparish628" title="Bug reports">🐛</a></td>
236
+ <td align="center"><a href="https://clintgoodman.com"><img src="https://avatars3.githubusercontent.com/u/5473697?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Clint Goodman</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=cgood92" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=cgood92" title="Documentation">📖</a> <a href="#example-cgood92" title="Examples">💡</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=cgood92" title="Tests">⚠️</a></td>
237
+ <td align="center"><a href="https://github.com/Dan503"><img src="https://avatars.githubusercontent.com/u/10610368?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Tonon</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=Dan503" title="Documentation">📖</a> <a href="#tool-Dan503" title="Tools">🔧</a> <a href="#a11y-Dan503" title="Accessibility">️️️️♿️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=Dan503" title="Code">💻</a></td>
238
+ <td align="center"><a href="http://davidtheclark.com/"><img src="https://avatars2.githubusercontent.com/u/628431?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Clark</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=davidtheclark" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Adavidtheclark" title="Bug reports">🐛</a> <a href="#infra-davidtheclark" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=davidtheclark" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=davidtheclark" title="Documentation">📖</a> <a href="#maintenance-davidtheclark" title="Maintenance">🚧</a></td>
239
+ <td align="center"><a href="https://github.com/features/security"><img src="https://avatars1.githubusercontent.com/u/27347476?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dependabot</b></sub></a><br /><a href="#maintenance-dependabot" title="Maintenance">🚧</a></td>
240
+ <td align="center"><a href="https://github.com/michael-ar"><img src="https://avatars3.githubusercontent.com/u/18557997?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Reynolds</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Amichael-ar" title="Bug reports">🐛</a></td>
241
+ <td align="center"><a href="https://github.com/liunate"><img src="https://avatars2.githubusercontent.com/u/38996291?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nate Liu</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=liunate" title="Tests">⚠️</a></td>
242
242
  </tr>
243
243
  <tr>
244
- <td align="center"><a href="https://recollectr.io"><img src="https://avatars2.githubusercontent.com/u/6835891?v=4" width="100px;" alt=""/><br /><sub><b>Slapbox</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3ASlapbox" title="Bug reports">🐛</a></td>
245
- <td align="center"><a href="https://github.com/bparish628"><img src="https://avatars1.githubusercontent.com/u/8492971?v=4" width="100px;" alt=""/><br /><sub><b>Benjamin Parish</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Abparish628" title="Bug reports">🐛</a></td>
246
- <td align="center"><a href="https://scottblinch.me/"><img src="https://avatars2.githubusercontent.com/u/4682114?v=4" width="100px;" alt=""/><br /><sub><b>Scott Blinch</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=scottblinch" title="Documentation">📖</a></td>
247
- <td align="center"><a href="https://clintgoodman.com"><img src="https://avatars3.githubusercontent.com/u/5473697?v=4" width="100px;" alt=""/><br /><sub><b>Clint Goodman</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=cgood92" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=cgood92" title="Documentation">📖</a> <a href="#example-cgood92" title="Examples">💡</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=cgood92" title="Tests">⚠️</a></td>
248
- <td align="center"><a href="https://github.com/zioth"><img src="https://avatars3.githubusercontent.com/u/945603?v=4" width="100px;" alt=""/><br /><sub><b>Zioth</b></sub></a><br /><a href="#ideas-zioth" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Azioth" title="Bug reports">🐛</a></td>
249
- <td align="center"><a href="https://github.com/randypuro"><img src="https://avatars2.githubusercontent.com/u/2579?v=4" width="100px;" alt=""/><br /><sub><b>Randy Puro</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Arandypuro" title="Bug reports">🐛</a></td>
250
- <td align="center"><a href="http://tylerhawkins.info/201R/"><img src="https://avatars0.githubusercontent.com/u/13806458?v=4" width="100px;" alt=""/><br /><sub><b>Tyler Hawkins</b></sub></a><br /><a href="#tool-thawkin3" title="Tools">🔧</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=thawkin3" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=thawkin3" title="Documentation">📖</a></td>
244
+ <td align="center"><a href="https://github.com/far-fetched"><img src="https://avatars.githubusercontent.com/u/11621383?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Piotr Panek</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Afar-fetched" title="Bug reports">🐛</a></td>
245
+ <td align="center"><a href="https://github.com/randypuro"><img src="https://avatars2.githubusercontent.com/u/2579?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Randy Puro</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Arandypuro" title="Bug reports">🐛</a></td>
246
+ <td align="center"><a href="https://github.com/sadick254"><img src="https://avatars2.githubusercontent.com/u/5238135?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sadick</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=sadick254" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=sadick254" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=sadick254" title="Documentation">📖</a></td>
247
+ <td align="center"><a href="https://scottblinch.me/"><img src="https://avatars2.githubusercontent.com/u/4682114?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Scott Blinch</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=scottblinch" title="Documentation">📖</a></td>
248
+ <td align="center"><a href="https://seanmcp.com/"><img src="https://avatars1.githubusercontent.com/u/6360367?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sean McPherson</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=SeanMcP" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=SeanMcP" title="Documentation">📖</a></td>
249
+ <td align="center"><a href="https://recollectr.io"><img src="https://avatars2.githubusercontent.com/u/6835891?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Slapbox</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3ASlapbox" title="Bug reports">🐛</a></td>
250
+ <td align="center"><a href="https://stefancameron.com/"><img src="https://avatars3.githubusercontent.com/u/2855350?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Stefan Cameron</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=stefcameron" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Astefcameron" title="Bug reports">🐛</a> <a href="#infra-stefcameron" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=stefcameron" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=stefcameron" title="Documentation">📖</a> <a href="#maintenance-stefcameron" title="Maintenance">🚧</a></td>
251
+ </tr>
252
+ <tr>
253
+ <td align="center"><a href="http://tylerhawkins.info/201R/"><img src="https://avatars0.githubusercontent.com/u/13806458?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tyler Hawkins</b></sub></a><br /><a href="#tool-thawkin3" title="Tools">🔧</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=thawkin3" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=thawkin3" title="Documentation">📖</a></td>
254
+ <td align="center"><a href="http://willmruzek.com/"><img src="https://avatars.githubusercontent.com/u/108522?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Will Mruzek</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=mruzekw" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=mruzekw" title="Documentation">📖</a> <a href="#example-mruzekw" title="Examples">💡</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=mruzekw" title="Tests">⚠️</a> <a href="#question-mruzekw" title="Answering Questions">💬</a></td>
255
+ <td align="center"><a href="https://github.com/zioth"><img src="https://avatars3.githubusercontent.com/u/945603?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zioth</b></sub></a><br /><a href="#ideas-zioth" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Azioth" title="Bug reports">🐛</a></td>
251
256
  </tr>
252
257
  </table>
253
258
 
254
- <!-- markdownlint-enable -->
259
+ <!-- markdownlint-restore -->
255
260
  <!-- prettier-ignore-end -->
261
+
256
262
  <!-- ALL-CONTRIBUTORS-LIST:END -->
@@ -1,32 +1,21 @@
1
1
  /*!
2
- * focus-trap 6.3.0
2
+ * focus-trap 6.6.0
3
3
  * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
4
4
  */
5
5
  import { tabbable, isFocusable } from 'tabbable';
6
6
 
7
- function _defineProperty(obj, key, value) {
8
- if (key in obj) {
9
- Object.defineProperty(obj, key, {
10
- value: value,
11
- enumerable: true,
12
- configurable: true,
13
- writable: true
14
- });
15
- } else {
16
- obj[key] = value;
17
- }
18
-
19
- return obj;
20
- }
21
-
22
7
  function ownKeys(object, enumerableOnly) {
23
8
  var keys = Object.keys(object);
24
9
 
25
10
  if (Object.getOwnPropertySymbols) {
26
11
  var symbols = Object.getOwnPropertySymbols(object);
27
- if (enumerableOnly) symbols = symbols.filter(function (sym) {
28
- return Object.getOwnPropertyDescriptor(object, sym).enumerable;
29
- });
12
+
13
+ if (enumerableOnly) {
14
+ symbols = symbols.filter(function (sym) {
15
+ return Object.getOwnPropertyDescriptor(object, sym).enumerable;
16
+ });
17
+ }
18
+
30
19
  keys.push.apply(keys, symbols);
31
20
  }
32
21
 
@@ -53,7 +42,20 @@ function _objectSpread2(target) {
53
42
  return target;
54
43
  }
55
44
 
56
- var activeFocusDelay;
45
+ function _defineProperty(obj, key, value) {
46
+ if (key in obj) {
47
+ Object.defineProperty(obj, key, {
48
+ value: value,
49
+ enumerable: true,
50
+ configurable: true,
51
+ writable: true
52
+ });
53
+ } else {
54
+ obj[key] = value;
55
+ }
56
+
57
+ return obj;
58
+ }
57
59
 
58
60
  var activeFocusTraps = function () {
59
61
  var trapQueue = [];
@@ -161,10 +163,17 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
161
163
  nodeFocusedBeforeActivation: null,
162
164
  mostRecentlyFocusedNode: null,
163
165
  active: false,
164
- paused: false
166
+ paused: false,
167
+ // timer ID for when delayInitialFocus is true and initial focus in this trap
168
+ // has been delayed during activation
169
+ delayInitialFocusTimer: undefined
165
170
  };
166
171
  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
167
172
 
173
+ var getOption = function getOption(configOverrideOptions, optionName, configOptionName) {
174
+ return configOverrideOptions && configOverrideOptions[optionName] !== undefined ? configOverrideOptions[optionName] : config[configOptionName || optionName];
175
+ };
176
+
168
177
  var containersContain = function containersContain(element) {
169
178
  return state.containers.some(function (container) {
170
179
  return container.contains(element);
@@ -200,7 +209,11 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
200
209
  };
201
210
 
202
211
  var getInitialFocusNode = function getInitialFocusNode() {
203
- var node;
212
+ var node; // false indicates we want no initialFocus at all
213
+
214
+ if (getOption({}, 'initialFocus') === false) {
215
+ return false;
216
+ }
204
217
 
205
218
  if (getNodeForOption('initialFocus') !== null) {
206
219
  node = getNodeForOption('initialFocus');
@@ -243,6 +256,10 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
243
256
  };
244
257
 
245
258
  var tryFocus = function tryFocus(node) {
259
+ if (node === false) {
260
+ return;
261
+ }
262
+
246
263
  if (node === doc.activeElement) {
247
264
  return;
248
265
  }
@@ -331,6 +348,8 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
331
348
 
332
349
  if (state.tabbableGroups.length > 0) {
333
350
  // make sure the target is actually contained in a group
351
+ // NOTE: the target may also be the container itself if it's tabbable
352
+ // with tabIndex='-1' and was given initial focus
334
353
  var containerIndex = findIndex(state.tabbableGroups, function (_ref) {
335
354
  var container = _ref.container;
336
355
  return container.contains(e.target);
@@ -348,24 +367,46 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
348
367
  }
349
368
  } else if (e.shiftKey) {
350
369
  // REVERSE
370
+ // is the target the first tabbable node in a group?
351
371
  var startOfGroupIndex = findIndex(state.tabbableGroups, function (_ref2) {
352
372
  var firstTabbableNode = _ref2.firstTabbableNode;
353
373
  return e.target === firstTabbableNode;
354
374
  });
355
375
 
376
+ if (startOfGroupIndex < 0 && state.tabbableGroups[containerIndex].container === e.target) {
377
+ // an exception case where the target is the container itself, in which
378
+ // case, we should handle shift+tab as if focus were on the container's
379
+ // first tabbable node, and go to the last tabbable node of the LAST group
380
+ startOfGroupIndex = containerIndex;
381
+ }
382
+
356
383
  if (startOfGroupIndex >= 0) {
384
+ // YES: then shift+tab should go to the last tabbable node in the
385
+ // previous group (and wrap around to the last tabbable node of
386
+ // the LAST group if it's the first tabbable node of the FIRST group)
357
387
  var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
358
388
  var destinationGroup = state.tabbableGroups[destinationGroupIndex];
359
389
  destinationNode = destinationGroup.lastTabbableNode;
360
390
  }
361
391
  } else {
362
392
  // FORWARD
393
+ // is the target the last tabbable node in a group?
363
394
  var lastOfGroupIndex = findIndex(state.tabbableGroups, function (_ref3) {
364
395
  var lastTabbableNode = _ref3.lastTabbableNode;
365
396
  return e.target === lastTabbableNode;
366
397
  });
367
398
 
399
+ if (lastOfGroupIndex < 0 && state.tabbableGroups[containerIndex].container === e.target) {
400
+ // an exception case where the target is the container itself, in which
401
+ // case, we should handle tab as if focus were on the container's
402
+ // last tabbable node, and go to the first tabbable node of the FIRST group
403
+ lastOfGroupIndex = containerIndex;
404
+ }
405
+
368
406
  if (lastOfGroupIndex >= 0) {
407
+ // YES: then tab should go to the first tabbable node in the next
408
+ // group (and wrap around to the first tabbable node of the FIRST
409
+ // group if it's the last tabbable node of the LAST group)
369
410
  var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
370
411
 
371
412
  var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
@@ -379,11 +420,12 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
379
420
  if (destinationNode) {
380
421
  e.preventDefault();
381
422
  tryFocus(destinationNode);
382
- }
423
+ } // else, let the browser take care of [shift+]tab and move the focus
424
+
383
425
  };
384
426
 
385
427
  var checkKey = function checkKey(e) {
386
- if (config.escapeDeactivates !== false && isEscapeEvent(e)) {
428
+ if (isEscapeEvent(e) && valueOrHandler(config.escapeDeactivates) !== false) {
387
429
  e.preventDefault();
388
430
  trap.deactivate();
389
431
  return;
@@ -424,7 +466,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
424
466
  activeFocusTraps.activateTrap(trap); // Delay ensures that the focused element doesn't capture the event
425
467
  // that caused the focus trap activation.
426
468
 
427
- activeFocusDelay = config.delayInitialFocus ? delay(function () {
469
+ state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () {
428
470
  tryFocus(getInitialFocusNode());
429
471
  }) : tryFocus(getInitialFocusNode());
430
472
  doc.addEventListener('focusin', checkFocusIn, true);
@@ -469,17 +511,40 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
469
511
  return this;
470
512
  }
471
513
 
472
- updateTabbableNodes();
514
+ var onActivate = getOption(activateOptions, 'onActivate');
515
+ var onPostActivate = getOption(activateOptions, 'onPostActivate');
516
+ var checkCanFocusTrap = getOption(activateOptions, 'checkCanFocusTrap');
517
+
518
+ if (!checkCanFocusTrap) {
519
+ updateTabbableNodes();
520
+ }
521
+
473
522
  state.active = true;
474
523
  state.paused = false;
475
524
  state.nodeFocusedBeforeActivation = doc.activeElement;
476
- var onActivate = activateOptions && activateOptions.onActivate ? activateOptions.onActivate : config.onActivate;
477
525
 
478
526
  if (onActivate) {
479
527
  onActivate();
480
528
  }
481
529
 
482
- addListeners();
530
+ var finishActivation = function finishActivation() {
531
+ if (checkCanFocusTrap) {
532
+ updateTabbableNodes();
533
+ }
534
+
535
+ addListeners();
536
+
537
+ if (onPostActivate) {
538
+ onPostActivate();
539
+ }
540
+ };
541
+
542
+ if (checkCanFocusTrap) {
543
+ checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation);
544
+ return this;
545
+ }
546
+
547
+ finishActivation();
483
548
  return this;
484
549
  },
485
550
  deactivate: function deactivate(deactivateOptions) {
@@ -487,25 +552,41 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
487
552
  return this;
488
553
  }
489
554
 
490
- clearTimeout(activeFocusDelay);
555
+ clearTimeout(state.delayInitialFocusTimer); // noop if undefined
556
+
557
+ state.delayInitialFocusTimer = undefined;
491
558
  removeListeners();
492
559
  state.active = false;
493
560
  state.paused = false;
494
561
  activeFocusTraps.deactivateTrap(trap);
495
- var onDeactivate = deactivateOptions && deactivateOptions.onDeactivate !== undefined ? deactivateOptions.onDeactivate : config.onDeactivate;
562
+ var onDeactivate = getOption(deactivateOptions, 'onDeactivate');
563
+ var onPostDeactivate = getOption(deactivateOptions, 'onPostDeactivate');
564
+ var checkCanReturnFocus = getOption(deactivateOptions, 'checkCanReturnFocus');
496
565
 
497
566
  if (onDeactivate) {
498
567
  onDeactivate();
499
568
  }
500
569
 
501
- var returnFocus = deactivateOptions && deactivateOptions.returnFocus !== undefined ? deactivateOptions.returnFocus : config.returnFocusOnDeactivate;
570
+ var returnFocus = getOption(deactivateOptions, 'returnFocus', 'returnFocusOnDeactivate');
502
571
 
503
- if (returnFocus) {
572
+ var finishDeactivation = function finishDeactivation() {
504
573
  delay(function () {
505
- tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
574
+ if (returnFocus) {
575
+ tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
576
+ }
577
+
578
+ if (onPostDeactivate) {
579
+ onPostDeactivate();
580
+ }
506
581
  });
582
+ };
583
+
584
+ if (returnFocus && checkCanReturnFocus) {
585
+ checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation);
586
+ return this;
507
587
  }
508
588
 
589
+ finishDeactivation();
509
590
  return this;
510
591
  },
511
592
  pause: function pause() {