import { fireEvent, render, screen } from "@testing-library/react";
import { useState } from "react";
import { useRovingTabindex } from "./useRovingTabindex";
function Group({ hidden = false }: { hidden?: boolean }) {
const [current, setCurrent] = useState(0);
const { groupProps, itemProps } = useRovingTabindex(`p:${current}`, (key) =>
key.startsWith("p:"),
);
return (
<>
>
);
}
const tabIndexes = () =>
["a", "b", "c"].map((name) =>
screen.getByRole("button", { name }).getAttribute("tabindex"),
);
describe("useRovingTabindex", () => {
it("makes the group ONE tab stop: only the current item has tabindex 0", () => {
render();
expect(tabIndexes()).toEqual(["0", "-1", "-1"]);
// fora do grupo, os botões comuns seguem como estão
expect(screen.getByRole("button", { name: "antes" })).not.toHaveAttribute(
"tabindex",
);
});
it("moves the focus with the arrows (and Home/End), stopping at the ends", () => {
render();
const [a, b, c] = ["a", "b", "c"].map((name) =>
screen.getByRole("button", { name }),
);
a.focus();
fireEvent.keyDown(a, { key: "ArrowDown" });
expect(b).toHaveFocus();
fireEvent.keyDown(b, { key: "ArrowRight" });
expect(c).toHaveFocus();
fireEvent.keyDown(c, { key: "ArrowDown" });
expect(c).toHaveFocus();
fireEvent.keyDown(c, { key: "ArrowUp" });
expect(b).toHaveFocus();
fireEvent.keyDown(b, { key: "ArrowLeft" });
expect(a).toHaveFocus();
fireEvent.keyDown(a, { key: "ArrowUp" });
expect(a).toHaveFocus();
fireEvent.keyDown(a, { key: "End" });
expect(c).toHaveFocus();
fireEvent.keyDown(c, { key: "Home" });
expect(a).toHaveFocus();
});
it("keeps the tab stop where the focus last was, so Shift+Tab and Tab come back to the same item", () => {
render();
const b = screen.getByRole("button", { name: "b" });
screen.getByRole("button", { name: "a" }).focus();
fireEvent.keyDown(screen.getByRole("button", { name: "a" }), {
key: "ArrowDown",
});
expect(b).toHaveFocus();
expect(tabIndexes()).toEqual(["-1", "0", "-1"]);
});
it("follows the current item while nothing has been focused yet", () => {
const { rerender } = render();
fireEvent.click(screen.getByRole("button", { name: "c" }));
rerender();
expect(tabIndexes()).toEqual(["-1", "-1", "0"]);
});
it("leaves Tab, Enter, space and any arrow with a modifier alone (no preventDefault)", () => {
render();
const a = screen.getByRole("button", { name: "a" });
a.focus();
for (const init of [
{ key: "Tab" },
{ key: "Enter" },
{ key: " " },
{ key: "ArrowDown", altKey: true },
{ key: "ArrowDown", ctrlKey: true },
{ key: "ArrowDown", metaKey: true },
{ key: "ArrowDown", shiftKey: true },
])
expect(fireEvent.keyDown(a, init)).toBe(true);
expect(a).toHaveFocus();
});
it("cancels the page scroll of an arrow that moved the focus", () => {
render();
const a = screen.getByRole("button", { name: "a" });
a.focus();
expect(fireEvent.keyDown(a, { key: "ArrowDown" })).toBe(false);
expect(
fireEvent.keyDown(screen.getByRole("button", { name: "b" }), {
key: "Home",
}),
).toBe(false);
});
it("skips items that CSS hides (they cannot take the focus)", () => {
const original = HTMLElement.prototype.checkVisibility;
HTMLElement.prototype.checkVisibility = function check(this: HTMLElement) {
return !this.hidden;
};
try {
render();
const c = screen.getByRole("button", { name: "c" });
c.focus();
fireEvent.keyDown(c, { key: "ArrowDown" });
expect(c).toHaveFocus();
fireEvent.keyDown(c, { key: "End" });
expect(c).toHaveFocus();
} finally {
HTMLElement.prototype.checkVisibility = original;
}
});
it("ignores keys pressed on something that is not one of the items", () => {
render();
const nav = screen.getByRole("navigation", { name: "grupo" });
expect(fireEvent.keyDown(nav, { key: "ArrowDown" })).toBe(true);
});
});