Skip to content

Commit

Permalink
Shadow DOM support (v5.3.0) (#570)
Browse files Browse the repository at this point in the history
* chore(dev): declarative shadow root test fixtures

- new development shadow-root-utils
- refactor setupFixture to render using new utils
- refactor current shadow dom tests to use declarative root
- debug page renders shadow root fixtures correctly

* feat: separate radio light/shadow dom groups

* feat: detect display across shadow boundaries

* feat: scan through shadow boundary

- iterate down dom instead of query when getShadowRoot is provided
- new candidate list/tree format with scoped lists

* chore(types): added getShadowDom to types

* fix: type of getShadowRoot option

* test: add test to locate tabbable host

* feat: slot elements are not focusable/tabbable

* chore: added some jsdocs

* refactor: modernize syntax

- as requested in PR

* Prepare for 5.3.0-beta.0

* 5.3.0-beta.0

* Adjusting code after #604 and comments

* Disable shadow DOM for isFocusable/isTabbable if getShadowRoot not given

This goes along with disabling it for `tabbable()` and `focusable()`
when the option isn't given.

* Clarify getShadowRoot must be set to enable shadow DOM support

* Add support for `getShadowRoot: true`

Note this is the equivalent of `getShadowRoot: () => false` which
simply enables shadow DOM support for all open shadows.

* Prepare for v5.3.0-beta.1

* 5.3.0-beta.1

* Add prepublishOnly script for manual publishing

* fix(index.js) The tabIndex of audio, video and details was left to the default if set to some NaN (#610)

* fix(index.js) The tabIndex of contentEditable elements was assumed to be zero in any case, not only in the case it was not specifically set.

* Simplified and optimized 'getTabIndex'.

* Made better use of short-circuit evaluation in 'isNodeMatchingSelectorTabbable', reducing the chances to call the computationally expensive 'isNodeMatchingSelectorFocusable'.

* (Re)Added 'isScope' parameter to 'getTabIndex'. This parameter wasn't present in the master branch, so I lost it in the rebase process.

* Added tests for a `contenteditable` with negative tab index.

* Fixed bug, now the getTabIndex can return 0 not only when the tabindex is not explicitly set, but also when is set to a value that gives NaN when parsed as integer (which would have been resulted in the default browser tabIndex, as if the tabindex wasn't set at all). Also added test for the case an element has a tab index that can't be turned into an integer.

* Added changeset, added entry in CHANGELOG.md and wrote more tests.

* Be consistent with asterisks

* Sync package.json/yarn.lock with beta-530 base branch

Co-authored-by: Stefan Cameron <[email protected]>

* [#632] Add test for radios in a form (#638)

Can't repro the issue, but might as well keep the test since we
seem to like fieldsets but not forms for some reason.

* Add changeset for shadow root support

Co-authored-by: Ido Rosenthal <[email protected]>
Co-authored-by: DaviDevMod <[email protected]>
  • Loading branch information
3 people authored Apr 20, 2022
1 parent a331ef1 commit 685a906
Show file tree
Hide file tree
Showing 24 changed files with 1,319 additions and 137 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-zebras-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tabbable': patch
---

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.
7 changes: 7 additions & 0 deletions .changeset/shadow-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'tabbable': minor
---

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.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 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
Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,25 @@ Type: `full` | `non-zero-area` | `none` . Default: `full`.

Configures how to check if an element is displayed, see ["Display check"](#display-check) below.

##### getShadowRoot

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.
- `function`:
- `node` will be a descendent of the `rootNode` 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](#display-check) when determining if it's visible, and so tabbable/focusable.
### isTabbable

```js
Expand Down Expand Up @@ -173,8 +192,8 @@ To reliably check if an element is tabbable/focusable, Tabbable defaults to the

The `displayCheck` configuration accepts the following options:

- `full`: (default) Most reliably resemble browser behavior, this option checks that an element is displayed and all of his ancestors are displayed as well (Notice that this doesn't exclude `visibility: hidden` or elements with zero size). This check is by far the slowest option as it might cause layout reflow.
- `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 is much less intensive then the `full` option and better for accessibility as zero-size elements with focusable content are considered a strong accessibility anti-pattern.
- `full`: (default) Most reliably resembling browser behavior, this option checks that an element is displayed and all of his ancestors are displayed as well (notice that this doesn't exclude `visibility: hidden` or elements with zero size). This check is by far the slowest option as it will cause layout reflow.
- `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 be less intensive than the `full` option, and better for accessibility, as zero-size elements with focusable content are considered a strong accessibility anti-pattern.
- `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.

**_Feedback and contributions more than welcome!_**
Expand Down
2 changes: 2 additions & 0 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ module.exports = (on, config) => {
require('@cypress/code-coverage/use-browserify-istanbul')
);
}

// fetch fixtures
on('task', {
getFixtures() {
return require('../../test/fixtures/index');
},
});

// IMPORTANT to return the config object
// with the any changed environment variables
return config;
Expand Down
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ type FocusableElement = HTMLElement | SVGElement;

export type CheckOptions = {
displayCheck?: 'full' | 'non-zero-area' | 'none';
getShadowRoot?: boolean | ((node: FocusableElement) => ShadowRoot | boolean | undefined);
};

export type TabbableOptions = {
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tabbable",
"version": "5.2.1",
"version": "5.3.0-beta.1",
"description": "Returns an array of all tabbable DOM nodes within a containing node.",
"main": "dist/index.js",
"module": "dist/index.esm.js",
Expand Down Expand Up @@ -31,10 +31,11 @@
"test": "yarn format:check && yarn lint && yarn test:types && yarn test:unit && yarn test:e2e",
"test:types": "tsc index.d.ts",
"test:unit": "jest",
"test:e2e": "cypress run",
"test:e2e": "ELECTRON_ENABLE_LOGGING=1 cypress run",
"test:e2e:dev": "cypress open",
"test:coverage": "cypress run --env coverage=true",
"prepare": "yarn build",
"prepublishOnly": "yarn test && yarn build",
"release": "yarn build && changeset publish"
},
"repository": {
Expand Down
Loading

0 comments on commit 685a906

Please sign in to comment.