diff --git a/.all-contributorsrc b/.all-contributorsrc
index f4c07fe9..42d4b596 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -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,
diff --git a/.changeset/violet-mugs-whisper.md b/.changeset/violet-mugs-whisper.md
new file mode 100644
index 00000000..67f5632a
--- /dev/null
+++ b/.changeset/violet-mugs-whisper.md
@@ -0,0 +1,5 @@
+---
+'tabbable': patch
+---
+
+fix: align with browser behaviour when a web component has a negative tabindex
diff --git a/README.md b/README.md
index 0ad04695..a26ba15c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# tabbable [](https://github.com/focus-trap/tabbable/actions?query=workflow:CI+branch:master) [](https://codecov.io/gh/focus-trap/tabbable) [](./LICENSE)
-[](#contributors)
+[](#contributors)
Small utility that returns an array of all\* tabbable DOM nodes within a containing node.
@@ -238,6 +238,7 @@ In alphabetical order:
 Richard VΕ‘ianskΓ½ π |
 Stefan Cameron π» π π β οΈ π π§ |
 Tyler Hawkins π§ β οΈ π π |
+  bfrost π |
 pebble2050 π |
diff --git a/src/index.js b/src/index.js
index ab2cc1ea..1123dd81 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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
@@ -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;
*/
/**
@@ -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
@@ -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.} candidates
* @returns Element[]
@@ -462,6 +482,7 @@ const tabbable = function (el, options) {
filter: isNodeMatchingSelectorTabbable.bind(null, options),
flatten: false,
getShadowRoot: options.getShadowRoot,
+ shadowRootFilter: isValidShadowRootTabbable,
});
} else {
candidates = getCandidates(
diff --git a/test/e2e/shadow-dom.e2e.js b/test/e2e/shadow-dom.e2e.js
index b74c84b9..111681be 100644
--- a/test/e2e/shadow-dom.e2e.js
+++ b/test/e2e/shadow-dom.e2e.js
@@ -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',
diff --git a/test/fixtures/index.js b/test/fixtures/index.js
index 9f6c998b..356268db 100644
--- a/test/fixtures/index.js
+++ b/test/fixtures/index.js
@@ -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'
+ ),
};
diff --git a/test/fixtures/shadow-dom-untabbable.html b/test/fixtures/shadow-dom-untabbable.html
new file mode 100644
index 00000000..5d474acf
--- /dev/null
+++ b/test/fixtures/shadow-dom-untabbable.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+