Legenda do mergulho até Projetos cobrir a cena; transição de página sem blur e com fundo escuro; foco no conteúdo ao trocar de página; listas com marcador e dois pesos de título; vídeos da SEV e da SC Plus regravados sem quadros brancos; scrub a 12-15 fps (-30%); vitrine com uma parada de Tab e setas, contador no celular e CSS sob demanda; âncoras num módulo só (fragment-nav); FramePulse com setInterval; CSS morto removido.
145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
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 (
|
|
<>
|
|
<button type="button">antes</button>
|
|
<nav aria-label="grupo" {...groupProps}>
|
|
{["a", "b", "c"].map((name, index) => (
|
|
<button
|
|
key={name}
|
|
type="button"
|
|
onClick={() => setCurrent(index)}
|
|
{...itemProps(`p:${index}`)}
|
|
>
|
|
{name}
|
|
</button>
|
|
))}
|
|
{hidden && (
|
|
<button type="button" {...itemProps("p:9")} hidden>
|
|
escondido
|
|
</button>
|
|
)}
|
|
</nav>
|
|
<button type="button">depois</button>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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(<Group />);
|
|
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(<Group />);
|
|
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(<Group />);
|
|
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(<Group />);
|
|
fireEvent.click(screen.getByRole("button", { name: "c" }));
|
|
rerender(<Group />);
|
|
expect(tabIndexes()).toEqual(["-1", "-1", "0"]);
|
|
});
|
|
|
|
it("leaves Tab, Enter, space and any arrow with a modifier alone (no preventDefault)", () => {
|
|
render(<Group />);
|
|
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(<Group />);
|
|
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(<Group hidden />);
|
|
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(<Group />);
|
|
const nav = screen.getByRole("navigation", { name: "grupo" });
|
|
expect(fireEvent.keyDown(nav, { key: "ArrowDown" })).toBe(true);
|
|
});
|
|
});
|