first-commit
This commit is contained in:
248
node_modules/tabbable/CHANGELOG.md
generated
vendored
Normal file
248
node_modules/tabbable/CHANGELOG.md
generated
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
# Changelog
|
||||
|
||||
## 6.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 18a093f: Add new `getTabIndex()` API which enables Focus-trap to determine tab indexes in the same way as Tabbable when necessary (see [focus-trap#974](https://github.com/focus-trap/focus-trap/pull/974)).
|
||||
|
||||
## 6.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b39b217: Pin jsdom downstream dependency nwsapi to v2.2.2 while awaiting fix ([#982](https://github.com/focus-trap/tabbable/issues/982))
|
||||
|
||||
## 6.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 97373cc: Fix JSDom not supporting HTMLElement.inert and HTMLElement.contentEditable APIs, and not supporting CSS selector ':not([inert *])' resulting in no nodes found and "focus-trap must have at least one tabbable node..." error in focus-trap.
|
||||
|
||||
## 6.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 1756c90: Add support for new [inert](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/inert#browser_compatibility) attribute ([#292](https://github.com/focus-trap/tabbable/issues/292))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b8c7550: Fix a corner case where a node's root node can be itself, indicating detachment from the DOM, leading to a crash in `isHidden() -> isNodeAttached() -> getRootNode()` if not handled properly ([focus-trap-react #905](https://github.com/focus-trap/focus-trap-react/issues/905))
|
||||
|
||||
## 6.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0aab1e3: Fix crash with tabbable scoped table header elements ([#832](https://github.com/focus-trap/tabbable/issues/832))
|
||||
|
||||
## 6.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 5f40c8e: Revised and clarified official browser support (still as broad and deep as _reasonably_ possible).
|
||||
- 5f40c8e: 🚨 **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.
|
||||
- a09ba0b: 🚨 **Breaking:** Default `displayCheck` 'full' option no longer treats detached nodes as visible. Use the new 'legacy-full' option to restore old (incorrect) behavior only if you must. Ideally, make sure tabbable only runs once all nodes of interest have been attached to the document.
|
||||
|
||||
## 5.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0210a1c: fix: align with browser behaviour when a web component has a negative tabindex
|
||||
|
||||
## 5.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 320bfd1: Updated docs for `displayCheck` configuration.
|
||||
- aa2a699: Fixed an issue with `displayCheck=full` (default setting) determining all nodes were hidden when the container is not attached to the document. In this case, tabbable will revert to a `displayCheck=none` mode, which is the equivalent legacy behavior. Also updated the `displayCheck` option docs to add warnings about this corner case for the `full` and `non-zero-area` modes. `non-zero-area` behaves differently in the corner case. See the docs for more info.
|
||||
|
||||
## 5.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cf1da66: Add warnings and help in documentation about running tabbable under JSDom (e.g. with Jest). JSDom is not technically supported, and 5.3.0 introduced some changes that use DOM APIs that JSDom stubs out, which may cause some JSDom-based tests to fail. Also revamp the API docs a bit to make them clearer, and add missing `getShadowRoot` option to `isTabbable()` and `isFocusable()` (docs only; no code changes necessary).
|
||||
|
||||
## 5.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 685a906: Adds new Shadow DOM support (must be explicitly enabled using the new `getShadowRoot` option).
|
||||
- When enabled, supports open shadows by default, and can support closed shadows if the option is a function that returns the shadow for a given node. See documentation for more information.
|
||||
- Includes all updates from `5.3.0-beta.0` and `5.3.0-beta.1` releases.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b341412: Made "isDisabledFromFieldset" more readable and concise (even marginally faster).
|
||||
- 685a906: Fixed a bug in `getTabIndex`: the tab index of `<audio>`, `<video>` and `<details>` was left to the browser default if explicitly set to a value that couldn't be parsed as integer, leading to inconsistent behavior across browsers. Also slightly modified the function's logic to make it more efficient. Finally added tests to cover the fix.
|
||||
- dd6d0ec: Optimized and extended `displayCheck: "full"` option (now checks for any element having no display boxes) and added test for `display: "contents"` property (this bug was never reported). [(#592)](https://github.com/focus-trap/tabbable/issues/592)
|
||||
- ⚠️ 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.
|
||||
- 193fca2: Fixed bug in `isDisabledFromFieldset`. The function wasn't checking whether the disabled `<fieldset>` containing `node` is the top-most disabled `<fieldset>` ([#596](https://github.com/focus-trap/tabbable/issues/596)).
|
||||
|
||||
## 5.3.0-beta.1
|
||||
|
||||
- Add support for setting `getShadowRoot: true` as an easy way to simply _enable_ shadow DOM support. This is the equivalent of setting `getShadowRoot: () => false`, which means tabbable will find nodes in **open** shadow roots only.
|
||||
|
||||
## 5.3.0-beta.0
|
||||
|
||||
- Includes new Shadow DOM support for open shadows by default
|
||||
- Includes a new `getShadowRoot()` configuration option, enabling support for closed shadows
|
||||
|
||||
## 5.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1d5fcb5: Fixed: Form elements in disabled fieldsets should not be tabbable/focusable (#413)
|
||||
|
||||
## 5.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- bf0a8f0: Exposed an option to select the way that an element is checked as displayed
|
||||
|
||||
## 5.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f9f6d25: Replaces Karma/Mocha/Sinon/Chai in test suite with Jest/Dom Testing Library. Removes README reference to identifying anchor tags with an `xlink:href` attribute as tabbable since that information is incorrect.
|
||||
|
||||
## 5.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c048203: fix crash when radio button name attributes contain CSS selector special characters (#168)
|
||||
|
||||
## 5.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a188c71: use element.matches fallback for IE11 and Webkit5
|
||||
- 0d4cdf8: Update the code to use const/let and function declarations only for the repo; this does NOT affect browser compatibility as the code is still transpiled when published into the `./dist` directory for various targets.
|
||||
|
||||
## 5.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5579825: fixes to details elements
|
||||
- ignore elements nested under a closed details element
|
||||
- ignore any extra summary elements after the first summary element
|
||||
- add details element as tabbable in case it has no direct summary element
|
||||
|
||||
## 5.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d3c6514: Fix UMD build incorrectly using `focusTrap` as output name.
|
||||
- 95563c2: Fix #99: 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.
|
||||
|
||||
## 5.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fb49d23: Fix #96: Transpile non-minified bundles for expected browser targets.
|
||||
|
||||
## 5.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- bd21d91: Add `focusable()` for getting all focusable nodes.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3665d0b: The TypeScript return type of `tabbable` has been fixed: Was `Array<Element>` (an `Element` is technically not focusable), is now `Array<HTMLElement | SVGElement>` (which are both still/also `Element` instances).
|
||||
- 8a25135: Fixed: Tabbable elements in fixed-position (`position: fixed`) containers should now be consistently found in supported browsers.
|
||||
- 9544de2: Replace `prepublishOnly` script with `prepare` script. This has the added benefit of running automatically when installing the package from GitHub (as supported by NPM) where the published `./dist` directory is not automatically included.
|
||||
- 672f4a2: Add `focusable()` type definition.
|
||||
- 2424c0f: Small improvements for improving tree-shakeability of this package. A missing `#__PURE__` annotation has been added to allow dropping one of the top-level calls (if its result stays unused) and removed minification of the file referenced as `package.json#module` to avoid dropping the comments (including existing `#__PURE__` annotations).
|
||||
|
||||
## 5.0.0
|
||||
|
||||
- Changed code formatting to use dangling commas where ES5 supports them.
|
||||
- Fixed a bug where `<audio controls />` and `<video controls />` elements _without `tabindex` attribute specified_ would be deemed **NOT** tabbable in Chrome, but would be in FireFox, because Chrome has `tabIndex` (the DOM Element property) returning -1 (focusable, but not tabbable), while FireFox has `tabIndex` returning 0 (focusable, and tabbable), yet **both** browsers include these elements in the _regular tab order_ (as if `tabIndex` was 0 for both browsers). Now these elements are considered tabbable in Chrome too!
|
||||
- Add any `<summary>` element directly under a `<details>` element as tabbable and focusable.
|
||||
- **BREAKING**: Changes to the `isTabbableRadio()` internal function in order to better support nested radio buttons:
|
||||
- In case a form parent element exists, include only nested radio inputs from that form.
|
||||
- Ignore checked radio elements from forms different from the one the validated node belongs to.
|
||||
- NOTE: This may result in _less_ radio elements being flagged as tabbable depending on context from the "root" node given to `tabbable()`.
|
||||
- **BREAKING**: The exports have changed to be all named, and separate, as follows in order to help make the module more compatible with tree shaking:
|
||||
- `tabbable` -> `import { tabbable } from 'tabbable';
|
||||
- `tabbable.isTabbable` -> `import { isTabbable } from 'tabbable';
|
||||
- `tabbable.isFocusable` -> `import { isFocusable } from 'tabbable';
|
||||
- Also to help with tree shaking, `package.json` now states `sideEffects: false` to mark this module as having no side effects as a result of merely importing it.
|
||||
- Added new UMD build, see `./dist/index.umd.*`.
|
||||
|
||||
## 4.0.0
|
||||
|
||||
- Improve performance by changing the method for detecting whether a DOM node is focusable or not. It's expected that this change will _not_ affect results; but this is a major version bump as a warning for you to check your edge cases before upgrading.
|
||||
|
||||
## 3.1.2
|
||||
|
||||
- Fix reference to root element that caused errors within Shadow DOM.
|
||||
|
||||
## 3.1.1
|
||||
|
||||
- Allow module to be imported by non-browser JavaScript.
|
||||
|
||||
## 3.1.0
|
||||
|
||||
- Add `tabbable.isFocusable` and `tabbable.isTabbable` functions.
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- Add `[contenteditable]` elements.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- Add `<audio>` and `<video>` elements with `controls` attributes.
|
||||
- Only consider radio buttons tabbable if they are the `checked` on in their group, or if none in their group are `checked`.
|
||||
|
||||
## 1.1.3
|
||||
|
||||
- Fix bug causing SVG elements to precede elements they should follow in the tab order in IE.
|
||||
|
||||
## 1.1.2
|
||||
|
||||
- Ensure `querySelectorAll` receives a string argument.
|
||||
|
||||
## 1.1.1
|
||||
|
||||
- Fix crash when you call `tabbable(document)` (passing the `document` element).
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- Add `includeContainer` option.
|
||||
|
||||
## 1.0.8
|
||||
|
||||
- Allows operation against elements that reside within iframes, by inspecting the element to determine its correct parent `document` (rather than relying on the global `document` object).
|
||||
|
||||
## 1.0.7
|
||||
|
||||
- Ensure stable sort of `tabindex`ed elements even in browsers that have an unstable `Array.prototype.sort`.
|
||||
|
||||
## 1.0.6
|
||||
|
||||
- Check `tabindex` attribute (via `getAttribute`), in addition to `node.tabIndex`, to fix handling of SVGs with `tabindex="-1"` in IE.
|
||||
|
||||
## 1.0.5
|
||||
|
||||
- Children of `visibility: hidden` elements that themselves have `visibility: visible` are considered tabbable.
|
||||
|
||||
## 1.0.4
|
||||
|
||||
- Fix IE9 compatibility.
|
||||
|
||||
## 1.0.3
|
||||
|
||||
- Further improvements to caching.
|
||||
|
||||
## 1.0.2
|
||||
|
||||
- Fix overaggressive caching that would prevent `tabbable` from knowing an element's children had changed.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Fix handling of `<a>` elements with `tabindex="0"`.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial release.
|
||||
22
node_modules/tabbable/LICENSE
generated
vendored
Normal file
22
node_modules/tabbable/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 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.
|
||||
|
||||
287
node_modules/tabbable/README.md
generated
vendored
Normal file
287
node_modules/tabbable/README.md
generated
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
# tabbable [](https://github.com/focus-trap/tabbable/actions?query=workflow:CI+branch:master) [](https://codecov.io/gh/focus-trap/tabbable) [](./LICENSE)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
Small utility that returns an array of all\* tabbable DOM nodes within a containing node.
|
||||
|
||||
<small>_\***all** has some necessary caveats, which you'll learn about by reading below._</small>
|
||||
|
||||
The following are considered tabbable:
|
||||
|
||||
- `<button>` elements
|
||||
- `<input>` elements
|
||||
- `<select>` elements
|
||||
- `<textarea>` elements
|
||||
- `<a>` elements with an `href` attribute
|
||||
- `<audio>` and `<video>` elements with `controls` attributes
|
||||
- the first `<summary>` element directly under a `<details>` element
|
||||
- `<details>` element without a `<summary>` element
|
||||
- elements with the `[contenteditable]` attribute
|
||||
- anything with a non-negative `tabindex` attribute
|
||||
|
||||
Any of the above will _not_ be considered tabbable, though, if any of the following are also true about it:
|
||||
|
||||
- has a negative `tabindex` attribute
|
||||
- has a `disabled` attribute
|
||||
- either the node itself _or an ancestor of it_ is hidden via `display: none` (*see ["Display check"](#displaycheck-option) below to modify this behavior)
|
||||
- has `visibility: hidden` style
|
||||
- is nested under a closed `<details>` element (with the exception of the first `<summary>` element)
|
||||
- is an `<input type="radio">` element and a different radio in its group is `checked`
|
||||
- is a form field (button, input, select, textarea) inside a disabled `<fieldset>`
|
||||
- is [inert](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/inert) or in an inert container
|
||||
- ❗️ Only supported in [newer browsers](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/inert#browser_compatibility) that support this new attribute)
|
||||
- ⚠️ Notably __not (yet) supported__ on Firefox and Safari (Feb 2023)
|
||||
|
||||
**If you think a node should be included in your array of tabbables _but it's not_, all you need to do is add `tabindex="0"` to deliberately include it.** (Or if it is in your array but you don't want it, you can add `tabindex="-1"` to deliberately exclude it.) This will also result in more consistent cross-browser behavior. For information about why your special node might _not_ be included, see ["More details"](#more-details), below.
|
||||
|
||||
## Goals
|
||||
|
||||
- Accurate (or, as accurate as possible & reasonable)
|
||||
- No dependencies
|
||||
- Small
|
||||
- Fast
|
||||
|
||||
## 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.
|
||||
|
||||
Tabbable is not officially tested on any mobile browsers or devices.
|
||||
|
||||
> ⚠️ 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.
|
||||
|
||||
> 💬 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.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
npm install tabbable
|
||||
```
|
||||
|
||||
> 💬 Some very old browsers may need a [polyfill](https://www.npmjs.com/package/css.escape) for the [CSS.escape](https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape) API for tabbable to work properly with radio buttons that have `name` attributes containing special characters.
|
||||
|
||||
## API
|
||||
|
||||
### tabbable
|
||||
|
||||
```js
|
||||
import { tabbable } from 'tabbable';
|
||||
|
||||
tabbable(container, [options]);
|
||||
```
|
||||
|
||||
- `container: Node` (**Required**)
|
||||
- `options`:
|
||||
- All the [common options](#common-options).
|
||||
- `includeContainer: boolean` (default: false)
|
||||
- If set to `true`, `container` will be included in the returned tabbable node array, if `container` is tabbable.
|
||||
- Note that whether this option is true or false, if the `container` is [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inert), none of its children (deep) will be considered tabbable.
|
||||
|
||||
Returns an array of ordered tabbable nodes (i.e. in tab order) within the `container`.
|
||||
|
||||
Summary of ordering principles:
|
||||
|
||||
- First include any nodes with positive `tabindex` attributes (1 or higher), ordered by ascending `tabindex` and source order.
|
||||
- Then include any nodes with a zero `tabindex` and any element that by default receives focus (listed above) and does not have a positive `tabindex` set, in source order.
|
||||
|
||||
### isTabbable
|
||||
|
||||
```js
|
||||
import { isTabbable } from 'tabbable';
|
||||
|
||||
isTabbable(node, [options]);
|
||||
```
|
||||
|
||||
- `node: Node` (**Required**)
|
||||
- `options`:
|
||||
- All the [common options](#common-options).
|
||||
|
||||
Returns a boolean indicating whether the provided node is considered tabbable.
|
||||
|
||||
> 💬 If the node has an [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inert) ancestor, it will not be tabbable.
|
||||
|
||||
### focusable
|
||||
|
||||
```js
|
||||
import { focusable } from 'tabbable';
|
||||
|
||||
focusable(container, [options]);
|
||||
```
|
||||
|
||||
- `container: Node`: **Required**
|
||||
- `options`:
|
||||
- All the [common options](#common-options).
|
||||
- `includeContainer: boolean` (default: false)
|
||||
- If set to `true`, `container` will be included in the returned focusable node array, if `container` is focusable.
|
||||
- Note that whether this option is true or false, if the `container` is [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inert), none of its children (deep) will be considered focusable.
|
||||
|
||||
Returns an array of focusable nodes within the `container`, in DOM order. This will not match the order in which `tabbable()` returns nodes.
|
||||
|
||||
### isFocusable
|
||||
|
||||
```js
|
||||
import { isFocusable } from 'tabbable';
|
||||
|
||||
isFocusable(node, [options]);
|
||||
```
|
||||
|
||||
- `node: Node` (**Required**)
|
||||
- `options`:
|
||||
- All the [common options](#common-options).
|
||||
|
||||
Returns a boolean indicating whether the provided node is considered _focusable_.
|
||||
|
||||
> 💬 All tabbable elements are focusable, but not all focusable elements are tabbable. For example, elements with `tabindex="-1"` are focusable but not tabbable. Also note that if the node has an[inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inert) ancestor, it will not be focusable.
|
||||
|
||||
### getTabIndex
|
||||
|
||||
```js
|
||||
import { getTabIndex } from 'tabbable';
|
||||
|
||||
getTabIndex(node);
|
||||
```
|
||||
|
||||
- `node: Element` (**Required**)
|
||||
|
||||
Returns a negative, 0, or positive number that expresses the node's tab index in the DOM, with exceptions made where there are browser inconsistencies related to `<audio>`, `<video>`, `<details>`, and elements with the `contenteditable="true"` attribute.
|
||||
|
||||
The specific exceptions may change over time. See the implementation for specific behavior.
|
||||
|
||||
## Common Options
|
||||
|
||||
These options apply to all APIs.
|
||||
|
||||
### displayCheck option
|
||||
|
||||
Type: `full` | `legacy-full` | `non-zero-area` | `none` . Default: `full`.
|
||||
|
||||
Configures how to check if an element is displayed.
|
||||
|
||||
To reliably check if an element is tabbable/focusable, Tabbable defaults to the most reliable option to keep consistent with browser behavior, however this comes at a cost since every node needs to be validated as displayed using Web APIs that cause layout reflow.
|
||||
|
||||
For this reason Tabbable offers the ability of an alternative way to check if an element is displayed (or completely opt out of the check).
|
||||
|
||||
The `displayCheck` configuration accepts the following options:
|
||||
|
||||
- `full`: (default) Most reliably resembling browser behavior, this option checks that an element is displayed, which requires it to be attached to the DOM, and for all of his ancestors to be displayed (notice this doesn't exclude `visibility: hidden` or elements with zero size). This option will cause layout reflow, however. If that is a concern, consider the `none` option.
|
||||
- ⚠️ If the container given to `tabbable()` or `focusable()`, or the node given to `isTabbable()` or `isFocusable()`, is not attached to the window's main `document`, the node will be considered hidden and neither tabbable nor focusable. This behavior is new as of `v6.0.0`.
|
||||
- If your code relies on the legacy behavior where detached nodes were considered visible, and you are unable to fix your code to use tabbable once the node is attached, use the `legacy-full` option.
|
||||
- `legacy-full`: Same as `full` but restores the __legacy behavior__ of treating detached nodes as visible. This means that if a node is detached, it's then treated as though the display check was set to `none` (see below for details).
|
||||
- ❗️ Since detached nodes are not treated as tabbable/focusable by browsers, using this option is __not recommended__ as it knowingly diverges from browser behavior.
|
||||
- ⚠️ This option may be removed in the future. Tabbable will not maintain it at the expense of new features or if having it makes the code disproportionately more complex. It only exists to make the upgrade path to the correct behavior (i.e. the `full` option) as long and smooth as reasonably possible.
|
||||
- The APIs used to determine a node's display are not supported unless its attached (i.e. the browser does not calculate its display unless it is attached). This has effectively been tabbable's behavior for a _very_ long time (up until the `v6.0.0` release), and you may never have encountered an issue if the nodes with which you used tabbable were always displayed anyway (i.e. the `none` mode assumption was coincidentally correct).
|
||||
- You may encounter the above situation if, for example, you render to a node via React, and this node is [not attached](https://github.com/facebook/react/issues/9117#issuecomment-284228870) to the document (or perhaps, due to timing, it is not _yet_ attached at the time you use tabbable's APIs on it).
|
||||
- `non-zero-area`: This option checks display under the assumption that elements that are not displayed have zero area (width AND height equals zero). While not keeping true to browser behavior, this option may enhance accessibility, as zero-size elements with focusable content are considered a strong accessibility anti-pattern.
|
||||
- Like the `full` option, this option also causes layout reflow, and should have basically the same performance. Consider the `none` option if reflow is a concern.
|
||||
- ⚠️ As with the `full` option, there is a nuance in behavior depending on whether tabbable APIs are executed on attached vs detached nodes using this mode: Attached nodes that are actually displayed will be deemed visible. Detached nodes, _even though displayed_ will always be deemed __hidden__ because detached nodes always have a zero area as the browser does not calculate is dimensions.
|
||||
- `none`: This completely opts out of the display check. **This option is not recommended**, as it might return elements that are not displayed, and as such not tabbable/focusable and can break accessibility. Make sure you know which elements in your DOM are not displayed and can filter them out yourself before using this option.
|
||||
|
||||
> ⚠️ __Testing in JSDom__ (e.g. with Jest): See notes about [testing in JSDom](#testing-in-jsdom).
|
||||
|
||||
### getShadowRoot option
|
||||
|
||||
By default, tabbable overlooks (i.e. does not consider) __all__ elements contained in shadow DOMs (whether open or closed). This has been the behavior since the beginning.
|
||||
|
||||
Setting this option to a _truthy_ value enables Shadow DOM support, which means tabbable will consider elements _inside_ web components as candidates, both open (automatically) and closed (provided this function returns the shadow root).
|
||||
|
||||
Type: `boolean | (node: FocusableElement) => ShadowRoot | boolean | undefined`
|
||||
|
||||
- `boolean`:
|
||||
- `true` simply enables shadow DOM support for any __open__ shadow roots, but never presumes there is an undisclosed shadow. This is the equivalent of setting `getShadowRoot: () => false`
|
||||
- `false` (default) disables shadow DOM support in so far as calculated tab order and closed shadow roots are concerned. If a child of a shadow (open or closed) is given to `isTabbable()` or `isFocusable()`, the shadow DOM is still considered for visibility and display checks.
|
||||
- `function`:
|
||||
- `node` will be a descendent of the `container` given to `tabbable()`, `isTabbable()`, `focusable()`, or `isFocusable()`.
|
||||
- Returns: The node's `ShadowRoot` if available, `true` indicating a `ShadowRoot` is attached but not available (i.e. "undisclosed"), or a _falsy_ value indicating there is no shadow attached to the node.
|
||||
|
||||
> If set to a function, and if it returns `true`, Tabbable assumes a closed `ShadowRoot` is attached and will treat the node as a scope, iterating its children for additional tabbable/focusable candidates as though it was looking inside the shadow, but not. This will get tabbing order _closer_ to -- but not necessarily the same as -- browser order.
|
||||
>
|
||||
> Returning `true` from a function will also inform how the node's visibility check is done, causing tabbable to use the __non-zero-area__ [Display Check](#displaycheck-option) when determining if it's visible, and so tabbable/focusable.
|
||||
|
||||
## More details
|
||||
|
||||
- **Tabbable tries to identify elements that are reliably tabbable across (not dead) browsers.** Browsers are inconsistent in their behavior, though — especially for edge-case elements like `<object>` and `<iframe>` — so this means _some_ elements that you _can_ tab to in _some_ browsers will be left out of the results. (To learn more about this inconsistency, see this [amazing table](https://allyjs.io/data-tables/focusable.html)). To provide better consistency across browsers and ensure the elements you _want_ in your tabbables list show up there, **try adding `tabindex="0"` to edge-case elements that Tabbable ignores**.
|
||||
- (Exemplifying the above ^^:) **The tabbability of `<iframe>`, `<embed>`, `<object>`, `<summary>`, and `<svg>` nodes is [inconsistent across browsers](https://allyjs.io/data-tables/focusable.html)**, so if you need an accurate read on one of these elements you should try giving it a `tabindex`. (You'll also need to pay attention to the `focusable` attribute on SVGs in Edge.) But you also might _not_ be able to get an accurate read — so you should avoid relying on it.
|
||||
- **Radio groups have some edge cases, which you can avoid by always having a `checked` one in each group** (and that is what you should usually do anyway). If there is no `checked` radio in the radio group, _all_ of the radios will be considered tabbable. (Some browsers do this, otherwise don't — there's not consistency.)
|
||||
- If you're thinking, "Why not just use the right `querySelectorAll`?", you _may_ be on to something ... but, as with most "just" statements, you're probably not. For example, a simple `querySelectorAll` approach will not figure out whether an element is _hidden_, and therefore not actually tabbable. (That said, if you do think Tabbable can be simplified or otherwise improved, I'd love to hear your idea.)
|
||||
- jQuery UI's `:tabbable` selector ignores elements with height and width of `0`. I'm not sure why — because I've found that I can still tab to those elements. So I kept them in. Only elements hidden with `display: none` or `visibility: hidden` are left out. See ["Display check"](#displaycheck-option) below for other options.
|
||||
- Although Tabbable tries to deal with positive tabindexes, **you should not use positive tabindexes**. Accessibility experts seem to be in (rare) unanimous and clear consent about this: rely on the order of elements in the document.
|
||||
- Safari on Mac OS X does not Tab to `<a>` elements by default: you have to change a setting to get the standard behavior. Tabbable does not know whether you've changed that setting or not, so it will include `<a>` elements in its list.
|
||||
|
||||
## 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.
|
||||
|
||||
Tabbable uses some DOM APIs such as [Element.getClientRects()](https://developer.mozilla.org/en-US/docs/Web/API/Element/getClientRects) in order to determine node visibility, which helps in deciding whether a node is tabbable, focusable, or neither.
|
||||
|
||||
When using test engines such as Jest that use [JSDom](https://github.com/jsdom/jsdom) under the hood in order to run tests in Node.js (as opposed to using an automated browser testing tool like Cypress, Playwright, or Nightwatch where a full DOM is available), it is __highly recommended__ (if not _essential_) to set the [displayCheck](#displaycheck-option) option to `none` when calling any of the APIs in this library that accept it.
|
||||
|
||||
Using any other `displayCheck` setting will likely lead to failed tests due to nodes expected to be tabbable/focusable being determined to be the opposite because JSDom doesn't fully support some of the DOM APIs being used (even old ones that have been around for a long time).
|
||||
|
||||
You can globally overwrite the `diplayCheck` property by including this file in your `__mocks__` folder:
|
||||
|
||||
```jsx
|
||||
// __mocks__/tabbable.js
|
||||
|
||||
const lib = jest.requireActual('tabbable');
|
||||
|
||||
const tabbable = {
|
||||
...lib,
|
||||
tabbable: (node, options) => lib.tabbable(node, { ...options, displayCheck: 'none' }),
|
||||
focusable: (node, options) => lib.focusable(node, { ...options, displayCheck: 'none' }),
|
||||
isFocusable: (node, options) => lib.isFocusable(node, { ...options, displayCheck: 'none' }),
|
||||
isTabbable: (node, options) => lib.isTabbable(node, { ...options, displayCheck: 'none' }),
|
||||
};
|
||||
|
||||
module.exports = tabbable;
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Feedback and contributions more than welcome!
|
||||
|
||||
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/tidychips"><img src="https://avatars2.githubusercontent.com/u/11446636?v=4?s=100" width="100px;" alt="Bryan Murphy"/><br /><sub><b>Bryan Murphy</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/issues?q=author%3Atidychips" title="Bug reports">🐛</a> <a href="https://github.com/focus-trap/tabbable/commits?author=tidychips" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/craigkovatch"><img src="https://avatars.githubusercontent.com/u/10970257?v=4?s=100" width="100px;" alt="Craig Kovatch"/><br /><sub><b>Craig Kovatch</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/issues?q=author%3Acraigkovatch" title="Bug reports">🐛</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/tabbable/issues?q=author%3ADaviDevMod" title="Bug reports">🐛</a> <a href="https://github.com/focus-trap/tabbable/commits?author=DaviDevMod" title="Code">💻</a> <a href="https://github.com/focus-trap/tabbable/commits?author=DaviDevMod" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/tabbable/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/tabbable/commits?author=davidtheclark" title="Code">💻</a> <a href="https://github.com/focus-trap/tabbable/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/tabbable/commits?author=davidtheclark" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/tabbable/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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/thumbsupep"><img src="https://avatars.githubusercontent.com/u/5598732?v=4?s=100" width="100px;" alt="Erica Pramer"/><br /><sub><b>Erica Pramer</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/commits?author=thumbsupep" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/idoros"><img src="https://avatars1.githubusercontent.com/u/574751?v=4?s=100" width="100px;" alt="Ido Rosenthal"/><br /><sub><b>Ido Rosenthal</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/issues?q=author%3Aidoros" title="Bug reports">🐛</a> <a href="https://github.com/focus-trap/tabbable/commits?author=idoros" title="Code">💻</a> <a href="https://github.com/focus-trap/tabbable/pulls?q=is%3Apr+reviewed-by%3Aidoros" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/focus-trap/tabbable/commits?author=idoros" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.khamilton.co.uk"><img src="https://avatars1.githubusercontent.com/u/4013283?v=4?s=100" width="100px;" alt="Kristian Hamilton"/><br /><sub><b>Kristian Hamilton</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/issues?q=author%3Akhamiltonuk" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/les-lim"><img src="https://avatars.githubusercontent.com/u/7660876?v=4?s=100" width="100px;" alt="Les Lim"/><br /><sub><b>Les Lim</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/issues?q=author%3Ales-lim" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Andarist"><img src="https://avatars2.githubusercontent.com/u/9800850?v=4?s=100" width="100px;" alt="Mateusz Burzyński"/><br /><sub><b>Mateusz Burzyński</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/commits?author=Andarist" title="Code">💻</a> <a href="https://github.com/focus-trap/tabbable/issues?q=author%3AAndarist" title="Bug reports">🐛</a> <a href="https://github.com/focus-trap/tabbable/commits?author=Andarist" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rvsia"><img src="https://avatars.githubusercontent.com/u/32869456?v=4?s=100" width="100px;" alt="Richard Všianský"/><br /><sub><b>Richard Všianský</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/commits?author=rvsia" title="Documentation">📖</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/tabbable/commits?author=stefcameron" title="Code">💻</a> <a href="https://github.com/focus-trap/tabbable/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/tabbable/commits?author=stefcameron" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/tabbable/commits?author=stefcameron" title="Documentation">📖</a> <a href="#maintenance-stefcameron" title="Maintenance">🚧</a></td>
|
||||
<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/tabbable/commits?author=thawkin3" title="Tests">⚠️</a> <a href="#infra-thawkin3" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/focus-trap/tabbable/commits?author=thawkin3" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BFrost"><img src="https://avatars.githubusercontent.com/u/3368761?v=4?s=100" width="100px;" alt="bfrost"/><br /><sub><b>bfrost</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/issues?q=author%3ABFrost" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pebble2050"><img src="https://avatars1.githubusercontent.com/u/47210889?v=4?s=100" width="100px;" alt="pebble2050"/><br /><sub><b>pebble2050</b></sub></a><br /><a href="https://github.com/focus-trap/tabbable/issues?q=author%3Apebble2050" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
37
node_modules/tabbable/SECURITY.md
generated
vendored
Normal file
37
node_modules/tabbable/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__
|
||||
|
||||
If we eventually publish 6.0.0 and a security issue is found in 5.1.3, and it's still in 6.0.0, then we will fix it in 6.0.1 or 6.1.0 (or possibly 7.0.0 if it requires breaking backward compatibility for some reason -- this should be rare), but we will not also publish 5.1.4 or 5.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 5.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 6.0.0-alpha.1, for example. After gathering some feedback, we may publishing additional pre-release versions, until we would finally officially publish as 6.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)
|
||||
571
node_modules/tabbable/dist/index.esm.js
generated
vendored
Normal file
571
node_modules/tabbable/dist/index.esm.js
generated
vendored
Normal file
@@ -0,0 +1,571 @@
|
||||
/*!
|
||||
* tabbable 6.2.0
|
||||
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
|
||||
*/
|
||||
// NOTE: separate `:not()` selectors has broader browser support than the newer
|
||||
// `:not([inert], [inert] *)` (Feb 2023)
|
||||
// CAREFUL: JSDom does not support `:not([inert] *)` as a selector; using it causes
|
||||
// the entire query to fail, resulting in no nodes found, which will break a lot
|
||||
// of things... so we have to rely on JS to identify nodes inside an inert container
|
||||
var candidateSelectors = ['input:not([inert])', 'select:not([inert])', 'textarea:not([inert])', 'a[href]:not([inert])', 'button:not([inert])', '[tabindex]:not(slot):not([inert])', 'audio[controls]:not([inert])', 'video[controls]:not([inert])', '[contenteditable]:not([contenteditable="false"]):not([inert])', 'details>summary:first-of-type:not([inert])', 'details:not([inert])'];
|
||||
var candidateSelector = /* #__PURE__ */candidateSelectors.join(',');
|
||||
var NoElement = typeof Element === 'undefined';
|
||||
var matches = NoElement ? function () {} : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
||||
var getRootNode = !NoElement && Element.prototype.getRootNode ? function (element) {
|
||||
var _element$getRootNode;
|
||||
return element === null || element === void 0 ? void 0 : (_element$getRootNode = element.getRootNode) === null || _element$getRootNode === void 0 ? void 0 : _element$getRootNode.call(element);
|
||||
} : function (element) {
|
||||
return element === null || element === void 0 ? void 0 : element.ownerDocument;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a node is inert or in an inert ancestor.
|
||||
* @param {Element} [node]
|
||||
* @param {boolean} [lookUp] If true and `node` is not inert, looks up at ancestors to
|
||||
* see if any of them are inert. If false, only `node` itself is considered.
|
||||
* @returns {boolean} True if inert itself or by way of being in an inert ancestor.
|
||||
* False if `node` is falsy.
|
||||
*/
|
||||
var isInert = function isInert(node, lookUp) {
|
||||
var _node$getAttribute;
|
||||
if (lookUp === void 0) {
|
||||
lookUp = true;
|
||||
}
|
||||
// CAREFUL: JSDom does not support inert at all, so we can't use the `HTMLElement.inert`
|
||||
// JS API property; we have to check the attribute, which can either be empty or 'true';
|
||||
// if it's `null` (not specified) or 'false', it's an active element
|
||||
var inertAtt = node === null || node === void 0 ? void 0 : (_node$getAttribute = node.getAttribute) === null || _node$getAttribute === void 0 ? void 0 : _node$getAttribute.call(node, 'inert');
|
||||
var inert = inertAtt === '' || inertAtt === 'true';
|
||||
|
||||
// NOTE: this could also be handled with `node.matches('[inert], :is([inert] *)')`
|
||||
// if it weren't for `matches()` not being a function on shadow roots; the following
|
||||
// code works for any kind of node
|
||||
// CAREFUL: JSDom does not appear to support certain selectors like `:not([inert] *)`
|
||||
// so it likely would not support `:is([inert] *)` either...
|
||||
var result = inert || lookUp && node && isInert(node.parentNode); // recursive
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a node's content is editable.
|
||||
* @param {Element} [node]
|
||||
* @returns True if it's content-editable; false if it's not or `node` is falsy.
|
||||
*/
|
||||
var isContentEditable = function isContentEditable(node) {
|
||||
var _node$getAttribute2;
|
||||
// CAREFUL: JSDom does not support the `HTMLElement.isContentEditable` API so we have
|
||||
// to use the attribute directly to check for this, which can either be empty or 'true';
|
||||
// if it's `null` (not specified) or 'false', it's a non-editable element
|
||||
var attValue = node === null || node === void 0 ? void 0 : (_node$getAttribute2 = node.getAttribute) === null || _node$getAttribute2 === void 0 ? void 0 : _node$getAttribute2.call(node, 'contenteditable');
|
||||
return attValue === '' || attValue === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Element} el container to check in
|
||||
* @param {boolean} includeContainer add container to check
|
||||
* @param {(node: Element) => boolean} filter filter candidates
|
||||
* @returns {Element[]}
|
||||
*/
|
||||
var getCandidates = function getCandidates(el, includeContainer, filter) {
|
||||
// even if `includeContainer=false`, we still have to check it for inertness because
|
||||
// if it's inert, all its children are inert
|
||||
if (isInert(el)) {
|
||||
return [];
|
||||
}
|
||||
var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector));
|
||||
if (includeContainer && matches.call(el, candidateSelector)) {
|
||||
candidates.unshift(el);
|
||||
}
|
||||
candidates = candidates.filter(filter);
|
||||
return candidates;
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback GetShadowRoot
|
||||
* @param {Element} element to check for shadow root
|
||||
* @returns {ShadowRoot|boolean} ShadowRoot if available or boolean indicating if a shadowRoot is attached but not available.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback ShadowRootFilter
|
||||
* @param {Element} shadowHostNode the element which contains shadow content
|
||||
* @returns {boolean} true if a shadow root could potentially contain valid candidates.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CandidateScope
|
||||
* @property {Element} scopeParent contains inner candidates
|
||||
* @property {Element[]} candidates list of candidates found in the scope parent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IterativeOptions
|
||||
* @property {GetShadowRoot|boolean} getShadowRoot true if shadow support is enabled; falsy if not;
|
||||
* if a function, implies shadow support is enabled and either returns the shadow root of an element
|
||||
* or a boolean stating if it has an undisclosed shadow root
|
||||
* @property {(node: Element) => boolean} filter filter candidates
|
||||
* @property {boolean} flatten if true then result will flatten any CandidateScope into the returned list
|
||||
* @property {ShadowRootFilter} shadowRootFilter filter shadow roots;
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Element[]} elements list of element containers to match candidates from
|
||||
* @param {boolean} includeContainer add container list to check
|
||||
* @param {IterativeOptions} options
|
||||
* @returns {Array.<Element|CandidateScope>}
|
||||
*/
|
||||
var getCandidatesIteratively = function getCandidatesIteratively(elements, includeContainer, options) {
|
||||
var candidates = [];
|
||||
var elementsToCheck = Array.from(elements);
|
||||
while (elementsToCheck.length) {
|
||||
var element = elementsToCheck.shift();
|
||||
if (isInert(element, false)) {
|
||||
// no need to look up since we're drilling down
|
||||
// anything inside this container will also be inert
|
||||
continue;
|
||||
}
|
||||
if (element.tagName === 'SLOT') {
|
||||
// add shadow dom slot scope (slot itself cannot be focusable)
|
||||
var assigned = element.assignedElements();
|
||||
var content = assigned.length ? assigned : element.children;
|
||||
var nestedCandidates = getCandidatesIteratively(content, true, options);
|
||||
if (options.flatten) {
|
||||
candidates.push.apply(candidates, nestedCandidates);
|
||||
} else {
|
||||
candidates.push({
|
||||
scopeParent: element,
|
||||
candidates: nestedCandidates
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// check candidate element
|
||||
var validCandidate = matches.call(element, candidateSelector);
|
||||
if (validCandidate && options.filter(element) && (includeContainer || !elements.includes(element))) {
|
||||
candidates.push(element);
|
||||
}
|
||||
|
||||
// iterate over shadow content if possible
|
||||
var shadowRoot = element.shadowRoot ||
|
||||
// check for an undisclosed shadow
|
||||
typeof options.getShadowRoot === 'function' && options.getShadowRoot(element);
|
||||
|
||||
// no inert look up because we're already drilling down and checking for inertness
|
||||
// on the way down, so all containers to this root node should have already been
|
||||
// vetted as non-inert
|
||||
var validShadowRoot = !isInert(shadowRoot, false) && (!options.shadowRootFilter || options.shadowRootFilter(element));
|
||||
if (shadowRoot && validShadowRoot) {
|
||||
// add shadow dom scope IIF a shadow root node was given; otherwise, an undisclosed
|
||||
// shadow exists, so look at light dom children as fallback BUT create a scope for any
|
||||
// child candidates found because they're likely slotted elements (elements that are
|
||||
// children of the web component element (which has the shadow), in the light dom, but
|
||||
// slotted somewhere _inside_ the undisclosed shadow) -- the scope is created below,
|
||||
// _after_ we return from this recursive call
|
||||
var _nestedCandidates = getCandidatesIteratively(shadowRoot === true ? element.children : shadowRoot.children, true, options);
|
||||
if (options.flatten) {
|
||||
candidates.push.apply(candidates, _nestedCandidates);
|
||||
} else {
|
||||
candidates.push({
|
||||
scopeParent: element,
|
||||
candidates: _nestedCandidates
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// there's not shadow so just dig into the element's (light dom) children
|
||||
// __without__ giving the element special scope treatment
|
||||
elementsToCheck.unshift.apply(elementsToCheck, element.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Determines if the node has an explicitly specified `tabindex` attribute.
|
||||
* @param {HTMLElement} node
|
||||
* @returns {boolean} True if so; false if not.
|
||||
*/
|
||||
var hasTabIndex = function hasTabIndex(node) {
|
||||
return !isNaN(parseInt(node.getAttribute('tabindex'), 10));
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the tab index of a given node.
|
||||
* @param {HTMLElement} node
|
||||
* @returns {number} Tab order (negative, 0, or positive number).
|
||||
* @throws {Error} If `node` is falsy.
|
||||
*/
|
||||
var getTabIndex = function getTabIndex(node) {
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (node.tabIndex < 0) {
|
||||
// in Chrome, <details/>, <audio controls/> and <video controls/> elements get a default
|
||||
// `tabIndex` of -1 when the 'tabindex' attribute isn't specified in the DOM,
|
||||
// yet they are still part of the regular tab order; in FF, they get a default
|
||||
// `tabIndex` of 0; since Chrome still puts those elements in the regular tab
|
||||
// order, consider their tab index to be 0.
|
||||
// Also browsers do not return `tabIndex` correctly for contentEditable nodes;
|
||||
// so if they don't have a tabindex attribute specifically set, assume it's 0.
|
||||
if ((/^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) || isContentEditable(node)) && !hasTabIndex(node)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return node.tabIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the tab index of a given node __for sort order purposes__.
|
||||
* @param {HTMLElement} node
|
||||
* @param {boolean} [isScope] True for a custom element with shadow root or slot that, by default,
|
||||
* has tabIndex -1, but needs to be sorted by document order in order for its content to be
|
||||
* inserted into the correct sort position.
|
||||
* @returns {number} Tab order (negative, 0, or positive number).
|
||||
*/
|
||||
var getSortOrderTabIndex = function getSortOrderTabIndex(node, isScope) {
|
||||
var tabIndex = getTabIndex(node);
|
||||
if (tabIndex < 0 && isScope && !hasTabIndex(node)) {
|
||||
return 0;
|
||||
}
|
||||
return tabIndex;
|
||||
};
|
||||
var sortOrderedTabbables = function sortOrderedTabbables(a, b) {
|
||||
return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
|
||||
};
|
||||
var isInput = function isInput(node) {
|
||||
return node.tagName === 'INPUT';
|
||||
};
|
||||
var isHiddenInput = function isHiddenInput(node) {
|
||||
return isInput(node) && node.type === 'hidden';
|
||||
};
|
||||
var isDetailsWithSummary = function isDetailsWithSummary(node) {
|
||||
var r = node.tagName === 'DETAILS' && Array.prototype.slice.apply(node.children).some(function (child) {
|
||||
return child.tagName === 'SUMMARY';
|
||||
});
|
||||
return r;
|
||||
};
|
||||
var getCheckedRadio = function getCheckedRadio(nodes, form) {
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].checked && nodes[i].form === form) {
|
||||
return nodes[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
var isTabbableRadio = function isTabbableRadio(node) {
|
||||
if (!node.name) {
|
||||
return true;
|
||||
}
|
||||
var radioScope = node.form || getRootNode(node);
|
||||
var queryRadios = function queryRadios(name) {
|
||||
return radioScope.querySelectorAll('input[type="radio"][name="' + name + '"]');
|
||||
};
|
||||
var radioSet;
|
||||
if (typeof window !== 'undefined' && typeof window.CSS !== 'undefined' && typeof window.CSS.escape === 'function') {
|
||||
radioSet = queryRadios(window.CSS.escape(node.name));
|
||||
} else {
|
||||
try {
|
||||
radioSet = queryRadios(node.name);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var checked = getCheckedRadio(radioSet, node.form);
|
||||
return !checked || checked === node;
|
||||
};
|
||||
var isRadio = function isRadio(node) {
|
||||
return isInput(node) && node.type === 'radio';
|
||||
};
|
||||
var isNonTabbableRadio = function isNonTabbableRadio(node) {
|
||||
return isRadio(node) && !isTabbableRadio(node);
|
||||
};
|
||||
|
||||
// determines if a node is ultimately attached to the window's document
|
||||
var isNodeAttached = function isNodeAttached(node) {
|
||||
var _nodeRoot;
|
||||
// The root node is the shadow root if the node is in a shadow DOM; some document otherwise
|
||||
// (but NOT _the_ document; see second 'If' comment below for more).
|
||||
// If rootNode is shadow root, it'll have a host, which is the element to which the shadow
|
||||
// is attached, and the one we need to check if it's in the document or not (because the
|
||||
// shadow, and all nodes it contains, is never considered in the document since shadows
|
||||
// behave like self-contained DOMs; but if the shadow's HOST, which is part of the document,
|
||||
// is hidden, or is not in the document itself but is detached, it will affect the shadow's
|
||||
// visibility, including all the nodes it contains). The host could be any normal node,
|
||||
// or a custom element (i.e. web component). Either way, that's the one that is considered
|
||||
// part of the document, not the shadow root, nor any of its children (i.e. the node being
|
||||
// tested).
|
||||
// To further complicate things, we have to look all the way up until we find a shadow HOST
|
||||
// that is attached (or find none) because the node might be in nested shadows...
|
||||
// If rootNode is not a shadow root, it won't have a host, and so rootNode should be the
|
||||
// document (per the docs) and while it's a Document-type object, that document does not
|
||||
// appear to be the same as the node's `ownerDocument` for some reason, so it's safer
|
||||
// to ignore the rootNode at this point, and use `node.ownerDocument`. Otherwise,
|
||||
// using `rootNode.contains(node)` will _always_ be true we'll get false-positives when
|
||||
// node is actually detached.
|
||||
// NOTE: If `nodeRootHost` or `node` happens to be the `document` itself (which is possible
|
||||
// if a tabbable/focusable node was quickly added to the DOM, focused, and then removed
|
||||
// from the DOM as in https://github.com/focus-trap/focus-trap-react/issues/905), then
|
||||
// `ownerDocument` will be `null`, hence the optional chaining on it.
|
||||
var nodeRoot = node && getRootNode(node);
|
||||
var nodeRootHost = (_nodeRoot = nodeRoot) === null || _nodeRoot === void 0 ? void 0 : _nodeRoot.host;
|
||||
|
||||
// in some cases, a detached node will return itself as the root instead of a document or
|
||||
// shadow root object, in which case, we shouldn't try to look further up the host chain
|
||||
var attached = false;
|
||||
if (nodeRoot && nodeRoot !== node) {
|
||||
var _nodeRootHost, _nodeRootHost$ownerDo, _node$ownerDocument;
|
||||
attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && (_nodeRootHost$ownerDo = _nodeRootHost.ownerDocument) !== null && _nodeRootHost$ownerDo !== void 0 && _nodeRootHost$ownerDo.contains(nodeRootHost) || node !== null && node !== void 0 && (_node$ownerDocument = node.ownerDocument) !== null && _node$ownerDocument !== void 0 && _node$ownerDocument.contains(node));
|
||||
while (!attached && nodeRootHost) {
|
||||
var _nodeRoot2, _nodeRootHost2, _nodeRootHost2$ownerD;
|
||||
// since it's not attached and we have a root host, the node MUST be in a nested shadow DOM,
|
||||
// which means we need to get the host's host and check if that parent host is contained
|
||||
// in (i.e. attached to) the document
|
||||
nodeRoot = getRootNode(nodeRootHost);
|
||||
nodeRootHost = (_nodeRoot2 = nodeRoot) === null || _nodeRoot2 === void 0 ? void 0 : _nodeRoot2.host;
|
||||
attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && (_nodeRootHost2$ownerD = _nodeRootHost2.ownerDocument) !== null && _nodeRootHost2$ownerD !== void 0 && _nodeRootHost2$ownerD.contains(nodeRootHost));
|
||||
}
|
||||
}
|
||||
return attached;
|
||||
};
|
||||
var isZeroArea = function isZeroArea(node) {
|
||||
var _node$getBoundingClie = node.getBoundingClientRect(),
|
||||
width = _node$getBoundingClie.width,
|
||||
height = _node$getBoundingClie.height;
|
||||
return width === 0 && height === 0;
|
||||
};
|
||||
var isHidden = function isHidden(node, _ref) {
|
||||
var displayCheck = _ref.displayCheck,
|
||||
getShadowRoot = _ref.getShadowRoot;
|
||||
// NOTE: visibility will be `undefined` if node is detached from the document
|
||||
// (see notes about this further down), which means we will consider it visible
|
||||
// (this is legacy behavior from a very long way back)
|
||||
// NOTE: we check this regardless of `displayCheck="none"` because this is a
|
||||
// _visibility_ check, not a _display_ check
|
||||
if (getComputedStyle(node).visibility === 'hidden') {
|
||||
return true;
|
||||
}
|
||||
var isDirectSummary = matches.call(node, 'details>summary:first-of-type');
|
||||
var nodeUnderDetails = isDirectSummary ? node.parentElement : node;
|
||||
if (matches.call(nodeUnderDetails, 'details:not([open]) *')) {
|
||||
return true;
|
||||
}
|
||||
if (!displayCheck || displayCheck === 'full' || displayCheck === 'legacy-full') {
|
||||
if (typeof getShadowRoot === 'function') {
|
||||
// figure out if we should consider the node to be in an undisclosed shadow and use the
|
||||
// 'non-zero-area' fallback
|
||||
var originalNode = node;
|
||||
while (node) {
|
||||
var parentElement = node.parentElement;
|
||||
var rootNode = getRootNode(node);
|
||||
if (parentElement && !parentElement.shadowRoot && getShadowRoot(parentElement) === true // check if there's an undisclosed shadow
|
||||
) {
|
||||
// node has an undisclosed shadow which means we can only treat it as a black box, so we
|
||||
// fall back to a non-zero-area test
|
||||
return isZeroArea(node);
|
||||
} else if (node.assignedSlot) {
|
||||
// iterate up slot
|
||||
node = node.assignedSlot;
|
||||
} else if (!parentElement && rootNode !== node.ownerDocument) {
|
||||
// cross shadow boundary
|
||||
node = rootNode.host;
|
||||
} else {
|
||||
// iterate up normal dom
|
||||
node = parentElement;
|
||||
}
|
||||
}
|
||||
node = originalNode;
|
||||
}
|
||||
// else, `getShadowRoot` might be true, but all that does is enable shadow DOM support
|
||||
// (i.e. it does not also presume that all nodes might have undisclosed shadows); or
|
||||
// it might be a falsy value, which means shadow DOM support is disabled
|
||||
|
||||
// Since we didn't find it sitting in an undisclosed shadow (or shadows are disabled)
|
||||
// now we can just test to see if it would normally be visible or not, provided it's
|
||||
// attached to the main document.
|
||||
// NOTE: We must consider case where node is inside a shadow DOM and given directly to
|
||||
// `isTabbable()` or `isFocusable()` -- regardless of `getShadowRoot` option setting.
|
||||
|
||||
if (isNodeAttached(node)) {
|
||||
// this works wherever the node is: if there's at least one client rect, it's
|
||||
// somehow displayed; it also covers the CSS 'display: contents' case where the
|
||||
// node itself is hidden in place of its contents; and there's no need to search
|
||||
// up the hierarchy either
|
||||
return !node.getClientRects().length;
|
||||
}
|
||||
|
||||
// Else, the node isn't attached to the document, which means the `getClientRects()`
|
||||
// API will __always__ return zero rects (this can happen, for example, if React
|
||||
// is used to render nodes onto a detached tree, as confirmed in this thread:
|
||||
// https://github.com/facebook/react/issues/9117#issuecomment-284228870)
|
||||
//
|
||||
// It also means that even window.getComputedStyle(node).display will return `undefined`
|
||||
// because styles are only computed for nodes that are in the document.
|
||||
//
|
||||
// NOTE: THIS HAS BEEN THE CASE FOR YEARS. It is not new, nor is it caused by tabbable
|
||||
// somehow. Though it was never stated officially, anyone who has ever used tabbable
|
||||
// APIs on nodes in detached containers has actually implicitly used tabbable in what
|
||||
// was later (as of v5.2.0 on Apr 9, 2021) called `displayCheck="none"` mode -- essentially
|
||||
// considering __everything__ to be visible because of the innability to determine styles.
|
||||
//
|
||||
// v6.0.0: As of this major release, the default 'full' option __no longer treats detached
|
||||
// nodes as visible with the 'none' fallback.__
|
||||
if (displayCheck !== 'legacy-full') {
|
||||
return true; // hidden
|
||||
}
|
||||
// else, fallback to 'none' mode and consider the node visible
|
||||
} else if (displayCheck === 'non-zero-area') {
|
||||
// NOTE: Even though this tests that the node's client rect is non-zero to determine
|
||||
// whether it's displayed, and that a detached node will __always__ have a zero-area
|
||||
// client rect, we don't special-case for whether the node is attached or not. In
|
||||
// this mode, we do want to consider nodes that have a zero area to be hidden at all
|
||||
// times, and that includes attached or not.
|
||||
return isZeroArea(node);
|
||||
}
|
||||
|
||||
// visible, as far as we can tell, or per current `displayCheck=none` mode, we assume
|
||||
// it's visible
|
||||
return false;
|
||||
};
|
||||
|
||||
// form fields (nested) inside a disabled fieldset are not focusable/tabbable
|
||||
// unless they are in the _first_ <legend> element of the top-most disabled
|
||||
// fieldset
|
||||
var isDisabledFromFieldset = function isDisabledFromFieldset(node) {
|
||||
if (/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(node.tagName)) {
|
||||
var parentNode = node.parentElement;
|
||||
// check if `node` is contained in a disabled <fieldset>
|
||||
while (parentNode) {
|
||||
if (parentNode.tagName === 'FIELDSET' && parentNode.disabled) {
|
||||
// look for the first <legend> among the children of the disabled <fieldset>
|
||||
for (var i = 0; i < parentNode.children.length; i++) {
|
||||
var child = parentNode.children.item(i);
|
||||
// when the first <legend> (in document order) is found
|
||||
if (child.tagName === 'LEGEND') {
|
||||
// if its parent <fieldset> is not nested in another disabled <fieldset>,
|
||||
// return whether `node` is a descendant of its first <legend>
|
||||
return matches.call(parentNode, 'fieldset[disabled] *') ? true : !child.contains(node);
|
||||
}
|
||||
}
|
||||
// the disabled <fieldset> containing `node` has no <legend>
|
||||
return true;
|
||||
}
|
||||
parentNode = parentNode.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// else, node's tabbable/focusable state should not be affected by a fieldset's
|
||||
// enabled/disabled state
|
||||
return false;
|
||||
};
|
||||
var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable(options, node) {
|
||||
if (node.disabled ||
|
||||
// we must do an inert look up to filter out any elements inside an inert ancestor
|
||||
// because we're limited in the type of selectors we can use in JSDom (see related
|
||||
// note related to `candidateSelectors`)
|
||||
isInert(node) || isHiddenInput(node) || isHidden(node, options) ||
|
||||
// For a details element with a summary, the summary element gets the focus
|
||||
isDetailsWithSummary(node) || isDisabledFromFieldset(node)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
var isNodeMatchingSelectorTabbable = function isNodeMatchingSelectorTabbable(options, node) {
|
||||
if (isNonTabbableRadio(node) || getTabIndex(node) < 0 || !isNodeMatchingSelectorFocusable(options, node)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
var isValidShadowRootTabbable = function isValidShadowRootTabbable(shadowHostNode) {
|
||||
var tabIndex = parseInt(shadowHostNode.getAttribute('tabindex'), 10);
|
||||
if (isNaN(tabIndex) || tabIndex >= 0) {
|
||||
return true;
|
||||
}
|
||||
// If a custom element has an explicit negative tabindex,
|
||||
// browsers will not allow tab targeting said element's children.
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array.<Element|CandidateScope>} candidates
|
||||
* @returns Element[]
|
||||
*/
|
||||
var sortByOrder = function sortByOrder(candidates) {
|
||||
var regularTabbables = [];
|
||||
var orderedTabbables = [];
|
||||
candidates.forEach(function (item, i) {
|
||||
var isScope = !!item.scopeParent;
|
||||
var element = isScope ? item.scopeParent : item;
|
||||
var candidateTabindex = getSortOrderTabIndex(element, isScope);
|
||||
var elements = isScope ? sortByOrder(item.candidates) : element;
|
||||
if (candidateTabindex === 0) {
|
||||
isScope ? regularTabbables.push.apply(regularTabbables, elements) : regularTabbables.push(element);
|
||||
} else {
|
||||
orderedTabbables.push({
|
||||
documentOrder: i,
|
||||
tabIndex: candidateTabindex,
|
||||
item: item,
|
||||
isScope: isScope,
|
||||
content: elements
|
||||
});
|
||||
}
|
||||
});
|
||||
return orderedTabbables.sort(sortOrderedTabbables).reduce(function (acc, sortable) {
|
||||
sortable.isScope ? acc.push.apply(acc, sortable.content) : acc.push(sortable.content);
|
||||
return acc;
|
||||
}, []).concat(regularTabbables);
|
||||
};
|
||||
var tabbable = function tabbable(container, options) {
|
||||
options = options || {};
|
||||
var candidates;
|
||||
if (options.getShadowRoot) {
|
||||
candidates = getCandidatesIteratively([container], options.includeContainer, {
|
||||
filter: isNodeMatchingSelectorTabbable.bind(null, options),
|
||||
flatten: false,
|
||||
getShadowRoot: options.getShadowRoot,
|
||||
shadowRootFilter: isValidShadowRootTabbable
|
||||
});
|
||||
} else {
|
||||
candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options));
|
||||
}
|
||||
return sortByOrder(candidates);
|
||||
};
|
||||
var focusable = function focusable(container, options) {
|
||||
options = options || {};
|
||||
var candidates;
|
||||
if (options.getShadowRoot) {
|
||||
candidates = getCandidatesIteratively([container], options.includeContainer, {
|
||||
filter: isNodeMatchingSelectorFocusable.bind(null, options),
|
||||
flatten: true,
|
||||
getShadowRoot: options.getShadowRoot
|
||||
});
|
||||
} else {
|
||||
candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options));
|
||||
}
|
||||
return candidates;
|
||||
};
|
||||
var isTabbable = function isTabbable(node, options) {
|
||||
options = options || {};
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (matches.call(node, candidateSelector) === false) {
|
||||
return false;
|
||||
}
|
||||
return isNodeMatchingSelectorTabbable(options, node);
|
||||
};
|
||||
var focusableCandidateSelector = /* #__PURE__ */candidateSelectors.concat('iframe').join(',');
|
||||
var isFocusable = function isFocusable(node, options) {
|
||||
options = options || {};
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (matches.call(node, focusableCandidateSelector) === false) {
|
||||
return false;
|
||||
}
|
||||
return isNodeMatchingSelectorFocusable(options, node);
|
||||
};
|
||||
|
||||
export { focusable, getTabIndex, isFocusable, isTabbable, tabbable };
|
||||
//# sourceMappingURL=index.esm.js.map
|
||||
1
node_modules/tabbable/dist/index.esm.js.map
generated
vendored
Normal file
1
node_modules/tabbable/dist/index.esm.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6
node_modules/tabbable/dist/index.esm.min.js
generated
vendored
Normal file
6
node_modules/tabbable/dist/index.esm.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
node_modules/tabbable/dist/index.esm.min.js.map
generated
vendored
Normal file
1
node_modules/tabbable/dist/index.esm.min.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
579
node_modules/tabbable/dist/index.js
generated
vendored
Normal file
579
node_modules/tabbable/dist/index.js
generated
vendored
Normal file
@@ -0,0 +1,579 @@
|
||||
/*!
|
||||
* tabbable 6.2.0
|
||||
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
// NOTE: separate `:not()` selectors has broader browser support than the newer
|
||||
// `:not([inert], [inert] *)` (Feb 2023)
|
||||
// CAREFUL: JSDom does not support `:not([inert] *)` as a selector; using it causes
|
||||
// the entire query to fail, resulting in no nodes found, which will break a lot
|
||||
// of things... so we have to rely on JS to identify nodes inside an inert container
|
||||
var candidateSelectors = ['input:not([inert])', 'select:not([inert])', 'textarea:not([inert])', 'a[href]:not([inert])', 'button:not([inert])', '[tabindex]:not(slot):not([inert])', 'audio[controls]:not([inert])', 'video[controls]:not([inert])', '[contenteditable]:not([contenteditable="false"]):not([inert])', 'details>summary:first-of-type:not([inert])', 'details:not([inert])'];
|
||||
var candidateSelector = /* #__PURE__ */candidateSelectors.join(',');
|
||||
var NoElement = typeof Element === 'undefined';
|
||||
var matches = NoElement ? function () {} : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
||||
var getRootNode = !NoElement && Element.prototype.getRootNode ? function (element) {
|
||||
var _element$getRootNode;
|
||||
return element === null || element === void 0 ? void 0 : (_element$getRootNode = element.getRootNode) === null || _element$getRootNode === void 0 ? void 0 : _element$getRootNode.call(element);
|
||||
} : function (element) {
|
||||
return element === null || element === void 0 ? void 0 : element.ownerDocument;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a node is inert or in an inert ancestor.
|
||||
* @param {Element} [node]
|
||||
* @param {boolean} [lookUp] If true and `node` is not inert, looks up at ancestors to
|
||||
* see if any of them are inert. If false, only `node` itself is considered.
|
||||
* @returns {boolean} True if inert itself or by way of being in an inert ancestor.
|
||||
* False if `node` is falsy.
|
||||
*/
|
||||
var isInert = function isInert(node, lookUp) {
|
||||
var _node$getAttribute;
|
||||
if (lookUp === void 0) {
|
||||
lookUp = true;
|
||||
}
|
||||
// CAREFUL: JSDom does not support inert at all, so we can't use the `HTMLElement.inert`
|
||||
// JS API property; we have to check the attribute, which can either be empty or 'true';
|
||||
// if it's `null` (not specified) or 'false', it's an active element
|
||||
var inertAtt = node === null || node === void 0 ? void 0 : (_node$getAttribute = node.getAttribute) === null || _node$getAttribute === void 0 ? void 0 : _node$getAttribute.call(node, 'inert');
|
||||
var inert = inertAtt === '' || inertAtt === 'true';
|
||||
|
||||
// NOTE: this could also be handled with `node.matches('[inert], :is([inert] *)')`
|
||||
// if it weren't for `matches()` not being a function on shadow roots; the following
|
||||
// code works for any kind of node
|
||||
// CAREFUL: JSDom does not appear to support certain selectors like `:not([inert] *)`
|
||||
// so it likely would not support `:is([inert] *)` either...
|
||||
var result = inert || lookUp && node && isInert(node.parentNode); // recursive
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a node's content is editable.
|
||||
* @param {Element} [node]
|
||||
* @returns True if it's content-editable; false if it's not or `node` is falsy.
|
||||
*/
|
||||
var isContentEditable = function isContentEditable(node) {
|
||||
var _node$getAttribute2;
|
||||
// CAREFUL: JSDom does not support the `HTMLElement.isContentEditable` API so we have
|
||||
// to use the attribute directly to check for this, which can either be empty or 'true';
|
||||
// if it's `null` (not specified) or 'false', it's a non-editable element
|
||||
var attValue = node === null || node === void 0 ? void 0 : (_node$getAttribute2 = node.getAttribute) === null || _node$getAttribute2 === void 0 ? void 0 : _node$getAttribute2.call(node, 'contenteditable');
|
||||
return attValue === '' || attValue === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Element} el container to check in
|
||||
* @param {boolean} includeContainer add container to check
|
||||
* @param {(node: Element) => boolean} filter filter candidates
|
||||
* @returns {Element[]}
|
||||
*/
|
||||
var getCandidates = function getCandidates(el, includeContainer, filter) {
|
||||
// even if `includeContainer=false`, we still have to check it for inertness because
|
||||
// if it's inert, all its children are inert
|
||||
if (isInert(el)) {
|
||||
return [];
|
||||
}
|
||||
var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector));
|
||||
if (includeContainer && matches.call(el, candidateSelector)) {
|
||||
candidates.unshift(el);
|
||||
}
|
||||
candidates = candidates.filter(filter);
|
||||
return candidates;
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback GetShadowRoot
|
||||
* @param {Element} element to check for shadow root
|
||||
* @returns {ShadowRoot|boolean} ShadowRoot if available or boolean indicating if a shadowRoot is attached but not available.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback ShadowRootFilter
|
||||
* @param {Element} shadowHostNode the element which contains shadow content
|
||||
* @returns {boolean} true if a shadow root could potentially contain valid candidates.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CandidateScope
|
||||
* @property {Element} scopeParent contains inner candidates
|
||||
* @property {Element[]} candidates list of candidates found in the scope parent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IterativeOptions
|
||||
* @property {GetShadowRoot|boolean} getShadowRoot true if shadow support is enabled; falsy if not;
|
||||
* if a function, implies shadow support is enabled and either returns the shadow root of an element
|
||||
* or a boolean stating if it has an undisclosed shadow root
|
||||
* @property {(node: Element) => boolean} filter filter candidates
|
||||
* @property {boolean} flatten if true then result will flatten any CandidateScope into the returned list
|
||||
* @property {ShadowRootFilter} shadowRootFilter filter shadow roots;
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Element[]} elements list of element containers to match candidates from
|
||||
* @param {boolean} includeContainer add container list to check
|
||||
* @param {IterativeOptions} options
|
||||
* @returns {Array.<Element|CandidateScope>}
|
||||
*/
|
||||
var getCandidatesIteratively = function getCandidatesIteratively(elements, includeContainer, options) {
|
||||
var candidates = [];
|
||||
var elementsToCheck = Array.from(elements);
|
||||
while (elementsToCheck.length) {
|
||||
var element = elementsToCheck.shift();
|
||||
if (isInert(element, false)) {
|
||||
// no need to look up since we're drilling down
|
||||
// anything inside this container will also be inert
|
||||
continue;
|
||||
}
|
||||
if (element.tagName === 'SLOT') {
|
||||
// add shadow dom slot scope (slot itself cannot be focusable)
|
||||
var assigned = element.assignedElements();
|
||||
var content = assigned.length ? assigned : element.children;
|
||||
var nestedCandidates = getCandidatesIteratively(content, true, options);
|
||||
if (options.flatten) {
|
||||
candidates.push.apply(candidates, nestedCandidates);
|
||||
} else {
|
||||
candidates.push({
|
||||
scopeParent: element,
|
||||
candidates: nestedCandidates
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// check candidate element
|
||||
var validCandidate = matches.call(element, candidateSelector);
|
||||
if (validCandidate && options.filter(element) && (includeContainer || !elements.includes(element))) {
|
||||
candidates.push(element);
|
||||
}
|
||||
|
||||
// iterate over shadow content if possible
|
||||
var shadowRoot = element.shadowRoot ||
|
||||
// check for an undisclosed shadow
|
||||
typeof options.getShadowRoot === 'function' && options.getShadowRoot(element);
|
||||
|
||||
// no inert look up because we're already drilling down and checking for inertness
|
||||
// on the way down, so all containers to this root node should have already been
|
||||
// vetted as non-inert
|
||||
var validShadowRoot = !isInert(shadowRoot, false) && (!options.shadowRootFilter || options.shadowRootFilter(element));
|
||||
if (shadowRoot && validShadowRoot) {
|
||||
// add shadow dom scope IIF a shadow root node was given; otherwise, an undisclosed
|
||||
// shadow exists, so look at light dom children as fallback BUT create a scope for any
|
||||
// child candidates found because they're likely slotted elements (elements that are
|
||||
// children of the web component element (which has the shadow), in the light dom, but
|
||||
// slotted somewhere _inside_ the undisclosed shadow) -- the scope is created below,
|
||||
// _after_ we return from this recursive call
|
||||
var _nestedCandidates = getCandidatesIteratively(shadowRoot === true ? element.children : shadowRoot.children, true, options);
|
||||
if (options.flatten) {
|
||||
candidates.push.apply(candidates, _nestedCandidates);
|
||||
} else {
|
||||
candidates.push({
|
||||
scopeParent: element,
|
||||
candidates: _nestedCandidates
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// there's not shadow so just dig into the element's (light dom) children
|
||||
// __without__ giving the element special scope treatment
|
||||
elementsToCheck.unshift.apply(elementsToCheck, element.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Determines if the node has an explicitly specified `tabindex` attribute.
|
||||
* @param {HTMLElement} node
|
||||
* @returns {boolean} True if so; false if not.
|
||||
*/
|
||||
var hasTabIndex = function hasTabIndex(node) {
|
||||
return !isNaN(parseInt(node.getAttribute('tabindex'), 10));
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the tab index of a given node.
|
||||
* @param {HTMLElement} node
|
||||
* @returns {number} Tab order (negative, 0, or positive number).
|
||||
* @throws {Error} If `node` is falsy.
|
||||
*/
|
||||
var getTabIndex = function getTabIndex(node) {
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (node.tabIndex < 0) {
|
||||
// in Chrome, <details/>, <audio controls/> and <video controls/> elements get a default
|
||||
// `tabIndex` of -1 when the 'tabindex' attribute isn't specified in the DOM,
|
||||
// yet they are still part of the regular tab order; in FF, they get a default
|
||||
// `tabIndex` of 0; since Chrome still puts those elements in the regular tab
|
||||
// order, consider their tab index to be 0.
|
||||
// Also browsers do not return `tabIndex` correctly for contentEditable nodes;
|
||||
// so if they don't have a tabindex attribute specifically set, assume it's 0.
|
||||
if ((/^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) || isContentEditable(node)) && !hasTabIndex(node)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return node.tabIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the tab index of a given node __for sort order purposes__.
|
||||
* @param {HTMLElement} node
|
||||
* @param {boolean} [isScope] True for a custom element with shadow root or slot that, by default,
|
||||
* has tabIndex -1, but needs to be sorted by document order in order for its content to be
|
||||
* inserted into the correct sort position.
|
||||
* @returns {number} Tab order (negative, 0, or positive number).
|
||||
*/
|
||||
var getSortOrderTabIndex = function getSortOrderTabIndex(node, isScope) {
|
||||
var tabIndex = getTabIndex(node);
|
||||
if (tabIndex < 0 && isScope && !hasTabIndex(node)) {
|
||||
return 0;
|
||||
}
|
||||
return tabIndex;
|
||||
};
|
||||
var sortOrderedTabbables = function sortOrderedTabbables(a, b) {
|
||||
return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
|
||||
};
|
||||
var isInput = function isInput(node) {
|
||||
return node.tagName === 'INPUT';
|
||||
};
|
||||
var isHiddenInput = function isHiddenInput(node) {
|
||||
return isInput(node) && node.type === 'hidden';
|
||||
};
|
||||
var isDetailsWithSummary = function isDetailsWithSummary(node) {
|
||||
var r = node.tagName === 'DETAILS' && Array.prototype.slice.apply(node.children).some(function (child) {
|
||||
return child.tagName === 'SUMMARY';
|
||||
});
|
||||
return r;
|
||||
};
|
||||
var getCheckedRadio = function getCheckedRadio(nodes, form) {
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].checked && nodes[i].form === form) {
|
||||
return nodes[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
var isTabbableRadio = function isTabbableRadio(node) {
|
||||
if (!node.name) {
|
||||
return true;
|
||||
}
|
||||
var radioScope = node.form || getRootNode(node);
|
||||
var queryRadios = function queryRadios(name) {
|
||||
return radioScope.querySelectorAll('input[type="radio"][name="' + name + '"]');
|
||||
};
|
||||
var radioSet;
|
||||
if (typeof window !== 'undefined' && typeof window.CSS !== 'undefined' && typeof window.CSS.escape === 'function') {
|
||||
radioSet = queryRadios(window.CSS.escape(node.name));
|
||||
} else {
|
||||
try {
|
||||
radioSet = queryRadios(node.name);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var checked = getCheckedRadio(radioSet, node.form);
|
||||
return !checked || checked === node;
|
||||
};
|
||||
var isRadio = function isRadio(node) {
|
||||
return isInput(node) && node.type === 'radio';
|
||||
};
|
||||
var isNonTabbableRadio = function isNonTabbableRadio(node) {
|
||||
return isRadio(node) && !isTabbableRadio(node);
|
||||
};
|
||||
|
||||
// determines if a node is ultimately attached to the window's document
|
||||
var isNodeAttached = function isNodeAttached(node) {
|
||||
var _nodeRoot;
|
||||
// The root node is the shadow root if the node is in a shadow DOM; some document otherwise
|
||||
// (but NOT _the_ document; see second 'If' comment below for more).
|
||||
// If rootNode is shadow root, it'll have a host, which is the element to which the shadow
|
||||
// is attached, and the one we need to check if it's in the document or not (because the
|
||||
// shadow, and all nodes it contains, is never considered in the document since shadows
|
||||
// behave like self-contained DOMs; but if the shadow's HOST, which is part of the document,
|
||||
// is hidden, or is not in the document itself but is detached, it will affect the shadow's
|
||||
// visibility, including all the nodes it contains). The host could be any normal node,
|
||||
// or a custom element (i.e. web component). Either way, that's the one that is considered
|
||||
// part of the document, not the shadow root, nor any of its children (i.e. the node being
|
||||
// tested).
|
||||
// To further complicate things, we have to look all the way up until we find a shadow HOST
|
||||
// that is attached (or find none) because the node might be in nested shadows...
|
||||
// If rootNode is not a shadow root, it won't have a host, and so rootNode should be the
|
||||
// document (per the docs) and while it's a Document-type object, that document does not
|
||||
// appear to be the same as the node's `ownerDocument` for some reason, so it's safer
|
||||
// to ignore the rootNode at this point, and use `node.ownerDocument`. Otherwise,
|
||||
// using `rootNode.contains(node)` will _always_ be true we'll get false-positives when
|
||||
// node is actually detached.
|
||||
// NOTE: If `nodeRootHost` or `node` happens to be the `document` itself (which is possible
|
||||
// if a tabbable/focusable node was quickly added to the DOM, focused, and then removed
|
||||
// from the DOM as in https://github.com/focus-trap/focus-trap-react/issues/905), then
|
||||
// `ownerDocument` will be `null`, hence the optional chaining on it.
|
||||
var nodeRoot = node && getRootNode(node);
|
||||
var nodeRootHost = (_nodeRoot = nodeRoot) === null || _nodeRoot === void 0 ? void 0 : _nodeRoot.host;
|
||||
|
||||
// in some cases, a detached node will return itself as the root instead of a document or
|
||||
// shadow root object, in which case, we shouldn't try to look further up the host chain
|
||||
var attached = false;
|
||||
if (nodeRoot && nodeRoot !== node) {
|
||||
var _nodeRootHost, _nodeRootHost$ownerDo, _node$ownerDocument;
|
||||
attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && (_nodeRootHost$ownerDo = _nodeRootHost.ownerDocument) !== null && _nodeRootHost$ownerDo !== void 0 && _nodeRootHost$ownerDo.contains(nodeRootHost) || node !== null && node !== void 0 && (_node$ownerDocument = node.ownerDocument) !== null && _node$ownerDocument !== void 0 && _node$ownerDocument.contains(node));
|
||||
while (!attached && nodeRootHost) {
|
||||
var _nodeRoot2, _nodeRootHost2, _nodeRootHost2$ownerD;
|
||||
// since it's not attached and we have a root host, the node MUST be in a nested shadow DOM,
|
||||
// which means we need to get the host's host and check if that parent host is contained
|
||||
// in (i.e. attached to) the document
|
||||
nodeRoot = getRootNode(nodeRootHost);
|
||||
nodeRootHost = (_nodeRoot2 = nodeRoot) === null || _nodeRoot2 === void 0 ? void 0 : _nodeRoot2.host;
|
||||
attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && (_nodeRootHost2$ownerD = _nodeRootHost2.ownerDocument) !== null && _nodeRootHost2$ownerD !== void 0 && _nodeRootHost2$ownerD.contains(nodeRootHost));
|
||||
}
|
||||
}
|
||||
return attached;
|
||||
};
|
||||
var isZeroArea = function isZeroArea(node) {
|
||||
var _node$getBoundingClie = node.getBoundingClientRect(),
|
||||
width = _node$getBoundingClie.width,
|
||||
height = _node$getBoundingClie.height;
|
||||
return width === 0 && height === 0;
|
||||
};
|
||||
var isHidden = function isHidden(node, _ref) {
|
||||
var displayCheck = _ref.displayCheck,
|
||||
getShadowRoot = _ref.getShadowRoot;
|
||||
// NOTE: visibility will be `undefined` if node is detached from the document
|
||||
// (see notes about this further down), which means we will consider it visible
|
||||
// (this is legacy behavior from a very long way back)
|
||||
// NOTE: we check this regardless of `displayCheck="none"` because this is a
|
||||
// _visibility_ check, not a _display_ check
|
||||
if (getComputedStyle(node).visibility === 'hidden') {
|
||||
return true;
|
||||
}
|
||||
var isDirectSummary = matches.call(node, 'details>summary:first-of-type');
|
||||
var nodeUnderDetails = isDirectSummary ? node.parentElement : node;
|
||||
if (matches.call(nodeUnderDetails, 'details:not([open]) *')) {
|
||||
return true;
|
||||
}
|
||||
if (!displayCheck || displayCheck === 'full' || displayCheck === 'legacy-full') {
|
||||
if (typeof getShadowRoot === 'function') {
|
||||
// figure out if we should consider the node to be in an undisclosed shadow and use the
|
||||
// 'non-zero-area' fallback
|
||||
var originalNode = node;
|
||||
while (node) {
|
||||
var parentElement = node.parentElement;
|
||||
var rootNode = getRootNode(node);
|
||||
if (parentElement && !parentElement.shadowRoot && getShadowRoot(parentElement) === true // check if there's an undisclosed shadow
|
||||
) {
|
||||
// node has an undisclosed shadow which means we can only treat it as a black box, so we
|
||||
// fall back to a non-zero-area test
|
||||
return isZeroArea(node);
|
||||
} else if (node.assignedSlot) {
|
||||
// iterate up slot
|
||||
node = node.assignedSlot;
|
||||
} else if (!parentElement && rootNode !== node.ownerDocument) {
|
||||
// cross shadow boundary
|
||||
node = rootNode.host;
|
||||
} else {
|
||||
// iterate up normal dom
|
||||
node = parentElement;
|
||||
}
|
||||
}
|
||||
node = originalNode;
|
||||
}
|
||||
// else, `getShadowRoot` might be true, but all that does is enable shadow DOM support
|
||||
// (i.e. it does not also presume that all nodes might have undisclosed shadows); or
|
||||
// it might be a falsy value, which means shadow DOM support is disabled
|
||||
|
||||
// Since we didn't find it sitting in an undisclosed shadow (or shadows are disabled)
|
||||
// now we can just test to see if it would normally be visible or not, provided it's
|
||||
// attached to the main document.
|
||||
// NOTE: We must consider case where node is inside a shadow DOM and given directly to
|
||||
// `isTabbable()` or `isFocusable()` -- regardless of `getShadowRoot` option setting.
|
||||
|
||||
if (isNodeAttached(node)) {
|
||||
// this works wherever the node is: if there's at least one client rect, it's
|
||||
// somehow displayed; it also covers the CSS 'display: contents' case where the
|
||||
// node itself is hidden in place of its contents; and there's no need to search
|
||||
// up the hierarchy either
|
||||
return !node.getClientRects().length;
|
||||
}
|
||||
|
||||
// Else, the node isn't attached to the document, which means the `getClientRects()`
|
||||
// API will __always__ return zero rects (this can happen, for example, if React
|
||||
// is used to render nodes onto a detached tree, as confirmed in this thread:
|
||||
// https://github.com/facebook/react/issues/9117#issuecomment-284228870)
|
||||
//
|
||||
// It also means that even window.getComputedStyle(node).display will return `undefined`
|
||||
// because styles are only computed for nodes that are in the document.
|
||||
//
|
||||
// NOTE: THIS HAS BEEN THE CASE FOR YEARS. It is not new, nor is it caused by tabbable
|
||||
// somehow. Though it was never stated officially, anyone who has ever used tabbable
|
||||
// APIs on nodes in detached containers has actually implicitly used tabbable in what
|
||||
// was later (as of v5.2.0 on Apr 9, 2021) called `displayCheck="none"` mode -- essentially
|
||||
// considering __everything__ to be visible because of the innability to determine styles.
|
||||
//
|
||||
// v6.0.0: As of this major release, the default 'full' option __no longer treats detached
|
||||
// nodes as visible with the 'none' fallback.__
|
||||
if (displayCheck !== 'legacy-full') {
|
||||
return true; // hidden
|
||||
}
|
||||
// else, fallback to 'none' mode and consider the node visible
|
||||
} else if (displayCheck === 'non-zero-area') {
|
||||
// NOTE: Even though this tests that the node's client rect is non-zero to determine
|
||||
// whether it's displayed, and that a detached node will __always__ have a zero-area
|
||||
// client rect, we don't special-case for whether the node is attached or not. In
|
||||
// this mode, we do want to consider nodes that have a zero area to be hidden at all
|
||||
// times, and that includes attached or not.
|
||||
return isZeroArea(node);
|
||||
}
|
||||
|
||||
// visible, as far as we can tell, or per current `displayCheck=none` mode, we assume
|
||||
// it's visible
|
||||
return false;
|
||||
};
|
||||
|
||||
// form fields (nested) inside a disabled fieldset are not focusable/tabbable
|
||||
// unless they are in the _first_ <legend> element of the top-most disabled
|
||||
// fieldset
|
||||
var isDisabledFromFieldset = function isDisabledFromFieldset(node) {
|
||||
if (/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(node.tagName)) {
|
||||
var parentNode = node.parentElement;
|
||||
// check if `node` is contained in a disabled <fieldset>
|
||||
while (parentNode) {
|
||||
if (parentNode.tagName === 'FIELDSET' && parentNode.disabled) {
|
||||
// look for the first <legend> among the children of the disabled <fieldset>
|
||||
for (var i = 0; i < parentNode.children.length; i++) {
|
||||
var child = parentNode.children.item(i);
|
||||
// when the first <legend> (in document order) is found
|
||||
if (child.tagName === 'LEGEND') {
|
||||
// if its parent <fieldset> is not nested in another disabled <fieldset>,
|
||||
// return whether `node` is a descendant of its first <legend>
|
||||
return matches.call(parentNode, 'fieldset[disabled] *') ? true : !child.contains(node);
|
||||
}
|
||||
}
|
||||
// the disabled <fieldset> containing `node` has no <legend>
|
||||
return true;
|
||||
}
|
||||
parentNode = parentNode.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// else, node's tabbable/focusable state should not be affected by a fieldset's
|
||||
// enabled/disabled state
|
||||
return false;
|
||||
};
|
||||
var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable(options, node) {
|
||||
if (node.disabled ||
|
||||
// we must do an inert look up to filter out any elements inside an inert ancestor
|
||||
// because we're limited in the type of selectors we can use in JSDom (see related
|
||||
// note related to `candidateSelectors`)
|
||||
isInert(node) || isHiddenInput(node) || isHidden(node, options) ||
|
||||
// For a details element with a summary, the summary element gets the focus
|
||||
isDetailsWithSummary(node) || isDisabledFromFieldset(node)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
var isNodeMatchingSelectorTabbable = function isNodeMatchingSelectorTabbable(options, node) {
|
||||
if (isNonTabbableRadio(node) || getTabIndex(node) < 0 || !isNodeMatchingSelectorFocusable(options, node)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
var isValidShadowRootTabbable = function isValidShadowRootTabbable(shadowHostNode) {
|
||||
var tabIndex = parseInt(shadowHostNode.getAttribute('tabindex'), 10);
|
||||
if (isNaN(tabIndex) || tabIndex >= 0) {
|
||||
return true;
|
||||
}
|
||||
// If a custom element has an explicit negative tabindex,
|
||||
// browsers will not allow tab targeting said element's children.
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array.<Element|CandidateScope>} candidates
|
||||
* @returns Element[]
|
||||
*/
|
||||
var sortByOrder = function sortByOrder(candidates) {
|
||||
var regularTabbables = [];
|
||||
var orderedTabbables = [];
|
||||
candidates.forEach(function (item, i) {
|
||||
var isScope = !!item.scopeParent;
|
||||
var element = isScope ? item.scopeParent : item;
|
||||
var candidateTabindex = getSortOrderTabIndex(element, isScope);
|
||||
var elements = isScope ? sortByOrder(item.candidates) : element;
|
||||
if (candidateTabindex === 0) {
|
||||
isScope ? regularTabbables.push.apply(regularTabbables, elements) : regularTabbables.push(element);
|
||||
} else {
|
||||
orderedTabbables.push({
|
||||
documentOrder: i,
|
||||
tabIndex: candidateTabindex,
|
||||
item: item,
|
||||
isScope: isScope,
|
||||
content: elements
|
||||
});
|
||||
}
|
||||
});
|
||||
return orderedTabbables.sort(sortOrderedTabbables).reduce(function (acc, sortable) {
|
||||
sortable.isScope ? acc.push.apply(acc, sortable.content) : acc.push(sortable.content);
|
||||
return acc;
|
||||
}, []).concat(regularTabbables);
|
||||
};
|
||||
var tabbable = function tabbable(container, options) {
|
||||
options = options || {};
|
||||
var candidates;
|
||||
if (options.getShadowRoot) {
|
||||
candidates = getCandidatesIteratively([container], options.includeContainer, {
|
||||
filter: isNodeMatchingSelectorTabbable.bind(null, options),
|
||||
flatten: false,
|
||||
getShadowRoot: options.getShadowRoot,
|
||||
shadowRootFilter: isValidShadowRootTabbable
|
||||
});
|
||||
} else {
|
||||
candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options));
|
||||
}
|
||||
return sortByOrder(candidates);
|
||||
};
|
||||
var focusable = function focusable(container, options) {
|
||||
options = options || {};
|
||||
var candidates;
|
||||
if (options.getShadowRoot) {
|
||||
candidates = getCandidatesIteratively([container], options.includeContainer, {
|
||||
filter: isNodeMatchingSelectorFocusable.bind(null, options),
|
||||
flatten: true,
|
||||
getShadowRoot: options.getShadowRoot
|
||||
});
|
||||
} else {
|
||||
candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options));
|
||||
}
|
||||
return candidates;
|
||||
};
|
||||
var isTabbable = function isTabbable(node, options) {
|
||||
options = options || {};
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (matches.call(node, candidateSelector) === false) {
|
||||
return false;
|
||||
}
|
||||
return isNodeMatchingSelectorTabbable(options, node);
|
||||
};
|
||||
var focusableCandidateSelector = /* #__PURE__ */candidateSelectors.concat('iframe').join(',');
|
||||
var isFocusable = function isFocusable(node, options) {
|
||||
options = options || {};
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (matches.call(node, focusableCandidateSelector) === false) {
|
||||
return false;
|
||||
}
|
||||
return isNodeMatchingSelectorFocusable(options, node);
|
||||
};
|
||||
|
||||
exports.focusable = focusable;
|
||||
exports.getTabIndex = getTabIndex;
|
||||
exports.isFocusable = isFocusable;
|
||||
exports.isTabbable = isTabbable;
|
||||
exports.tabbable = tabbable;
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
node_modules/tabbable/dist/index.js.map
generated
vendored
Normal file
1
node_modules/tabbable/dist/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6
node_modules/tabbable/dist/index.min.js
generated
vendored
Normal file
6
node_modules/tabbable/dist/index.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
node_modules/tabbable/dist/index.min.js.map
generated
vendored
Normal file
1
node_modules/tabbable/dist/index.min.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
590
node_modules/tabbable/dist/index.umd.js
generated
vendored
Normal file
590
node_modules/tabbable/dist/index.umd.js
generated
vendored
Normal file
@@ -0,0 +1,590 @@
|
||||
/*!
|
||||
* tabbable 6.2.0
|
||||
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
|
||||
*/
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, (function () {
|
||||
var current = global.tabbable;
|
||||
var exports = global.tabbable = {};
|
||||
factory(exports);
|
||||
exports.noConflict = function () { global.tabbable = current; return exports; };
|
||||
})());
|
||||
})(this, (function (exports) { 'use strict';
|
||||
|
||||
// NOTE: separate `:not()` selectors has broader browser support than the newer
|
||||
// `:not([inert], [inert] *)` (Feb 2023)
|
||||
// CAREFUL: JSDom does not support `:not([inert] *)` as a selector; using it causes
|
||||
// the entire query to fail, resulting in no nodes found, which will break a lot
|
||||
// of things... so we have to rely on JS to identify nodes inside an inert container
|
||||
var candidateSelectors = ['input:not([inert])', 'select:not([inert])', 'textarea:not([inert])', 'a[href]:not([inert])', 'button:not([inert])', '[tabindex]:not(slot):not([inert])', 'audio[controls]:not([inert])', 'video[controls]:not([inert])', '[contenteditable]:not([contenteditable="false"]):not([inert])', 'details>summary:first-of-type:not([inert])', 'details:not([inert])'];
|
||||
var candidateSelector = /* #__PURE__ */candidateSelectors.join(',');
|
||||
var NoElement = typeof Element === 'undefined';
|
||||
var matches = NoElement ? function () {} : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
||||
var getRootNode = !NoElement && Element.prototype.getRootNode ? function (element) {
|
||||
var _element$getRootNode;
|
||||
return element === null || element === void 0 ? void 0 : (_element$getRootNode = element.getRootNode) === null || _element$getRootNode === void 0 ? void 0 : _element$getRootNode.call(element);
|
||||
} : function (element) {
|
||||
return element === null || element === void 0 ? void 0 : element.ownerDocument;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a node is inert or in an inert ancestor.
|
||||
* @param {Element} [node]
|
||||
* @param {boolean} [lookUp] If true and `node` is not inert, looks up at ancestors to
|
||||
* see if any of them are inert. If false, only `node` itself is considered.
|
||||
* @returns {boolean} True if inert itself or by way of being in an inert ancestor.
|
||||
* False if `node` is falsy.
|
||||
*/
|
||||
var isInert = function isInert(node, lookUp) {
|
||||
var _node$getAttribute;
|
||||
if (lookUp === void 0) {
|
||||
lookUp = true;
|
||||
}
|
||||
// CAREFUL: JSDom does not support inert at all, so we can't use the `HTMLElement.inert`
|
||||
// JS API property; we have to check the attribute, which can either be empty or 'true';
|
||||
// if it's `null` (not specified) or 'false', it's an active element
|
||||
var inertAtt = node === null || node === void 0 ? void 0 : (_node$getAttribute = node.getAttribute) === null || _node$getAttribute === void 0 ? void 0 : _node$getAttribute.call(node, 'inert');
|
||||
var inert = inertAtt === '' || inertAtt === 'true';
|
||||
|
||||
// NOTE: this could also be handled with `node.matches('[inert], :is([inert] *)')`
|
||||
// if it weren't for `matches()` not being a function on shadow roots; the following
|
||||
// code works for any kind of node
|
||||
// CAREFUL: JSDom does not appear to support certain selectors like `:not([inert] *)`
|
||||
// so it likely would not support `:is([inert] *)` either...
|
||||
var result = inert || lookUp && node && isInert(node.parentNode); // recursive
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a node's content is editable.
|
||||
* @param {Element} [node]
|
||||
* @returns True if it's content-editable; false if it's not or `node` is falsy.
|
||||
*/
|
||||
var isContentEditable = function isContentEditable(node) {
|
||||
var _node$getAttribute2;
|
||||
// CAREFUL: JSDom does not support the `HTMLElement.isContentEditable` API so we have
|
||||
// to use the attribute directly to check for this, which can either be empty or 'true';
|
||||
// if it's `null` (not specified) or 'false', it's a non-editable element
|
||||
var attValue = node === null || node === void 0 ? void 0 : (_node$getAttribute2 = node.getAttribute) === null || _node$getAttribute2 === void 0 ? void 0 : _node$getAttribute2.call(node, 'contenteditable');
|
||||
return attValue === '' || attValue === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Element} el container to check in
|
||||
* @param {boolean} includeContainer add container to check
|
||||
* @param {(node: Element) => boolean} filter filter candidates
|
||||
* @returns {Element[]}
|
||||
*/
|
||||
var getCandidates = function getCandidates(el, includeContainer, filter) {
|
||||
// even if `includeContainer=false`, we still have to check it for inertness because
|
||||
// if it's inert, all its children are inert
|
||||
if (isInert(el)) {
|
||||
return [];
|
||||
}
|
||||
var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector));
|
||||
if (includeContainer && matches.call(el, candidateSelector)) {
|
||||
candidates.unshift(el);
|
||||
}
|
||||
candidates = candidates.filter(filter);
|
||||
return candidates;
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback GetShadowRoot
|
||||
* @param {Element} element to check for shadow root
|
||||
* @returns {ShadowRoot|boolean} ShadowRoot if available or boolean indicating if a shadowRoot is attached but not available.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback ShadowRootFilter
|
||||
* @param {Element} shadowHostNode the element which contains shadow content
|
||||
* @returns {boolean} true if a shadow root could potentially contain valid candidates.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CandidateScope
|
||||
* @property {Element} scopeParent contains inner candidates
|
||||
* @property {Element[]} candidates list of candidates found in the scope parent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IterativeOptions
|
||||
* @property {GetShadowRoot|boolean} getShadowRoot true if shadow support is enabled; falsy if not;
|
||||
* if a function, implies shadow support is enabled and either returns the shadow root of an element
|
||||
* or a boolean stating if it has an undisclosed shadow root
|
||||
* @property {(node: Element) => boolean} filter filter candidates
|
||||
* @property {boolean} flatten if true then result will flatten any CandidateScope into the returned list
|
||||
* @property {ShadowRootFilter} shadowRootFilter filter shadow roots;
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Element[]} elements list of element containers to match candidates from
|
||||
* @param {boolean} includeContainer add container list to check
|
||||
* @param {IterativeOptions} options
|
||||
* @returns {Array.<Element|CandidateScope>}
|
||||
*/
|
||||
var getCandidatesIteratively = function getCandidatesIteratively(elements, includeContainer, options) {
|
||||
var candidates = [];
|
||||
var elementsToCheck = Array.from(elements);
|
||||
while (elementsToCheck.length) {
|
||||
var element = elementsToCheck.shift();
|
||||
if (isInert(element, false)) {
|
||||
// no need to look up since we're drilling down
|
||||
// anything inside this container will also be inert
|
||||
continue;
|
||||
}
|
||||
if (element.tagName === 'SLOT') {
|
||||
// add shadow dom slot scope (slot itself cannot be focusable)
|
||||
var assigned = element.assignedElements();
|
||||
var content = assigned.length ? assigned : element.children;
|
||||
var nestedCandidates = getCandidatesIteratively(content, true, options);
|
||||
if (options.flatten) {
|
||||
candidates.push.apply(candidates, nestedCandidates);
|
||||
} else {
|
||||
candidates.push({
|
||||
scopeParent: element,
|
||||
candidates: nestedCandidates
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// check candidate element
|
||||
var validCandidate = matches.call(element, candidateSelector);
|
||||
if (validCandidate && options.filter(element) && (includeContainer || !elements.includes(element))) {
|
||||
candidates.push(element);
|
||||
}
|
||||
|
||||
// iterate over shadow content if possible
|
||||
var shadowRoot = element.shadowRoot ||
|
||||
// check for an undisclosed shadow
|
||||
typeof options.getShadowRoot === 'function' && options.getShadowRoot(element);
|
||||
|
||||
// no inert look up because we're already drilling down and checking for inertness
|
||||
// on the way down, so all containers to this root node should have already been
|
||||
// vetted as non-inert
|
||||
var validShadowRoot = !isInert(shadowRoot, false) && (!options.shadowRootFilter || options.shadowRootFilter(element));
|
||||
if (shadowRoot && validShadowRoot) {
|
||||
// add shadow dom scope IIF a shadow root node was given; otherwise, an undisclosed
|
||||
// shadow exists, so look at light dom children as fallback BUT create a scope for any
|
||||
// child candidates found because they're likely slotted elements (elements that are
|
||||
// children of the web component element (which has the shadow), in the light dom, but
|
||||
// slotted somewhere _inside_ the undisclosed shadow) -- the scope is created below,
|
||||
// _after_ we return from this recursive call
|
||||
var _nestedCandidates = getCandidatesIteratively(shadowRoot === true ? element.children : shadowRoot.children, true, options);
|
||||
if (options.flatten) {
|
||||
candidates.push.apply(candidates, _nestedCandidates);
|
||||
} else {
|
||||
candidates.push({
|
||||
scopeParent: element,
|
||||
candidates: _nestedCandidates
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// there's not shadow so just dig into the element's (light dom) children
|
||||
// __without__ giving the element special scope treatment
|
||||
elementsToCheck.unshift.apply(elementsToCheck, element.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Determines if the node has an explicitly specified `tabindex` attribute.
|
||||
* @param {HTMLElement} node
|
||||
* @returns {boolean} True if so; false if not.
|
||||
*/
|
||||
var hasTabIndex = function hasTabIndex(node) {
|
||||
return !isNaN(parseInt(node.getAttribute('tabindex'), 10));
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the tab index of a given node.
|
||||
* @param {HTMLElement} node
|
||||
* @returns {number} Tab order (negative, 0, or positive number).
|
||||
* @throws {Error} If `node` is falsy.
|
||||
*/
|
||||
var getTabIndex = function getTabIndex(node) {
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (node.tabIndex < 0) {
|
||||
// in Chrome, <details/>, <audio controls/> and <video controls/> elements get a default
|
||||
// `tabIndex` of -1 when the 'tabindex' attribute isn't specified in the DOM,
|
||||
// yet they are still part of the regular tab order; in FF, they get a default
|
||||
// `tabIndex` of 0; since Chrome still puts those elements in the regular tab
|
||||
// order, consider their tab index to be 0.
|
||||
// Also browsers do not return `tabIndex` correctly for contentEditable nodes;
|
||||
// so if they don't have a tabindex attribute specifically set, assume it's 0.
|
||||
if ((/^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) || isContentEditable(node)) && !hasTabIndex(node)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return node.tabIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the tab index of a given node __for sort order purposes__.
|
||||
* @param {HTMLElement} node
|
||||
* @param {boolean} [isScope] True for a custom element with shadow root or slot that, by default,
|
||||
* has tabIndex -1, but needs to be sorted by document order in order for its content to be
|
||||
* inserted into the correct sort position.
|
||||
* @returns {number} Tab order (negative, 0, or positive number).
|
||||
*/
|
||||
var getSortOrderTabIndex = function getSortOrderTabIndex(node, isScope) {
|
||||
var tabIndex = getTabIndex(node);
|
||||
if (tabIndex < 0 && isScope && !hasTabIndex(node)) {
|
||||
return 0;
|
||||
}
|
||||
return tabIndex;
|
||||
};
|
||||
var sortOrderedTabbables = function sortOrderedTabbables(a, b) {
|
||||
return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
|
||||
};
|
||||
var isInput = function isInput(node) {
|
||||
return node.tagName === 'INPUT';
|
||||
};
|
||||
var isHiddenInput = function isHiddenInput(node) {
|
||||
return isInput(node) && node.type === 'hidden';
|
||||
};
|
||||
var isDetailsWithSummary = function isDetailsWithSummary(node) {
|
||||
var r = node.tagName === 'DETAILS' && Array.prototype.slice.apply(node.children).some(function (child) {
|
||||
return child.tagName === 'SUMMARY';
|
||||
});
|
||||
return r;
|
||||
};
|
||||
var getCheckedRadio = function getCheckedRadio(nodes, form) {
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].checked && nodes[i].form === form) {
|
||||
return nodes[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
var isTabbableRadio = function isTabbableRadio(node) {
|
||||
if (!node.name) {
|
||||
return true;
|
||||
}
|
||||
var radioScope = node.form || getRootNode(node);
|
||||
var queryRadios = function queryRadios(name) {
|
||||
return radioScope.querySelectorAll('input[type="radio"][name="' + name + '"]');
|
||||
};
|
||||
var radioSet;
|
||||
if (typeof window !== 'undefined' && typeof window.CSS !== 'undefined' && typeof window.CSS.escape === 'function') {
|
||||
radioSet = queryRadios(window.CSS.escape(node.name));
|
||||
} else {
|
||||
try {
|
||||
radioSet = queryRadios(node.name);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var checked = getCheckedRadio(radioSet, node.form);
|
||||
return !checked || checked === node;
|
||||
};
|
||||
var isRadio = function isRadio(node) {
|
||||
return isInput(node) && node.type === 'radio';
|
||||
};
|
||||
var isNonTabbableRadio = function isNonTabbableRadio(node) {
|
||||
return isRadio(node) && !isTabbableRadio(node);
|
||||
};
|
||||
|
||||
// determines if a node is ultimately attached to the window's document
|
||||
var isNodeAttached = function isNodeAttached(node) {
|
||||
var _nodeRoot;
|
||||
// The root node is the shadow root if the node is in a shadow DOM; some document otherwise
|
||||
// (but NOT _the_ document; see second 'If' comment below for more).
|
||||
// If rootNode is shadow root, it'll have a host, which is the element to which the shadow
|
||||
// is attached, and the one we need to check if it's in the document or not (because the
|
||||
// shadow, and all nodes it contains, is never considered in the document since shadows
|
||||
// behave like self-contained DOMs; but if the shadow's HOST, which is part of the document,
|
||||
// is hidden, or is not in the document itself but is detached, it will affect the shadow's
|
||||
// visibility, including all the nodes it contains). The host could be any normal node,
|
||||
// or a custom element (i.e. web component). Either way, that's the one that is considered
|
||||
// part of the document, not the shadow root, nor any of its children (i.e. the node being
|
||||
// tested).
|
||||
// To further complicate things, we have to look all the way up until we find a shadow HOST
|
||||
// that is attached (or find none) because the node might be in nested shadows...
|
||||
// If rootNode is not a shadow root, it won't have a host, and so rootNode should be the
|
||||
// document (per the docs) and while it's a Document-type object, that document does not
|
||||
// appear to be the same as the node's `ownerDocument` for some reason, so it's safer
|
||||
// to ignore the rootNode at this point, and use `node.ownerDocument`. Otherwise,
|
||||
// using `rootNode.contains(node)` will _always_ be true we'll get false-positives when
|
||||
// node is actually detached.
|
||||
// NOTE: If `nodeRootHost` or `node` happens to be the `document` itself (which is possible
|
||||
// if a tabbable/focusable node was quickly added to the DOM, focused, and then removed
|
||||
// from the DOM as in https://github.com/focus-trap/focus-trap-react/issues/905), then
|
||||
// `ownerDocument` will be `null`, hence the optional chaining on it.
|
||||
var nodeRoot = node && getRootNode(node);
|
||||
var nodeRootHost = (_nodeRoot = nodeRoot) === null || _nodeRoot === void 0 ? void 0 : _nodeRoot.host;
|
||||
|
||||
// in some cases, a detached node will return itself as the root instead of a document or
|
||||
// shadow root object, in which case, we shouldn't try to look further up the host chain
|
||||
var attached = false;
|
||||
if (nodeRoot && nodeRoot !== node) {
|
||||
var _nodeRootHost, _nodeRootHost$ownerDo, _node$ownerDocument;
|
||||
attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && (_nodeRootHost$ownerDo = _nodeRootHost.ownerDocument) !== null && _nodeRootHost$ownerDo !== void 0 && _nodeRootHost$ownerDo.contains(nodeRootHost) || node !== null && node !== void 0 && (_node$ownerDocument = node.ownerDocument) !== null && _node$ownerDocument !== void 0 && _node$ownerDocument.contains(node));
|
||||
while (!attached && nodeRootHost) {
|
||||
var _nodeRoot2, _nodeRootHost2, _nodeRootHost2$ownerD;
|
||||
// since it's not attached and we have a root host, the node MUST be in a nested shadow DOM,
|
||||
// which means we need to get the host's host and check if that parent host is contained
|
||||
// in (i.e. attached to) the document
|
||||
nodeRoot = getRootNode(nodeRootHost);
|
||||
nodeRootHost = (_nodeRoot2 = nodeRoot) === null || _nodeRoot2 === void 0 ? void 0 : _nodeRoot2.host;
|
||||
attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && (_nodeRootHost2$ownerD = _nodeRootHost2.ownerDocument) !== null && _nodeRootHost2$ownerD !== void 0 && _nodeRootHost2$ownerD.contains(nodeRootHost));
|
||||
}
|
||||
}
|
||||
return attached;
|
||||
};
|
||||
var isZeroArea = function isZeroArea(node) {
|
||||
var _node$getBoundingClie = node.getBoundingClientRect(),
|
||||
width = _node$getBoundingClie.width,
|
||||
height = _node$getBoundingClie.height;
|
||||
return width === 0 && height === 0;
|
||||
};
|
||||
var isHidden = function isHidden(node, _ref) {
|
||||
var displayCheck = _ref.displayCheck,
|
||||
getShadowRoot = _ref.getShadowRoot;
|
||||
// NOTE: visibility will be `undefined` if node is detached from the document
|
||||
// (see notes about this further down), which means we will consider it visible
|
||||
// (this is legacy behavior from a very long way back)
|
||||
// NOTE: we check this regardless of `displayCheck="none"` because this is a
|
||||
// _visibility_ check, not a _display_ check
|
||||
if (getComputedStyle(node).visibility === 'hidden') {
|
||||
return true;
|
||||
}
|
||||
var isDirectSummary = matches.call(node, 'details>summary:first-of-type');
|
||||
var nodeUnderDetails = isDirectSummary ? node.parentElement : node;
|
||||
if (matches.call(nodeUnderDetails, 'details:not([open]) *')) {
|
||||
return true;
|
||||
}
|
||||
if (!displayCheck || displayCheck === 'full' || displayCheck === 'legacy-full') {
|
||||
if (typeof getShadowRoot === 'function') {
|
||||
// figure out if we should consider the node to be in an undisclosed shadow and use the
|
||||
// 'non-zero-area' fallback
|
||||
var originalNode = node;
|
||||
while (node) {
|
||||
var parentElement = node.parentElement;
|
||||
var rootNode = getRootNode(node);
|
||||
if (parentElement && !parentElement.shadowRoot && getShadowRoot(parentElement) === true // check if there's an undisclosed shadow
|
||||
) {
|
||||
// node has an undisclosed shadow which means we can only treat it as a black box, so we
|
||||
// fall back to a non-zero-area test
|
||||
return isZeroArea(node);
|
||||
} else if (node.assignedSlot) {
|
||||
// iterate up slot
|
||||
node = node.assignedSlot;
|
||||
} else if (!parentElement && rootNode !== node.ownerDocument) {
|
||||
// cross shadow boundary
|
||||
node = rootNode.host;
|
||||
} else {
|
||||
// iterate up normal dom
|
||||
node = parentElement;
|
||||
}
|
||||
}
|
||||
node = originalNode;
|
||||
}
|
||||
// else, `getShadowRoot` might be true, but all that does is enable shadow DOM support
|
||||
// (i.e. it does not also presume that all nodes might have undisclosed shadows); or
|
||||
// it might be a falsy value, which means shadow DOM support is disabled
|
||||
|
||||
// Since we didn't find it sitting in an undisclosed shadow (or shadows are disabled)
|
||||
// now we can just test to see if it would normally be visible or not, provided it's
|
||||
// attached to the main document.
|
||||
// NOTE: We must consider case where node is inside a shadow DOM and given directly to
|
||||
// `isTabbable()` or `isFocusable()` -- regardless of `getShadowRoot` option setting.
|
||||
|
||||
if (isNodeAttached(node)) {
|
||||
// this works wherever the node is: if there's at least one client rect, it's
|
||||
// somehow displayed; it also covers the CSS 'display: contents' case where the
|
||||
// node itself is hidden in place of its contents; and there's no need to search
|
||||
// up the hierarchy either
|
||||
return !node.getClientRects().length;
|
||||
}
|
||||
|
||||
// Else, the node isn't attached to the document, which means the `getClientRects()`
|
||||
// API will __always__ return zero rects (this can happen, for example, if React
|
||||
// is used to render nodes onto a detached tree, as confirmed in this thread:
|
||||
// https://github.com/facebook/react/issues/9117#issuecomment-284228870)
|
||||
//
|
||||
// It also means that even window.getComputedStyle(node).display will return `undefined`
|
||||
// because styles are only computed for nodes that are in the document.
|
||||
//
|
||||
// NOTE: THIS HAS BEEN THE CASE FOR YEARS. It is not new, nor is it caused by tabbable
|
||||
// somehow. Though it was never stated officially, anyone who has ever used tabbable
|
||||
// APIs on nodes in detached containers has actually implicitly used tabbable in what
|
||||
// was later (as of v5.2.0 on Apr 9, 2021) called `displayCheck="none"` mode -- essentially
|
||||
// considering __everything__ to be visible because of the innability to determine styles.
|
||||
//
|
||||
// v6.0.0: As of this major release, the default 'full' option __no longer treats detached
|
||||
// nodes as visible with the 'none' fallback.__
|
||||
if (displayCheck !== 'legacy-full') {
|
||||
return true; // hidden
|
||||
}
|
||||
// else, fallback to 'none' mode and consider the node visible
|
||||
} else if (displayCheck === 'non-zero-area') {
|
||||
// NOTE: Even though this tests that the node's client rect is non-zero to determine
|
||||
// whether it's displayed, and that a detached node will __always__ have a zero-area
|
||||
// client rect, we don't special-case for whether the node is attached or not. In
|
||||
// this mode, we do want to consider nodes that have a zero area to be hidden at all
|
||||
// times, and that includes attached or not.
|
||||
return isZeroArea(node);
|
||||
}
|
||||
|
||||
// visible, as far as we can tell, or per current `displayCheck=none` mode, we assume
|
||||
// it's visible
|
||||
return false;
|
||||
};
|
||||
|
||||
// form fields (nested) inside a disabled fieldset are not focusable/tabbable
|
||||
// unless they are in the _first_ <legend> element of the top-most disabled
|
||||
// fieldset
|
||||
var isDisabledFromFieldset = function isDisabledFromFieldset(node) {
|
||||
if (/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(node.tagName)) {
|
||||
var parentNode = node.parentElement;
|
||||
// check if `node` is contained in a disabled <fieldset>
|
||||
while (parentNode) {
|
||||
if (parentNode.tagName === 'FIELDSET' && parentNode.disabled) {
|
||||
// look for the first <legend> among the children of the disabled <fieldset>
|
||||
for (var i = 0; i < parentNode.children.length; i++) {
|
||||
var child = parentNode.children.item(i);
|
||||
// when the first <legend> (in document order) is found
|
||||
if (child.tagName === 'LEGEND') {
|
||||
// if its parent <fieldset> is not nested in another disabled <fieldset>,
|
||||
// return whether `node` is a descendant of its first <legend>
|
||||
return matches.call(parentNode, 'fieldset[disabled] *') ? true : !child.contains(node);
|
||||
}
|
||||
}
|
||||
// the disabled <fieldset> containing `node` has no <legend>
|
||||
return true;
|
||||
}
|
||||
parentNode = parentNode.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// else, node's tabbable/focusable state should not be affected by a fieldset's
|
||||
// enabled/disabled state
|
||||
return false;
|
||||
};
|
||||
var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable(options, node) {
|
||||
if (node.disabled ||
|
||||
// we must do an inert look up to filter out any elements inside an inert ancestor
|
||||
// because we're limited in the type of selectors we can use in JSDom (see related
|
||||
// note related to `candidateSelectors`)
|
||||
isInert(node) || isHiddenInput(node) || isHidden(node, options) ||
|
||||
// For a details element with a summary, the summary element gets the focus
|
||||
isDetailsWithSummary(node) || isDisabledFromFieldset(node)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
var isNodeMatchingSelectorTabbable = function isNodeMatchingSelectorTabbable(options, node) {
|
||||
if (isNonTabbableRadio(node) || getTabIndex(node) < 0 || !isNodeMatchingSelectorFocusable(options, node)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
var isValidShadowRootTabbable = function isValidShadowRootTabbable(shadowHostNode) {
|
||||
var tabIndex = parseInt(shadowHostNode.getAttribute('tabindex'), 10);
|
||||
if (isNaN(tabIndex) || tabIndex >= 0) {
|
||||
return true;
|
||||
}
|
||||
// If a custom element has an explicit negative tabindex,
|
||||
// browsers will not allow tab targeting said element's children.
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array.<Element|CandidateScope>} candidates
|
||||
* @returns Element[]
|
||||
*/
|
||||
var sortByOrder = function sortByOrder(candidates) {
|
||||
var regularTabbables = [];
|
||||
var orderedTabbables = [];
|
||||
candidates.forEach(function (item, i) {
|
||||
var isScope = !!item.scopeParent;
|
||||
var element = isScope ? item.scopeParent : item;
|
||||
var candidateTabindex = getSortOrderTabIndex(element, isScope);
|
||||
var elements = isScope ? sortByOrder(item.candidates) : element;
|
||||
if (candidateTabindex === 0) {
|
||||
isScope ? regularTabbables.push.apply(regularTabbables, elements) : regularTabbables.push(element);
|
||||
} else {
|
||||
orderedTabbables.push({
|
||||
documentOrder: i,
|
||||
tabIndex: candidateTabindex,
|
||||
item: item,
|
||||
isScope: isScope,
|
||||
content: elements
|
||||
});
|
||||
}
|
||||
});
|
||||
return orderedTabbables.sort(sortOrderedTabbables).reduce(function (acc, sortable) {
|
||||
sortable.isScope ? acc.push.apply(acc, sortable.content) : acc.push(sortable.content);
|
||||
return acc;
|
||||
}, []).concat(regularTabbables);
|
||||
};
|
||||
var tabbable = function tabbable(container, options) {
|
||||
options = options || {};
|
||||
var candidates;
|
||||
if (options.getShadowRoot) {
|
||||
candidates = getCandidatesIteratively([container], options.includeContainer, {
|
||||
filter: isNodeMatchingSelectorTabbable.bind(null, options),
|
||||
flatten: false,
|
||||
getShadowRoot: options.getShadowRoot,
|
||||
shadowRootFilter: isValidShadowRootTabbable
|
||||
});
|
||||
} else {
|
||||
candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options));
|
||||
}
|
||||
return sortByOrder(candidates);
|
||||
};
|
||||
var focusable = function focusable(container, options) {
|
||||
options = options || {};
|
||||
var candidates;
|
||||
if (options.getShadowRoot) {
|
||||
candidates = getCandidatesIteratively([container], options.includeContainer, {
|
||||
filter: isNodeMatchingSelectorFocusable.bind(null, options),
|
||||
flatten: true,
|
||||
getShadowRoot: options.getShadowRoot
|
||||
});
|
||||
} else {
|
||||
candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options));
|
||||
}
|
||||
return candidates;
|
||||
};
|
||||
var isTabbable = function isTabbable(node, options) {
|
||||
options = options || {};
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (matches.call(node, candidateSelector) === false) {
|
||||
return false;
|
||||
}
|
||||
return isNodeMatchingSelectorTabbable(options, node);
|
||||
};
|
||||
var focusableCandidateSelector = /* #__PURE__ */candidateSelectors.concat('iframe').join(',');
|
||||
var isFocusable = function isFocusable(node, options) {
|
||||
options = options || {};
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (matches.call(node, focusableCandidateSelector) === false) {
|
||||
return false;
|
||||
}
|
||||
return isNodeMatchingSelectorFocusable(options, node);
|
||||
};
|
||||
|
||||
exports.focusable = focusable;
|
||||
exports.getTabIndex = getTabIndex;
|
||||
exports.isFocusable = isFocusable;
|
||||
exports.isTabbable = isTabbable;
|
||||
exports.tabbable = tabbable;
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=index.umd.js.map
|
||||
1
node_modules/tabbable/dist/index.umd.js.map
generated
vendored
Normal file
1
node_modules/tabbable/dist/index.umd.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6
node_modules/tabbable/dist/index.umd.min.js
generated
vendored
Normal file
6
node_modules/tabbable/dist/index.umd.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
node_modules/tabbable/dist/index.umd.min.js.map
generated
vendored
Normal file
1
node_modules/tabbable/dist/index.umd.min.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
34
node_modules/tabbable/index.d.ts
generated
vendored
Normal file
34
node_modules/tabbable/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
type FocusableElement = HTMLElement | SVGElement;
|
||||
|
||||
export type CheckOptions = {
|
||||
displayCheck?: 'full' | 'legacy-full' | 'non-zero-area' | 'none';
|
||||
getShadowRoot?: boolean | ((node: FocusableElement) => ShadowRoot | boolean | undefined);
|
||||
};
|
||||
|
||||
export type TabbableOptions = {
|
||||
includeContainer?: boolean;
|
||||
};
|
||||
|
||||
export declare function tabbable(
|
||||
container: Element,
|
||||
options?: TabbableOptions & CheckOptions
|
||||
): FocusableElement[];
|
||||
|
||||
export declare function focusable(
|
||||
container: Element,
|
||||
options?: TabbableOptions & CheckOptions
|
||||
): FocusableElement[];
|
||||
|
||||
export declare function isTabbable(
|
||||
node: Element,
|
||||
options?: CheckOptions
|
||||
): boolean;
|
||||
|
||||
export declare function isFocusable(
|
||||
node: Element,
|
||||
options?: CheckOptions
|
||||
): boolean;
|
||||
|
||||
export declare function getTabIndex(
|
||||
node: Element,
|
||||
): number;
|
||||
97
node_modules/tabbable/package.json
generated
vendored
Normal file
97
node_modules/tabbable/package.json
generated
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "tabbable",
|
||||
"version": "6.2.0",
|
||||
"description": "Returns an array of all tabbable DOM nodes within a containing node.",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"package.json",
|
||||
"dist",
|
||||
"src",
|
||||
"index.d.ts",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"SECURITY.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run clean && npm run compile",
|
||||
"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": "npm run compile:esm && npm run compile:cjs && npm run compile:umd",
|
||||
"format": "prettier --write \"{*,src/**/*,test/**/*,.github/workflows/*}.+(js|yml)\"",
|
||||
"format:check": "prettier --check \"{*,src/**/*,test/**/*,.github/workflows/*}.+(js|yml)\"",
|
||||
"format:watch": "onchange \"{*,src/**/*,test/**/*,.github/workflows/*}.+(js|yml)\" -- prettier --write {{changed}}",
|
||||
"lint": "eslint \"*.js\" \"src/**/*.js\" \"test/**/*.js\"",
|
||||
"start": "npm run compile:cjs && budo test/debug.js --live --dir -- -t brfs",
|
||||
"test": "npm run format:check && npm run lint && npm run test:types && npm run test:unit && npm run test:e2e",
|
||||
"test:types": "tsc index.d.ts",
|
||||
"test:unit": "jest",
|
||||
"test:e2e": "ELECTRON_ENABLE_LOGGING=1 cypress run",
|
||||
"test:e2e:dev": "cypress open",
|
||||
"test:coverage": "BABEL_ENV=test npm run test:e2e --env coverage=true",
|
||||
"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/tabbable.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "David Clark",
|
||||
"url": "http://davidtheclark.com/"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/focus-trap/tabbable/issues"
|
||||
},
|
||||
"homepage": "https://github.com/focus-trap/tabbable#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@babel/eslint-parser": "^7.22.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.20.7",
|
||||
"@babel/preset-env": "^7.22.5",
|
||||
"@changesets/cli": "^2.26.1",
|
||||
"@cypress/code-coverage": "^3.10.7",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-commonjs": "^25.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.1.0",
|
||||
"@rollup/plugin-terser": "^0.4.3",
|
||||
"@testing-library/dom": "^9.3.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@types/node": "^20.3.1",
|
||||
"all-contributors-cli": "^6.26.0",
|
||||
"babel-jest": "^29.5.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"brfs": "^2.0.2",
|
||||
"browserify": "^17.0.0",
|
||||
"budo": "^11.8.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^12.15.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-cypress": "^2.13.3",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.2",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jest-watch-typeahead": "^2.2.2",
|
||||
"onchange": "^7.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-sourcemaps": "^0.6.3",
|
||||
"typescript": "^5.1.3",
|
||||
"watchify": "^4.0.0",
|
||||
"webpack": "^5.87.0"
|
||||
},
|
||||
"overrides": {
|
||||
"nwsapi": "2.2.2"
|
||||
}
|
||||
}
|
||||
688
node_modules/tabbable/src/index.js
generated
vendored
Normal file
688
node_modules/tabbable/src/index.js
generated
vendored
Normal file
@@ -0,0 +1,688 @@
|
||||
// NOTE: separate `:not()` selectors has broader browser support than the newer
|
||||
// `:not([inert], [inert] *)` (Feb 2023)
|
||||
// CAREFUL: JSDom does not support `:not([inert] *)` as a selector; using it causes
|
||||
// the entire query to fail, resulting in no nodes found, which will break a lot
|
||||
// of things... so we have to rely on JS to identify nodes inside an inert container
|
||||
const candidateSelectors = [
|
||||
'input:not([inert])',
|
||||
'select:not([inert])',
|
||||
'textarea:not([inert])',
|
||||
'a[href]:not([inert])',
|
||||
'button:not([inert])',
|
||||
'[tabindex]:not(slot):not([inert])',
|
||||
'audio[controls]:not([inert])',
|
||||
'video[controls]:not([inert])',
|
||||
'[contenteditable]:not([contenteditable="false"]):not([inert])',
|
||||
'details>summary:first-of-type:not([inert])',
|
||||
'details:not([inert])',
|
||||
];
|
||||
const candidateSelector = /* #__PURE__ */ candidateSelectors.join(',');
|
||||
|
||||
const NoElement = typeof Element === 'undefined';
|
||||
|
||||
const matches = NoElement
|
||||
? function () {}
|
||||
: Element.prototype.matches ||
|
||||
Element.prototype.msMatchesSelector ||
|
||||
Element.prototype.webkitMatchesSelector;
|
||||
|
||||
const getRootNode =
|
||||
!NoElement && Element.prototype.getRootNode
|
||||
? (element) => element?.getRootNode?.()
|
||||
: (element) => element?.ownerDocument;
|
||||
|
||||
/**
|
||||
* Determines if a node is inert or in an inert ancestor.
|
||||
* @param {Element} [node]
|
||||
* @param {boolean} [lookUp] If true and `node` is not inert, looks up at ancestors to
|
||||
* see if any of them are inert. If false, only `node` itself is considered.
|
||||
* @returns {boolean} True if inert itself or by way of being in an inert ancestor.
|
||||
* False if `node` is falsy.
|
||||
*/
|
||||
const isInert = function (node, lookUp = true) {
|
||||
// CAREFUL: JSDom does not support inert at all, so we can't use the `HTMLElement.inert`
|
||||
// JS API property; we have to check the attribute, which can either be empty or 'true';
|
||||
// if it's `null` (not specified) or 'false', it's an active element
|
||||
const inertAtt = node?.getAttribute?.('inert');
|
||||
const inert = inertAtt === '' || inertAtt === 'true';
|
||||
|
||||
// NOTE: this could also be handled with `node.matches('[inert], :is([inert] *)')`
|
||||
// if it weren't for `matches()` not being a function on shadow roots; the following
|
||||
// code works for any kind of node
|
||||
// CAREFUL: JSDom does not appear to support certain selectors like `:not([inert] *)`
|
||||
// so it likely would not support `:is([inert] *)` either...
|
||||
const result = inert || (lookUp && node && isInert(node.parentNode)); // recursive
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a node's content is editable.
|
||||
* @param {Element} [node]
|
||||
* @returns True if it's content-editable; false if it's not or `node` is falsy.
|
||||
*/
|
||||
const isContentEditable = function (node) {
|
||||
// CAREFUL: JSDom does not support the `HTMLElement.isContentEditable` API so we have
|
||||
// to use the attribute directly to check for this, which can either be empty or 'true';
|
||||
// if it's `null` (not specified) or 'false', it's a non-editable element
|
||||
const attValue = node?.getAttribute?.('contenteditable');
|
||||
return attValue === '' || attValue === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Element} el container to check in
|
||||
* @param {boolean} includeContainer add container to check
|
||||
* @param {(node: Element) => boolean} filter filter candidates
|
||||
* @returns {Element[]}
|
||||
*/
|
||||
const getCandidates = function (el, includeContainer, filter) {
|
||||
// even if `includeContainer=false`, we still have to check it for inertness because
|
||||
// if it's inert, all its children are inert
|
||||
if (isInert(el)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let candidates = Array.prototype.slice.apply(
|
||||
el.querySelectorAll(candidateSelector)
|
||||
);
|
||||
if (includeContainer && matches.call(el, candidateSelector)) {
|
||||
candidates.unshift(el);
|
||||
}
|
||||
candidates = candidates.filter(filter);
|
||||
return candidates;
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback GetShadowRoot
|
||||
* @param {Element} element to check for shadow root
|
||||
* @returns {ShadowRoot|boolean} ShadowRoot if available or boolean indicating if a shadowRoot is attached but not available.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback ShadowRootFilter
|
||||
* @param {Element} shadowHostNode the element which contains shadow content
|
||||
* @returns {boolean} true if a shadow root could potentially contain valid candidates.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CandidateScope
|
||||
* @property {Element} scopeParent contains inner candidates
|
||||
* @property {Element[]} candidates list of candidates found in the scope parent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IterativeOptions
|
||||
* @property {GetShadowRoot|boolean} getShadowRoot true if shadow support is enabled; falsy if not;
|
||||
* if a function, implies shadow support is enabled and either returns the shadow root of an element
|
||||
* or a boolean stating if it has an undisclosed shadow root
|
||||
* @property {(node: Element) => boolean} filter filter candidates
|
||||
* @property {boolean} flatten if true then result will flatten any CandidateScope into the returned list
|
||||
* @property {ShadowRootFilter} shadowRootFilter filter shadow roots;
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Element[]} elements list of element containers to match candidates from
|
||||
* @param {boolean} includeContainer add container list to check
|
||||
* @param {IterativeOptions} options
|
||||
* @returns {Array.<Element|CandidateScope>}
|
||||
*/
|
||||
const getCandidatesIteratively = function (
|
||||
elements,
|
||||
includeContainer,
|
||||
options
|
||||
) {
|
||||
const candidates = [];
|
||||
const elementsToCheck = Array.from(elements);
|
||||
while (elementsToCheck.length) {
|
||||
const element = elementsToCheck.shift();
|
||||
if (isInert(element, false)) {
|
||||
// no need to look up since we're drilling down
|
||||
// anything inside this container will also be inert
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element.tagName === 'SLOT') {
|
||||
// add shadow dom slot scope (slot itself cannot be focusable)
|
||||
const assigned = element.assignedElements();
|
||||
const content = assigned.length ? assigned : element.children;
|
||||
const nestedCandidates = getCandidatesIteratively(content, true, options);
|
||||
if (options.flatten) {
|
||||
candidates.push(...nestedCandidates);
|
||||
} else {
|
||||
candidates.push({
|
||||
scopeParent: element,
|
||||
candidates: nestedCandidates,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// check candidate element
|
||||
const validCandidate = matches.call(element, candidateSelector);
|
||||
if (
|
||||
validCandidate &&
|
||||
options.filter(element) &&
|
||||
(includeContainer || !elements.includes(element))
|
||||
) {
|
||||
candidates.push(element);
|
||||
}
|
||||
|
||||
// iterate over shadow content if possible
|
||||
const shadowRoot =
|
||||
element.shadowRoot ||
|
||||
// check for an undisclosed shadow
|
||||
(typeof options.getShadowRoot === 'function' &&
|
||||
options.getShadowRoot(element));
|
||||
|
||||
// no inert look up because we're already drilling down and checking for inertness
|
||||
// on the way down, so all containers to this root node should have already been
|
||||
// vetted as non-inert
|
||||
const validShadowRoot =
|
||||
!isInert(shadowRoot, false) &&
|
||||
(!options.shadowRootFilter || options.shadowRootFilter(element));
|
||||
|
||||
if (shadowRoot && validShadowRoot) {
|
||||
// add shadow dom scope IIF a shadow root node was given; otherwise, an undisclosed
|
||||
// shadow exists, so look at light dom children as fallback BUT create a scope for any
|
||||
// child candidates found because they're likely slotted elements (elements that are
|
||||
// children of the web component element (which has the shadow), in the light dom, but
|
||||
// slotted somewhere _inside_ the undisclosed shadow) -- the scope is created below,
|
||||
// _after_ we return from this recursive call
|
||||
const nestedCandidates = getCandidatesIteratively(
|
||||
shadowRoot === true ? element.children : shadowRoot.children,
|
||||
true,
|
||||
options
|
||||
);
|
||||
|
||||
if (options.flatten) {
|
||||
candidates.push(...nestedCandidates);
|
||||
} else {
|
||||
candidates.push({
|
||||
scopeParent: element,
|
||||
candidates: nestedCandidates,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// there's not shadow so just dig into the element's (light dom) children
|
||||
// __without__ giving the element special scope treatment
|
||||
elementsToCheck.unshift(...element.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Determines if the node has an explicitly specified `tabindex` attribute.
|
||||
* @param {HTMLElement} node
|
||||
* @returns {boolean} True if so; false if not.
|
||||
*/
|
||||
const hasTabIndex = function (node) {
|
||||
return !isNaN(parseInt(node.getAttribute('tabindex'), 10));
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the tab index of a given node.
|
||||
* @param {HTMLElement} node
|
||||
* @returns {number} Tab order (negative, 0, or positive number).
|
||||
* @throws {Error} If `node` is falsy.
|
||||
*/
|
||||
const getTabIndex = function (node) {
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
|
||||
if (node.tabIndex < 0) {
|
||||
// in Chrome, <details/>, <audio controls/> and <video controls/> elements get a default
|
||||
// `tabIndex` of -1 when the 'tabindex' attribute isn't specified in the DOM,
|
||||
// yet they are still part of the regular tab order; in FF, they get a default
|
||||
// `tabIndex` of 0; since Chrome still puts those elements in the regular tab
|
||||
// order, consider their tab index to be 0.
|
||||
// Also browsers do not return `tabIndex` correctly for contentEditable nodes;
|
||||
// so if they don't have a tabindex attribute specifically set, assume it's 0.
|
||||
if (
|
||||
(/^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) ||
|
||||
isContentEditable(node)) &&
|
||||
!hasTabIndex(node)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return node.tabIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the tab index of a given node __for sort order purposes__.
|
||||
* @param {HTMLElement} node
|
||||
* @param {boolean} [isScope] True for a custom element with shadow root or slot that, by default,
|
||||
* has tabIndex -1, but needs to be sorted by document order in order for its content to be
|
||||
* inserted into the correct sort position.
|
||||
* @returns {number} Tab order (negative, 0, or positive number).
|
||||
*/
|
||||
const getSortOrderTabIndex = function (node, isScope) {
|
||||
const tabIndex = getTabIndex(node);
|
||||
|
||||
if (tabIndex < 0 && isScope && !hasTabIndex(node)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return tabIndex;
|
||||
};
|
||||
|
||||
const sortOrderedTabbables = function (a, b) {
|
||||
return a.tabIndex === b.tabIndex
|
||||
? a.documentOrder - b.documentOrder
|
||||
: a.tabIndex - b.tabIndex;
|
||||
};
|
||||
|
||||
const isInput = function (node) {
|
||||
return node.tagName === 'INPUT';
|
||||
};
|
||||
|
||||
const isHiddenInput = function (node) {
|
||||
return isInput(node) && node.type === 'hidden';
|
||||
};
|
||||
|
||||
const isDetailsWithSummary = function (node) {
|
||||
const r =
|
||||
node.tagName === 'DETAILS' &&
|
||||
Array.prototype.slice
|
||||
.apply(node.children)
|
||||
.some((child) => child.tagName === 'SUMMARY');
|
||||
return r;
|
||||
};
|
||||
|
||||
const getCheckedRadio = function (nodes, form) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].checked && nodes[i].form === form) {
|
||||
return nodes[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isTabbableRadio = function (node) {
|
||||
if (!node.name) {
|
||||
return true;
|
||||
}
|
||||
const radioScope = node.form || getRootNode(node);
|
||||
const queryRadios = function (name) {
|
||||
return radioScope.querySelectorAll(
|
||||
'input[type="radio"][name="' + name + '"]'
|
||||
);
|
||||
};
|
||||
|
||||
let radioSet;
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.CSS !== 'undefined' &&
|
||||
typeof window.CSS.escape === 'function'
|
||||
) {
|
||||
radioSet = queryRadios(window.CSS.escape(node.name));
|
||||
} else {
|
||||
try {
|
||||
radioSet = queryRadios(node.name);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s',
|
||||
err.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const checked = getCheckedRadio(radioSet, node.form);
|
||||
return !checked || checked === node;
|
||||
};
|
||||
|
||||
const isRadio = function (node) {
|
||||
return isInput(node) && node.type === 'radio';
|
||||
};
|
||||
|
||||
const isNonTabbableRadio = function (node) {
|
||||
return isRadio(node) && !isTabbableRadio(node);
|
||||
};
|
||||
|
||||
// determines if a node is ultimately attached to the window's document
|
||||
const isNodeAttached = function (node) {
|
||||
// The root node is the shadow root if the node is in a shadow DOM; some document otherwise
|
||||
// (but NOT _the_ document; see second 'If' comment below for more).
|
||||
// If rootNode is shadow root, it'll have a host, which is the element to which the shadow
|
||||
// is attached, and the one we need to check if it's in the document or not (because the
|
||||
// shadow, and all nodes it contains, is never considered in the document since shadows
|
||||
// behave like self-contained DOMs; but if the shadow's HOST, which is part of the document,
|
||||
// is hidden, or is not in the document itself but is detached, it will affect the shadow's
|
||||
// visibility, including all the nodes it contains). The host could be any normal node,
|
||||
// or a custom element (i.e. web component). Either way, that's the one that is considered
|
||||
// part of the document, not the shadow root, nor any of its children (i.e. the node being
|
||||
// tested).
|
||||
// To further complicate things, we have to look all the way up until we find a shadow HOST
|
||||
// that is attached (or find none) because the node might be in nested shadows...
|
||||
// If rootNode is not a shadow root, it won't have a host, and so rootNode should be the
|
||||
// document (per the docs) and while it's a Document-type object, that document does not
|
||||
// appear to be the same as the node's `ownerDocument` for some reason, so it's safer
|
||||
// to ignore the rootNode at this point, and use `node.ownerDocument`. Otherwise,
|
||||
// using `rootNode.contains(node)` will _always_ be true we'll get false-positives when
|
||||
// node is actually detached.
|
||||
// NOTE: If `nodeRootHost` or `node` happens to be the `document` itself (which is possible
|
||||
// if a tabbable/focusable node was quickly added to the DOM, focused, and then removed
|
||||
// from the DOM as in https://github.com/focus-trap/focus-trap-react/issues/905), then
|
||||
// `ownerDocument` will be `null`, hence the optional chaining on it.
|
||||
let nodeRoot = node && getRootNode(node);
|
||||
let nodeRootHost = nodeRoot?.host;
|
||||
|
||||
// in some cases, a detached node will return itself as the root instead of a document or
|
||||
// shadow root object, in which case, we shouldn't try to look further up the host chain
|
||||
let attached = false;
|
||||
if (nodeRoot && nodeRoot !== node) {
|
||||
attached = !!(
|
||||
nodeRootHost?.ownerDocument?.contains(nodeRootHost) ||
|
||||
node?.ownerDocument?.contains(node)
|
||||
);
|
||||
|
||||
while (!attached && nodeRootHost) {
|
||||
// since it's not attached and we have a root host, the node MUST be in a nested shadow DOM,
|
||||
// which means we need to get the host's host and check if that parent host is contained
|
||||
// in (i.e. attached to) the document
|
||||
nodeRoot = getRootNode(nodeRootHost);
|
||||
nodeRootHost = nodeRoot?.host;
|
||||
attached = !!nodeRootHost?.ownerDocument?.contains(nodeRootHost);
|
||||
}
|
||||
}
|
||||
|
||||
return attached;
|
||||
};
|
||||
|
||||
const isZeroArea = function (node) {
|
||||
const { width, height } = node.getBoundingClientRect();
|
||||
return width === 0 && height === 0;
|
||||
};
|
||||
const isHidden = function (node, { displayCheck, getShadowRoot }) {
|
||||
// NOTE: visibility will be `undefined` if node is detached from the document
|
||||
// (see notes about this further down), which means we will consider it visible
|
||||
// (this is legacy behavior from a very long way back)
|
||||
// NOTE: we check this regardless of `displayCheck="none"` because this is a
|
||||
// _visibility_ check, not a _display_ check
|
||||
if (getComputedStyle(node).visibility === 'hidden') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isDirectSummary = matches.call(node, 'details>summary:first-of-type');
|
||||
const nodeUnderDetails = isDirectSummary ? node.parentElement : node;
|
||||
if (matches.call(nodeUnderDetails, 'details:not([open]) *')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
!displayCheck ||
|
||||
displayCheck === 'full' ||
|
||||
displayCheck === 'legacy-full'
|
||||
) {
|
||||
if (typeof getShadowRoot === 'function') {
|
||||
// figure out if we should consider the node to be in an undisclosed shadow and use the
|
||||
// 'non-zero-area' fallback
|
||||
const originalNode = node;
|
||||
while (node) {
|
||||
const parentElement = node.parentElement;
|
||||
const rootNode = getRootNode(node);
|
||||
if (
|
||||
parentElement &&
|
||||
!parentElement.shadowRoot &&
|
||||
getShadowRoot(parentElement) === true // check if there's an undisclosed shadow
|
||||
) {
|
||||
// node has an undisclosed shadow which means we can only treat it as a black box, so we
|
||||
// fall back to a non-zero-area test
|
||||
return isZeroArea(node);
|
||||
} else if (node.assignedSlot) {
|
||||
// iterate up slot
|
||||
node = node.assignedSlot;
|
||||
} else if (!parentElement && rootNode !== node.ownerDocument) {
|
||||
// cross shadow boundary
|
||||
node = rootNode.host;
|
||||
} else {
|
||||
// iterate up normal dom
|
||||
node = parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
node = originalNode;
|
||||
}
|
||||
// else, `getShadowRoot` might be true, but all that does is enable shadow DOM support
|
||||
// (i.e. it does not also presume that all nodes might have undisclosed shadows); or
|
||||
// it might be a falsy value, which means shadow DOM support is disabled
|
||||
|
||||
// Since we didn't find it sitting in an undisclosed shadow (or shadows are disabled)
|
||||
// now we can just test to see if it would normally be visible or not, provided it's
|
||||
// attached to the main document.
|
||||
// NOTE: We must consider case where node is inside a shadow DOM and given directly to
|
||||
// `isTabbable()` or `isFocusable()` -- regardless of `getShadowRoot` option setting.
|
||||
|
||||
if (isNodeAttached(node)) {
|
||||
// this works wherever the node is: if there's at least one client rect, it's
|
||||
// somehow displayed; it also covers the CSS 'display: contents' case where the
|
||||
// node itself is hidden in place of its contents; and there's no need to search
|
||||
// up the hierarchy either
|
||||
return !node.getClientRects().length;
|
||||
}
|
||||
|
||||
// Else, the node isn't attached to the document, which means the `getClientRects()`
|
||||
// API will __always__ return zero rects (this can happen, for example, if React
|
||||
// is used to render nodes onto a detached tree, as confirmed in this thread:
|
||||
// https://github.com/facebook/react/issues/9117#issuecomment-284228870)
|
||||
//
|
||||
// It also means that even window.getComputedStyle(node).display will return `undefined`
|
||||
// because styles are only computed for nodes that are in the document.
|
||||
//
|
||||
// NOTE: THIS HAS BEEN THE CASE FOR YEARS. It is not new, nor is it caused by tabbable
|
||||
// somehow. Though it was never stated officially, anyone who has ever used tabbable
|
||||
// APIs on nodes in detached containers has actually implicitly used tabbable in what
|
||||
// was later (as of v5.2.0 on Apr 9, 2021) called `displayCheck="none"` mode -- essentially
|
||||
// considering __everything__ to be visible because of the innability to determine styles.
|
||||
//
|
||||
// v6.0.0: As of this major release, the default 'full' option __no longer treats detached
|
||||
// nodes as visible with the 'none' fallback.__
|
||||
if (displayCheck !== 'legacy-full') {
|
||||
return true; // hidden
|
||||
}
|
||||
// else, fallback to 'none' mode and consider the node visible
|
||||
} else if (displayCheck === 'non-zero-area') {
|
||||
// NOTE: Even though this tests that the node's client rect is non-zero to determine
|
||||
// whether it's displayed, and that a detached node will __always__ have a zero-area
|
||||
// client rect, we don't special-case for whether the node is attached or not. In
|
||||
// this mode, we do want to consider nodes that have a zero area to be hidden at all
|
||||
// times, and that includes attached or not.
|
||||
return isZeroArea(node);
|
||||
}
|
||||
|
||||
// visible, as far as we can tell, or per current `displayCheck=none` mode, we assume
|
||||
// it's visible
|
||||
return false;
|
||||
};
|
||||
|
||||
// form fields (nested) inside a disabled fieldset are not focusable/tabbable
|
||||
// unless they are in the _first_ <legend> element of the top-most disabled
|
||||
// fieldset
|
||||
const isDisabledFromFieldset = function (node) {
|
||||
if (/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(node.tagName)) {
|
||||
let parentNode = node.parentElement;
|
||||
// check if `node` is contained in a disabled <fieldset>
|
||||
while (parentNode) {
|
||||
if (parentNode.tagName === 'FIELDSET' && parentNode.disabled) {
|
||||
// look for the first <legend> among the children of the disabled <fieldset>
|
||||
for (let i = 0; i < parentNode.children.length; i++) {
|
||||
const child = parentNode.children.item(i);
|
||||
// when the first <legend> (in document order) is found
|
||||
if (child.tagName === 'LEGEND') {
|
||||
// if its parent <fieldset> is not nested in another disabled <fieldset>,
|
||||
// return whether `node` is a descendant of its first <legend>
|
||||
return matches.call(parentNode, 'fieldset[disabled] *')
|
||||
? true
|
||||
: !child.contains(node);
|
||||
}
|
||||
}
|
||||
// the disabled <fieldset> containing `node` has no <legend>
|
||||
return true;
|
||||
}
|
||||
parentNode = parentNode.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// else, node's tabbable/focusable state should not be affected by a fieldset's
|
||||
// enabled/disabled state
|
||||
return false;
|
||||
};
|
||||
|
||||
const isNodeMatchingSelectorFocusable = function (options, node) {
|
||||
if (
|
||||
node.disabled ||
|
||||
// we must do an inert look up to filter out any elements inside an inert ancestor
|
||||
// because we're limited in the type of selectors we can use in JSDom (see related
|
||||
// note related to `candidateSelectors`)
|
||||
isInert(node) ||
|
||||
isHiddenInput(node) ||
|
||||
isHidden(node, options) ||
|
||||
// For a details element with a summary, the summary element gets the focus
|
||||
isDetailsWithSummary(node) ||
|
||||
isDisabledFromFieldset(node)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isNodeMatchingSelectorTabbable = function (options, node) {
|
||||
if (
|
||||
isNonTabbableRadio(node) ||
|
||||
getTabIndex(node) < 0 ||
|
||||
!isNodeMatchingSelectorFocusable(options, node)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isValidShadowRootTabbable = function (shadowHostNode) {
|
||||
const tabIndex = parseInt(shadowHostNode.getAttribute('tabindex'), 10);
|
||||
if (isNaN(tabIndex) || tabIndex >= 0) {
|
||||
return true;
|
||||
}
|
||||
// If a custom element has an explicit negative tabindex,
|
||||
// browsers will not allow tab targeting said element's children.
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array.<Element|CandidateScope>} candidates
|
||||
* @returns Element[]
|
||||
*/
|
||||
const sortByOrder = function (candidates) {
|
||||
const regularTabbables = [];
|
||||
const orderedTabbables = [];
|
||||
candidates.forEach(function (item, i) {
|
||||
const isScope = !!item.scopeParent;
|
||||
const element = isScope ? item.scopeParent : item;
|
||||
const candidateTabindex = getSortOrderTabIndex(element, isScope);
|
||||
const elements = isScope ? sortByOrder(item.candidates) : element;
|
||||
if (candidateTabindex === 0) {
|
||||
isScope
|
||||
? regularTabbables.push(...elements)
|
||||
: regularTabbables.push(element);
|
||||
} else {
|
||||
orderedTabbables.push({
|
||||
documentOrder: i,
|
||||
tabIndex: candidateTabindex,
|
||||
item: item,
|
||||
isScope: isScope,
|
||||
content: elements,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return orderedTabbables
|
||||
.sort(sortOrderedTabbables)
|
||||
.reduce((acc, sortable) => {
|
||||
sortable.isScope
|
||||
? acc.push(...sortable.content)
|
||||
: acc.push(sortable.content);
|
||||
return acc;
|
||||
}, [])
|
||||
.concat(regularTabbables);
|
||||
};
|
||||
|
||||
const tabbable = function (container, options) {
|
||||
options = options || {};
|
||||
|
||||
let candidates;
|
||||
if (options.getShadowRoot) {
|
||||
candidates = getCandidatesIteratively(
|
||||
[container],
|
||||
options.includeContainer,
|
||||
{
|
||||
filter: isNodeMatchingSelectorTabbable.bind(null, options),
|
||||
flatten: false,
|
||||
getShadowRoot: options.getShadowRoot,
|
||||
shadowRootFilter: isValidShadowRootTabbable,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
candidates = getCandidates(
|
||||
container,
|
||||
options.includeContainer,
|
||||
isNodeMatchingSelectorTabbable.bind(null, options)
|
||||
);
|
||||
}
|
||||
return sortByOrder(candidates);
|
||||
};
|
||||
|
||||
const focusable = function (container, options) {
|
||||
options = options || {};
|
||||
|
||||
let candidates;
|
||||
if (options.getShadowRoot) {
|
||||
candidates = getCandidatesIteratively(
|
||||
[container],
|
||||
options.includeContainer,
|
||||
{
|
||||
filter: isNodeMatchingSelectorFocusable.bind(null, options),
|
||||
flatten: true,
|
||||
getShadowRoot: options.getShadowRoot,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
candidates = getCandidates(
|
||||
container,
|
||||
options.includeContainer,
|
||||
isNodeMatchingSelectorFocusable.bind(null, options)
|
||||
);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
};
|
||||
|
||||
const isTabbable = function (node, options) {
|
||||
options = options || {};
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (matches.call(node, candidateSelector) === false) {
|
||||
return false;
|
||||
}
|
||||
return isNodeMatchingSelectorTabbable(options, node);
|
||||
};
|
||||
|
||||
const focusableCandidateSelector = /* #__PURE__ */ candidateSelectors
|
||||
.concat('iframe')
|
||||
.join(',');
|
||||
|
||||
const isFocusable = function (node, options) {
|
||||
options = options || {};
|
||||
if (!node) {
|
||||
throw new Error('No node provided');
|
||||
}
|
||||
if (matches.call(node, focusableCandidateSelector) === false) {
|
||||
return false;
|
||||
}
|
||||
return isNodeMatchingSelectorFocusable(options, node);
|
||||
};
|
||||
|
||||
export { tabbable, focusable, isTabbable, isFocusable, getTabIndex };
|
||||
Reference in New Issue
Block a user