diff --git a/__tests__/react/tab.js b/__tests__/react/tab.js
index 55b3e5e8..fcd25279 100644
--- a/__tests__/react/tab.js
+++ b/__tests__/react/tab.js
@@ -157,7 +157,7 @@ describe("userEvent.tab", () => {
expect(link).toHaveFocus();
});
- it("should stay within a focus trab", () => {
+ it("should stay within a focus trap", () => {
const { getAllByTestId, getByTestId } = render(
<>
@@ -219,4 +219,29 @@ describe("userEvent.tab", () => {
// cycle goes back to first element
expect(checkbox2).toHaveFocus();
});
+
+ // prior to node 11, Array.sort was unstable for arrays w/ length > 10.
+ // https://twitter.com/mathias/status/1036626116654637057
+ // for this reason, the tab() function needs to account for this in it's sorting.
+ // for example under node 10 in this test:
+ // > 'abcdefghijklmnopqrstuvwxyz'.split('').sort(() => 0).join('')
+ // will give you 'nacdefghijklmbopqrstuvwxyz'
+ it("should support unstable sorting environments like node 10", () => {
+ const letters = "abcdefghijklmnopqrstuvwxyz";
+
+ const { getByTestId } = render(
+ <>
+ {letters.split("").map(letter => (
+
+ ))}
+ >
+ );
+
+ expect.assertions(26);
+
+ letters.split("").forEach(letter => {
+ userEvent.tab();
+ expect(getByTestId(letter)).toHaveFocus();
+ });
+ });
});
diff --git a/src/index.js b/src/index.js
index 043ef857..ba53d28c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,7 @@
import { fireEvent } from "@testing-library/dom";
function wait(time) {
- return new Promise(function(resolve) {
+ return new Promise(function (resolve) {
setTimeout(() => resolve(), time);
});
}
@@ -232,21 +232,24 @@ const userEvent = {
const focusableElements = focusTrap.querySelectorAll(
"input, button, select, textarea, a[href], [tabindex]"
);
- const list = Array.prototype.filter
- .call(focusableElements, function(item) {
- return item.getAttribute("tabindex") !== "-1";
- })
+ let list = Array.prototype.filter.call(focusableElements, function (item) {
+ return item.getAttribute("tabindex") !== "-1";
+ }).map((el, idx) => ({ el, idx }))
.sort((a, b) => {
- const tabIndexA = a.getAttribute("tabindex");
- const tabIndexB = b.getAttribute("tabindex");
- return tabIndexA < tabIndexB ? -1 : tabIndexA > tabIndexB ? 1 : 0;
- });
- const index = list.indexOf(document.activeElement);
+ const tabIndexA = a.el.getAttribute("tabindex");
+ const tabIndexB = b.el.getAttribute("tabindex");
+
+ const diff = tabIndexA - tabIndexB;
+
+ return diff !== 0 ? diff : a.idx - b.idx;
+ })
+
+ const index = list.findIndex(({ el }) => el === document.activeElement);
let nextIndex = shift ? index - 1 : index + 1;
let defaultIndex = shift ? list.length - 1 : 0;
- const next = list[nextIndex] || list[defaultIndex];
+ const { el: next } = (list[nextIndex] || list[defaultIndex]);
if (next.getAttribute("tabindex") === null) {
next.setAttribute("tabindex", "0"); // jsdom requires tabIndex=0 for an item to become 'document.activeElement' (the browser does not)