Skip to content

Commit

Permalink
fix: align with browser behaviour when a web component has a tabindex…
Browse files Browse the repository at this point in the history
… of -1 (#681)

A browser will skip tab-targeting an element if it is a child of a web component which
has a tabindex of -1.
  • Loading branch information
benjamin-t-frost authored May 25, 2022
1 parent cf24df4 commit 0210a1c
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 2 deletions.
9 changes: 9 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@
"contributions": [
"bug"
]
},
{
"login": "BFrost",
"name": "bfrost",
"avatar_url": "https://avatars.githubusercontent.com/u/3368761?v=4",
"profile": "https://github.com/BFrost",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,
Expand Down
5 changes: 5 additions & 0 deletions .changeset/violet-mugs-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tabbable': patch
---

fix: align with browser behaviour when a web component has a negative tabindex
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# tabbable [![CI](https://github.com/focus-trap/tabbable/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/focus-trap/tabbable/actions?query=workflow:CI+branch:master) [![Codecov](https://img.shields.io/codecov/c/github/focus-trap/tabbable)](https://codecov.io/gh/focus-trap/tabbable) [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE)

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors)
[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors)
<!-- ALL-CONTRIBUTORS-BADGE:END -->

Small utility that returns an array of all\* tabbable DOM nodes within a containing node.
Expand Down Expand Up @@ -238,6 +238,7 @@ In alphabetical order:
<td align="center"><a href="https://github.com/rvsia"><img src="https://avatars.githubusercontent.com/u/32869456?v=4?s=100" width="100px;" alt=""/><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"><a href="https://stefancameron.com/"><img src="https://avatars3.githubusercontent.com/u/2855350?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Stefan Cameron</b></sub></a><br /><a href="https://github.com/focus-trap/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"><a href="http://tylerhawkins.info/201R/"><img src="https://avatars0.githubusercontent.com/u/13806458?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tyler Hawkins</b></sub></a><br /><a href="#tool-thawkin3" title="Tools">🔧</a> <a href="https://github.com/focus-trap/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"><a href="https://github.com/BFrost"><img src="https://avatars.githubusercontent.com/u/3368761?v=4?s=100" width="100px;" alt=""/><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>
<td align="center"><a href="https://github.com/pebble2050"><img src="https://avatars1.githubusercontent.com/u/47210889?v=4?s=100" width="100px;" alt=""/><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>
</table>
Expand Down
23 changes: 22 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ const getCandidates = function (el, includeContainer, filter) {
* @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} CandidatesScope
* @property {Element} scope contains inner candidates
Expand All @@ -62,6 +68,7 @@ const getCandidates = function (el, includeContainer, filter) {
* 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 CandidatesScope into the returned list
* @property {ShadowRootFilter} shadowRootFilter filter shadow roots;
*/

/**
Expand Down Expand Up @@ -110,7 +117,10 @@ const getCandidatesIteratively = function (
(typeof options.getShadowRoot === 'function' &&
options.getShadowRoot(element));

if (shadowRoot) {
const validShadowRoot =
!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
Expand Down Expand Up @@ -415,6 +425,16 @@ const isNodeMatchingSelectorTabbable = function (options, node) {
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|CandidatesScope>} candidates
* @returns Element[]
Expand Down Expand Up @@ -462,6 +482,7 @@ const tabbable = function (el, options) {
filter: isNodeMatchingSelectorTabbable.bind(null, options),
flatten: false,
getShadowRoot: options.getShadowRoot,
shadowRootFilter: isValidShadowRootTabbable,
});
} else {
candidates = getCandidates(
Expand Down
16 changes: 16 additions & 0 deletions test/e2e/shadow-dom.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,22 @@ describe('web-components', () => {
expect(getIdsFromElementsArray(result), 'using `true`').to.eql(expected);
});

it('should not find elements inside shadow dom that browsers will skip due to -1 tabindex on host', () => {
const expected = [];
const { container } = setupFixture(fixtures['shadow-dom-untabbable'], {
window,
});
const shadowElement = container.querySelector('test-shadow');

let result = tabbable(shadowElement, { getShadowRoot() {} });
expect(getIdsFromElementsArray(result), 'using `() => {}`').to.eql(
expected
);

result = tabbable(shadowElement, { getShadowRoot: true });
expect(getIdsFromElementsArray(result), 'using `true`').to.eql(expected);
});

it('should sort slots inside shadow dom', () => {
const expected = [
'light-before',
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ module.exports = {
path.join(__dirname, 'shadow-dom-query.html'),
'utf8'
),
'shadow-dom-untabbable': fs.readFileSync(
path.join(__dirname, 'shadow-dom-untabbable.html'),
'utf8'
),
};
7 changes: 7 additions & 0 deletions test/fixtures/shadow-dom-untabbable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<test-shadow tabindex="-1">
<template shadowroot="open">
<div id="container">
<input id="input" type="text" />
</div>
</template>
</test-shadow>

0 comments on commit 0210a1c

Please sign in to comment.