initial vitepress site with basic nav
This commit is contained in:
parent
a7df2e049d
commit
2029f16583
1900 changed files with 1014692 additions and 0 deletions
428
node_modules/focus-trap/CHANGELOG.md
generated
vendored
Normal file
428
node_modules/focus-trap/CHANGELOG.md
generated
vendored
Normal file
|
@ -0,0 +1,428 @@
|
|||
# Changelog
|
||||
|
||||
## 7.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 134678b: Ensure focus is kept inside the trap when focused element is removed from the DOM ([focus-trap/focus-trap-react#962](https://github.com/focus-trap/focus-trap-react/issues/962))
|
||||
|
||||
## 7.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- db62ce3: Clicking on open shadowDOM components within a focus trap's container when `clickOutsideDeactivates=true` should not deactivate the focus trap. ([#959](https://github.com/focus-trap/focus-trap/issues/959))
|
||||
|
||||
## 7.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4f720ff: Bump tabbable to v6.1.2 for nwsapi patch
|
||||
|
||||
## 7.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- da97007: Added new onPost/Pause and onPost/Unpause hooks when un/pausing a trap (also called when auto-un/paused as a result of de/activating a second trap while another is currently active). ([focus-trap-react#948](https://github.com/focus-trap/focus-trap-react/issues/948))
|
||||
|
||||
## 7.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a00cfa5: Bump tabbable to v6.1.1 for JSDom fixes
|
||||
|
||||
## 7.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ae7129d: Bump tabbable to v6.1.0 adding support for the new HTML `inert` attribute in browsers that support it (NOTE: FireFox does not support it at this time). Also fixes a bug. See tabbable CHANGELOG for more info.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a27ad58: Fix incorrect behavior of `returnFocusOnDeactivate` option when set to true (or defaulted to true) along with `clickOutsideDeactivates=true` and the outside click that deactivates is on a focusable node. Focus was remaining on that node instead of returning to the node focused just prior to activation. ([#893](https://github.com/focus-trap/focus-trap/issues/893))
|
||||
|
||||
## 7.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- b0482af: Add new `isKeyForward()` and `isKeyBackward()` options ([#612](https://github.com/focus-trap/focus-trap/issues/612))
|
||||
|
||||
## 7.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 88cc9bc: Provide new `trapStack` option to make it possible to coordinate auto-activation/pausing between multiple `focus-trap` instances on the same page.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4f97b38: Bump tabbable to v6.0.1 for a bug fix. See tabbable's changelog for more details.
|
||||
- f13de76: Mention special Safari setting to enable normal DOM-based tab order in README. [#783](https://github.com/focus-trap/focus-trap/issues/783)
|
||||
|
||||
## 7.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 5b64423: Revised and clarified official browser support (still as broad and deep as _reasonably_ possible).
|
||||
- 47f62ac: 🚨 **Breaking:** Tabbable dependency has been updated to v6.0.0 and contains a breaking change related to detached nodes with its default `displayCheck` setting. See tabbable's [changelog](https://github.com/focus-trap/tabbable/blob/master/CHANGELOG.md#600) for more information.
|
||||
- 5b64423: 🚨 **Breaking:** Dropped support of IE browsers, all versions.
|
||||
- IE11 was [officially retired](https://blogs.windows.com/windowsexperience/2022/06/15/internet-explorer-11-has-retired-and-is-officially-out-of-support-what-you-need-to-know/) on June 15, 2022 (6 weeks ago). There are no longer any versions of IE that are still maintained or even supported by Microsoft.
|
||||
|
||||
## 6.9.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f68882e: Fix docs and typings to clarify that initialFocus, fallbackFocus, and setReturnFocus options can be functions that also return selector strings.
|
||||
|
||||
## 6.9.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8a8b1f1: Bump tabbable to v5.3.3 to pick up a small bug fix to web component (shadow DOM) support.
|
||||
|
||||
## 6.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ef0ce48: Handle unexpected param (true) passed as the value for the `initialFocus`, `fallbackFocus`, and `setReturnFocus` options: Ignore and perform default behavior.
|
||||
|
||||
## 6.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 83262a7: Bumps tabbable to v5.3.2 to pick-up a fix to `displayCheck=full` (default) option behavior that caused issues with detached nodes.
|
||||
|
||||
## 6.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 2a57e4b: Add new `trap.active` and `trap.paused` readonly state properties on the trap so that the trap's active/paused state can be queried.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8fd49df: Fixed bug where `clickOutsideDeactivate` handler would get called on the 'click' event even if the node clicked was in the trap. As with 'mousedown' and 'touchstart' events where this option is also used, the handler should only get called if the target node is _outside_ the trap.
|
||||
- c32c60a: Fixed: onDeactivate, onPostDeactivate, and checkCanReturnFocus options originally given to createFocusTrap() were not being used by default when calling `trap.deactivate({...})` with an option set even if that option set didn't specify any overrides of these options.
|
||||
|
||||
## 6.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7c86111:
|
||||
- Bump tabbable to `^5.3.1` (fixing previous update which was incorrectly set to `5.3.0`).
|
||||
- Fix `tabbableOptions` not being used in all internal uses of tabbable APIs.
|
||||
- Expose `displayCheck` option in `tabbableOptions` typings and pass it through to tabbable APIs.
|
||||
- Add info to README about testing traps in JSDom (which is not officially supported).
|
||||
|
||||
## 6.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 21458c9: Bumps tabbable to v5.3.0 and includes all changes from the past v6.8.0 beta releases. The big new feature is opt-in Shadow DOM support in tabbable, and a new `getShadowRoot` tabbable option exposed in a new `tabbableOptions` focus-trap config option.
|
||||
- ⚠️ This will likely break your tests **if you're using JSDom** (e.g. with Jest). See [testing in JSDom](./README.md#testing-in-jsdom) for more info.
|
||||
|
||||
## 6.8.0-beta.2
|
||||
|
||||
- When updating tabbable nodes, make sure that `getShadowRoot` tabbable option is also passed to `focusable()`.
|
||||
- Fix bug where having a tabbable node inside a web component in the middle of a tab sequence would cause the tab key to seemingly stop working just before focus should move to it ((#643)[https://github.com/focus-trap/focus-trap/issues/643]).
|
||||
- Bumps tabbable to `v5.3.0-beta.1`
|
||||
|
||||
## 6.8.0-beta.1
|
||||
|
||||
- Previous beta didn't include new source. This one does.
|
||||
|
||||
## 6.8.0-beta.0
|
||||
|
||||
- Adds new `tabbableOptions` configuration option, which allows specifically for the new `getShadowRoot` Tabbable configuration option: `focusTrap.createFocusTrap(rootElement, { tabbableOptions: { getShadowRoot: (node) => closedShadowRoot } })`, for example (where your code has the reference to `closedShadowRoot` previously created on `node` which Tabbable cannot find on its own).
|
||||
- Bumps tabbable to `v5.3.0-beta.0`
|
||||
|
||||
## 6.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ab20d3d: Fix issue with focusing negative tabindex node and then tabbing away when this node is _not_ the last node in the trap's container ((#611)[https://github.com/focus-trap/focus-trap/issues/611])
|
||||
|
||||
## 6.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c932330: Fixed bug where tabbing forward from an element with negative tabindex that is last in the trap would result in focus remaining on that element ([565](https://github.com/focus-trap/focus-trap/issues/565))
|
||||
|
||||
## 6.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 28a069f: Fix bug from #504 where it's no longer possible to create a trap without any options [#525]
|
||||
|
||||
## 6.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 893dd2c: Add `document` option to support focus traps inside `<iframe>` elements (#97)
|
||||
- 244f0c1: Extend the `setReturnFocus` option to receive a reference to the element that had focus prior to the trap being activated when a function is specified. Additionally, the function can now return `false` to leave focus where it is at the time of deactivation. (#485)
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 60162eb: Fix bug where `KeyboardEvent` was not being passed to `escapeDeactivates` option when it's a function (#498)
|
||||
- 7b6abfa: Fix how focus-trap determines the event's target, which was preventing traps inside open shadow DOMs from working properly (#496)
|
||||
- 14b0ee8: Fix `initialFocus` option not supporting function returning `false` as documented (#490)
|
||||
|
||||
## 6.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 24063d7: Update tabbable to v5.2.1 to get bug fix for disabled fieldsets.
|
||||
|
||||
## 6.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 281e66c: Add option to allow no initial focus when trap activates via `initialFocus: false`
|
||||
|
||||
There may be cases where we don't want to focus the first tabbable element when a focus trap activates.
|
||||
|
||||
Examples use-cases:
|
||||
|
||||
- Modals/dialogs
|
||||
- On mobile devices where "tabbing" doesn't make sense without a connected Bluetooth keyboard
|
||||
|
||||
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.
|
||||
|
||||
Example usage:
|
||||
|
||||
When the trap activates, there will be no initially focused element inside the new trap.
|
||||
|
||||
```js
|
||||
const focusTrap = createFocusTrap('#some-container', {
|
||||
initialFocus: false,
|
||||
});
|
||||
```
|
||||
|
||||
- 75be463: `escapeDeactivates` can now be either a boolean (as before) or a function that takes an event and returns a boolean.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 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.
|
||||
|
||||
## 6.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c38bf3f: onPostDeactivate should always be called even if returnFocus/OnDeactivate is disabled.
|
||||
|
||||
## 6.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 278e77e: Adding 4 new configuration event options to improve support for animated dialogs and animated focus trap triggers: `checkCanFocusTrap()`, `onPostActivate()`, `checkCanReturnFocus()`, and `onPostDeactivate()`.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 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`.
|
||||
|
||||
## 6.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 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.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1baf62e: Fix focus trapped on initial focus container with tabindex=-1 when pressing shift+tab (#363)
|
||||
|
||||
## 6.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a882d62: `clickOutsideDeactivates` can now also be a function that returns a `boolean`, similar to `allowOutsideClick`. The function receives the `MouseEvent` that triggered the click. (#289)
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4d67dee: Fix a focus escape when pressing TAB after hiding element with focus (#281)
|
||||
- ca32014: Bump tabbable from 5.1.4 to 5.1.5
|
||||
|
||||
## 6.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 036a72e: Fix crash in IE due to use of `Array.findIndex()` not supported in that browser (#257)
|
||||
|
||||
## 6.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fd3f2d1: Fix a bug where a multi-container trap would cease to work if all tabbable nodes were removed from one of the containers (fixes #223). As a result, an error is now thrown if the trap is left in a state where none of its containers contain any tabbable nodes (unless a `fallbackFocus` node has been configured in the trap's options). Also, the most-recently-focused node is more reliably tracked now, should focus somehow escape the trap and be brought back in by the trap, resulting in the truly most-recently-focused node to regain focus if that ever happens.
|
||||
|
||||
## 6.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f0c2aff: Bump tabbable to [5.1.4](https://github.com/focus-trap/tabbable/blob/master/CHANGELOG.md#514) for bug fix.
|
||||
- 2ba512b:
|
||||
- Refactored code to use function declarations instead of hoisted functions (this should have no bearing on functionality in the build output included in `./dist`.
|
||||
- Fixed bugs where `trap.activate()` and `trap.deactivate()` would not always return the trap (now they do in all circumstances).
|
||||
- d26d2e1: Refactoring to use const/let, and simplify a few lines. This does NOT impact the build output published in `./dist`, however, and hence does not impact browser support.
|
||||
|
||||
## 6.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 2267d17: Adding support for multiple elements to be passed in #217
|
||||
|
||||
## 6.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 38b6b98: Update tabbable to [5.1.3](https://github.com/focus-trap/tabbable/blob/master/CHANGELOG.md#513) to get bug fixes related to detail and summary elements.
|
||||
|
||||
## 6.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a39217: Close the gap with #172 and bump `tabbable` to 5.1.2 which has a similar fix.
|
||||
- 756c79d: Fix #172 (again): Transpile ESM bundle down to the same browser target used for the CJS and UMD bundles. ESM is just the module system, not the browser target.
|
||||
|
||||
## 6.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 00674dd: Fix #172: Transpile non-minified bundles so they are compatible with IE11.
|
||||
- 679009b: Update tabbable dependency to 5.1.1 to get transpiled non-minified bundles.
|
||||
|
||||
## 6.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fe2b0ad: Fixed #103: `returnFocusOnDeactivate` is now respected on auto-deactivation with `clickOutsideDeactivates=true`.
|
||||
|
||||
## 6.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5174ce1: Add delayInitialFocus option
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 53b906b: Change `prepublishOnly` script to `prepare` script so that it also runs if someone installs the package directly from the git repo (e.g. from your work in which you fixed a bug or added a feature you're waiting to get merged to master and published to NPM).
|
||||
- 31bb28e: Update tabbable dependency to 5.1.0. The most significant update for focus-trap is a bug fix related to fixed-position containers.
|
||||
|
||||
## 6.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 694e2fa: Package main/module entries no longer point to minified bundles.
|
||||
|
||||
## 6.0.0
|
||||
|
||||
- Add boolean value support for `allowOutsideClick` option.
|
||||
- New `preventScroll` feature to _prevent_ scrolling to the element getting focus if not in the viewport.
|
||||
- Changed code formatting to use dangling commas where ES5 supports them.
|
||||
- **BREAKING**: Updated [tabbable](https://github.com/focus-trap/tabbable/blob/master/CHANGELOG.md#500) dependency to the new 5.0.0 release which contains breaking changes to its `isTabbableRadio()` internal function.
|
||||
- Help with tree shaking by having `package.json` state `sideEffects: false` to mark this module as having no side effects as a result of merely importing it.
|
||||
- **BREAKING**: This `package.json`'s "main" no longer points to `./index.js` in the package (although it still points to a CJS module, so it's possible this actually doesn't break anything). It now has:
|
||||
- "main": `dist/focus-trap.min.js` (the CJS bundle)
|
||||
- "module": `dist/focus-trap.esm.min.js` (the **new ESM bundle**)
|
||||
- the UMD is `dist/focus-trap.umd.min.js` if needed (convenient for loading directly in an older browser that doesn't support ESM)
|
||||
- **NOTE:** The CJS build no longer provides a function as a default export. Use `const { createFocusTrap } = require('focus-trap');` to get the function from before.
|
||||
- **NOTE:** The ESM build does not provide a default export. Use `import { createFocusTrap } from 'focus-trap';` to import the module.
|
||||
- **New ESM Build**: Included in `dist/focus-trap.esm.*`.
|
||||
- New UMD Build: Included in `dist/focus-trap.umd.*`.
|
||||
|
||||
## 5.1.0
|
||||
|
||||
- Add `setReturnFocus` option that allows you to set which element receives focus when the trap closes.
|
||||
|
||||
## 5.0.2
|
||||
|
||||
- Add `allowOutsideClick` option that allows you to pass a click event through, even when `clickOutsideDeactivates` is `false`.
|
||||
|
||||
## 5.0.0
|
||||
|
||||
- Update Tabbable to improve performance (see [Tabbable's changelog](https://github.com/davidtheclark/tabbable/blob/master/CHANGELOG.md)).
|
||||
- **Breaking (kind of):** if the `onActivate` callback changes the list of tabbable nodes and the `initialFocus` option is not used, the initial focus will still go to the first element present before the callback.
|
||||
- Improve performance of activating a trap.
|
||||
- Register document-level event listeners as active (`passive: false`).
|
||||
|
||||
## 4.0.2
|
||||
|
||||
- Fix reference to root element that caused errors within Shadow DOM.
|
||||
|
||||
(Release 4.0.1 was a mistake, containing no changes.)
|
||||
|
||||
## 4.0.0
|
||||
|
||||
- **Breaking (kind of):** Focus trap now manages a queue of traps, so when a trap is paused because another trap activates, it will be unpaused when that other trap deactivates. If Trap A was automatically _paused_ because Trap B activated (existing behavior), when Trap B is deactivated Trap A will be automatically _unpaused_ (new behavior).
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- **Breaking (kind of):** Update Tabbable to detect more elements and be more careful with radio buttons (see [Tabbable's changelog](https://github.com/davidtheclark/tabbable/blob/master/CHANGELOG.md)).
|
||||
- **Breaking (kind of):** If `clickOutsideDeactivates` and `returnFocusOnDeactivate` are both `true`, focus will be returned to the pre-trap element only if the clicked element is not focusable.
|
||||
|
||||
## 2.4.6
|
||||
|
||||
- Add slight delay before moving focus to the first element in the trap. This should prevent an occasional bug caused when the first element in the trap will close the trap if it picks up on the event that triggered the trap's opening.
|
||||
|
||||
## 2.4.5
|
||||
|
||||
- Fix `"main"` field in `package.json`.
|
||||
|
||||
## 2.4.4
|
||||
|
||||
- Publish UMD build so people can download it from `unpkg.com`.
|
||||
|
||||
## 2.4.3
|
||||
|
||||
- Fixed: TypeScript signature for `activate` function.
|
||||
|
||||
## 2.4.2
|
||||
|
||||
- Added: TypeScript declaration file.
|
||||
|
||||
## 2.3.1
|
||||
|
||||
- Fixed: Activation does not re-focus already-focused node.
|
||||
- Fixed: Tabbing works as expected when initially focused Node has a negative `tabindex` and is in the middle of other tabbable elements.
|
||||
|
||||
## 2.3.0
|
||||
|
||||
- Added: `initialFocus` and `fallbackFocus` options can take functions that return DOM nodes.
|
||||
- Fixed: `pause` and `unpause` cannot accidentally add extra event listeners.
|
||||
|
||||
## 2.2.0
|
||||
|
||||
- Added/fixed, depending on your perspective: If focus is already inside the focus trap when it is activated, leave focus where it is instead of forcing it to the first tabbable node or `initialFocus`.
|
||||
|
||||
## 2.1.0
|
||||
|
||||
- Added: `fallbackFocus` option.
|
||||
|
||||
## 2.0.2
|
||||
|
||||
- Fixed: `clickOutsideDeactivates` no longer triggers deactivation when you click _inside_ the trap.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
- Fix bug when activating multiple focus traps.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- Rewrote the thing, altering the API. Read the new docs please.
|
||||
- Update `tabbable` to fix handling of traps with changing contents.
|
||||
|
||||
## 1.1.1
|
||||
|
||||
- Improve `clickOutsideDeactivates` functionality.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- Add `clickOutsideDeactivates` option.
|
||||
- Add `escapeDeactivates` option.
|
||||
|
||||
## 1.0.2
|
||||
|
||||
- Make sure to `select()` `<input>` elements when they receive focus via tab.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Fix buggy attempts to focus nodes that don't exist.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial release.
|
21
node_modules/focus-trap/LICENSE
generated
vendored
Normal file
21
node_modules/focus-trap/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2016 David Clark
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
393
node_modules/focus-trap/README.md
generated
vendored
Normal file
393
node_modules/focus-trap/README.md
generated
vendored
Normal file
|
@ -0,0 +1,393 @@
|
|||
# focus-trap [](https://github.com/focus-trap/focus-trap/actions?query=workflow:CI+branch:master) [](./LICENSE)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
Trap focus within a DOM node.
|
||||
|
||||
There may come a time when you find it important to trap focus within a DOM node — so that when a user hits `Tab` or `Shift+Tab` or clicks around, she can't escape a certain cycle of focusable elements.
|
||||
|
||||
You will definitely face this challenge when you are trying to build **accessible modals**.
|
||||
|
||||
This module is a little, modular **vanilla JS** solution to that problem.
|
||||
|
||||
Use it in your higher-level components. For example, if you are using React check out [focus-trap-react](https://github.com/focus-trap/focus-trap-react), a light wrapper around this library. If you are not a React user, consider creating light wrappers in your framework-of-choice.
|
||||
|
||||
## What it does
|
||||
|
||||
When a focus trap is activated, this is what should happen:
|
||||
|
||||
- Some element within the focus trap receives focus. By default, this will be the first element in the focus trap's tab order (as determined by [tabbable](https://github.com/focus-trap/tabbable)). Alternately, you can specify an element that should receive this initial focus.
|
||||
- The `Tab` and `Shift+Tab` keys will cycle through the focus trap's tabbable elements *but will not leave the focus trap*.
|
||||
- Clicks within the focus trap behave normally; but clicks *outside* the focus trap are blocked.
|
||||
- The `Escape` key will deactivate the focus trap.
|
||||
|
||||
When the focus trap is deactivated, this is what should happen:
|
||||
|
||||
- Focus is passed to *whichever element had focus when the trap was activated* (e.g. the button that opened the modal or menu).
|
||||
- Tabbing and clicking behave normally everywhere.
|
||||
|
||||
[Check out the demos.](http://focus-trap.github.io/focus-trap/)
|
||||
|
||||
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.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install focus-trap
|
||||
```
|
||||
|
||||
### UMD
|
||||
|
||||
You can also use a UMD version published to `unpkg.com` as `dist/focus-trap.umd.js` and `dist/focus-trap.umd.min.js`.
|
||||
|
||||
> NOTE: The UMD build does not bundle the `tabbable` dependency. Therefore you will have to also include that one, and include it *before* `focus-trap`.
|
||||
|
||||
```html
|
||||
<head>
|
||||
<script src="https://unpkg.com/tabbable/dist/index.umd.js"></script>
|
||||
<script src="https://unpkg.com/focus-trap/dist/focus-trap.umd.js"></script>
|
||||
</head>
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
As old and as broad as _reasonably_ possible, excluding browsers that are out of support or have nearly no user base.
|
||||
|
||||
Focused on desktop browsers, particularly Chrome, Edge, FireFox, Safari, and Opera.
|
||||
|
||||
Focus-trap is not officially tested on any mobile browsers or devices.
|
||||
|
||||
> ❗️ __Safari__: By default, Safari does not tab through all elements on a page, which alters the normal DOM-based tab order expected by focus-trap. If you use or support Safari with this library, make sure you and your users know they __must enable__ the `Preferences > Advanced > Press Tab to highlight each item on a webpage` feature. Otherwise, your traps [will not work the way you expect them to](https://github.com/focus-trap/focus-trap/issues/783).
|
||||
|
||||
> ⚠️ Microsoft [no longer supports](https://blogs.windows.com/windowsexperience/2022/06/15/internet-explorer-11-has-retired-and-is-officially-out-of-support-what-you-need-to-know/) any version of IE, so IE is no longer supported by this library.
|
||||
|
||||
> 💬 Focus-trap relies on tabbable so its browser support is at least [what tabbable supports](https://github.com/focus-trap/tabbable#browser-support).
|
||||
|
||||
> 💬 Keep in mind that performance optimization and old browser support are often at odds, so tabbable may not always be able to use the most optimal (typically modern) APIs in all cases.
|
||||
|
||||
## Usage
|
||||
|
||||
### createFocusTrap()
|
||||
|
||||
```javascript
|
||||
import * as focusTrap from 'focus-trap'; // ESM
|
||||
const focusTrap = require('focus-trap'); // CJS
|
||||
// UMD: `focusTrap` is defined as a global on `window`
|
||||
|
||||
trap = focusTrap.createFocusTrap(element[, createOptions]);
|
||||
```
|
||||
|
||||
Returns a new focus trap on `element` (one or more "containers" of tabbable nodes that, together, form the total set of nodes that can be visited, with clicks or the tab key, within the trap).
|
||||
|
||||
`element` can be:
|
||||
|
||||
- a DOM node (the focus trap itself);
|
||||
- a selector string (which will be passed to `document.querySelector()` to find the DOM node); or
|
||||
- an array of DOM nodes or selector strings (where the order determines where the focus will go after the last tabbable element of a DOM node/selector is reached).
|
||||
|
||||
> 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.
|
||||
|
||||
#### createOptions
|
||||
|
||||
- **onActivate** `{() => void}`: A function that will be called **before** sending focus to the target element upon activation.
|
||||
- **onPostActivate** `{() => void}`: A function that will be called **after** sending focus to the target element upon activation.
|
||||
- **onPause** `{() => void}`: A function that will be called immediately after the trap's state is updated to be paused.
|
||||
- **onPostPause** `{() => void}`: A function that will be called after the trap has been completely paused and is no longer managing/trapping focus.
|
||||
- **onUnpause** `{() => void}`: A function that will be called immediately after the trap's state is updated to be active again, but prior to updating its knowledge of what nodes are tabbable within its containers, and prior to actively managing/trapping focus.
|
||||
- **onPostUnpause** `{() => void}`: A function that will be called after the trap has been completely unpaused and is once again managing/trapping focus.
|
||||
- **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).
|
||||
- **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.
|
||||
- **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.
|
||||
- **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).
|
||||
- **initialFocus** `{HTMLElement | SVGElement | string | false | (() => HTMLElement | SVGElement | string | 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 any of these. You can also set this option to `false` (or to a function that returns `false`) to prevent any initial focus at all when the trap activates.
|
||||
- 💬 Setting this option to `false` (or a function that returns `false`) will prevent the `fallbackFocus` option from being used.
|
||||
- ⚠️ See warning below about **Shadow DOM** and selector strings.
|
||||
- **fallbackFocus** `{HTMLElement | SVGElement | string | () => HTMLElement | SVGElement | string}`: 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 any of these.
|
||||
- 💬 If `initialFocus` is `false` (or a function that returns `false`), this function will not be called when the trap is activated, and no element will be initially focused. This function may still be called while the trap is active if things change such that there are no longer any tabbable nodes in the trap.
|
||||
- ⚠️ See warning below about **Shadow DOM** and selector strings.
|
||||
- **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.
|
||||
- **clickOutsideDeactivates** `{boolean | (e: MouseEvent | TouchEvent) => boolean}`: If `true` or returns `true`, a click outside the focus trap will immediately 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`.
|
||||
- 💬 If a function is provided, it will be called up to **twice** (but only if the click occurs *outside* the trap's containers): First on the `mousedown` (or `touchstart` on mobile) event and, if `true` was returned, again on the `click` event. It will get the same node each time, and it's recommended that the returned value is also the same each time. Be sure to check the event type if the double call is an issue in your code.
|
||||
- ⚠️ 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.
|
||||
- **allowOutsideClick** `{boolean | (e: MouseEvent | TouchEvent) => boolean}`: If set and is or returns `true`, a click outside the focus trap will not be prevented (letting focus temporarily escape the trap, without deactivating it), even if `clickOutsideDeactivates=false`. Default: `false`.
|
||||
- 💬 If this is a function, it will be called up to **twice** on every click (but only if the click occurs *outside* the trap's containers): 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.
|
||||
- 💡 When `clickOutsideDeactivates=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`.
|
||||
- **returnFocusOnDeactivate** `{boolean}`: Default: `true`. If `false`, when the trap is deactivated, focus will *not* return to the element that had focus before activation.
|
||||
- 💬 When using this option in conjunction with `clickOutsideDeactivates=true`:
|
||||
- If `returnFocusOnDeactivate=true` and the outside click causing deactivation is on a focusable element, focus will __not__ return to that element; instead, it will return to the node focused just before activation.
|
||||
- If `returnFocusOnDeactivate=false` and the outside click is on a focusable node, focus will __remain__ on that node instead of the node focused just before activation. If the outside click is on a non-focusable node, then "nothing" will have focus post-deactivation.
|
||||
- **setReturnFocus** `{HTMLElement | SVGElement | string | (previousActiveElement: HTMLElement | SVGElement) => HTMLElement | SVGElement | string | false}`: By default, on **deactivation**, if `returnFocusOnDeactivate=true` (or if `returnFocus=true` in the [deactivation options](#trapdeactivate)), focus will be returned to the element that was focused just before activation. With this option, you can specify another element to programmatically receive focus after deactivation. It can be a DOM node, a selector string (which will be passed to `document.querySelector()` to find the DOM node **upon deactivation**), or a function that returns any of these to call **upon deactivation** (i.e. the selector and function options are only executed at the time the trap is deactivated). Can also be `false` (or return `false`) to leave focus where it is at the time of deactivation.
|
||||
- 💬 Using the selector or function options is a good way to return focus to a DOM node that may not exist at the time the trap is activated.
|
||||
- ⚠️ See warning below about **Shadow DOM** and selector strings.
|
||||
- **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.
|
||||
- **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.
|
||||
- **document** {Document}: Default: `window.document`. Document where the focus trap will be active. This enables the use of FocusTrap [inside an iFrame](https://focus-trap.github.io/focus-trap/#demo-in-iframe).
|
||||
- ⚠️ Note that FocusTrap will be unable to trap focus outside the iFrame if you configure this option to be the iFrame's document. It will only trap focus _inside_ of it (as the demo shows). If you want to trap focus _outside_ as well, then your FocusTrap must be configured on an element that [contains the iFrame](https://focus-trap.github.io/focus-trap/#demo-iframe).
|
||||
- **tabbableOptions**: (optional) [tabbable options](https://github.com/focus-trap/tabbable#common-options) configurable on FocusTrap (all the *common options*).
|
||||
- ⚠️ See notes about **[testing in JSDom](#testing-in-jsdom)** (e.g. using Jest).
|
||||
- **trapStack** (optional) `{Array<FocusTrap>}`: Define the global trap stack. This makes it possible to share the same stack in multiple instances of `focus-trap` in the same page such that auto-activation/pausing of traps is properly coordinated among all instances as activating a trap when another is already active should result in the other being auto-paused. By default, each instance will have its own internal stack, leading to conflicts if they each try to trap the focus at the same time.
|
||||
- **isKeyForward** `{(event: KeyboardEvent) => boolean}`: (optional) Determines if the given keyboard event is a "tab forward" event that will move the focus to the next trapped element in tab order. Defaults to the `TAB` key. Use this to override the trap's behavior if you want to use arrow keys to control keyboard navigation within the trap, for example. Also see `isKeyBackward()` option.
|
||||
- ⚠️ Using this option will not automatically prevent use of the `TAB` key as the browser will continue to respond to it by moving focus forward because that's what using the `TAB` key does in a browser, but it will no longer respect the trap's container edges as it normally would. You will need to add your own `keydown` handler to call `preventDefault()` on a `TAB` key event if you want to completely suppress the use of the `TAB` key.
|
||||
- **isKeyBackward** `{(event: KeyboardEvent) => boolean}`: (optional) Determines if the given keyboard event is a "tab backward" event that will move the focus to the previous trapped element in tab order. Defaults to the `SHIFT+TAB` key. Use this to override the trap's behavior if you want to use arrow keys to control keyboard navigation within the trap, for example. Also see `isKeyForward()` option.
|
||||
- ⚠️ Using this option will not automatically prevent use of the `SHIFT+TAB` key as the browser will continue to respond to it by moving focus backward because that's what using the `SHIFT+TAB` key sequence does in a browser, but it will no longer respect the trap's container edges as it normally would. You will need to add your own `keydown` handler to call `preventDefault()` on a `TAB` key event if you want to completely suppress the use of the `SHIFT+TAB` key sequence.
|
||||
|
||||
#### Shadow DOM
|
||||
|
||||
##### Selector strings
|
||||
|
||||
⚠️ Beware that putting a focus-trap **inside** an open Shadow DOM means you must **not use selector strings** for options that support these (because nodes inside Shadow DOMs, even open shadows, are not visible via `document.querySelector()`).
|
||||
|
||||
##### Closed shadows
|
||||
|
||||
If you have closed shadow roots that you would like considered for tabbable/focusable nodes, use the `tabbableOptions.getShadowRoot` option to provide Tabbable (used internally) with a reference to a given node's shadow root so that it can be searched for candidates.
|
||||
|
||||
### trap.active
|
||||
|
||||
```typescript
|
||||
trap.active: boolean
|
||||
```
|
||||
|
||||
True if the trap is currently active.
|
||||
|
||||
### trap.paused
|
||||
|
||||
```typescript
|
||||
trap.paused: boolean
|
||||
```
|
||||
|
||||
True if the trap is currently paused.
|
||||
|
||||
### trap.activate()
|
||||
|
||||
```typescript
|
||||
trap.activate([activateOptions]) => FocusTrap
|
||||
```
|
||||
|
||||
Activates the focus trap, adding various event listeners to the document.
|
||||
|
||||
If focus is already within it the trap, it remains unaffected. Otherwise, focus-trap will try to focus the following nodes, in order:
|
||||
|
||||
- `createOptions.initialFocus`
|
||||
- The first tabbable node in the trap
|
||||
- `createOptions.fallbackFocus`
|
||||
|
||||
If none of the above exist, an error will be thrown. You cannot have a focus trap that lacks focus.
|
||||
|
||||
Returns the `trap`.
|
||||
|
||||
`activateOptions`:
|
||||
|
||||
These options are used to override the focus trap's default behavior for this particular activation.
|
||||
|
||||
- **onActivate** `{() => void}`: Default: whatever you chose for `createOptions.onActivate`. `null` or `false` are the equivalent of a `noop`.
|
||||
- **onPostActivate** `{() => void}`: Default: whatever you chose for `createOptions.onPostActivate`. `null` or `false` are the equivalent of a `noop`.
|
||||
- **checkCanFocusTrap** `{(containers: Array<HTMLElement | SVGElement>) => Promise<void>}`: Default: whatever you chose for `createOptions.checkCanFocusTrap`.
|
||||
|
||||
### trap.deactivate()
|
||||
|
||||
```typescript
|
||||
trap.deactivate([deactivateOptions]) => FocusTrap
|
||||
```
|
||||
|
||||
Deactivates the focus trap.
|
||||
|
||||
Returns the `trap`.
|
||||
|
||||
`deactivateOptions`:
|
||||
|
||||
These options are used to override the focus trap's default behavior for this particular deactivation.
|
||||
|
||||
- **returnFocus** `{boolean}`: Default: whatever you set for `createOptions.returnFocusOnDeactivate`. If `true`, then the `setReturnFocus` option (specified when the trap was created) is used to determine where focus will be returned.
|
||||
- **onDeactivate** `{() => void}`: Default: whatever you set for `createOptions.onDeactivate`. `null` or `false` are the equivalent of a `noop`.
|
||||
- **onPostDeactivate** `{() => void}`: Default: whatever you set for `createOptions.onPostDeactivate`. `null` or `false` are the equivalent of a `noop`.
|
||||
- **checkCanReturnFocus** `{(trigger: HTMLElement | SVGElement) => Promise<void>}`: Default: whatever you set 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.
|
||||
|
||||
### trap.pause()
|
||||
|
||||
```typescript
|
||||
trap.pause([pauseOptions]) => FocusTrap
|
||||
```
|
||||
|
||||
Pause an active focus trap's event listening without deactivating the trap.
|
||||
|
||||
If the focus trap has not been activated, nothing happens.
|
||||
|
||||
Returns the `trap`.
|
||||
|
||||
Any `onDeactivate` callback will not be called, and focus will not return to the element that was focused before the trap's activation. But the trap's behavior will be paused.
|
||||
|
||||
This is useful in various cases, one of which is when you want one focus trap within another. `demo-six` exemplifies how you can implement this.
|
||||
|
||||
`pauseOptions`:
|
||||
|
||||
These options are used to override the focus trap's default behavior for this particular pausing.
|
||||
|
||||
- **onPause** `{() => void}`: Default: whatever you chose for `createOptions.onPause`. `null` or `false` are the equivalent of a `noop`.
|
||||
- **onPostPause** `{() => void}`: Default: whatever you chose for `createOptions.onPostPause`. `null` or `false` are the equivalent of a `noop`.
|
||||
|
||||
### trap.unpause()
|
||||
|
||||
```typescript
|
||||
trap.unpause([unpauseOptions]) => FocusTrap
|
||||
```
|
||||
|
||||
Unpause an active focus trap. (See `pause()`, above.)
|
||||
|
||||
Focus is forced into the trap just as described for `focusTrap.activate()`.
|
||||
|
||||
If the focus trap has not been activated or has not been paused, nothing happens.
|
||||
|
||||
Returns the `trap`.
|
||||
|
||||
`unpauseOptions`:
|
||||
|
||||
These options are used to override the focus trap's default behavior for this particular unpausing.
|
||||
|
||||
- **onUnpause** `{() => void}`: Default: whatever you chose for `createOptions.onUnpause`. `null` or `false` are the equivalent of a `noop`.
|
||||
- **onPostUnpause** `{() => void}`: Default: whatever you chose for `createOptions.onPostUnpause`. `null` or `false` are the equivalent of a `noop`.
|
||||
|
||||
### trap.updateContainerElements()
|
||||
|
||||
```typescript
|
||||
trap.updateContainerElements(HTMLElement | SVGElement | string | Array<HTMLElement | SVGElement | string>) => FocusTrap
|
||||
```
|
||||
|
||||
Update the element(s) that are used as containers for the focus trap.
|
||||
|
||||
When you call `createFocusTrap()`, you give it an element (or selector), or an array of elements (or selectors) to keep the focus within. This method simply allows you to update which elements to keep the focus within even while the trap is active.
|
||||
|
||||
A use case for this is found in focus-trap-react, where React `ref`'s may not be initialized yet, but when they are you want to have them be a container element.
|
||||
|
||||
Returns the `trap`.
|
||||
|
||||
## Examples
|
||||
|
||||
Read code in `docs/` and [see how it works](http://focus-trap.github.io/focus-trap/).
|
||||
|
||||
Here's generally what happens in `default.js` (the "default behavior" demo):
|
||||
|
||||
```js
|
||||
const { createFocusTrap } = require('../../index');
|
||||
|
||||
const container = document.getElementById('default');
|
||||
|
||||
const focusTrap = createFocusTrap('#default', {
|
||||
onActivate: () => container.classList.add('is-active'),
|
||||
onDeactivate: () => container.classList.remove('is-active'),
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById('activate-default')
|
||||
.addEventListener('click', focusTrap.activate);
|
||||
document
|
||||
.getElementById('deactivate-default')
|
||||
.addEventListener('click', focusTrap.deactivate);
|
||||
```
|
||||
|
||||
## Other details
|
||||
|
||||
### One at a time
|
||||
|
||||
*Only one focus trap can be listening at a time.* If a second focus trap is activated the first will automatically pause. The first trap is unpaused and again traps focus when the second is deactivated.
|
||||
|
||||
Focus trap manages a queue of traps: if A activates; then B activates, pausing A; then C activates, pausing B; when C then deactivates, B is unpaused; and when B then deactivates, A is unpaused.
|
||||
|
||||
### Use predictable elements for the first and last tabbable elements in your trap
|
||||
|
||||
The focus trap will work best if the *first* and *last* focusable elements in your trap are simple elements that all browsers treat the same, like buttons and inputs.**
|
||||
|
||||
Tabbing will work as expected with trickier, less predictable elements — like iframes, shadow trees, audio and video elements, etc. — as long as they are *between* more predictable elements (that is, if they are not the first or last tabbable element in the trap).
|
||||
|
||||
This limitation is ultimately rooted in browser inconsistencies and inadequacies, but it comes to focus-trap through its dependency [Tabbable](https://github.com/focus-trap/tabbable). You can read about more details [in the Tabbable documentation](https://github.com/focus-trap/tabbable#more-details).
|
||||
|
||||
### Your trap should include a tabbable element or a focusable container
|
||||
|
||||
You can't have a focus trap without focus, so an error will be thrown if you try to initialize focus-trap with an element that contains no tabbable nodes.
|
||||
|
||||
If you find yourself in this situation, you should give you container `tabindex="-1"` and set it as `initialFocus` or `fallbackFocus`. A couple of demos illustrate this.
|
||||
|
||||
## Development
|
||||
|
||||
Because of the nature of the functionality, involving keyboard and click and (especially) focus events, JavaScript unit tests don't make sense. After all, JSDom does not fully support focus events. Since the demo was developed to also be the test, we use Cypress to automate running through all demos in the demo page.
|
||||
|
||||
## Help
|
||||
|
||||
### Testing in JSDom
|
||||
|
||||
> ⚠️ JSDom is not officially supported. Your mileage may vary, and tests may break from one release to the next (even a patch or minor release).
|
||||
>
|
||||
> This topic is just here to help with what we know may affect your tests.
|
||||
|
||||
In general, a focus trap is best tested in a full browser environment such as Cypress, Playwright, or Nightwatch where a full DOM is available.
|
||||
|
||||
Sometimes, that's not entirely desirable, and depending on what you're testing, you may be able to get away with using JSDom (e.g. via Jest), but you'll have to configure your traps using the `tabbableOptions.displayCheck: 'none'` option.
|
||||
|
||||
See [Testing tabbable in JSDom](https://github.com/focus-trap/tabbable#testing-in-jsdom) for more details.
|
||||
|
||||
### ERROR: Your focus-trap must have at least one container with at least one tabbable node in it at all times
|
||||
|
||||
This error happens when the containers you specified when you [setup](#createfocustrap) your focus trap do not have -- or no longer have -- any tabbable elements in them, which means that focus will inevitably escape your trap because focus __must__ always go _somewhere_.
|
||||
|
||||
You will hit this error if your trap does not have (or no longer has) any [tabbable](https://github.com/focus-trap/tabbable#readme) (and therefore focusable) elements in it, and it was not configured with a backup element (see the `fallbackFocus` [option](#createoptions) -- which must still be in the trap, but does not necessarily have to be tabbable, i.e. it could have `tabindex="-1"`, making it focusable, but not tabbable).
|
||||
|
||||
This often happens when traps are related to elements that appear and disappear dynamically. Typically, the error will fire either as the element is being shown (because the trap gets created before the trapped children have been inserted into the DOM), or as it's being hidden (because the trapped children are destroyed before the trap is either destroyed or disabled).
|
||||
|
||||
# Contributing
|
||||
|
||||
See [CONTRIBUTING](CONTRIBUTING.md).
|
||||
|
||||
## Contributors
|
||||
|
||||
In alphabetical order:
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andersthorsen"><img src="https://avatars.githubusercontent.com/u/190081?v=4?s=100" width="100px;" alt="Anders Thorsen"/><br /><sub><b>Anders Thorsen</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Aandersthorsen" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bparish628"><img src="https://avatars1.githubusercontent.com/u/8492971?v=4?s=100" width="100px;" alt="Benjamin Parish"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://clintgoodman.com"><img src="https://avatars3.githubusercontent.com/u/5473697?v=4?s=100" width="100px;" alt="Clint Goodman"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Dan503"><img src="https://avatars.githubusercontent.com/u/10610368?v=4?s=100" width="100px;" alt="Daniel Tonon"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DaviDevMod"><img src="https://avatars.githubusercontent.com/u/98312056?v=4?s=100" width="100px;" alt="DaviDevMod"/><br /><sub><b>DaviDevMod</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=DaviDevMod" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://davidtheclark.com/"><img src="https://avatars2.githubusercontent.com/u/628431?v=4?s=100" width="100px;" alt="David Clark"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/features/security"><img src="https://avatars1.githubusercontent.com/u/27347476?v=4?s=100" width="100px;" alt="Dependabot"/><br /><sub><b>Dependabot</b></sub></a><br /><a href="#maintenance-dependabot" title="Maintenance">🚧</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.schilljs.com/"><img src="https://avatars.githubusercontent.com/u/213943?v=4?s=100" width="100px;" alt="Joas Schilling"/><br /><sub><b>Joas Schilling</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/pulls?q=is%3Apr+reviewed-by%3Anickvergessen" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/skjnldsv"><img src="https://avatars.githubusercontent.com/u/14975046?v=4?s=100" width="100px;" alt="John Molakvoæ"/><br /><sub><b>John Molakvoæ</b></sub></a><br /><a href="#ideas-skjnldsv" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://reload.dk"><img src="https://avatars.githubusercontent.com/u/73966?v=4?s=100" width="100px;" alt="Kasper Garnæs"/><br /><sub><b>Kasper Garnæs</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=kasperg" title="Documentation">📖</a> <a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Akasperg" title="Bug reports">🐛</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=kasperg" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://blogs.esri.com/esri/arcgis/"><img src="https://avatars.githubusercontent.com/u/1231455?v=4?s=100" width="100px;" alt="Matt Driscoll"/><br /><sub><b>Matt Driscoll</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Adriskull" title="Bug reports">🐛</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=driskull" title="Code">💻</a> <a href="#tutorial-driskull" title="Tutorials">✅</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/michael-ar"><img src="https://avatars3.githubusercontent.com/u/18557997?v=4?s=100" width="100px;" alt="Michael Reynolds"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liunate"><img src="https://avatars2.githubusercontent.com/u/38996291?v=4?s=100" width="100px;" alt="Nate Liu"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/far-fetched"><img src="https://avatars.githubusercontent.com/u/11621383?v=4?s=100" width="100px;" alt="Piotr Panek"/><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> <a href="https://github.com/focus-trap/focus-trap/commits?author=far-fetched" title="Documentation">📖</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=far-fetched" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=far-fetched" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/randypuro"><img src="https://avatars2.githubusercontent.com/u/2579?v=4?s=100" width="100px;" alt="Randy Puro"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sadick254"><img src="https://avatars2.githubusercontent.com/u/5238135?v=4?s=100" width="100px;" alt="Sadick"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://scottblinch.me/"><img src="https://avatars2.githubusercontent.com/u/4682114?v=4?s=100" width="100px;" alt="Scott Blinch"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://seanmcp.com/"><img src="https://avatars1.githubusercontent.com/u/6360367?v=4?s=100" width="100px;" alt="Sean McPherson"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/skriems"><img src="https://avatars.githubusercontent.com/u/15573317?v=4?s=100" width="100px;" alt="Sebastian Kriems"/><br /><sub><b>Sebastian Kriems</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Askriems" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://recollectr.io"><img src="https://avatars2.githubusercontent.com/u/6835891?v=4?s=100" width="100px;" alt="Slapbox"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://stefancameron.com/"><img src="https://avatars3.githubusercontent.com/u/2855350?v=4?s=100" width="100px;" alt="Stefan Cameron"/><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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://tylerhawkins.info/201R/"><img src="https://avatars0.githubusercontent.com/u/13806458?v=4?s=100" width="100px;" alt="Tyler Hawkins"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vasiliki-b"><img src="https://avatars.githubusercontent.com/u/98032598?v=4?s=100" width="100px;" alt="Vasiliki Boutas"/><br /><sub><b>Vasiliki Boutas</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Avasiliki-b" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://vinicius73.dev/"><img src="https://avatars.githubusercontent.com/u/1561347?v=4?s=100" width="100px;" alt="Vinicius Reis"/><br /><sub><b>Vinicius Reis</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=vinicius73" title="Code">💻</a> <a href="#ideas-vinicius73" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wandroll"><img src="https://avatars.githubusercontent.com/u/4492317?v=4?s=100" width="100px;" alt="Wandrille Verlut"/><br /><sub><b>Wandrille Verlut</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/commits?author=wandroll" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=wandroll" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap/commits?author=wandroll" title="Documentation">📖</a> <a href="#tool-wandroll" title="Tools">🔧</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://willmruzek.com/"><img src="https://avatars.githubusercontent.com/u/108522?v=4?s=100" width="100px;" alt="Will Mruzek"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zioth"><img src="https://avatars3.githubusercontent.com/u/945603?v=4?s=100" width="100px;" alt="Zioth"/><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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jpveooys"><img src="https://avatars.githubusercontent.com/u/66470099?v=4?s=100" width="100px;" alt="jpveooys"/><br /><sub><b>jpveooys</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap/issues?q=author%3Ajpveooys" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
37
node_modules/focus-trap/SECURITY.md
generated
vendored
Normal file
37
node_modules/focus-trap/SECURITY.md
generated
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The most recently published version is the only supported version. We simply do not have the maintainer capacity to support multiple versions.
|
||||
|
||||
## Security Releases
|
||||
|
||||
The most recently published version is the only supported version. If there's a security issue in that version, then we will fix it by publishing a new version that addresses the vulnerability, but we will not support or update any previous versions.
|
||||
|
||||
__Example Scenario__
|
||||
|
||||
Let's say we publish 9.0.0 and a security issue is found in 8.1.3, and it's still in 9.0.0, then we will fix it in 9.0.1 or 9.1.0 (or possibly 10.0.0 if it requires breaking backward compatibility for some reason -- this should be rare), but we will not also publish 8.1.4 or 8.2.1 to fix it.
|
||||
|
||||
There could also be a scenario where we're in a pre-release on a new major and a security issue is discovered in the current 8.1.3 release. In that case, we would try to fix it in the current non-pre-release, and bring that forward into the pre-release, but that's as far back as we would go (though we don't consider that going back because the latest release isn't a "full" release, it's still in pre-release stage, so we don't expect everyone to want to adopt a pre-release to get security fix).
|
||||
|
||||
## Release Cadence
|
||||
|
||||
This happens whenever there's something new to publish, regardless of the [Semver](https://semver.org/) bump, though we try to avoid breaking changes (majors) as much as possible. That time may come, however, and the major version change is an indication that there _may_ be a large change/break in functionality. We may also publish a major out of an abundance of caution, even if there are technically no known backward compatibility breaks, if there have been many internal changes.
|
||||
|
||||
When planning a major break in functionality for a new major release where we wish to gather feedback from the community prior to officially publishing it, we would leverage the pre-release version indicator by publishing 9.0.0-alpha.1, for example. After gathering some feedback, we may publishing additional pre-release versions, until we would finally officially publish as 9.0.0.
|
||||
|
||||
We may not always leverage pre-releases for breaking changes, however. One scenario would be a complex security issue that would force a breaking change, and needs immediate fixing.
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
This is only guaranteed _within_ a major, not from one major to the next. [Semver](https://semver.org/) states that, "the major version is incremented if any backwards _incompatible_ changes are introduced." That is what we respect for this package. Patches are bug fixes that remain backward compatible (to the current major), minors for new features (or significant internal changes) that remain backward-compatible (to the current major), and majors are for breaking changes (from the previous major).
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
If you believe you have found a security vulnerability in this package, please contact one of the maintainers directly and provide them with details, severity, and a reproduction. We would also welcome a suggested fix if you have one.
|
||||
|
||||
Any verified and accepted security vulnerabilities will be rewarded with a listing in a special "Hall of Fame" section of our README, similar to our [Contributors](./README.md#contributors) section. We do NOT offer any type of reward whatsoever other than this listing, and we do NOT guarantee that the listing will remain in the repository for any release made after the one which will address the vulnerability.
|
||||
|
||||
### Maintainers
|
||||
|
||||
- [Stefan Cameron](mailto:stefan@stefcameron.com)
|
771
node_modules/focus-trap/dist/focus-trap.esm.js
generated
vendored
Normal file
771
node_modules/focus-trap/dist/focus-trap.esm.js
generated
vendored
Normal file
|
@ -0,0 +1,771 @@
|
|||
/*!
|
||||
* focus-trap 7.4.3
|
||||
* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
|
||||
*/
|
||||
import { isFocusable, tabbable, focusable, isTabbable } from 'tabbable';
|
||||
|
||||
function ownKeys(object, enumerableOnly) {
|
||||
var keys = Object.keys(object);
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var symbols = Object.getOwnPropertySymbols(object);
|
||||
enumerableOnly && (symbols = symbols.filter(function (sym) {
|
||||
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
|
||||
})), keys.push.apply(keys, symbols);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
function _objectSpread2(target) {
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var source = null != arguments[i] ? arguments[i] : {};
|
||||
i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
|
||||
_defineProperty(target, key, source[key]);
|
||||
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
|
||||
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
function _defineProperty(obj, key, value) {
|
||||
key = _toPropertyKey(key);
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function _toPrimitive(input, hint) {
|
||||
if (typeof input !== "object" || input === null) return input;
|
||||
var prim = input[Symbol.toPrimitive];
|
||||
if (prim !== undefined) {
|
||||
var res = prim.call(input, hint || "default");
|
||||
if (typeof res !== "object") return res;
|
||||
throw new TypeError("@@toPrimitive must return a primitive value.");
|
||||
}
|
||||
return (hint === "string" ? String : Number)(input);
|
||||
}
|
||||
function _toPropertyKey(arg) {
|
||||
var key = _toPrimitive(arg, "string");
|
||||
return typeof key === "symbol" ? key : String(key);
|
||||
}
|
||||
|
||||
var activeFocusTraps = {
|
||||
activateTrap: function activateTrap(trapStack, trap) {
|
||||
if (trapStack.length > 0) {
|
||||
var activeTrap = trapStack[trapStack.length - 1];
|
||||
if (activeTrap !== trap) {
|
||||
activeTrap.pause();
|
||||
}
|
||||
}
|
||||
var trapIndex = trapStack.indexOf(trap);
|
||||
if (trapIndex === -1) {
|
||||
trapStack.push(trap);
|
||||
} else {
|
||||
// move this existing trap to the front of the queue
|
||||
trapStack.splice(trapIndex, 1);
|
||||
trapStack.push(trap);
|
||||
}
|
||||
},
|
||||
deactivateTrap: function deactivateTrap(trapStack, trap) {
|
||||
var trapIndex = trapStack.indexOf(trap);
|
||||
if (trapIndex !== -1) {
|
||||
trapStack.splice(trapIndex, 1);
|
||||
}
|
||||
if (trapStack.length > 0) {
|
||||
trapStack[trapStack.length - 1].unpause();
|
||||
}
|
||||
}
|
||||
};
|
||||
var isSelectableInput = function isSelectableInput(node) {
|
||||
return node.tagName && node.tagName.toLowerCase() === 'input' && typeof node.select === 'function';
|
||||
};
|
||||
var isEscapeEvent = function isEscapeEvent(e) {
|
||||
return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
|
||||
};
|
||||
var isTabEvent = function isTabEvent(e) {
|
||||
return e.key === 'Tab' || e.keyCode === 9;
|
||||
};
|
||||
|
||||
// checks for TAB by default
|
||||
var isKeyForward = function isKeyForward(e) {
|
||||
return isTabEvent(e) && !e.shiftKey;
|
||||
};
|
||||
|
||||
// checks for SHIFT+TAB by default
|
||||
var isKeyBackward = function isKeyBackward(e) {
|
||||
return isTabEvent(e) && e.shiftKey;
|
||||
};
|
||||
var delay = function delay(fn) {
|
||||
return setTimeout(fn, 0);
|
||||
};
|
||||
|
||||
// Array.find/findIndex() are not supported on IE; this replicates enough
|
||||
// of Array.findIndex() for our needs
|
||||
var findIndex = function findIndex(arr, fn) {
|
||||
var idx = -1;
|
||||
arr.every(function (value, i) {
|
||||
if (fn(value)) {
|
||||
idx = i;
|
||||
return false; // break
|
||||
}
|
||||
|
||||
return true; // next
|
||||
});
|
||||
|
||||
return idx;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an option's value when it could be a plain value, or a handler that provides
|
||||
* the value.
|
||||
* @param {*} value Option's value to check.
|
||||
* @param {...*} [params] Any parameters to pass to the handler, if `value` is a function.
|
||||
* @returns {*} The `value`, or the handler's returned value.
|
||||
*/
|
||||
var valueOrHandler = function valueOrHandler(value) {
|
||||
for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||
params[_key - 1] = arguments[_key];
|
||||
}
|
||||
return typeof value === 'function' ? value.apply(void 0, params) : value;
|
||||
};
|
||||
var getActualTarget = function getActualTarget(event) {
|
||||
// NOTE: If the trap is _inside_ a shadow DOM, event.target will always be the
|
||||
// shadow host. However, event.target.composedPath() will be an array of
|
||||
// nodes "clicked" from inner-most (the actual element inside the shadow) to
|
||||
// outer-most (the host HTML document). If we have access to composedPath(),
|
||||
// then use its first element; otherwise, fall back to event.target (and
|
||||
// this only works for an _open_ shadow DOM; otherwise,
|
||||
// composedPath()[0] === event.target always).
|
||||
return event.target.shadowRoot && typeof event.composedPath === 'function' ? event.composedPath()[0] : event.target;
|
||||
};
|
||||
|
||||
// NOTE: this must be _outside_ `createFocusTrap()` to make sure all traps in this
|
||||
// current instance use the same stack if `userOptions.trapStack` isn't specified
|
||||
var internalTrapStack = [];
|
||||
var createFocusTrap = function createFocusTrap(elements, userOptions) {
|
||||
// SSR: a live trap shouldn't be created in this type of environment so this
|
||||
// should be safe code to execute if the `document` option isn't specified
|
||||
var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document;
|
||||
var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || internalTrapStack;
|
||||
var config = _objectSpread2({
|
||||
returnFocusOnDeactivate: true,
|
||||
escapeDeactivates: true,
|
||||
delayInitialFocus: true,
|
||||
isKeyForward: isKeyForward,
|
||||
isKeyBackward: isKeyBackward
|
||||
}, userOptions);
|
||||
var state = {
|
||||
// containers given to createFocusTrap()
|
||||
// @type {Array<HTMLElement>}
|
||||
containers: [],
|
||||
// list of objects identifying tabbable nodes in `containers` in the trap
|
||||
// NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap
|
||||
// is active, but the trap should never get to a state where there isn't at least one group
|
||||
// with at least one tabbable node in it (that would lead to an error condition that would
|
||||
// result in an error being thrown)
|
||||
// @type {Array<{
|
||||
// container: HTMLElement,
|
||||
// tabbableNodes: Array<HTMLElement>, // empty if none
|
||||
// focusableNodes: Array<HTMLElement>, // empty if none
|
||||
// firstTabbableNode: HTMLElement|null,
|
||||
// lastTabbableNode: HTMLElement|null,
|
||||
// nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined
|
||||
// }>}
|
||||
containerGroups: [],
|
||||
// same order/length as `containers` list
|
||||
|
||||
// references to objects in `containerGroups`, but only those that actually have
|
||||
// tabbable nodes in them
|
||||
// NOTE: same order as `containers` and `containerGroups`, but __not necessarily__
|
||||
// the same length
|
||||
tabbableGroups: [],
|
||||
nodeFocusedBeforeActivation: null,
|
||||
mostRecentlyFocusedNode: null,
|
||||
active: false,
|
||||
paused: false,
|
||||
// timer ID for when delayInitialFocus is true and initial focus in this trap
|
||||
// has been delayed during activation
|
||||
delayInitialFocusTimer: undefined
|
||||
};
|
||||
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
|
||||
|
||||
/**
|
||||
* Gets a configuration option value.
|
||||
* @param {Object|undefined} configOverrideOptions If true, and option is defined in this set,
|
||||
* value will be taken from this object. Otherwise, value will be taken from base configuration.
|
||||
* @param {string} optionName Name of the option whose value is sought.
|
||||
* @param {string|undefined} [configOptionName] Name of option to use __instead of__ `optionName`
|
||||
* IIF `configOverrideOptions` is not defined. Otherwise, `optionName` is used.
|
||||
*/
|
||||
var getOption = function getOption(configOverrideOptions, optionName, configOptionName) {
|
||||
return configOverrideOptions && configOverrideOptions[optionName] !== undefined ? configOverrideOptions[optionName] : config[configOptionName || optionName];
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the index of the container that contains the element.
|
||||
* @param {HTMLElement} element
|
||||
* @param {Event} [event]
|
||||
* @returns {number} Index of the container in either `state.containers` or
|
||||
* `state.containerGroups` (the order/length of these lists are the same); -1
|
||||
* if the element isn't found.
|
||||
*/
|
||||
var findContainerIndex = function findContainerIndex(element, event) {
|
||||
var composedPath = typeof (event === null || event === void 0 ? void 0 : event.composedPath) === 'function' ? event.composedPath() : undefined;
|
||||
// NOTE: search `containerGroups` because it's possible a group contains no tabbable
|
||||
// nodes, but still contains focusable nodes (e.g. if they all have `tabindex=-1`)
|
||||
// and we still need to find the element in there
|
||||
return state.containerGroups.findIndex(function (_ref) {
|
||||
var container = _ref.container,
|
||||
tabbableNodes = _ref.tabbableNodes;
|
||||
return container.contains(element) || ( // fall back to explicit tabbable search which will take into consideration any
|
||||
// web components if the `tabbableOptions.getShadowRoot` option was used for
|
||||
// the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
|
||||
// look inside web components even if open)
|
||||
composedPath === null || composedPath === void 0 ? void 0 : composedPath.includes(container)) || tabbableNodes.find(function (node) {
|
||||
return node === element;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the node for the given option, which is expected to be an option that
|
||||
* can be either a DOM node, a string that is a selector to get a node, `false`
|
||||
* (if a node is explicitly NOT given), or a function that returns any of these
|
||||
* values.
|
||||
* @param {string} optionName
|
||||
* @returns {undefined | false | HTMLElement | SVGElement} Returns
|
||||
* `undefined` if the option is not specified; `false` if the option
|
||||
* resolved to `false` (node explicitly not given); otherwise, the resolved
|
||||
* DOM node.
|
||||
* @throws {Error} If the option is set, not `false`, and is not, or does not
|
||||
* resolve to a node.
|
||||
*/
|
||||
var getNodeForOption = function getNodeForOption(optionName) {
|
||||
var optionValue = config[optionName];
|
||||
if (typeof optionValue === 'function') {
|
||||
for (var _len2 = arguments.length, params = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
||||
params[_key2 - 1] = arguments[_key2];
|
||||
}
|
||||
optionValue = optionValue.apply(void 0, params);
|
||||
}
|
||||
if (optionValue === true) {
|
||||
optionValue = undefined; // use default value
|
||||
}
|
||||
|
||||
if (!optionValue) {
|
||||
if (optionValue === undefined || optionValue === false) {
|
||||
return optionValue;
|
||||
}
|
||||
// else, empty string (invalid), null (invalid), 0 (invalid)
|
||||
|
||||
throw new Error("`".concat(optionName, "` was specified but was not a node, or did not return a node"));
|
||||
}
|
||||
var node = optionValue; // could be HTMLElement, SVGElement, or non-empty string at this point
|
||||
|
||||
if (typeof optionValue === 'string') {
|
||||
node = doc.querySelector(optionValue); // resolve to node, or null if fails
|
||||
if (!node) {
|
||||
throw new Error("`".concat(optionName, "` as selector refers to no known node"));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
};
|
||||
var getInitialFocusNode = function getInitialFocusNode() {
|
||||
var node = getNodeForOption('initialFocus');
|
||||
|
||||
// false explicitly indicates we want no initialFocus at all
|
||||
if (node === false) {
|
||||
return false;
|
||||
}
|
||||
if (node === undefined || !isFocusable(node, config.tabbableOptions)) {
|
||||
// option not specified nor focusable: use fallback options
|
||||
if (findContainerIndex(doc.activeElement) >= 0) {
|
||||
node = doc.activeElement;
|
||||
} else {
|
||||
var firstTabbableGroup = state.tabbableGroups[0];
|
||||
var firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
|
||||
|
||||
// NOTE: `fallbackFocus` option function cannot return `false` (not supported)
|
||||
node = firstTabbableNode || getNodeForOption('fallbackFocus');
|
||||
}
|
||||
}
|
||||
if (!node) {
|
||||
throw new Error('Your focus-trap needs to have at least one focusable element');
|
||||
}
|
||||
return node;
|
||||
};
|
||||
var updateTabbableNodes = function updateTabbableNodes() {
|
||||
state.containerGroups = state.containers.map(function (container) {
|
||||
var tabbableNodes = tabbable(container, config.tabbableOptions);
|
||||
|
||||
// NOTE: if we have tabbable nodes, we must have focusable nodes; focusable nodes
|
||||
// are a superset of tabbable nodes
|
||||
var focusableNodes = focusable(container, config.tabbableOptions);
|
||||
return {
|
||||
container: container,
|
||||
tabbableNodes: tabbableNodes,
|
||||
focusableNodes: focusableNodes,
|
||||
firstTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[0] : null,
|
||||
lastTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : null,
|
||||
/**
|
||||
* Finds the __tabbable__ node that follows the given node in the specified direction,
|
||||
* in this container, if any.
|
||||
* @param {HTMLElement} node
|
||||
* @param {boolean} [forward] True if going in forward tab order; false if going
|
||||
* in reverse.
|
||||
* @returns {HTMLElement|undefined} The next tabbable node, if any.
|
||||
*/
|
||||
nextTabbableNode: function nextTabbableNode(node) {
|
||||
var forward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
||||
// NOTE: If tabindex is positive (in order to manipulate the tab order separate
|
||||
// from the DOM order), this __will not work__ because the list of focusableNodes,
|
||||
// while it contains tabbable nodes, does not sort its nodes in any order other
|
||||
// than DOM order, because it can't: Where would you place focusable (but not
|
||||
// tabbable) nodes in that order? They have no order, because they aren't tabbale...
|
||||
// Support for positive tabindex is already broken and hard to manage (possibly
|
||||
// not supportable, TBD), so this isn't going to make things worse than they
|
||||
// already are, and at least makes things better for the majority of cases where
|
||||
// tabindex is either 0/unset or negative.
|
||||
// FYI, positive tabindex issue: https://github.com/focus-trap/focus-trap/issues/375
|
||||
var nodeIdx = focusableNodes.findIndex(function (n) {
|
||||
return n === node;
|
||||
});
|
||||
if (nodeIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (forward) {
|
||||
return focusableNodes.slice(nodeIdx + 1).find(function (n) {
|
||||
return isTabbable(n, config.tabbableOptions);
|
||||
});
|
||||
}
|
||||
return focusableNodes.slice(0, nodeIdx).reverse().find(function (n) {
|
||||
return isTabbable(n, config.tabbableOptions);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
state.tabbableGroups = state.containerGroups.filter(function (group) {
|
||||
return group.tabbableNodes.length > 0;
|
||||
});
|
||||
|
||||
// throw if no groups have tabbable nodes and we don't have a fallback focus node either
|
||||
if (state.tabbableGroups.length <= 0 && !getNodeForOption('fallbackFocus') // returning false not supported for this option
|
||||
) {
|
||||
throw new Error('Your focus-trap must have at least one container with at least one tabbable node in it at all times');
|
||||
}
|
||||
};
|
||||
var tryFocus = function tryFocus(node) {
|
||||
if (node === false) {
|
||||
return;
|
||||
}
|
||||
if (node === doc.activeElement) {
|
||||
return;
|
||||
}
|
||||
if (!node || !node.focus) {
|
||||
tryFocus(getInitialFocusNode());
|
||||
return;
|
||||
}
|
||||
node.focus({
|
||||
preventScroll: !!config.preventScroll
|
||||
});
|
||||
state.mostRecentlyFocusedNode = node;
|
||||
if (isSelectableInput(node)) {
|
||||
node.select();
|
||||
}
|
||||
};
|
||||
var getReturnFocusNode = function getReturnFocusNode(previousActiveElement) {
|
||||
var node = getNodeForOption('setReturnFocus', previousActiveElement);
|
||||
return node ? node : node === false ? false : previousActiveElement;
|
||||
};
|
||||
|
||||
// This needs to be done on mousedown and touchstart instead of click
|
||||
// so that it precedes the focus event.
|
||||
var checkPointerDown = function checkPointerDown(e) {
|
||||
var target = getActualTarget(e);
|
||||
if (findContainerIndex(target, e) >= 0) {
|
||||
// allow the click since it ocurred inside the trap
|
||||
return;
|
||||
}
|
||||
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
||||
// immediately deactivate the trap
|
||||
trap.deactivate({
|
||||
// NOTE: by setting `returnFocus: false`, deactivate() will do nothing,
|
||||
// which will result in the outside click setting focus to the node
|
||||
// that was clicked (and if not focusable, to "nothing"); by setting
|
||||
// `returnFocus: true`, we'll attempt to re-focus the node originally-focused
|
||||
// on activation (or the configured `setReturnFocus` node), whether the
|
||||
// outside click was on a focusable node or not
|
||||
returnFocus: config.returnFocusOnDeactivate
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// This is needed for mobile devices.
|
||||
// (If we'll only let `click` events through,
|
||||
// then on mobile they will be blocked anyways if `touchstart` is blocked.)
|
||||
if (valueOrHandler(config.allowOutsideClick, e)) {
|
||||
// allow the click outside the trap to take place
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, prevent the click
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// In case focus escapes the trap for some strange reason, pull it back in.
|
||||
var checkFocusIn = function checkFocusIn(e) {
|
||||
var target = getActualTarget(e);
|
||||
var targetContained = findContainerIndex(target, e) >= 0;
|
||||
|
||||
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
||||
if (targetContained || target instanceof Document) {
|
||||
if (targetContained) {
|
||||
state.mostRecentlyFocusedNode = target;
|
||||
}
|
||||
} else {
|
||||
// escaped! pull it back in to where it just left
|
||||
e.stopImmediatePropagation();
|
||||
tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
|
||||
}
|
||||
};
|
||||
|
||||
// Hijack key nav events on the first and last focusable nodes of the trap,
|
||||
// in order to prevent focus from escaping. If it escapes for even a
|
||||
// moment it can end up scrolling the page and causing confusion so we
|
||||
// kind of need to capture the action at the keydown phase.
|
||||
var checkKeyNav = function checkKeyNav(event) {
|
||||
var isBackward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
||||
var target = getActualTarget(event);
|
||||
updateTabbableNodes();
|
||||
var destinationNode = null;
|
||||
if (state.tabbableGroups.length > 0) {
|
||||
// make sure the target is actually contained in a group
|
||||
// NOTE: the target may also be the container itself if it's focusable
|
||||
// with tabIndex='-1' and was given initial focus
|
||||
var containerIndex = findContainerIndex(target, event);
|
||||
var containerGroup = containerIndex >= 0 ? state.containerGroups[containerIndex] : undefined;
|
||||
if (containerIndex < 0) {
|
||||
// target not found in any group: quite possible focus has escaped the trap,
|
||||
// so bring it back into...
|
||||
if (isBackward) {
|
||||
// ...the last node in the last group
|
||||
destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode;
|
||||
} else {
|
||||
// ...the first node in the first group
|
||||
destinationNode = state.tabbableGroups[0].firstTabbableNode;
|
||||
}
|
||||
} else if (isBackward) {
|
||||
// REVERSE
|
||||
|
||||
// is the target the first tabbable node in a group?
|
||||
var startOfGroupIndex = findIndex(state.tabbableGroups, function (_ref2) {
|
||||
var firstTabbableNode = _ref2.firstTabbableNode;
|
||||
return target === firstTabbableNode;
|
||||
});
|
||||
if (startOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target, false))) {
|
||||
// an exception case where the target is either the container itself, or
|
||||
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
||||
// and user clicked on it or node was programmatically given focus)
|
||||
// and is not followed by any other tabbable node, in which
|
||||
// case, we should handle shift+tab as if focus were on the container's
|
||||
// first tabbable node, and go to the last tabbable node of the LAST group
|
||||
startOfGroupIndex = containerIndex;
|
||||
}
|
||||
if (startOfGroupIndex >= 0) {
|
||||
// YES: then shift+tab should go to the last tabbable node in the
|
||||
// previous group (and wrap around to the last tabbable node of
|
||||
// the LAST group if it's the first tabbable node of the FIRST group)
|
||||
var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
|
||||
var destinationGroup = state.tabbableGroups[destinationGroupIndex];
|
||||
destinationNode = destinationGroup.lastTabbableNode;
|
||||
} else if (!isTabEvent(event)) {
|
||||
// user must have customized the nav keys so we have to move focus manually _within_
|
||||
// the active group: do this based on the order determined by tabbable()
|
||||
destinationNode = containerGroup.nextTabbableNode(target, false);
|
||||
}
|
||||
} else {
|
||||
// FORWARD
|
||||
|
||||
// is the target the last tabbable node in a group?
|
||||
var lastOfGroupIndex = findIndex(state.tabbableGroups, function (_ref3) {
|
||||
var lastTabbableNode = _ref3.lastTabbableNode;
|
||||
return target === lastTabbableNode;
|
||||
});
|
||||
if (lastOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target))) {
|
||||
// an exception case where the target is the container itself, or
|
||||
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
||||
// and user clicked on it or node was programmatically given focus)
|
||||
// and is not followed by any other tabbable node, in which
|
||||
// case, we should handle tab as if focus were on the container's
|
||||
// last tabbable node, and go to the first tabbable node of the FIRST group
|
||||
lastOfGroupIndex = containerIndex;
|
||||
}
|
||||
if (lastOfGroupIndex >= 0) {
|
||||
// YES: then tab should go to the first tabbable node in the next
|
||||
// group (and wrap around to the first tabbable node of the FIRST
|
||||
// group if it's the last tabbable node of the LAST group)
|
||||
var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
|
||||
var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
|
||||
destinationNode = _destinationGroup.firstTabbableNode;
|
||||
} else if (!isTabEvent(event)) {
|
||||
// user must have customized the nav keys so we have to move focus manually _within_
|
||||
// the active group: do this based on the order determined by tabbable()
|
||||
destinationNode = containerGroup.nextTabbableNode(target);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no groups available
|
||||
// NOTE: the fallbackFocus option does not support returning false to opt-out
|
||||
destinationNode = getNodeForOption('fallbackFocus');
|
||||
}
|
||||
if (destinationNode) {
|
||||
if (isTabEvent(event)) {
|
||||
// since tab natively moves focus, we wouldn't have a destination node unless we
|
||||
// were on the edge of a container and had to move to the next/previous edge, in
|
||||
// which case we want to prevent default to keep the browser from moving focus
|
||||
// to where it normally would
|
||||
event.preventDefault();
|
||||
}
|
||||
tryFocus(destinationNode);
|
||||
}
|
||||
// else, let the browser take care of [shift+]tab and move the focus
|
||||
};
|
||||
|
||||
var checkKey = function checkKey(event) {
|
||||
if (isEscapeEvent(event) && valueOrHandler(config.escapeDeactivates, event) !== false) {
|
||||
event.preventDefault();
|
||||
trap.deactivate();
|
||||
return;
|
||||
}
|
||||
if (config.isKeyForward(event) || config.isKeyBackward(event)) {
|
||||
checkKeyNav(event, config.isKeyBackward(event));
|
||||
}
|
||||
};
|
||||
var checkClick = function checkClick(e) {
|
||||
var target = getActualTarget(e);
|
||||
if (findContainerIndex(target, e) >= 0) {
|
||||
return;
|
||||
}
|
||||
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
||||
return;
|
||||
}
|
||||
if (valueOrHandler(config.allowOutsideClick, e)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
//
|
||||
// EVENT LISTENERS
|
||||
//
|
||||
|
||||
var addListeners = function addListeners() {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There can be only one listening focus trap at a time
|
||||
activeFocusTraps.activateTrap(trapStack, trap);
|
||||
|
||||
// Delay ensures that the focused element doesn't capture the event
|
||||
// that caused the focus trap activation.
|
||||
state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () {
|
||||
tryFocus(getInitialFocusNode());
|
||||
}) : tryFocus(getInitialFocusNode());
|
||||
doc.addEventListener('focusin', checkFocusIn, true);
|
||||
doc.addEventListener('mousedown', checkPointerDown, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
doc.addEventListener('touchstart', checkPointerDown, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
doc.addEventListener('click', checkClick, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
doc.addEventListener('keydown', checkKey, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
return trap;
|
||||
};
|
||||
var removeListeners = function removeListeners() {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
doc.removeEventListener('focusin', checkFocusIn, true);
|
||||
doc.removeEventListener('mousedown', checkPointerDown, true);
|
||||
doc.removeEventListener('touchstart', checkPointerDown, true);
|
||||
doc.removeEventListener('click', checkClick, true);
|
||||
doc.removeEventListener('keydown', checkKey, true);
|
||||
return trap;
|
||||
};
|
||||
|
||||
//
|
||||
// MUTATION OBSERVER
|
||||
//
|
||||
|
||||
var checkDomRemoval = function checkDomRemoval(mutations) {
|
||||
var isFocusedNodeRemoved = mutations.some(function (mutation) {
|
||||
var removedNodes = Array.from(mutation.removedNodes);
|
||||
return removedNodes.some(function (node) {
|
||||
return node === state.mostRecentlyFocusedNode;
|
||||
});
|
||||
});
|
||||
|
||||
// If the currently focused is removed then browsers will move focus to the
|
||||
// <body> element. If this happens, try to move focus back into the trap.
|
||||
if (isFocusedNodeRemoved) {
|
||||
tryFocus(getInitialFocusNode());
|
||||
}
|
||||
};
|
||||
|
||||
// Use MutationObserver - if supported - to detect if focused node is removed
|
||||
// from the DOM.
|
||||
var mutationObserver = typeof window !== 'undefined' && 'MutationObserver' in window ? new MutationObserver(checkDomRemoval) : undefined;
|
||||
var updateObservedNodes = function updateObservedNodes() {
|
||||
if (!mutationObserver) {
|
||||
return;
|
||||
}
|
||||
mutationObserver.disconnect();
|
||||
if (state.active && !state.paused) {
|
||||
state.containers.map(function (container) {
|
||||
mutationObserver.observe(container, {
|
||||
subtree: true,
|
||||
childList: true
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// TRAP DEFINITION
|
||||
//
|
||||
|
||||
trap = {
|
||||
get active() {
|
||||
return state.active;
|
||||
},
|
||||
get paused() {
|
||||
return state.paused;
|
||||
},
|
||||
activate: function activate(activateOptions) {
|
||||
if (state.active) {
|
||||
return this;
|
||||
}
|
||||
var onActivate = getOption(activateOptions, 'onActivate');
|
||||
var onPostActivate = getOption(activateOptions, 'onPostActivate');
|
||||
var checkCanFocusTrap = getOption(activateOptions, 'checkCanFocusTrap');
|
||||
if (!checkCanFocusTrap) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
state.active = true;
|
||||
state.paused = false;
|
||||
state.nodeFocusedBeforeActivation = doc.activeElement;
|
||||
onActivate === null || onActivate === void 0 ? void 0 : onActivate();
|
||||
var finishActivation = function finishActivation() {
|
||||
if (checkCanFocusTrap) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
addListeners();
|
||||
updateObservedNodes();
|
||||
onPostActivate === null || onPostActivate === void 0 ? void 0 : onPostActivate();
|
||||
};
|
||||
if (checkCanFocusTrap) {
|
||||
checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation);
|
||||
return this;
|
||||
}
|
||||
finishActivation();
|
||||
return this;
|
||||
},
|
||||
deactivate: function deactivate(deactivateOptions) {
|
||||
if (!state.active) {
|
||||
return this;
|
||||
}
|
||||
var options = _objectSpread2({
|
||||
onDeactivate: config.onDeactivate,
|
||||
onPostDeactivate: config.onPostDeactivate,
|
||||
checkCanReturnFocus: config.checkCanReturnFocus
|
||||
}, deactivateOptions);
|
||||
clearTimeout(state.delayInitialFocusTimer); // noop if undefined
|
||||
state.delayInitialFocusTimer = undefined;
|
||||
removeListeners();
|
||||
state.active = false;
|
||||
state.paused = false;
|
||||
updateObservedNodes();
|
||||
activeFocusTraps.deactivateTrap(trapStack, trap);
|
||||
var onDeactivate = getOption(options, 'onDeactivate');
|
||||
var onPostDeactivate = getOption(options, 'onPostDeactivate');
|
||||
var checkCanReturnFocus = getOption(options, 'checkCanReturnFocus');
|
||||
var returnFocus = getOption(options, 'returnFocus', 'returnFocusOnDeactivate');
|
||||
onDeactivate === null || onDeactivate === void 0 ? void 0 : onDeactivate();
|
||||
var finishDeactivation = function finishDeactivation() {
|
||||
delay(function () {
|
||||
if (returnFocus) {
|
||||
tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
|
||||
}
|
||||
onPostDeactivate === null || onPostDeactivate === void 0 ? void 0 : onPostDeactivate();
|
||||
});
|
||||
};
|
||||
if (returnFocus && checkCanReturnFocus) {
|
||||
checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation);
|
||||
return this;
|
||||
}
|
||||
finishDeactivation();
|
||||
return this;
|
||||
},
|
||||
pause: function pause(pauseOptions) {
|
||||
if (state.paused || !state.active) {
|
||||
return this;
|
||||
}
|
||||
var onPause = getOption(pauseOptions, 'onPause');
|
||||
var onPostPause = getOption(pauseOptions, 'onPostPause');
|
||||
state.paused = true;
|
||||
onPause === null || onPause === void 0 ? void 0 : onPause();
|
||||
removeListeners();
|
||||
updateObservedNodes();
|
||||
onPostPause === null || onPostPause === void 0 ? void 0 : onPostPause();
|
||||
return this;
|
||||
},
|
||||
unpause: function unpause(unpauseOptions) {
|
||||
if (!state.paused || !state.active) {
|
||||
return this;
|
||||
}
|
||||
var onUnpause = getOption(unpauseOptions, 'onUnpause');
|
||||
var onPostUnpause = getOption(unpauseOptions, 'onPostUnpause');
|
||||
state.paused = false;
|
||||
onUnpause === null || onUnpause === void 0 ? void 0 : onUnpause();
|
||||
updateTabbableNodes();
|
||||
addListeners();
|
||||
updateObservedNodes();
|
||||
onPostUnpause === null || onPostUnpause === void 0 ? void 0 : onPostUnpause();
|
||||
return this;
|
||||
},
|
||||
updateContainerElements: function updateContainerElements(containerElements) {
|
||||
var elementsAsArray = [].concat(containerElements).filter(Boolean);
|
||||
state.containers = elementsAsArray.map(function (element) {
|
||||
return typeof element === 'string' ? doc.querySelector(element) : element;
|
||||
});
|
||||
if (state.active) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
updateObservedNodes();
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
// initialize container elements
|
||||
trap.updateContainerElements(elements);
|
||||
return trap;
|
||||
};
|
||||
|
||||
export { createFocusTrap };
|
||||
//# sourceMappingURL=focus-trap.esm.js.map
|
1
node_modules/focus-trap/dist/focus-trap.esm.js.map
generated
vendored
Normal file
1
node_modules/focus-trap/dist/focus-trap.esm.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6
node_modules/focus-trap/dist/focus-trap.esm.min.js
generated
vendored
Normal file
6
node_modules/focus-trap/dist/focus-trap.esm.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
node_modules/focus-trap/dist/focus-trap.esm.min.js.map
generated
vendored
Normal file
1
node_modules/focus-trap/dist/focus-trap.esm.min.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
775
node_modules/focus-trap/dist/focus-trap.js
generated
vendored
Normal file
775
node_modules/focus-trap/dist/focus-trap.js
generated
vendored
Normal file
|
@ -0,0 +1,775 @@
|
|||
/*!
|
||||
* focus-trap 7.4.3
|
||||
* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
var tabbable = require('tabbable');
|
||||
|
||||
function ownKeys(object, enumerableOnly) {
|
||||
var keys = Object.keys(object);
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var symbols = Object.getOwnPropertySymbols(object);
|
||||
enumerableOnly && (symbols = symbols.filter(function (sym) {
|
||||
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
|
||||
})), keys.push.apply(keys, symbols);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
function _objectSpread2(target) {
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var source = null != arguments[i] ? arguments[i] : {};
|
||||
i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
|
||||
_defineProperty(target, key, source[key]);
|
||||
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
|
||||
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
function _defineProperty(obj, key, value) {
|
||||
key = _toPropertyKey(key);
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function _toPrimitive(input, hint) {
|
||||
if (typeof input !== "object" || input === null) return input;
|
||||
var prim = input[Symbol.toPrimitive];
|
||||
if (prim !== undefined) {
|
||||
var res = prim.call(input, hint || "default");
|
||||
if (typeof res !== "object") return res;
|
||||
throw new TypeError("@@toPrimitive must return a primitive value.");
|
||||
}
|
||||
return (hint === "string" ? String : Number)(input);
|
||||
}
|
||||
function _toPropertyKey(arg) {
|
||||
var key = _toPrimitive(arg, "string");
|
||||
return typeof key === "symbol" ? key : String(key);
|
||||
}
|
||||
|
||||
var activeFocusTraps = {
|
||||
activateTrap: function activateTrap(trapStack, trap) {
|
||||
if (trapStack.length > 0) {
|
||||
var activeTrap = trapStack[trapStack.length - 1];
|
||||
if (activeTrap !== trap) {
|
||||
activeTrap.pause();
|
||||
}
|
||||
}
|
||||
var trapIndex = trapStack.indexOf(trap);
|
||||
if (trapIndex === -1) {
|
||||
trapStack.push(trap);
|
||||
} else {
|
||||
// move this existing trap to the front of the queue
|
||||
trapStack.splice(trapIndex, 1);
|
||||
trapStack.push(trap);
|
||||
}
|
||||
},
|
||||
deactivateTrap: function deactivateTrap(trapStack, trap) {
|
||||
var trapIndex = trapStack.indexOf(trap);
|
||||
if (trapIndex !== -1) {
|
||||
trapStack.splice(trapIndex, 1);
|
||||
}
|
||||
if (trapStack.length > 0) {
|
||||
trapStack[trapStack.length - 1].unpause();
|
||||
}
|
||||
}
|
||||
};
|
||||
var isSelectableInput = function isSelectableInput(node) {
|
||||
return node.tagName && node.tagName.toLowerCase() === 'input' && typeof node.select === 'function';
|
||||
};
|
||||
var isEscapeEvent = function isEscapeEvent(e) {
|
||||
return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
|
||||
};
|
||||
var isTabEvent = function isTabEvent(e) {
|
||||
return e.key === 'Tab' || e.keyCode === 9;
|
||||
};
|
||||
|
||||
// checks for TAB by default
|
||||
var isKeyForward = function isKeyForward(e) {
|
||||
return isTabEvent(e) && !e.shiftKey;
|
||||
};
|
||||
|
||||
// checks for SHIFT+TAB by default
|
||||
var isKeyBackward = function isKeyBackward(e) {
|
||||
return isTabEvent(e) && e.shiftKey;
|
||||
};
|
||||
var delay = function delay(fn) {
|
||||
return setTimeout(fn, 0);
|
||||
};
|
||||
|
||||
// Array.find/findIndex() are not supported on IE; this replicates enough
|
||||
// of Array.findIndex() for our needs
|
||||
var findIndex = function findIndex(arr, fn) {
|
||||
var idx = -1;
|
||||
arr.every(function (value, i) {
|
||||
if (fn(value)) {
|
||||
idx = i;
|
||||
return false; // break
|
||||
}
|
||||
|
||||
return true; // next
|
||||
});
|
||||
|
||||
return idx;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an option's value when it could be a plain value, or a handler that provides
|
||||
* the value.
|
||||
* @param {*} value Option's value to check.
|
||||
* @param {...*} [params] Any parameters to pass to the handler, if `value` is a function.
|
||||
* @returns {*} The `value`, or the handler's returned value.
|
||||
*/
|
||||
var valueOrHandler = function valueOrHandler(value) {
|
||||
for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||
params[_key - 1] = arguments[_key];
|
||||
}
|
||||
return typeof value === 'function' ? value.apply(void 0, params) : value;
|
||||
};
|
||||
var getActualTarget = function getActualTarget(event) {
|
||||
// NOTE: If the trap is _inside_ a shadow DOM, event.target will always be the
|
||||
// shadow host. However, event.target.composedPath() will be an array of
|
||||
// nodes "clicked" from inner-most (the actual element inside the shadow) to
|
||||
// outer-most (the host HTML document). If we have access to composedPath(),
|
||||
// then use its first element; otherwise, fall back to event.target (and
|
||||
// this only works for an _open_ shadow DOM; otherwise,
|
||||
// composedPath()[0] === event.target always).
|
||||
return event.target.shadowRoot && typeof event.composedPath === 'function' ? event.composedPath()[0] : event.target;
|
||||
};
|
||||
|
||||
// NOTE: this must be _outside_ `createFocusTrap()` to make sure all traps in this
|
||||
// current instance use the same stack if `userOptions.trapStack` isn't specified
|
||||
var internalTrapStack = [];
|
||||
var createFocusTrap = function createFocusTrap(elements, userOptions) {
|
||||
// SSR: a live trap shouldn't be created in this type of environment so this
|
||||
// should be safe code to execute if the `document` option isn't specified
|
||||
var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document;
|
||||
var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || internalTrapStack;
|
||||
var config = _objectSpread2({
|
||||
returnFocusOnDeactivate: true,
|
||||
escapeDeactivates: true,
|
||||
delayInitialFocus: true,
|
||||
isKeyForward: isKeyForward,
|
||||
isKeyBackward: isKeyBackward
|
||||
}, userOptions);
|
||||
var state = {
|
||||
// containers given to createFocusTrap()
|
||||
// @type {Array<HTMLElement>}
|
||||
containers: [],
|
||||
// list of objects identifying tabbable nodes in `containers` in the trap
|
||||
// NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap
|
||||
// is active, but the trap should never get to a state where there isn't at least one group
|
||||
// with at least one tabbable node in it (that would lead to an error condition that would
|
||||
// result in an error being thrown)
|
||||
// @type {Array<{
|
||||
// container: HTMLElement,
|
||||
// tabbableNodes: Array<HTMLElement>, // empty if none
|
||||
// focusableNodes: Array<HTMLElement>, // empty if none
|
||||
// firstTabbableNode: HTMLElement|null,
|
||||
// lastTabbableNode: HTMLElement|null,
|
||||
// nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined
|
||||
// }>}
|
||||
containerGroups: [],
|
||||
// same order/length as `containers` list
|
||||
|
||||
// references to objects in `containerGroups`, but only those that actually have
|
||||
// tabbable nodes in them
|
||||
// NOTE: same order as `containers` and `containerGroups`, but __not necessarily__
|
||||
// the same length
|
||||
tabbableGroups: [],
|
||||
nodeFocusedBeforeActivation: null,
|
||||
mostRecentlyFocusedNode: null,
|
||||
active: false,
|
||||
paused: false,
|
||||
// timer ID for when delayInitialFocus is true and initial focus in this trap
|
||||
// has been delayed during activation
|
||||
delayInitialFocusTimer: undefined
|
||||
};
|
||||
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
|
||||
|
||||
/**
|
||||
* Gets a configuration option value.
|
||||
* @param {Object|undefined} configOverrideOptions If true, and option is defined in this set,
|
||||
* value will be taken from this object. Otherwise, value will be taken from base configuration.
|
||||
* @param {string} optionName Name of the option whose value is sought.
|
||||
* @param {string|undefined} [configOptionName] Name of option to use __instead of__ `optionName`
|
||||
* IIF `configOverrideOptions` is not defined. Otherwise, `optionName` is used.
|
||||
*/
|
||||
var getOption = function getOption(configOverrideOptions, optionName, configOptionName) {
|
||||
return configOverrideOptions && configOverrideOptions[optionName] !== undefined ? configOverrideOptions[optionName] : config[configOptionName || optionName];
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the index of the container that contains the element.
|
||||
* @param {HTMLElement} element
|
||||
* @param {Event} [event]
|
||||
* @returns {number} Index of the container in either `state.containers` or
|
||||
* `state.containerGroups` (the order/length of these lists are the same); -1
|
||||
* if the element isn't found.
|
||||
*/
|
||||
var findContainerIndex = function findContainerIndex(element, event) {
|
||||
var composedPath = typeof (event === null || event === void 0 ? void 0 : event.composedPath) === 'function' ? event.composedPath() : undefined;
|
||||
// NOTE: search `containerGroups` because it's possible a group contains no tabbable
|
||||
// nodes, but still contains focusable nodes (e.g. if they all have `tabindex=-1`)
|
||||
// and we still need to find the element in there
|
||||
return state.containerGroups.findIndex(function (_ref) {
|
||||
var container = _ref.container,
|
||||
tabbableNodes = _ref.tabbableNodes;
|
||||
return container.contains(element) || ( // fall back to explicit tabbable search which will take into consideration any
|
||||
// web components if the `tabbableOptions.getShadowRoot` option was used for
|
||||
// the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
|
||||
// look inside web components even if open)
|
||||
composedPath === null || composedPath === void 0 ? void 0 : composedPath.includes(container)) || tabbableNodes.find(function (node) {
|
||||
return node === element;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the node for the given option, which is expected to be an option that
|
||||
* can be either a DOM node, a string that is a selector to get a node, `false`
|
||||
* (if a node is explicitly NOT given), or a function that returns any of these
|
||||
* values.
|
||||
* @param {string} optionName
|
||||
* @returns {undefined | false | HTMLElement | SVGElement} Returns
|
||||
* `undefined` if the option is not specified; `false` if the option
|
||||
* resolved to `false` (node explicitly not given); otherwise, the resolved
|
||||
* DOM node.
|
||||
* @throws {Error} If the option is set, not `false`, and is not, or does not
|
||||
* resolve to a node.
|
||||
*/
|
||||
var getNodeForOption = function getNodeForOption(optionName) {
|
||||
var optionValue = config[optionName];
|
||||
if (typeof optionValue === 'function') {
|
||||
for (var _len2 = arguments.length, params = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
||||
params[_key2 - 1] = arguments[_key2];
|
||||
}
|
||||
optionValue = optionValue.apply(void 0, params);
|
||||
}
|
||||
if (optionValue === true) {
|
||||
optionValue = undefined; // use default value
|
||||
}
|
||||
|
||||
if (!optionValue) {
|
||||
if (optionValue === undefined || optionValue === false) {
|
||||
return optionValue;
|
||||
}
|
||||
// else, empty string (invalid), null (invalid), 0 (invalid)
|
||||
|
||||
throw new Error("`".concat(optionName, "` was specified but was not a node, or did not return a node"));
|
||||
}
|
||||
var node = optionValue; // could be HTMLElement, SVGElement, or non-empty string at this point
|
||||
|
||||
if (typeof optionValue === 'string') {
|
||||
node = doc.querySelector(optionValue); // resolve to node, or null if fails
|
||||
if (!node) {
|
||||
throw new Error("`".concat(optionName, "` as selector refers to no known node"));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
};
|
||||
var getInitialFocusNode = function getInitialFocusNode() {
|
||||
var node = getNodeForOption('initialFocus');
|
||||
|
||||
// false explicitly indicates we want no initialFocus at all
|
||||
if (node === false) {
|
||||
return false;
|
||||
}
|
||||
if (node === undefined || !tabbable.isFocusable(node, config.tabbableOptions)) {
|
||||
// option not specified nor focusable: use fallback options
|
||||
if (findContainerIndex(doc.activeElement) >= 0) {
|
||||
node = doc.activeElement;
|
||||
} else {
|
||||
var firstTabbableGroup = state.tabbableGroups[0];
|
||||
var firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
|
||||
|
||||
// NOTE: `fallbackFocus` option function cannot return `false` (not supported)
|
||||
node = firstTabbableNode || getNodeForOption('fallbackFocus');
|
||||
}
|
||||
}
|
||||
if (!node) {
|
||||
throw new Error('Your focus-trap needs to have at least one focusable element');
|
||||
}
|
||||
return node;
|
||||
};
|
||||
var updateTabbableNodes = function updateTabbableNodes() {
|
||||
state.containerGroups = state.containers.map(function (container) {
|
||||
var tabbableNodes = tabbable.tabbable(container, config.tabbableOptions);
|
||||
|
||||
// NOTE: if we have tabbable nodes, we must have focusable nodes; focusable nodes
|
||||
// are a superset of tabbable nodes
|
||||
var focusableNodes = tabbable.focusable(container, config.tabbableOptions);
|
||||
return {
|
||||
container: container,
|
||||
tabbableNodes: tabbableNodes,
|
||||
focusableNodes: focusableNodes,
|
||||
firstTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[0] : null,
|
||||
lastTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : null,
|
||||
/**
|
||||
* Finds the __tabbable__ node that follows the given node in the specified direction,
|
||||
* in this container, if any.
|
||||
* @param {HTMLElement} node
|
||||
* @param {boolean} [forward] True if going in forward tab order; false if going
|
||||
* in reverse.
|
||||
* @returns {HTMLElement|undefined} The next tabbable node, if any.
|
||||
*/
|
||||
nextTabbableNode: function nextTabbableNode(node) {
|
||||
var forward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
||||
// NOTE: If tabindex is positive (in order to manipulate the tab order separate
|
||||
// from the DOM order), this __will not work__ because the list of focusableNodes,
|
||||
// while it contains tabbable nodes, does not sort its nodes in any order other
|
||||
// than DOM order, because it can't: Where would you place focusable (but not
|
||||
// tabbable) nodes in that order? They have no order, because they aren't tabbale...
|
||||
// Support for positive tabindex is already broken and hard to manage (possibly
|
||||
// not supportable, TBD), so this isn't going to make things worse than they
|
||||
// already are, and at least makes things better for the majority of cases where
|
||||
// tabindex is either 0/unset or negative.
|
||||
// FYI, positive tabindex issue: https://github.com/focus-trap/focus-trap/issues/375
|
||||
var nodeIdx = focusableNodes.findIndex(function (n) {
|
||||
return n === node;
|
||||
});
|
||||
if (nodeIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (forward) {
|
||||
return focusableNodes.slice(nodeIdx + 1).find(function (n) {
|
||||
return tabbable.isTabbable(n, config.tabbableOptions);
|
||||
});
|
||||
}
|
||||
return focusableNodes.slice(0, nodeIdx).reverse().find(function (n) {
|
||||
return tabbable.isTabbable(n, config.tabbableOptions);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
state.tabbableGroups = state.containerGroups.filter(function (group) {
|
||||
return group.tabbableNodes.length > 0;
|
||||
});
|
||||
|
||||
// throw if no groups have tabbable nodes and we don't have a fallback focus node either
|
||||
if (state.tabbableGroups.length <= 0 && !getNodeForOption('fallbackFocus') // returning false not supported for this option
|
||||
) {
|
||||
throw new Error('Your focus-trap must have at least one container with at least one tabbable node in it at all times');
|
||||
}
|
||||
};
|
||||
var tryFocus = function tryFocus(node) {
|
||||
if (node === false) {
|
||||
return;
|
||||
}
|
||||
if (node === doc.activeElement) {
|
||||
return;
|
||||
}
|
||||
if (!node || !node.focus) {
|
||||
tryFocus(getInitialFocusNode());
|
||||
return;
|
||||
}
|
||||
node.focus({
|
||||
preventScroll: !!config.preventScroll
|
||||
});
|
||||
state.mostRecentlyFocusedNode = node;
|
||||
if (isSelectableInput(node)) {
|
||||
node.select();
|
||||
}
|
||||
};
|
||||
var getReturnFocusNode = function getReturnFocusNode(previousActiveElement) {
|
||||
var node = getNodeForOption('setReturnFocus', previousActiveElement);
|
||||
return node ? node : node === false ? false : previousActiveElement;
|
||||
};
|
||||
|
||||
// This needs to be done on mousedown and touchstart instead of click
|
||||
// so that it precedes the focus event.
|
||||
var checkPointerDown = function checkPointerDown(e) {
|
||||
var target = getActualTarget(e);
|
||||
if (findContainerIndex(target, e) >= 0) {
|
||||
// allow the click since it ocurred inside the trap
|
||||
return;
|
||||
}
|
||||
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
||||
// immediately deactivate the trap
|
||||
trap.deactivate({
|
||||
// NOTE: by setting `returnFocus: false`, deactivate() will do nothing,
|
||||
// which will result in the outside click setting focus to the node
|
||||
// that was clicked (and if not focusable, to "nothing"); by setting
|
||||
// `returnFocus: true`, we'll attempt to re-focus the node originally-focused
|
||||
// on activation (or the configured `setReturnFocus` node), whether the
|
||||
// outside click was on a focusable node or not
|
||||
returnFocus: config.returnFocusOnDeactivate
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// This is needed for mobile devices.
|
||||
// (If we'll only let `click` events through,
|
||||
// then on mobile they will be blocked anyways if `touchstart` is blocked.)
|
||||
if (valueOrHandler(config.allowOutsideClick, e)) {
|
||||
// allow the click outside the trap to take place
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, prevent the click
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// In case focus escapes the trap for some strange reason, pull it back in.
|
||||
var checkFocusIn = function checkFocusIn(e) {
|
||||
var target = getActualTarget(e);
|
||||
var targetContained = findContainerIndex(target, e) >= 0;
|
||||
|
||||
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
||||
if (targetContained || target instanceof Document) {
|
||||
if (targetContained) {
|
||||
state.mostRecentlyFocusedNode = target;
|
||||
}
|
||||
} else {
|
||||
// escaped! pull it back in to where it just left
|
||||
e.stopImmediatePropagation();
|
||||
tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
|
||||
}
|
||||
};
|
||||
|
||||
// Hijack key nav events on the first and last focusable nodes of the trap,
|
||||
// in order to prevent focus from escaping. If it escapes for even a
|
||||
// moment it can end up scrolling the page and causing confusion so we
|
||||
// kind of need to capture the action at the keydown phase.
|
||||
var checkKeyNav = function checkKeyNav(event) {
|
||||
var isBackward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
||||
var target = getActualTarget(event);
|
||||
updateTabbableNodes();
|
||||
var destinationNode = null;
|
||||
if (state.tabbableGroups.length > 0) {
|
||||
// make sure the target is actually contained in a group
|
||||
// NOTE: the target may also be the container itself if it's focusable
|
||||
// with tabIndex='-1' and was given initial focus
|
||||
var containerIndex = findContainerIndex(target, event);
|
||||
var containerGroup = containerIndex >= 0 ? state.containerGroups[containerIndex] : undefined;
|
||||
if (containerIndex < 0) {
|
||||
// target not found in any group: quite possible focus has escaped the trap,
|
||||
// so bring it back into...
|
||||
if (isBackward) {
|
||||
// ...the last node in the last group
|
||||
destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode;
|
||||
} else {
|
||||
// ...the first node in the first group
|
||||
destinationNode = state.tabbableGroups[0].firstTabbableNode;
|
||||
}
|
||||
} else if (isBackward) {
|
||||
// REVERSE
|
||||
|
||||
// is the target the first tabbable node in a group?
|
||||
var startOfGroupIndex = findIndex(state.tabbableGroups, function (_ref2) {
|
||||
var firstTabbableNode = _ref2.firstTabbableNode;
|
||||
return target === firstTabbableNode;
|
||||
});
|
||||
if (startOfGroupIndex < 0 && (containerGroup.container === target || tabbable.isFocusable(target, config.tabbableOptions) && !tabbable.isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target, false))) {
|
||||
// an exception case where the target is either the container itself, or
|
||||
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
||||
// and user clicked on it or node was programmatically given focus)
|
||||
// and is not followed by any other tabbable node, in which
|
||||
// case, we should handle shift+tab as if focus were on the container's
|
||||
// first tabbable node, and go to the last tabbable node of the LAST group
|
||||
startOfGroupIndex = containerIndex;
|
||||
}
|
||||
if (startOfGroupIndex >= 0) {
|
||||
// YES: then shift+tab should go to the last tabbable node in the
|
||||
// previous group (and wrap around to the last tabbable node of
|
||||
// the LAST group if it's the first tabbable node of the FIRST group)
|
||||
var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
|
||||
var destinationGroup = state.tabbableGroups[destinationGroupIndex];
|
||||
destinationNode = destinationGroup.lastTabbableNode;
|
||||
} else if (!isTabEvent(event)) {
|
||||
// user must have customized the nav keys so we have to move focus manually _within_
|
||||
// the active group: do this based on the order determined by tabbable()
|
||||
destinationNode = containerGroup.nextTabbableNode(target, false);
|
||||
}
|
||||
} else {
|
||||
// FORWARD
|
||||
|
||||
// is the target the last tabbable node in a group?
|
||||
var lastOfGroupIndex = findIndex(state.tabbableGroups, function (_ref3) {
|
||||
var lastTabbableNode = _ref3.lastTabbableNode;
|
||||
return target === lastTabbableNode;
|
||||
});
|
||||
if (lastOfGroupIndex < 0 && (containerGroup.container === target || tabbable.isFocusable(target, config.tabbableOptions) && !tabbable.isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target))) {
|
||||
// an exception case where the target is the container itself, or
|
||||
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
||||
// and user clicked on it or node was programmatically given focus)
|
||||
// and is not followed by any other tabbable node, in which
|
||||
// case, we should handle tab as if focus were on the container's
|
||||
// last tabbable node, and go to the first tabbable node of the FIRST group
|
||||
lastOfGroupIndex = containerIndex;
|
||||
}
|
||||
if (lastOfGroupIndex >= 0) {
|
||||
// YES: then tab should go to the first tabbable node in the next
|
||||
// group (and wrap around to the first tabbable node of the FIRST
|
||||
// group if it's the last tabbable node of the LAST group)
|
||||
var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
|
||||
var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
|
||||
destinationNode = _destinationGroup.firstTabbableNode;
|
||||
} else if (!isTabEvent(event)) {
|
||||
// user must have customized the nav keys so we have to move focus manually _within_
|
||||
// the active group: do this based on the order determined by tabbable()
|
||||
destinationNode = containerGroup.nextTabbableNode(target);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no groups available
|
||||
// NOTE: the fallbackFocus option does not support returning false to opt-out
|
||||
destinationNode = getNodeForOption('fallbackFocus');
|
||||
}
|
||||
if (destinationNode) {
|
||||
if (isTabEvent(event)) {
|
||||
// since tab natively moves focus, we wouldn't have a destination node unless we
|
||||
// were on the edge of a container and had to move to the next/previous edge, in
|
||||
// which case we want to prevent default to keep the browser from moving focus
|
||||
// to where it normally would
|
||||
event.preventDefault();
|
||||
}
|
||||
tryFocus(destinationNode);
|
||||
}
|
||||
// else, let the browser take care of [shift+]tab and move the focus
|
||||
};
|
||||
|
||||
var checkKey = function checkKey(event) {
|
||||
if (isEscapeEvent(event) && valueOrHandler(config.escapeDeactivates, event) !== false) {
|
||||
event.preventDefault();
|
||||
trap.deactivate();
|
||||
return;
|
||||
}
|
||||
if (config.isKeyForward(event) || config.isKeyBackward(event)) {
|
||||
checkKeyNav(event, config.isKeyBackward(event));
|
||||
}
|
||||
};
|
||||
var checkClick = function checkClick(e) {
|
||||
var target = getActualTarget(e);
|
||||
if (findContainerIndex(target, e) >= 0) {
|
||||
return;
|
||||
}
|
||||
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
||||
return;
|
||||
}
|
||||
if (valueOrHandler(config.allowOutsideClick, e)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
//
|
||||
// EVENT LISTENERS
|
||||
//
|
||||
|
||||
var addListeners = function addListeners() {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There can be only one listening focus trap at a time
|
||||
activeFocusTraps.activateTrap(trapStack, trap);
|
||||
|
||||
// Delay ensures that the focused element doesn't capture the event
|
||||
// that caused the focus trap activation.
|
||||
state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () {
|
||||
tryFocus(getInitialFocusNode());
|
||||
}) : tryFocus(getInitialFocusNode());
|
||||
doc.addEventListener('focusin', checkFocusIn, true);
|
||||
doc.addEventListener('mousedown', checkPointerDown, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
doc.addEventListener('touchstart', checkPointerDown, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
doc.addEventListener('click', checkClick, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
doc.addEventListener('keydown', checkKey, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
return trap;
|
||||
};
|
||||
var removeListeners = function removeListeners() {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
doc.removeEventListener('focusin', checkFocusIn, true);
|
||||
doc.removeEventListener('mousedown', checkPointerDown, true);
|
||||
doc.removeEventListener('touchstart', checkPointerDown, true);
|
||||
doc.removeEventListener('click', checkClick, true);
|
||||
doc.removeEventListener('keydown', checkKey, true);
|
||||
return trap;
|
||||
};
|
||||
|
||||
//
|
||||
// MUTATION OBSERVER
|
||||
//
|
||||
|
||||
var checkDomRemoval = function checkDomRemoval(mutations) {
|
||||
var isFocusedNodeRemoved = mutations.some(function (mutation) {
|
||||
var removedNodes = Array.from(mutation.removedNodes);
|
||||
return removedNodes.some(function (node) {
|
||||
return node === state.mostRecentlyFocusedNode;
|
||||
});
|
||||
});
|
||||
|
||||
// If the currently focused is removed then browsers will move focus to the
|
||||
// <body> element. If this happens, try to move focus back into the trap.
|
||||
if (isFocusedNodeRemoved) {
|
||||
tryFocus(getInitialFocusNode());
|
||||
}
|
||||
};
|
||||
|
||||
// Use MutationObserver - if supported - to detect if focused node is removed
|
||||
// from the DOM.
|
||||
var mutationObserver = typeof window !== 'undefined' && 'MutationObserver' in window ? new MutationObserver(checkDomRemoval) : undefined;
|
||||
var updateObservedNodes = function updateObservedNodes() {
|
||||
if (!mutationObserver) {
|
||||
return;
|
||||
}
|
||||
mutationObserver.disconnect();
|
||||
if (state.active && !state.paused) {
|
||||
state.containers.map(function (container) {
|
||||
mutationObserver.observe(container, {
|
||||
subtree: true,
|
||||
childList: true
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// TRAP DEFINITION
|
||||
//
|
||||
|
||||
trap = {
|
||||
get active() {
|
||||
return state.active;
|
||||
},
|
||||
get paused() {
|
||||
return state.paused;
|
||||
},
|
||||
activate: function activate(activateOptions) {
|
||||
if (state.active) {
|
||||
return this;
|
||||
}
|
||||
var onActivate = getOption(activateOptions, 'onActivate');
|
||||
var onPostActivate = getOption(activateOptions, 'onPostActivate');
|
||||
var checkCanFocusTrap = getOption(activateOptions, 'checkCanFocusTrap');
|
||||
if (!checkCanFocusTrap) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
state.active = true;
|
||||
state.paused = false;
|
||||
state.nodeFocusedBeforeActivation = doc.activeElement;
|
||||
onActivate === null || onActivate === void 0 ? void 0 : onActivate();
|
||||
var finishActivation = function finishActivation() {
|
||||
if (checkCanFocusTrap) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
addListeners();
|
||||
updateObservedNodes();
|
||||
onPostActivate === null || onPostActivate === void 0 ? void 0 : onPostActivate();
|
||||
};
|
||||
if (checkCanFocusTrap) {
|
||||
checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation);
|
||||
return this;
|
||||
}
|
||||
finishActivation();
|
||||
return this;
|
||||
},
|
||||
deactivate: function deactivate(deactivateOptions) {
|
||||
if (!state.active) {
|
||||
return this;
|
||||
}
|
||||
var options = _objectSpread2({
|
||||
onDeactivate: config.onDeactivate,
|
||||
onPostDeactivate: config.onPostDeactivate,
|
||||
checkCanReturnFocus: config.checkCanReturnFocus
|
||||
}, deactivateOptions);
|
||||
clearTimeout(state.delayInitialFocusTimer); // noop if undefined
|
||||
state.delayInitialFocusTimer = undefined;
|
||||
removeListeners();
|
||||
state.active = false;
|
||||
state.paused = false;
|
||||
updateObservedNodes();
|
||||
activeFocusTraps.deactivateTrap(trapStack, trap);
|
||||
var onDeactivate = getOption(options, 'onDeactivate');
|
||||
var onPostDeactivate = getOption(options, 'onPostDeactivate');
|
||||
var checkCanReturnFocus = getOption(options, 'checkCanReturnFocus');
|
||||
var returnFocus = getOption(options, 'returnFocus', 'returnFocusOnDeactivate');
|
||||
onDeactivate === null || onDeactivate === void 0 ? void 0 : onDeactivate();
|
||||
var finishDeactivation = function finishDeactivation() {
|
||||
delay(function () {
|
||||
if (returnFocus) {
|
||||
tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
|
||||
}
|
||||
onPostDeactivate === null || onPostDeactivate === void 0 ? void 0 : onPostDeactivate();
|
||||
});
|
||||
};
|
||||
if (returnFocus && checkCanReturnFocus) {
|
||||
checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation);
|
||||
return this;
|
||||
}
|
||||
finishDeactivation();
|
||||
return this;
|
||||
},
|
||||
pause: function pause(pauseOptions) {
|
||||
if (state.paused || !state.active) {
|
||||
return this;
|
||||
}
|
||||
var onPause = getOption(pauseOptions, 'onPause');
|
||||
var onPostPause = getOption(pauseOptions, 'onPostPause');
|
||||
state.paused = true;
|
||||
onPause === null || onPause === void 0 ? void 0 : onPause();
|
||||
removeListeners();
|
||||
updateObservedNodes();
|
||||
onPostPause === null || onPostPause === void 0 ? void 0 : onPostPause();
|
||||
return this;
|
||||
},
|
||||
unpause: function unpause(unpauseOptions) {
|
||||
if (!state.paused || !state.active) {
|
||||
return this;
|
||||
}
|
||||
var onUnpause = getOption(unpauseOptions, 'onUnpause');
|
||||
var onPostUnpause = getOption(unpauseOptions, 'onPostUnpause');
|
||||
state.paused = false;
|
||||
onUnpause === null || onUnpause === void 0 ? void 0 : onUnpause();
|
||||
updateTabbableNodes();
|
||||
addListeners();
|
||||
updateObservedNodes();
|
||||
onPostUnpause === null || onPostUnpause === void 0 ? void 0 : onPostUnpause();
|
||||
return this;
|
||||
},
|
||||
updateContainerElements: function updateContainerElements(containerElements) {
|
||||
var elementsAsArray = [].concat(containerElements).filter(Boolean);
|
||||
state.containers = elementsAsArray.map(function (element) {
|
||||
return typeof element === 'string' ? doc.querySelector(element) : element;
|
||||
});
|
||||
if (state.active) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
updateObservedNodes();
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
// initialize container elements
|
||||
trap.updateContainerElements(elements);
|
||||
return trap;
|
||||
};
|
||||
|
||||
exports.createFocusTrap = createFocusTrap;
|
||||
//# sourceMappingURL=focus-trap.js.map
|
1
node_modules/focus-trap/dist/focus-trap.js.map
generated
vendored
Normal file
1
node_modules/focus-trap/dist/focus-trap.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6
node_modules/focus-trap/dist/focus-trap.min.js
generated
vendored
Normal file
6
node_modules/focus-trap/dist/focus-trap.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
node_modules/focus-trap/dist/focus-trap.min.js.map
generated
vendored
Normal file
1
node_modules/focus-trap/dist/focus-trap.min.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
784
node_modules/focus-trap/dist/focus-trap.umd.js
generated
vendored
Normal file
784
node_modules/focus-trap/dist/focus-trap.umd.js
generated
vendored
Normal file
|
@ -0,0 +1,784 @@
|
|||
/*!
|
||||
* focus-trap 7.4.3
|
||||
* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
|
||||
*/
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tabbable')) :
|
||||
typeof define === 'function' && define.amd ? define(['exports', 'tabbable'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, (function () {
|
||||
var current = global.focusTrap;
|
||||
var exports = global.focusTrap = {};
|
||||
factory(exports, global.tabbable);
|
||||
exports.noConflict = function () { global.focusTrap = current; return exports; };
|
||||
})());
|
||||
})(this, (function (exports, tabbable) { 'use strict';
|
||||
|
||||
function ownKeys(object, enumerableOnly) {
|
||||
var keys = Object.keys(object);
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var symbols = Object.getOwnPropertySymbols(object);
|
||||
enumerableOnly && (symbols = symbols.filter(function (sym) {
|
||||
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
|
||||
})), keys.push.apply(keys, symbols);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
function _objectSpread2(target) {
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var source = null != arguments[i] ? arguments[i] : {};
|
||||
i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
|
||||
_defineProperty(target, key, source[key]);
|
||||
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
|
||||
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
function _defineProperty(obj, key, value) {
|
||||
key = _toPropertyKey(key);
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function _toPrimitive(input, hint) {
|
||||
if (typeof input !== "object" || input === null) return input;
|
||||
var prim = input[Symbol.toPrimitive];
|
||||
if (prim !== undefined) {
|
||||
var res = prim.call(input, hint || "default");
|
||||
if (typeof res !== "object") return res;
|
||||
throw new TypeError("@@toPrimitive must return a primitive value.");
|
||||
}
|
||||
return (hint === "string" ? String : Number)(input);
|
||||
}
|
||||
function _toPropertyKey(arg) {
|
||||
var key = _toPrimitive(arg, "string");
|
||||
return typeof key === "symbol" ? key : String(key);
|
||||
}
|
||||
|
||||
var activeFocusTraps = {
|
||||
activateTrap: function activateTrap(trapStack, trap) {
|
||||
if (trapStack.length > 0) {
|
||||
var activeTrap = trapStack[trapStack.length - 1];
|
||||
if (activeTrap !== trap) {
|
||||
activeTrap.pause();
|
||||
}
|
||||
}
|
||||
var trapIndex = trapStack.indexOf(trap);
|
||||
if (trapIndex === -1) {
|
||||
trapStack.push(trap);
|
||||
} else {
|
||||
// move this existing trap to the front of the queue
|
||||
trapStack.splice(trapIndex, 1);
|
||||
trapStack.push(trap);
|
||||
}
|
||||
},
|
||||
deactivateTrap: function deactivateTrap(trapStack, trap) {
|
||||
var trapIndex = trapStack.indexOf(trap);
|
||||
if (trapIndex !== -1) {
|
||||
trapStack.splice(trapIndex, 1);
|
||||
}
|
||||
if (trapStack.length > 0) {
|
||||
trapStack[trapStack.length - 1].unpause();
|
||||
}
|
||||
}
|
||||
};
|
||||
var isSelectableInput = function isSelectableInput(node) {
|
||||
return node.tagName && node.tagName.toLowerCase() === 'input' && typeof node.select === 'function';
|
||||
};
|
||||
var isEscapeEvent = function isEscapeEvent(e) {
|
||||
return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
|
||||
};
|
||||
var isTabEvent = function isTabEvent(e) {
|
||||
return e.key === 'Tab' || e.keyCode === 9;
|
||||
};
|
||||
|
||||
// checks for TAB by default
|
||||
var isKeyForward = function isKeyForward(e) {
|
||||
return isTabEvent(e) && !e.shiftKey;
|
||||
};
|
||||
|
||||
// checks for SHIFT+TAB by default
|
||||
var isKeyBackward = function isKeyBackward(e) {
|
||||
return isTabEvent(e) && e.shiftKey;
|
||||
};
|
||||
var delay = function delay(fn) {
|
||||
return setTimeout(fn, 0);
|
||||
};
|
||||
|
||||
// Array.find/findIndex() are not supported on IE; this replicates enough
|
||||
// of Array.findIndex() for our needs
|
||||
var findIndex = function findIndex(arr, fn) {
|
||||
var idx = -1;
|
||||
arr.every(function (value, i) {
|
||||
if (fn(value)) {
|
||||
idx = i;
|
||||
return false; // break
|
||||
}
|
||||
|
||||
return true; // next
|
||||
});
|
||||
|
||||
return idx;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an option's value when it could be a plain value, or a handler that provides
|
||||
* the value.
|
||||
* @param {*} value Option's value to check.
|
||||
* @param {...*} [params] Any parameters to pass to the handler, if `value` is a function.
|
||||
* @returns {*} The `value`, or the handler's returned value.
|
||||
*/
|
||||
var valueOrHandler = function valueOrHandler(value) {
|
||||
for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||
params[_key - 1] = arguments[_key];
|
||||
}
|
||||
return typeof value === 'function' ? value.apply(void 0, params) : value;
|
||||
};
|
||||
var getActualTarget = function getActualTarget(event) {
|
||||
// NOTE: If the trap is _inside_ a shadow DOM, event.target will always be the
|
||||
// shadow host. However, event.target.composedPath() will be an array of
|
||||
// nodes "clicked" from inner-most (the actual element inside the shadow) to
|
||||
// outer-most (the host HTML document). If we have access to composedPath(),
|
||||
// then use its first element; otherwise, fall back to event.target (and
|
||||
// this only works for an _open_ shadow DOM; otherwise,
|
||||
// composedPath()[0] === event.target always).
|
||||
return event.target.shadowRoot && typeof event.composedPath === 'function' ? event.composedPath()[0] : event.target;
|
||||
};
|
||||
|
||||
// NOTE: this must be _outside_ `createFocusTrap()` to make sure all traps in this
|
||||
// current instance use the same stack if `userOptions.trapStack` isn't specified
|
||||
var internalTrapStack = [];
|
||||
var createFocusTrap = function createFocusTrap(elements, userOptions) {
|
||||
// SSR: a live trap shouldn't be created in this type of environment so this
|
||||
// should be safe code to execute if the `document` option isn't specified
|
||||
var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document;
|
||||
var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || internalTrapStack;
|
||||
var config = _objectSpread2({
|
||||
returnFocusOnDeactivate: true,
|
||||
escapeDeactivates: true,
|
||||
delayInitialFocus: true,
|
||||
isKeyForward: isKeyForward,
|
||||
isKeyBackward: isKeyBackward
|
||||
}, userOptions);
|
||||
var state = {
|
||||
// containers given to createFocusTrap()
|
||||
// @type {Array<HTMLElement>}
|
||||
containers: [],
|
||||
// list of objects identifying tabbable nodes in `containers` in the trap
|
||||
// NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap
|
||||
// is active, but the trap should never get to a state where there isn't at least one group
|
||||
// with at least one tabbable node in it (that would lead to an error condition that would
|
||||
// result in an error being thrown)
|
||||
// @type {Array<{
|
||||
// container: HTMLElement,
|
||||
// tabbableNodes: Array<HTMLElement>, // empty if none
|
||||
// focusableNodes: Array<HTMLElement>, // empty if none
|
||||
// firstTabbableNode: HTMLElement|null,
|
||||
// lastTabbableNode: HTMLElement|null,
|
||||
// nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined
|
||||
// }>}
|
||||
containerGroups: [],
|
||||
// same order/length as `containers` list
|
||||
|
||||
// references to objects in `containerGroups`, but only those that actually have
|
||||
// tabbable nodes in them
|
||||
// NOTE: same order as `containers` and `containerGroups`, but __not necessarily__
|
||||
// the same length
|
||||
tabbableGroups: [],
|
||||
nodeFocusedBeforeActivation: null,
|
||||
mostRecentlyFocusedNode: null,
|
||||
active: false,
|
||||
paused: false,
|
||||
// timer ID for when delayInitialFocus is true and initial focus in this trap
|
||||
// has been delayed during activation
|
||||
delayInitialFocusTimer: undefined
|
||||
};
|
||||
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
|
||||
|
||||
/**
|
||||
* Gets a configuration option value.
|
||||
* @param {Object|undefined} configOverrideOptions If true, and option is defined in this set,
|
||||
* value will be taken from this object. Otherwise, value will be taken from base configuration.
|
||||
* @param {string} optionName Name of the option whose value is sought.
|
||||
* @param {string|undefined} [configOptionName] Name of option to use __instead of__ `optionName`
|
||||
* IIF `configOverrideOptions` is not defined. Otherwise, `optionName` is used.
|
||||
*/
|
||||
var getOption = function getOption(configOverrideOptions, optionName, configOptionName) {
|
||||
return configOverrideOptions && configOverrideOptions[optionName] !== undefined ? configOverrideOptions[optionName] : config[configOptionName || optionName];
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the index of the container that contains the element.
|
||||
* @param {HTMLElement} element
|
||||
* @param {Event} [event]
|
||||
* @returns {number} Index of the container in either `state.containers` or
|
||||
* `state.containerGroups` (the order/length of these lists are the same); -1
|
||||
* if the element isn't found.
|
||||
*/
|
||||
var findContainerIndex = function findContainerIndex(element, event) {
|
||||
var composedPath = typeof (event === null || event === void 0 ? void 0 : event.composedPath) === 'function' ? event.composedPath() : undefined;
|
||||
// NOTE: search `containerGroups` because it's possible a group contains no tabbable
|
||||
// nodes, but still contains focusable nodes (e.g. if they all have `tabindex=-1`)
|
||||
// and we still need to find the element in there
|
||||
return state.containerGroups.findIndex(function (_ref) {
|
||||
var container = _ref.container,
|
||||
tabbableNodes = _ref.tabbableNodes;
|
||||
return container.contains(element) || ( // fall back to explicit tabbable search which will take into consideration any
|
||||
// web components if the `tabbableOptions.getShadowRoot` option was used for
|
||||
// the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
|
||||
// look inside web components even if open)
|
||||
composedPath === null || composedPath === void 0 ? void 0 : composedPath.includes(container)) || tabbableNodes.find(function (node) {
|
||||
return node === element;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the node for the given option, which is expected to be an option that
|
||||
* can be either a DOM node, a string that is a selector to get a node, `false`
|
||||
* (if a node is explicitly NOT given), or a function that returns any of these
|
||||
* values.
|
||||
* @param {string} optionName
|
||||
* @returns {undefined | false | HTMLElement | SVGElement} Returns
|
||||
* `undefined` if the option is not specified; `false` if the option
|
||||
* resolved to `false` (node explicitly not given); otherwise, the resolved
|
||||
* DOM node.
|
||||
* @throws {Error} If the option is set, not `false`, and is not, or does not
|
||||
* resolve to a node.
|
||||
*/
|
||||
var getNodeForOption = function getNodeForOption(optionName) {
|
||||
var optionValue = config[optionName];
|
||||
if (typeof optionValue === 'function') {
|
||||
for (var _len2 = arguments.length, params = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
||||
params[_key2 - 1] = arguments[_key2];
|
||||
}
|
||||
optionValue = optionValue.apply(void 0, params);
|
||||
}
|
||||
if (optionValue === true) {
|
||||
optionValue = undefined; // use default value
|
||||
}
|
||||
|
||||
if (!optionValue) {
|
||||
if (optionValue === undefined || optionValue === false) {
|
||||
return optionValue;
|
||||
}
|
||||
// else, empty string (invalid), null (invalid), 0 (invalid)
|
||||
|
||||
throw new Error("`".concat(optionName, "` was specified but was not a node, or did not return a node"));
|
||||
}
|
||||
var node = optionValue; // could be HTMLElement, SVGElement, or non-empty string at this point
|
||||
|
||||
if (typeof optionValue === 'string') {
|
||||
node = doc.querySelector(optionValue); // resolve to node, or null if fails
|
||||
if (!node) {
|
||||
throw new Error("`".concat(optionName, "` as selector refers to no known node"));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
};
|
||||
var getInitialFocusNode = function getInitialFocusNode() {
|
||||
var node = getNodeForOption('initialFocus');
|
||||
|
||||
// false explicitly indicates we want no initialFocus at all
|
||||
if (node === false) {
|
||||
return false;
|
||||
}
|
||||
if (node === undefined || !tabbable.isFocusable(node, config.tabbableOptions)) {
|
||||
// option not specified nor focusable: use fallback options
|
||||
if (findContainerIndex(doc.activeElement) >= 0) {
|
||||
node = doc.activeElement;
|
||||
} else {
|
||||
var firstTabbableGroup = state.tabbableGroups[0];
|
||||
var firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
|
||||
|
||||
// NOTE: `fallbackFocus` option function cannot return `false` (not supported)
|
||||
node = firstTabbableNode || getNodeForOption('fallbackFocus');
|
||||
}
|
||||
}
|
||||
if (!node) {
|
||||
throw new Error('Your focus-trap needs to have at least one focusable element');
|
||||
}
|
||||
return node;
|
||||
};
|
||||
var updateTabbableNodes = function updateTabbableNodes() {
|
||||
state.containerGroups = state.containers.map(function (container) {
|
||||
var tabbableNodes = tabbable.tabbable(container, config.tabbableOptions);
|
||||
|
||||
// NOTE: if we have tabbable nodes, we must have focusable nodes; focusable nodes
|
||||
// are a superset of tabbable nodes
|
||||
var focusableNodes = tabbable.focusable(container, config.tabbableOptions);
|
||||
return {
|
||||
container: container,
|
||||
tabbableNodes: tabbableNodes,
|
||||
focusableNodes: focusableNodes,
|
||||
firstTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[0] : null,
|
||||
lastTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : null,
|
||||
/**
|
||||
* Finds the __tabbable__ node that follows the given node in the specified direction,
|
||||
* in this container, if any.
|
||||
* @param {HTMLElement} node
|
||||
* @param {boolean} [forward] True if going in forward tab order; false if going
|
||||
* in reverse.
|
||||
* @returns {HTMLElement|undefined} The next tabbable node, if any.
|
||||
*/
|
||||
nextTabbableNode: function nextTabbableNode(node) {
|
||||
var forward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
||||
// NOTE: If tabindex is positive (in order to manipulate the tab order separate
|
||||
// from the DOM order), this __will not work__ because the list of focusableNodes,
|
||||
// while it contains tabbable nodes, does not sort its nodes in any order other
|
||||
// than DOM order, because it can't: Where would you place focusable (but not
|
||||
// tabbable) nodes in that order? They have no order, because they aren't tabbale...
|
||||
// Support for positive tabindex is already broken and hard to manage (possibly
|
||||
// not supportable, TBD), so this isn't going to make things worse than they
|
||||
// already are, and at least makes things better for the majority of cases where
|
||||
// tabindex is either 0/unset or negative.
|
||||
// FYI, positive tabindex issue: https://github.com/focus-trap/focus-trap/issues/375
|
||||
var nodeIdx = focusableNodes.findIndex(function (n) {
|
||||
return n === node;
|
||||
});
|
||||
if (nodeIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (forward) {
|
||||
return focusableNodes.slice(nodeIdx + 1).find(function (n) {
|
||||
return tabbable.isTabbable(n, config.tabbableOptions);
|
||||
});
|
||||
}
|
||||
return focusableNodes.slice(0, nodeIdx).reverse().find(function (n) {
|
||||
return tabbable.isTabbable(n, config.tabbableOptions);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
state.tabbableGroups = state.containerGroups.filter(function (group) {
|
||||
return group.tabbableNodes.length > 0;
|
||||
});
|
||||
|
||||
// throw if no groups have tabbable nodes and we don't have a fallback focus node either
|
||||
if (state.tabbableGroups.length <= 0 && !getNodeForOption('fallbackFocus') // returning false not supported for this option
|
||||
) {
|
||||
throw new Error('Your focus-trap must have at least one container with at least one tabbable node in it at all times');
|
||||
}
|
||||
};
|
||||
var tryFocus = function tryFocus(node) {
|
||||
if (node === false) {
|
||||
return;
|
||||
}
|
||||
if (node === doc.activeElement) {
|
||||
return;
|
||||
}
|
||||
if (!node || !node.focus) {
|
||||
tryFocus(getInitialFocusNode());
|
||||
return;
|
||||
}
|
||||
node.focus({
|
||||
preventScroll: !!config.preventScroll
|
||||
});
|
||||
state.mostRecentlyFocusedNode = node;
|
||||
if (isSelectableInput(node)) {
|
||||
node.select();
|
||||
}
|
||||
};
|
||||
var getReturnFocusNode = function getReturnFocusNode(previousActiveElement) {
|
||||
var node = getNodeForOption('setReturnFocus', previousActiveElement);
|
||||
return node ? node : node === false ? false : previousActiveElement;
|
||||
};
|
||||
|
||||
// This needs to be done on mousedown and touchstart instead of click
|
||||
// so that it precedes the focus event.
|
||||
var checkPointerDown = function checkPointerDown(e) {
|
||||
var target = getActualTarget(e);
|
||||
if (findContainerIndex(target, e) >= 0) {
|
||||
// allow the click since it ocurred inside the trap
|
||||
return;
|
||||
}
|
||||
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
||||
// immediately deactivate the trap
|
||||
trap.deactivate({
|
||||
// NOTE: by setting `returnFocus: false`, deactivate() will do nothing,
|
||||
// which will result in the outside click setting focus to the node
|
||||
// that was clicked (and if not focusable, to "nothing"); by setting
|
||||
// `returnFocus: true`, we'll attempt to re-focus the node originally-focused
|
||||
// on activation (or the configured `setReturnFocus` node), whether the
|
||||
// outside click was on a focusable node or not
|
||||
returnFocus: config.returnFocusOnDeactivate
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// This is needed for mobile devices.
|
||||
// (If we'll only let `click` events through,
|
||||
// then on mobile they will be blocked anyways if `touchstart` is blocked.)
|
||||
if (valueOrHandler(config.allowOutsideClick, e)) {
|
||||
// allow the click outside the trap to take place
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, prevent the click
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// In case focus escapes the trap for some strange reason, pull it back in.
|
||||
var checkFocusIn = function checkFocusIn(e) {
|
||||
var target = getActualTarget(e);
|
||||
var targetContained = findContainerIndex(target, e) >= 0;
|
||||
|
||||
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
||||
if (targetContained || target instanceof Document) {
|
||||
if (targetContained) {
|
||||
state.mostRecentlyFocusedNode = target;
|
||||
}
|
||||
} else {
|
||||
// escaped! pull it back in to where it just left
|
||||
e.stopImmediatePropagation();
|
||||
tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
|
||||
}
|
||||
};
|
||||
|
||||
// Hijack key nav events on the first and last focusable nodes of the trap,
|
||||
// in order to prevent focus from escaping. If it escapes for even a
|
||||
// moment it can end up scrolling the page and causing confusion so we
|
||||
// kind of need to capture the action at the keydown phase.
|
||||
var checkKeyNav = function checkKeyNav(event) {
|
||||
var isBackward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
||||
var target = getActualTarget(event);
|
||||
updateTabbableNodes();
|
||||
var destinationNode = null;
|
||||
if (state.tabbableGroups.length > 0) {
|
||||
// make sure the target is actually contained in a group
|
||||
// NOTE: the target may also be the container itself if it's focusable
|
||||
// with tabIndex='-1' and was given initial focus
|
||||
var containerIndex = findContainerIndex(target, event);
|
||||
var containerGroup = containerIndex >= 0 ? state.containerGroups[containerIndex] : undefined;
|
||||
if (containerIndex < 0) {
|
||||
// target not found in any group: quite possible focus has escaped the trap,
|
||||
// so bring it back into...
|
||||
if (isBackward) {
|
||||
// ...the last node in the last group
|
||||
destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode;
|
||||
} else {
|
||||
// ...the first node in the first group
|
||||
destinationNode = state.tabbableGroups[0].firstTabbableNode;
|
||||
}
|
||||
} else if (isBackward) {
|
||||
// REVERSE
|
||||
|
||||
// is the target the first tabbable node in a group?
|
||||
var startOfGroupIndex = findIndex(state.tabbableGroups, function (_ref2) {
|
||||
var firstTabbableNode = _ref2.firstTabbableNode;
|
||||
return target === firstTabbableNode;
|
||||
});
|
||||
if (startOfGroupIndex < 0 && (containerGroup.container === target || tabbable.isFocusable(target, config.tabbableOptions) && !tabbable.isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target, false))) {
|
||||
// an exception case where the target is either the container itself, or
|
||||
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
||||
// and user clicked on it or node was programmatically given focus)
|
||||
// and is not followed by any other tabbable node, in which
|
||||
// case, we should handle shift+tab as if focus were on the container's
|
||||
// first tabbable node, and go to the last tabbable node of the LAST group
|
||||
startOfGroupIndex = containerIndex;
|
||||
}
|
||||
if (startOfGroupIndex >= 0) {
|
||||
// YES: then shift+tab should go to the last tabbable node in the
|
||||
// previous group (and wrap around to the last tabbable node of
|
||||
// the LAST group if it's the first tabbable node of the FIRST group)
|
||||
var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
|
||||
var destinationGroup = state.tabbableGroups[destinationGroupIndex];
|
||||
destinationNode = destinationGroup.lastTabbableNode;
|
||||
} else if (!isTabEvent(event)) {
|
||||
// user must have customized the nav keys so we have to move focus manually _within_
|
||||
// the active group: do this based on the order determined by tabbable()
|
||||
destinationNode = containerGroup.nextTabbableNode(target, false);
|
||||
}
|
||||
} else {
|
||||
// FORWARD
|
||||
|
||||
// is the target the last tabbable node in a group?
|
||||
var lastOfGroupIndex = findIndex(state.tabbableGroups, function (_ref3) {
|
||||
var lastTabbableNode = _ref3.lastTabbableNode;
|
||||
return target === lastTabbableNode;
|
||||
});
|
||||
if (lastOfGroupIndex < 0 && (containerGroup.container === target || tabbable.isFocusable(target, config.tabbableOptions) && !tabbable.isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target))) {
|
||||
// an exception case where the target is the container itself, or
|
||||
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
||||
// and user clicked on it or node was programmatically given focus)
|
||||
// and is not followed by any other tabbable node, in which
|
||||
// case, we should handle tab as if focus were on the container's
|
||||
// last tabbable node, and go to the first tabbable node of the FIRST group
|
||||
lastOfGroupIndex = containerIndex;
|
||||
}
|
||||
if (lastOfGroupIndex >= 0) {
|
||||
// YES: then tab should go to the first tabbable node in the next
|
||||
// group (and wrap around to the first tabbable node of the FIRST
|
||||
// group if it's the last tabbable node of the LAST group)
|
||||
var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
|
||||
var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
|
||||
destinationNode = _destinationGroup.firstTabbableNode;
|
||||
} else if (!isTabEvent(event)) {
|
||||
// user must have customized the nav keys so we have to move focus manually _within_
|
||||
// the active group: do this based on the order determined by tabbable()
|
||||
destinationNode = containerGroup.nextTabbableNode(target);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no groups available
|
||||
// NOTE: the fallbackFocus option does not support returning false to opt-out
|
||||
destinationNode = getNodeForOption('fallbackFocus');
|
||||
}
|
||||
if (destinationNode) {
|
||||
if (isTabEvent(event)) {
|
||||
// since tab natively moves focus, we wouldn't have a destination node unless we
|
||||
// were on the edge of a container and had to move to the next/previous edge, in
|
||||
// which case we want to prevent default to keep the browser from moving focus
|
||||
// to where it normally would
|
||||
event.preventDefault();
|
||||
}
|
||||
tryFocus(destinationNode);
|
||||
}
|
||||
// else, let the browser take care of [shift+]tab and move the focus
|
||||
};
|
||||
|
||||
var checkKey = function checkKey(event) {
|
||||
if (isEscapeEvent(event) && valueOrHandler(config.escapeDeactivates, event) !== false) {
|
||||
event.preventDefault();
|
||||
trap.deactivate();
|
||||
return;
|
||||
}
|
||||
if (config.isKeyForward(event) || config.isKeyBackward(event)) {
|
||||
checkKeyNav(event, config.isKeyBackward(event));
|
||||
}
|
||||
};
|
||||
var checkClick = function checkClick(e) {
|
||||
var target = getActualTarget(e);
|
||||
if (findContainerIndex(target, e) >= 0) {
|
||||
return;
|
||||
}
|
||||
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
||||
return;
|
||||
}
|
||||
if (valueOrHandler(config.allowOutsideClick, e)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
//
|
||||
// EVENT LISTENERS
|
||||
//
|
||||
|
||||
var addListeners = function addListeners() {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There can be only one listening focus trap at a time
|
||||
activeFocusTraps.activateTrap(trapStack, trap);
|
||||
|
||||
// Delay ensures that the focused element doesn't capture the event
|
||||
// that caused the focus trap activation.
|
||||
state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () {
|
||||
tryFocus(getInitialFocusNode());
|
||||
}) : tryFocus(getInitialFocusNode());
|
||||
doc.addEventListener('focusin', checkFocusIn, true);
|
||||
doc.addEventListener('mousedown', checkPointerDown, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
doc.addEventListener('touchstart', checkPointerDown, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
doc.addEventListener('click', checkClick, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
doc.addEventListener('keydown', checkKey, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
return trap;
|
||||
};
|
||||
var removeListeners = function removeListeners() {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
doc.removeEventListener('focusin', checkFocusIn, true);
|
||||
doc.removeEventListener('mousedown', checkPointerDown, true);
|
||||
doc.removeEventListener('touchstart', checkPointerDown, true);
|
||||
doc.removeEventListener('click', checkClick, true);
|
||||
doc.removeEventListener('keydown', checkKey, true);
|
||||
return trap;
|
||||
};
|
||||
|
||||
//
|
||||
// MUTATION OBSERVER
|
||||
//
|
||||
|
||||
var checkDomRemoval = function checkDomRemoval(mutations) {
|
||||
var isFocusedNodeRemoved = mutations.some(function (mutation) {
|
||||
var removedNodes = Array.from(mutation.removedNodes);
|
||||
return removedNodes.some(function (node) {
|
||||
return node === state.mostRecentlyFocusedNode;
|
||||
});
|
||||
});
|
||||
|
||||
// If the currently focused is removed then browsers will move focus to the
|
||||
// <body> element. If this happens, try to move focus back into the trap.
|
||||
if (isFocusedNodeRemoved) {
|
||||
tryFocus(getInitialFocusNode());
|
||||
}
|
||||
};
|
||||
|
||||
// Use MutationObserver - if supported - to detect if focused node is removed
|
||||
// from the DOM.
|
||||
var mutationObserver = typeof window !== 'undefined' && 'MutationObserver' in window ? new MutationObserver(checkDomRemoval) : undefined;
|
||||
var updateObservedNodes = function updateObservedNodes() {
|
||||
if (!mutationObserver) {
|
||||
return;
|
||||
}
|
||||
mutationObserver.disconnect();
|
||||
if (state.active && !state.paused) {
|
||||
state.containers.map(function (container) {
|
||||
mutationObserver.observe(container, {
|
||||
subtree: true,
|
||||
childList: true
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// TRAP DEFINITION
|
||||
//
|
||||
|
||||
trap = {
|
||||
get active() {
|
||||
return state.active;
|
||||
},
|
||||
get paused() {
|
||||
return state.paused;
|
||||
},
|
||||
activate: function activate(activateOptions) {
|
||||
if (state.active) {
|
||||
return this;
|
||||
}
|
||||
var onActivate = getOption(activateOptions, 'onActivate');
|
||||
var onPostActivate = getOption(activateOptions, 'onPostActivate');
|
||||
var checkCanFocusTrap = getOption(activateOptions, 'checkCanFocusTrap');
|
||||
if (!checkCanFocusTrap) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
state.active = true;
|
||||
state.paused = false;
|
||||
state.nodeFocusedBeforeActivation = doc.activeElement;
|
||||
onActivate === null || onActivate === void 0 ? void 0 : onActivate();
|
||||
var finishActivation = function finishActivation() {
|
||||
if (checkCanFocusTrap) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
addListeners();
|
||||
updateObservedNodes();
|
||||
onPostActivate === null || onPostActivate === void 0 ? void 0 : onPostActivate();
|
||||
};
|
||||
if (checkCanFocusTrap) {
|
||||
checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation);
|
||||
return this;
|
||||
}
|
||||
finishActivation();
|
||||
return this;
|
||||
},
|
||||
deactivate: function deactivate(deactivateOptions) {
|
||||
if (!state.active) {
|
||||
return this;
|
||||
}
|
||||
var options = _objectSpread2({
|
||||
onDeactivate: config.onDeactivate,
|
||||
onPostDeactivate: config.onPostDeactivate,
|
||||
checkCanReturnFocus: config.checkCanReturnFocus
|
||||
}, deactivateOptions);
|
||||
clearTimeout(state.delayInitialFocusTimer); // noop if undefined
|
||||
state.delayInitialFocusTimer = undefined;
|
||||
removeListeners();
|
||||
state.active = false;
|
||||
state.paused = false;
|
||||
updateObservedNodes();
|
||||
activeFocusTraps.deactivateTrap(trapStack, trap);
|
||||
var onDeactivate = getOption(options, 'onDeactivate');
|
||||
var onPostDeactivate = getOption(options, 'onPostDeactivate');
|
||||
var checkCanReturnFocus = getOption(options, 'checkCanReturnFocus');
|
||||
var returnFocus = getOption(options, 'returnFocus', 'returnFocusOnDeactivate');
|
||||
onDeactivate === null || onDeactivate === void 0 ? void 0 : onDeactivate();
|
||||
var finishDeactivation = function finishDeactivation() {
|
||||
delay(function () {
|
||||
if (returnFocus) {
|
||||
tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
|
||||
}
|
||||
onPostDeactivate === null || onPostDeactivate === void 0 ? void 0 : onPostDeactivate();
|
||||
});
|
||||
};
|
||||
if (returnFocus && checkCanReturnFocus) {
|
||||
checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation);
|
||||
return this;
|
||||
}
|
||||
finishDeactivation();
|
||||
return this;
|
||||
},
|
||||
pause: function pause(pauseOptions) {
|
||||
if (state.paused || !state.active) {
|
||||
return this;
|
||||
}
|
||||
var onPause = getOption(pauseOptions, 'onPause');
|
||||
var onPostPause = getOption(pauseOptions, 'onPostPause');
|
||||
state.paused = true;
|
||||
onPause === null || onPause === void 0 ? void 0 : onPause();
|
||||
removeListeners();
|
||||
updateObservedNodes();
|
||||
onPostPause === null || onPostPause === void 0 ? void 0 : onPostPause();
|
||||
return this;
|
||||
},
|
||||
unpause: function unpause(unpauseOptions) {
|
||||
if (!state.paused || !state.active) {
|
||||
return this;
|
||||
}
|
||||
var onUnpause = getOption(unpauseOptions, 'onUnpause');
|
||||
var onPostUnpause = getOption(unpauseOptions, 'onPostUnpause');
|
||||
state.paused = false;
|
||||
onUnpause === null || onUnpause === void 0 ? void 0 : onUnpause();
|
||||
updateTabbableNodes();
|
||||
addListeners();
|
||||
updateObservedNodes();
|
||||
onPostUnpause === null || onPostUnpause === void 0 ? void 0 : onPostUnpause();
|
||||
return this;
|
||||
},
|
||||
updateContainerElements: function updateContainerElements(containerElements) {
|
||||
var elementsAsArray = [].concat(containerElements).filter(Boolean);
|
||||
state.containers = elementsAsArray.map(function (element) {
|
||||
return typeof element === 'string' ? doc.querySelector(element) : element;
|
||||
});
|
||||
if (state.active) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
updateObservedNodes();
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
// initialize container elements
|
||||
trap.updateContainerElements(elements);
|
||||
return trap;
|
||||
};
|
||||
|
||||
exports.createFocusTrap = createFocusTrap;
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=focus-trap.umd.js.map
|
1
node_modules/focus-trap/dist/focus-trap.umd.js.map
generated
vendored
Normal file
1
node_modules/focus-trap/dist/focus-trap.umd.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6
node_modules/focus-trap/dist/focus-trap.umd.min.js
generated
vendored
Normal file
6
node_modules/focus-trap/dist/focus-trap.umd.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
node_modules/focus-trap/dist/focus-trap.umd.min.js.map
generated
vendored
Normal file
1
node_modules/focus-trap/dist/focus-trap.umd.min.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
253
node_modules/focus-trap/index.d.ts
generated
vendored
Normal file
253
node_modules/focus-trap/index.d.ts
generated
vendored
Normal file
|
@ -0,0 +1,253 @@
|
|||
import { CheckOptions as TabbableCheckOptions } from 'tabbable';
|
||||
|
||||
declare module 'focus-trap' {
|
||||
export type FocusTargetValue = HTMLElement | SVGElement | string;
|
||||
export type FocusTargetValueOrFalse = FocusTargetValue | false;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export type FocusTarget = FocusTargetValue | (() => FocusTargetValue);
|
||||
|
||||
/**
|
||||
* A DOM node, a selector string (which will be passed to
|
||||
* `document.querySelector()` to find the DOM node), `false` to explicitly indicate
|
||||
* an opt-out, or a function that returns a DOM node or `false`.
|
||||
*/
|
||||
export type FocusTargetOrFalse = FocusTargetValueOrFalse | (() => FocusTargetValueOrFalse);
|
||||
|
||||
type MouseEventToBoolean = (event: MouseEvent | TouchEvent) => boolean;
|
||||
type KeyboardEventToBoolean = (event: KeyboardEvent) => boolean;
|
||||
|
||||
/** tabbable options supported by focus-trap. */
|
||||
export interface FocusTrapTabbableOptions extends TabbableCheckOptions {
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
/**
|
||||
* A function that will be called **before** sending focus to the
|
||||
* target element upon activation.
|
||||
*/
|
||||
onActivate?: () => void;
|
||||
|
||||
/**
|
||||
* A function that will be called **after** focus has been sent to the
|
||||
* target element upon activation.
|
||||
*/
|
||||
onPostActivate?: () => void;
|
||||
|
||||
/**
|
||||
* A function that will be called immediately after the trap's state is updated to be paused.
|
||||
*/
|
||||
onPause?: () => void;
|
||||
|
||||
/**
|
||||
* A function that will be called after the trap has been completely paused and is no longer
|
||||
* managing/trapping focus.
|
||||
*/
|
||||
onPostPause?: () => void;
|
||||
|
||||
/**
|
||||
* A function that will be called immediately after the trap's state is updated to be active
|
||||
* again, but prior to updating its knowledge of what nodes are tabbable within its containers,
|
||||
* and prior to actively managing/trapping focus.
|
||||
*/
|
||||
onUnpause?: () => void;
|
||||
|
||||
/**
|
||||
* A function that will be called after the trap has been completely unpaused and is once
|
||||
* again managing/trapping focus.
|
||||
*/
|
||||
onPostUnpause?: () => void;
|
||||
|
||||
/**
|
||||
* A function for determining if it is safe to send focus to the focus trap
|
||||
* or not.
|
||||
*
|
||||
* It should return a promise that only resolves once all the listed `containers`
|
||||
* are able to receive focus.
|
||||
*
|
||||
* The purpose of this is to prevent early focus-trap activation on animated
|
||||
* dialogs that fade in and out. When a dialog fades in, there is a brief delay
|
||||
* between the activation of the trap and the trap element being focusable.
|
||||
*/
|
||||
checkCanFocusTrap?: (
|
||||
containers: Array<HTMLElement | SVGElement>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* A function that will be called **before** sending focus to the
|
||||
* trigger element upon deactivation.
|
||||
*/
|
||||
onDeactivate?: () => void;
|
||||
|
||||
/**
|
||||
* A function that will be called after the trap is deactivated, after `onDeactivate`.
|
||||
* If `returnFocus` was set, it will be called **after** focus has been sent to the trigger
|
||||
* element upon deactivation; otherwise, it will be called after deactivation completes.
|
||||
*/
|
||||
onPostDeactivate?: () => void;
|
||||
/**
|
||||
* A function for determining if it is safe to send focus back to the `trigger` element.
|
||||
*
|
||||
* It should return a promise that only resolves once `trigger` is focusable.
|
||||
*
|
||||
* The purpose of this is to prevent the focus being sent to an animated trigger element too early.
|
||||
* If a trigger element fades in upon trap deactivation, there is a brief delay between the deactivation
|
||||
* of the trap and when the trigger element is focusable.
|
||||
*
|
||||
* `trigger` will be either the node that had focus prior to the trap being activated,
|
||||
* or the result of the `setReturnFocus` option, if configured.
|
||||
*
|
||||
* This handler is **not** called if the `returnFocusOnDeactivate` configuration option
|
||||
* (or the `returnFocus` deactivation option) is falsy.
|
||||
*/
|
||||
checkCanReturnFocus?: (trigger: HTMLElement | SVGElement) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 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, or use `false`
|
||||
* for no initially focused element at all.
|
||||
*
|
||||
* NOTE: Setting this option to `false` (or a function that returns `false`)
|
||||
* will prevent the `fallbackFocus` option from being used.
|
||||
*/
|
||||
initialFocus?: FocusTargetOrFalse;
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* NOTE: If `initialFocus` is `false` (or a function that returns `false`),
|
||||
* this function will not be called when the trap is activated, and no element
|
||||
* will be initially focused. This function may still be called while the trap
|
||||
* is active if things change such that there are no longer any tabbable nodes
|
||||
* in the trap.
|
||||
*/
|
||||
fallbackFocus?: FocusTarget;
|
||||
/**
|
||||
* Default: `true`. If `false`, when the trap is deactivated,
|
||||
* focus will *not* return to the element that had focus before activation.
|
||||
*/
|
||||
returnFocusOnDeactivate?: boolean;
|
||||
/**
|
||||
* By default, focus trap on deactivation will return to the element
|
||||
* that was focused before activation.
|
||||
*/
|
||||
setReturnFocus?:
|
||||
| FocusTargetValueOrFalse
|
||||
| ((
|
||||
nodeFocusedBeforeActivation: HTMLElement | SVGElement
|
||||
) => FocusTargetValueOrFalse);
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
escapeDeactivates?: boolean | KeyboardEventToBoolean;
|
||||
/**
|
||||
* 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`, causing
|
||||
* that option to be ignored. Default: `false`.
|
||||
*/
|
||||
clickOutsideDeactivates?: boolean | MouseEventToBoolean;
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
allowOutsideClick?: boolean | MouseEventToBoolean;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
preventScroll?: 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.
|
||||
*/
|
||||
delayInitialFocus?: boolean;
|
||||
/**
|
||||
* Default: `window.document`. Document where the focus trap will be active.
|
||||
* This allows to use FocusTrap in an iFrame context.
|
||||
*/
|
||||
document?: Document;
|
||||
|
||||
/**
|
||||
* Specific tabbable options configurable on focus-trap.
|
||||
*/
|
||||
tabbableOptions?: FocusTrapTabbableOptions;
|
||||
|
||||
/**
|
||||
* Define the global trap stack. This makes it possible to share the same stack
|
||||
* in multiple instances of `focus-trap` in the same page such that
|
||||
* auto-activation/pausing of traps is properly coordinated among all instances
|
||||
* as activating a trap when another is already active should result in the other
|
||||
* being auto-paused. By default, each instance will have its own internal stack,
|
||||
* leading to conflicts if they each try to trap the focus at the same time.
|
||||
*/
|
||||
trapStack?: Array<FocusTrap>;
|
||||
|
||||
/**
|
||||
* Determines if the given keyboard event is a "tab forward" event that will move
|
||||
* the focus to the next trapped element in tab order. Defaults to the `TAB` key.
|
||||
* Use this to override the trap's behavior if you want to use arrow keys to control
|
||||
* keyboard navigation within the trap, for example. Also see `isKeyBackward()` option.
|
||||
*/
|
||||
isKeyForward?: KeyboardEventToBoolean;
|
||||
|
||||
/**
|
||||
* Determines if the given keyboard event is a "tab backward" event that will move
|
||||
* the focus to the previous trapped element in tab order. Defaults to the `SHIFT+TAB` key.
|
||||
* Use this to override the trap's behavior if you want to use arrow keys to control
|
||||
* keyboard navigation within the trap, for example. Also see `isKeyForward()` option.
|
||||
*/
|
||||
isKeyBackward?: KeyboardEventToBoolean;
|
||||
}
|
||||
|
||||
type ActivateOptions = Pick<Options, 'onActivate' | 'onPostActivate' | 'checkCanFocusTrap'>;
|
||||
type PauseOptions = Pick<Options, 'onPause' | 'onPostPause'>;
|
||||
type UnpauseOptions = Pick<Options, 'onUnpause' | 'onPostUnpause'>;
|
||||
|
||||
interface DeactivateOptions extends Pick<Options, 'onDeactivate' | 'onPostDeactivate' | 'checkCanReturnFocus'> {
|
||||
returnFocus?: boolean;
|
||||
}
|
||||
|
||||
export interface FocusTrap {
|
||||
active: boolean,
|
||||
paused: boolean,
|
||||
activate(activateOptions?: ActivateOptions): FocusTrap;
|
||||
deactivate(deactivateOptions?: DeactivateOptions): FocusTrap;
|
||||
pause(pauseOptions?: PauseOptions): FocusTrap;
|
||||
unpause(unpauseOptions?: UnpauseOptions): FocusTrap;
|
||||
updateContainerElements(containerElements: HTMLElement | SVGElement | string | Array<HTMLElement | SVGElement | string>): FocusTrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new focus trap on `element`.
|
||||
*
|
||||
* @param element
|
||||
* The element to be the focus trap, or a selector that will be used to
|
||||
* find the element.
|
||||
*/
|
||||
export function createFocusTrap(
|
||||
element: HTMLElement | SVGElement | string | Array<HTMLElement | SVGElement | string>,
|
||||
userOptions?: Options
|
||||
): FocusTrap;
|
||||
}
|
866
node_modules/focus-trap/index.js
generated
vendored
Normal file
866
node_modules/focus-trap/index.js
generated
vendored
Normal file
|
@ -0,0 +1,866 @@
|
|||
import { tabbable, focusable, isFocusable, isTabbable } from 'tabbable';
|
||||
|
||||
const activeFocusTraps = {
|
||||
activateTrap(trapStack, trap) {
|
||||
if (trapStack.length > 0) {
|
||||
const activeTrap = trapStack[trapStack.length - 1];
|
||||
if (activeTrap !== trap) {
|
||||
activeTrap.pause();
|
||||
}
|
||||
}
|
||||
|
||||
const trapIndex = trapStack.indexOf(trap);
|
||||
if (trapIndex === -1) {
|
||||
trapStack.push(trap);
|
||||
} else {
|
||||
// move this existing trap to the front of the queue
|
||||
trapStack.splice(trapIndex, 1);
|
||||
trapStack.push(trap);
|
||||
}
|
||||
},
|
||||
|
||||
deactivateTrap(trapStack, trap) {
|
||||
const trapIndex = trapStack.indexOf(trap);
|
||||
if (trapIndex !== -1) {
|
||||
trapStack.splice(trapIndex, 1);
|
||||
}
|
||||
|
||||
if (trapStack.length > 0) {
|
||||
trapStack[trapStack.length - 1].unpause();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const isSelectableInput = function (node) {
|
||||
return (
|
||||
node.tagName &&
|
||||
node.tagName.toLowerCase() === 'input' &&
|
||||
typeof node.select === 'function'
|
||||
);
|
||||
};
|
||||
|
||||
const isEscapeEvent = function (e) {
|
||||
return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
|
||||
};
|
||||
|
||||
const isTabEvent = function (e) {
|
||||
return e.key === 'Tab' || e.keyCode === 9;
|
||||
};
|
||||
|
||||
// checks for TAB by default
|
||||
const isKeyForward = function (e) {
|
||||
return isTabEvent(e) && !e.shiftKey;
|
||||
};
|
||||
|
||||
// checks for SHIFT+TAB by default
|
||||
const isKeyBackward = function (e) {
|
||||
return isTabEvent(e) && e.shiftKey;
|
||||
};
|
||||
|
||||
const delay = function (fn) {
|
||||
return setTimeout(fn, 0);
|
||||
};
|
||||
|
||||
// Array.find/findIndex() are not supported on IE; this replicates enough
|
||||
// of Array.findIndex() for our needs
|
||||
const findIndex = function (arr, fn) {
|
||||
let idx = -1;
|
||||
|
||||
arr.every(function (value, i) {
|
||||
if (fn(value)) {
|
||||
idx = i;
|
||||
return false; // break
|
||||
}
|
||||
|
||||
return true; // next
|
||||
});
|
||||
|
||||
return idx;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an option's value when it could be a plain value, or a handler that provides
|
||||
* the value.
|
||||
* @param {*} value Option's value to check.
|
||||
* @param {...*} [params] Any parameters to pass to the handler, if `value` is a function.
|
||||
* @returns {*} The `value`, or the handler's returned value.
|
||||
*/
|
||||
const valueOrHandler = function (value, ...params) {
|
||||
return typeof value === 'function' ? value(...params) : value;
|
||||
};
|
||||
|
||||
const getActualTarget = function (event) {
|
||||
// NOTE: If the trap is _inside_ a shadow DOM, event.target will always be the
|
||||
// shadow host. However, event.target.composedPath() will be an array of
|
||||
// nodes "clicked" from inner-most (the actual element inside the shadow) to
|
||||
// outer-most (the host HTML document). If we have access to composedPath(),
|
||||
// then use its first element; otherwise, fall back to event.target (and
|
||||
// this only works for an _open_ shadow DOM; otherwise,
|
||||
// composedPath()[0] === event.target always).
|
||||
return event.target.shadowRoot && typeof event.composedPath === 'function'
|
||||
? event.composedPath()[0]
|
||||
: event.target;
|
||||
};
|
||||
|
||||
// NOTE: this must be _outside_ `createFocusTrap()` to make sure all traps in this
|
||||
// current instance use the same stack if `userOptions.trapStack` isn't specified
|
||||
const internalTrapStack = [];
|
||||
|
||||
const createFocusTrap = function (elements, userOptions) {
|
||||
// SSR: a live trap shouldn't be created in this type of environment so this
|
||||
// should be safe code to execute if the `document` option isn't specified
|
||||
const doc = userOptions?.document || document;
|
||||
|
||||
const trapStack = userOptions?.trapStack || internalTrapStack;
|
||||
|
||||
const config = {
|
||||
returnFocusOnDeactivate: true,
|
||||
escapeDeactivates: true,
|
||||
delayInitialFocus: true,
|
||||
isKeyForward,
|
||||
isKeyBackward,
|
||||
...userOptions,
|
||||
};
|
||||
|
||||
const state = {
|
||||
// containers given to createFocusTrap()
|
||||
// @type {Array<HTMLElement>}
|
||||
containers: [],
|
||||
|
||||
// list of objects identifying tabbable nodes in `containers` in the trap
|
||||
// NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap
|
||||
// is active, but the trap should never get to a state where there isn't at least one group
|
||||
// with at least one tabbable node in it (that would lead to an error condition that would
|
||||
// result in an error being thrown)
|
||||
// @type {Array<{
|
||||
// container: HTMLElement,
|
||||
// tabbableNodes: Array<HTMLElement>, // empty if none
|
||||
// focusableNodes: Array<HTMLElement>, // empty if none
|
||||
// firstTabbableNode: HTMLElement|null,
|
||||
// lastTabbableNode: HTMLElement|null,
|
||||
// nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined
|
||||
// }>}
|
||||
containerGroups: [], // same order/length as `containers` list
|
||||
|
||||
// references to objects in `containerGroups`, but only those that actually have
|
||||
// tabbable nodes in them
|
||||
// NOTE: same order as `containers` and `containerGroups`, but __not necessarily__
|
||||
// the same length
|
||||
tabbableGroups: [],
|
||||
|
||||
nodeFocusedBeforeActivation: null,
|
||||
mostRecentlyFocusedNode: null,
|
||||
active: false,
|
||||
paused: false,
|
||||
|
||||
// timer ID for when delayInitialFocus is true and initial focus in this trap
|
||||
// has been delayed during activation
|
||||
delayInitialFocusTimer: undefined,
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* Gets a configuration option value.
|
||||
* @param {Object|undefined} configOverrideOptions If true, and option is defined in this set,
|
||||
* value will be taken from this object. Otherwise, value will be taken from base configuration.
|
||||
* @param {string} optionName Name of the option whose value is sought.
|
||||
* @param {string|undefined} [configOptionName] Name of option to use __instead of__ `optionName`
|
||||
* IIF `configOverrideOptions` is not defined. Otherwise, `optionName` is used.
|
||||
*/
|
||||
const getOption = (configOverrideOptions, optionName, configOptionName) => {
|
||||
return configOverrideOptions &&
|
||||
configOverrideOptions[optionName] !== undefined
|
||||
? configOverrideOptions[optionName]
|
||||
: config[configOptionName || optionName];
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the index of the container that contains the element.
|
||||
* @param {HTMLElement} element
|
||||
* @param {Event} [event]
|
||||
* @returns {number} Index of the container in either `state.containers` or
|
||||
* `state.containerGroups` (the order/length of these lists are the same); -1
|
||||
* if the element isn't found.
|
||||
*/
|
||||
const findContainerIndex = function (element, event) {
|
||||
const composedPath =
|
||||
typeof event?.composedPath === 'function'
|
||||
? event.composedPath()
|
||||
: undefined;
|
||||
// NOTE: search `containerGroups` because it's possible a group contains no tabbable
|
||||
// nodes, but still contains focusable nodes (e.g. if they all have `tabindex=-1`)
|
||||
// and we still need to find the element in there
|
||||
return state.containerGroups.findIndex(
|
||||
({ container, tabbableNodes }) =>
|
||||
container.contains(element) ||
|
||||
// fall back to explicit tabbable search which will take into consideration any
|
||||
// web components if the `tabbableOptions.getShadowRoot` option was used for
|
||||
// the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
|
||||
// look inside web components even if open)
|
||||
composedPath?.includes(container) ||
|
||||
tabbableNodes.find((node) => node === element)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the node for the given option, which is expected to be an option that
|
||||
* can be either a DOM node, a string that is a selector to get a node, `false`
|
||||
* (if a node is explicitly NOT given), or a function that returns any of these
|
||||
* values.
|
||||
* @param {string} optionName
|
||||
* @returns {undefined | false | HTMLElement | SVGElement} Returns
|
||||
* `undefined` if the option is not specified; `false` if the option
|
||||
* resolved to `false` (node explicitly not given); otherwise, the resolved
|
||||
* DOM node.
|
||||
* @throws {Error} If the option is set, not `false`, and is not, or does not
|
||||
* resolve to a node.
|
||||
*/
|
||||
const getNodeForOption = function (optionName, ...params) {
|
||||
let optionValue = config[optionName];
|
||||
|
||||
if (typeof optionValue === 'function') {
|
||||
optionValue = optionValue(...params);
|
||||
}
|
||||
|
||||
if (optionValue === true) {
|
||||
optionValue = undefined; // use default value
|
||||
}
|
||||
|
||||
if (!optionValue) {
|
||||
if (optionValue === undefined || optionValue === false) {
|
||||
return optionValue;
|
||||
}
|
||||
// else, empty string (invalid), null (invalid), 0 (invalid)
|
||||
|
||||
throw new Error(
|
||||
`\`${optionName}\` was specified but was not a node, or did not return a node`
|
||||
);
|
||||
}
|
||||
|
||||
let node = optionValue; // could be HTMLElement, SVGElement, or non-empty string at this point
|
||||
|
||||
if (typeof optionValue === 'string') {
|
||||
node = doc.querySelector(optionValue); // resolve to node, or null if fails
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
`\`${optionName}\` as selector refers to no known node`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
const getInitialFocusNode = function () {
|
||||
let node = getNodeForOption('initialFocus');
|
||||
|
||||
// false explicitly indicates we want no initialFocus at all
|
||||
if (node === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node === undefined || !isFocusable(node, config.tabbableOptions)) {
|
||||
// option not specified nor focusable: use fallback options
|
||||
if (findContainerIndex(doc.activeElement) >= 0) {
|
||||
node = doc.activeElement;
|
||||
} else {
|
||||
const firstTabbableGroup = state.tabbableGroups[0];
|
||||
const firstTabbableNode =
|
||||
firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
|
||||
|
||||
// NOTE: `fallbackFocus` option function cannot return `false` (not supported)
|
||||
node = firstTabbableNode || getNodeForOption('fallbackFocus');
|
||||
}
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
'Your focus-trap needs to have at least one focusable element'
|
||||
);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
const updateTabbableNodes = function () {
|
||||
state.containerGroups = state.containers.map((container) => {
|
||||
const tabbableNodes = tabbable(container, config.tabbableOptions);
|
||||
|
||||
// NOTE: if we have tabbable nodes, we must have focusable nodes; focusable nodes
|
||||
// are a superset of tabbable nodes
|
||||
const focusableNodes = focusable(container, config.tabbableOptions);
|
||||
|
||||
return {
|
||||
container,
|
||||
tabbableNodes,
|
||||
focusableNodes,
|
||||
firstTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[0] : null,
|
||||
lastTabbableNode:
|
||||
tabbableNodes.length > 0
|
||||
? tabbableNodes[tabbableNodes.length - 1]
|
||||
: null,
|
||||
|
||||
/**
|
||||
* Finds the __tabbable__ node that follows the given node in the specified direction,
|
||||
* in this container, if any.
|
||||
* @param {HTMLElement} node
|
||||
* @param {boolean} [forward] True if going in forward tab order; false if going
|
||||
* in reverse.
|
||||
* @returns {HTMLElement|undefined} The next tabbable node, if any.
|
||||
*/
|
||||
nextTabbableNode(node, forward = true) {
|
||||
// NOTE: If tabindex is positive (in order to manipulate the tab order separate
|
||||
// from the DOM order), this __will not work__ because the list of focusableNodes,
|
||||
// while it contains tabbable nodes, does not sort its nodes in any order other
|
||||
// than DOM order, because it can't: Where would you place focusable (but not
|
||||
// tabbable) nodes in that order? They have no order, because they aren't tabbale...
|
||||
// Support for positive tabindex is already broken and hard to manage (possibly
|
||||
// not supportable, TBD), so this isn't going to make things worse than they
|
||||
// already are, and at least makes things better for the majority of cases where
|
||||
// tabindex is either 0/unset or negative.
|
||||
// FYI, positive tabindex issue: https://github.com/focus-trap/focus-trap/issues/375
|
||||
const nodeIdx = focusableNodes.findIndex((n) => n === node);
|
||||
if (nodeIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (forward) {
|
||||
return focusableNodes
|
||||
.slice(nodeIdx + 1)
|
||||
.find((n) => isTabbable(n, config.tabbableOptions));
|
||||
}
|
||||
|
||||
return focusableNodes
|
||||
.slice(0, nodeIdx)
|
||||
.reverse()
|
||||
.find((n) => isTabbable(n, config.tabbableOptions));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
state.tabbableGroups = state.containerGroups.filter(
|
||||
(group) => group.tabbableNodes.length > 0
|
||||
);
|
||||
|
||||
// throw if no groups have tabbable nodes and we don't have a fallback focus node either
|
||||
if (
|
||||
state.tabbableGroups.length <= 0 &&
|
||||
!getNodeForOption('fallbackFocus') // returning false not supported for this option
|
||||
) {
|
||||
throw new Error(
|
||||
'Your focus-trap must have at least one container with at least one tabbable node in it at all times'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const tryFocus = function (node) {
|
||||
if (node === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node === doc.activeElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node || !node.focus) {
|
||||
tryFocus(getInitialFocusNode());
|
||||
return;
|
||||
}
|
||||
|
||||
node.focus({ preventScroll: !!config.preventScroll });
|
||||
state.mostRecentlyFocusedNode = node;
|
||||
|
||||
if (isSelectableInput(node)) {
|
||||
node.select();
|
||||
}
|
||||
};
|
||||
|
||||
const getReturnFocusNode = function (previousActiveElement) {
|
||||
const node = getNodeForOption('setReturnFocus', previousActiveElement);
|
||||
return node ? node : node === false ? false : previousActiveElement;
|
||||
};
|
||||
|
||||
// This needs to be done on mousedown and touchstart instead of click
|
||||
// so that it precedes the focus event.
|
||||
const checkPointerDown = function (e) {
|
||||
const target = getActualTarget(e);
|
||||
|
||||
if (findContainerIndex(target, e) >= 0) {
|
||||
// allow the click since it ocurred inside the trap
|
||||
return;
|
||||
}
|
||||
|
||||
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
||||
// immediately deactivate the trap
|
||||
trap.deactivate({
|
||||
// NOTE: by setting `returnFocus: false`, deactivate() will do nothing,
|
||||
// which will result in the outside click setting focus to the node
|
||||
// that was clicked (and if not focusable, to "nothing"); by setting
|
||||
// `returnFocus: true`, we'll attempt to re-focus the node originally-focused
|
||||
// on activation (or the configured `setReturnFocus` node), whether the
|
||||
// outside click was on a focusable node or not
|
||||
returnFocus: config.returnFocusOnDeactivate,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// This is needed for mobile devices.
|
||||
// (If we'll only let `click` events through,
|
||||
// then on mobile they will be blocked anyways if `touchstart` is blocked.)
|
||||
if (valueOrHandler(config.allowOutsideClick, e)) {
|
||||
// allow the click outside the trap to take place
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, prevent the click
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// In case focus escapes the trap for some strange reason, pull it back in.
|
||||
const checkFocusIn = function (e) {
|
||||
const target = getActualTarget(e);
|
||||
const targetContained = findContainerIndex(target, e) >= 0;
|
||||
|
||||
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
||||
if (targetContained || target instanceof Document) {
|
||||
if (targetContained) {
|
||||
state.mostRecentlyFocusedNode = target;
|
||||
}
|
||||
} else {
|
||||
// escaped! pull it back in to where it just left
|
||||
e.stopImmediatePropagation();
|
||||
tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
|
||||
}
|
||||
};
|
||||
|
||||
// Hijack key nav events on the first and last focusable nodes of the trap,
|
||||
// in order to prevent focus from escaping. If it escapes for even a
|
||||
// moment it can end up scrolling the page and causing confusion so we
|
||||
// kind of need to capture the action at the keydown phase.
|
||||
const checkKeyNav = function (event, isBackward = false) {
|
||||
const target = getActualTarget(event);
|
||||
updateTabbableNodes();
|
||||
|
||||
let destinationNode = null;
|
||||
|
||||
if (state.tabbableGroups.length > 0) {
|
||||
// make sure the target is actually contained in a group
|
||||
// NOTE: the target may also be the container itself if it's focusable
|
||||
// with tabIndex='-1' and was given initial focus
|
||||
const containerIndex = findContainerIndex(target, event);
|
||||
const containerGroup =
|
||||
containerIndex >= 0 ? state.containerGroups[containerIndex] : undefined;
|
||||
|
||||
if (containerIndex < 0) {
|
||||
// target not found in any group: quite possible focus has escaped the trap,
|
||||
// so bring it back into...
|
||||
if (isBackward) {
|
||||
// ...the last node in the last group
|
||||
destinationNode =
|
||||
state.tabbableGroups[state.tabbableGroups.length - 1]
|
||||
.lastTabbableNode;
|
||||
} else {
|
||||
// ...the first node in the first group
|
||||
destinationNode = state.tabbableGroups[0].firstTabbableNode;
|
||||
}
|
||||
} else if (isBackward) {
|
||||
// REVERSE
|
||||
|
||||
// is the target the first tabbable node in a group?
|
||||
let startOfGroupIndex = findIndex(
|
||||
state.tabbableGroups,
|
||||
({ firstTabbableNode }) => target === firstTabbableNode
|
||||
);
|
||||
|
||||
if (
|
||||
startOfGroupIndex < 0 &&
|
||||
(containerGroup.container === target ||
|
||||
(isFocusable(target, config.tabbableOptions) &&
|
||||
!isTabbable(target, config.tabbableOptions) &&
|
||||
!containerGroup.nextTabbableNode(target, false)))
|
||||
) {
|
||||
// an exception case where the target is either the container itself, or
|
||||
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
||||
// and user clicked on it or node was programmatically given focus)
|
||||
// and is not followed by any other tabbable node, in which
|
||||
// case, we should handle shift+tab as if focus were on the container's
|
||||
// first tabbable node, and go to the last tabbable node of the LAST group
|
||||
startOfGroupIndex = containerIndex;
|
||||
}
|
||||
|
||||
if (startOfGroupIndex >= 0) {
|
||||
// YES: then shift+tab should go to the last tabbable node in the
|
||||
// previous group (and wrap around to the last tabbable node of
|
||||
// the LAST group if it's the first tabbable node of the FIRST group)
|
||||
const destinationGroupIndex =
|
||||
startOfGroupIndex === 0
|
||||
? state.tabbableGroups.length - 1
|
||||
: startOfGroupIndex - 1;
|
||||
|
||||
const destinationGroup = state.tabbableGroups[destinationGroupIndex];
|
||||
destinationNode = destinationGroup.lastTabbableNode;
|
||||
} else if (!isTabEvent(event)) {
|
||||
// user must have customized the nav keys so we have to move focus manually _within_
|
||||
// the active group: do this based on the order determined by tabbable()
|
||||
destinationNode = containerGroup.nextTabbableNode(target, false);
|
||||
}
|
||||
} else {
|
||||
// FORWARD
|
||||
|
||||
// is the target the last tabbable node in a group?
|
||||
let lastOfGroupIndex = findIndex(
|
||||
state.tabbableGroups,
|
||||
({ lastTabbableNode }) => target === lastTabbableNode
|
||||
);
|
||||
|
||||
if (
|
||||
lastOfGroupIndex < 0 &&
|
||||
(containerGroup.container === target ||
|
||||
(isFocusable(target, config.tabbableOptions) &&
|
||||
!isTabbable(target, config.tabbableOptions) &&
|
||||
!containerGroup.nextTabbableNode(target)))
|
||||
) {
|
||||
// an exception case where the target is the container itself, or
|
||||
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
||||
// and user clicked on it or node was programmatically given focus)
|
||||
// and is not followed by any other tabbable node, in which
|
||||
// case, we should handle tab as if focus were on the container's
|
||||
// last tabbable node, and go to the first tabbable node of the FIRST group
|
||||
lastOfGroupIndex = containerIndex;
|
||||
}
|
||||
|
||||
if (lastOfGroupIndex >= 0) {
|
||||
// YES: then tab should go to the first tabbable node in the next
|
||||
// group (and wrap around to the first tabbable node of the FIRST
|
||||
// group if it's the last tabbable node of the LAST group)
|
||||
const destinationGroupIndex =
|
||||
lastOfGroupIndex === state.tabbableGroups.length - 1
|
||||
? 0
|
||||
: lastOfGroupIndex + 1;
|
||||
|
||||
const destinationGroup = state.tabbableGroups[destinationGroupIndex];
|
||||
destinationNode = destinationGroup.firstTabbableNode;
|
||||
} else if (!isTabEvent(event)) {
|
||||
// user must have customized the nav keys so we have to move focus manually _within_
|
||||
// the active group: do this based on the order determined by tabbable()
|
||||
destinationNode = containerGroup.nextTabbableNode(target);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no groups available
|
||||
// NOTE: the fallbackFocus option does not support returning false to opt-out
|
||||
destinationNode = getNodeForOption('fallbackFocus');
|
||||
}
|
||||
|
||||
if (destinationNode) {
|
||||
if (isTabEvent(event)) {
|
||||
// since tab natively moves focus, we wouldn't have a destination node unless we
|
||||
// were on the edge of a container and had to move to the next/previous edge, in
|
||||
// which case we want to prevent default to keep the browser from moving focus
|
||||
// to where it normally would
|
||||
event.preventDefault();
|
||||
}
|
||||
tryFocus(destinationNode);
|
||||
}
|
||||
// else, let the browser take care of [shift+]tab and move the focus
|
||||
};
|
||||
|
||||
const checkKey = function (event) {
|
||||
if (
|
||||
isEscapeEvent(event) &&
|
||||
valueOrHandler(config.escapeDeactivates, event) !== false
|
||||
) {
|
||||
event.preventDefault();
|
||||
trap.deactivate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.isKeyForward(event) || config.isKeyBackward(event)) {
|
||||
checkKeyNav(event, config.isKeyBackward(event));
|
||||
}
|
||||
};
|
||||
|
||||
const checkClick = function (e) {
|
||||
const target = getActualTarget(e);
|
||||
|
||||
if (findContainerIndex(target, e) >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (valueOrHandler(config.allowOutsideClick, e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
//
|
||||
// EVENT LISTENERS
|
||||
//
|
||||
|
||||
const addListeners = function () {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There can be only one listening focus trap at a time
|
||||
activeFocusTraps.activateTrap(trapStack, trap);
|
||||
|
||||
// Delay ensures that the focused element doesn't capture the event
|
||||
// that caused the focus trap activation.
|
||||
state.delayInitialFocusTimer = config.delayInitialFocus
|
||||
? delay(function () {
|
||||
tryFocus(getInitialFocusNode());
|
||||
})
|
||||
: tryFocus(getInitialFocusNode());
|
||||
|
||||
doc.addEventListener('focusin', checkFocusIn, true);
|
||||
doc.addEventListener('mousedown', checkPointerDown, {
|
||||
capture: true,
|
||||
passive: false,
|
||||
});
|
||||
doc.addEventListener('touchstart', checkPointerDown, {
|
||||
capture: true,
|
||||
passive: false,
|
||||
});
|
||||
doc.addEventListener('click', checkClick, {
|
||||
capture: true,
|
||||
passive: false,
|
||||
});
|
||||
doc.addEventListener('keydown', checkKey, {
|
||||
capture: true,
|
||||
passive: false,
|
||||
});
|
||||
|
||||
return trap;
|
||||
};
|
||||
|
||||
const removeListeners = function () {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.removeEventListener('focusin', checkFocusIn, true);
|
||||
doc.removeEventListener('mousedown', checkPointerDown, true);
|
||||
doc.removeEventListener('touchstart', checkPointerDown, true);
|
||||
doc.removeEventListener('click', checkClick, true);
|
||||
doc.removeEventListener('keydown', checkKey, true);
|
||||
|
||||
return trap;
|
||||
};
|
||||
|
||||
//
|
||||
// MUTATION OBSERVER
|
||||
//
|
||||
|
||||
const checkDomRemoval = function (mutations) {
|
||||
const isFocusedNodeRemoved = mutations.some(function (mutation) {
|
||||
const removedNodes = Array.from(mutation.removedNodes);
|
||||
return removedNodes.some(function (node) {
|
||||
return node === state.mostRecentlyFocusedNode;
|
||||
});
|
||||
});
|
||||
|
||||
// If the currently focused is removed then browsers will move focus to the
|
||||
// <body> element. If this happens, try to move focus back into the trap.
|
||||
if (isFocusedNodeRemoved) {
|
||||
tryFocus(getInitialFocusNode());
|
||||
}
|
||||
};
|
||||
|
||||
// Use MutationObserver - if supported - to detect if focused node is removed
|
||||
// from the DOM.
|
||||
const mutationObserver =
|
||||
typeof window !== 'undefined' && 'MutationObserver' in window
|
||||
? new MutationObserver(checkDomRemoval)
|
||||
: undefined;
|
||||
|
||||
const updateObservedNodes = function () {
|
||||
if (!mutationObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutationObserver.disconnect();
|
||||
if (state.active && !state.paused) {
|
||||
state.containers.map(function (container) {
|
||||
mutationObserver.observe(container, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// TRAP DEFINITION
|
||||
//
|
||||
|
||||
trap = {
|
||||
get active() {
|
||||
return state.active;
|
||||
},
|
||||
|
||||
get paused() {
|
||||
return state.paused;
|
||||
},
|
||||
|
||||
activate(activateOptions) {
|
||||
if (state.active) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const onActivate = getOption(activateOptions, 'onActivate');
|
||||
const onPostActivate = getOption(activateOptions, 'onPostActivate');
|
||||
const checkCanFocusTrap = getOption(activateOptions, 'checkCanFocusTrap');
|
||||
|
||||
if (!checkCanFocusTrap) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
|
||||
state.active = true;
|
||||
state.paused = false;
|
||||
state.nodeFocusedBeforeActivation = doc.activeElement;
|
||||
|
||||
onActivate?.();
|
||||
|
||||
const finishActivation = () => {
|
||||
if (checkCanFocusTrap) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
addListeners();
|
||||
updateObservedNodes();
|
||||
onPostActivate?.();
|
||||
};
|
||||
|
||||
if (checkCanFocusTrap) {
|
||||
checkCanFocusTrap(state.containers.concat()).then(
|
||||
finishActivation,
|
||||
finishActivation
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
finishActivation();
|
||||
return this;
|
||||
},
|
||||
|
||||
deactivate(deactivateOptions) {
|
||||
if (!state.active) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const options = {
|
||||
onDeactivate: config.onDeactivate,
|
||||
onPostDeactivate: config.onPostDeactivate,
|
||||
checkCanReturnFocus: config.checkCanReturnFocus,
|
||||
...deactivateOptions,
|
||||
};
|
||||
|
||||
clearTimeout(state.delayInitialFocusTimer); // noop if undefined
|
||||
state.delayInitialFocusTimer = undefined;
|
||||
|
||||
removeListeners();
|
||||
state.active = false;
|
||||
state.paused = false;
|
||||
updateObservedNodes();
|
||||
|
||||
activeFocusTraps.deactivateTrap(trapStack, trap);
|
||||
|
||||
const onDeactivate = getOption(options, 'onDeactivate');
|
||||
const onPostDeactivate = getOption(options, 'onPostDeactivate');
|
||||
const checkCanReturnFocus = getOption(options, 'checkCanReturnFocus');
|
||||
const returnFocus = getOption(
|
||||
options,
|
||||
'returnFocus',
|
||||
'returnFocusOnDeactivate'
|
||||
);
|
||||
|
||||
onDeactivate?.();
|
||||
|
||||
const finishDeactivation = () => {
|
||||
delay(() => {
|
||||
if (returnFocus) {
|
||||
tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
|
||||
}
|
||||
onPostDeactivate?.();
|
||||
});
|
||||
};
|
||||
|
||||
if (returnFocus && checkCanReturnFocus) {
|
||||
checkCanReturnFocus(
|
||||
getReturnFocusNode(state.nodeFocusedBeforeActivation)
|
||||
).then(finishDeactivation, finishDeactivation);
|
||||
return this;
|
||||
}
|
||||
|
||||
finishDeactivation();
|
||||
return this;
|
||||
},
|
||||
|
||||
pause(pauseOptions) {
|
||||
if (state.paused || !state.active) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const onPause = getOption(pauseOptions, 'onPause');
|
||||
const onPostPause = getOption(pauseOptions, 'onPostPause');
|
||||
|
||||
state.paused = true;
|
||||
onPause?.();
|
||||
|
||||
removeListeners();
|
||||
updateObservedNodes();
|
||||
|
||||
onPostPause?.();
|
||||
return this;
|
||||
},
|
||||
|
||||
unpause(unpauseOptions) {
|
||||
if (!state.paused || !state.active) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const onUnpause = getOption(unpauseOptions, 'onUnpause');
|
||||
const onPostUnpause = getOption(unpauseOptions, 'onPostUnpause');
|
||||
|
||||
state.paused = false;
|
||||
onUnpause?.();
|
||||
|
||||
updateTabbableNodes();
|
||||
addListeners();
|
||||
updateObservedNodes();
|
||||
|
||||
onPostUnpause?.();
|
||||
return this;
|
||||
},
|
||||
|
||||
updateContainerElements(containerElements) {
|
||||
const elementsAsArray = [].concat(containerElements).filter(Boolean);
|
||||
|
||||
state.containers = elementsAsArray.map((element) =>
|
||||
typeof element === 'string' ? doc.querySelector(element) : element
|
||||
);
|
||||
|
||||
if (state.active) {
|
||||
updateTabbableNodes();
|
||||
}
|
||||
|
||||
updateObservedNodes();
|
||||
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
// initialize container elements
|
||||
trap.updateContainerElements(elements);
|
||||
|
||||
return trap;
|
||||
};
|
||||
|
||||
export { createFocusTrap };
|
99
node_modules/focus-trap/package.json
generated
vendored
Normal file
99
node_modules/focus-trap/package.json
generated
vendored
Normal file
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"name": "focus-trap",
|
||||
"version": "7.4.3",
|
||||
"description": "Trap focus within a DOM node.",
|
||||
"main": "dist/focus-trap.js",
|
||||
"module": "dist/focus-trap.esm.js",
|
||||
"types": "index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"package.json",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"SECURITY.md",
|
||||
"LICENSE",
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"demo-bundle": "npm run compile:demo",
|
||||
"format": "prettier --write \"{*,src/**/*,test/**/*,docs/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\"",
|
||||
"format:check": "prettier --check \"{*,src/**/*,test/**/*,docs/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\"",
|
||||
"format:watch": "onchange \"{*,src/**/*,test/**/*,docs/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\" -- prettier --write {{changed}}",
|
||||
"lint": "eslint \"*.js\" \"docs/js/**/*.js\" \"cypress/**/*.js\"",
|
||||
"clean": "rm -rf ./dist",
|
||||
"compile:esm": "cross-env BUILD_ENV=esm BABEL_ENV=esm rollup -c",
|
||||
"compile:cjs": "cross-env BUILD_ENV=cjs BABEL_ENV=es5 rollup -c",
|
||||
"compile:umd": "cross-env BUILD_ENV=umd BABEL_ENV=es5 rollup -c",
|
||||
"compile:demo": "cross-env BUILD_ENV=demo BABEL_ENV=es5 rollup -c",
|
||||
"compile": "npm run compile:esm && npm run compile:cjs && npm run compile:umd",
|
||||
"build": "npm run clean && npm run compile",
|
||||
"start": "npm run compile:demo -- --watch --environment SERVE,RELOAD",
|
||||
"start:cypress": "npm run compile:demo -- --environment SERVE,IS_CYPRESS_ENV:\"$CYPRESS_BROWSER\"",
|
||||
"test:types": "tsc index.d.ts",
|
||||
"test:unit": "echo \"No unit tests to run!\"",
|
||||
"test:e2e": "ELECTRON_ENABLE_LOGGING=1 start-server-and-test start:cypress 9966 'cypress run --browser $CYPRESS_BROWSER --headless'",
|
||||
"test:e2e:chrome": "CYPRESS_BROWSER=chrome npm run test:e2e",
|
||||
"test:e2e:dev": "ELECTRON_ENABLE_LOGGING=1 start-server-and-test start:cypress 9966 'cypress open'",
|
||||
"test": "npm run format:check && npm run lint && npm run test:unit && npm run test:types && npm run test:e2e:chrome",
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm run test && npm run build",
|
||||
"release": "npm run build && changeset publish"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/focus-trap/focus-trap.git"
|
||||
},
|
||||
"keywords": [
|
||||
"focus",
|
||||
"accessibility",
|
||||
"trap",
|
||||
"capture",
|
||||
"keyboard",
|
||||
"modal"
|
||||
],
|
||||
"author": {
|
||||
"name": "David Clark",
|
||||
"url": "http://davidtheclark.com/"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/focus-trap/focus-trap/issues"
|
||||
},
|
||||
"homepage": "https://github.com/focus-trap/focus-trap#readme",
|
||||
"dependencies": {
|
||||
"tabbable": "^6.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.21.5",
|
||||
"@babel/core": "^7.21.8",
|
||||
"@babel/eslint-parser": "^7.21.8",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@changesets/cli": "^2.26.1",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-commonjs": "^25.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||
"@testing-library/cypress": "^9.0.0",
|
||||
"@types/jquery": "^3.5.16",
|
||||
"all-contributors-cli": "^6.25.1",
|
||||
"babel-loader": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^12.12.0",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-cypress": "^2.13.3",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"onchange": "^7.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-inject-process-env": "^1.3.1",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
"rollup-plugin-serve": "^2.0.2",
|
||||
"rollup-plugin-sourcemaps": "^0.6.3",
|
||||
"rollup-plugin-terser": "^7.0.1",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue